use super::traits::{Tool, ToolResult};
use async_trait::async_trait;
use serde_json::json;
use std::collections::{BTreeSet, VecDeque};
use std::path::{Path, PathBuf};

/// Compact-mode helper for loading a skill's source file on demand.
pub struct ReadSkillTool {
    workspace_dir: PathBuf,
    runtime_skills_dir: Option<PathBuf>,
    allow_scripts: bool,
    open_skills_enabled: bool,
    open_skills_dir: Option<String>,
}

impl ReadSkillTool {
    pub fn new(
        workspace_dir: PathBuf,
        open_skills_enabled: bool,
        open_skills_dir: Option<String>,
    ) -> Self {
        Self {
            workspace_dir,
            runtime_skills_dir: None,
            allow_scripts: false,
            open_skills_enabled,
            open_skills_dir,
        }
    }

    pub fn with_runtime_skills_dir(
        workspace_dir: PathBuf,
        runtime_skills_dir: Option<PathBuf>,
        allow_scripts: bool,
        open_skills_enabled: bool,
        open_skills_dir: Option<String>,
    ) -> Self {
        Self {
            workspace_dir,
            runtime_skills_dir,
            allow_scripts,
            open_skills_enabled,
            open_skills_dir,
        }
    }
}

#[async_trait]
impl Tool for ReadSkillTool {
    fn name(&self) -> &str {
        "read_skill"
    }

    fn description(&self) -> &str {
        "Read the full source file for an available skill by name. Use this in compact skills mode when you need the complete skill instructions without remembering file paths."
    }

    fn parameters_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "The skill name exactly as listed in <available_skills>."
                }
            },
            "required": ["name"]
        })
    }

    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
        let requested = args
            .get("name")
            .and_then(|value| value.as_str())
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .ok_or_else(|| anyhow::anyhow!("Missing 'name' parameter"))?;

        let mut skills = crate::skills::load_skills_with_open_skills_settings(
            &self.workspace_dir,
            self.open_skills_enabled,
            self.open_skills_dir.as_deref(),
        );
        let default_skills_dir = self.workspace_dir.join("skills");
        if let Some(runtime_skills_dir) = &self.runtime_skills_dir {
            if runtime_skills_dir != &default_skills_dir {
                skills.retain(|skill| {
                    skill
                        .location
                        .as_ref()
                        .map(|location| !location.starts_with(&default_skills_dir))
                        .unwrap_or(true)
                });
                skills.extend(crate::skills::load_skills_from_directory(
                    runtime_skills_dir,
                    self.allow_scripts,
                ));
            }
        }

        let Some(skill) = skills
            .iter()
            .find(|skill| skill.name.eq_ignore_ascii_case(requested))
        else {
            let mut names: Vec<&str> = skills.iter().map(|skill| skill.name.as_str()).collect();
            names.sort_unstable();
            let available = if names.is_empty() {
                "none".to_string()
            } else {
                names.join(", ")
            };

            return Ok(ToolResult {
                success: false,
                output: String::new(),
                error: Some(format!(
                    "Unknown skill '{requested}'. Available skills: {available}"
                )),
            });
        };

        let Some(location) = skill.location.as_ref() else {
            return Ok(ToolResult {
                success: false,
                output: String::new(),
                error: Some(format!(
                    "Skill '{}' has no readable source location.",
                    skill.name
                )),
            });
        };

        match read_skill_bundle(location).await {
            Ok(output) => Ok(ToolResult {
                success: true,
                output,
                error: None,
            }),
            Err(err) => Ok(ToolResult {
                success: false,
                output: String::new(),
                error: Some(format!(
                    "Failed to read skill '{}' from {}: {err}",
                    skill.name,
                    location.display()
                )),
            }),
        }
    }
}

pub async fn read_skill_bundle(location: &Path) -> std::io::Result<String> {
    let primary = tokio::fs::read_to_string(location).await?;
    let Some(skill_root) = location.parent() else {
        return Ok(primary);
    };
    let skill_root = skill_root
        .canonicalize()
        .unwrap_or_else(|_| skill_root.to_path_buf());
    let mut output = primary.clone();
    let mut appended = BTreeSet::new();
    let mut queued = BTreeSet::new();
    let mut pending = VecDeque::new();

    enqueue_reference_paths(
        &primary,
        location.parent().unwrap_or(skill_root.as_path()),
        &skill_root,
        &mut queued,
        &mut pending,
    );

    if location.file_name().and_then(|name| name.to_str()) == Some("SKILL.toml") {
        let sibling_markdown = skill_root.join("SKILL.md");
        if sibling_markdown.exists() {
            if let Ok(markdown) = tokio::fs::read_to_string(&sibling_markdown).await {
                output.push_str("\n\n## Referenced File: SKILL.md\n\n");
                output.push_str(&markdown);
                enqueue_reference_paths(
                    &markdown,
                    sibling_markdown.parent().unwrap_or(skill_root.as_path()),
                    &skill_root,
                    &mut queued,
                    &mut pending,
                );
            }
        }
    }

    while let Some(path) = pending.pop_front() {
        let canonical = path.canonicalize().unwrap_or(path.clone());
        if !canonical.starts_with(&skill_root) || !appended.insert(canonical.clone()) {
            continue;
        }

        let Ok(content) = tokio::fs::read_to_string(&canonical).await else {
            continue;
        };
        let relative = canonical
            .strip_prefix(&skill_root)
            .unwrap_or(canonical.as_path())
            .display()
            .to_string()
            .replace('\\', "/");
        output.push_str("\n\n## Referenced File: ");
        output.push_str(&relative);
        output.push_str("\n\n");
        output.push_str(&content);

        enqueue_reference_paths(
            &content,
            canonical.parent().unwrap_or(skill_root.as_path()),
            &skill_root,
            &mut queued,
            &mut pending,
        );
    }

    Ok(output)
}

fn enqueue_reference_paths(
    content: &str,
    base_dir: &Path,
    skill_root: &Path,
    queued: &mut BTreeSet<PathBuf>,
    pending: &mut VecDeque<PathBuf>,
) {
    for candidate in extract_reference_paths(content) {
        for resolved in resolve_reference_candidates(&candidate, base_dir, skill_root) {
            let canonical = resolved.canonicalize().unwrap_or(resolved);
            if !canonical.starts_with(skill_root) || !is_supported_reference_file(&canonical) {
                continue;
            }
            if queued.insert(canonical.clone()) {
                pending.push_back(canonical);
            }
        }
    }
}

fn extract_reference_paths(content: &str) -> Vec<String> {
    let mut paths = Vec::new();

    let mut cursor = content;
    while let Some(start) = cursor.find("](") {
        cursor = &cursor[start + 2..];
        let Some(end) = cursor.find(')') else {
            break;
        };
        let raw = cursor[..end].trim();
        if looks_like_relative_reference_path(raw) {
            paths.push(raw.to_string());
        }
        cursor = &cursor[end + 1..];
    }

    let mut in_backticks = false;
    let mut token = String::new();
    for ch in content.chars() {
        if ch == '`' {
            if in_backticks {
                let raw = token.trim();
                if looks_like_relative_reference_path(raw) {
                    paths.push(raw.to_string());
                }
                token.clear();
            }
            in_backticks = !in_backticks;
            continue;
        }
        if in_backticks {
            token.push(ch);
        }
    }

    paths
}

fn looks_like_relative_reference_path(raw: &str) -> bool {
    if raw.is_empty()
        || raw.starts_with('/')
        || raw.starts_with("http://")
        || raw.starts_with("https://")
        || raw.starts_with('#')
    {
        return false;
    }

    let candidate = raw
        .split('#')
        .next()
        .unwrap_or(raw)
        .split('?')
        .next()
        .unwrap_or(raw);
    let path = Path::new(candidate);
    if path
        .components()
        .any(|component| matches!(component, std::path::Component::ParentDir))
    {
        return false;
    }

    candidate.contains('/') && is_supported_reference_file(path)
}

fn is_supported_reference_file(path: &Path) -> bool {
    matches!(
        path.extension().and_then(|value| value.to_str()),
        Some("md" | "txt" | "json" | "html" | "toml" | "yaml" | "yml" | "csv")
    )
}

fn resolve_reference_candidates(raw: &str, base_dir: &Path, skill_root: &Path) -> Vec<PathBuf> {
    let mut candidates = vec![base_dir.join(raw)];
    let skill_root_candidate = skill_root.join(raw);
    if skill_root_candidate != candidates[0] {
        candidates.push(skill_root_candidate);
    }
    candidates
}

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

    fn make_tool(tmp: &TempDir) -> ReadSkillTool {
        ReadSkillTool::new(tmp.path().join("workspace"), false, None)
    }

    #[tokio::test]
    async fn reads_markdown_skill_by_name() {
        let tmp = TempDir::new().unwrap();
        let skill_dir = tmp.path().join("workspace/skills/weather");
        std::fs::create_dir_all(&skill_dir).unwrap();
        std::fs::write(
            skill_dir.join("SKILL.md"),
            "# Weather\n\nUse this skill for forecast lookups.\n",
        )
        .unwrap();

        let result = make_tool(&tmp)
            .execute(json!({ "name": "weather" }))
            .await
            .unwrap();

        assert!(result.success);
        assert!(result.output.contains("# Weather"));
        assert!(result.output.contains("forecast lookups"));
    }

    #[tokio::test]
    async fn reads_toml_skill_manifest_by_name() {
        let tmp = TempDir::new().unwrap();
        let skill_dir = tmp.path().join("workspace/skills/deploy");
        std::fs::create_dir_all(&skill_dir).unwrap();
        std::fs::write(
            skill_dir.join("SKILL.toml"),
            r#"[skill]
name = "deploy"
description = "Ship safely"
"#,
        )
        .unwrap();

        let result = make_tool(&tmp)
            .execute(json!({ "name": "deploy" }))
            .await
            .unwrap();

        assert!(result.success);
        assert!(result.output.contains("[skill]"));
        assert!(result.output.contains("Ship safely"));
    }

    #[tokio::test]
    async fn unknown_skill_lists_available_names() {
        let tmp = TempDir::new().unwrap();
        let skill_dir = tmp.path().join("workspace/skills/weather");
        std::fs::create_dir_all(&skill_dir).unwrap();
        std::fs::write(skill_dir.join("SKILL.md"), "# Weather\n").unwrap();

        let result = make_tool(&tmp)
            .execute(json!({ "name": "calendar" }))
            .await
            .unwrap();

        assert!(!result.success);
        assert_eq!(
            result.error.as_deref(),
            Some("Unknown skill 'calendar'. Available skills: weather")
        );
    }

    #[tokio::test]
    async fn inlines_markdown_reference_files_for_skill_context() {
        let tmp = TempDir::new().unwrap();
        let skill_dir = tmp.path().join("workspace/skills/zhihu-hotlist");
        let refs_dir = skill_dir.join("references");
        std::fs::create_dir_all(&refs_dir).unwrap();
        std::fs::write(
            skill_dir.join("SKILL.md"),
            concat!(
                "# Zhihu Hotlist\n\n",
                "Follow [collection-flow.md](references/collection-flow.md).\n",
                "Apply [data-quality.md](references/data-quality.md).\n",
            ),
        )
        .unwrap();
        std::fs::write(
            refs_dir.join("collection-flow.md"),
            "# Collection Flow\n\nCollect rows from the hotlist first.\n",
        )
        .unwrap();
        std::fs::write(
            refs_dir.join("data-quality.md"),
            "# Data Quality\n\nMark partial metrics explicitly.\n",
        )
        .unwrap();

        let result = make_tool(&tmp)
            .execute(json!({ "name": "zhihu-hotlist" }))
            .await
            .unwrap();

        assert!(result.success);
        assert!(result.output.contains("# Zhihu Hotlist"));
        assert!(result
            .output
            .contains("## Referenced File: references/collection-flow.md"));
        assert!(result
            .output
            .contains("Collect rows from the hotlist first."));
        assert!(result
            .output
            .contains("## Referenced File: references/data-quality.md"));
        assert!(result.output.contains("Mark partial metrics explicitly."));
    }
}
