//! Compiles [`LogMatcherDef`] instances into [`CompiledMatcher`] structs.
//!
//! This module is purely deterministic and synchronous — no I/O, no async.

use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use once_cell::sync::Lazy;
use regex::Regex;

use super::message::{has_errors, Message};
use super::schema::LogMatcherDef;
use super::types::{
    BodyRule, CompiledMatcher, EmitSeverity, EmitTemplate, EndCondition, MatcherId,
};

// ---------------------------------------------------------------------------
// CompileOptions
// ---------------------------------------------------------------------------

/// Options controlling the compilation process.
#[derive(Debug, Clone)]
pub struct CompileOptions {
    /// Source file name embedded in diagnostic references (e.g. `"extension.yaml"`).
    /// `None` renders as `"unknown"`.
    pub source_file: Option<String>,
    /// Emit a [`crate::log_matcher::MessageLevel::Warning`] for each `{{ group }}` template reference
    /// that does not correspond to a named capture group in any compiled regex.
    pub warn_unused_captures: bool,
    /// Maximum schema version accepted. Matchers with a higher `schema_version`
    /// are rejected. Current maximum is `1`.
    pub max_schema_version: u32,
}

impl Default for CompileOptions {
    fn default() -> Self {
        Self {
            source_file: None,
            warn_unused_captures: false,
            max_schema_version: 1,
        }
    }
}

// ---------------------------------------------------------------------------
// CompileResult
// ---------------------------------------------------------------------------

/// Successful compilation output.
///
/// Even on success, `messages` may contain warnings or info diagnostics.
#[derive(Debug)]
pub struct CompileResult {
    pub matchers: Vec<CompiledMatcher>,
    pub messages: Vec<Message>,
}

// ---------------------------------------------------------------------------
// compile_matchers
// ---------------------------------------------------------------------------

/// Compile a list of [`LogMatcherDef`] into [`CompiledMatcher`] instances.
///
/// All errors are collected before returning so callers can fix everything
/// in one pass.
///
/// Returns `Ok(CompileResult)` if no errors occurred (warnings may still be
/// present in `CompileResult::messages`). Returns `Err(messages)` if any
/// error was found; the message list contains the full diagnostic set.
pub fn compile_matchers(
    defs: Vec<LogMatcherDef>,
    options: CompileOptions,
) -> Result<CompileResult, Vec<Message>> {
    let src = options.source_file.as_deref().unwrap_or("unknown");
    let mut messages: Vec<Message> = Vec::new();
    let mut matchers: Vec<CompiledMatcher> = Vec::new();

    // -----------------------------------------------------------------------
    // Pass 1 — detect empty and duplicate IDs.
    // Indices in this set are skipped in pass 2.
    // -----------------------------------------------------------------------
    let mut skip_indices: HashSet<usize> = HashSet::new();
    let mut first_occurrence: HashMap<String, usize> = HashMap::new();

    for (idx, def) in defs.iter().enumerate() {
        let id_field = format!("{}:matchers[{}].id", src, idx);
        if def.id.is_empty() {
            messages.push(Message::error_at("matcher id must not be empty", &id_field));
            skip_indices.insert(idx);
        } else if let Some(&first_idx) = first_occurrence.get(&def.id) {
            let first_path = format!("{}:matchers[{}].id", src, first_idx);
            messages.push(
                Message::error_at(
                    format!("duplicate matcher id: {}", def.id),
                    &id_field,
                )
                .with_related(&first_path, "first defined here"),
            );
            skip_indices.insert(idx);
        } else {
            first_occurrence.insert(def.id.clone(), idx);
        }
    }

    // -----------------------------------------------------------------------
    // Pass 2 — compile each non-skipped matcher.
    // -----------------------------------------------------------------------
    for (idx, def) in defs.iter().enumerate() {
        if skip_indices.contains(&idx) {
            continue;
        }

        let field = |suffix: &str| format!("{}:matchers[{}].{}", src, idx, suffix);
        let mut matcher_msgs: Vec<Message> = Vec::new();

        // schema_version
        if def.schema_version > options.max_schema_version {
            matcher_msgs.push(Message::error_at(
                format!(
                    "unsupported schema version {}; max is {}",
                    def.schema_version, options.max_schema_version
                ),
                field("schema_version"),
            ));
        }

        // source non-empty
        if def.source.is_empty() {
            matcher_msgs.push(Message::error_at("source must not be empty", field("source")));
        }

        // start regex
        let start_regex = compile_regex(&def.start.pattern, field("start.match"), &mut matcher_msgs);

        // end condition (computed before body rules for the `repeat` check)
        let has_valid_end =
            matches!(def.end.condition.as_str(), "next_start" | "blank_line");
        let end = match def.end.condition.as_str() {
            "next_start" => Some(EndCondition::NextStart),
            "blank_line" => Some(EndCondition::BlankLine),
            other => {
                matcher_msgs.push(Message::error_at(
                    format!(
                        "unknown condition '{}'; expected next_start or blank_line",
                        other
                    ),
                    field("end.condition"),
                ));
                None
            }
        };

        // body rules
        let mut body_rules: Vec<BodyRule> = Vec::new();
        for (bidx, brule) in def.body.iter().enumerate() {
            let brule_field =
                |s: &str| format!("{}:matchers[{}].body[{}].{}", src, idx, bidx, s);

            if brule.repeat && !has_valid_end {
                matcher_msgs.push(Message::error_at(
                    "repeat: true requires a valid end condition",
                    brule_field("repeat"),
                ));
            }

            let pattern =
                compile_regex(&brule.pattern, brule_field("match"), &mut matcher_msgs);
            if let Some(r) = pattern {
                body_rules.push(BodyRule {
                    pattern: r,
                    optional: brule.optional,
                    repeat: brule.repeat,
                });
            }
        }

        // emit.message
        if def.emit.message.is_empty() {
            matcher_msgs.push(Message::error_at(
                "emit.message is required",
                field("emit.message"),
            ));
        }

        // emit.severity
        let severity =
            parse_severity(&def.emit.severity, field("emit.severity"), &mut matcher_msgs);

        // Capture group lint (optional warnings)
        if options.warn_unused_captures {
            let all_names: HashSet<String> = {
                let mut names = HashSet::new();
                if let Some(r) = &start_regex {
                    collect_capture_names(r, &mut names);
                }
                for rule in &body_rules {
                    collect_capture_names(&rule.pattern, &mut names);
                }
                names
            };

            let emit = &def.emit;
            let template_fields: &[(&str, Option<&String>)] = &[
                ("emit.message", Some(&emit.message)),
                ("emit.file", emit.file.as_ref()),
                ("emit.line", emit.line.as_ref()),
                ("emit.column", emit.column.as_ref()),
                ("emit.code", emit.code.as_ref()),
            ];

            for (fname, tmpl) in template_fields.iter() {
                if let Some(t) = tmpl {
                    for group_name in extract_template_refs(t) {
                        if !all_names.contains(&group_name) {
                            matcher_msgs.push(Message::warning_at(
                                format!(
                                    "template '{{{{ {} }}}}' references capture group not found in any regex",
                                    group_name
                                ),
                                field(fname),
                            ));
                        }
                    }
                }
            }
        }

        messages.extend(matcher_msgs.iter().cloned());

        // Build CompiledMatcher only if this matcher has no errors.
        if !has_errors(&matcher_msgs)
            && let (Some(start), Some(end), Some(severity)) = (start_regex, end, severity)
        {
            matchers.push(CompiledMatcher {
                id: MatcherId(def.id.clone()),
                source: def.source.clone(),
                priority: def.priority,
                schema_version: def.schema_version,
                start,
                body: body_rules,
                max_lines: def.max_lines,
                end,
                emit: EmitTemplate {
                    severity,
                    message: def.emit.message.clone(),
                    file: def.emit.file.clone(),
                    line: def.emit.line.clone(),
                    column: def.emit.column.clone(),
                    code: def.emit.code.clone(),
                },
            });
        }
    }

    if has_errors(&messages) {
        Err(messages)
    } else {
        Ok(CompileResult { matchers, messages })
    }
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

fn compile_regex(
    pattern: &str,
    field_path: impl Into<String>,
    messages: &mut Vec<Message>,
) -> Option<Arc<Regex>> {
    match Regex::new(pattern) {
        Ok(r) => Some(Arc::new(r)),
        Err(e) => {
            messages.push(Message::error_at(
                format!("invalid regex: {}", e),
                field_path,
            ));
            None
        }
    }
}

fn parse_severity(
    s: &str,
    field_path: impl Into<String>,
    messages: &mut Vec<Message>,
) -> Option<EmitSeverity> {
    match s {
        "error" => Some(EmitSeverity::Error),
        "warning" => Some(EmitSeverity::Warning),
        "info" => Some(EmitSeverity::Info),
        "hint" => Some(EmitSeverity::Hint),
        other => {
            messages.push(Message::error_at(
                format!(
                    "unknown severity '{}'; expected error, warning, info, or hint",
                    other
                ),
                field_path,
            ));
            None
        }
    }
}

fn collect_capture_names(regex: &Regex, out: &mut HashSet<String>) {
    for name in regex.capture_names().flatten() {
        out.insert(name.to_string());
    }
}

static TEMPLATE_REF_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"\{\{\s*(\w+)\s*\}\}").expect("static regex"));

fn extract_template_refs(template: &str) -> Vec<String> {
    TEMPLATE_REF_RE
        .captures_iter(template)
        .map(|c| c[1].to_string())
        .collect()
}

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::log_matcher::schema::{BodyRuleDef, EmitDef, EndDef, StartDef};

    fn minimal_def(id: &str) -> LogMatcherDef {
        LogMatcherDef {
            id: id.to_string(),
            source: "test".to_string(),
            priority: 0,
            schema_version: 1,
            start: StartDef {
                pattern: "^test".to_string(),
            },
            body: vec![],
            max_lines: None,
            end: EndDef {
                condition: "next_start".to_string(),
            },
            emit: EmitDef {
                severity: "error".to_string(),
                message: "test message".to_string(),
                file: None,
                line: None,
                column: None,
                code: None,
            },
        }
    }

    fn compile_one(def: LogMatcherDef) -> Result<CompileResult, Vec<Message>> {
        compile_matchers(vec![def], CompileOptions::default())
    }

    #[test]
    fn valid_matcher_compiles() {
        let result = compile_one(minimal_def("my.matcher")).expect("should succeed");
        assert_eq!(result.matchers.len(), 1);
        assert_eq!(result.matchers[0].id.0, "my.matcher");
        assert_eq!(result.matchers[0].priority, 0);
        assert_eq!(result.matchers[0].schema_version, 1);
        assert!(result.messages.is_empty());
    }

    #[test]
    fn empty_defs_succeeds() {
        let result = compile_matchers(vec![], CompileOptions::default()).expect("should succeed");
        assert!(result.matchers.is_empty());
        assert!(result.messages.is_empty());
    }

    #[test]
    fn invalid_start_regex_rejected_with_field_path() {
        let mut def = minimal_def("bad.regex");
        def.start.pattern = "^error(unclosed".to_string();
        let msgs = compile_one(def).expect_err("should fail");
        let ref_paths: Vec<&str> = msgs
            .iter()
            .filter_map(|m| m.reference.as_ref())
            .map(|r| r.filename.as_str())
            .collect();
        assert!(
            ref_paths.iter().any(|p| p.contains("start.match")),
            "expected start.match in refs, got: {:?}",
            ref_paths
        );
        assert!(msgs.iter().any(|m| m.text.contains("invalid regex")));
    }

    #[test]
    fn invalid_body_regex_rejected() {
        let mut def = minimal_def("bad.body");
        def.body.push(BodyRuleDef {
            pattern: "^(bad".to_string(),
            optional: false,
            repeat: false,
        });
        let msgs = compile_one(def).expect_err("should fail");
        assert!(msgs.iter().any(|m| m
            .reference
            .as_ref()
            .is_some_and(|r| r.filename.contains("body[0].match"))));
    }

    #[test]
    fn missing_emit_message_rejected() {
        let mut def = minimal_def("no.message");
        def.emit.message = String::new();
        let msgs = compile_one(def).expect_err("should fail");
        assert!(msgs.iter().any(|m| m
            .reference
            .as_ref()
            .is_some_and(|r| r.filename.contains("emit.message"))));
        assert!(msgs.iter().any(|m| m.text.contains("emit.message is required")));
    }

    #[test]
    fn multiple_matchers_compile() {
        let defs = vec![
            minimal_def("a.matcher"),
            minimal_def("b.matcher"),
            minimal_def("c.matcher"),
        ];
        let result = compile_matchers(defs, CompileOptions::default()).expect("should succeed");
        assert_eq!(result.matchers.len(), 3);
    }

    #[test]
    fn duplicate_id_error_has_related_ref() {
        let defs = vec![minimal_def("dup.id"), minimal_def("dup.id")];
        let msgs = compile_matchers(defs, CompileOptions::default()).expect_err("should fail");
        let dup_msg = msgs
            .iter()
            .find(|m| m.text.contains("duplicate matcher id"))
            .expect("should have duplicate error");
        assert!(
            !dup_msg.related.is_empty(),
            "expected related reference for duplicate"
        );
        assert!(dup_msg.related[0].label.contains("first defined here"));
    }

    #[test]
    fn first_of_duplicate_still_compiles() {
        // The first definition should succeed; only the second should fail.
        // Since we return Err when any error exists, we check the error messages.
        let defs = vec![minimal_def("dup"), minimal_def("dup")];
        let msgs = compile_matchers(defs, CompileOptions::default()).expect_err("should fail");
        // Only one duplicate error (not two)
        let dup_errors: Vec<_> = msgs
            .iter()
            .filter(|m| m.text.contains("duplicate matcher id: dup"))
            .collect();
        assert_eq!(dup_errors.len(), 1, "expected exactly one duplicate error");
    }

    #[test]
    fn defaults_applied() {
        let result = compile_one(minimal_def("defaults")).expect("should succeed");
        let m = &result.matchers[0];
        assert_eq!(m.priority, 0);
        assert_eq!(m.schema_version, 1);
        assert!(m.body.is_empty());
        assert!(m.max_lines.is_none());
    }

    #[test]
    fn schema_version_too_high_rejected() {
        let mut def = minimal_def("future");
        def.schema_version = 99;
        let msgs = compile_one(def).expect_err("should fail");
        assert!(msgs.iter().any(|m| m
            .reference
            .as_ref()
            .is_some_and(|r| r.filename.contains("schema_version"))));
        assert!(msgs.iter().any(|m| m.text.contains("unsupported schema version")));
    }

    #[test]
    fn unknown_end_condition_rejected() {
        let mut def = minimal_def("bad.end");
        def.end.condition = "timeout".to_string();
        let msgs = compile_one(def).expect_err("should fail");
        assert!(msgs.iter().any(|m| m
            .reference
            .as_ref()
            .is_some_and(|r| r.filename.contains("end.condition"))));
    }

    #[test]
    fn blank_line_end_condition_compiles() {
        let mut def = minimal_def("blank.end");
        def.end.condition = "blank_line".to_string();
        let result = compile_one(def).expect("should succeed");
        assert_eq!(result.matchers[0].end, crate::log_matcher::types::EndCondition::BlankLine);
    }

    #[test]
    fn unknown_severity_rejected() {
        let mut def = minimal_def("bad.sev");
        def.emit.severity = "fatal".to_string();
        let msgs = compile_one(def).expect_err("should fail");
        assert!(msgs.iter().any(|m| m.text.contains("unknown severity")));
    }

    #[test]
    fn repeat_without_end_condition_rejected() {
        let mut def = minimal_def("bad.repeat");
        def.end.condition = "bad_cond".to_string();
        def.body.push(BodyRuleDef {
            pattern: "^.*".to_string(),
            optional: false,
            repeat: true,
        });
        let msgs = compile_one(def).expect_err("should fail");
        assert!(msgs
            .iter()
            .any(|m| m.text.contains("repeat: true requires a valid end condition")));
    }

    #[test]
    fn unknown_capture_warning_emitted() {
        let mut def = minimal_def("warn.captures");
        def.start.pattern = "^error (?P<message>.+)".to_string();
        def.emit.message = "{{ unknown_group }}".to_string();
        let result = compile_matchers(
            vec![def],
            CompileOptions {
                warn_unused_captures: true,
                ..Default::default()
            },
        )
        .expect("should succeed with warnings");
        assert!(
            result
                .messages
                .iter()
                .any(|m| m.text.contains("unknown_group")),
            "expected warning about unknown_group"
        );
    }

    #[test]
    fn no_warning_for_known_capture() {
        let mut def = minimal_def("known.capture");
        def.start.pattern = "^error (?P<message>.+)".to_string();
        def.emit.message = "{{ message }}".to_string();
        let result = compile_matchers(
            vec![def],
            CompileOptions {
                warn_unused_captures: true,
                ..Default::default()
            },
        )
        .expect("should succeed");
        // No warnings about "message" because it IS a named group
        assert!(
            !result.messages.iter().any(|m| m.text.contains("message")),
            "should not warn about known capture group"
        );
    }

    #[test]
    fn source_file_embedded_in_references() {
        let mut def = minimal_def("ref.test");
        def.start.pattern = "unclosed[".to_string();
        let msgs = compile_matchers(
            vec![def],
            CompileOptions {
                source_file: Some("my-extension.yaml".to_string()),
                ..Default::default()
            },
        )
        .expect_err("should fail");
        assert!(msgs
            .iter()
            .any(|m| m.reference.as_ref().is_some_and(|r| r.filename.contains("my-extension.yaml"))));
    }

    #[test]
    fn all_errors_collected_across_matchers() {
        // Two broken matchers — should get errors for both, not just the first.
        let mut def1 = minimal_def("broken.one");
        def1.start.pattern = "unclosed[".to_string();
        let mut def2 = minimal_def("broken.two");
        def2.emit.message = String::new();
        let msgs =
            compile_matchers(vec![def1, def2], CompileOptions::default()).expect_err("should fail");
        // Should have errors for both matchers
        let has_broken_one = msgs.iter().any(|m| {
            m.reference
                .as_ref()
                .is_some_and(|r| r.filename.contains("matchers[0]"))
        });
        let has_broken_two = msgs.iter().any(|m| {
            m.reference
                .as_ref()
                .is_some_and(|r| r.filename.contains("matchers[1]"))
        });
        assert!(has_broken_one, "expected error for matchers[0]");
        assert!(has_broken_two, "expected error for matchers[1]");
    }
}
