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

use crate::memory::{entries as memory_entries, promote as memory_promote};
use crate::state::session as session_state;

use super::behavioral_drift::{collect_session_commits, collect_session_touched_paths};
use super::{
    active_session_start_count, CandidateAction, CandidateActionApply, CandidateUpdate,
    CandidateUpdates, HandoffState, MemoryState,
};

pub(super) fn build_candidate_updates(
    repo_root: &Path,
    profile: &str,
    handoff: &HandoffState,
    effective_memory: &MemoryState,
    tracked_session: Option<&session_state::SessionStateFile>,
    branch_memory_available: bool,
    handoff_candidate_refresh_enabled: bool,
) -> CandidateUpdates {
    CandidateUpdates {
        handoff: build_handoff_refresh_candidates(
            repo_root,
            handoff,
            tracked_session,
            handoff_candidate_refresh_enabled,
        ),
        memory: build_memory_candidates(
            repo_root,
            profile,
            effective_memory,
            tracked_session,
            branch_memory_available,
        ),
    }
}

/// 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",
                summary: format!(
                    "Immediate action #{} looks completed by {} commit(s) in `session_start..HEAD`: {}",
                    index + 1,
                    hits.len(),
                    action,
                ),
                section: Some("immediate_actions"),
                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",
                        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"),
                        replacement_lines: Some(matched),
                        entry_id: None,
                        source_scope: None,
                        suggested_destination: None,
                        action: None,
                    });
                }
            }
        }
    }

    out
}

/// Conservative token extraction from an immediate-action line.
///
/// Drops common CCD/English stop-words and short tokens that would
/// false-positive against generic commit subjects, keeping identifiers,
/// file paths, and issue tokens (`#596`, `ccd#596`). Matching is
/// lowercase substring — aligns with the "signals not rewrites" discipline
/// in ccd#596: a hit flags the action for operator review, it does not
/// claim the action is definitely done.
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.as_ref())
        })
        .map(str::to_owned)
        .collect();
    tokens.sort();
    tokens.dedup();
    tokens
}

/// Whether a handoff key-file entry intersects the set of paths the
/// session touched. The entry may be a bare repo-root-relative path, a
/// prose line containing one, or a glob-ish hint (e.g. `src/state/radar/*.rs`).
/// Keep the surface narrow: exact match wins; otherwise fall back to
/// substring containment inside a touched path (handles `src/foo.rs` being
/// named in the handoff while the commit touches `src/foo.rs::inner`).
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()))
}

#[derive(Clone, Copy)]
enum MemoryCandidateScope {
    Clone,
    Branch,
}

impl MemoryCandidateScope {
    fn label(self) -> &'static str {
        match self {
            Self::Clone => "workspace",
            Self::Branch => "work_stream",
        }
    }

    fn source_scope(self) -> &'static str {
        match self {
            Self::Clone => "workspace-memory",
            Self::Branch => "work-stream-memory",
        }
    }

    fn suggested_destination(self, branch_memory_available: bool) -> &'static str {
        match self {
            Self::Clone if branch_memory_available => "work-stream-memory",
            Self::Clone | Self::Branch => "project-memory",
        }
    }

    fn promotion_review_summary(self, entry: &memory_entries::StructuredMemoryEntry) -> String {
        match self {
            Self::Clone => format!(
                "{} memory entry `{}` was touched this session; review whether it should stay workspace-local or be promoted to a higher memory scope.",
                self.label(),
                entry.id
            ),
            Self::Branch => format!(
                "{} memory entry `{}` was touched this session; review whether it should move into project memory.",
                self.label(),
                entry.id
            ),
        }
    }
}

fn build_memory_candidates(
    repo_root: &Path,
    profile: &str,
    effective_memory: &MemoryState,
    tracked_session: Option<&session_state::SessionStateFile>,
    branch_memory_available: bool,
) -> Vec<CandidateUpdate> {
    let Some(current_session_start_count) = active_session_start_count(tracked_session) else {
        return Vec::new();
    };

    let branch_entry_ids = effective_memory
        .structured
        .branch_entries
        .iter()
        .map(|entry| entry.id.clone())
        .collect::<BTreeSet<_>>();
    let repo_entry_ids = effective_memory
        .structured
        .repo_entries
        .iter()
        .map(|entry| entry.id.clone())
        .collect::<BTreeSet<_>>();
    let context = MemoryCandidateContext {
        repo_root,
        profile,
        branch_entry_ids: &branch_entry_ids,
        repo_entry_ids: &repo_entry_ids,
        current_session_start_count,
        branch_memory_available,
    };

    let mut memory = Vec::new();
    extend_memory_candidates(
        &mut memory,
        &effective_memory.structured.clone_entries,
        MemoryCandidateScope::Clone,
        &context,
    );
    extend_memory_candidates(
        &mut memory,
        &effective_memory.structured.branch_entries,
        MemoryCandidateScope::Branch,
        &context,
    );
    memory
}

struct MemoryCandidateContext<'a> {
    repo_root: &'a Path,
    profile: &'a str,
    branch_entry_ids: &'a BTreeSet<String>,
    repo_entry_ids: &'a BTreeSet<String>,
    current_session_start_count: u64,
    branch_memory_available: bool,
}

fn extend_memory_candidates(
    candidates: &mut Vec<CandidateUpdate>,
    entries: &[memory_entries::StructuredMemoryEntry],
    scope: MemoryCandidateScope,
    context: &MemoryCandidateContext<'_>,
) {
    candidates.extend(
        entries
            .iter()
            .filter_map(|entry| memory_candidate_from_entry(entry, scope, context)),
    );
}

fn memory_candidate_from_entry(
    entry: &memory_entries::StructuredMemoryEntry,
    scope: MemoryCandidateScope,
    context: &MemoryCandidateContext<'_>,
) -> Option<CandidateUpdate> {
    if entry.state != "active"
        || entry.last_touched_session != context.current_session_start_count
        || entry.source_ref.is_some()
        || !memory_promote::is_promotable_entry_type(&entry.entry_type)
    {
        return None;
    }

    let suggested_destination = scope.suggested_destination(context.branch_memory_available);
    if candidate_already_exists(
        &entry.id,
        suggested_destination,
        context.branch_entry_ids,
        context.repo_entry_ids,
    ) {
        return None;
    }

    Some(CandidateUpdate {
        kind: "promotion_review",
        summary: scope.promotion_review_summary(entry),
        section: None,
        replacement_lines: None,
        entry_id: Some(entry.id.clone()),
        source_scope: Some(scope.source_scope()),
        suggested_destination: Some(suggested_destination),
        action: Some(memory_candidate_admit_preview_action(
            context.repo_root,
            context.profile,
            entry.id.as_str(),
            scope,
            suggested_destination,
        )),
    })
}

fn candidate_already_exists(
    entry_id: &str,
    suggested_destination: &'static str,
    branch_entry_ids: &BTreeSet<String>,
    repo_entry_ids: &BTreeSet<String>,
) -> bool {
    match suggested_destination {
        "work-stream-memory" => {
            branch_entry_ids.contains(entry_id) || repo_entry_ids.contains(entry_id)
        }
        "project-memory" => repo_entry_ids.contains(entry_id),
        _ => false,
    }
}

fn memory_candidate_admit_preview_action(
    repo_root: &Path,
    profile: &str,
    entry_id: &str,
    scope: MemoryCandidateScope,
    suggested_destination: &'static str,
) -> CandidateAction {
    CandidateAction {
        kind: "memory_candidate_admit_preview",
        argv: vec![
            "ccd".to_owned(),
            "memory".to_owned(),
            "candidate".to_owned(),
            "admit".to_owned(),
            "--path".to_owned(),
            repo_root.display().to_string(),
            "--profile".to_owned(),
            profile.to_owned(),
            "--entry".to_owned(),
            entry_id.to_owned(),
            "--source-scope".to_owned(),
            scope.source_scope().to_owned(),
            "--destination".to_owned(),
            suggested_destination.to_owned(),
        ],
        note: "Preview only. Add `--write` to stage this reviewed candidate as a higher-scope `promotion_candidate` entry.",
        apply: Some(memory_candidate_admit_apply_action(
            repo_root,
            profile,
            entry_id,
            scope,
            suggested_destination,
        )),
    }
}

fn memory_candidate_admit_apply_action(
    repo_root: &Path,
    profile: &str,
    entry_id: &str,
    scope: MemoryCandidateScope,
    suggested_destination: &'static str,
) -> CandidateActionApply {
    CandidateActionApply {
        argv: vec![
            "ccd".to_owned(),
            "memory".to_owned(),
            "candidate".to_owned(),
            "admit".to_owned(),
            "--path".to_owned(),
            repo_root.display().to_string(),
            "--profile".to_owned(),
            profile.to_owned(),
            "--entry".to_owned(),
            entry_id.to_owned(),
            "--source-scope".to_owned(),
            scope.source_scope().to_owned(),
            "--destination".to_owned(),
            suggested_destination.to_owned(),
            "--write".to_owned(),
        ],
        requirements: Vec::new(),
        note: "Mutates memory surfaces. Execute this apply command only after reviewing the staged candidate.",
    }
}
