//! Target parsing and pipeline driver skeleton for labeler conformance checks.

use std::borrow::Cow;
use std::time::Duration;

use miette::Diagnostic;
use thiserror::Error;
use url::Url;

use crate::commands::test::labeler::crypto;
use crate::commands::test::labeler::http::{self, RealHttpTee};
use crate::commands::test::labeler::identity;
use crate::commands::test::labeler::report::{
    CheckResult, CheckStatus, LabelerReport, ReportHeader, Stage,
};
use crate::commands::test::labeler::subscription::{self, RealWebSocketClient};
use crate::common::identity::{Did, DnsResolver, HttpClient};

/// A labeler target: either a resolvable identifier (handle or DID) or a raw endpoint URL.
#[derive(Debug, Clone)]
pub enum LabelerTarget {
    /// A handle or DID that can be resolved.
    Identified {
        /// The handle or DID to resolve.
        identifier: AtIdentifier,
        /// An optional explicit DID override (for cross-checking).
        explicit_did: Option<Did>,
    },
    /// A raw HTTP endpoint, optionally with a DID for identity checks.
    Endpoint {
        /// The endpoint URL.
        url: Url,
        /// An optional DID to cross-check against the endpoint.
        did: Option<Did>,
    },
}

/// An ATProto identifier: a handle or a DID.
#[derive(Debug, Clone)]
pub enum AtIdentifier {
    /// An ATProto handle (e.g., `alice.bsky.social`).
    Handle(String),
    /// A decentralized identifier (e.g., `did:plc:...` or `did:web:...`).
    Did(Did),
}

/// Options for running the labeler pipeline.
pub struct LabelerOptions<'a> {
    /// HTTP client for network requests.
    pub http: &'a dyn HttpClient,
    /// DNS resolver for handle lookups.
    pub dns: &'a dyn DnsResolver,
    /// HTTP tee to use for the labeler endpoint.
    pub http_tee: HttpTee<'a>,
    /// WebSocket client for subscription stage (optional, defaults to RealWebSocketClient).
    pub ws_client: Option<&'a dyn subscription::WebSocketClient>,
    /// Per-connection time budget for subscription checks.
    pub subscribe_timeout: Duration,
    /// Whether to emit verbose diagnostics.
    pub verbose: bool,
}

/// HTTP tee to use for the labeler endpoint.
pub enum HttpTee<'a> {
    /// Shared reqwest client for both identity and HTTP stages.
    Real(&'a reqwest::Client),
    /// Explicit control (for tests).
    Test(&'a dyn http::RawHttpTee),
}

/// Error from target parsing.
#[derive(Debug, Error, Diagnostic)]
#[error("{message}")]
pub struct TargetParseError {
    /// The error message.
    pub message: String,
}

impl TargetParseError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }

    fn unrecognized_target(raw: &str) -> Self {
        Self::new(format!(
            "Unrecognized target '{raw}'. Expected one of:\n  - ATProto handle (e.g., alice.bsky.social)\n  - DID (e.g., did:plc:abc123 or did:web:example.com)\n  - HTTPS endpoint URL (e.g., https://labeler.example.com)"
        ))
    }

    fn http_not_supported(raw: &str) -> Self {
        Self::new(format!(
            "HTTP endpoint '{raw}' is not supported. Please use an HTTPS endpoint instead."
        ))
    }

    fn ambiguous_did(raw: &str, explicit: &str) -> Self {
        Self::new(format!(
            "Ambiguous target specification: target '{raw}' is already a DID, but --did {explicit} was also provided. Please use only one."
        ))
    }
}

/// Check if a string is a valid ATProto handle.
///
/// A valid handle:
/// - Contains at least one dot.
/// - Contains only alphanumeric characters, hyphens, and dots.
/// - Does not start or end with a hyphen or dot.
/// - Has no empty segments (no consecutive dots or leading/trailing dots).
fn is_valid_handle(s: &str) -> bool {
    if !s.contains('.') {
        return false;
    }

    // Check for empty string or leading/trailing special chars.
    if s.is_empty()
        || s.starts_with('-')
        || s.starts_with('.')
        || s.ends_with('-')
        || s.ends_with('.')
    {
        return false;
    }

    // Check all characters are alphanumeric, hyphen, or dot.
    for c in s.chars() {
        if !c.is_ascii_alphanumeric() && c != '-' && c != '.' {
            return false;
        }
    }

    // Check no empty segments (no consecutive dots).
    if s.contains("..") {
        return false;
    }

    true
}

/// Parse a labeler target from a string and optional explicit DID.
///
/// Returns a `LabelerTarget` on success, or a `TargetParseError` on failure.
///
/// Rules:
/// - If `raw` starts with `did:`, parse as a DID. If `explicit_did` is also provided, return an error.
/// - If `raw` starts with `https://`, parse as a URL. `explicit_did` is carried as an optional DID.
/// - If `raw` starts with `http://`, return an error pointing the user to HTTPS.
/// - If `raw` contains a dot and matches handle grammar, treat as a handle. `explicit_did` is carried.
/// - Otherwise, return an unrecognized target error.
pub fn parse_target(
    raw: &str,
    explicit_did: Option<&str>,
) -> Result<LabelerTarget, TargetParseError> {
    // Check for DID.
    if raw.starts_with("did:") {
        if let Some(ed) = explicit_did {
            return Err(TargetParseError::ambiguous_did(raw, ed));
        }
        return Ok(LabelerTarget::Identified {
            identifier: AtIdentifier::Did(Did(raw.to_string())),
            explicit_did: None,
        });
    }

    // Check for HTTPS URL.
    if raw.starts_with("https://") {
        let url = Url::parse(raw)
            .map_err(|e| TargetParseError::new(format!("Invalid URL '{raw}': {e}")))?;
        return Ok(LabelerTarget::Endpoint {
            url,
            did: explicit_did.map(|d| Did(d.to_string())),
        });
    }

    // Check for HTTP URL (reject with helpful message).
    if raw.starts_with("http://") {
        return Err(TargetParseError::http_not_supported(raw));
    }

    // Check for handle.
    if is_valid_handle(raw) {
        return Ok(LabelerTarget::Identified {
            identifier: AtIdentifier::Handle(raw.to_string()),
            explicit_did: explicit_did.map(|d| Did(d.to_string())),
        });
    }

    // Unrecognized target.
    Err(TargetParseError::unrecognized_target(raw))
}

/// Run the full labeler conformance pipeline.
///
/// This is the main driver that orchestrates all validation stages.
pub async fn run_pipeline(target: LabelerTarget, opts: LabelerOptions<'_>) -> LabelerReport {
    // Build initial header from target.
    let header = ReportHeader {
        target: format_target(&target),
        resolved_did: None,
        pds_endpoint: None,
        labeler_endpoint: None,
    };

    let mut report = LabelerReport::new(header);

    // Run the identity stage.
    let identity_output = identity::run(&target, opts.http, opts.dns).await;

    // Populate header from facts if available.
    if let Some(ref facts) = identity_output.facts {
        report.header.resolved_did = Some(facts.did.to_string());
        report.header.pds_endpoint = Some(facts.pds_endpoint.to_string());
        report.header.labeler_endpoint = Some(facts.labeler_endpoint.to_string());
    }

    // Determine if subsequent stages are blocked by identity failures or just not supplied.
    // If all identity results are Skipped with "no DID supplied" reason, then HTTP/Subscription
    // should say they're "not yet implemented" rather than "blocked". Otherwise, they're blocked.
    let is_no_did_supplied = !identity_output.results.is_empty()
        && identity_output.results.iter().all(|r| {
            r.status == CheckStatus::Skipped
                && r.skipped_reason
                    .as_ref()
                    .map(|reason| reason.contains("no DID supplied"))
                    .unwrap_or(false)
        });

    // Record all identity stage results.
    for result in identity_output.results {
        report.record(result);
    }

    // Determine the labeler endpoint for the HTTP stage.
    let labeler_endpoint = if let Some(ref facts) = identity_output.facts {
        Some(facts.labeler_endpoint.clone())
    } else if let LabelerTarget::Endpoint { url, .. } = &target {
        Some(url.clone())
    } else {
        None
    };

    // Run the HTTP stage if we have an endpoint.
    let mut http_facts = None;
    if let Some(endpoint) = &labeler_endpoint {
        let output = match opts.http_tee {
            HttpTee::Test(tee) => {
                // Use the supplied tee (for testing).
                http::run(tee).await
            }
            HttpTee::Real(client) => {
                let http_client = client.clone();
                let real_tee = RealHttpTee::new(http_client, endpoint.clone());
                http::run(&real_tee).await
            }
        };
        for result in output.results {
            report.record(result);
        }
        http_facts = output.facts.clone();
    } else if identity_output.facts.is_none() && !is_no_did_supplied {
        // HTTP stage blocked by identity failures.
        report.record(CheckResult {
            id: "http::not_run",
            stage: Stage::Http,
            status: CheckStatus::Skipped,
            summary: Cow::Borrowed("HTTP stage (not run)"),
            diagnostic: None,
            skipped_reason: Some(Cow::Borrowed("blocked by identity stage failures")),
        });
    } else {
        // HTTP stage not run because no endpoint could be derived.
        report.record(CheckResult {
            id: "http::not_run",
            stage: Stage::Http,
            status: CheckStatus::Skipped,
            summary: Cow::Borrowed("HTTP stage (not run)"),
            diagnostic: None,
            skipped_reason: Some(Cow::Borrowed("identity stage produced no labeler endpoint")),
        });
    }

    // Run the subscription stage if we have an endpoint.
    let mut subscription_facts = None;
    if let Some(endpoint) = &labeler_endpoint {
        let ws: &dyn subscription::WebSocketClient = if let Some(injected_ws) = opts.ws_client {
            injected_ws
        } else {
            &RealWebSocketClient
        };
        let sub_output = subscription::run(endpoint, ws, opts.subscribe_timeout).await;
        for result in sub_output.results {
            report.record(result);
        }
        subscription_facts = sub_output.facts;
    } else if identity_output.facts.is_none() && !is_no_did_supplied {
        // Subscription stage blocked by identity failures.
        report.record(CheckResult {
            id: "subscription::not_run",
            stage: Stage::Subscription,
            status: CheckStatus::Skipped,
            summary: Cow::Borrowed("Subscription stage (not run)"),
            diagnostic: None,
            skipped_reason: Some(Cow::Borrowed("blocked by identity stage failures")),
        });
    } else {
        // Subscription stage not run because no endpoint could be derived.
        report.record(CheckResult {
            id: "subscription::not_run",
            stage: Stage::Subscription,
            status: CheckStatus::Skipped,
            summary: Cow::Borrowed("Subscription stage (not run)"),
            diagnostic: None,
            skipped_reason: Some(Cow::Borrowed("identity stage produced no labeler endpoint")),
        });
    }

    // Run the crypto stage if identity succeeded and at least one of the HTTP
    // or subscription stages produced labels to verify. Labels from both
    // sources are combined so a failure in one does not block verification
    // on the other — the HTTP stage yields JSON-decoded labels via
    // `queryLabels` and the subscription stage yields CBOR-decoded labels
    // from `subscribeLabels`, and both encodings are covered.
    let sub_has_labels = subscription_facts
        .as_ref()
        .map(|f| !f.sample_labels.is_empty())
        .unwrap_or(false);
    if let Some(identity_facts) = &identity_output.facts {
        if http_facts.is_some() || sub_has_labels {
            let mut combined_labels: Vec<atrium_api::com::atproto::label::defs::Label> = Vec::new();
            if let Some(h) = &http_facts {
                combined_labels.extend(h.first_page.iter().cloned());
            }
            if let Some(s) = &subscription_facts {
                combined_labels.extend(s.sample_labels.iter().cloned());
            }
            // Crypto stage uses the same HTTP seam as the rest of the
            // pipeline so tests can inject a fake client for the PLC audit
            // log fetch.
            let crypto_output = crypto::run(identity_facts, &combined_labels, opts.http).await;
            for result in crypto_output.results {
                report.record(result);
            }
        } else {
            report.record(CheckResult {
                id: "crypto::not_run",
                stage: Stage::Crypto,
                status: CheckStatus::Skipped,
                summary: Cow::Borrowed("Crypto stage (not run)"),
                diagnostic: None,
                skipped_reason: Some(Cow::Borrowed(
                    "neither HTTP nor subscription stage produced labels to verify",
                )),
            });
        }
    } else if identity_output.facts.is_none() && !is_no_did_supplied {
        // Crypto stage blocked by identity failures.
        report.record(CheckResult {
            id: "crypto::not_run",
            stage: Stage::Crypto,
            status: CheckStatus::Skipped,
            summary: Cow::Borrowed("Crypto stage (not run)"),
            diagnostic: None,
            skipped_reason: Some(Cow::Borrowed("blocked by upstream stage failures")),
        });
    } else {
        // Crypto stage not run (no identity or no endpoint).
        report.record(CheckResult {
            id: "crypto::not_run",
            stage: Stage::Crypto,
            status: CheckStatus::Skipped,
            summary: Cow::Borrowed("Crypto stage (not run)"),
            diagnostic: None,
            skipped_reason: Some(Cow::Borrowed("identity stage produced no labeler endpoint")),
        });
    }

    report.finish();
    report
}

/// Format a target for display in the report header.
fn format_target(target: &LabelerTarget) -> String {
    match target {
        LabelerTarget::Identified {
            identifier,
            explicit_did,
        } => {
            let id_str = match identifier {
                AtIdentifier::Handle(h) => h.clone(),
                AtIdentifier::Did(d) => d.0.clone(),
            };
            if explicit_did.is_some() {
                format!("{id_str} (with explicit DID)")
            } else {
                id_str
            }
        }
        LabelerTarget::Endpoint { url, did } => {
            if did.is_some() {
                format!("{url} (with explicit DID)")
            } else {
                url.to_string()
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_target_handle() {
        let target = parse_target("alice.bsky.social", None).expect("should parse");
        match target {
            LabelerTarget::Identified {
                identifier,
                explicit_did,
            } => {
                assert!(
                    matches!(identifier, AtIdentifier::Handle(ref h) if h == "alice.bsky.social")
                );
                assert!(explicit_did.is_none());
            }
            _ => panic!("expected Identified variant"),
        }
    }

    #[test]
    fn parse_target_did_plc() {
        let target = parse_target("did:plc:abc123", None).expect("should parse");
        match target {
            LabelerTarget::Identified {
                identifier,
                explicit_did,
            } => {
                assert!(matches!(identifier, AtIdentifier::Did(ref d) if d.0 == "did:plc:abc123"));
                assert!(explicit_did.is_none());
            }
            _ => panic!("expected Identified variant"),
        }
    }

    #[test]
    fn parse_target_did_web() {
        let target = parse_target("did:web:example.com", None).expect("should parse");
        match target {
            LabelerTarget::Identified {
                identifier,
                explicit_did,
            } => {
                assert!(
                    matches!(identifier, AtIdentifier::Did(ref d) if d.0 == "did:web:example.com")
                );
                assert!(explicit_did.is_none());
            }
            _ => panic!("expected Identified variant"),
        }
    }

    #[test]
    fn parse_target_endpoint_https() {
        let target = parse_target("https://example.com/labeler", None).expect("should parse");
        match target {
            LabelerTarget::Endpoint { url, did } => {
                assert_eq!(url.as_str(), "https://example.com/labeler");
                assert!(did.is_none());
            }
            _ => panic!("expected Endpoint variant"),
        }
    }

    #[test]
    fn parse_target_endpoint_with_explicit_did() {
        let target =
            parse_target("https://example.com/labeler", Some("did:plc:xyz")).expect("should parse");
        match target {
            LabelerTarget::Endpoint { url, did } => {
                assert_eq!(url.as_str(), "https://example.com/labeler");
                assert_eq!(did.map(|d| d.0.clone()), Some("did:plc:xyz".to_string()));
            }
            _ => panic!("expected Endpoint variant"),
        }
    }

    #[test]
    fn parse_target_endpoint_http_rejected() {
        let err = parse_target("http://evil.example", None).expect_err("should reject http");
        assert!(err.message.contains("HTTPS"));
    }

    #[test]
    fn parse_target_unrecognised() {
        let err = parse_target("not a handle or did", None).expect_err("should fail");
        assert!(err.message.contains("Unrecognized target"));
    }

    #[test]
    fn parse_target_did_with_conflicting_flag() {
        let err = parse_target("did:plc:abc", Some("did:web:example.com"))
            .expect_err("should reject ambiguous target");
        assert!(err.message.contains("Ambiguous"));
    }
}
