use std::collections::{BTreeMap, BTreeSet};

use config_source::{
    ConfigSchemaContribution, MarkdownDocumentProjection, VirtualPath, project_markdown_document,
};
use serde::Deserialize;
use worker::skill::{
    SkillActivationResponse, SkillCatalogEntry, SkillCatalogResponse, SkillDetailResponse,
    SkillDiagnostic, SkillDiagnosticSeverity, SkillProvenance, SkillResourceRef, SkillSourceKind,
};

use crate::config_source::{
    WorkspaceConfigSchemaProvider, WorkspaceConfigState, evaluate_workspace_config_state,
};

const BUILTIN_SKILL_ID: &str = "agent-skills";
const BUILTIN_SKILL_SOURCE: &str = include_str!("../../../resources/skills/agent-skills/SKILL.md");
const BUILTIN_SKILL_VIRTUAL_PATH: &str = "builtin/skills/agent-skills/SKILL.md";
const SKILL_SCHEMA_PROVIDER_ID: &str = "builtin:skills";
const SKILL_SCHEMA_NAMESPACE: &str = "skills";
const SKILL_SCHEMA_VERSION: &str = "1";
const SKILL_CATALOG_AUTHORITY: &str = "workspace-config-skills-v1";

/// Skill documents are values imported from `SKILL.md`. Known Agent Skills
/// frontmatter is typed while extension keys remain concrete values.
pub const SKILL_DOCUMENT_SCHEMA_SOURCE: &str = r#"{
    frontmatter = {
        name = String;
        description = String;
        license = String default "";
        compatibility = String default "";
        metadata = {...String} default {};
        ...Unknown
    };
    content = String;
}"#;

const SKILL_CONFIG_SCHEMA_SOURCE: &str = r#"{
    skills = {...{
        frontmatter = {
            name = String;
            description = String;
            license = String default "";
            compatibility = String default "";
            metadata = {...String} default {};
            ...Unknown
        };
        content = String;
    }} default {};
}"#;

#[derive(Debug, Clone, Copy)]
pub struct SkillConfigSchemaProvider;

impl WorkspaceConfigSchemaProvider for SkillConfigSchemaProvider {
    fn contribution(&self) -> crate::Result<ConfigSchemaContribution> {
        ConfigSchemaContribution::new(
            SKILL_SCHEMA_PROVIDER_ID,
            SKILL_SCHEMA_NAMESPACE,
            SKILL_SCHEMA_VERSION,
            SKILL_CONFIG_SCHEMA_SOURCE,
        )
        .map_err(|error| crate::Error::Config(error.to_string()))
    }
}

#[derive(Debug, thiserror::Error)]
pub enum SkillError {
    #[error("unknown Skill `{0}`")]
    NotFound(String),
    #[error("Skill `{0}` has blocking diagnostics")]
    InvalidSkill(String),
    #[error("failed to evaluate the active Workspace config revision: {0}")]
    Evaluation(String),
    #[error("the active Workspace config projection is missing its root value")]
    MissingProjection,
    #[error("the active Workspace config Skill projection is invalid: {0}")]
    InvalidProjection(String),
}

#[derive(Debug, Clone)]
struct ParsedSkill {
    name: String,
    description: String,
    allowed_tools: Vec<String>,
    body: String,
    provenance: SkillProvenance,
    overrides: Vec<SkillProvenance>,
    resources: Vec<SkillResourceRef>,
    diagnostics: Vec<SkillDiagnostic>,
}

#[derive(Debug, Deserialize)]
struct WorkspaceSkillProjection {
    #[serde(default)]
    skills: BTreeMap<String, MarkdownDocumentProjection>,
}

pub fn catalog(state: &WorkspaceConfigState) -> Result<SkillCatalogResponse, SkillError> {
    let entries = merged_skills(state)?
        .into_values()
        .map(|skill| skill.catalog_entry())
        .collect();
    Ok(SkillCatalogResponse {
        authority: SKILL_CATALOG_AUTHORITY.to_string(),
        entries,
        diagnostics: Vec::new(),
    })
}

pub fn lint(state: &WorkspaceConfigState) -> Result<SkillCatalogResponse, SkillError> {
    catalog(state)
}

pub fn detail(state: &WorkspaceConfigState, name: &str) -> Result<SkillDetailResponse, SkillError> {
    let skill = merged_skills(state)?
        .remove(name)
        .ok_or_else(|| SkillError::NotFound(name.to_string()))?;
    Ok(SkillDetailResponse {
        name: skill.name,
        description: skill.description,
        provenance: skill.provenance,
        overrides: skill.overrides,
        diagnostics: skill.diagnostics,
        body: skill.body,
        allowed_tools: skill.allowed_tools,
        allowed_tools_status: "experimental_hint_only".to_string(),
        resources: skill.resources,
    })
}

pub fn activation(
    state: &WorkspaceConfigState,
    name: &str,
) -> Result<SkillActivationResponse, SkillError> {
    let skill = merged_skills(state)?
        .remove(name)
        .ok_or_else(|| SkillError::NotFound(name.to_string()))?;
    if skill.has_errors() {
        return Err(SkillError::InvalidSkill(name.to_string()));
    }
    Ok(SkillActivationResponse {
        name: skill.name,
        provenance: skill.provenance,
        diagnostics: skill.diagnostics,
        body: skill.body,
    })
}

fn merged_skills(
    state: &WorkspaceConfigState,
) -> Result<BTreeMap<String, ParsedSkill>, SkillError> {
    let mut merged = BTreeMap::new();
    let builtin_projection = project_markdown_document(BUILTIN_SKILL_SOURCE)
        .expect("embedded built-in Skill Markdown is valid");
    let builtin = parse_skill(
        BUILTIN_SKILL_ID,
        builtin_projection,
        SkillProvenance {
            kind: SkillSourceKind::Builtin,
            id: format!("builtin:{BUILTIN_SKILL_ID}"),
            virtual_path: Some(BUILTIN_SKILL_VIRTUAL_PATH.to_string()),
            revision: None,
            source_digest: Some(config_source::digest_bytes(BUILTIN_SKILL_SOURCE.as_bytes())),
            tree_digest: None,
        },
        Vec::new(),
        None,
    );
    merged.insert(builtin.name.clone(), builtin);

    let evaluation = evaluate_workspace_config_state(state, state.contract.schema_bundle.clone())
        .map_err(|error| SkillError::Evaluation(error.to_string()))?;
    let projection = evaluation
        .projections
        .first()
        .ok_or(SkillError::MissingProjection)?;
    let workspace =
        serde_json::from_value::<WorkspaceSkillProjection>(projection.data_json.clone())
            .map_err(|error| SkillError::InvalidProjection(error.to_string()))?;

    for (config_key, document) in workspace.skills {
        let name = string_field(&document.frontmatter, "name")
            .unwrap_or(&config_key)
            .to_string();
        let canonical_path = format!("skills/{name}/SKILL.md");
        let mut source_diagnostic = None;
        let source_entry = VirtualPath::parse(&canonical_path)
            .ok()
            .and_then(|path| state.snapshot.entries.get(&path));
        if let Some(entry) = source_entry {
            match project_markdown_document(&entry.content) {
                Ok(expected) if normalize_document(expected.clone()) == document => {}
                Ok(_) => {
                    source_diagnostic = Some(SkillDiagnostic::error(
                        "skill_source_mismatch",
                        format!(
                            "Skill `{name}` must be the imported value of `{canonical_path}` in the active config revision"
                        ),
                        Some(format!("workspace:{name}")),
                    ));
                }
                Err(message) => {
                    source_diagnostic = Some(SkillDiagnostic::error(
                        "invalid_skill_markdown",
                        format!("{canonical_path}: {message}"),
                        Some(format!("workspace:{name}")),
                    ));
                }
            }
        } else {
            source_diagnostic = Some(SkillDiagnostic::error(
                "missing_skill_source",
                format!("Skill `{name}` requires `{canonical_path}` in the active config revision"),
                Some(format!("workspace:{name}")),
            ));
        }
        let source_digest = source_entry
            .map(|entry| entry.content_digest.clone())
            .unwrap_or_else(|| config_source::digest_bytes(canonical_path.as_bytes()));
        let resources = workspace_resources(state, &name);
        let mut skill = parse_skill(
            &name,
            document,
            SkillProvenance {
                kind: SkillSourceKind::Workspace,
                id: format!("workspace:{name}"),
                virtual_path: Some(canonical_path),
                revision: Some(state.snapshot.revision),
                source_digest: Some(source_digest),
                tree_digest: Some(state.snapshot.digest.clone()),
            },
            resources,
            source_diagnostic,
        );
        if let Some(overridden) = merged.insert(name, skill.clone()) {
            skill.overrides.push(overridden.provenance);
            merged.insert(skill.name.clone(), skill);
        }
    }
    Ok(merged)
}

fn normalize_document(mut document: MarkdownDocumentProjection) -> MarkdownDocumentProjection {
    document
        .frontmatter
        .entry("license")
        .or_insert_with(|| serde_json::Value::String(String::new()));
    document
        .frontmatter
        .entry("compatibility")
        .or_insert_with(|| serde_json::Value::String(String::new()));
    document
        .frontmatter
        .entry("metadata")
        .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
    document
}

fn parse_skill(
    fallback_name: &str,
    document: MarkdownDocumentProjection,
    provenance: SkillProvenance,
    resources: Vec<SkillResourceRef>,
    source_diagnostic: Option<SkillDiagnostic>,
) -> ParsedSkill {
    let mut diagnostics = Vec::new();
    if let Some(diagnostic) = source_diagnostic {
        diagnostics.push(diagnostic);
    }
    let frontmatter = document.frontmatter;
    let name = string_field(&frontmatter, "name").unwrap_or(fallback_name);
    if !valid_skill_name(name) {
        diagnostics.push(SkillDiagnostic::error(
            "invalid_skill_name",
            "frontmatter `name` must be a lowercase kebab-case Skill id",
            Some(provenance.id.clone()),
        ));
    }
    if name != fallback_name {
        diagnostics.push(SkillDiagnostic::error(
            "skill_name_mismatch",
            format!("frontmatter name `{name}` must match Skill id `{fallback_name}`"),
            Some(provenance.id.clone()),
        ));
    }
    let description = string_field(&frontmatter, "description")
        .unwrap_or_default()
        .trim()
        .to_string();
    if description.is_empty() {
        diagnostics.push(SkillDiagnostic::error(
            "missing_description",
            "frontmatter `description` must be a non-empty string",
            Some(provenance.id.clone()),
        ));
    }
    for field in [
        "profile",
        "system_prompt",
        "prompt",
        "plugins",
        "plugin",
        "model_invokation",
        "model_invocation",
        "user_invocable",
        "graph",
        "invocation",
    ] {
        if frontmatter.contains_key(field) {
            diagnostics.push(SkillDiagnostic::error(
                "workflow_authority",
                format!("frontmatter `{field}` is workflow/profile authority and is not allowed"),
                Some(provenance.id.clone()),
            ));
        }
    }
    let allowed_tools = frontmatter
        .get("allowed-tools")
        .map(parse_allowed_tools)
        .unwrap_or_default();
    if !allowed_tools.is_empty() {
        diagnostics.push(SkillDiagnostic::warning(
            "allowed_tools_hint",
            "frontmatter `allowed-tools` is an instruction hint only and does not grant tools",
            Some(provenance.id.clone()),
        ));
    }
    ParsedSkill {
        name: fallback_name.to_string(),
        description,
        allowed_tools,
        body: document.content,
        provenance,
        overrides: Vec::new(),
        resources,
        diagnostics,
    }
}

fn valid_skill_name(name: &str) -> bool {
    !name.is_empty()
        && !name.starts_with('-')
        && !name.ends_with('-')
        && !name.contains("--")
        && name
            .bytes()
            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
}

fn string_field<'a>(
    frontmatter: &'a serde_json::Map<String, serde_json::Value>,
    field: &str,
) -> Option<&'a str> {
    frontmatter.get(field).and_then(serde_json::Value::as_str)
}

fn parse_allowed_tools(value: &serde_json::Value) -> Vec<String> {
    match value {
        serde_json::Value::String(value) => value
            .split(|character: char| character == ',' || character.is_whitespace())
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .collect(),
        serde_json::Value::Array(values) => values
            .iter()
            .filter_map(serde_json::Value::as_str)
            .map(ToOwned::to_owned)
            .collect(),
        _ => Vec::new(),
    }
}

fn workspace_resources(state: &WorkspaceConfigState, name: &str) -> Vec<SkillResourceRef> {
    let mut resources = Vec::new();
    for (kind, directory) in [
        ("reference", "references"),
        ("asset", "assets"),
        ("script", "scripts"),
    ] {
        let prefix = format!("skills/{name}/{directory}/");
        for child in resource_children(state, &prefix) {
            resources.push(SkillResourceRef {
                kind: kind.to_string(),
                name: child,
                supported: kind == "reference",
                diagnostic: (kind != "reference").then(|| {
                    format!("{kind} resources are catalogued but are not loaded automatically")
                }),
            });
        }
    }
    resources
}

fn resource_children(state: &WorkspaceConfigState, prefix: &str) -> Vec<String> {
    let mut children = BTreeSet::new();
    for path in state.snapshot.entries.keys() {
        let Some(relative) = path.as_str().strip_prefix(prefix) else {
            continue;
        };
        if relative.is_empty() {
            continue;
        }
        let child = relative.split('/').next().unwrap_or(relative);
        children.insert(format!("{prefix}{child}"));
    }
    children.into_iter().collect()
}

impl ParsedSkill {
    fn has_errors(&self) -> bool {
        self.diagnostics
            .iter()
            .any(|diagnostic| diagnostic.severity == SkillDiagnosticSeverity::Error)
    }

    fn catalog_entry(&self) -> SkillCatalogEntry {
        SkillCatalogEntry {
            name: self.name.clone(),
            description: self.description.clone(),
            provenance: self.provenance.clone(),
            overrides: self.overrides.clone(),
            diagnostics: self.diagnostics.clone(),
        }
    }
}

#[cfg(test)]
mod tests {
    use config_source::{
        ConfigContentType, ConfigEntry, ConfigTreeSnapshot, ToolchainContract,
        WorkspaceConfigSchemaBundle,
    };

    use super::*;

    fn state(main: &str, markdown: &str, name: &str) -> WorkspaceConfigState {
        let skill_path = format!("skills/{name}/SKILL.md");
        let reference_path = format!("skills/{name}/references/checklist.md");
        let tree = ConfigTreeSnapshot::from_entries(
            9,
            [
                ConfigEntry::new(
                    VirtualPath::parse("main.dcdl").unwrap(),
                    ConfigContentType::Decodal,
                    main,
                )
                .unwrap(),
                ConfigEntry::new(
                    VirtualPath::parse(&skill_path).unwrap(),
                    ConfigContentType::Text,
                    markdown,
                )
                .unwrap(),
                ConfigEntry::new(
                    VirtualPath::parse(&reference_path).unwrap(),
                    ConfigContentType::Text,
                    "checklist",
                )
                .unwrap(),
            ],
        )
        .unwrap();
        let bundle = WorkspaceConfigSchemaBundle::compose([SkillConfigSchemaProvider
            .contribution()
            .unwrap()])
        .unwrap();
        WorkspaceConfigState {
            snapshot: tree,
            contract: ToolchainContract::with_schema_bundle(
                1,
                vec![VirtualPath::parse("main.dcdl").unwrap()],
                1,
                bundle,
            ),
            projection_digest: "projection".to_string(),
        }
    }

    fn main_source(name: &str) -> String {
        let config_key = name.replace('-', "_");
        format!(
            r#"{{ skills = {{ {config_key} = import "./skills/{name}/SKILL.md" as {}; }}; }}"#,
            SKILL_DOCUMENT_SCHEMA_SOURCE
        )
    }

    #[test]
    fn workspace_skill_projection_keeps_extensions_and_uses_virtual_resources() {
        let markdown = concat!(
            "---\n",
            "name: debug-rust\n",
            "description: Debug Rust failures\n",
            "custom-authority: no\n",
            "allowed-tools: Read Grep\n",
            "metadata:\n  owner: platform\n",
            "---\n",
            "# Debug Rust\n",
        );
        let state = state(&main_source("debug-rust"), markdown, "debug-rust");
        let evaluation =
            evaluate_workspace_config_state(&state, state.contract.schema_bundle.clone()).unwrap();
        assert_eq!(
            evaluation.projections[0].data_json["skills"]["debug_rust"]["frontmatter"]["custom-authority"],
            "no"
        );
        let catalog = catalog(&state).unwrap();
        let item = catalog
            .entries
            .iter()
            .find(|item| item.name == "debug-rust")
            .unwrap();
        assert_eq!(item.provenance.kind, SkillSourceKind::Workspace);
        assert_eq!(item.provenance.revision, Some(9));
        assert!(
            item.diagnostics
                .iter()
                .all(|diagnostic| diagnostic.severity != SkillDiagnosticSeverity::Error)
        );
        let detail = detail(&state, "debug-rust").unwrap();
        assert_eq!(detail.body, "# Debug Rust\n");
        assert_eq!(detail.allowed_tools, vec!["Read", "Grep"]);
        assert_eq!(
            detail.resources[0].name,
            "skills/debug-rust/references/checklist.md"
        );
        assert_eq!(
            activation(&state, "debug-rust").unwrap().body,
            "# Debug Rust\n"
        );
    }

    #[test]
    fn workspace_override_replaces_builtin_deterministically() {
        let markdown = concat!(
            "---\nname: agent-skills\n",
            "description: Workspace override\n",
            "---\n# Workspace agent skills\n",
        );
        let state = state(&main_source("agent-skills"), markdown, "agent-skills");
        let catalog = catalog(&state).unwrap();
        assert_eq!(
            catalog
                .entries
                .iter()
                .filter(|item| item.name == "agent-skills")
                .count(),
            1
        );
        let item = catalog
            .entries
            .iter()
            .find(|item| item.name == "agent-skills")
            .unwrap();
        assert_eq!(item.description, "Workspace override");
        assert_eq!(item.provenance.kind, SkillSourceKind::Workspace);
        assert_eq!(item.overrides[0].kind, SkillSourceKind::Builtin);
    }

    #[test]
    fn inline_skill_value_cannot_claim_canonical_source_identity() {
        let main = r#"{
            skills = {
                debug_rust = {
                    frontmatter = {
                        name = "debug-rust";
                        description = "Inline authority";
                    };
                    content = "inline";
                };
            };
        }"#;
        let markdown = "---\nname: debug-rust\ndescription: File authority\n---\nfile\n";
        let state = state(main, markdown, "debug-rust");
        let item = catalog(&state)
            .unwrap()
            .entries
            .into_iter()
            .find(|item| item.name == "debug-rust")
            .unwrap();
        assert!(
            item.diagnostics
                .iter()
                .any(|diagnostic| diagnostic.code == "skill_source_mismatch")
        );
        assert!(matches!(
            activation(&state, "debug-rust"),
            Err(SkillError::InvalidSkill(_))
        ));
    }

    #[test]
    fn schema_accepts_unknown_extensions_but_keeps_skill_documents_typed() {
        let contribution = SkillConfigSchemaProvider.contribution().unwrap();
        assert_eq!(contribution.namespace, "skills");
        assert!(contribution.source.contains("...Unknown"));
        assert!(contribution.source.contains("metadata = {...String}"));
    }
}
