use std::collections::HashMap;
use std::path::Path;

use crate::agents::bundle::{AgentBundle, AgentEntry};
use indoc::indoc;

/// Default instructions template used when no custom template is supplied.
pub const DEFAULT_INSTRUCTIONS_TEMPLATE: &str = indoc! {r#"
    # {HEADER}

    **Auto-generated by memex — Do not edit manually**
    Generated: {GENERATED} | Git: {GIT_HASH}

    ---

    ## Instructions for AI Assistants

    ### Document Overview

    This file contains **summaries only** from `{DOCS_SRC}/agents/`. Each summary includes:
    - **Source path** - Location of the full file
    - **Read when** - Scenarios where this doc is relevant
    - **Contains** - Key topics covered
    - **Size** - Approximate line count and token estimate

    When a summary indicates relevance to your task, read the complete source file using the path provided.

    ### File Organization

    - `{DOCS_SRC}/agents/` - Agent-editable documentation (you can modify these)
    - `{DOCS_SRC}/api/` - Auto-generated from rustdoc (read-only, see API Reference in SUMMARY)
    - `{DOCS_SRC}/` (other files) - Human-authored (don't edit unless explicitly asked)

    ### Editing Guidelines

    **You may freely create and modify files in `{DOCS_SRC}/agents/`** to maintain documentation.
    Avoid touching other areas unless a maintainer explicitly requests it.

    #### Required Format for Agent Documentation

    Every file in `{DOCS_SRC}/agents/` MUST begin with an `<ai>` block:

    ```xml
    <ai>
    <read_when>
    Describe when this doc is relevant (e.g., "Working with authentication" or "Debugging database queries")
    </read_when>
    <contains>
    List key topics (can use markdown lists, multiple lines)
    </contains>
    <related_files>
    Optional: Related source files or docs
    </related_files>
    </ai>
    ```

    This block is automatically converted to a markdown summary during build.

    #### Best Practices for <ai> blocks

    - **Be concise** - Summaries are loaded into every agent context
    - **Link, don't duplicate** - Reference rustdoc or detailed specs instead of copying
    - **Focus on decisions** - Architecture choices, integration points, workflows
    - **Flag drift** - Note when human-owned docs may be outdated rather than editing them

    **After editing, regenerate this file:** `cargo xtask ai-doc`

    ---
"#};

/// Rendering configuration for the default markdown generator.
#[derive(Debug, Clone)]
pub struct DefaultRenderOptions {
    /// Title inserted into the instructions header (defaults to `"AI Agents Documentation"`).
    pub header: String,
    /// Human-readable timestamp used to populate the `{GENERATED}` placeholder.
    pub generated: Option<String>,
    /// Short git identifier substituted into the `{GIT_HASH}` placeholder.
    pub git_hash: Option<String>,
    /// Optional `SUMMARY.md` fragment rendered beneath the instructions block.
    pub doc_structure: Option<String>,
    /// Path displayed alongside the documentation structure fragment.
    pub doc_structure_source: Option<String>,
    /// Repository tree snapshot included when `include_tree` is enabled.
    pub repo_tree: Option<String>,
    /// Display-friendly root path used when expanding `{DOCS_SRC}`.
    pub docs_root_display: String,
    /// Additional placeholder overrides applied after bundle-provided values.
    pub format_overrides: HashMap<String, String>,
}

impl Default for DefaultRenderOptions {
    /// Create a `DefaultRenderOptions` instance with the built-in header and no optional sections.
    fn default() -> Self {
        Self {
            header: "AI Agents Documentation".to_string(),
            generated: None,
            git_hash: None,
            doc_structure: None,
            doc_structure_source: None,
            repo_tree: None,
            docs_root_display: String::new(),
            format_overrides: HashMap::new(),
        }
    }
}

/// Render an [`AgentBundle`] into markdown using the default layout.
pub fn render_default_markdown(bundle: &AgentBundle, opts: &DefaultRenderOptions) -> String {
    let template = bundle
        .template
        .as_deref()
        .unwrap_or(DEFAULT_INSTRUCTIONS_TEMPLATE);

    let instructions = render_instructions(template, bundle, opts);

    let mut output = String::new();
    output.push_str(instructions.trim_end());
    output.push_str("\n\n");

    if let Some(doc_structure) = &opts.doc_structure {
        output.push_str("## Documentation Structure\n\n");
        if let Some(source) = &opts.doc_structure_source {
            output.push_str(&format!("**Source:** `{}`\n\n", source));
        }
        output.push_str(doc_structure.trim_end());
        output.push_str("\n\n---\n\n");
    }

    if bundle.options.include_tree {
        if let Some(tree) = &opts.repo_tree {
            output.push_str("## Repository Directory Structure\n\n```\n");
            output.push_str(tree.trim_end());
            output.push_str("\n```\n\n---\n\n");
        }
    }

    if bundle.options.include_summary {
        output.push_str("## Agent Documentation Summaries\n\n");
        if bundle.entries.is_empty() {
            output.push_str("_No agent documentation files found._\n\n---\n\n");
        } else {
            for entry in &bundle.entries {
                output.push_str(&format_summary_entry(bundle, entry));
                output.push_str("\n---\n\n");
            }
        }
    }

    output.trim_end().to_string()
}

/// Expand a template string with bundle-provided and override placeholders.
fn render_instructions(
    template: &str,
    bundle: &AgentBundle,
    opts: &DefaultRenderOptions,
) -> String {
    let mut variables = bundle.format.clone();
    variables
        .entry("HEADER".to_string())
        .or_insert_with(|| opts.header.clone());
    let docs_src_value = if !opts.docs_root_display.is_empty() {
        opts.docs_root_display.clone()
    } else {
        bundle.docs_root_display.clone()
    };
    variables
        .entry("DOCS_SRC".to_string())
        .or_insert(docs_src_value);
    variables.insert(
        "GENERATED".to_string(),
        opts.generated
            .clone()
            .unwrap_or_else(|| "unknown".to_string()),
    );
    variables.insert(
        "GIT_HASH".to_string(),
        opts.git_hash
            .clone()
            .unwrap_or_else(|| "unknown".to_string()),
    );

    for (key, value) in &opts.format_overrides {
        variables.insert(key.clone(), value.clone());
    }

    apply_template(template, &variables)
}

/// Render a single agent entry summary section for inclusion in the bundle.
fn format_summary_entry(bundle: &AgentBundle, entry: &AgentEntry) -> String {
    let mut summary = String::new();
    let rel = normalize_slashes(&entry.relative_path);
    let full_path = if bundle.source_dir_display.is_empty() {
        rel
    } else {
        format!("{}/{}", bundle.source_dir_display, rel)
    };
    summary.push_str(&format!("### File: {}\n\n", full_path));
    summary.push_str(&format_field("Read this file when", &entry.read_when));
    summary.push('\n');
    summary.push_str(&format_field("Contains", &entry.contains));

    if let Some(related) = &entry.related_files {
        summary.push('\n');
        summary.push_str(&format_field("Related files", related));
    }

    summary.push('\n');
    summary.push_str(&format!(
        "**Size:** ~{} lines (~{} tokens)\n",
        entry.content_size.non_empty_lines, entry.content_size.estimated_tokens
    ));
    summary
}

/// Format a labeled field, preserving multi-line and list formatting.
fn format_field(label: &str, content: &str) -> String {
    let trimmed = content.trim();
    if trimmed.is_empty() {
        return format!("**{label}:**\n");
    }

    if trimmed.starts_with('-') || trimmed.contains('\n') {
        format!("**{label}:**\n{}\n", trimmed)
    } else {
        format!("**{label}:** {}\n", trimmed)
    }
}

/// Apply simple placeholder substitution (e.g. `{TOKEN}`) to a template string.
fn apply_template(template: &str, values: &HashMap<String, String>) -> String {
    let mut rendered = template.to_string();
    for (key, value) in values {
        let placeholder = format!("{{{key}}}");
        rendered = rendered.replace(&placeholder, value);
    }
    rendered
}

/// Render a relative path using `/` as the separator.
fn normalize_slashes(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::agents::bundle::{AgentBundle, AgentBundleOptions, AgentEntry, ContentSize};
    use std::collections::HashMap;
    use std::path::PathBuf;

    fn sample_bundle() -> AgentBundle {
        AgentBundle {
            template: None,
            format: HashMap::new(),
            entries: vec![AgentEntry {
                relative_path: PathBuf::from("auth.md"),
                read_when: "When handling auth".to_string(),
                contains: "- JWT flow".to_string(),
                related_files: Some("crates/auth/src/lib.rs".to_string()),
                content_size: ContentSize {
                    non_empty_lines: 42,
                    estimated_tokens: 1000,
                },
            }],
            options: AgentBundleOptions {
                include_summary: true,
                include_tree: true,
                tree_excludes: Vec::new(),
            },
            source_dir: PathBuf::from("docs/src/agents"),
            source_dir_display: "docs/src/agents".to_string(),
            docs_root_display: "docs/src".to_string(),
        }
    }

    #[test]
    fn render_includes_sections() {
        let bundle = sample_bundle();
        let opts = DefaultRenderOptions {
            generated: Some("2025-01-01".to_string()),
            git_hash: Some("abc123".to_string()),
            doc_structure: Some("- [Overview](docs/src/overview.md)".to_string()),
            doc_structure_source: Some("docs/src/SUMMARY.md".to_string()),
            repo_tree: Some(".\n├── docs".to_string()),
            ..Default::default()
        };

        let markdown = render_default_markdown(&bundle, &opts);
        assert!(markdown.contains("## Documentation Structure"));
        assert!(markdown.contains("## Repository Directory Structure"));
        assert!(markdown.contains("## Agent Documentation Summaries"));
        assert!(markdown.contains("### File: docs/src/agents/auth.md"));
        assert!(markdown.contains("**Size:** ~42 lines (~1000 tokens)"));
    }

    #[test]
    fn render_handles_empty_entries() {
        let mut bundle = sample_bundle();
        bundle.entries.clear();
        let opts = DefaultRenderOptions::default();
        let markdown = render_default_markdown(&bundle, &opts);
        assert!(markdown.contains("_No agent documentation files found._"));
    }

    #[test]
    fn render_respects_format_overrides() {
        let mut bundle = sample_bundle();
        bundle.template = Some("Hello {NAME}".to_string());
        bundle.format.insert("NAME".to_string(), "Base".to_string());
        let mut opts = DefaultRenderOptions::default();
        opts.format_overrides
            .insert("NAME".to_string(), "Override".to_string());
        let markdown = render_default_markdown(&bundle, &opts);
        assert!(markdown.starts_with("Hello Override"));
    }
}
