use crate::app;
use crate::config::{AppConfig, ProjectConfig};
use crate::domain::{
    ContextBundle, MemoryRecord, OutputFormat, RouteInput, TargetTool, WakeupPacket, WakeupProfile,
};
use crate::enhancement_trace::{PromptOptimizeTrace, write_latest_prompt_optimize_trace};
use crate::lifecycle_store::{LifecycleStore, lifecycle_root_from_config, wakeup_ready_entries};
use crate::output;
use crate::vault::WakeupSnapshot;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub enum MemoryGatewayIntent {
    Context,
    Wakeup { profile: WakeupProfile },
}

#[derive(Debug, Clone)]
pub struct MemoryGatewayRequest {
    pub input: RouteInput,
    pub intent: MemoryGatewayIntent,
}

#[derive(Debug, Clone)]
pub struct MemoryGatewayResponse {
    pub bundle: ContextBundle,
    pub wakeup_packet: Option<WakeupPacket>,
    pub used_vault_root: PathBuf,
}

impl MemoryGatewayResponse {
    pub fn wakeup_packet(&self) -> Option<&WakeupPacket> {
        self.wakeup_packet.as_ref()
    }
}

#[derive(Debug, Clone)]
pub struct PromptOptimizeRequest {
    pub input: RouteInput,
    pub profile: WakeupProfile,
    pub provider: Option<String>,
    pub session_id: Option<String>,
    pub persist_runtime_trace: bool,
}

#[derive(Debug, Clone, serde::Serialize, ts_rs::TS)]
#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
pub struct PromptOptimizeResponse {
    pub combined_prompt: String,
    pub context_prompt: String,
    pub wakeup_prompt: String,
    pub packet: WakeupPacket,
    pub context_bundle: ContextBundle,
    #[ts(type = "string")]
    pub used_vault_root: PathBuf,
    pub target: TargetTool,
    pub profile: WakeupProfile,
    pub provider: Option<String>,
    pub session_id: Option<String>,
    pub runtime_trace: Option<PromptOptimizeTrace>,
}

pub fn load_config(config_path: &Path) -> anyhow::Result<AppConfig> {
    app::load(config_path)
}

pub fn context_request(input: RouteInput) -> MemoryGatewayRequest {
    MemoryGatewayRequest {
        input,
        intent: MemoryGatewayIntent::Context,
    }
}

pub fn wakeup_request(input: RouteInput, profile: WakeupProfile) -> MemoryGatewayRequest {
    MemoryGatewayRequest {
        input,
        intent: MemoryGatewayIntent::Wakeup { profile },
    }
}

pub fn prompt_optimize_request(
    input: RouteInput,
    profile: WakeupProfile,
    provider: Option<String>,
    session_id: Option<String>,
    persist_runtime_trace: bool,
) -> PromptOptimizeRequest {
    PromptOptimizeRequest {
        input,
        profile,
        provider,
        session_id,
        persist_runtime_trace,
    }
}

pub fn execute(
    config_path: &Path,
    request: MemoryGatewayRequest,
    vault_root_override: Option<&Path>,
) -> anyhow::Result<MemoryGatewayResponse> {
    let mut config = app::load(config_path)?;
    if let Some(vault_root_override) = vault_root_override {
        config.vault.root = app::resolve_override_path(vault_root_override, config_path)?;
    }

    let used_vault_root = config.vault.root.clone();
    let lifecycle_root = lifecycle_root_for_config(config_path);
    let lifecycle_records = load_lifecycle_records_from_root(&lifecycle_root);
    let reference_map = crate::reference_tracker::read(&lifecycle_root);
    match request.intent {
        MemoryGatewayIntent::Context => {
            let bundle = app::build_bundle_with_lifecycle_and_refs(
                &config,
                request.input,
                &lifecycle_records,
                Some(&reference_map),
            )?;
            touch_lifecycle_candidates(&lifecycle_root, &bundle);
            Ok(MemoryGatewayResponse {
                bundle,
                wakeup_packet: None,
                used_vault_root,
            })
        }
        MemoryGatewayIntent::Wakeup { profile } => {
            let (bundle, packet) = build_wakeup_from_config(
                &config,
                request.input,
                profile,
                &lifecycle_records,
                &lifecycle_root,
            )?;
            touch_lifecycle_candidates(&lifecycle_root, &bundle);
            Ok(MemoryGatewayResponse {
                bundle,
                wakeup_packet: Some(packet),
                used_vault_root,
            })
        }
    }
}

pub fn execute_prompt_optimize(
    config_path: &Path,
    request: PromptOptimizeRequest,
    vault_root_override: Option<&Path>,
) -> anyhow::Result<PromptOptimizeResponse> {
    let mut config = app::load(config_path)?;
    if let Some(vault_root_override) = vault_root_override {
        config.vault.root = app::resolve_override_path(vault_root_override, config_path)?;
    }

    let used_vault_root = config.vault.root.clone();
    let lifecycle_root = lifecycle_root_for_config(config_path);
    let lifecycle_records = load_lifecycle_records_from_root(&lifecycle_root);
    let reference_map = crate::reference_tracker::read(&lifecycle_root);
    let target = request.input.target;
    let context_input = RouteInput {
        format: OutputFormat::Prompt,
        ..request.input.clone()
    };
    let context_bundle = app::build_bundle_with_lifecycle_and_refs(
        &config,
        context_input,
        &lifecycle_records,
        Some(&reference_map),
    )?;
    let context_prompt = output::render(
        &context_bundle,
        config.output.max_chars,
        OutputFormat::Prompt,
    );

    let (_wakeup_bundle, packet) = build_wakeup_from_config(
        &config,
        request.input,
        request.profile,
        &lifecycle_records,
        &lifecycle_root,
    )?;
    let wakeup_prompt = output::wakeup::render(&packet, OutputFormat::Prompt);
    let combined_prompt = format!("{}\n\n{}", wakeup_prompt.trim(), context_prompt.trim());

    // Touch lifecycle candidates from the context bundle
    touch_lifecycle_candidates(&lifecycle_root, &context_bundle);

    let runtime_trace = if request.persist_runtime_trace {
        let trace = PromptOptimizeTrace::new(
            context_bundle.input.cwd.display().to_string(),
            context_bundle.input.task.clone(),
            target_label(target),
            profile_label(request.profile),
            request.provider.clone(),
            request.session_id.clone(),
            context_bundle.route.debug.matched_project_id.clone(),
            context_bundle.route.debug.note_count,
            used_vault_root.display().to_string(),
        );
        write_latest_prompt_optimize_trace(config_path, &trace)?;
        Some(trace)
    } else {
        None
    };

    Ok(PromptOptimizeResponse {
        combined_prompt,
        context_prompt,
        wakeup_prompt,
        packet,
        context_bundle,
        used_vault_root,
        target,
        profile: request.profile,
        provider: request.provider,
        session_id: request.session_id,
        runtime_trace,
    })
}

fn build_wakeup_from_config(
    config: &AppConfig,
    input: RouteInput,
    profile: WakeupProfile,
    lifecycle_records: &[(String, MemoryRecord)],
    lifecycle_root: &Path,
) -> anyhow::Result<(ContextBundle, WakeupPacket)> {
    let input = RouteInput {
        format: crate::domain::OutputFormat::Json,
        ..input
    };
    let (wakeup_snapshot, bundle) =
        wakeup_bundle_and_snapshot(config, input.clone(), profile, lifecycle_records)?;
    let packet = packet_for_profile(
        config,
        &input,
        &wakeup_snapshot,
        &bundle,
        profile,
        lifecycle_root,
    );
    Ok((bundle, packet))
}

fn wakeup_bundle_and_snapshot(
    config: &AppConfig,
    input: RouteInput,
    profile: WakeupProfile,
    lifecycle_records: &[(String, MemoryRecord)],
) -> anyhow::Result<(WakeupSnapshot, ContextBundle)> {
    let wakeup_snapshot = resolve_wakeup_snapshot(config, &input.cwd, profile)?;
    ensure_wakeup_contract(config, &wakeup_snapshot, profile)?;
    let bundle = build_wakeup_bundle(config, &wakeup_snapshot, input, lifecycle_records);
    Ok((wakeup_snapshot, bundle))
}

fn build_wakeup_bundle(
    config: &AppConfig,
    wakeup_snapshot: &WakeupSnapshot,
    input: RouteInput,
    lifecycle_records: &[(String, MemoryRecord)],
) -> ContextBundle {
    let debug = crate::domain::DebugTrace {
        matched_project_id: wakeup_snapshot.project_id.clone(),
        note_roots: wakeup_snapshot.note_roots.clone(),
        scan_roots: wakeup_snapshot.snapshot.scan_roots.clone(),
        limits: config.vault.limits.clone(),
        note_count: wakeup_snapshot.snapshot.notes.len(),
    };
    crate::engine::build_context_with_lifecycle(
        config,
        &wakeup_snapshot.snapshot.notes,
        lifecycle_records,
        input,
        debug,
    )
}

fn lifecycle_root_for_config(config_path: &Path) -> PathBuf {
    let config_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
    lifecycle_root_from_config(config_dir)
}

fn load_lifecycle_records_from_root(root: &Path) -> Vec<(String, MemoryRecord)> {
    if !root.exists() {
        return Vec::new();
    }
    let store = LifecycleStore::new(root);
    wakeup_ready_entries(&store)
        .unwrap_or_default()
        .into_iter()
        .map(|entry| (entry.record_id, entry.record))
        .collect()
}

/// Fire-and-forget touch for staleness tracking: update the reference
/// tracker for all lifecycle candidates that made it into the bundle.
fn touch_lifecycle_candidates(lifecycle_root: &Path, bundle: &ContextBundle) {
    if bundle.route.lifecycle_candidates.is_empty() {
        return;
    }
    let ids: Vec<&str> = bundle
        .route
        .lifecycle_candidates
        .iter()
        .map(|c| c.record_id.as_str())
        .collect();
    crate::reference_tracker::touch(lifecycle_root, &ids);
}

fn resolve_wakeup_snapshot(
    config: &AppConfig,
    cwd: &Path,
    profile: WakeupProfile,
) -> anyhow::Result<WakeupSnapshot> {
    match profile {
        WakeupProfile::Project => build_project_wakeup_snapshot(config, cwd),
        WakeupProfile::Developer => build_developer_wakeup_snapshot(config, cwd),
    }
}

fn build_project_wakeup_snapshot(config: &AppConfig, cwd: &Path) -> anyhow::Result<WakeupSnapshot> {
    let project_config = require_project_config(config, cwd)?;
    let note_roots = project_config.note_roots.clone();
    let snapshot = crate::vault::cached_scan_notes_with_debug(
        &config.vault.root,
        note_roots.as_slice(),
        &config.vault.limits,
    )?;

    Ok(WakeupSnapshot {
        project_id: Some(project_config.id.clone()),
        note_roots,
        snapshot,
    })
}

fn build_developer_wakeup_snapshot(
    config: &AppConfig,
    cwd: &Path,
) -> anyhow::Result<WakeupSnapshot> {
    let matched_project = crate::engine::project_config_for_input(config, cwd);
    let note_roots = config.developer.effective_note_roots(
        matched_project
            .map(|project| project.note_roots.as_slice())
            .unwrap_or(&[]),
    );
    if note_roots.is_empty() {
        anyhow::bail!("developer wakeup has no note_roots configured");
    }
    let snapshot = crate::vault::cached_scan_notes_with_debug(
        &config.vault.root,
        note_roots.as_slice(),
        &config.vault.limits,
    )?;

    Ok(WakeupSnapshot {
        project_id: matched_project.map(|project| project.id.clone()),
        note_roots,
        snapshot,
    })
}

fn ensure_wakeup_contract(
    config: &AppConfig,
    wakeup_snapshot: &WakeupSnapshot,
    profile: WakeupProfile,
) -> anyhow::Result<()> {
    if wakeup_snapshot.note_roots.is_empty() {
        anyhow::bail!("wakeup profile has no note_roots configured");
    }
    if matches!(profile, WakeupProfile::Project) && wakeup_snapshot.project_id.is_none() {
        anyhow::bail!("project wakeup requires a matched project");
    }
    if matches!(profile, WakeupProfile::Developer)
        && config.developer.effective_note_roots(&[]).is_empty()
    {
        anyhow::bail!("developer wakeup requires developer note_roots");
    }
    Ok(())
}

fn packet_for_profile(
    config: &AppConfig,
    input: &RouteInput,
    wakeup_snapshot: &WakeupSnapshot,
    bundle: &ContextBundle,
    profile: WakeupProfile,
    lifecycle_root: &Path,
) -> WakeupPacket {
    match profile {
        WakeupProfile::Developer => {
            build_developer_packet(config, wakeup_snapshot, bundle, input, lifecycle_root)
        }
        WakeupProfile::Project => {
            build_project_packet(config, wakeup_snapshot, bundle, input, lifecycle_root)
        }
    }
}

fn target_label(target: TargetTool) -> &'static str {
    match target {
        TargetTool::Claude => "claude",
        TargetTool::Codex => "codex",
        TargetTool::Opencode => "opencode",
    }
}

fn profile_label(profile: WakeupProfile) -> &'static str {
    match profile {
        WakeupProfile::Developer => "developer",
        WakeupProfile::Project => "project",
    }
}

fn build_developer_packet(
    config: &AppConfig,
    wakeup_snapshot: &WakeupSnapshot,
    bundle: &ContextBundle,
    input: &RouteInput,
    lifecycle_root: &Path,
) -> WakeupPacket {
    let matched_project = matched_project_for_wakeup(config, wakeup_snapshot);
    let scored_notes = build_wakeup_scored_notes(
        config,
        bundle,
        matched_project,
        wakeup_snapshot,
        input,
        WakeupProfile::Developer,
    );
    build_wakeup_packet(
        bundle,
        &scored_notes,
        matched_project,
        config,
        WakeupProfile::Developer,
        lifecycle_root,
    )
}

fn build_project_packet(
    config: &AppConfig,
    wakeup_snapshot: &WakeupSnapshot,
    bundle: &ContextBundle,
    input: &RouteInput,
    lifecycle_root: &Path,
) -> WakeupPacket {
    let matched_project = matched_project_for_wakeup(config, wakeup_snapshot);
    let scored_notes = build_wakeup_scored_notes(
        config,
        bundle,
        matched_project,
        wakeup_snapshot,
        input,
        WakeupProfile::Project,
    );
    build_wakeup_packet(
        bundle,
        &scored_notes,
        matched_project,
        config,
        WakeupProfile::Project,
        lifecycle_root,
    )
}

fn matched_project_for_wakeup<'a>(
    config: &'a AppConfig,
    wakeup_snapshot: &'a WakeupSnapshot,
) -> Option<&'a ProjectConfig> {
    wakeup_snapshot
        .project_id
        .as_deref()
        .and_then(|project_id| {
            config
                .projects
                .iter()
                .find(|project| project.id == project_id)
        })
}

fn build_wakeup_scored_notes(
    config: &AppConfig,
    bundle: &ContextBundle,
    matched_project: Option<&ProjectConfig>,
    wakeup_snapshot: &WakeupSnapshot,
    input: &RouteInput,
    profile: WakeupProfile,
) -> Vec<crate::domain::ScoredNote> {
    crate::engine::selector::select_scored_notes(
        matched_project,
        bundle.route.project.as_ref(),
        &bundle.route.modules,
        &bundle.route.scenes,
        &wakeup_snapshot.snapshot.notes,
        input,
        match profile {
            WakeupProfile::Developer => config.output.max_notes.max(12),
            WakeupProfile::Project => config.output.max_notes.max(8),
        },
    )
}

fn build_wakeup_packet(
    bundle: &ContextBundle,
    scored_notes: &[crate::domain::ScoredNote],
    matched_project: Option<&ProjectConfig>,
    config: &AppConfig,
    profile: WakeupProfile,
    lifecycle_root: &Path,
) -> WakeupPacket {
    let vault_root = config.vault.root.as_path();
    let current_project_id = bundle
        .route
        .project
        .as_ref()
        .map(|project| project.id.as_str());
    let knowledge_index = crate::wiki_index::load_index_section(vault_root, current_project_id);
    crate::wakeup::build_packet_with_index(
        bundle,
        scored_notes,
        matched_project,
        &config.developer.note_roots,
        profile,
        knowledge_index,
        Some(lifecycle_root),
    )
}

fn require_project_config<'a>(
    config: &'a AppConfig,
    cwd: &Path,
) -> anyhow::Result<&'a ProjectConfig> {
    let project = crate::engine::project_config_for_input(config, cwd)
        .ok_or_else(|| anyhow::anyhow!("no project matched cwd: {}", cwd.display()))?;
    if project.note_roots.is_empty() {
        anyhow::bail!(
            "matched project has no note_roots configured: {}",
            project.id
        );
    }
    Ok(project)
}

#[cfg(test)]
mod tests {
    use super::{
        context_request, execute, execute_prompt_optimize, load_config, prompt_optimize_request,
        wakeup_request,
    };
    use crate::domain::{OutputFormat, RouteInput, TargetTool, WakeupProfile};
    use crate::enhancement_trace::read_latest_prompt_optimize_trace;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn gateway_should_build_context_and_optional_wakeup() {
        let temp = tempdir().unwrap();
        let vault_dir = temp.path().join("vault");
        let repo_dir = temp.path().join("repo");
        fs::create_dir_all(vault_dir.join("10-Projects")).unwrap();
        fs::create_dir_all(&repo_dir).unwrap();

        fs::write(
            vault_dir.join("10-Projects/project.md"),
            "# spool\n\nproject context\n",
        )
        .unwrap();

        let config = format!(
            "[vault]\nroot = \"{}\"\n\n[[projects]]\nid = \"spool\"\nname = \"spool\"\nrepo_paths = [\"{}\"]\nnote_roots = [\"10-Projects\"]\n",
            vault_dir.display(),
            repo_dir.display()
        );
        let config_path = temp.path().join("spool.toml");
        fs::write(&config_path, config).unwrap();

        let context = execute(
            &config_path,
            context_request(RouteInput {
                task: "gateway route".to_string(),
                cwd: repo_dir.clone(),
                files: vec![],
                target: TargetTool::Claude,
                format: OutputFormat::Markdown,
            }),
            None,
        )
        .unwrap();

        let wakeup = execute(
            &config_path,
            wakeup_request(
                RouteInput {
                    task: "gateway wakeup".to_string(),
                    cwd: repo_dir.clone(),
                    files: vec![],
                    target: TargetTool::Claude,
                    format: OutputFormat::Markdown,
                },
                WakeupProfile::Project,
            ),
            None,
        )
        .unwrap();

        let loaded = load_config(&config_path).unwrap();
        assert!(
            crate::output::render(
                &context.bundle,
                loaded.output.max_chars,
                OutputFormat::Markdown
            )
            .contains("spool")
                || crate::output::render(
                    &context.bundle,
                    loaded.output.max_chars,
                    OutputFormat::Markdown
                )
                .contains("project")
        );
        assert!(crate::output::explain(&context.bundle).contains("# route explain"));
        assert_eq!(
            wakeup.wakeup_packet().unwrap().profile,
            WakeupProfile::Project
        );
    }

    #[test]
    fn gateway_should_build_combined_prompt_and_optional_runtime_trace() {
        let temp = tempdir().unwrap();
        let vault_dir = temp.path().join("vault");
        let repo_dir = temp.path().join("repo");
        fs::create_dir_all(vault_dir.join("10-Projects")).unwrap();
        fs::create_dir_all(&repo_dir).unwrap();

        fs::write(
            vault_dir.join("10-Projects/project.md"),
            "# spool\n\nproject context\n",
        )
        .unwrap();

        let config = format!(
            "[vault]\nroot = \"{}\"\n\n[output]\ndefault_format = \"markdown\"\nmax_chars = 12000\nmax_notes = 8\n\n[[projects]]\nid = \"spool\"\nname = \"spool\"\nrepo_paths = [\"{}\"]\nnote_roots = [\"10-Projects\"]\n",
            vault_dir.display(),
            repo_dir.display()
        );
        let config_path = temp.path().join("spool.toml");
        fs::write(&config_path, config).unwrap();

        let response = execute_prompt_optimize(
            &config_path,
            prompt_optimize_request(
                RouteInput {
                    task: "optimize prompt".to_string(),
                    cwd: repo_dir.clone(),
                    files: vec!["src/mcp.rs".to_string()],
                    target: TargetTool::Codex,
                    format: OutputFormat::Prompt,
                },
                WakeupProfile::Project,
                Some("codex".to_string()),
                Some("codex:session-99".to_string()),
                true,
            ),
            None,
        )
        .unwrap();

        assert!(response.combined_prompt.contains("Codex"));
        assert_eq!(response.target, TargetTool::Codex);
        assert_eq!(response.profile, WakeupProfile::Project);
        assert_eq!(response.provider.as_deref(), Some("codex"));
        assert_eq!(response.session_id.as_deref(), Some("codex:session-99"));
        assert!(response.runtime_trace.is_some());

        let trace = read_latest_prompt_optimize_trace(&config_path)
            .unwrap()
            .unwrap();
        assert_eq!(trace.session_id.as_deref(), Some("codex:session-99"));
    }
}
