//! Helpers for managing AGENTS.md Kanbus instructions.

use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::{self, Write};
use std::path::Path;

#[cfg(tarpaulin)]
use crate::config::default_project_configuration;
use crate::config_loader::load_project_configuration;
use crate::error::KanbusError;
use crate::file_io::get_configuration_path;
use crate::models::ProjectConfiguration;
use crate::project_management_template::{
    DEFAULT_PROJECT_MANAGEMENT_TEMPLATE, DEFAULT_PROJECT_MANAGEMENT_TEMPLATE_FILENAME,
};
use serde::Serialize;

const KANBUS_SECTION_HEADER: &str = "## Project management with Kanbus";
const KANBUS_SECTION_LINES: [&str; 9] = [
    KANBUS_SECTION_HEADER,
    "",
    "Use Kanbus for task management.",
    "Why: Kanbus task management is MANDATORY here; every task must live in Kanbus.",
    "When: Create/update the Kanbus task before coding; close it only after the change lands.",
    "How: See CONTRIBUTING_AGENT.md for the Kanbus workflow, hierarchy, status rules, priorities, command examples, and the sins to avoid. Never inspect project/ or issue JSON directly (including with cat or jq); use Kanbus commands only.",
    "Performance: Prefer kanbusr (Rust) when available; kanbus (Python) is equivalent but slower.",
    "Warning: Editing project/ directly is a sin against The Way. Do not read or write anything in project/; work only through Kanbus.",
    "",
];
const AGENTS_HEADER_LINES: [&str; 2] = ["# Agent Instructions", ""];
const PROJECT_MANAGEMENT_FILENAME: &str = "CONTRIBUTING_AGENT.md";

#[derive(Debug, Clone)]
struct SectionMatch {
    start: usize,
    end: usize,
}

/// Ensure AGENTS.md exists and contains the Kanbus section.
///
/// # Arguments
/// * `root` - Repository root path
/// * `force` - Overwrite existing Kanbus section without prompting
///
/// # Errors
/// Returns `KanbusError::IssueOperation` if overwrite is required but not confirmed.
pub fn ensure_agents_file(root: &Path, force: bool) -> Result<(), KanbusError> {
    let instructions_text = build_project_management_text(root)?;
    let agents_path = root.join("AGENTS.md");
    if !agents_path.exists() {
        let content = build_new_agents_file();
        fs::write(&agents_path, content).map_err(|error| KanbusError::Io(error.to_string()))?;
        ensure_project_management_file(root, force, &instructions_text)?;
        ensure_project_guard_files(root)?;
        return Ok(());
    }

    let contents =
        fs::read_to_string(&agents_path).map_err(|error| KanbusError::Io(error.to_string()))?;
    let lines: Vec<String> = contents.lines().map(|line| line.to_string()).collect();
    let sections = find_kanbus_sections(&lines);
    if let Some(section) = sections.first() {
        if !force && !confirm_overwrite()? {
            ensure_project_management_file(root, force, &instructions_text)?;
            ensure_project_guard_files(root)?;
            return Ok(());
        }
        let updated = replace_sections(&lines, &sections, section, &KANBUS_SECTION_LINES);
        fs::write(&agents_path, updated).map_err(|error| KanbusError::Io(error.to_string()))?;
        ensure_project_management_file(root, force, &instructions_text)?;
        ensure_project_guard_files(root)?;
        return Ok(());
    }

    let updated = insert_kanbus_section(&lines, &KANBUS_SECTION_LINES);
    fs::write(&agents_path, updated).map_err(|error| KanbusError::Io(error.to_string()))?;
    ensure_project_management_file(root, force, &instructions_text)?;
    ensure_project_guard_files(root)?;
    Ok(())
}

/// Return the canonical Kanbus section text.
pub fn kanbus_section_text() -> String {
    let lines = KANBUS_SECTION_LINES
        .iter()
        .map(|value| value.to_string())
        .collect::<Vec<_>>();
    join_lines(&lines)
}

/// Return the Kanbus project management text derived from configuration.
///
/// # Arguments
/// * `root` - Repository root path
///
/// # Errors
/// Returns `KanbusError` if configuration lookup fails.
pub fn project_management_text(root: &Path) -> Result<String, KanbusError> {
    build_project_management_text(root)
}

fn build_project_management_text(root: &Path) -> Result<String, KanbusError> {
    let configuration_path = get_configuration_path(root)?;
    let configuration = load_project_configuration(&configuration_path)?;
    let template_path = resolve_project_management_template_path(root, &configuration)?;
    let template_text = match template_path {
        Some(path) => {
            std::fs::read_to_string(&path).map_err(|error| KanbusError::Io(error.to_string()))?
        }
        None => DEFAULT_PROJECT_MANAGEMENT_TEMPLATE.to_string(),
    };
    let context = build_project_management_context(&configuration);
    let env = minijinja::Environment::new();
    env.render_str(&template_text, context)
        .map_err(|error| KanbusError::IssueOperation(error.to_string()))
}

fn build_new_agents_file() -> String {
    let mut lines: Vec<&str> = Vec::new();
    lines.extend(AGENTS_HEADER_LINES);
    lines.extend(KANBUS_SECTION_LINES);
    join_lines(
        &lines
            .iter()
            .map(|value| value.to_string())
            .collect::<Vec<_>>(),
    )
}

#[derive(Debug, Serialize)]
struct WorkflowContext {
    name: String,
    statuses: Vec<WorkflowStatusContext>,
}

#[derive(Debug, Serialize)]
struct WorkflowStatusContext {
    name: String,
    transitions: Vec<String>,
}

#[derive(Debug, Serialize)]
struct PriorityContext {
    value: u8,
    name: String,
}

#[derive(Debug, Serialize)]
struct SemanticReleaseMapping {
    r#type: String,
    category: String,
}

#[derive(Debug, Serialize)]
struct ProjectManagementContext {
    project_key: String,
    hierarchy_order: String,
    non_hierarchical_types: Vec<String>,
    parent_child_rules: Vec<String>,
    initial_status: String,
    workflows: Vec<WorkflowContext>,
    priorities: Vec<PriorityContext>,
    default_priority_value: u8,
    default_priority_name: String,
    command_examples: Vec<String>,
    semantic_release_mapping: Vec<SemanticReleaseMapping>,
    has_story: bool,
    gherkin_example: Vec<String>,
}

fn resolve_project_management_template_path(
    root: &Path,
    configuration: &ProjectConfiguration,
) -> Result<Option<std::path::PathBuf>, KanbusError> {
    if let Some(path) = configuration.project_management_template.as_ref() {
        let resolved = if std::path::Path::new(path).is_absolute() {
            std::path::PathBuf::from(path)
        } else {
            root.join(path)
        };
        if !resolved.exists() {
            return Err(KanbusError::IssueOperation(format!(
                "project management template not found: {}",
                resolved.display()
            )));
        }
        return Ok(Some(resolved));
    }
    let conventional = root.join(DEFAULT_PROJECT_MANAGEMENT_TEMPLATE_FILENAME);
    if conventional.exists() {
        return Ok(Some(conventional));
    }
    Ok(None)
}

fn build_project_management_context(
    configuration: &ProjectConfiguration,
) -> ProjectManagementContext {
    let hierarchy = &configuration.hierarchy;
    let types = &configuration.types;
    let workflows = build_workflow_context(&configuration.workflows);
    let priorities = build_priority_context(&configuration.priorities);
    let default_priority_name = configuration
        .priorities
        .get(&configuration.default_priority)
        .map(|definition| definition.name.clone())
        .unwrap_or_else(|| configuration.default_priority.to_string());
    ProjectManagementContext {
        project_key: configuration.project_key.clone(),
        hierarchy_order: if hierarchy.is_empty() {
            "none".to_string()
        } else {
            hierarchy.join(" -> ")
        },
        non_hierarchical_types: types.clone(),
        parent_child_rules: build_parent_child_rules(hierarchy, types),
        initial_status: configuration.initial_status.clone(),
        workflows,
        priorities,
        default_priority_value: configuration.default_priority,
        default_priority_name,
        command_examples: build_command_examples(configuration),
        semantic_release_mapping: build_semantic_release_mapping(types),
        has_story: types.iter().any(|value| value.to_lowercase() == "story"),
        gherkin_example: vec![
            "Feature:".to_string(),
            "Scenario:".to_string(),
            "Given".to_string(),
            "When".to_string(),
            "Then".to_string(),
        ],
    }
}

fn build_parent_child_rules(hierarchy: &[String], types: &[String]) -> Vec<String> {
    let mut rules = Vec::new();
    if hierarchy.len() > 1 {
        for index in 1..hierarchy.len() {
            let child = &hierarchy[index];
            let parent = &hierarchy[index - 1];
            rules.push(format!("{child} can have parent {parent}."));
        }
    }
    if !types.is_empty() {
        let parents = if hierarchy.len() > 1 {
            hierarchy[..hierarchy.len() - 1].join(", ")
        } else {
            String::new()
        };
        if parents.is_empty() {
            rules.push(format!("{} cannot have parents.", types.join(", ")));
        } else {
            rules.push(format!("{} can have parent {}.", types.join(", "), parents));
        }
    }
    if hierarchy.len() <= 1 && types.is_empty() {
        rules.push("No parent-child relationships are defined.".to_string());
    }
    rules
}

fn build_workflow_context(
    workflows: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
) -> Vec<WorkflowContext> {
    let mut context = Vec::new();
    for (workflow_name, workflow) in workflows {
        let mut statuses = Vec::new();
        for (status, transitions) in workflow {
            statuses.push(WorkflowStatusContext {
                name: status.clone(),
                transitions: transitions.clone(),
            });
        }
        context.push(WorkflowContext {
            name: workflow_name.clone(),
            statuses,
        });
    }
    context
}

fn build_priority_context(
    priorities: &BTreeMap<u8, crate::models::PriorityDefinition>,
) -> Vec<PriorityContext> {
    let mut context = Vec::new();
    for (value, definition) in priorities {
        context.push(PriorityContext {
            value: *value,
            name: definition.name.clone(),
        });
    }
    context
}

fn build_command_examples(configuration: &ProjectConfiguration) -> Vec<String> {
    let hierarchy = &configuration.hierarchy;
    let types = &configuration.types;
    let priority_example = configuration
        .priorities
        .keys()
        .next()
        .copied()
        .unwrap_or(configuration.default_priority);
    let workflow_name = if configuration.workflows.contains_key("default") {
        "default".to_string()
    } else {
        configuration
            .workflows
            .keys()
            .next()
            .cloned()
            .unwrap_or_else(|| "default".to_string())
    };
    let empty_workflow = BTreeMap::new();
    let workflow = configuration
        .workflows
        .get(&workflow_name)
        .unwrap_or(&empty_workflow);
    let status_example = select_status_example(&configuration.initial_status, workflow);
    let status_set = collect_statuses(workflow);
    let mut lines = Vec::new();
    if let Some(top) = hierarchy.first() {
        lines.push(format!("kanbus create \"Plan the roadmap\" --type {top}"));
    }
    if hierarchy.len() > 1 {
        lines.push(format!(
            "kanbus create \"Release v1\" --type {} --parent <{}-id>",
            hierarchy[1], hierarchy[0]
        ));
    }
    if hierarchy.len() > 2 {
        lines.push(format!(
            "kanbus create \"Implement feature\" --type {} --parent <{}-id>",
            hierarchy[2], hierarchy[1]
        ));
    }
    if let Some(issue_type) = types.first() {
        let parent = if hierarchy.len() > 1 {
            Some(&hierarchy[1])
        } else {
            None
        };
        let parent_fragment = parent
            .map(|value| format!(" --parent <{value}-id>"))
            .unwrap_or_default();
        lines.push(format!(
            "kanbus create \"Fix crash on launch\" --type {issue_type} --priority {priority_example}{parent_fragment}"
        ));
    }
    lines.push(format!(
        "kanbus update <id> --status {status_example} --assignee \"you@example.com\""
    ));
    if status_set.contains("blocked") && status_example != "blocked" {
        lines.push("kanbus update <id> --status blocked".to_string());
    }
    lines.push("kanbus comment <id> \"Progress note\"".to_string());
    lines.push(format!(
        "kanbus list --status {}",
        configuration.initial_status
    ));
    lines.push("kanbus close <id> --comment \"Summary of the change\"".to_string());
    lines
}

fn build_semantic_release_mapping(types: &[String]) -> Vec<SemanticReleaseMapping> {
    let mut mapping = Vec::new();
    for issue_type in types {
        let lowered = issue_type.to_lowercase();
        let category = if lowered.contains("bug") || lowered.contains("fix") {
            "fix"
        } else if lowered.contains("story") || lowered.contains("feature") {
            "feat"
        } else {
            "chore"
        };
        mapping.push(SemanticReleaseMapping {
            r#type: issue_type.clone(),
            category: category.to_string(),
        });
    }
    mapping
}

fn collect_statuses(workflow: &BTreeMap<String, Vec<String>>) -> BTreeSet<String> {
    let mut statuses = BTreeSet::new();
    for (status, transitions) in workflow {
        statuses.insert(status.clone());
        for transition in transitions {
            statuses.insert(transition.clone());
        }
    }
    statuses
}

fn select_status_example(initial_status: &str, workflow: &BTreeMap<String, Vec<String>>) -> String {
    if let Some(transitions) = workflow.get(initial_status) {
        if let Some(value) = transitions.first() {
            return value.clone();
        }
    }
    for transitions in workflow.values() {
        if let Some(value) = transitions.first() {
            return value.clone();
        }
    }
    initial_status.to_string()
}

fn ensure_project_management_file(
    root: &Path,
    force: bool,
    content: &str,
) -> Result<(), KanbusError> {
    let instructions_path = root.join(PROJECT_MANAGEMENT_FILENAME);
    if instructions_path.exists() && !force {
        return Ok(());
    }
    fs::write(&instructions_path, content).map_err(|error| KanbusError::Io(error.to_string()))?;
    Ok(())
}

fn ensure_project_guard_files(root: &Path) -> Result<(), KanbusError> {
    let project_dir = root.join("project");
    if !project_dir.exists() {
        return Ok(());
    }
    let project_agents = project_dir.join("AGENTS.md");
    let project_agents_content = [
        "# DO NOT EDIT HERE",
        "",
        "Editing anything under project/ directly is hacking the data and is a sin against The Way.",
        "Do not read or write in this folder. Use Kanbus commands instead.",
        "",
        "See ../AGENTS.md and ../CONTRIBUTING_AGENT.md for required process.",
    ]
    .join("\n")
        + "\n";
    fs::write(&project_agents, project_agents_content)
        .map_err(|error| KanbusError::Io(error.to_string()))?;

    let do_not_edit = project_dir.join("DO_NOT_EDIT");
    let do_not_edit_content = [
        "DO NOT EDIT ANYTHING IN project/",
        "This folder is guarded by The Way.",
        "All changes must go through Kanbus (see ../AGENTS.md and ../CONTRIBUTING_AGENT.md).",
    ]
    .join("\n")
        + "\n";
    fs::write(&do_not_edit, do_not_edit_content).map_err(|error| KanbusError::Io(error.to_string()))
}

fn confirm_overwrite() -> Result<bool, KanbusError> {
    print!("Kanbus section already exists in AGENTS.md. Overwrite it? [y/N] ");
    io::stdout()
        .flush()
        .map_err(|error| KanbusError::Io(error.to_string()))?;
    let mut input = String::new();
    let bytes = io::stdin()
        .read_line(&mut input)
        .map_err(|error| KanbusError::Io(error.to_string()))?;
    if bytes == 0 {
        return Err(KanbusError::IssueOperation(
            "Kanbus section already exists in AGENTS.md. Re-run with --force to overwrite."
                .to_string(),
        ));
    }
    let response = input.trim().to_lowercase();
    Ok(response == "y" || response == "yes")
}

fn find_kanbus_sections(lines: &[String]) -> Vec<SectionMatch> {
    let mut sections = Vec::new();
    for (index, line) in lines.iter().enumerate() {
        if let Some((level, text)) = parse_header(line) {
            if text.to_lowercase().contains("kanbus") {
                let end = find_section_end(lines, index + 1, level);
                sections.push(SectionMatch { start: index, end });
            }
        }
    }
    sections
}

fn find_section_end(lines: &[String], start: usize, level: usize) -> usize {
    for (index, line) in lines.iter().enumerate().skip(start) {
        if let Some((next_level, _)) = parse_header(line) {
            if next_level <= level {
                return index;
            }
        }
    }
    lines.len()
}

fn parse_header(line: &str) -> Option<(usize, String)> {
    let trimmed = line.trim_end_matches(&['\r', '\n'][..]);
    if !trimmed.starts_with('#') {
        return None;
    }
    let mut count = 0;
    for ch in trimmed.chars() {
        if ch == '#' {
            count += 1;
        } else {
            break;
        }
    }
    if count == 0 || count > 6 {
        return None;
    }
    let rest = &trimmed[count..];
    if !rest.starts_with(' ') && !rest.starts_with('\t') {
        return None;
    }
    let text = rest.trim();
    if text.is_empty() {
        return None;
    }
    Some((count, text.to_string()))
}

#[cfg(tarpaulin)]
pub fn cover_parse_header_cases() {
    let _ = parse_header("plain text");
    let _ = parse_header("####### too many");
    let _ = parse_header("#NoSpace");
    let _ = parse_header("# ");
    let _ = parse_header("# Header");
    let _ = parse_header("##\tTabbed");
}

#[cfg(tarpaulin)]
pub fn cover_agents_management_paths(root: &Path) {
    let mut configuration = default_project_configuration();
    configuration.project_management_template = Some("missing-template.md".to_string());
    let _ = resolve_project_management_template_path(root, &configuration);
    let absolute_template = root.join("absolute-template.md");
    let _ = fs::write(&absolute_template, "template");
    configuration.project_management_template =
        Some(absolute_template.to_string_lossy().to_string());
    let _ = resolve_project_management_template_path(root, &configuration);
    configuration.project_management_template = None;
    let _ = resolve_project_management_template_path(root, &configuration);
    let conventional = root.join(DEFAULT_PROJECT_MANAGEMENT_TEMPLATE_FILENAME);
    let _ = fs::write(&conventional, "template");
    let _ = resolve_project_management_template_path(root, &configuration);

    let _ = build_parent_child_rules(&[], &["bug".to_string()]);
    let _ = build_parent_child_rules(&[], &[]);

    let mut empty_workflows = configuration.clone();
    empty_workflows.workflows = BTreeMap::new();
    let _ = build_command_examples(&empty_workflows);
    let mut single_level = configuration.clone();
    single_level.hierarchy = vec!["task".to_string()];
    single_level.types = vec!["bug".to_string()];
    let _ = build_command_examples(&single_level);

    let mut transitions = BTreeMap::new();
    transitions.insert("open".to_string(), Vec::new());
    transitions.insert("done".to_string(), vec!["closed".to_string()]);
    let _ = select_status_example("open", &transitions);
    let _ = select_status_example("open", &BTreeMap::new());

    let _ = find_section_end(&[String::from("No headers")], 0, 1);
    let _ = parse_header("# ");
    let _ = replace_sections(
        &[String::from("# Header")],
        &[],
        &SectionMatch { start: 1, end: 1 },
        &["## Project management with Kanbus"],
    );
    let _ = find_insert_index(&[String::from("No header here")]);

    let instructions = root.join(PROJECT_MANAGEMENT_FILENAME);
    let _ = fs::write(&instructions, "content");
    let _ = ensure_project_management_file(root, false, "content");
}

fn replace_sections(
    lines: &[String],
    sections: &[SectionMatch],
    primary: &SectionMatch,
    section_lines: &[&str],
) -> String {
    let mut updated = Vec::new();
    let mut inserted = false;
    for (index, line) in lines.iter().enumerate() {
        if is_in_sections(index, sections) {
            if index == primary.start && !inserted {
                updated.extend(section_lines.iter().map(|value| value.to_string()));
                inserted = true;
            }
            continue;
        }
        updated.push(line.clone());
    }
    if !inserted {
        updated.extend(section_lines.iter().map(|value| value.to_string()));
    }
    join_lines(&updated)
}

fn is_in_sections(index: usize, sections: &[SectionMatch]) -> bool {
    sections
        .iter()
        .any(|section| index >= section.start && index < section.end)
}

fn insert_kanbus_section(lines: &[String], section_lines: &[&str]) -> String {
    let mut updated: Vec<String> = lines.to_vec();
    let mut insert_index = find_insert_index(lines);
    if insert_index > 0 && insert_index < updated.len() && !updated[insert_index].trim().is_empty()
    {
        updated.insert(insert_index, String::new());
        insert_index += 1;
    }
    let section_strings = section_lines.iter().map(|value| value.to_string());
    updated.splice(insert_index..insert_index, section_strings);
    join_lines(&updated)
}

fn find_insert_index(lines: &[String]) -> usize {
    for (index, line) in lines.iter().enumerate() {
        if let Some((level, _)) = parse_header(line) {
            if level == 1 {
                let mut insert_index = index + 1;
                while insert_index < lines.len() && lines[insert_index].trim().is_empty() {
                    insert_index += 1;
                }
                return insert_index;
            }
        }
    }
    0
}

fn join_lines(lines: &[String]) -> String {
    let mut output = lines.join("\n");
    output.push('\n');
    output
}
