use std::sync::Arc;

use mongodb::bson::doc;
use tracing::Instrument;

use compliance_core::models::*;
use compliance_core::traits::issue_tracker::IssueTracker;
use compliance_core::traits::Scanner;
use compliance_core::AgentConfig;

use crate::database::Database;
use crate::error::AgentError;
use crate::llm::LlmClient;
use crate::pipeline::code_review::CodeReviewScanner;
use crate::pipeline::cve::CveScanner;
use crate::pipeline::git::GitOps;
use crate::pipeline::gitleaks::GitleaksScanner;
use crate::pipeline::lint::LintScanner;
use crate::pipeline::patterns::{GdprPatternScanner, OAuthPatternScanner};
use crate::pipeline::sbom::SbomScanner;
use crate::pipeline::semgrep::SemgrepScanner;
use crate::trackers;

/// Enum dispatch for issue trackers (async traits aren't dyn-compatible).
enum TrackerDispatch {
    GitHub(trackers::github::GitHubTracker),
    GitLab(trackers::gitlab::GitLabTracker),
    Gitea(trackers::gitea::GiteaTracker),
    Jira(trackers::jira::JiraTracker),
}

impl TrackerDispatch {
    fn name(&self) -> &str {
        match self {
            Self::GitHub(t) => t.name(),
            Self::GitLab(t) => t.name(),
            Self::Gitea(t) => t.name(),
            Self::Jira(t) => t.name(),
        }
    }

    async fn create_issue(
        &self,
        owner: &str,
        repo: &str,
        title: &str,
        body: &str,
        labels: &[String],
    ) -> Result<TrackerIssue, compliance_core::error::CoreError> {
        match self {
            Self::GitHub(t) => t.create_issue(owner, repo, title, body, labels).await,
            Self::GitLab(t) => t.create_issue(owner, repo, title, body, labels).await,
            Self::Gitea(t) => t.create_issue(owner, repo, title, body, labels).await,
            Self::Jira(t) => t.create_issue(owner, repo, title, body, labels).await,
        }
    }

    async fn find_existing_issue(
        &self,
        owner: &str,
        repo: &str,
        fingerprint: &str,
    ) -> Result<Option<TrackerIssue>, compliance_core::error::CoreError> {
        match self {
            Self::GitHub(t) => t.find_existing_issue(owner, repo, fingerprint).await,
            Self::GitLab(t) => t.find_existing_issue(owner, repo, fingerprint).await,
            Self::Gitea(t) => t.find_existing_issue(owner, repo, fingerprint).await,
            Self::Jira(t) => t.find_existing_issue(owner, repo, fingerprint).await,
        }
    }

    async fn create_pr_review(
        &self,
        owner: &str,
        repo: &str,
        pr_number: u64,
        body: &str,
        comments: Vec<compliance_core::traits::issue_tracker::ReviewComment>,
    ) -> Result<(), compliance_core::error::CoreError> {
        match self {
            Self::GitHub(t) => {
                t.create_pr_review(owner, repo, pr_number, body, comments)
                    .await
            }
            Self::GitLab(t) => {
                t.create_pr_review(owner, repo, pr_number, body, comments)
                    .await
            }
            Self::Gitea(t) => {
                t.create_pr_review(owner, repo, pr_number, body, comments)
                    .await
            }
            Self::Jira(t) => {
                t.create_pr_review(owner, repo, pr_number, body, comments)
                    .await
            }
        }
    }
}

/// Context from graph analysis passed to LLM triage for enhanced filtering
#[derive(Debug)]
#[allow(dead_code)]
pub struct GraphContext {
    pub node_count: u32,
    pub edge_count: u32,
    pub community_count: u32,
    pub impacts: Vec<compliance_core::models::graph::ImpactAnalysis>,
}

pub struct PipelineOrchestrator {
    config: AgentConfig,
    db: Database,
    llm: Arc<LlmClient>,
    http: reqwest::Client,
}

impl PipelineOrchestrator {
    pub fn new(
        config: AgentConfig,
        db: Database,
        llm: Arc<LlmClient>,
        http: reqwest::Client,
    ) -> Self {
        Self {
            config,
            db,
            llm,
            http,
        }
    }

    #[tracing::instrument(skip_all, fields(repo_id = %repo_id, trigger = ?trigger))]
    pub async fn run(&self, repo_id: &str, trigger: ScanTrigger) -> Result<(), AgentError> {
        // Look up the repository
        let repo = self
            .db
            .repositories()
            .find_one(doc! { "_id": mongodb::bson::oid::ObjectId::parse_str(repo_id).map_err(|e| AgentError::Other(e.to_string()))? })
            .await?
            .ok_or_else(|| AgentError::Other(format!("Repository {repo_id} not found")))?;

        // Create scan run
        let scan_run = ScanRun::new(repo_id.to_string(), trigger);
        let insert = self.db.scan_runs().insert_one(&scan_run).await?;
        let scan_run_id = insert
            .inserted_id
            .as_object_id()
            .map(|id| id.to_hex())
            .unwrap_or_default();

        let result = self.run_pipeline(&repo, &scan_run_id).await;

        // Update scan run status
        match &result {
            Ok(count) => {
                self.db
                    .scan_runs()
                    .update_one(
                        doc! { "_id": &insert.inserted_id },
                        doc! {
                            "$set": {
                                "status": "completed",
                                "current_phase": "completed",
                                "new_findings_count": *count as i64,
                                "completed_at": mongodb::bson::DateTime::now(),
                            }
                        },
                    )
                    .await?;
            }
            Err(e) => {
                tracing::error!(repo_id, error = %e, "Scan pipeline failed");
                self.db
                    .scan_runs()
                    .update_one(
                        doc! { "_id": &insert.inserted_id },
                        doc! {
                            "$set": {
                                "status": "failed",
                                "error_message": e.to_string(),
                                "completed_at": mongodb::bson::DateTime::now(),
                            }
                        },
                    )
                    .await?;
            }
        }

        result.map(|_| ())
    }

    #[tracing::instrument(skip_all, fields(repo_id = repo.name.as_str()))]
    async fn run_pipeline(
        &self,
        repo: &TrackedRepository,
        scan_run_id: &str,
    ) -> Result<u32, AgentError> {
        let repo_id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default();

        // Stage 0: Change detection
        tracing::info!("[{repo_id}] Stage 0: Change detection");
        let creds = GitOps::make_repo_credentials(&self.config, repo);
        let git_ops = GitOps::new(&self.config.git_clone_base_path, creds);
        let repo_path = git_ops.clone_or_fetch(&repo.git_url, &repo.name)?;

        if !GitOps::has_new_commits(&repo_path, repo.last_scanned_commit.as_deref())? {
            tracing::info!("[{repo_id}] No new commits, skipping scan");
            return Ok(0);
        }

        let current_sha = GitOps::get_head_sha(&repo_path)?;
        let mut all_findings: Vec<Finding> = Vec::new();

        // Stage 1: Semgrep SAST
        tracing::info!("[{repo_id}] Stage 1: Semgrep SAST");
        self.update_phase(scan_run_id, "sast").await;
        match async {
            let semgrep = SemgrepScanner;
            semgrep.scan(&repo_path, &repo_id).await
        }
        .instrument(tracing::info_span!("stage_sast"))
        .await
        {
            Ok(output) => all_findings.extend(output.findings),
            Err(e) => tracing::warn!("[{repo_id}] Semgrep failed: {e}"),
        }

        // Stage 2: SBOM Generation
        tracing::info!("[{repo_id}] Stage 2: SBOM Generation");
        self.update_phase(scan_run_id, "sbom_generation").await;
        let mut sbom_entries = match async {
            let sbom_scanner = SbomScanner;
            sbom_scanner.scan(&repo_path, &repo_id).await
        }
        .instrument(tracing::info_span!("stage_sbom_generation"))
        .await
        {
            Ok(output) => output.sbom_entries,
            Err(e) => {
                tracing::warn!("[{repo_id}] SBOM generation failed: {e}");
                Vec::new()
            }
        };

        // Stage 3: CVE Scanning
        tracing::info!("[{repo_id}] Stage 3: CVE Scanning");
        self.update_phase(scan_run_id, "cve_scanning").await;
        let cve_scanner = CveScanner::new(
            self.http.clone(),
            self.config.searxng_url.clone(),
            self.config.nvd_api_key.as_ref().map(|k| {
                use secrecy::ExposeSecret;
                k.expose_secret().to_string()
            }),
        );
        let cve_alerts = match async {
            cve_scanner
                .scan_dependencies(&repo_id, &mut sbom_entries)
                .await
        }
        .instrument(tracing::info_span!("stage_cve_scanning"))
        .await
        {
            Ok(alerts) => alerts,
            Err(e) => {
                tracing::warn!("[{repo_id}] CVE scanning failed: {e}");
                Vec::new()
            }
        };

        // Stage 4: Pattern Scanning (GDPR + OAuth)
        tracing::info!("[{repo_id}] Stage 4: Pattern Scanning");
        self.update_phase(scan_run_id, "pattern_scanning").await;
        {
            let pattern_findings = async {
                let mut findings = Vec::new();
                let gdpr = GdprPatternScanner::new();
                match gdpr.scan(&repo_path, &repo_id).await {
                    Ok(output) => findings.extend(output.findings),
                    Err(e) => tracing::warn!("[{repo_id}] GDPR pattern scan failed: {e}"),
                }
                let oauth = OAuthPatternScanner::new();
                match oauth.scan(&repo_path, &repo_id).await {
                    Ok(output) => findings.extend(output.findings),
                    Err(e) => tracing::warn!("[{repo_id}] OAuth pattern scan failed: {e}"),
                }
                findings
            }
            .instrument(tracing::info_span!("stage_pattern_scanning"))
            .await;
            all_findings.extend(pattern_findings);
        }

        // Stage 4a: Secret Detection (Gitleaks)
        tracing::info!("[{repo_id}] Stage 4a: Secret Detection");
        self.update_phase(scan_run_id, "secret_detection").await;
        match async {
            let gitleaks = GitleaksScanner;
            gitleaks.scan(&repo_path, &repo_id).await
        }
        .instrument(tracing::info_span!("stage_secret_detection"))
        .await
        {
            Ok(output) => all_findings.extend(output.findings),
            Err(e) => tracing::warn!("[{repo_id}] Gitleaks failed: {e}"),
        }

        // Stage 4b: Lint Scanning
        tracing::info!("[{repo_id}] Stage 4b: Lint Scanning");
        self.update_phase(scan_run_id, "lint_scanning").await;
        match async {
            let lint = LintScanner;
            lint.scan(&repo_path, &repo_id).await
        }
        .instrument(tracing::info_span!("stage_lint_scanning"))
        .await
        {
            Ok(output) => all_findings.extend(output.findings),
            Err(e) => tracing::warn!("[{repo_id}] Lint scanning failed: {e}"),
        }

        // Stage 4c: LLM Code Review (only on incremental scans)
        if let Some(old_sha) = &repo.last_scanned_commit {
            tracing::info!("[{repo_id}] Stage 4c: LLM Code Review");
            self.update_phase(scan_run_id, "code_review").await;
            let review_output = async {
                let reviewer = CodeReviewScanner::new(self.llm.clone());
                reviewer
                    .review_diff(&repo_path, &repo_id, old_sha, &current_sha)
                    .await
            }
            .instrument(tracing::info_span!("stage_code_review"))
            .await;
            all_findings.extend(review_output.findings);
        }

        // Stage 4.5: Graph Building
        tracing::info!("[{repo_id}] Stage 4.5: Graph Building");
        self.update_phase(scan_run_id, "graph_building").await;
        let graph_context = match async {
            self.build_code_graph(&repo_path, &repo_id, &all_findings)
                .await
        }
        .instrument(tracing::info_span!("stage_graph_building"))
        .await
        {
            Ok(ctx) => Some(ctx),
            Err(e) => {
                tracing::warn!("[{repo_id}] Graph building failed: {e}");
                None
            }
        };

        // Stage 5: LLM Triage (enhanced with graph context)
        tracing::info!(
            "[{repo_id}] Stage 5: LLM Triage ({} findings)",
            all_findings.len()
        );
        self.update_phase(scan_run_id, "llm_triage").await;
        let triaged = crate::llm::triage::triage_findings(
            &self.llm,
            &mut all_findings,
            graph_context.as_ref(),
        )
        .await;
        tracing::info!("[{repo_id}] Triaged: {triaged} findings passed confidence threshold");

        // Dedup against existing findings and insert new ones
        let mut new_count = 0u32;
        let mut new_findings: Vec<Finding> = Vec::new();
        for mut finding in all_findings {
            finding.scan_run_id = Some(scan_run_id.to_string());
            // Check if fingerprint already exists
            let existing = self
                .db
                .findings()
                .find_one(doc! { "fingerprint": &finding.fingerprint })
                .await?;
            if existing.is_none() {
                let result = self.db.findings().insert_one(&finding).await?;
                finding.id = result.inserted_id.as_object_id();
                new_findings.push(finding);
                new_count += 1;
            }
        }

        // Remove stale SBOM entries for this repo before reinserting
        if !sbom_entries.is_empty() {
            self.db
                .sbom_entries()
                .delete_many(doc! { "repo_id": &repo.id })
                .await?;
        }

        // Persist SBOM entries
        for entry in &sbom_entries {
            let filter = doc! {
                "repo_id": &entry.repo_id,
                "name": &entry.name,
                "version": &entry.version,
            };
            let update = mongodb::bson::to_document(entry)
                .map(|d| doc! { "$set": d })
                .unwrap_or_else(|_| doc! {});
            self.db
                .sbom_entries()
                .update_one(filter, update)
                .upsert(true)
                .await?;
        }

        // Persist CVE alerts (upsert by cve_id + repo_id)
        for alert in &cve_alerts {
            let filter = doc! {
                "cve_id": &alert.cve_id,
                "repo_id": &alert.repo_id,
            };
            let update = mongodb::bson::to_document(alert)
                .map(|d| doc! { "$set": d })
                .unwrap_or_else(|_| doc! {});
            self.db
                .cve_alerts()
                .update_one(filter, update)
                .upsert(true)
                .await?;
        }

        // Stage 6: Issue Creation
        tracing::info!("[{repo_id}] Stage 6: Issue Creation");
        self.update_phase(scan_run_id, "issue_creation").await;
        if let Err(e) = self
            .create_tracker_issues(repo, &repo_id, &new_findings)
            .await
        {
            tracing::warn!("[{repo_id}] Issue creation failed: {e}");
        }

        // Stage 7: Update repository
        self.db
            .repositories()
            .update_one(
                doc! { "_id": repo.id },
                doc! {
                    "$set": {
                        "last_scanned_commit": &current_sha,
                        "updated_at": mongodb::bson::DateTime::now(),
                    },
                    "$inc": { "findings_count": new_count as i64 },
                },
            )
            .await?;

        // Stage 8: DAST (async, optional — only if a DastTarget is configured)
        tracing::info!("[{repo_id}] Stage 8: Checking for DAST targets");
        self.update_phase(scan_run_id, "dast_scanning").await;
        self.maybe_trigger_dast(&repo_id, scan_run_id).await;

        tracing::info!("[{repo_id}] Scan complete: {new_count} new findings");
        Ok(new_count)
    }

    /// Build the code knowledge graph for a repo and compute impact analyses
    async fn build_code_graph(
        &self,
        repo_path: &std::path::Path,
        repo_id: &str,
        findings: &[Finding],
    ) -> Result<GraphContext, AgentError> {
        let graph_build_id = uuid::Uuid::new_v4().to_string();
        let engine = compliance_graph::GraphEngine::new(50_000);

        let (mut code_graph, build_run) =
            engine
                .build_graph(repo_path, repo_id, &graph_build_id)
                .map_err(|e| AgentError::Other(format!("Graph build error: {e}")))?;

        // Apply community detection
        compliance_graph::graph::community::apply_communities(&mut code_graph);

        // Store graph in MongoDB
        let store = compliance_graph::graph::persistence::GraphStore::new(self.db.inner());
        store
            .delete_repo_graph(repo_id)
            .await
            .map_err(|e| AgentError::Other(format!("Graph cleanup error: {e}")))?;
        store
            .store_graph(&build_run, &code_graph.nodes, &code_graph.edges)
            .await
            .map_err(|e| AgentError::Other(format!("Graph store error: {e}")))?;

        // Compute impact analysis for each finding
        let analyzer = compliance_graph::GraphEngine::impact_analyzer(&code_graph);
        let mut impacts = Vec::new();

        for finding in findings {
            if let Some(file_path) = &finding.file_path {
                let impact = analyzer.analyze(
                    repo_id,
                    &finding.fingerprint,
                    &graph_build_id,
                    file_path,
                    finding.line_number,
                );
                store
                    .store_impact(&impact)
                    .await
                    .map_err(|e| AgentError::Other(format!("Impact store error: {e}")))?;
                impacts.push(impact);
            }
        }

        Ok(GraphContext {
            node_count: build_run.node_count,
            edge_count: build_run.edge_count,
            community_count: build_run.community_count,
            impacts,
        })
    }

    /// Trigger DAST scan if a target is configured for this repo
    async fn maybe_trigger_dast(&self, repo_id: &str, scan_run_id: &str) {
        use futures_util::TryStreamExt;

        let filter = mongodb::bson::doc! { "repo_id": repo_id };
        let targets: Vec<compliance_core::models::DastTarget> =
            match self.db.dast_targets().find(filter).await {
                Ok(cursor) => cursor.try_collect().await.unwrap_or_default(),
                Err(_) => return,
            };

        if targets.is_empty() {
            tracing::info!("[{repo_id}] No DAST targets configured, skipping");
            return;
        }

        for target in targets {
            let db = self.db.clone();
            let scan_run_id = scan_run_id.to_string();
            tokio::spawn(async move {
                let orchestrator = compliance_dast::DastOrchestrator::new(100);
                match orchestrator.run_scan(&target, Vec::new()).await {
                    Ok((mut scan_run, findings)) => {
                        scan_run.sast_scan_run_id = Some(scan_run_id);
                        if let Err(e) = db.dast_scan_runs().insert_one(&scan_run).await {
                            tracing::error!("Failed to store DAST scan run: {e}");
                        }
                        for finding in &findings {
                            if let Err(e) = db.dast_findings().insert_one(finding).await {
                                tracing::error!("Failed to store DAST finding: {e}");
                            }
                        }
                        tracing::info!("DAST scan complete: {} findings", findings.len());
                    }
                    Err(e) => {
                        tracing::error!("DAST scan failed: {e}");
                    }
                }
            });
        }
    }

    /// Build an issue tracker client from a repository's tracker configuration.
    /// Returns `None` if the repo has no tracker configured.
    fn build_tracker(&self, repo: &TrackedRepository) -> Option<TrackerDispatch> {
        let tracker_type = repo.tracker_type.as_ref()?;
        // Per-repo token takes precedence, fall back to global config
        match tracker_type {
            TrackerType::GitHub => {
                let token = repo.tracker_token.clone().or_else(|| {
                    self.config.github_token.as_ref().map(|t| {
                        use secrecy::ExposeSecret;
                        t.expose_secret().to_string()
                    })
                })?;
                let secret = secrecy::SecretString::from(token);
                match trackers::github::GitHubTracker::new(&secret) {
                    Ok(t) => Some(TrackerDispatch::GitHub(t)),
                    Err(e) => {
                        tracing::warn!("Failed to build GitHub tracker: {e}");
                        None
                    }
                }
            }
            TrackerType::GitLab => {
                let base_url = self
                    .config
                    .gitlab_url
                    .clone()
                    .unwrap_or_else(|| "https://gitlab.com".to_string());
                let token = repo.tracker_token.clone().or_else(|| {
                    self.config.gitlab_token.as_ref().map(|t| {
                        use secrecy::ExposeSecret;
                        t.expose_secret().to_string()
                    })
                })?;
                let secret = secrecy::SecretString::from(token);
                Some(TrackerDispatch::GitLab(
                    trackers::gitlab::GitLabTracker::new(base_url, secret),
                ))
            }
            TrackerType::Gitea => {
                let token = repo.tracker_token.clone()?;
                let base_url = extract_base_url(&repo.git_url)?;
                let secret = secrecy::SecretString::from(token);
                Some(TrackerDispatch::Gitea(trackers::gitea::GiteaTracker::new(
                    base_url, secret,
                )))
            }
            TrackerType::Jira => {
                let base_url = self.config.jira_url.clone()?;
                let email = self.config.jira_email.clone()?;
                let project_key = self.config.jira_project_key.clone()?;
                let token = repo.tracker_token.clone().or_else(|| {
                    self.config.jira_api_token.as_ref().map(|t| {
                        use secrecy::ExposeSecret;
                        t.expose_secret().to_string()
                    })
                })?;
                let secret = secrecy::SecretString::from(token);
                Some(TrackerDispatch::Jira(trackers::jira::JiraTracker::new(
                    base_url,
                    email,
                    secret,
                    project_key,
                )))
            }
        }
    }

    /// Create tracker issues for new findings (severity >= Medium).
    /// Checks for duplicates via fingerprint search before creating.
    #[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
    async fn create_tracker_issues(
        &self,
        repo: &TrackedRepository,
        repo_id: &str,
        new_findings: &[Finding],
    ) -> Result<(), AgentError> {
        let tracker = match self.build_tracker(repo) {
            Some(t) => t,
            None => {
                tracing::info!("[{repo_id}] No issue tracker configured, skipping");
                return Ok(());
            }
        };

        let owner = match repo.tracker_owner.as_deref() {
            Some(o) => o,
            None => {
                tracing::warn!("[{repo_id}] tracker_owner not set, skipping issue creation");
                return Ok(());
            }
        };
        let tracker_repo_name = match repo.tracker_repo.as_deref() {
            Some(r) => r,
            None => {
                tracing::warn!("[{repo_id}] tracker_repo not set, skipping issue creation");
                return Ok(());
            }
        };

        // Only create issues for medium+ severity findings
        let actionable: Vec<&Finding> = new_findings
            .iter()
            .filter(|f| {
                matches!(
                    f.severity,
                    Severity::Medium | Severity::High | Severity::Critical
                )
            })
            .collect();

        if actionable.is_empty() {
            tracing::info!("[{repo_id}] No medium+ findings, skipping issue creation");
            return Ok(());
        }

        tracing::info!(
            "[{repo_id}] Creating issues for {} findings via {}",
            actionable.len(),
            tracker.name()
        );

        let mut created = 0u32;
        for finding in actionable {
            let title = format!(
                "[{}] {}: {}",
                finding.severity, finding.scanner, finding.title
            );

            // Check if an issue already exists by fingerprint first, then by title
            let mut found_existing = false;
            for search_term in [&finding.fingerprint, &title] {
                match tracker
                    .find_existing_issue(owner, tracker_repo_name, search_term)
                    .await
                {
                    Ok(Some(existing)) => {
                        tracing::debug!(
                            "[{repo_id}] Issue already exists for '{}': {}",
                            search_term,
                            existing.external_url
                        );
                        found_existing = true;
                        break;
                    }
                    Ok(None) => {}
                    Err(e) => {
                        tracing::warn!("[{repo_id}] Failed to search for existing issue: {e}");
                    }
                }
            }
            if found_existing {
                continue;
            }
            let body = format_issue_body(finding);
            let labels = vec![
                format!("severity:{}", finding.severity),
                format!("scanner:{}", finding.scanner),
                "compliance-scanner".to_string(),
            ];

            match tracker
                .create_issue(owner, tracker_repo_name, &title, &body, &labels)
                .await
            {
                Ok(mut issue) => {
                    issue.finding_id = finding
                        .id
                        .as_ref()
                        .map(|id| id.to_hex())
                        .unwrap_or_default();

                    // Update the finding with the issue URL
                    if let Some(finding_id) = &finding.id {
                        let _ = self
                            .db
                            .findings()
                            .update_one(
                                doc! { "_id": finding_id },
                                doc! { "$set": { "tracker_issue_url": &issue.external_url } },
                            )
                            .await;
                    }

                    // Store the tracker issue record
                    if let Err(e) = self.db.tracker_issues().insert_one(&issue).await {
                        tracing::warn!("[{repo_id}] Failed to store tracker issue: {e}");
                    }

                    created += 1;
                }
                Err(e) => {
                    tracing::warn!(
                        "[{repo_id}] Failed to create issue for {}: {e}",
                        finding.fingerprint
                    );
                }
            }
        }

        tracing::info!("[{repo_id}] Created {created} tracker issues");
        Ok(())
    }

    /// Run an incremental scan on a PR diff and post review comments.
    #[tracing::instrument(skip_all, fields(repo_id = %repo_id, pr_number))]
    pub async fn run_pr_review(
        &self,
        repo: &TrackedRepository,
        repo_id: &str,
        pr_number: u64,
        base_sha: &str,
        head_sha: &str,
    ) -> Result<(), AgentError> {
        let tracker = match self.build_tracker(repo) {
            Some(t) => t,
            None => {
                tracing::warn!("[{repo_id}] No tracker configured, cannot post PR review");
                return Ok(());
            }
        };
        let owner = repo.tracker_owner.as_deref().unwrap_or("");
        let tracker_repo_name = repo.tracker_repo.as_deref().unwrap_or("");
        if owner.is_empty() || tracker_repo_name.is_empty() {
            tracing::warn!("[{repo_id}] tracker_owner or tracker_repo not set");
            return Ok(());
        }

        // Clone/fetch the repo
        let creds = GitOps::make_repo_credentials(&self.config, repo);
        let git_ops = GitOps::new(&self.config.git_clone_base_path, creds);
        let repo_path = git_ops.clone_or_fetch(&repo.git_url, &repo.name)?;

        // Get diff between base and head
        let diff_files = GitOps::get_diff_content(&repo_path, base_sha, head_sha)?;
        if diff_files.is_empty() {
            tracing::info!("[{repo_id}] PR #{pr_number}: no diff files, skipping review");
            return Ok(());
        }

        // Run semgrep on the full repo but we'll filter findings to changed files
        let changed_paths: std::collections::HashSet<String> =
            diff_files.iter().map(|f| f.path.clone()).collect();

        let mut pr_findings: Vec<Finding> = Vec::new();

        // SAST scan (semgrep)
        match SemgrepScanner.scan(&repo_path, repo_id).await {
            Ok(output) => {
                for f in output.findings {
                    if let Some(fp) = &f.file_path {
                        if changed_paths.contains(fp.as_str()) {
                            pr_findings.push(f);
                        }
                    }
                }
            }
            Err(e) => tracing::warn!("[{repo_id}] PR semgrep failed: {e}"),
        }

        // LLM code review on the diff
        let reviewer = CodeReviewScanner::new(self.llm.clone());
        let review_output = reviewer
            .review_diff(&repo_path, repo_id, base_sha, head_sha)
            .await;
        pr_findings.extend(review_output.findings);

        if pr_findings.is_empty() {
            // Post a clean review
            if let Err(e) = tracker
                .create_pr_review(
                    owner,
                    tracker_repo_name,
                    pr_number,
                    "Compliance scan: no issues found in this PR.",
                    Vec::new(),
                )
                .await
            {
                tracing::warn!("[{repo_id}] Failed to post clean PR review: {e}");
            }
            return Ok(());
        }

        // Build review comments from findings
        let mut review_comments = Vec::new();
        for finding in &pr_findings {
            if let (Some(path), Some(line)) = (&finding.file_path, finding.line_number) {
                let comment_body = format!(
                    "**[{}] {}**\n\n{}\n\n*Scanner: {} | {}*",
                    finding.severity,
                    finding.title,
                    finding.description,
                    finding.scanner,
                    finding
                        .cwe
                        .as_deref()
                        .map(|c| format!("CWE: {c}"))
                        .unwrap_or_default(),
                );
                review_comments.push(compliance_core::traits::issue_tracker::ReviewComment {
                    path: path.clone(),
                    line,
                    body: comment_body,
                });
            }
        }

        let summary = format!(
            "Compliance scan found **{}** issue(s) in this PR:\n\n{}",
            pr_findings.len(),
            pr_findings
                .iter()
                .map(|f| format!("- **[{}]** {}: {}", f.severity, f.scanner, f.title))
                .collect::<Vec<_>>()
                .join("\n"),
        );

        if let Err(e) = tracker
            .create_pr_review(
                owner,
                tracker_repo_name,
                pr_number,
                &summary,
                review_comments,
            )
            .await
        {
            tracing::warn!("[{repo_id}] Failed to post PR review: {e}");
        } else {
            tracing::info!(
                "[{repo_id}] Posted PR review on #{pr_number} with {} findings",
                pr_findings.len()
            );
        }

        Ok(())
    }

    async fn update_phase(&self, scan_run_id: &str, phase: &str) {
        if let Ok(oid) = mongodb::bson::oid::ObjectId::parse_str(scan_run_id) {
            let _ = self
                .db
                .scan_runs()
                .update_one(
                    doc! { "_id": oid },
                    doc! {
                        "$set": { "current_phase": phase },
                        "$push": { "phases_completed": phase },
                    },
                )
                .await;
        }
    }
}

/// Extract the scheme + host from a git URL.
/// e.g. "https://gitea.example.com/owner/repo.git" → "https://gitea.example.com"
/// e.g. "ssh://git@gitea.example.com:22/owner/repo.git" → "https://gitea.example.com"
fn extract_base_url(git_url: &str) -> Option<String> {
    if let Some(rest) = git_url.strip_prefix("https://") {
        let host = rest.split('/').next()?;
        Some(format!("https://{host}"))
    } else if let Some(rest) = git_url.strip_prefix("http://") {
        let host = rest.split('/').next()?;
        Some(format!("http://{host}"))
    } else if let Some(rest) = git_url.strip_prefix("ssh://") {
        // ssh://git@host:port/path → extract host
        let after_at = rest.find('@').map(|i| &rest[i + 1..]).unwrap_or(rest);
        let host = after_at.split(&[':', '/'][..]).next()?;
        Some(format!("https://{host}"))
    } else if let Some(at_pos) = git_url.find('@') {
        // SCP-style: git@host:owner/repo.git
        let after_at = &git_url[at_pos + 1..];
        let host = after_at.split(':').next()?;
        Some(format!("https://{host}"))
    } else {
        None
    }
}

/// Format a finding into a markdown issue body for the tracker.
fn format_issue_body(finding: &Finding) -> String {
    let mut body = String::new();

    body.push_str(&format!("## {} Finding\n\n", finding.severity));
    body.push_str(&format!("**Scanner:** {}\n", finding.scanner));
    body.push_str(&format!("**Severity:** {}\n", finding.severity));

    if let Some(rule) = &finding.rule_id {
        body.push_str(&format!("**Rule:** {}\n", rule));
    }
    if let Some(cwe) = &finding.cwe {
        body.push_str(&format!("**CWE:** {}\n", cwe));
    }

    body.push_str(&format!("\n### Description\n\n{}\n", finding.description));

    if let Some(file_path) = &finding.file_path {
        body.push_str(&format!("\n### Location\n\n**File:** `{}`", file_path));
        if let Some(line) = finding.line_number {
            body.push_str(&format!(" (line {})", line));
        }
        body.push('\n');
    }

    if let Some(snippet) = &finding.code_snippet {
        body.push_str(&format!("\n### Code\n\n```\n{}\n```\n", snippet));
    }

    if let Some(remediation) = &finding.remediation {
        body.push_str(&format!("\n### Remediation\n\n{}\n", remediation));
    }

    if let Some(fix) = &finding.suggested_fix {
        body.push_str(&format!("\n### Suggested Fix\n\n```\n{}\n```\n", fix));
    }

    body.push_str(&format!(
        "\n---\n*Fingerprint:* `{}`\n*Generated by compliance-scanner*",
        finding.fingerprint
    ));

    body
}
