use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::path::Path;

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::state::continuity as runtime_state;
use crate::state::reference_paths::{
    collect_prose_path_candidates, extract_key_files_candidates, token_is_forward_reference,
};

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub(crate) struct HandoffReferenceAudit {
    #[serde(default)]
    pub(crate) missing: Vec<String>,
    #[serde(default)]
    pub(crate) outside: Vec<HandoffReferenceOutside>,
}

impl HandoffReferenceAudit {
    pub(crate) fn is_clean(&self) -> bool {
        self.missing.is_empty() && self.outside.is_empty()
    }
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub(crate) struct HandoffReferenceOutside {
    pub(crate) path: String,
    pub(crate) reason: String,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub(crate) struct HandoffReferenceBaseline {
    pub(crate) handoff_digest: String,
    pub(crate) audit: HandoffReferenceAudit,
}

pub(crate) struct HandoffReferenceAuditResult {
    pub(crate) checked: usize,
    pub(crate) audit: HandoffReferenceAudit,
}

#[derive(Clone, Debug)]
enum HandoffReferenceOutcome {
    Resolved,
    Missing,
    OutsideRepo { reason: &'static str },
}

fn classify_handoff_reference(
    repo_root: &Path,
    canonical_root: &Path,
    candidate: &str,
) -> HandoffReferenceOutcome {
    let path = Path::new(candidate);
    if path.is_absolute() {
        return HandoffReferenceOutcome::OutsideRepo {
            reason: "absolute path",
        };
    }
    if path
        .components()
        .any(|c| matches!(c, std::path::Component::ParentDir))
    {
        return HandoffReferenceOutcome::OutsideRepo {
            reason: "parent-directory traversal escapes the repo root",
        };
    }
    let target = repo_root.join(path);
    if !target.exists() {
        return HandoffReferenceOutcome::Missing;
    }
    let canonical_target = match target.canonicalize() {
        Ok(target) => target,
        Err(_) => return HandoffReferenceOutcome::Missing,
    };
    if canonical_target.starts_with(canonical_root) {
        HandoffReferenceOutcome::Resolved
    } else {
        HandoffReferenceOutcome::OutsideRepo {
            reason: "symlink target escapes the repo root",
        }
    }
}

enum HandoffEntryOutcome {
    Resolved,
    Missing,
    OutsideRepo(&'static str),
}

fn classify_handoff_entry(
    repo_root: &Path,
    canonical_root: &Path,
    candidates: &[String],
) -> HandoffEntryOutcome {
    let mut last_outside: Option<&'static str> = None;
    for candidate in candidates {
        match classify_handoff_reference(repo_root, canonical_root, candidate) {
            HandoffReferenceOutcome::Resolved => return HandoffEntryOutcome::Resolved,
            HandoffReferenceOutcome::Missing => {}
            HandoffReferenceOutcome::OutsideRepo { reason } => last_outside = Some(reason),
        }
    }
    match last_outside {
        Some(reason) => HandoffEntryOutcome::OutsideRepo(reason),
        None => HandoffEntryOutcome::Missing,
    }
}

pub(crate) fn audit_handoff_references(
    repo_root: &Path,
    handoff: &runtime_state::RuntimeHandoffState,
) -> Result<HandoffReferenceAudit> {
    Ok(audit_handoff_references_with_count(repo_root, handoff)?.audit)
}

pub(crate) fn audit_handoff_references_with_count(
    repo_root: &Path,
    handoff: &runtime_state::RuntimeHandoffState,
) -> Result<HandoffReferenceAuditResult> {
    let canonical_root = repo_root
        .canonicalize()
        .with_context(|| format!("could not canonicalize repo root {}", repo_root.display()))?;
    let mut checked = 0usize;
    let mut missing = BTreeSet::new();
    let mut outside = BTreeMap::new();

    for item in handoff.key_files.iter().filter(|i| i.lifecycle.is_active()) {
        let candidates = extract_key_files_candidates(&item.text);
        if candidates.is_empty() {
            continue;
        }
        checked = checked.saturating_add(1);
        match classify_handoff_entry(repo_root, &canonical_root, &candidates) {
            HandoffEntryOutcome::Resolved => {}
            HandoffEntryOutcome::Missing => {
                missing.insert(item.text.clone());
            }
            HandoffEntryOutcome::OutsideRepo(reason) => {
                outside.insert(item.text.clone(), reason.to_owned());
            }
        }
    }

    let mut prose_seen = HashSet::new();
    let prose_fields: [&[runtime_state::RuntimeHandoffItem]; 3] = [
        &handoff.immediate_actions,
        &handoff.operational_guardrails,
        &handoff.definition_of_done,
    ];
    for field in prose_fields {
        for item in field.iter().filter(|i| i.lifecycle.is_active()) {
            let mut tokens = Vec::new();
            collect_prose_path_candidates(&item.text, &mut tokens);
            for token in tokens {
                if !prose_seen.insert(token.clone()) {
                    continue;
                }
                checked = checked.saturating_add(1);
                match classify_handoff_reference(repo_root, &canonical_root, &token) {
                    HandoffReferenceOutcome::Resolved => {}
                    HandoffReferenceOutcome::Missing => {
                        if token_is_forward_reference(&item.text, &token) {
                            continue;
                        }
                        missing.insert(token);
                    }
                    HandoffReferenceOutcome::OutsideRepo { reason } => {
                        outside.insert(token, reason.to_owned());
                    }
                }
            }
        }
    }

    Ok(HandoffReferenceAuditResult {
        checked,
        audit: HandoffReferenceAudit {
            missing: missing.into_iter().collect(),
            outside: outside
                .into_iter()
                .map(|(path, reason)| HandoffReferenceOutside { path, reason })
                .collect(),
        },
    })
}
