//! Human-edit linter for Workflow files.

use std::collections::HashSet;

use memory::WorkspaceLayout;

use crate::{Slug, WorkflowLintError};
use lint_common::RecordLintError;
use serde::de::DeserializeOwned;

use crate::schema::{WORKFLOW_BODY_LIMIT, WorkflowFrontmatter, split_frontmatter};
use crate::workflow::WORKFLOW_DESCRIPTION_HARD_CAP;

#[derive(Debug, Default, Clone)]
pub struct WorkflowLintReport {
    pub errors: Vec<WorkflowLintError>,
}

impl WorkflowLintReport {
    pub fn has_errors(&self) -> bool {
        !self.errors.is_empty()
    }

    pub fn push_error(&mut self, err: WorkflowLintError) {
        self.errors.push(err);
    }
}

#[derive(Debug, Clone)]
pub struct WorkflowLinter {
    layout: WorkspaceLayout,
}

impl WorkflowLinter {
    pub fn new(layout: WorkspaceLayout) -> Self {
        Self { layout }
    }

    pub fn layout(&self) -> &WorkspaceLayout {
        &self.layout
    }

    /// Validate a human-authored Workflow document.
    ///
    /// Verifies frontmatter shape, body size, resident description size, and
    /// that every `requires` slug points at an existing Knowledge record.
    pub fn lint(&self, content: &str) -> WorkflowLintReport {
        let mut report = WorkflowLintReport::default();
        let parsed = match parse_frontmatter::<WorkflowFrontmatter>(content) {
            Ok(parsed) => parsed,
            Err(err) => {
                report.push_error(err);
                return report;
            }
        };

        let body_chars = parsed.body.chars().count();
        if body_chars > WORKFLOW_BODY_LIMIT {
            report.push_error(WorkflowLintError::BodyTooLong {
                actual: body_chars,
                limit: WORKFLOW_BODY_LIMIT,
            });
        }

        if parsed.frontmatter.model_invokation {
            let actual = parsed.frontmatter.description.chars().count();
            if actual > WORKFLOW_DESCRIPTION_HARD_CAP {
                report.push_error(WorkflowLintError::DescriptionTooLong {
                    actual,
                    limit: WORKFLOW_DESCRIPTION_HARD_CAP,
                });
            }
        }

        let knowledge = match scan_knowledge_slugs(&self.layout) {
            Ok(knowledge) => knowledge,
            Err(err) => {
                report.push_error(WorkflowLintError::Record(
                    RecordLintError::MalformedFrontmatter(format!(
                        "failed to scan existing Knowledge records: {err}"
                    )),
                ));
                return report;
            }
        };

        for slug in &parsed.frontmatter.requires {
            if !knowledge.contains(slug) {
                report.push_error(WorkflowLintError::UnknownReference {
                    field: "requires",
                    kind: "knowledge",
                    slug: slug.to_string(),
                });
            }
        }

        report
    }
}

struct Parsed<'a, F> {
    frontmatter: F,
    body: &'a str,
}

fn parse_frontmatter<F: DeserializeOwned>(
    content: &str,
) -> Result<Parsed<'_, F>, WorkflowLintError> {
    let (yaml, body) = split_frontmatter(content)?;
    let frontmatter = serde_yaml::from_str::<F>(yaml).map_err(|err| {
        let msg = err.to_string();
        if let Some(field) = parse_missing_field(&msg) {
            WorkflowLintError::MissingField(field)
        } else {
            WorkflowLintError::Record(RecordLintError::MalformedFrontmatter(msg))
        }
    })?;
    Ok(Parsed { frontmatter, body })
}

fn parse_missing_field(msg: &str) -> Option<&'static str> {
    let needle = "missing field `";
    let start = msg.find(needle)? + needle.len();
    let end = msg[start..].find('`')? + start;
    match &msg[start..end] {
        "description" => Some("description"),
        "model_invokation" => Some("model_invokation"),
        "user_invocable" => Some("user_invocable"),
        "requires" => Some("requires"),
        _ => None,
    }
}

fn scan_knowledge_slugs(layout: &WorkspaceLayout) -> std::io::Result<HashSet<Slug>> {
    let mut out = HashSet::new();
    let entries = match std::fs::read_dir(layout.knowledge_dir()) {
        Ok(entries) => entries,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(out),
        Err(err) => return Err(err),
    };
    for entry in entries {
        let entry = entry?;
        let path = entry.path();
        if !path.is_file() || path.extension().and_then(|s| s.to_str()) != Some("md") {
            continue;
        }
        let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
            continue;
        };
        if let Ok(slug) = Slug::parse(stem) {
            out.insert(slug);
        }
    }
    Ok(out)
}

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

    fn write(path: &std::path::Path, content: &str) {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).unwrap();
        }
        std::fs::write(path, content).unwrap();
    }

    fn workspace() -> (TempDir, WorkflowLinter) {
        let dir = TempDir::new().unwrap();
        let layout = WorkspaceLayout::new(dir.path().to_path_buf());
        (dir, WorkflowLinter::new(layout))
    }

    #[test]
    fn workflow_lint_accepts_valid_file() {
        let (dir, linter) = workspace();
        write(
            &dir.path().join(".yoi/knowledge/policy.md"),
            "---\ndescription: p\n---\nbody",
        );
        let wf = "---\ndescription: run\nrequires: [policy]\n---\nbody";
        let report = linter.lint(wf);
        assert!(!report.has_errors(), "{:?}", report.errors);
    }

    #[test]
    fn workflow_lint_rejects_missing_required_knowledge() {
        let (_dir, linter) = workspace();
        let wf = "---\ndescription: run\nrequires: [ghost]\n---\nbody";
        let report = linter.lint(wf);
        assert!(report.errors.iter().any(|err| matches!(
            err,
            WorkflowLintError::UnknownReference { field: "requires", kind: "knowledge", slug }
                if slug == "ghost"
        )));
    }

    #[test]
    fn workflow_lint_enforces_resident_description_cap() {
        let (_dir, linter) = workspace();
        let desc = "x".repeat(WORKFLOW_DESCRIPTION_HARD_CAP + 1);
        let wf = format!("---\ndescription: {desc}\nmodel_invokation: true\n---\nbody");
        let report = linter.lint(&wf);
        assert!(
            report
                .errors
                .iter()
                .any(|err| matches!(err, WorkflowLintError::DescriptionTooLong { .. }))
        );
    }

    #[test]
    fn workflow_lint_enforces_body_limit() {
        let (_dir, linter) = workspace();
        let body = "x".repeat(WORKFLOW_BODY_LIMIT + 1);
        let wf = format!("---\ndescription: run\n---\n{body}");
        let report = linter.lint(&wf);
        assert!(
            report
                .errors
                .iter()
                .any(|err| matches!(err, WorkflowLintError::BodyTooLong { .. }))
        );
    }
}
