use std::fmt::Write as _;
use std::future::Future;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use serde::Serialize;

use crate::instructions::{
    InstructionBundle, InstructionDiscoveryOptions, InstructionSegment,
    extract_instruction_highlights, read_instruction_bundle, render_instruction_summary_markdown,
};
use crate::persistent_memory::{
    MEMORY_FILENAME, MEMORY_SUMMARY_FILENAME, PersistentMemoryExcerpt, extract_memory_highlights,
    read_persistent_memory_excerpt,
};
use crate::skills::model::SkillMetadata;
use crate::utils::file_utils::canonicalize_with_context;
use vtcode_config::core::AgentConfig;

pub const PROJECT_DOC_SEPARATOR: &str = "\n\n--- project-doc ---\n\n";
pub const PERSISTENT_MEMORY_SEPARATOR: &str = "\n\n--- persistent-memory ---\n\n";
const PROJECT_DOC_SUMMARY_TITLE: &str = "PROJECT DOCUMENTATION";
const PROJECT_DOC_TRUNCATION_NOTE: &str = "Some instruction files exceeded the configured prompt budget and were indexed instead of fully inlined.";
const PERSISTENT_MEMORY_TRUNCATION_NOTE: &str =
    "Persistent memory was truncated to the configured startup excerpt budget.";
const PERSISTENT_MEMORY_HIGHLIGHT_LIMIT: usize = 3;

#[derive(Debug, Clone, Serialize)]
pub struct ProjectDocBundle {
    pub contents: String,
    pub sources: Vec<PathBuf>,
    pub segments: Vec<InstructionSegment>,
    pub truncated: bool,
    pub bytes_read: usize,
}

impl ProjectDocBundle {
    pub fn highlights(&self, limit: usize) -> Vec<String> {
        extract_instruction_highlights(&self.segments, limit)
    }
}

pub struct ProjectDocOptions<'a> {
    pub current_dir: &'a Path,
    pub project_root: &'a Path,
    pub home_dir: Option<&'a Path>,
    pub extra_instruction_files: &'a [String],
    pub fallback_filenames: &'a [String],
    pub exclude_patterns: &'a [String],
    pub match_paths: &'a [PathBuf],
    pub import_max_depth: usize,
    pub max_bytes: usize,
}

#[derive(Debug, Clone, Serialize)]
pub struct InstructionAppendixBundle {
    pub contents: String,
    pub project_doc: Option<ProjectDocBundle>,
    pub persistent_memory: Option<PersistentMemoryExcerpt>,
    pub project_root: PathBuf,
    pub home_dir: Option<PathBuf>,
}

pub async fn read_project_doc_with_options(
    options: &ProjectDocOptions<'_>,
) -> Result<Option<ProjectDocBundle>> {
    if options.max_bytes == 0 {
        return Ok(None);
    }

    match read_instruction_bundle(
        &InstructionDiscoveryOptions {
            current_dir: options.current_dir,
            project_root: options.project_root,
            home_dir: options.home_dir,
            extra_patterns: options.extra_instruction_files,
            fallback_filenames: options.fallback_filenames,
            exclude_patterns: options.exclude_patterns,
            match_paths: options.match_paths,
            import_max_depth: options.import_max_depth,
        },
        options.max_bytes,
    )
    .await?
    {
        Some(bundle) => Ok(Some(convert_bundle(bundle))),
        None => Ok(None),
    }
}

pub async fn read_project_doc(cwd: &Path, max_bytes: usize) -> Result<Option<ProjectDocBundle>> {
    if max_bytes == 0 {
        return Ok(None);
    }

    let project_root = resolve_project_root(cwd).unwrap_or_else(|_| cwd.to_path_buf());
    let home_dir = dirs::home_dir();

    read_project_doc_with_options(&ProjectDocOptions {
        current_dir: cwd,
        project_root: &project_root,
        home_dir: home_dir.as_deref(),
        extra_instruction_files: &[],
        fallback_filenames: &[],
        exclude_patterns: &[],
        match_paths: &[],
        import_max_depth: 5,
        max_bytes,
    })
    .await
}

pub fn get_user_instructions<'a>(
    config: &'a AgentConfig,
    active_dir: &'a Path,
    _skills: Option<&'a [SkillMetadata]>,
) -> impl Future<Output = Option<String>> + 'a {
    build_instruction_appendix(config, active_dir)
}

pub fn build_instruction_appendix<'a>(
    config: &'a AgentConfig,
    active_dir: &'a Path,
) -> impl Future<Output = Option<String>> + 'a {
    build_instruction_appendix_with_context(config, active_dir, &[])
}

pub async fn build_instruction_appendix_with_context(
    config: &AgentConfig,
    active_dir: &Path,
    match_paths: &[PathBuf],
) -> Option<String> {
    load_instruction_appendix(config, active_dir, match_paths)
        .await
        .map(|bundle| bundle.contents)
}

pub async fn load_instruction_appendix(
    config: &AgentConfig,
    active_dir: &Path,
    match_paths: &[PathBuf],
) -> Option<InstructionAppendixBundle> {
    let project_root =
        resolve_project_root(active_dir).unwrap_or_else(|_| active_dir.to_path_buf());
    let home_dir = dirs::home_dir();
    let bundle = read_project_doc_with_options(&ProjectDocOptions {
        current_dir: active_dir,
        project_root: &project_root,
        home_dir: home_dir.as_deref(),
        extra_instruction_files: &config.instruction_files,
        fallback_filenames: &config.project_doc_fallback_filenames,
        exclude_patterns: &config.instruction_excludes,
        match_paths,
        import_max_depth: config.instruction_import_max_depth,
        max_bytes: config.instruction_max_bytes,
    })
    .await
    .ok()
    .flatten();
    let persistent_memory =
        read_persistent_memory_excerpt(&config.persistent_memory, &project_root)
            .await
            .ok()
            .flatten();

    let contents = render_instruction_appendix(
        config.user_instructions.as_deref(),
        bundle.as_ref(),
        persistent_memory.as_ref(),
        &project_root,
        home_dir.as_deref(),
    )?;

    Some(InstructionAppendixBundle {
        contents,
        project_doc: bundle,
        persistent_memory,
        project_root,
        home_dir,
    })
}

pub fn render_instruction_appendix(
    user_instructions: Option<&str>,
    bundle: Option<&ProjectDocBundle>,
    persistent_memory: Option<&PersistentMemoryExcerpt>,
    project_root: &Path,
    home_dir: Option<&Path>,
) -> Option<String> {
    let mut section = String::with_capacity(1024);

    if let Some(user_inst) = user_instructions.map(str::trim)
        && !user_inst.is_empty()
    {
        section.push_str(user_inst);
    }

    if let Some(bundle) = bundle
        && !bundle.segments.is_empty()
    {
        if !section.is_empty() {
            section.push_str(PROJECT_DOC_SEPARATOR);
        }

        section.push_str(
            render_instruction_summary_markdown(
                PROJECT_DOC_SUMMARY_TITLE,
                &bundle.segments,
                bundle.truncated,
                project_root,
                home_dir,
                6,
                PROJECT_DOC_TRUNCATION_NOTE,
            )
            .trim_end(),
        );
    }

    if let Some(memory_section) =
        persistent_memory.and_then(render_persistent_memory_summary_markdown)
    {
        if !section.is_empty() {
            section.push_str(PERSISTENT_MEMORY_SEPARATOR);
        }

        section.push_str(memory_section.trim_end());
    }

    if section.is_empty() {
        None
    } else {
        Some(section)
    }
}

fn render_persistent_memory_summary_markdown(memory: &PersistentMemoryExcerpt) -> Option<String> {
    let highlights = extract_memory_highlights(&memory.contents, PERSISTENT_MEMORY_HIGHLIGHT_LIMIT);
    if highlights.is_empty() && memory.contents.trim().is_empty() {
        return None;
    }

    let mut section = String::with_capacity(512);
    section.push_str("## PERSISTENT MEMORY\n\n");
    section.push_str("### Files\n");
    let _ = writeln!(section, "- `{MEMORY_SUMMARY_FILENAME}`: startup summary");
    let _ = writeln!(section, "- `{MEMORY_FILENAME}`: durable registry");

    if !highlights.is_empty() {
        section.push_str("\n### Key points\n");
        for highlight in highlights {
            let _ = writeln!(section, "- {highlight}");
        }
    }

    section.push_str(
        "\n### On-demand loading\n- Open `memory_summary.md` or `MEMORY.md` when exact wording matters.\n",
    );

    if memory.truncated {
        let _ = writeln!(section, "\n_{PERSISTENT_MEMORY_TRUNCATION_NOTE}_");
    }

    section.push('\n');
    Some(section)
}

pub fn merge_project_docs_with_skills(
    project_doc: Option<String>,
    skills_section: Option<String>,
) -> Option<String> {
    match (project_doc, skills_section) {
        (Some(doc), Some(skills)) => Some(format!("{}\n\n{}", doc, skills)),
        (Some(doc), None) => Some(doc),
        (None, Some(skills)) => Some(skills),
        (None, None) => None,
    }
}

fn convert_bundle(bundle: InstructionBundle) -> ProjectDocBundle {
    let contents = bundle.combined_text();
    let segments = bundle.segments;
    let sources = segments
        .iter()
        .map(|segment| segment.source.path.clone())
        .collect::<Vec<_>>();

    ProjectDocBundle {
        contents,
        sources,
        segments,
        truncated: bundle.truncated,
        bytes_read: bundle.bytes_read,
    }
}

fn resolve_project_root(cwd: &Path) -> Result<PathBuf> {
    let mut cursor = canonicalize_with_context(cwd, "working directory")?;

    loop {
        let git_marker = cursor.join(".git");
        match std::fs::metadata(&git_marker) {
            Ok(_) => return Ok(cursor),
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
            Err(err) => {
                return Err(err).with_context(|| {
                    format!(
                        "Failed to inspect potential git root {}",
                        git_marker.display()
                    )
                });
            }
        }

        match cursor.parent() {
            Some(parent) => {
                cursor = parent.to_path_buf();
            }
            None => return Ok(cursor),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::instructions::{InstructionScope, InstructionSource, InstructionSourceKind};
    use tempfile::tempdir;

    fn write_doc(dir: &Path, content: &str) -> Result<()> {
        std::fs::write(dir.join("AGENTS.md"), content).context("write AGENTS.md")?;
        Ok(())
    }

    #[tokio::test]
    async fn returns_none_when_no_docs_present() {
        let tmp = tempdir().expect("failed to unwrap");
        let result = read_project_doc(tmp.path(), 4096)
            .await
            .expect("failed to unwrap");
        assert!(result.is_none());
    }

    #[tokio::test]
    async fn reads_doc_within_limit() {
        let tmp = tempdir().expect("failed to unwrap");
        write_doc(tmp.path(), "hello world").expect("write doc");

        let result = read_project_doc(tmp.path(), 4096)
            .await
            .expect("failed to unwrap")
            .expect("failed to unwrap");
        assert_eq!(result.contents, "hello world");
        assert_eq!(result.bytes_read, "hello world".len());
    }

    #[tokio::test]
    async fn truncates_when_limit_exceeded() {
        let tmp = tempdir().expect("failed to unwrap");
        let content = "A".repeat(64);
        write_doc(tmp.path(), &content).expect("write doc");

        let result = read_project_doc(tmp.path(), 16)
            .await
            .expect("failed to unwrap")
            .expect("failed to unwrap");
        assert!(result.truncated);
        assert_eq!(result.contents.len(), 16);
    }

    #[tokio::test]
    async fn reads_docs_from_repo_root_downwards() {
        let repo = tempdir().expect("failed to unwrap");
        std::fs::write(repo.path().join(".git"), "gitdir: /tmp/git").expect("failed to unwrap");
        write_doc(repo.path(), "root doc").expect("write doc");

        let nested = repo.path().join("nested/sub");
        std::fs::create_dir_all(&nested).expect("failed to unwrap");
        write_doc(&nested, "nested doc").expect("write doc");

        let bundle = read_project_doc_with_options(&ProjectDocOptions {
            current_dir: &nested,
            project_root: repo.path(),
            home_dir: None,
            extra_instruction_files: &[],
            fallback_filenames: &[],
            exclude_patterns: &[],
            match_paths: &[],
            import_max_depth: 5,
            max_bytes: 4096,
        })
        .await
        .expect("failed to unwrap")
        .expect("failed to unwrap");
        assert!(bundle.contents.contains("root doc"));
        assert!(bundle.contents.contains("nested doc"));
        assert_eq!(bundle.sources.len(), 2);
    }

    #[tokio::test]
    async fn instruction_appendix_uses_instruction_hierarchy_scope_and_budget() {
        let repo = tempdir().expect("repo");
        std::fs::write(repo.path().join(".git"), "gitdir: /tmp/git").expect("write git");
        write_doc(repo.path(), "root doc").expect("write root doc");

        let nested = repo.path().join("nested/sub");
        std::fs::create_dir_all(&nested).expect("create nested");
        write_doc(&nested, "nested doc").expect("write nested doc");

        let extra_dir = repo.path().join("docs");
        std::fs::create_dir_all(&extra_dir).expect("create docs");
        std::fs::write(extra_dir.join("guidelines.md"), "extra doc").expect("write extra doc");

        let config = AgentConfig {
            user_instructions: Some("user note".to_string()),
            instruction_files: vec!["docs/*.md".to_string()],
            instruction_max_bytes: 4096,
            project_doc_max_bytes: 1,
            ..Default::default()
        };

        let appendix = build_instruction_appendix_with_context(
            &config,
            &nested,
            &[repo.path().join("nested/sub/file.rs")],
        )
        .await
        .expect("instruction appendix");

        assert!(appendix.starts_with("user note"));
        assert!(appendix.contains("--- project-doc ---"));
        assert!(appendix.contains("### Instruction map"));
        assert!(appendix.contains("AGENTS.md (workspace AGENTS)"));
        assert!(appendix.contains("docs/guidelines.md (custom extra instructions)"));
        assert!(appendix.contains("nested/sub/AGENTS.md (workspace AGENTS)"));
        assert!(appendix.contains("root doc"));
        assert!(appendix.contains("extra doc"));
        assert!(appendix.contains("nested doc"));
    }

    #[tokio::test]
    async fn instruction_appendix_returns_none_when_empty() {
        let tmp = tempdir().expect("tmp");
        let appendix = build_instruction_appendix(&AgentConfig::default(), tmp.path()).await;
        assert!(appendix.is_none());
    }

    #[tokio::test]
    async fn instruction_appendix_marks_truncation() {
        let repo = tempdir().expect("repo");
        std::fs::write(repo.path().join(".git"), "gitdir: /tmp/git").expect("write git");
        write_doc(
            repo.path(),
            "- Root summary\n\nThis detail should stay out of the prompt appendix.\n",
        )
        .expect("write doc");

        let config = AgentConfig {
            instruction_max_bytes: 16,
            ..Default::default()
        };

        let appendix = build_instruction_appendix(&config, repo.path())
            .await
            .expect("instruction appendix");

        assert!(appendix.contains("## PROJECT DOCUMENTATION"));
        assert!(appendix.contains("### Instruction map"));
        assert!(appendix.contains("### On-demand loading"));
        assert!(appendix.contains("Some instruction files exceeded the configured prompt budget"));
    }

    #[tokio::test]
    async fn includes_extra_instruction_files() {
        let repo = tempdir().expect("failed to unwrap");
        write_doc(repo.path(), "root doc").expect("write doc");
        let docs = repo.path().join("docs");
        std::fs::create_dir_all(&docs).expect("failed to unwrap");
        let extra = docs.join("guidelines.md");
        std::fs::write(&extra, "extra doc").expect("failed to unwrap");

        let bundle = read_project_doc_with_options(&ProjectDocOptions {
            current_dir: repo.path(),
            project_root: repo.path(),
            home_dir: None,
            extra_instruction_files: &["docs/*.md".to_owned()],
            fallback_filenames: &[],
            exclude_patterns: &[],
            match_paths: &[],
            import_max_depth: 5,
            max_bytes: 4096,
        })
        .await
        .expect("failed to unwrap")
        .expect("failed to unwrap");

        assert!(bundle.contents.contains("root doc"));
        assert!(bundle.contents.contains("extra doc"));
        assert_eq!(bundle.sources.len(), 2);
    }

    #[test]
    fn highlights_extract_bullets() {
        let bundle = ProjectDocBundle {
            contents: "- First\n- Second\n".to_owned(),
            sources: Vec::new(),
            segments: vec![InstructionSegment {
                source: InstructionSource {
                    path: PathBuf::from("AGENTS.md"),
                    scope: InstructionScope::Workspace,
                    kind: InstructionSourceKind::Agents,
                    matched: false,
                },
                contents: "- First\n- Second\n".to_owned(),
            }],
            truncated: false,
            bytes_read: 0,
        };
        let highlights = bundle.highlights(1);
        assert_eq!(highlights, vec!["First".to_owned()]);
    }

    #[tokio::test]
    async fn renders_compact_instruction_appendix() {
        let repo = tempdir().expect("failed to unwrap");
        std::fs::write(repo.path().join(".git"), "gitdir: /tmp/git").expect("failed to unwrap");
        write_doc(
            repo.path(),
            "- Root summary\n\nFollow the repository-level guidance first.\n",
        )
        .expect("write doc");

        let nested = repo.path().join("nested/sub");
        std::fs::create_dir_all(&nested).expect("failed to unwrap");
        write_doc(
            &nested,
            "- Nested summary\n\nFollow the nested guidance last.\n",
        )
        .expect("write doc");

        let instructions = get_user_instructions(&AgentConfig::default(), &nested, None)
            .await
            .expect("expected instructions");

        assert!(instructions.contains("### Instruction map"));
        assert!(instructions.contains("AGENTS.md (workspace AGENTS)"));
        assert!(instructions.contains("nested/sub/AGENTS.md (workspace AGENTS)"));
        assert!(instructions.contains("Root summary"));
        assert!(instructions.contains("Nested summary"));
        assert!(instructions.contains("### Key points"));
        assert!(instructions.contains("### On-demand loading"));
    }

    #[tokio::test]
    async fn instruction_appendix_includes_persistent_memory_after_authored_guidance() {
        let repo = tempdir().expect("repo");
        std::fs::write(repo.path().join(".git"), "gitdir: /tmp/git").expect("git marker");
        std::fs::write(repo.path().join(".vtcode-project"), "repo").expect("project name");
        write_doc(repo.path(), "root doc").expect("write root doc");

        let memory_dir = repo.path().join(".memory-root");
        let config = AgentConfig {
            persistent_memory: vtcode_config::core::PersistentMemoryConfig {
                enabled: true,
                directory_override: Some(memory_dir.display().to_string()),
                ..Default::default()
            },
            ..Default::default()
        };

        let project_memory_dir = memory_dir.join("projects").join("repo").join("memory");
        std::fs::create_dir_all(&project_memory_dir).expect("memory dir");
        std::fs::write(
            project_memory_dir.join("memory_summary.md"),
            "# VT Code Memory Summary\n\n- remembered detail\n",
        )
        .expect("write memory summary");

        let appendix = build_instruction_appendix(&config, repo.path())
            .await
            .expect("instruction appendix");

        let project_doc_idx = appendix.find("root doc").expect("project doc");
        let memory_idx = appendix.find("remembered detail").expect("memory detail");
        assert!(project_doc_idx < memory_idx);
        assert!(appendix.contains("--- persistent-memory ---"));
        assert!(appendix.contains("### Files"));
        assert!(appendix.contains("### On-demand loading"));
        assert!(appendix.contains("memory_summary.md"));
        assert!(appendix.contains("MEMORY.md"));
        assert!(!appendix.contains("# VT Code Memory Summary"));
    }

    #[tokio::test]
    async fn instruction_appendix_keeps_persistent_memory_compact() {
        let repo = tempdir().expect("repo");
        std::fs::write(repo.path().join(".git"), "gitdir: /tmp/git").expect("git marker");
        std::fs::write(repo.path().join(".vtcode-project"), "repo").expect("project name");

        let memory_dir = repo.path().join(".memory-root");
        let config = AgentConfig {
            persistent_memory: vtcode_config::core::PersistentMemoryConfig {
                enabled: true,
                directory_override: Some(memory_dir.display().to_string()),
                ..Default::default()
            },
            ..Default::default()
        };

        let project_memory_dir = memory_dir.join("projects").join("repo").join("memory");
        std::fs::create_dir_all(&project_memory_dir).expect("memory dir");
        std::fs::write(
            project_memory_dir.join("memory_summary.md"),
            "# VT Code Memory Summary\n\n- keep changes surgical\n- run ./scripts/check.sh\n- use cargo nextest for targeted tests\n- prefer docs/ARCHITECTURE.md for orientation\n- extra detail that should stay out of the prompt body\n",
        )
        .expect("write memory summary");

        let appendix = build_instruction_appendix(&config, repo.path())
            .await
            .expect("instruction appendix");
        let approx_tokens = appendix.len() / 4;

        assert!(appendix.contains("### Key points"));
        assert!(appendix.contains("Open `memory_summary.md` or `MEMORY.md`"));
        assert!(approx_tokens < 120, "got ~{} tokens", approx_tokens);
    }

    #[tokio::test]
    async fn instruction_appendix_stays_summary_sized() {
        let repo = tempdir().expect("repo");
        std::fs::write(repo.path().join(".git"), "gitdir: /tmp/git").expect("git marker");
        write_doc(
            repo.path(),
            "- run ./scripts/check.sh\n- avoid adding to vtcode-core\n- use Conventional Commits\n- start with docs/ARCHITECTURE.md\n",
        )
        .expect("write root doc");

        let appendix = build_instruction_appendix(&AgentConfig::default(), repo.path())
            .await
            .expect("instruction appendix");
        let approx_tokens = appendix.len() / 4;

        assert!(appendix.contains("### Instruction map"));
        assert!(appendix.contains("### Key points"));
        assert!(appendix.contains("### On-demand loading"));
        assert!(approx_tokens < 140, "got ~{} tokens", approx_tokens);
    }
}
