use crate::fs_utils::atomic_write_with_mode;
use crate::sdd::project::config::{load_config, write_config};
use crate::sdd::shared::constants::{LLMANSPEC_DIR_NAME, SPEC_FILE};
use crate::sdd::spec::backend::{BACKEND, SpecBackend};
use crate::sdd::spec::ir::{MainSpecDoc, RequirementEntry, ScenarioEntry};
use anyhow::{Result, anyhow};
use regex::Regex;
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub struct ImportArgs {
    pub source: Option<PathBuf>,
    pub scope: Option<String>,
    pub dry_run: bool,
    pub force: bool,
    pub no_interactive: bool,
}

#[derive(Debug, Clone)]
struct ParsedScenario {
    id: String,
    given: String,
    when_: String,
    then_: String,
}

#[derive(Debug, Clone)]
struct ParsedRequirement {
    title: String,
    statement: String,
    scenarios: Vec<ParsedScenario>,
}

#[derive(Debug, Clone)]
struct ParsedSpec {
    name: String,
    purpose: String,
    requirements: Vec<ParsedRequirement>,
}

#[derive(Debug)]
struct MigrationResult {
    name: String,
    status: MigrationStatus,
    req_count: usize,
    scenario_count: usize,
    errors: Vec<String>,
    reason: String,
}

#[derive(Debug, PartialEq)]
enum MigrationStatus {
    Ok,
    Partial,
    Error,
    Skip,
}

impl std::fmt::Display for MigrationStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Ok => write!(f, "ok"),
            Self::Partial => write!(f, "partial"),
            Self::Error => write!(f, "error"),
            Self::Skip => write!(f, "skip"),
        }
    }
}

pub fn run(root: &Path, args: ImportArgs) -> Result<()> {
    let source = args
        .source
        .unwrap_or_else(|| root.join("openspec").join("specs"));
    let target = root.join(LLMANSPEC_DIR_NAME).join("specs");

    if !source.exists() {
        return Err(anyhow!(
            "source directory does not exist: {}",
            source.display()
        ));
    }
    if !target.parent().is_some_and(|p| p.exists()) {
        return Err(anyhow!(
            "target parent directory does not exist: {} (run `llman sdd init` first)",
            target.display()
        ));
    }

    let mut spec_dirs: Vec<PathBuf> = fs::read_dir(&source)?
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| p.is_dir() && p.join("spec.md").exists())
        .collect();
    spec_dirs.sort();

    if let Some(ref scope) = args.scope {
        let pattern = glob::Pattern::new(scope)
            .map_err(|e| anyhow!("invalid --scope glob pattern `{scope}`: {e}"))?;
        spec_dirs.retain(|d| {
            d.file_name()
                .and_then(|n| n.to_str())
                .is_some_and(|n| pattern.matches(n))
        });
    }

    if args.dry_run {
        println!("Mode: DRY RUN (no files will be written)");
    }
    println!("Source: {}", source.display());
    println!("Target: {}", target.display());

    let openspec_config = source.parent().map(|p| p.join("config.yaml"));
    if let Some(ref cfg_path) = openspec_config
        && cfg_path.exists()
    {
        match migrate_config(root, cfg_path, args.dry_run) {
            Ok(true) => println!("Config: merged context from openspec/config.yaml"),
            Ok(false) => println!("Config: no context to merge"),
            Err(e) => println!("Config: merge failed ({e}), skipping"),
        }
    }

    println!("Found {} specs to migrate\n", spec_dirs.len());

    let mut ok_count = 0usize;
    let mut partial_count = 0usize;
    let mut error_count = 0usize;
    let mut skip_count = 0usize;
    let mut total_reqs = 0usize;
    let mut total_scenarios = 0usize;
    let mut failed: Vec<MigrationResult> = Vec::new();
    let mut partial: Vec<MigrationResult> = Vec::new();

    for (i, spec_dir) in spec_dirs.iter().enumerate() {
        let dir_name = spec_dir
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("unknown");
        print!("[{:3}/{}] {}... ", i + 1, spec_dirs.len(), dir_name);

        let spec_file = spec_dir.join("spec.md");
        let text = match fs::read_to_string(&spec_file) {
            Ok(t) => t,
            Err(e) => {
                let r = MigrationResult {
                    name: dir_name.to_string(),
                    status: MigrationStatus::Error,
                    req_count: 0,
                    scenario_count: 0,
                    errors: vec![],
                    reason: format!("read failed: {e}"),
                };
                println!("{} ({})", r.status, r.reason);
                error_count += 1;
                failed.push(r);
                continue;
            }
        };

        let mut parsed = parse_openspec_md(&text);
        if parsed.name.is_empty() {
            parsed.name = dir_name.to_string();
        }

        let target_dir = target.join(dir_name);
        let result = migrate_spec(&parsed, &target_dir, args.dry_run, args.force);

        println!(
            "{} (reqs={}, scenarios={})",
            result.status, result.req_count, result.scenario_count
        );
        if !result.errors.is_empty() {
            for err in &result.errors {
                println!("         ! {err}");
            }
        }

        total_reqs += result.req_count;
        total_scenarios += result.scenario_count;

        match result.status {
            MigrationStatus::Ok => ok_count += 1,
            MigrationStatus::Partial => {
                partial_count += 1;
                partial.push(result);
            }
            MigrationStatus::Error => {
                error_count += 1;
                failed.push(result);
            }
            MigrationStatus::Skip => skip_count += 1,
        }
    }

    println!("\n{}", "=".repeat(60));
    println!("Migration summary:");
    println!("  OK:      {ok_count}");
    println!("  Partial: {partial_count}");
    println!("  Error:   {error_count}");
    println!("  Skip:    {skip_count}");
    println!("\n  Total requirements: {total_reqs}");
    println!("  Total scenarios:    {total_scenarios}");

    if !failed.is_empty() {
        println!("\nFailed specs:");
        for r in &failed {
            println!("  - {}: {}", r.name, r.reason);
        }
    }

    if !partial.is_empty() {
        println!("\nPartially migrated specs:");
        for r in &partial {
            println!("  - {}: {} error(s)", r.name, r.errors.len());
            for err in &r.errors {
                println!("      {err}");
            }
        }
    }

    if !args.dry_run && (ok_count > 0 || partial_count > 0) {
        println!("\nNext steps:");
        println!("  1. Run: llman sdd validate --all --strict --no-interactive");
        println!("  2. Fix any validation errors in partial specs");
        println!("  3. Review and commit the migrated specs");
    }

    Ok(())
}

fn migrate_spec(
    spec: &ParsedSpec,
    target_dir: &Path,
    dry_run: bool,
    force: bool,
) -> MigrationResult {
    let target_file = target_dir.join(SPEC_FILE);
    if target_file.exists() && !force {
        return MigrationResult {
            name: spec.name.clone(),
            status: MigrationStatus::Skip,
            req_count: 0,
            scenario_count: 0,
            errors: vec![],
            reason: "already exists".into(),
        };
    }

    let total_scenarios: usize = spec.requirements.iter().map(|r| r.scenarios.len()).sum();

    if dry_run {
        return MigrationResult {
            name: spec.name.clone(),
            status: MigrationStatus::Ok,
            req_count: spec.requirements.len(),
            scenario_count: total_scenarios,
            errors: vec![],
            reason: String::new(),
        };
    }

    let mut errors: Vec<String> = Vec::new();
    let mut requirements = Vec::new();
    let mut scenarios = Vec::new();
    let mut written_scenarios = 0usize;

    for (i, req) in spec.requirements.iter().enumerate() {
        let req_id = format!("r{}", i + 1);
        let title = truncate(&req.title, 80);
        let mut statement = truncate(&req.statement, 500);

        if !contains_shall_or_must(&statement) {
            statement = format!("System MUST {statement}");
        }

        if statement.starts_with("- ") {
            errors.push(format!(
                "req {req_id} ({title}): statement starts with '- ', may cause issues"
            ));
        }

        requirements.push(RequirementEntry {
            req_id: req_id.clone(),
            title,
            statement,
        });

        if req.scenarios.is_empty() {
            scenarios.push(ScenarioEntry {
                req_id: req_id.clone(),
                id: "baseline".into(),
                given: String::new(),
                when_: "TODO: describe the trigger".into(),
                then_: "TODO: describe the expected result".into(),
            });
            written_scenarios += 1;
        } else {
            for sc in &req.scenarios {
                scenarios.push(ScenarioEntry {
                    req_id: req_id.clone(),
                    id: sc.id.clone(),
                    given: sc.given.clone(),
                    when_: if sc.when_.is_empty() {
                        "condition is met".into()
                    } else {
                        sc.when_.clone()
                    },
                    then_: if sc.then_.is_empty() {
                        "expected behavior occurs".into()
                    } else {
                        sc.then_.clone()
                    },
                });
                written_scenarios += 1;
            }
        }
    }

    let doc = MainSpecDoc {
        kind: "llman.sdd.spec".into(),
        name: spec.name.clone(),
        purpose: if spec.purpose.is_empty() {
            "TBD".into()
        } else {
            truncate(&spec.purpose, 200)
        },
        valid_scope: vec!["src/".to_string(), "tests/".to_string()],
        requirements,
        scenarios,
        feature_refs: None,
    };

    let content = match BACKEND.dump_main_spec(&doc) {
        Ok(p) => p,
        Err(e) => {
            return MigrationResult {
                name: spec.name.clone(),
                status: MigrationStatus::Error,
                req_count: spec.requirements.len(),
                scenario_count: 0,
                errors: vec![],
                reason: format!("TOON serialization failed: {e}"),
            };
        }
    };

    if let Some(parent) = target_file.parent()
        && let Err(e) = fs::create_dir_all(parent)
    {
        return MigrationResult {
            name: spec.name.clone(),
            status: MigrationStatus::Error,
            req_count: spec.requirements.len(),
            scenario_count: 0,
            errors: vec![],
            reason: format!("mkdir failed: {e}"),
        };
    }

    if let Err(e) = atomic_write_with_mode(&target_file, content.as_bytes(), None) {
        return MigrationResult {
            name: spec.name.clone(),
            status: MigrationStatus::Error,
            req_count: spec.requirements.len(),
            scenario_count: 0,
            errors: vec![],
            reason: format!("write failed: {e}"),
        };
    }

    let status = if errors.is_empty() {
        MigrationStatus::Ok
    } else {
        MigrationStatus::Partial
    };

    MigrationResult {
        name: spec.name.clone(),
        status,
        req_count: spec.requirements.len(),
        scenario_count: written_scenarios,
        errors,
        reason: String::new(),
    }
}

fn migrate_config(root: &Path, openspec_config_path: &Path, dry_run: bool) -> Result<bool> {
    let content = fs::read_to_string(openspec_config_path)?;
    let openspec_val: serde_yaml::Value = serde_yaml::from_str(&content)?;
    let openspec_map = openspec_val
        .as_mapping()
        .ok_or_else(|| anyhow!("not a mapping"))?;

    let context = openspec_map
        .get(serde_yaml::Value::String("context".into()))
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());

    let rules: Option<std::collections::BTreeMap<String, Vec<String>>> = openspec_map
        .get(serde_yaml::Value::String("rules".into()))
        .and_then(|v| serde_yaml::from_value(v.clone()).ok());

    if context.is_none() && rules.is_none() {
        return Ok(false);
    }

    if dry_run {
        return Ok(true);
    }

    let llmanspec_dir = root.join(LLMANSPEC_DIR_NAME);
    let mut config = load_config(&llmanspec_dir)?
        .ok_or_else(|| anyhow!("llmanspec config not found (run `llman sdd init` first)"))?;

    if config.context.is_none() {
        config.context = context;
    }
    if config.rules.is_none() {
        config.rules = rules;
    }

    write_config(&llmanspec_dir, &config)?;
    Ok(true)
}

fn parse_openspec_md(text: &str) -> ParsedSpec {
    let mut spec = ParsedSpec {
        name: String::new(),
        purpose: String::new(),
        requirements: Vec::new(),
    };

    let name_re = Regex::new(r"^#\s+(\S+)\s+Specification").expect("regex");
    if let Some(caps) = name_re.captures(text) {
        spec.name = caps[1].to_string();
    }

    let purpose_re = Regex::new(r"(?s)##\s+Purpose\s*\n(.*?)(?:\n##\s|\z)").expect("regex");
    if let Some(caps) = purpose_re.captures(text) {
        let raw = caps[1].trim();
        let bold_re = Regex::new(r"\*\*.*?\*\*\s*").expect("regex");
        let cleaned = bold_re.replace_all(raw, "");
        let lines: Vec<&str> = cleaned
            .lines()
            .map(|l| l.trim())
            .filter(|l| !l.is_empty())
            .collect();
        spec.purpose = if lines.is_empty() {
            "TBD".into()
        } else {
            truncate(&lines.join(" "), 300)
        };
    }

    let req_header_re = Regex::new(r"(?m)^###\s+Requirement:\s*(.+)$").expect("regex");
    let req_positions: Vec<(usize, String)> = req_header_re
        .captures_iter(text)
        .map(|c| {
            let m = c.get(0).unwrap();
            let title = c[1].trim().to_string();
            (m.end(), title)
        })
        .collect();

    let section_end_re = Regex::new(r"(?m)^##\s").expect("regex");

    for (idx, (body_start, title)) in req_positions.iter().enumerate() {
        let body_end = if let Some(next) = req_positions.get(idx + 1) {
            text[..next.0].rfind("\n###").unwrap_or(next.0)
        } else if let Some(m) = section_end_re.find(&text[*body_start..]) {
            body_start + m.start()
        } else {
            text.len()
        };

        let body = text[*body_start..body_end].trim();

        let before_scenarios: &str = body.split("\n#### Scenario:").next().unwrap_or("");
        let mut stmt_lines = Vec::new();
        for line in before_scenarios.lines() {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }
            if trimmed.starts_with("## ") || trimmed.starts_with("### ") {
                break;
            }
            stmt_lines.push(trimmed);
        }
        let statement = if stmt_lines.is_empty() {
            title.clone()
        } else {
            stmt_lines.join(" ")
        };

        let scenarios = parse_scenarios(body);

        spec.requirements.push(ParsedRequirement {
            title: title.clone(),
            statement,
            scenarios,
        });
    }

    spec
}

fn parse_scenarios(req_body: &str) -> Vec<ParsedScenario> {
    let header_re = Regex::new(r"(?m)^####\s+Scenario:\s*(.+)$").expect("regex");
    let positions: Vec<(usize, String)> = header_re
        .captures_iter(req_body)
        .map(|c| {
            let m = c.get(0).unwrap();
            let name = c[1].trim().to_string();
            (m.end(), name)
        })
        .collect();

    let mut scenarios = Vec::new();
    let mut seen_ids: HashSet<String> = HashSet::new();

    for (idx, (body_start, sc_name)) in positions.iter().enumerate() {
        let body_end = if let Some(next) = positions.get(idx + 1) {
            req_body[..next.0].rfind("\n####").unwrap_or(next.0)
        } else {
            req_body.len()
        };

        let sc_body = req_body[*body_start..body_end].trim();
        let (given, when_, then_) = extract_gherkin(sc_body);
        let mut sc_id = slugify(sc_name, 60);
        if seen_ids.contains(&sc_id) {
            sc_id = format!("{}-{}", sc_id, seen_ids.len());
        }
        seen_ids.insert(sc_id.clone());
        scenarios.push(ParsedScenario {
            id: sc_id,
            given,
            when_,
            then_,
        });
    }

    scenarios
}

fn extract_gherkin(body: &str) -> (String, String, String) {
    let given_re =
        Regex::new(r"(?si)\*\*(?:GIVEN|Given)\*\*\s*(.*?)(?:\n\s*-\s*\*\*|\z)").expect("regex");
    let when_re =
        Regex::new(r"(?si)\*\*(?:WHEN|When)\*\*\s*(.*?)(?:\n\s*-\s*\*\*|\z)").expect("regex");
    let then_re =
        Regex::new(r"(?si)\*\*(?:THEN|Then)\*\*\s*(.*?)(?:\n\s*-\s*\*\*|\z)").expect("regex");

    let given = given_re
        .captures(body)
        .map(|c| clean_gherkin(&c[1]))
        .unwrap_or_default();
    let mut when_ = when_re
        .captures(body)
        .map(|c| clean_gherkin(&c[1]))
        .unwrap_or_default();
    let mut then_ = then_re
        .captures(body)
        .map(|c| clean_gherkin(&c[1]))
        .unwrap_or_default();

    if when_.is_empty() && then_.is_empty() {
        let lines: Vec<&str> = body
            .lines()
            .map(|l| l.trim())
            .filter(|l| !l.is_empty())
            .collect();
        let combined = lines.join(" ");
        when_ = truncate(&combined, 200);
        if when_.is_empty() {
            when_ = "condition is met".into();
        }
        then_ = "expected behavior occurs".into();
    }

    if when_.is_empty() {
        when_ = "condition is met".into();
    }
    if then_.is_empty() {
        then_ = "expected behavior occurs".into();
    }

    (given, when_, then_)
}

fn clean_gherkin(text: &str) -> String {
    let and_re = Regex::new(r"\*\*(?:AND|and|And)\*\*\s*").expect("regex");
    let bullet_start = Regex::new(r"^\s*-\s*").expect("regex");
    let mut lines = Vec::new();
    for line in text.trim().lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("- **") {
            break;
        }
        let cleaned = bullet_start.replace(trimmed, "");
        let cleaned = and_re.replace_all(&cleaned, "");
        let cleaned = cleaned.trim().to_string();
        if !cleaned.is_empty() {
            lines.push(cleaned);
        }
    }
    let result = lines.join(" ");
    let ws_re = Regex::new(r"\s+").expect("regex");
    ws_re.replace_all(result.trim(), " ").to_string()
}

fn slugify(name: &str, max_len: usize) -> String {
    let lower = name.to_lowercase();
    let slug_re = Regex::new(r"[^a-z0-9\u{4e00}-\u{9fff}]+").expect("regex");
    let slug = slug_re.replace_all(&lower, "-");
    let slug = slug.trim_matches('-');
    if slug.is_empty() {
        return "default".into();
    }
    truncate(slug, max_len)
}

fn truncate(s: &str, max_len: usize) -> String {
    if s.len() <= max_len {
        s.to_string()
    } else {
        s.chars().take(max_len).collect()
    }
}

fn contains_shall_or_must(text: &str) -> bool {
    text.contains("SHALL") || text.contains("MUST")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_simple_openspec_md() {
        let md = r#"# my-feature Specification
## Purpose
Define how my-feature works.
## Requirements
### Requirement: Basic operation
The CLI MUST support basic operation with standard input.
#### Scenario: Happy path
- **WHEN** the user runs the command
- **THEN** the system produces expected output
### Requirement: Error handling
The CLI MUST return an error for invalid input.
#### Scenario: Invalid input
- **GIVEN** the user provides invalid data
- **WHEN** the command is executed
- **THEN** the system returns a descriptive error
"#;

        let parsed = parse_openspec_md(md);
        assert_eq!(parsed.name, "my-feature");
        assert!(parsed.purpose.contains("my-feature works"));
        assert_eq!(parsed.requirements.len(), 2);
        assert_eq!(parsed.requirements[0].title, "Basic operation");
        assert_eq!(parsed.requirements[0].scenarios.len(), 1);
        assert_eq!(parsed.requirements[0].scenarios[0].id, "happy-path");
        assert_eq!(
            parsed.requirements[1].scenarios[0].given,
            "the user provides invalid data"
        );
    }

    #[test]
    fn slugify_names() {
        assert_eq!(slugify("Happy path", 60), "happy-path");
        assert_eq!(
            slugify("CLI override provided", 60),
            "cli-override-provided"
        );
        assert_eq!(slugify("", 60), "default");
    }

    #[test]
    fn truncate_long_string() {
        assert_eq!(truncate("hello", 3), "hel");
        assert_eq!(truncate("hi", 10), "hi");
    }

    #[test]
    fn extract_gherkin_full() {
        let body = r#"- **GIVEN** precondition exists
- **WHEN** user triggers action
- **THEN** system responds correctly"#;
        let (g, w, t) = extract_gherkin(body);
        assert_eq!(g, "precondition exists");
        assert_eq!(w, "user triggers action");
        assert_eq!(t, "system responds correctly");
    }

    #[test]
    fn extract_gherkin_missing_given() {
        let body = r#"- **WHEN** user triggers action
- **THEN** system responds correctly"#;
        let (g, w, t) = extract_gherkin(body);
        assert!(g.is_empty());
        assert_eq!(w, "user triggers action");
        assert_eq!(t, "system responds correctly");
    }
}
