//! Integrator — generates Corvus-standard SKILL.md (with YAML frontmatter) from scout results.

use std::fs;
use std::path::PathBuf;

use anyhow::{bail, Context, Result};
use tracing::info;

use super::scout::ScoutResult;

// ---------------------------------------------------------------------------
// Integrator
// ---------------------------------------------------------------------------

pub struct Integrator {
    output_dir: PathBuf,
}

impl Integrator {
    pub fn new(output_dir: String) -> Self {
        Self {
            output_dir: PathBuf::from(output_dir),
        }
    }

    /// Write SKILL.md (with YAML frontmatter) for the given candidate.
    pub fn integrate(&self, candidate: &ScoutResult) -> Result<PathBuf> {
        let safe_name = sanitize_path_component(&candidate.name)?;
        let skill_dir = self.output_dir.join(&safe_name);
        fs::create_dir_all(&skill_dir)
            .with_context(|| format!("Failed to create dir: {}", skill_dir.display()))?;

        let md_path = skill_dir.join("SKILL.md");
        let md_content = self.generate_md(candidate);

        fs::write(&md_path, &md_content)
            .with_context(|| format!("Failed to write {}", md_path.display()))?;

        info!(
            skill = candidate.name.as_str(),
            path = %skill_dir.display(),
            "Integrated skill"
        );

        Ok(skill_dir)
    }

    // -- Generators ---------------------------------------------------------

    fn generate_md(&self, c: &ScoutResult) -> String {
        let lang = yaml_sanitize(c.language.as_deref().unwrap_or("unknown"));
        let tags_yaml = format!("  - discovered\n  - {lang}");
        format!(
            r#"---
name: {name}
description: {description}
version: 0.1.0
author: {owner}
tags:
{tags}
---

# {title}

> Auto-generated by SkillForge

{raw_description}

## Overview

- **Source**: [{url}]({url})
- **Owner**: {raw_owner}
- **Language**: {lang}
- **Stars**: {stars}
- **License**: {license}

## Usage

```bash
corvus skills install {url} --trust
```

## Notes

This manifest was auto-generated from repository metadata.
Review before enabling in production.
"#,
            name = yaml_sanitize(&c.name),
            description = yaml_sanitize(&c.description),
            owner = yaml_sanitize(&c.owner),
            tags = tags_yaml,
            title = c.name,
            raw_description = c.description,
            url = c.url,
            raw_owner = c.owner,
            lang = lang,
            stars = c.stars,
            license = if c.has_license { "yes" } else { "unknown" },
        )
    }
}

/// Sanitize a string for safe inclusion in YAML frontmatter.
/// The skills frontmatter parser only strips outer double quotes —
/// it does NOT handle backslash escapes. So we remove problematic
/// characters instead of escaping them.
fn yaml_sanitize(s: &str) -> String {
    let sanitized: String = s
        .chars()
        .filter(|&c| c != '\n' && c != '\r')
        .map(|c| if c == '"' { '\'' } else { c })
        .collect();
    // Quote if contains YAML-special chars
    if sanitized.contains(':') || sanitized.contains('#') {
        format!("\"{}\"", sanitized)
    } else {
        sanitized
    }
}

/// Sanitize a string for use as a single path component.
/// Rejects empty names, "..", and names containing path separators or NUL.
fn sanitize_path_component(name: &str) -> Result<String> {
    let trimmed = name.trim().trim_matches('.');
    if trimmed.is_empty() {
        bail!("Skill name is empty or only dots after sanitization");
    }
    let sanitized: String = trimmed
        .chars()
        .map(|c| match c {
            '/' | '\\' | '\0' => '_',
            _ => c,
        })
        .collect();
    if sanitized == ".." || sanitized.contains('/') || sanitized.contains('\\') {
        bail!("Skill name '{}' is unsafe as a path component", name);
    }
    Ok(sanitized)
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::skillforge::scout::{ScoutResult, ScoutSource};
    use chrono::Utc;
    use std::fs;

    fn sample_candidate() -> ScoutResult {
        ScoutResult {
            name: "test-skill".into(),
            url: "https://github.com/user/test-skill".into(),
            description: "A test skill for unit tests".into(),
            stars: 42,
            language: Some("Rust".into()),
            updated_at: Some(Utc::now()),
            source: ScoutSource::GitHub,
            owner: "user".into(),
            has_license: true,
        }
    }

    #[test]
    fn integrate_creates_skill_md_with_frontmatter() {
        let tmp =
            std::env::temp_dir().join(format!("corvus-test-integrate-{}", std::process::id()));
        let _ = fs::remove_dir_all(&tmp);

        let integrator = Integrator::new(tmp.to_string_lossy().into_owned());
        let c = sample_candidate();
        let path = integrator.integrate(&c).unwrap();

        // SKILL.toml must NOT be generated
        assert!(!path.join("SKILL.toml").exists());
        assert!(path.join("SKILL.md").exists());

        let md = fs::read_to_string(path.join("SKILL.md")).unwrap();

        // YAML frontmatter fields
        assert!(md.starts_with("---\n"));
        assert!(md.contains("name: test-skill"));
        assert!(md.contains("description: A test skill for unit tests"));
        assert!(md.contains("version: 0.1.0"));
        assert!(md.contains("author: user"));
        assert!(md.contains("  - discovered"));
        assert!(md.contains("  - Rust"));

        // Body content
        assert!(md.contains("# test-skill"));
        assert!(md.contains("A test skill for unit tests"));

        // Parse frontmatter to validate round-trip
        let fm = crate::skills::frontmatter::parse_frontmatter(&md);
        assert_eq!(fm.name.as_deref(), Some("test-skill"));
        assert_eq!(
            fm.description.as_deref(),
            Some("A test skill for unit tests"),
        );
        assert_eq!(fm.version.as_deref(), Some("0.1.0"));
        assert_eq!(fm.author.as_deref(), Some("user"));
        assert!(fm.tags.contains(&"discovered".to_string()));
        assert!(fm.tags.contains(&"Rust".to_string()));

        let _ = fs::remove_dir_all(&tmp);
    }

    #[test]
    fn sanitize_rejects_traversal() {
        assert!(sanitize_path_component("..").is_err());
        assert!(sanitize_path_component("...").is_err());
        assert!(sanitize_path_component("").is_err());
        assert!(sanitize_path_component("  ").is_err());
    }

    #[test]
    fn sanitize_replaces_separators() {
        let s = sanitize_path_component("foo/bar\\baz\0qux").unwrap();
        assert!(!s.contains('/'));
        assert!(!s.contains('\\'));
        assert!(!s.contains('\0'));
        assert_eq!(s, "foo_bar_baz_qux");
    }

    #[test]
    fn sanitize_trims_dots() {
        let s = sanitize_path_component(".hidden.").unwrap();
        assert_eq!(s, "hidden");
    }

    #[test]
    fn integrate_handles_special_chars_in_description() {
        let tmp = std::env::temp_dir().join(format!(
            "corvus-test-integrate-special-{}",
            std::process::id(),
        ));
        let _ = fs::remove_dir_all(&tmp);

        let integrator = Integrator::new(tmp.to_string_lossy().into_owned());
        let mut c = sample_candidate();
        c.description = "A skill with: colons and \"quotes\"".into();
        let path = integrator.integrate(&c).unwrap();

        let md = fs::read_to_string(path.join("SKILL.md")).unwrap();
        let fm = crate::skills::frontmatter::parse_frontmatter(&md);
        // Description should be parseable (quotes replaced with single quotes)
        assert!(fm.description.is_some());

        let _ = fs::remove_dir_all(&tmp);
    }
}
