//! Positioning HTTP handlers (ADR-035) — exposes the per-project marketing
//! surface as queryable JSON over HTTP. Each handler loads positioning from
//! disk on demand via [`ontoref_ontology::ontology::Positioning::load`]; the
//! walker is cheap (a handful of small `.ncl` files) and avoids coupling
//! positioning state to `AppState` until a per-project cache is justified.
//!
//! Endpoints:
//!   - `GET  /api/positioning`              — full layer
//!   - `GET  /api/positioning/audience/:id` — single audience
//!   - `GET  /api/positioning/value-prop/:id` — single value-prop
//!   - `POST /api/positioning/audit`        — drift report (evidence_adrs → ADR
//!     status)

use std::path::{Path, PathBuf};

use axum::extract::{Path as PathExt, State};
use axum::http::StatusCode;
use axum::Json;
use ontoref_ontology::ontology::Positioning;
use ontoref_ontology::types::{
    Audience, Competitor, Differentiator, DifusionMechanism, PositioningStatus, Proof, ValueProp,
    ViabilityPath,
};
use serde::Serialize;

use crate::api::AppState;

fn positioning_dir(state: &AppState) -> PathBuf {
    state.project_root.join(".ontoref").join("positioning")
}

fn load_or_500(dir: &Path) -> Result<Positioning, (StatusCode, Json<serde_json::Value>)> {
    Positioning::load(dir).map_err(|e| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(serde_json::json!({
                "error": "positioning load failed",
                "detail": format!("{e:#}"),
            })),
        )
    })
}

#[derive(Serialize)]
pub struct PositioningResponse {
    pub audiences: Vec<Audience>,
    pub value_props: Vec<ValueProp>,
    pub campaigns: Vec<ontoref_ontology::types::Campaign>,
    pub differentiators: Vec<Differentiator>,
    pub competitors: Vec<Competitor>,
    pub mechanisms: Vec<DifusionMechanism>,
    pub proofs: Vec<Proof>,
    pub viability_paths: Vec<ViabilityPath>,
    pub counts: PositioningCounts,
}

#[derive(Serialize)]
pub struct PositioningCounts {
    pub audiences: usize,
    pub value_props: usize,
    pub campaigns: usize,
    pub live_value_props: usize,
    pub differentiators: usize,
    pub competitors: usize,
    pub mechanisms: usize,
    pub proofs: usize,
    pub viability_paths: usize,
    pub realized_viability_paths: usize,
}

/// Full positioning layer: audiences, value-props, campaigns + counts.
///
/// Returns an empty layer (all counts 0) when the project has no
/// `.ontoref/positioning/` directory — adoption is opt-in per ADR-035.
#[ontoref_derive::onto_api(
    method = "GET",
    path = "/api/positioning",
    auth = "none",
    actors = "agent, developer, ci, admin",
    tags = "positioning, catalog"
)]
pub async fn positioning_get(
    State(state): State<AppState>,
) -> Result<Json<PositioningResponse>, (StatusCode, Json<serde_json::Value>)> {
    let p = load_or_500(&positioning_dir(&state))?;
    let live_value_props = p
        .value_props()
        .iter()
        .filter(|v| v.status == PositioningStatus::Live)
        .count();
    // A ViabilityPath is realized only past Draft AND carrying a measured
    // outcome — the one-directional gate (ADR-055), surfaced as a count.
    let realized_viability_paths = p
        .viability_paths()
        .iter()
        .filter(|v| v.status != PositioningStatus::Draft && v.realized_outcome.is_some())
        .count();
    let counts = PositioningCounts {
        audiences: p.audiences().len(),
        value_props: p.value_props().len(),
        campaigns: p.campaigns().len(),
        live_value_props,
        differentiators: p.differentiators().len(),
        competitors: p.competitors().len(),
        mechanisms: p.mechanisms().len(),
        proofs: p.proofs().len(),
        viability_paths: p.viability_paths().len(),
        realized_viability_paths,
    };
    Ok(Json(PositioningResponse {
        audiences: p.audiences().to_vec(),
        value_props: p.value_props().to_vec(),
        campaigns: p.campaigns().to_vec(),
        differentiators: p.differentiators().to_vec(),
        competitors: p.competitors().to_vec(),
        mechanisms: p.mechanisms().to_vec(),
        proofs: p.proofs().to_vec(),
        viability_paths: p.viability_paths().to_vec(),
        counts,
    }))
}

/// Single audience by id
#[ontoref_derive::onto_api(
    method = "GET",
    path = "/api/positioning/audience/{id}",
    auth = "none",
    actors = "agent, developer, ci, admin",
    params = "id:string:required:Audience id (e.g. platform-engineer)",
    tags = "positioning"
)]
pub async fn positioning_audience(
    State(state): State<AppState>,
    PathExt(id): PathExt<String>,
) -> Result<Json<Audience>, (StatusCode, Json<serde_json::Value>)> {
    let p = load_or_500(&positioning_dir(&state))?;
    p.audience_by_id(&id).cloned().map(Json).ok_or_else(|| {
        (
            StatusCode::NOT_FOUND,
            Json(serde_json::json!({ "error": "audience not found", "id": id })),
        )
    })
}

/// Single value-prop by id
#[ontoref_derive::onto_api(
    method = "GET",
    path = "/api/positioning/value-prop/{id}",
    auth = "none",
    actors = "agent, developer, ci, admin",
    params = "id:string:required:Value-prop id (e.g. vp-001-structure-that-remembers)",
    tags = "positioning"
)]
pub async fn positioning_value_prop(
    State(state): State<AppState>,
    PathExt(id): PathExt<String>,
) -> Result<Json<ValueProp>, (StatusCode, Json<serde_json::Value>)> {
    let p = load_or_500(&positioning_dir(&state))?;
    p.value_prop_by_id(&id).cloned().map(Json).ok_or_else(|| {
        (
            StatusCode::NOT_FOUND,
            Json(serde_json::json!({ "error": "value-prop not found", "id": id })),
        )
    })
}

#[derive(Serialize)]
pub struct AuditReport {
    pub checked_value_props: usize,
    pub drift: Vec<DriftEntry>,
}

#[derive(Serialize, Clone)]
pub struct DriftEntry {
    pub value_prop_id: String,
    pub status: PositioningStatus,
    pub adr_id: String,
    pub issue: AdrIssue,
}

#[derive(Serialize, Clone)]
#[serde(tag = "kind")]
pub enum AdrIssue {
    Missing,
    Superseded { by: Option<String> },
    Deprecated,
    Proposed,
}

/// Cross-reference value-props' `evidence_adrs` against the ADRs directory.
///
/// Drift kinds:
///   - `Missing`     — cited ADR file does not exist
///   - `Superseded`  — ADR status is `'Superseded`
///   - `Deprecated`  — ADR status is `'Deprecated`
///   - `Proposed`    — ADR is still `'Proposed` (claim cites uncommitted
///     decision)
///
/// Only Accepted ADRs are clean backing for a Live value-prop. The audit
/// reports drift across ALL value-props regardless of status — `Draft` ones
/// surface upcoming issues before they go Live.
#[ontoref_derive::onto_api(
    method = "POST",
    path = "/api/positioning/audit",
    auth = "none",
    actors = "agent, developer, ci, admin",
    tags = "positioning, audit"
)]
pub async fn positioning_audit(
    State(state): State<AppState>,
) -> Result<Json<AuditReport>, (StatusCode, Json<serde_json::Value>)> {
    let p = load_or_500(&positioning_dir(&state))?;
    let adrs_dir = ontoref_ontology::layout::resolve_section(&state.project_root, "adrs");
    Ok(Json(audit_positioning(&p, &adrs_dir)))
}

/// Run the audit logic against an arbitrary positioning + adrs_dir pair.
///
/// Extracted from [`positioning_audit`] so the MCP surface and any other
/// caller can reuse it without going through axum extractors.
pub(crate) fn audit_positioning(p: &Positioning, adrs_dir: &Path) -> AuditReport {
    let mut drift: Vec<DriftEntry> = Vec::new();
    for vp in p.value_props() {
        for adr_id in &vp.evidence_adrs {
            let adr_path = adrs_dir.join(format!("{adr_id}.ncl"));
            if !adr_path.exists() {
                drift.push(DriftEntry {
                    value_prop_id: vp.id.clone(),
                    status: vp.status.clone(),
                    adr_id: adr_id.clone(),
                    issue: AdrIssue::Missing,
                });
                continue;
            }
            let raw = match std::fs::read_to_string(&adr_path) {
                Ok(s) => s,
                Err(_) => {
                    drift.push(DriftEntry {
                        value_prop_id: vp.id.clone(),
                        status: vp.status.clone(),
                        adr_id: adr_id.clone(),
                        issue: AdrIssue::Missing,
                    });
                    continue;
                }
            };
            if let Some(issue) = classify_adr_status(&raw) {
                drift.push(DriftEntry {
                    value_prop_id: vp.id.clone(),
                    status: vp.status.clone(),
                    adr_id: adr_id.clone(),
                    issue,
                });
            }
        }
    }
    AuditReport {
        checked_value_props: p.value_props().len(),
        drift,
    }
}

/// Classify an ADR's status by string search.
///
/// Avoids running `nickel export` per ADR (would 10× the audit cost). The
/// pattern `status = 'X` is the canonical form in every ADR template since
/// `.ontoref/adrs/_template.ncl`; a future ADR-schema lift-out may make this
/// machine-readable, but file-text classification is sufficient for the audit.
fn classify_adr_status(raw: &str) -> Option<AdrIssue> {
    if raw.contains("status = 'Superseded") || raw.contains("status='Superseded") {
        let by = extract_superseded_by(raw);
        return Some(AdrIssue::Superseded { by });
    }
    if raw.contains("status = 'Deprecated") || raw.contains("status='Deprecated") {
        return Some(AdrIssue::Deprecated);
    }
    if raw.contains("status = 'Proposed") || raw.contains("status='Proposed") {
        return Some(AdrIssue::Proposed);
    }
    None
}

fn extract_superseded_by(raw: &str) -> Option<String> {
    let key = "superseded_by";
    let pos = raw.find(key)?;
    let tail = &raw[pos + key.len()..];
    let quote_start = tail.find('"')?;
    let after = &tail[quote_start + 1..];
    let quote_end = after.find('"')?;
    Some(after[..quote_end].to_string())
}

impl std::fmt::Debug for AdrIssue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AdrIssue::Missing => write!(f, "Missing"),
            AdrIssue::Superseded { by } => write!(f, "Superseded({by:?})"),
            AdrIssue::Deprecated => write!(f, "Deprecated"),
            AdrIssue::Proposed => write!(f, "Proposed"),
        }
    }
}

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

    #[test]
    fn classify_accepted_returns_none() {
        let txt = r#"d.make_adr { id="adr-001", status = 'Accepted, ... }"#;
        assert!(classify_adr_status(txt).is_none());
    }

    #[test]
    fn classify_proposed_returns_proposed() {
        let txt = "status = 'Proposed,";
        assert!(matches!(classify_adr_status(txt), Some(AdrIssue::Proposed)));
    }

    #[test]
    fn classify_deprecated_returns_deprecated() {
        let txt = "status='Deprecated,";
        assert!(matches!(
            classify_adr_status(txt),
            Some(AdrIssue::Deprecated)
        ));
    }

    #[test]
    fn classify_superseded_extracts_by_when_present() {
        let txt = r#"status = 'Superseded,
        superseded_by = "adr-099-replacement","#;
        match classify_adr_status(txt) {
            Some(AdrIssue::Superseded { by }) => {
                assert_eq!(by, Some("adr-099-replacement".to_string()));
            }
            other => panic!("expected Superseded with by, got {other:?}"),
        }
    }

    #[test]
    fn classify_superseded_without_by_returns_none_for_by() {
        let txt = "status = 'Superseded,";
        match classify_adr_status(txt) {
            Some(AdrIssue::Superseded { by }) => assert!(by.is_none()),
            other => panic!("expected Superseded with no by, got {other:?}"),
        }
    }
}
