use crate::config::{AppConfig, ProjectConfig, load_from_path};
use crate::domain::{
    ContextBundle, DebugTrace, OutputFormat, RouteInput, WakeupPacket, WakeupProfile,
};
use crate::lifecycle_store::lifecycle_root_from_config;
use crate::vault::{RoutedSnapshot, WakeupSnapshot};
use crate::{engine, output, vault, wakeup};
use std::path::{Path, PathBuf};

fn developer_scored_note_limit(config: &AppConfig) -> usize {
    config.output.max_notes.max(12)
}

fn wakeup_scored_note_limit(config: &AppConfig, profile: WakeupProfile) -> usize {
    match profile {
        WakeupProfile::Developer => developer_scored_note_limit(config),
        WakeupProfile::Project => config.output.max_notes.max(8),
    }
}

fn build_wakeup_debug(config: &AppConfig, wakeup_snapshot: &WakeupSnapshot) -> DebugTrace {
    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(),
    }
}

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

fn build_developer_note_roots(
    config: &AppConfig,
    matched_project: Option<&ProjectConfig>,
) -> Vec<String> {
    config.developer.effective_note_roots(
        matched_project
            .map(|project| project.note_roots.as_slice())
            .unwrap_or(&[]),
    )
}

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

fn build_wakeup_packet(
    bundle: &ContextBundle,
    scored_notes: &[crate::domain::ScoredNote],
    matched_project: Option<&ProjectConfig>,
    config: &AppConfig,
    profile: WakeupProfile,
    lifecycle_root: Option<&Path>,
) -> WakeupPacket {
    let knowledge_index = load_wakeup_index(bundle, config);
    wakeup::build_packet_with_index(
        bundle,
        scored_notes,
        matched_project,
        &config.developer.note_roots,
        profile,
        knowledge_index,
        lifecycle_root,
    )
}

fn load_wakeup_index(bundle: &ContextBundle, config: &AppConfig) -> Option<String> {
    let vault_root = config.vault.root.as_path();
    let current_project_id = bundle
        .route
        .project
        .as_ref()
        .map(|project| project.id.as_str());
    crate::wiki_index::load_index_section(vault_root, current_project_id)
}

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 = 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 = engine::project_config_for_input(config, cwd);
    let note_roots = build_developer_note_roots(config, matched_project);
    if note_roots.is_empty() {
        anyhow::bail!("developer wakeup has no note_roots configured");
    }
    let snapshot = 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 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_wakeup_bundle(
    config: &AppConfig,
    wakeup_snapshot: &WakeupSnapshot,
    input: RouteInput,
) -> ContextBundle {
    let debug = build_wakeup_debug(config, wakeup_snapshot);
    engine::build_context(config, &wakeup_snapshot.snapshot.notes, input, debug)
}

fn matched_project_for_wakeup<'a>(
    config: &'a AppConfig,
    wakeup_snapshot: &WakeupSnapshot,
) -> Option<&'a ProjectConfig> {
    find_project_config(config, wakeup_snapshot.project_id.as_deref())
}

fn build_developer_packet(
    config: &AppConfig,
    wakeup_snapshot: &WakeupSnapshot,
    bundle: &ContextBundle,
    input: &RouteInput,
    lifecycle_root: Option<&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: Option<&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 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 build_packet_from_profile(
    config: &AppConfig,
    wakeup_snapshot: &WakeupSnapshot,
    bundle: &ContextBundle,
    input: &RouteInput,
    profile: WakeupProfile,
    lifecycle_root: Option<&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 build_wakeup_input(mut input: RouteInput) -> RouteInput {
    input.format = OutputFormat::Json;
    input
}

fn resolve_wakeup_profile(profile: WakeupProfile) -> WakeupProfile {
    profile
}

fn wakeup_bundle_and_snapshot(
    config: &AppConfig,
    input: RouteInput,
    profile: WakeupProfile,
) -> 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);
    Ok((wakeup_snapshot, bundle))
}

fn packet_for_profile(
    config: &AppConfig,
    input: &RouteInput,
    wakeup_snapshot: &WakeupSnapshot,
    bundle: &ContextBundle,
    profile: WakeupProfile,
    lifecycle_root: &Path,
) -> WakeupPacket {
    build_packet_from_profile(
        config,
        wakeup_snapshot,
        bundle,
        input,
        profile,
        Some(lifecycle_root),
    )
}

fn wakeup_config(config_path: &Path) -> anyhow::Result<AppConfig> {
    load_from_path(config_path)
}

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 wakeup_input(input: RouteInput) -> RouteInput {
    build_wakeup_input(input)
}

pub struct AppResult {
    pub bundle: ContextBundle,
    pub rendered: String,
    pub explain: String,
    pub used_format: OutputFormat,
    pub used_vault_root: PathBuf,
}

pub fn run(
    config_path: &Path,
    input: RouteInput,
    requested_format: Option<OutputFormat>,
) -> anyhow::Result<AppResult> {
    run_with_overrides(config_path, input, requested_format, None)
}

pub fn run_with_overrides(
    config_path: &Path,
    mut input: RouteInput,
    requested_format: Option<OutputFormat>,
    vault_root_override: Option<&Path>,
) -> anyhow::Result<AppResult> {
    let mut config = load_from_path(config_path)?;
    if let Some(vault_root_override) = vault_root_override {
        config.vault.root = resolve_override_path(vault_root_override, config_path)?;
    }
    let used_format = requested_format.unwrap_or(config.output.default_format);
    input.format = used_format;

    let bundle = build_bundle(&config, input)?;
    let rendered = output::render(&bundle, config.output.max_chars, used_format);
    let explain = output::explain(&bundle);

    Ok(AppResult {
        bundle,
        rendered,
        explain,
        used_format,
        used_vault_root: config.vault.root.clone(),
    })
}

pub(crate) fn resolve_override_path(
    override_path: &Path,
    config_path: &Path,
) -> anyhow::Result<PathBuf> {
    let candidate = if override_path.is_absolute() {
        override_path.to_path_buf()
    } else {
        let base_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
        base_dir.join(override_path)
    };
    Ok(candidate
        .canonicalize()
        .unwrap_or_else(|_| normalize_absolute_path(&candidate)))
}

fn normalize_absolute_path(path: &Path) -> PathBuf {
    use std::path::Component;

    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::CurDir => {}
            Component::ParentDir => {
                normalized.pop();
            }
            other => normalized.push(other.as_os_str()),
        }
    }
    normalized
}

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

pub fn build_bundle(config: &AppConfig, input: RouteInput) -> anyhow::Result<ContextBundle> {
    build_bundle_with_lifecycle(config, input, &[])
}

pub fn build_bundle_with_lifecycle(
    config: &AppConfig,
    input: RouteInput,
    lifecycle_records: &[(String, crate::domain::MemoryRecord)],
) -> anyhow::Result<ContextBundle> {
    build_bundle_with_lifecycle_and_refs(config, input, lifecycle_records, None)
}

pub fn build_bundle_with_lifecycle_and_refs(
    config: &AppConfig,
    input: RouteInput,
    lifecycle_records: &[(String, crate::domain::MemoryRecord)],
    reference_map: Option<&crate::reference_tracker::ReferenceMap>,
) -> anyhow::Result<ContextBundle> {
    let routed = build_routed_snapshot(config, &input.cwd)?;
    let debug = DebugTrace {
        matched_project_id: Some(routed.project_id),
        note_roots: routed.note_roots,
        scan_roots: routed.snapshot.scan_roots.clone(),
        limits: config.vault.limits.clone(),
        note_count: routed.snapshot.notes.len(),
    };
    Ok(engine::build_context_with_lifecycle_and_refs(
        config,
        &routed.snapshot.notes,
        lifecycle_records,
        input,
        debug,
        reference_map,
    ))
}

pub fn render(config: &AppConfig, bundle: &ContextBundle, format: OutputFormat) -> String {
    output::render(bundle, config.output.max_chars, format)
}

pub fn build_wakeup(
    config_path: &Path,
    input: RouteInput,
    profile: WakeupProfile,
) -> anyhow::Result<WakeupPacket> {
    let config = wakeup_config(config_path)?;
    let profile = resolve_wakeup_profile(profile);
    let input = wakeup_input(input);
    let (wakeup_snapshot, bundle) = wakeup_bundle_and_snapshot(&config, input.clone(), profile)?;
    let lifecycle_root = lifecycle_root_for_config(config_path);
    Ok(packet_for_profile(
        &config,
        &input,
        &wakeup_snapshot,
        &bundle,
        profile,
        &lifecycle_root,
    ))
}

pub fn explain(bundle: &ContextBundle) -> String {
    output::explain(bundle)
}

pub fn resolve_format(config: &AppConfig, requested_format: Option<OutputFormat>) -> OutputFormat {
    requested_format.unwrap_or(config.output.default_format)
}

fn require_project_config<'a>(
    config: &'a AppConfig,
    cwd: &Path,
) -> anyhow::Result<&'a ProjectConfig> {
    let project = 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)
}

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

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

#[cfg(test)]
mod tests {
    use super::{resolve_format, resolve_override_path};
    use crate::config::{
        AppConfig, DeveloperConfig, EmbeddingConfig, OutputConfig, SceneConfig, VaultConfig,
        VaultLimits,
    };
    use crate::domain::OutputFormat;
    use std::path::{Path, PathBuf};

    #[test]
    fn resolve_format_falls_back_to_config_default() {
        let config = AppConfig {
            vault: VaultConfig {
                root: PathBuf::from("/tmp"),
                limits: VaultLimits::default(),
            },
            output: OutputConfig {
                default_format: OutputFormat::Json,
                max_chars: 100,
                max_notes: 3,
                max_lifecycle: 5,
            },
            developer: DeveloperConfig::default(),
            projects: Vec::new(),
            scenes: Vec::<SceneConfig>::new(),
            embedding: EmbeddingConfig::default(),
        };

        assert_eq!(resolve_format(&config, None), OutputFormat::Json);
        assert_eq!(
            resolve_format(&config, Some(OutputFormat::Prompt)),
            OutputFormat::Prompt
        );
    }

    #[test]
    fn resolve_override_path_against_config_dir() {
        let resolved = resolve_override_path(
            Path::new("../vault-dev"),
            Path::new("/tmp/example/config/spool.toml"),
        )
        .unwrap();

        assert_eq!(resolved, Path::new("/tmp/example/vault-dev"));
    }
}
