use std::collections::BTreeSet;
use std::path::Path;

use crate::state::session as session_state;

use super::behavioral_drift::{collect_session_commits, collect_session_touched_paths};
use super::{CandidateUpdate, HandoffState};

/// Deterministic handoff-refresh candidates from `session_start..HEAD`.
///
/// Returns an empty vector when the feature flag is off, when there is no
/// active session boundary, or when the local git surface does not yield
/// any completion signals. Never mutates the persisted handoff: consumers
/// surface the candidates through `/ccd-radar`, where an operator confirms
/// the rewrite and `ccd handoff write` performs the canonical update.
pub(super) fn build_handoff_refresh_candidates(
    repo_root: &Path,
    handoff: &HandoffState,
    tracked_session: Option<&session_state::SessionStateFile>,
    enabled: bool,
) -> Vec<CandidateUpdate> {
    if !enabled {
        return Vec::new();
    }
    let session_start = match tracked_session {
        Some(session) if session.started_at_epoch_s > 0 => session.started_at_epoch_s,
        _ => return Vec::new(),
    };
    if handoff.immediate_actions.is_empty() && handoff.key_files.is_empty() {
        return Vec::new();
    }

    let commits = match collect_session_commits(repo_root, session_start) {
        Ok(commits) => commits,
        Err(_) => return Vec::new(),
    };
    if commits.is_empty() {
        return Vec::new();
    }

    let mut out = Vec::new();

    for (index, action) in handoff.immediate_actions.iter().enumerate() {
        let tokens = distinctive_tokens(action);
        if tokens.is_empty() {
            continue;
        }
        let mut hits = Vec::new();
        for commit in &commits {
            let subject_lower = commit.subject.to_ascii_lowercase();
            if tokens.iter().any(|token| subject_lower.contains(token)) {
                hits.push(format!("{} ({})", commit.short_hash, commit.subject));
            }
        }
        if !hits.is_empty() {
            out.push(CandidateUpdate {
                kind: "handoff_refresh".to_owned(),
                summary: format!(
                    "Immediate action #{} looks completed by {} commit(s) in `session_start..HEAD`: {}",
                    index + 1,
                    hits.len(),
                    action,
                ),
                section: Some("immediate_actions".to_owned()),
                replacement_lines: Some(hits),
                entry_id: None,
                source_scope: None,
                suggested_destination: None,
                action: None,
            });
        }
    }

    if !handoff.key_files.is_empty() {
        if let Ok(touched) = collect_session_touched_paths(repo_root, session_start) {
            if !touched.is_empty() {
                let matched: Vec<String> = handoff
                    .key_files
                    .iter()
                    .filter(|key_file| key_file_matches_touched(key_file, &touched))
                    .cloned()
                    .collect();
                if !matched.is_empty() {
                    out.push(CandidateUpdate {
                        kind: "handoff_refresh".to_owned(),
                        summary: format!(
                            "{} key-file entry(ies) intersect paths touched in `session_start..HEAD`; review whether their role has shifted.",
                            matched.len(),
                        ),
                        section: Some("key_files".to_owned()),
                        replacement_lines: Some(matched),
                        entry_id: None,
                        source_scope: None,
                        suggested_destination: None,
                        action: None,
                    });
                }
            }
        }
    }

    out
}

/// Conservative token extraction from an immediate-action line.
fn distinctive_tokens(action: &str) -> Vec<String> {
    const STOP: &[&str] = &[
        "the",
        "and",
        "for",
        "with",
        "from",
        "into",
        "onto",
        "that",
        "this",
        "when",
        "then",
        "open",
        "run",
        "add",
        "update",
        "implement",
        "ensure",
        "cover",
        "branch",
        "commit",
        "pull",
        "push",
        "merge",
        "review",
        "write",
        "trace",
        "build",
        "refresh",
        "session",
        "handoff",
        "candidate",
        "config",
        "flag",
        "radar",
        "ccd",
        "main",
        "origin",
        "pr",
    ];
    let lower = action.to_ascii_lowercase();
    let mut tokens: Vec<String> = lower
        .split(|c: char| {
            !(c.is_ascii_alphanumeric() || c == '#' || c == '_' || c == '/' || c == '.' || c == '-')
        })
        .filter(|token| !token.is_empty())
        .filter(|token| {
            if token.starts_with('#')
                || token.contains('#')
                || token.contains('/')
                || token.contains('.')
            {
                return true;
            }
            if token.len() < 5 {
                return false;
            }
            !STOP.contains(token)
        })
        .map(str::to_owned)
        .collect();
    tokens.sort();
    tokens.dedup();
    tokens
}

/// Whether a handoff key-file entry intersects the set of paths the
/// session touched.
fn key_file_matches_touched(key_file: &str, touched: &BTreeSet<String>) -> bool {
    let trimmed = key_file.trim();
    if trimmed.is_empty() {
        return false;
    }
    if touched.contains(trimmed) {
        return true;
    }
    touched
        .iter()
        .any(|path| path == trimmed || trimmed.contains(path.as_str()))
}
