use crate::config::ProjectConfig;
use crate::domain::{
    ContextBundle, ScoredNote, WakeupIdentity, WakeupMemoryItem, WakeupPacket, WakeupProfile,
    WakeupProvenance, WakeupProvenanceSource, WakeupQuery, WakeupRecommendedNote, WakeupSection,
};
use crate::wakeup_policy::WakeupPolicyState;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};

const DEFAULT_DEVELOPER_ROOTS: &[&str] = &["00-Identity", "20-Areas", "30-Workflows"];

fn effective_developer_roots(configured: &[String]) -> Vec<String> {
    if configured.is_empty() {
        DEFAULT_DEVELOPER_ROOTS
            .iter()
            .map(|value| value.to_string())
            .collect()
    } else {
        configured.to_vec()
    }
}

fn build_identity(
    bundle: &ContextBundle,
    project_config: Option<&ProjectConfig>,
    developer_roots: &[String],
    profile: WakeupProfile,
) -> WakeupIdentity {
    WakeupIdentity {
        project_id: bundle
            .route
            .project
            .as_ref()
            .map(|project| project.id.clone()),
        project_name: bundle
            .route
            .project
            .as_ref()
            .map(|project| project.name.clone()),
        repo_paths: project_config
            .map(|project| {
                project
                    .repo_paths
                    .iter()
                    .map(|path| path.display().to_string())
                    .collect()
            })
            .unwrap_or_default(),
        modules: bundle
            .route
            .modules
            .iter()
            .map(|module| module.id.clone())
            .collect(),
        scenes: bundle
            .route
            .scenes
            .iter()
            .map(|scene| scene.id.clone())
            .collect(),
        active_profile: match profile {
            WakeupProfile::Developer => "developer",
            WakeupProfile::Project => "project",
        }
        .to_string(),
        developer_roots: effective_developer_roots(developer_roots),
    }
}

fn apply_selection_basis(
    selection_basis: &mut Vec<String>,
    bundle: &ContextBundle,
    developer_roots: &[String],
    profile: WakeupProfile,
) {
    let headline = match profile {
        WakeupProfile::Project => bundle
            .route
            .project
            .as_ref()
            .map(|project| format!("project matched {}", project.id))
            .unwrap_or_else(|| "project profile active".to_string()),
        WakeupProfile::Developer => format!(
            "developer roots active {}",
            effective_developer_roots(developer_roots).join(", ")
        ),
    };
    selection_basis.insert(0, headline);
}

fn build_priorities(
    constraints: &[WakeupMemoryItem],
    active_context: &[WakeupMemoryItem],
    working_style: &[WakeupMemoryItem],
    _profile: WakeupProfile,
) -> Vec<String> {
    let mut priorities = constraints
        .iter()
        .take(3)
        .map(|item| item.title.clone())
        .chain(active_context.iter().take(2).map(|item| item.title.clone()))
        .collect::<Vec<_>>();

    if priorities.is_empty() {
        priorities.extend(working_style.iter().take(3).map(|item| item.title.clone()));
    }

    priorities
}

const STALENESS_DAYS: u64 = 180;
const STALENESS_EXEMPT_TYPES: &[&str] = &["constraint", "preference"];

fn build_maintenance_hints(lifecycle_root: Option<&Path>) -> Vec<String> {
    let Some(root) = lifecycle_root else {
        return Vec::new();
    };
    let store = crate::lifecycle_store::LifecycleStore::new(root);
    let entries = store.read_all().unwrap_or_default();

    let mut hints = Vec::new();

    // Check pending review backlog
    let pending_count = entries
        .iter()
        .filter(|e| e.record.state == crate::domain::MemoryLifecycleState::Candidate)
        .count();
    if pending_count >= 5 {
        hints.push(format!(
            "{pending_count} 条 AI 提议待审核，建议运行 memory list --view pending-review 查看"
        ));
    }

    // Check staleness
    let ref_map = crate::reference_tracker::read(root);
    if !ref_map.records.is_empty() {
        let active_ids: std::collections::HashSet<&str> = entries
            .iter()
            .filter(|e| {
                matches!(
                    e.record.state,
                    crate::domain::MemoryLifecycleState::Accepted
                        | crate::domain::MemoryLifecycleState::Canonical
                ) && !STALENESS_EXEMPT_TYPES.contains(&e.record.memory_type.as_str())
            })
            .map(|e| e.record_id.as_str())
            .collect();

        let stale_count = ref_map
            .records
            .iter()
            .filter(|(id, entry)| {
                active_ids.contains(id.as_str())
                    && crate::reference_tracker::age_days(entry)
                        .is_some_and(|days| days >= STALENESS_DAYS)
            })
            .count();

        if stale_count > 0 {
            hints.push(format!(
                "{stale_count} 条记忆超过 {STALENESS_DAYS} 天未引用，建议运行 memory lint 审查"
            ));
        }
    }

    hints
}

fn ensure_developer_sections(
    profile: WakeupProfile,
    working_style: &mut Vec<WakeupMemoryItem>,
    active_context: &mut Vec<WakeupMemoryItem>,
    recommended_notes: &[WakeupRecommendedNote],
) {
    if !matches!(profile, WakeupProfile::Developer) {
        return;
    }

    if working_style.is_empty()
        && let Some(note) = recommended_notes.first()
    {
        working_style.push(WakeupMemoryItem {
            title: note.title.clone(),
            summary: note.why_relevant.clone(),
            memory_type: note.memory_type.clone(),
            source: note.path.clone(),
            sensitivity: None,
            source_of_truth: false,
            confidence: note.confidence,
        });
    }

    if active_context.is_empty()
        && let Some(note) = recommended_notes
            .get(1)
            .or_else(|| recommended_notes.first())
    {
        active_context.push(WakeupMemoryItem {
            title: note.title.clone(),
            summary: note.why_relevant.clone(),
            memory_type: note.memory_type.clone(),
            source: note.path.clone(),
            sensitivity: None,
            source_of_truth: false,
            confidence: note.confidence,
        });
    }
}

fn push_limited<T>(items: &mut Vec<T>, item: T, limit: usize) {
    if items.len() < limit {
        items.push(item);
    }
}

fn push_recommended(
    items: &mut Vec<WakeupRecommendedNote>,
    item: WakeupRecommendedNote,
    limit: usize,
) {
    if items.len() >= limit {
        return;
    }
    if items.iter().any(|existing| existing.path == item.path) {
        return;
    }
    items.push(item);
}

fn generated_at_string() -> String {
    let seconds = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .unwrap_or_default();
    format!("unix:{seconds}")
}

pub fn build_packet(
    bundle: &ContextBundle,
    scored_notes: &[ScoredNote],
    project_config: Option<&ProjectConfig>,
    developer_roots: &[String],
    profile: WakeupProfile,
) -> WakeupPacket {
    build_packet_with_index(
        bundle,
        scored_notes,
        project_config,
        developer_roots,
        profile,
        None,
        None,
    )
}

pub fn build_packet_with_index(
    bundle: &ContextBundle,
    scored_notes: &[ScoredNote],
    project_config: Option<&ProjectConfig>,
    developer_roots: &[String],
    profile: WakeupProfile,
    knowledge_index: Option<String>,
    lifecycle_root: Option<&Path>,
) -> WakeupPacket {
    let mut working_style = Vec::new();
    let mut active_context = Vec::new();
    let mut constraints = Vec::new();
    let mut decisions = Vec::new();
    let mut incidents = Vec::new();
    let mut recommended_notes = Vec::new();
    let mut derived_from = Vec::new();
    let mut selection_basis = Vec::new();
    let mut policy_state = WakeupPolicyState::new();

    for scored in scored_notes {
        let note = &scored.note;
        let prepared = policy_state.prepare_item(scored);
        if prepared.suppressed {
            continue;
        }
        let item = prepared.item;
        let memory_type = item.memory_type.clone();
        let source_of_truth = item.source_of_truth;

        if derived_from
            .iter()
            .all(|source: &WakeupProvenanceSource| source.path != note.relative_path)
        {
            derived_from.push(WakeupProvenanceSource {
                path: note.relative_path.clone(),
                source_of_truth,
                memory_type: memory_type.clone(),
            });
        }

        for reason in &scored.reasons {
            if !selection_basis.iter().any(|existing| existing == reason) {
                selection_basis.push(reason.clone());
            }
        }

        match memory_type.as_deref() {
            Some("preference") | Some("workflow") => push_limited(&mut working_style, item, 5),
            Some("project") => push_limited(&mut active_context, item, 5),
            Some("constraint") => push_limited(&mut constraints, item, 5),
            Some("decision") => push_limited(&mut decisions, item, 5),
            Some("incident") => push_limited(&mut incidents, item, 3),
            Some("session") => {}
            _ => {
                push_recommended(
                    &mut recommended_notes,
                    WakeupRecommendedNote {
                        path: note.relative_path.clone(),
                        title: note.title.clone(),
                        memory_type,
                        why_relevant: scored
                            .reasons
                            .first()
                            .cloned()
                            .unwrap_or_else(|| "matched retrieval query".to_string()),
                        score: scored.score,
                        confidence: scored.confidence,
                    },
                    8,
                );
            }
        }
    }

    // Inject lifecycle candidates into the appropriate sections.
    for candidate in &bundle.route.lifecycle_candidates {
        let item = WakeupMemoryItem {
            title: candidate.title.clone(),
            summary: candidate.summary.clone(),
            memory_type: Some(candidate.memory_type.clone()),
            source: format!("ledger:{}", candidate.record_id),
            sensitivity: None,
            source_of_truth: false,
            confidence: candidate.confidence,
        };
        match candidate.memory_type.as_str() {
            "preference" | "workflow" => push_limited(&mut working_style, item, 5),
            "project" => push_limited(&mut active_context, item, 5),
            "constraint" => push_limited(&mut constraints, item, 5),
            "decision" => push_limited(&mut decisions, item, 5),
            "incident" => push_limited(&mut incidents, item, 3),
            _ => {}
        }
    }

    ensure_developer_sections(
        profile,
        &mut working_style,
        &mut active_context,
        &recommended_notes,
    );
    apply_selection_basis(&mut selection_basis, bundle, developer_roots, profile);

    let priorities = build_priorities(&constraints, &active_context, &working_style, profile);

    let maintenance_hints = build_maintenance_hints(lifecycle_root);

    WakeupPacket {
        version: "wakeup.v1".to_string(),
        generated_at: generated_at_string(),
        target: bundle.input.target,
        profile,
        query: WakeupQuery {
            task: bundle.input.task.clone(),
            cwd: bundle.input.cwd.display().to_string(),
            files: bundle.input.files.clone(),
        },
        identity: build_identity(bundle, project_config, developer_roots, profile),
        knowledge_index,
        working_style: WakeupSection {
            items: working_style,
        },
        active_context: WakeupSection {
            items: active_context,
        },
        priorities,
        constraints,
        decisions,
        incidents,
        recommended_notes,
        maintenance_hints,
        provenance: WakeupProvenance {
            derived_from,
            selection_basis,
        },
        policy: policy_state.build_policy(),
    }
}

#[cfg(test)]
mod tests {
    use super::build_packet;
    use crate::config::{ProjectConfig, VaultLimits};
    use crate::domain::{
        CandidateNote, ConfidenceTier, ContextBundle, DebugTrace, MatchedProject, Note,
        OutputFormat, RouteInput, RouteResult, Section, TargetTool, WakeupProfile,
    };
    use serde_json::json;
    use std::collections::BTreeMap;
    use std::path::PathBuf;

    fn make_note(relative_path: &str, title: &str, memory_type: &str, sensitivity: &str) -> Note {
        let mut frontmatter = BTreeMap::new();
        frontmatter.insert("memory_type".to_string(), json!(memory_type));
        frontmatter.insert("sensitivity".to_string(), json!(sensitivity));
        frontmatter.insert("source_of_truth".to_string(), json!(true));
        Note::new(
            PathBuf::from(relative_path),
            relative_path.to_string(),
            title.to_string(),
            frontmatter,
            vec![Section {
                heading: Some(title.to_string()),
                level: 1,
                content: format!("{title} body"),
            }],
            Vec::new(),
            format!("{title} body"),
        )
    }

    #[test]
    fn wakeup_should_map_memory_types_into_packet_sections() {
        let notes = [
            make_note("pref.md", "Preference", "preference", "internal"),
            make_note("workflow.md", "Workflow", "workflow", "internal"),
            make_note("project.md", "Project", "project", "internal"),
            make_note("constraint.md", "Constraint", "constraint", "internal"),
            make_note("decision.md", "Decision", "decision", "internal"),
            make_note("incident.md", "Incident", "incident", "internal"),
            make_note("pattern.md", "Pattern", "pattern", "internal"),
            make_note("session.md", "Session", "session", "internal"),
        ];
        let bundle = make_bundle(
            notes
                .iter()
                .map(|note| CandidateNote {
                    relative_path: note.relative_path.clone(),
                    title: note.title.clone(),
                    score: 10,
                    reasons: vec!["matched task token".to_string()],
                    score_breakdown: Vec::new(),
                    confidence: ConfidenceTier::Medium,
                    excerpt: note.raw_content.clone(),
                    memory_type: note.memory_type().map(ToString::to_string),
                    sensitivity: note.sensitivity().map(ToString::to_string),
                    source_of_truth: note.source_of_truth(),
                })
                .collect(),
        );
        let scored_notes = notes
            .iter()
            .map(|note| note.to_scored(10, vec!["matched task token".to_string()]))
            .collect::<Vec<_>>();
        let packet = build_packet(
            &bundle,
            &scored_notes,
            Some(&make_project_config()),
            &[],
            WakeupProfile::Project,
        );

        assert_eq!(packet.working_style.items.len(), 2);
        assert_eq!(packet.active_context.items.len(), 1);
        assert_eq!(packet.constraints.len(), 1);
        assert_eq!(packet.decisions.len(), 1);
        assert_eq!(packet.incidents.len(), 1);
        assert_eq!(packet.recommended_notes.len(), 1);
        assert!(
            packet
                .recommended_notes
                .iter()
                .all(|item| item.path != "session.md")
        );
    }

    #[test]
    fn wakeup_policy_should_redact_confidential_content() {
        let confidential = make_note(
            "confidential.md",
            "Confidential",
            "constraint",
            "confidential",
        );
        let bundle = make_bundle(vec![CandidateNote {
            relative_path: confidential.relative_path.clone(),
            title: confidential.title.clone(),
            score: 18,
            reasons: vec!["confidential reason".to_string()],
            score_breakdown: Vec::new(),
            confidence: ConfidenceTier::Medium,
            excerpt: "Highly specific confidential implementation details that should not be exposed verbatim.".to_string(),
            memory_type: confidential.memory_type().map(ToString::to_string),
            sensitivity: confidential.sensitivity().map(ToString::to_string),
            source_of_truth: confidential.source_of_truth(),
        }]);
        let packet = build_packet(
            &bundle,
            &[confidential.to_scored(18, vec!["confidential reason".to_string()])],
            Some(&make_project_config()),
            &[],
            WakeupProfile::Project,
        );

        assert_eq!(
            packet.policy.max_sensitivity_included.as_deref(),
            Some("confidential")
        );
        assert!(packet.policy.redactions_applied);
        assert!(packet.constraints[0].summary.contains("[redacted]"));
    }

    #[test]
    fn wakeup_policy_should_suppress_secret_notes_and_report_counts() {
        let visible = make_note("visible.md", "Visible", "constraint", "internal");
        let secret = make_note("secret.md", "Secret", "constraint", "secret");
        let notes = [visible.clone(), secret.clone()];
        let bundle = make_bundle(vec![
            CandidateNote {
                relative_path: visible.relative_path.clone(),
                title: visible.title.clone(),
                score: 12,
                reasons: vec!["visible reason".to_string()],
                score_breakdown: Vec::new(),
                confidence: ConfidenceTier::Medium,
                excerpt: visible.raw_content.clone(),
                memory_type: visible.memory_type().map(ToString::to_string),
                sensitivity: visible.sensitivity().map(ToString::to_string),
                source_of_truth: visible.source_of_truth(),
            },
            CandidateNote {
                relative_path: secret.relative_path.clone(),
                title: secret.title.clone(),
                score: 20,
                reasons: vec!["secret reason".to_string()],
                score_breakdown: Vec::new(),
                confidence: ConfidenceTier::Medium,
                excerpt: secret.raw_content.clone(),
                memory_type: secret.memory_type().map(ToString::to_string),
                sensitivity: secret.sensitivity().map(ToString::to_string),
                source_of_truth: secret.source_of_truth(),
            },
        ]);
        let scored_notes = notes
            .iter()
            .zip([12, 20])
            .zip([
                vec!["visible reason".to_string()],
                vec!["secret reason".to_string()],
            ])
            .map(|((note, score), reasons)| note.to_scored(score, reasons))
            .collect::<Vec<_>>();
        let packet = build_packet(
            &bundle,
            &scored_notes,
            Some(&make_project_config()),
            &[],
            WakeupProfile::Project,
        );

        assert_eq!(packet.policy.suppressed_note_count, 1);
        assert!(
            packet
                .constraints
                .iter()
                .all(|item| item.source != "secret.md")
        );
        assert_eq!(
            packet.policy.max_sensitivity_included.as_deref(),
            Some("internal")
        );
    }

    #[test]
    fn wakeup_should_preserve_provenance_for_promoted_items() {
        let note = make_note("constraint.md", "Constraint", "constraint", "internal");
        let bundle = make_bundle(vec![CandidateNote {
            relative_path: note.relative_path.clone(),
            title: note.title.clone(),
            score: 15,
            reasons: vec!["source_of_truth boosted retrieval".to_string()],
            score_breakdown: Vec::new(),
            confidence: ConfidenceTier::Medium,
            excerpt: note.raw_content.clone(),
            memory_type: note.memory_type().map(ToString::to_string),
            sensitivity: note.sensitivity().map(ToString::to_string),
            source_of_truth: note.source_of_truth(),
        }]);
        let packet = build_packet(
            &bundle,
            &[note.to_scored(15, vec!["source_of_truth boosted retrieval".to_string()])],
            Some(&make_project_config()),
            &[],
            WakeupProfile::Project,
        );

        assert_eq!(packet.provenance.derived_from.len(), 1);
        assert_eq!(packet.provenance.derived_from[0].path, "constraint.md");
        assert!(
            packet
                .provenance
                .selection_basis
                .iter()
                .any(|reason| reason.contains("source_of_truth"))
        );
    }

    #[test]
    fn developer_packet_should_use_developer_roots_and_fill_sections() {
        let preference = make_note(
            "00-Identity/preferences.md",
            "偏好",
            "preference",
            "internal",
        );
        let project = make_note("10-Projects/spool.md", "项目", "project", "internal");
        let bundle = make_bundle(vec![
            CandidateNote {
                relative_path: preference.relative_path.clone(),
                title: preference.title.clone(),
                score: 12,
                reasons: vec!["matched task token preference".to_string()],
                score_breakdown: Vec::new(),
                confidence: ConfidenceTier::Medium,
                excerpt: preference.raw_content.clone(),
                memory_type: preference.memory_type().map(ToString::to_string),
                sensitivity: preference.sensitivity().map(ToString::to_string),
                source_of_truth: preference.source_of_truth(),
            },
            CandidateNote {
                relative_path: project.relative_path.clone(),
                title: project.title.clone(),
                score: 10,
                reasons: vec!["matched project token".to_string()],
                score_breakdown: Vec::new(),
                confidence: ConfidenceTier::Medium,
                excerpt: project.raw_content.clone(),
                memory_type: project.memory_type().map(ToString::to_string),
                sensitivity: project.sensitivity().map(ToString::to_string),
                source_of_truth: project.source_of_truth(),
            },
        ]);
        let scored_notes = vec![
            preference.to_scored(12, vec!["matched task token preference".to_string()]),
            project.to_scored(10, vec!["matched project token".to_string()]),
        ];
        let packet = build_packet(
            &bundle,
            &scored_notes,
            Some(&make_project_config()),
            &["00-Identity".to_string(), "20-Areas".to_string()],
            WakeupProfile::Developer,
        );

        assert_eq!(packet.identity.active_profile, "developer");
        assert_eq!(packet.identity.developer_roots[0], "00-Identity");
        assert!(!packet.working_style.items.is_empty());
        assert!(!packet.active_context.items.is_empty());
    }

    fn make_bundle(candidates: Vec<CandidateNote>) -> ContextBundle {
        ContextBundle {
            input: RouteInput {
                task: "design wakeup".to_string(),
                cwd: PathBuf::from("/tmp/repo"),
                files: vec!["src/app.rs".to_string()],
                target: TargetTool::Claude,
                format: OutputFormat::Json,
            },
            route: RouteResult {
                project: Some(MatchedProject {
                    id: "spool".to_string(),
                    name: "spool".to_string(),
                    reason: "cwd matched repo_path".to_string(),
                }),
                modules: Vec::new(),
                scenes: Vec::new(),
                sources: candidates
                    .iter()
                    .map(|item| item.relative_path.clone())
                    .collect(),
                candidates,
                lifecycle_candidates: Vec::new(),
                debug: DebugTrace {
                    matched_project_id: Some("spool".to_string()),
                    note_roots: vec!["10-Projects".to_string()],
                    scan_roots: vec!["10-Projects".to_string()],
                    limits: VaultLimits::default(),
                    note_count: 1,
                },
                crystallize_hint: None,
            },
        }
    }

    fn make_project_config() -> ProjectConfig {
        ProjectConfig {
            id: "spool".to_string(),
            name: "spool".to_string(),
            repo_paths: vec![PathBuf::from("/tmp/repo")],
            note_roots: vec!["10-Projects".to_string()],
            default_tags: Vec::new(),
            modules: Vec::new(),
        }
    }
}
