use anyhow::{anyhow, bail, Context, Result};
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::OnceLock;

const CONTRACT_YAML: &str = include_str!("../schemas/sdd-contract.yaml");

static CONTRACT: OnceLock<SddContract> = OnceLock::new();

#[derive(Debug, Deserialize)]
pub struct SddContract {
    pub version: u32,
    pub orchestration: OrchestrationContract,
    pub states: ContractStates,
    #[serde(default)]
    pub validations: ValidationCatalog,
    pub artifacts: BTreeMap<String, ArtifactContract>,
    pub stages: Vec<StageContract>,
}

#[derive(Debug, Deserialize)]
pub struct OrchestrationContract {
    pub canonical_command: String,
    #[serde(default)]
    pub legacy_aliases: Vec<String>,
}

#[derive(Debug, Deserialize)]
pub struct ContractStates {
    pub orchestration: Vec<String>,
    pub artifact: Vec<String>,
    pub tui_status: Vec<String>,
}

#[derive(Debug, Default, Deserialize)]
pub struct ValidationCatalog {
    #[serde(default)]
    pub diagrams: DiagramValidation,
    #[serde(default)]
    pub prompts_agent: PromptValidation,
}

#[derive(Debug, Default, Deserialize)]
pub struct DiagramValidation {
    #[serde(default)]
    pub allowed_markers: Vec<String>,
}

#[derive(Debug, Default, Deserialize)]
pub struct PromptValidation {
    #[serde(default)]
    pub task_id_patterns: Vec<String>,
}

#[derive(Debug, Deserialize)]
pub struct ArtifactContract {
    #[serde(default = "default_true")]
    pub gate: bool,
    #[serde(default)]
    pub aliases: Vec<String>,
    #[serde(default)]
    pub required_sections: Vec<String>,
    #[serde(default)]
    pub semantic_validations: Vec<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StagePhase {
    Pre,
    Core,
    Post,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BoardLane {
    Guided,
    Recorded,
}

#[derive(Debug, Deserialize)]
pub struct StageContract {
    pub key: String,
    pub command: String,
    pub label: String,
    pub order: u32,
    pub phase: StagePhase,
    pub filename: String,
    #[serde(default)]
    pub optional: bool,
    #[serde(default)]
    pub guided: bool,
    #[serde(default)]
    pub board_visible: bool,
    pub board_lane: BoardLane,
    #[serde(default)]
    pub requires_human_checkpoint: bool,
    pub initial_state: String,
    pub artifact_type: Option<String>,
    #[serde(default)]
    pub aliases: Vec<String>,
    #[serde(default)]
    pub surfaces: Vec<String>,
    #[serde(default)]
    pub traceability_from: Vec<String>,
}

fn default_true() -> bool {
    true
}

pub fn contract() -> &'static SddContract {
    CONTRACT.get_or_init(|| {
        parse_contract(CONTRACT_YAML).expect("embedded sdd contract must stay valid")
    })
}

pub fn embedded_contract_yaml() -> &'static str {
    CONTRACT_YAML
}

pub fn parse_contract(text: &str) -> Result<SddContract> {
    let contract: SddContract =
        serde_yaml::from_str(text).context("parsing schemas/sdd-contract.yaml")?;
    validate_contract(&contract)?;
    Ok(contract)
}

pub fn validate_contract(contract: &SddContract) -> Result<()> {
    if contract.version == 0 {
        bail!("contract version must be positive");
    }
    if contract.orchestration.canonical_command != "orchestration" {
        bail!("canonical command must be `orchestration`");
    }
    if !contract
        .orchestration
        .legacy_aliases
        .iter()
        .any(|alias| alias == "orchestrator")
    {
        bail!("legacy alias `orchestrator` must be preserved");
    }
    if contract.stages.is_empty() {
        bail!("contract must declare at least one stage");
    }
    if contract.states.orchestration.is_empty()
        || contract.states.artifact.is_empty()
        || contract.states.tui_status.is_empty()
    {
        bail!("contract states must declare orchestration, artifact and tui_status values");
    }

    let artifact_states: BTreeSet<_> = contract
        .states
        .artifact
        .iter()
        .map(String::as_str)
        .collect();
    let mut stage_keys = BTreeSet::new();
    let mut stage_commands = BTreeSet::new();
    let mut stage_filenames = BTreeSet::new();
    let mut stage_orders = BTreeSet::new();
    let mut artifact_aliases = BTreeSet::new();

    for (artifact_name, artifact) in &contract.artifacts {
        if artifact.required_sections.is_empty() {
            bail!("artifact `{artifact_name}` must declare required_sections");
        }
        for alias in &artifact.aliases {
            if !artifact_aliases.insert(alias.as_str()) {
                bail!("duplicate artifact alias `{alias}` in contract");
            }
        }
    }

    for stage in &contract.stages {
        if !stage_keys.insert(stage.key.as_str()) {
            bail!("duplicate stage key `{}` in contract", stage.key);
        }
        if !stage_commands.insert(stage.command.as_str()) {
            bail!("duplicate stage command `{}` in contract", stage.command);
        }
        if !stage_filenames.insert(stage.filename.as_str()) {
            bail!("duplicate stage filename `{}` in contract", stage.filename);
        }
        if !stage_orders.insert(stage.order) {
            bail!("duplicate stage order `{}` in contract", stage.order);
        }
        if stage.surfaces.is_empty() {
            bail!("stage `{}` must declare at least one surface", stage.key);
        }
        if stage.guided && stage.board_lane != BoardLane::Guided {
            bail!(
                "guided stage `{}` must use the guided board lane",
                stage.key
            );
        }
        if stage.requires_human_checkpoint && stage.artifact_type.is_none() {
            bail!(
                "stage `{}` requires a human checkpoint but has no artifact_type",
                stage.key
            );
        }
        if !artifact_states.contains(stage.initial_state.as_str()) {
            bail!(
                "stage `{}` uses unknown initial_state `{}`",
                stage.key,
                stage.initial_state
            );
        }
        if let Some(artifact_type) = &stage.artifact_type {
            if !contract.artifacts.contains_key(artifact_type) {
                bail!(
                    "stage `{}` references unknown artifact_type `{artifact_type}`",
                    stage.key
                );
            }
        }
        for dependency in &stage.traceability_from {
            if !contract
                .stages
                .iter()
                .any(|candidate| &candidate.key == dependency)
            {
                bail!(
                    "stage `{}` references unknown traceability dependency `{dependency}`",
                    stage.key
                );
            }
        }
    }

    Ok(())
}

pub fn stages() -> &'static [StageContract] {
    contract().stages.as_slice()
}

pub fn stage_by_key(key: &str) -> Option<&'static StageContract> {
    stages().iter().find(|stage| stage.key == key)
}

pub fn stage_by_command(command: &str) -> Option<&'static StageContract> {
    stages().iter().find(|stage| stage.command == command)
}

pub fn stage_file(key: &str) -> Option<&'static str> {
    stage_by_key(key).map(|stage| stage.filename.as_str())
}

pub fn stage_label(key: &str) -> Option<&'static str> {
    stage_by_key(key).map(|stage| stage.label.as_str())
}

pub fn stage_title_from_command(command: &str) -> Option<&'static str> {
    stage_by_command(command).map(|stage| stage.label.as_str())
}

pub fn command_names() -> Vec<&'static str> {
    stages()
        .iter()
        .map(|stage| stage.command.as_str())
        .collect()
}

pub fn guided_stages() -> Vec<&'static StageContract> {
    stages().iter().filter(|stage| stage.guided).collect()
}

pub fn board_stages(lane: BoardLane) -> Vec<&'static StageContract> {
    stages()
        .iter()
        .filter(|stage| stage.board_visible && stage.board_lane == lane)
        .collect()
}

pub fn artifact_spec(name: &str) -> Option<&'static ArtifactContract> {
    contract().artifacts.get(name)
}

pub fn artifact_required_section_map() -> BTreeMap<&'static str, Vec<&'static str>> {
    contract()
        .artifacts
        .iter()
        .map(|(name, artifact)| {
            (
                name.as_str(),
                artifact
                    .required_sections
                    .iter()
                    .map(String::as_str)
                    .collect::<Vec<_>>(),
            )
        })
        .collect()
}

pub fn artifact_semantic_validations(name: &str) -> Vec<&'static str> {
    artifact_spec(name)
        .map(|artifact| {
            artifact
                .semantic_validations
                .iter()
                .map(String::as_str)
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

pub fn artifact_allowed_diagram_markers() -> Vec<&'static str> {
    contract()
        .validations
        .diagrams
        .allowed_markers
        .iter()
        .map(String::as_str)
        .collect()
}

pub fn artifact_prompt_id_patterns() -> Vec<&'static str> {
    contract()
        .validations
        .prompts_agent
        .task_id_patterns
        .iter()
        .map(String::as_str)
        .collect()
}

pub fn stage_traceability_from(key: &str) -> Vec<&'static StageContract> {
    stage_by_key(key)
        .map(|stage| {
            stage
                .traceability_from
                .iter()
                .filter_map(|dependency| stage_by_key(dependency))
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

pub fn canonical_artifact_for_stem(stem: &str) -> Option<Option<&'static str>> {
    stage_by_filename_stem(stem).map(|stage| {
        stage.artifact_type.as_deref().and_then(|artifact_type| {
            artifact_spec(artifact_type).and_then(|artifact| artifact.gate.then_some(artifact_type))
        })
    })
}

pub fn stage_by_filename_stem(stem: &str) -> Option<&'static StageContract> {
    stages()
        .iter()
        .find(|stage| stage.filename.strip_suffix(".md") == Some(stem))
}

pub fn inference_hints() -> Vec<(&'static str, Vec<&'static str>)> {
    let mut hints = Vec::new();
    for stage in stages() {
        let Some(artifact_type) = stage.artifact_type.as_deref() else {
            continue;
        };
        let Some(artifact) = artifact_spec(artifact_type) else {
            continue;
        };
        if !artifact.gate {
            continue;
        }
        let mut aliases = Vec::new();
        aliases.extend(artifact.aliases.iter().map(String::as_str));
        aliases.extend(stage.aliases.iter().map(String::as_str));
        aliases.push(stage.key.as_str());
        if stage.command != stage.key {
            aliases.push(stage.command.as_str());
        }
        aliases.sort_unstable();
        aliases.dedup();
        hints.push((artifact_type, aliases));
    }
    hints
}

pub fn template_stage_pairs() -> Vec<(String, String)> {
    stages()
        .iter()
        .map(|stage| (stage.key.clone(), stage.filename.clone()))
        .collect()
}

pub fn parse_template_stage_pairs(yaml: &str) -> Result<Vec<(String, String)>> {
    let value: serde_yaml::Value = serde_yaml::from_str(yaml).context("parsing template yaml")?;
    let artifacts = value
        .get("artifacts")
        .and_then(serde_yaml::Value::as_mapping)
        .ok_or_else(|| anyhow!("template yaml missing `artifacts` mapping"))?;
    let mut pairs = Vec::new();
    for stage in stages() {
        let key = serde_yaml::Value::String(stage.key.clone());
        let file = artifacts
            .get(&key)
            .and_then(|item| item.get("file"))
            .and_then(serde_yaml::Value::as_str)
            .ok_or_else(|| anyhow!("template yaml missing file for stage `{}`", stage.key))?;
        pairs.push((stage.key.clone(), file.to_string()));
    }
    Ok(pairs)
}

pub fn parse_artifact_sections_json(text: &str) -> Result<BTreeMap<String, Vec<String>>> {
    let value: serde_json::Value =
        serde_json::from_str(text).context("parsing artifact sections schema json")?;
    let object = value
        .as_object()
        .ok_or_else(|| anyhow!("artifact sections schema must be a JSON object"))?;
    let mut out = BTreeMap::new();
    for (artifact_name, artifact_value) in object {
        let required = artifact_value
            .get("required_sections")
            .and_then(serde_json::Value::as_array)
            .ok_or_else(|| {
                anyhow!("artifact sections schema missing required_sections for `{artifact_name}`")
            })?;
        out.insert(
            artifact_name.clone(),
            required
                .iter()
                .filter_map(serde_json::Value::as_str)
                .map(ToString::to_string)
                .collect(),
        );
    }
    Ok(out)
}

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

    #[test]
    fn embedded_contract_is_valid() {
        let parsed = parse_contract(CONTRACT_YAML).unwrap();
        assert_eq!(parsed.orchestration.canonical_command, "orchestration");
        assert!(parsed
            .stages
            .iter()
            .any(|stage| stage.key == "project-discovery"));
        assert!(parsed
            .stages
            .iter()
            .any(|stage| stage.key == "adr" && stage.phase == StagePhase::Post));
    }

    #[test]
    fn adr_is_a_recorded_post_execution_lane() {
        let adr = stage_by_key("adr").unwrap();
        assert!(!adr.guided);
        assert!(adr.board_visible);
        assert_eq!(adr.board_lane, BoardLane::Recorded);
        assert_eq!(adr.filename, "06-adr.md");
    }

    #[test]
    fn risk_is_a_validated_artifact_contract() {
        let risk = artifact_spec("risk").unwrap();
        assert!(risk.gate);
        assert_eq!(
            risk.required_sections,
            [
                "Rastreabilidade",
                "Classificação de risco",
                "Fatores",
                "Checkpoints",
                "Evidências mínimas",
            ]
        );
        assert_eq!(
            canonical_artifact_for_stem("00-risk-classification"),
            Some(Some("risk"))
        );
    }

    #[test]
    fn artifact_sections_json_mirrors_contract() {
        let json = include_str!("../schemas/artifact-sections.json");
        let parsed = parse_artifact_sections_json(json).unwrap();
        let expected = contract()
            .artifacts
            .iter()
            .map(|(name, artifact)| (name.clone(), artifact.required_sections.clone()))
            .collect::<BTreeMap<_, _>>();
        assert_eq!(parsed, expected);
    }

    #[test]
    fn creation_templates_mirror_contract_stage_files() {
        let expected = template_stage_pairs();
        for template in [
            include_str!("../templates/traceability-map.yaml"),
            include_str!("../templates/artifact-index.yaml"),
        ] {
            assert_eq!(parse_template_stage_pairs(template).unwrap(), expected);
        }
    }
}
