// `POST /api/projects/:id/ai-audit` — original 10-step audit (SSE),
// + `GET /api/projects/:id/audit-status` for the polling UI to resume
// the progress bar after navigating away.

use std::convert::Infallible;

use axum::{
    extract::{Path, State},
    response::sse::{Event, Sse},
    Json,
};
use chrono::Utc;

use crate::agents::runner;
use crate::core::scanner;
use crate::models::*;
use crate::AppState;

use super::helpers::remove_bootstrap_block;
use super::{SseStream, ANALYSIS_STEPS, PROMPT_PREAMBLE};

/// POST /api/projects/:id/ai-audit
/// Runs a 10-step AI audit, streaming progress via SSE.
pub async fn run_audit(
    State(state): State<AppState>,
    Path(id): Path<String>,
    Json(req): Json<LaunchAuditRequest>,
) -> Sse<SseStream> {
    // Look up project
    let project = state.db.with_conn({
        let id = id.clone();
        move |conn| crate::db::projects::get_project(conn, &id)
    }).await.ok().flatten();

    if project.is_none() {
        let stream: SseStream = Box::pin(futures::stream::once(async {
            Ok::<_, Infallible>(
                Event::default().event("error").data("{\"error\":\"Project not found\"}")
            )
        }));
        return Sse::new(stream);
    }

    // Safety: early return above guarantees project is Some
    let project = project.expect("project is Some after early return");
    let project_path_str = project.path.clone();
    let project_path = scanner::resolve_host_path(&project.path);
    let briefing_notes = crate::api::projects::resolve_briefing_notes(&project_path, &project.briefing_notes);
    let linked_repos_block = crate::api::projects::format_linked_repos_for_prompt(&project.linked_repos);
    let pid_for_universe = project.id.clone();
    let kronn_projects_universe_block = match state
        .db
        .with_conn(crate::db::projects::list_projects)
        .await
    {
        Ok(all) => crate::api::projects::format_kronn_projects_universe_for_prompt(&all, &pid_for_universe),
        Err(_) => None,
    };

    // Remove bootstrap prompt before running audit
    let index_file = project_path.join("docs/AGENTS.md");
    if index_file.exists() {
        remove_bootstrap_block(&index_file);
    }

    let tokens = {
        let config = state.config.read().await;
        config.tokens.clone()
    };

    let agent_type = req.agent;
    let total_steps = ANALYSIS_STEPS.len();
    let audit_tracker = state.audit_tracker.clone();
    let project_id_for_progress = id.clone();

    let stream: SseStream = Box::pin(async_stream::try_stream! {
        // Seed live progress so the UI can resume via GET /audit-status after
        // navigating away. Cleared on done / error / cancelled.
        if let Ok(mut t) = audit_tracker.lock() {
            t.start_progress(&project_id_for_progress, total_steps as u32, "full");
        }

        let start = serde_json::json!({ "total_steps": total_steps });
        yield Event::default().event("start").data(start.to_string());

        for (step_num, analysis_step) in ANALYSIS_STEPS.iter().enumerate() {
            let step = step_num + 1;
            let file_label = if analysis_step.target_file == "REVIEW" { "Final review" } else { analysis_step.target_file };

            // Mirror the step_start event into the shared tracker so a
            // polling client catches the same "step 3/10 — repo-map.md".
            if let Ok(mut t) = audit_tracker.lock() {
                t.advance_step(&project_id_for_progress, step as u32, Some(file_label.to_string()));
                t.clear_step_chips(&project_id_for_progress);
            }

            let step_start = serde_json::json!({
                "step": step,
                "total": total_steps,
                "file": file_label
            });
            yield Event::default().event("step_start").data(step_start.to_string());

            // Inject today's date so agents don't have to guess it
            let today = Utc::now().format("%Y-%m-%d").to_string();
            let today_compact = Utc::now().format("%Y%m%d").to_string();
            let mut full_prompt = format!("{}\n\n{}", PROMPT_PREAMBLE, analysis_step.prompt)
                .replace("YYYYMMDD=today", &format!("YYYYMMDD={}", today_compact))
                .replace("today's date (YYYY-MM-DD)", &today)
                .replace("set to today's date", &format!("set to {}", today));

            // Inject user briefing notes if available
            if let Some(ref notes) = briefing_notes {
                full_prompt.push_str(&format!("\n\n## Project briefing (from the user)\n{}\n", notes));
            }
            if let Some(ref block) = linked_repos_block {
                full_prompt.push_str(&format!("\n\n{}\n", block));
            }
            if let Some(ref block) = kronn_projects_universe_block {
                full_prompt.push_str(&format!("\n\n{}\n", block));
            }

            // No profiles for audit — solo agent mode produces clean factual documentation.
            // Multi-profile debate format would pollute docs/ files with discussion artifacts.

            // Always use full_access for audit (agent needs to write files)
            match runner::start_agent_with_config(runner::AgentStartConfig {
                agent_type: &agent_type, project_path: &project_path_str, work_dir: None,
                prompt: &full_prompt, tokens: &tokens, full_access: true,
                skill_ids: &[], directive_ids: &[], profile_ids: &[],
                mcp_context_override: None,
                tier: crate::models::ModelTier::Reasoning, model_tiers: None, context_files_prompt: "",
                discussion_id: None,
            }).await {
                Ok(mut process) => {
                    while let Some(line) = process.next_line().await {
                        let chunk = serde_json::json!({ "text": line, "step": step });
                        yield Event::default().event("chunk").data(chunk.to_string());
                    }

                    let status = process.child.wait().await;
                    process.fix_ownership();
                    tracing::debug!("Audit step {}: fix_ownership applied for {}", step, file_label);
                    let success = status.map(|s| s.success()).unwrap_or(false);

                    let step_done = serde_json::json!({
                        "step": step,
                        "success": success,
                        "file": file_label
                    });
                    yield Event::default().event("step_done").data(step_done.to_string());
                }
                Err(e) => {
                    tracing::error!("Audit step {} failed to start: {}", step, e);
                    let err = serde_json::json!({
                        "error": format!("Step {} ({}): {}", step, file_label, e),
                        "step": step
                    });
                    yield Event::default().event("step_error").data(err.to_string());
                    // Continue to next step (same behavior as CLI)
                }
            }
        }

        // Generate checksums for drift detection
        {
            let pp = project_path.clone();
            let _ = tokio::task::spawn_blocking(move || {
                let mappings: Vec<crate::core::checksums::ChecksumMapping> = ANALYSIS_STEPS.iter()
                    .enumerate()
                    .filter(|(_, s)| !s.sources.is_empty())
                    .map(|(i, s)| {
                        let checksums = crate::core::checksums::compute_step_checksums(&pp, s.sources);
                        crate::core::checksums::ChecksumMapping {
                            ai_file: s.target_file.to_string(),
                            audit_step: i + 1,
                            sources: s.sources.iter().map(|p| p.to_string()).collect(),
                            checksums,
                        }
                    })
                    .collect();
                if let Err(e) = crate::core::checksums::write_checksums_file(&pp, &mappings) {
                    tracing::warn!("Failed to write checksums: {}", e);
                } else {
                    tracing::info!("Wrote docs/checksums.json with {} mappings", mappings.len());
                }
                if let Err(e) = crate::core::kronn_state::record_audit(&pp, "full") {
                    tracing::warn!("Failed to record audit in .kronn.json: {}", e);
                }
            }).await;
        }

        // Audit finished cleanly — drop the progress entry so the UI stops
        // polling. This runs BEFORE yielding `done` so a client racing the
        // next request sees a consistent "no audit running" state.
        if let Ok(mut t) = audit_tracker.lock() {
            t.clear_progress(&project_id_for_progress);
        }

        let done = serde_json::json!({ "status": "complete", "total_steps": total_steps });
        yield Event::default().event("done").data(done.to_string());
    });

    Sse::new(stream)
}

/// GET /api/projects/:id/audit-status
///
/// Returns the current in-flight audit progress for this project, or `None`
/// if no audit is running. The UI polls this endpoint every ~2 s while its
/// `kronn:audit:<projectId>` localStorage entry is set, so the progress bar
/// survives tab/page navigation (the server-side audit process keeps
/// running whether or not an SSE client is attached).
///
/// Progress entries are written by `run_audit`, `partial_audit`, and
/// `full_audit` as they advance, and cleared on done / cancelled / error.
pub async fn audit_status(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Json<ApiResponse<Option<AuditProgress>>> {
    let snapshot = match state.audit_tracker.lock() {
        Ok(t) => t.get_progress(&id),
        Err(_) => return Json(ApiResponse::err("audit tracker lock poisoned")),
    };
    Json(ApiResponse::ok(snapshot))
}

/// 0.8.3 (#288) — list ALL audits currently in progress across every
/// project. Powers the `ActiveAuditsPopover` on the Projets nav button,
/// same UX as `ActiveRunsPopover` for workflows: one badge with the
/// running count, click intercepts navigation to surface the list +
/// per-audit Stop button. Returns an empty Vec when no audit is
/// running (the popover then hides itself; the nav button keeps the
/// normal click-to-navigate behavior).
pub async fn audit_status_all(
    State(state): State<AppState>,
) -> Json<ApiResponse<Vec<AuditProgress>>> {
    let snapshot = match state.audit_tracker.lock() {
        Ok(t) => t.progress.values().cloned().collect::<Vec<_>>(),
        Err(_) => return Json(ApiResponse::err("audit tracker lock poisoned")),
    };
    Json(ApiResponse::ok(snapshot))
}

/// 0.8.4 (#298) — fetch the most-recent **completed** audit run for a
/// project, or `None`. Sister of `audit_latest_resumable` (which only
/// returns Interrupted rows); this one returns Completed rows so the
/// ProjectCard recap panel knows which `audit_run_id` to feed to
/// `audit_run_steps` below.
pub async fn audit_latest(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Json<ApiResponse<Option<crate::models::AuditRun>>> {
    let result = state.db.with_conn(move |conn| {
        crate::db::audit_runs::latest_completed(conn, &id)
    }).await;
    match result {
        Ok(row) => Json(ApiResponse::ok(row)),
        Err(e) => Json(ApiResponse::err(format!("db: {e}"))),
    }
}

/// 0.8.4 (#317 / B1) — admin cleanup: force-mark every `Running`
/// audit_run as Interrupted, regardless of age. Used by the recap-
/// panel "Nettoyer l'historique" button when the operator KNOWS
/// nothing is actually running (just rebuilt docker, mass-killed
/// stuck audits, etc.). Returns the count of rows touched.
///
/// Boot-time reconcile (30-min threshold) is automatic in
/// `Database::open`. This endpoint is the manual escape hatch.
pub async fn audit_runs_cleanup(
    State(state): State<AppState>,
) -> Json<ApiResponse<u64>> {
    let result = state.db.with_conn(|conn| {
        crate::db::audit_runs::reconcile_all_running(conn)
    }).await;
    match result {
        Ok(n) => Json(ApiResponse::ok(n)),
        Err(e) => Json(ApiResponse::err(format!("db: {e}"))),
    }
}

/// 0.8.4 (#298) — history of recent audit runs for a project, newest
/// first. Powers the audit-history chip strip on the ProjectCard recap
/// panel: each chip = one row from `audit_runs`, click switches the
/// per-step table to that run's data. Capped at 20 to avoid heavy
/// renders on projects with hundreds of historical audits.
pub async fn audit_history(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Json<ApiResponse<Vec<crate::models::AuditRun>>> {
    let result = state.db.with_conn(move |conn| {
        crate::db::audit_runs::list_recent(conn, &id, 20)
    }).await;
    match result {
        Ok(runs) => Json(ApiResponse::ok(runs)),
        Err(e) => Json(ApiResponse::err(format!("db: {e}"))),
    }
}

/// 0.8.4 (#298) — list per-step metrics for a finished (or running)
/// audit run. Powers the "▾ Détails du dernier audit" collapsed panel
/// on ProjectCard: one row per step with file label, duration_ms,
/// step_tokens, cumulative_tokens, success/warning. Ordered by
/// step_index ASC so the UI can render the timeline directly.
///
/// Returns an empty Vec for run_ids with no recorded steps yet (which
/// is also the legacy case — runs that completed before 0.8.4 don't
/// have an `audit_run_steps` row).
pub async fn audit_run_steps(
    State(state): State<AppState>,
    Path(run_id): Path<String>,
) -> Json<ApiResponse<Vec<crate::models::AuditRunStep>>> {
    let result = state.db.with_conn(move |conn| {
        crate::db::audit_runs::list_audit_steps(conn, &run_id)
    }).await;
    match result {
        Ok(steps) => Json(ApiResponse::ok(steps)),
        Err(e) => Json(ApiResponse::err(format!("db: {e}"))),
    }
}

/// 0.8.3 (#311) — fetch the most-recent resumable audit run for a project,
/// or `None`. Resumable = `status = 'Interrupted'` AND
/// `last_completed_step` ∈ 1..=9. The frontend uses this to decide
/// whether the "Lancer l'audit" button should become "Reprendre Step N/10"
/// + populate `resume_from` in the next launch request.
pub async fn audit_latest_resumable(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Json<ApiResponse<Option<crate::models::AuditRun>>> {
    let result = state.db.with_conn(move |conn| {
        crate::db::audit_runs::latest_resumable(conn, &id)
    }).await;
    match result {
        Ok(row) => Json(ApiResponse::ok(row)),
        Err(e) => Json(ApiResponse::err(format!("db: {e}"))),
    }
}
