//! Runtime log-matcher execution engine (streaming state machine).
//!
//! [`MatcherEngine`] processes task output line-by-line, executing
//! [`super::CompiledMatcher`] rules against each line to accumulate
//! multi-line diagnostic blocks and emit [`crate::issue_registry::NewIssue`]s
//! when a block is complete.
//!
//! # Usage
//!
//! ```ignore
//! let mut engine = MatcherEngine::new(matchers, "task:build:crate:a");
//! for line in output_lines {
//!     for issue in engine.process_line(line) {
//!         // send Operation::AddIssue { issue } …
//!     }
//! }
//! // On task completion or cancellation (end-of-stream):
//! for issue in engine.flush() { /* … */ }
//! ```
//!
//! # Design guarantees
//!
//! * **Deterministic** – given identical input and matchers, produces identical
//!   output.  Priority tie-breaking uses original insertion order.
//! * **No panics** – invalid template references and non-numeric position
//!   strings are silently discarded; the engine never panics on bad input.
//! * **Single active block** – at most one block accumulates at any time; any
//!   new `start` match terminates the current block before starting another.
//! * **Best-effort recovery** – if a required body rule fails to match, the
//!   block stays active and the line is silently consumed.  The block still
//!   emits on the next `start` match or on `flush()`.

use std::collections::HashMap;
use std::path::PathBuf;

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

use crate::editor::position::Position;
use crate::issue_registry::{NewIssue, Severity};

use super::types::{BodyRule, CompiledMatcher, EmitSeverity, EndCondition};

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/// Named capture group values extracted from a matched regex.
pub type CaptureMap = HashMap<String, String>;

// ---------------------------------------------------------------------------
// Internal types
// ---------------------------------------------------------------------------

struct ActiveBlock {
    /// Index into the engine's sorted `matchers` slice.
    matcher_index: usize,
    captures: CaptureMap,
    /// Which body rule we are currently evaluating.
    body_index: usize,
    /// Number of non-start body lines accumulated (for `max_lines` safety cap).
    lines_accumulated: u32,
}

// ---------------------------------------------------------------------------
// MatcherEngine
// ---------------------------------------------------------------------------

/// Streaming log-matcher execution engine.
///
/// Create one instance **per output stream** (stdout / stderr) per task.
/// Call [`process_line`](MatcherEngine::process_line) for each line, then
/// [`flush`](MatcherEngine::flush) once the stream ends.
pub struct MatcherEngine {
    /// Matchers sorted *priority descending*, ties broken by *insertion order
    /// ascending*.  Iterating finds the highest-priority winner first.
    matchers: Vec<CompiledMatcher>,
    active: Option<ActiveBlock>,
    marker: String,
}

impl MatcherEngine {
    /// Create a new engine.
    ///
    /// * `matchers` – compiled matchers for this stream.  An empty slice
    ///   makes the engine a deterministic no-op.
    /// * `marker` – ephemeral issue marker applied to every emitted issue
    ///   (typically `"task:{queue}:{target}"`).
    pub fn new(mut matchers: Vec<CompiledMatcher>, marker: impl Into<String>) -> Self {
        // Sort: highest priority first; ties broken by original insertion order
        // (stable sort preserves relative order within equal-priority groups).
        matchers.sort_by_key(|b| std::cmp::Reverse(b.priority));

        Self {
            matchers,
            active: None,
            marker: marker.into(),
        }
    }

    /// Process a single text line from task output.
    ///
    /// Returns zero or more [`NewIssue`]s that became complete because of this
    /// line (typically zero; usually one only when a block terminates).
    pub fn process_line(&mut self, line: &str) -> Vec<NewIssue> {
        let mut issues = Vec::new();

        // ── 1. Start-match scan (highest-priority matcher wins) ──────────────
        let start_winner = self.matchers.iter().enumerate().find_map(|(idx, m)| {
            if m.start.is_match(line) {
                Some((idx, extract_captures(&m.start, line)))
            } else {
                None
            }
        });

        if let Some((sorted_idx, captures)) = start_winner {
            // Any start unconditionally terminates the current block.
            issues.extend(
                self.active.take().and_then(|old| {
                    emit_issue(&self.matchers[old.matcher_index], &old.captures, &self.marker)
                }),
            );
            self.active = Some(ActiveBlock {
                matcher_index: sorted_idx,
                captures,
                body_index: 0,
                lines_accumulated: 0,
            });
            return issues;
        }

        // ── 2. No start match ────────────────────────────────────────────────
        if self.active.is_none() {
            return issues; // nothing active — ignore line
        }

        // ── 3. BlankLine end condition ───────────────────────────────────────
        let is_blank_end = {
            let block = self.active.as_ref().unwrap();
            self.matchers[block.matcher_index].end == EndCondition::BlankLine
                && line.trim().is_empty()
        };
        if is_blank_end {
            let old = self.active.take().unwrap();
            if let Some(issue) =
                emit_issue(&self.matchers[old.matcher_index], &old.captures, &self.marker)
            {
                issues.push(issue);
            }
            return issues;
        }

        // ── 4. Accumulate body line ──────────────────────────────────────────
        {
            let block = self.active.as_mut().unwrap();
            block.lines_accumulated += 1;
        }

        // ── 5. max_lines safety cap ──────────────────────────────────────────
        let max_exceeded = {
            let block = self.active.as_ref().unwrap();
            self.matchers[block.matcher_index]
                .max_lines
                .is_some_and(|max| block.lines_accumulated > max)
        };
        if max_exceeded {
            let old = self.active.take().unwrap();
            if let Some(issue) =
                emit_issue(&self.matchers[old.matcher_index], &old.captures, &self.marker)
            {
                issues.push(issue);
            }
            return issues;
        }

        // ── 6. Body rule processing ──────────────────────────────────────────
        // Clone body rules to satisfy the borrow checker (Arc<Regex> is O(1)).
        let body = {
            let block = self.active.as_ref().unwrap();
            self.matchers[block.matcher_index].body.clone()
        };
        if let Some(ref mut block) = self.active {
            process_body_line(block, &body, line);
        }

        issues
    }

    /// Emit any pending block (end-of-stream).
    ///
    /// Call once when the output stream closes (task completed or cancelled).
    /// Returns at most one issue (the pending block, if any).
    pub fn flush(&mut self) -> Vec<NewIssue> {
        self.active
            .take()
            .and_then(|old| {
                emit_issue(&self.matchers[old.matcher_index], &old.captures, &self.marker)
            })
            .into_iter()
            .collect()
    }
}

// ---------------------------------------------------------------------------
// Body-rule state machine
// ---------------------------------------------------------------------------

fn process_body_line(block: &mut ActiveBlock, body: &[BodyRule], line: &str) {
    if body.is_empty() || block.body_index >= body.len() {
        return; // all rules satisfied — accept line as noise
    }

    // Bounded retry loop: skipping an optional rule retries the same line
    // against the next rule.  Cap at `body.len() + 1` to prevent any infinite
    // loop (the +1 covers the edge case of skipping the last optional rule).
    let max_iter = body.len() + 1;
    let mut iter = 0;

    while iter < max_iter && block.body_index < body.len() {
        iter += 1;
        let rule = &body[block.body_index];

        if rule.pattern.is_match(line) {
            // Merge named captures, overwriting any existing keys.
            let new_caps = extract_captures(&rule.pattern, line);
            block.captures.extend(new_caps);

            if rule.repeat {
                // Stay on this rule — the same rule may match the next line.
            } else {
                block.body_index += 1;
            }
            return; // line consumed
        } else if rule.optional {
            // Skip optional rule and retry same line against next rule.
            block.body_index += 1;
            // Continue loop.
        } else {
            // Required rule did not match — best-effort: stop body processing.
            // The block stays active; future lines may still match subsequent
            // rules once the body_index is eventually advanced.
            return;
        }
    }
    // body_index >= body.len(): all rules done; line is accepted as noise.
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

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

/// Render a template string, replacing `{{ name }}` placeholders with values
/// from `captures`.  Missing keys produce an empty string (no panic).
pub fn render_template(template: &str, captures: &CaptureMap) -> String {
    PLACEHOLDER_RE
        .replace_all(template, |caps: &regex::Captures<'_>| {
            captures
                .get(caps[1].trim())
                .cloned()
                .unwrap_or_default()
        })
        .into_owned()
}

/// Extract all named capture group values from `line` using `re`.
///
/// Only named groups are collected; the full-match group (index 0) is ignored.
/// Returns an empty map if `re` does not match or has no named groups.
pub fn extract_captures(re: &Regex, line: &str) -> CaptureMap {
    let mut map = CaptureMap::new();
    if let Some(caps) = re.captures(line) {
        for name in re.capture_names().flatten() {
            if let Some(m) = caps.name(name) {
                map.insert(name.to_string(), m.as_str().to_string());
            }
        }
    }
    map
}

/// Build a [`NewIssue`] from a completed block's captures and the matcher's
/// emit template.
///
/// Returns `None` if the rendered message is empty (safety guard).
fn emit_issue(matcher: &CompiledMatcher, captures: &CaptureMap, marker: &str) -> Option<NewIssue> {
    let message = render_template(&matcher.emit.message, captures);
    if message.is_empty() {
        return None;
    }

    let severity = match matcher.emit.severity {
        EmitSeverity::Error => Severity::Error,
        EmitSeverity::Warning => Severity::Warning,
        EmitSeverity::Info | EmitSeverity::Hint => Severity::Info,
    };

    // Resolve file path template (skip if the rendered value is empty).
    let path = matcher
        .emit
        .file
        .as_deref()
        .and_then(|tmpl| {
            let s = render_template(tmpl, captures);
            if s.is_empty() { None } else { Some(PathBuf::from(s)) }
        });

    // Resolve line / column templates.
    // `line` in the template is 1-indexed; `Position.line` is 0-indexed.
    let range = {
        let line_num = matcher.emit.line.as_deref().and_then(|tmpl| {
            render_template(tmpl, captures)
                .parse::<usize>()
                .ok()
                .map(|n| n.saturating_sub(1))
        });
        line_num.map(|ln| {
            let col = matcher
                .emit
                .column
                .as_deref()
                .and_then(|tmpl| render_template(tmpl, captures).parse::<usize>().ok())
                .unwrap_or(0);
            let pos = Position::new(ln, col);
            (pos, pos)
        })
    };

    Some(NewIssue {
        marker: Some(marker.to_string()),
        source: matcher.source.clone(),
        path,
        range,
        message,
        severity,
    })
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::log_matcher::types::{BodyRule, CompiledMatcher, EmitSeverity, EmitTemplate, EndCondition, MatcherId};
    use std::sync::Arc;

    fn make_matcher(
        id: &str,
        start_pat: &str,
        body: Vec<BodyRule>,
        end: EndCondition,
        emit_msg: &str,
        priority: u32,
    ) -> CompiledMatcher {
        CompiledMatcher {
            id: MatcherId(id.to_string()),
            source: "test".to_string(),
            priority,
            schema_version: 1,
            start: Arc::new(Regex::new(start_pat).unwrap()),
            body,
            max_lines: None,
            end,
            emit: EmitTemplate {
                severity: EmitSeverity::Error,
                message: emit_msg.to_string(),
                file: None,
                line: None,
                column: None,
                code: None,
            },
        }
    }

    fn body_rule(pat: &str, optional: bool, repeat: bool) -> BodyRule {
        BodyRule {
            pattern: Arc::new(Regex::new(pat).unwrap()),
            optional,
            repeat,
        }
    }

    // ── render_template ─────────────────────────────────────────────────────

    #[test]
    fn render_template_basic() {
        let mut caps = CaptureMap::new();
        caps.insert("msg".to_string(), "hello world".to_string());
        assert_eq!(render_template("error: {{ msg }}", &caps), "error: hello world");
    }

    #[test]
    fn render_template_missing_key() {
        let caps = CaptureMap::new();
        assert_eq!(render_template("{{ missing }}", &caps), "");
    }

    #[test]
    fn render_template_multiple() {
        let mut caps = CaptureMap::new();
        caps.insert("file".to_string(), "src/main.rs".to_string());
        caps.insert("line".to_string(), "42".to_string());
        assert_eq!(
            render_template("{{ file }}:{{ line }}", &caps),
            "src/main.rs:42"
        );
    }

    // ── extract_captures ────────────────────────────────────────────────────

    #[test]
    fn extract_captures_basic() {
        let re = Regex::new(r"^(?P<file>.+):(?P<line>\d+)").unwrap();
        let caps = extract_captures(&re, "src/main.rs:42");
        assert_eq!(caps.get("file").map(|s| s.as_str()), Some("src/main.rs"));
        assert_eq!(caps.get("line").map(|s| s.as_str()), Some("42"));
    }

    #[test]
    fn extract_captures_no_match() {
        let re = Regex::new(r"^(?P<file>.+):(?P<line>\d+)").unwrap();
        let caps = extract_captures(&re, "no match here");
        assert!(caps.is_empty());
    }

    // ── single-line start → flush emits ─────────────────────────────────────

    #[test]
    fn single_start_flush() {
        let m = make_matcher(
            "t.err",
            r"^error: (?P<message>.+)",
            vec![],
            EndCondition::NextStart,
            "{{ message }}",
            0,
        );
        let mut engine = MatcherEngine::new(vec![m], "task:q:t");
        let issues = engine.process_line("error: something broke");
        assert!(issues.is_empty(), "not emitted yet");
        let flushed = engine.flush();
        assert_eq!(flushed.len(), 1);
        assert_eq!(flushed[0].message, "something broke");
    }

    // ── multi-line block captures body ───────────────────────────────────────

    #[test]
    fn multiline_block() {
        let m = {
            let mut m = make_matcher(
                "t.err",
                r"^error: (?P<message>.+)",
                vec![body_rule(r"^ --> (?P<file>.+):(?P<line>\d+)", false, false)],
                EndCondition::NextStart,
                "{{ message }}",
                0,
            );
            m.emit.file = Some("{{ file }}".to_string());
            m.emit.line = Some("{{ line }}".to_string());
            m
        };
        let mut engine = MatcherEngine::new(vec![m], "task:q:t");
        engine.process_line("error: my error");
        engine.process_line(" --> src/lib.rs:10");
        let issues = engine.flush();
        assert_eq!(issues.len(), 1);
        let issue = &issues[0];
        assert_eq!(issue.message, "my error");
        assert_eq!(issue.path.as_deref(), Some(std::path::Path::new("src/lib.rs")));
        // line 10 → 0-indexed position 9
        assert_eq!(issue.range.map(|(s, _)| s.line), Some(9));
    }

    // ── next_start terminates block ──────────────────────────────────────────

    #[test]
    fn next_start_terminates_block() {
        let m = make_matcher(
            "t.err",
            r"^error: (?P<message>.+)",
            vec![],
            EndCondition::NextStart,
            "{{ message }}",
            0,
        );
        let mut engine = MatcherEngine::new(vec![m], "task:q:t");
        let i1 = engine.process_line("error: first error");
        assert!(i1.is_empty());
        let i2 = engine.process_line("error: second error");
        assert_eq!(i2.len(), 1, "first block emitted on second start");
        assert_eq!(i2[0].message, "first error");
        let flushed = engine.flush();
        assert_eq!(flushed.len(), 1);
        assert_eq!(flushed[0].message, "second error");
    }

    // ── blank_line terminates block ──────────────────────────────────────────

    #[test]
    fn blank_line_terminates_block() {
        let m = make_matcher(
            "t.warn",
            r"^warning: (?P<message>.+)",
            vec![],
            EndCondition::BlankLine,
            "{{ message }}",
            0,
        );
        let mut engine = MatcherEngine::new(vec![m], "task:q:t");
        engine.process_line("warning: some warning");
        let issues = engine.process_line("");
        assert_eq!(issues.len(), 1);
        assert_eq!(issues[0].message, "some warning");
        // No more active block.
        assert!(engine.flush().is_empty());
    }

    // ── optional body rule skipped when non-matching ─────────────────────────

    #[test]
    fn optional_body_skipped() {
        let m = make_matcher(
            "t.err",
            r"^error: (?P<message>.+)",
            vec![body_rule(r"^ --> (?P<file>.+)", true, false)], // optional
            EndCondition::NextStart,
            "{{ message }}",
            0,
        );
        let mut engine = MatcherEngine::new(vec![m], "task:q:t");
        engine.process_line("error: oops");
        // Skip the optional body entirely.
        let issues = engine.flush();
        assert_eq!(issues.len(), 1);
        assert_eq!(issues[0].message, "oops");
    }

    // ── repeat body rule matches multiple lines ───────────────────────────────

    #[test]
    fn repeat_body_rule() {
        let m = make_matcher(
            "t.note",
            r"^note: (?P<message>.+)",
            vec![body_rule(r"^\s+\| (?P<detail>.+)", true, true)], // optional+repeat
            EndCondition::NextStart,
            "{{ message }}",
            0,
        );
        let mut engine = MatcherEngine::new(vec![m], "task:q:t");
        engine.process_line("note: context");
        engine.process_line("   | line one");
        engine.process_line("   | line two");
        let issues = engine.flush();
        assert_eq!(issues.len(), 1);
        // Last match overwrites `detail`.
        assert_eq!(issues[0].message, "context");
    }

    // ── priority resolution ──────────────────────────────────────────────────

    #[test]
    fn priority_resolution() {
        let low = {
            let mut m = make_matcher(
                "t.low",
                r"^msg: (?P<message>.+)",
                vec![],
                EndCondition::NextStart,
                "LOW: {{ message }}",
                1,
            );
            m.id = MatcherId("t.low".to_string());
            m
        };
        let high = {
            let mut m = make_matcher(
                "t.high",
                r"^msg: (?P<message>.+)",
                vec![],
                EndCondition::NextStart,
                "HIGH: {{ message }}",
                100,
            );
            m.id = MatcherId("t.high".to_string());
            m
        };
        // Insert low-priority first, high-priority second — engine should pick high.
        let mut engine = MatcherEngine::new(vec![low, high], "task:q:t");
        engine.process_line("msg: hello");
        let issues = engine.flush();
        assert_eq!(issues.len(), 1);
        assert!(issues[0].message.starts_with("HIGH:"), "high-priority matcher should win");
    }

    // ── flush on empty engine ────────────────────────────────────────────────

    #[test]
    fn flush_empty() {
        let mut engine = MatcherEngine::new(vec![], "task:q:t");
        assert!(engine.flush().is_empty());
    }

    // ── emit_issue: empty message returns None ───────────────────────────────

    #[test]
    fn emit_issue_empty_message() {
        let m = make_matcher(
            "t.empty",
            r"^error: (?P<message>.+)",
            vec![],
            EndCondition::NextStart,
            // Template produces empty string when capture is missing.
            "{{ missing_key }}",
            0,
        );
        let mut engine = MatcherEngine::new(vec![m], "task:q:t");
        engine.process_line("error: something");
        // The start-match won't find `missing_key` in captures, so message == "".
        // This means emit returns None and flush returns empty.
        let issues = engine.flush();
        // `message` capture IS present (start pattern captures it as "message" group
        // but we used "missing_key" in the template) → empty → no issue.
        assert!(issues.is_empty(), "empty rendered message should produce no issue");
    }

    // ── non-start line with no active block is ignored ────────────────────────

    #[test]
    fn idle_line_ignored() {
        let m = make_matcher(
            "t.err",
            r"^error: (?P<message>.+)",
            vec![],
            EndCondition::NextStart,
            "{{ message }}",
            0,
        );
        let mut engine = MatcherEngine::new(vec![m], "task:q:t");
        let issues = engine.process_line("just some random output");
        assert!(issues.is_empty());
        assert!(engine.flush().is_empty());
    }

    // ── max_lines cap force-emits block ──────────────────────────────────────

    #[test]
    fn max_lines_cap() {
        let mut m = make_matcher(
            "t.err",
            r"^error: (?P<message>.+)",
            vec![],
            EndCondition::NextStart,
            "{{ message }}",
            0,
        );
        m.max_lines = Some(2); // Force-emit after 2 body lines.

        let mut engine = MatcherEngine::new(vec![m], "task:q:t");
        engine.process_line("error: my message");
        engine.process_line("body line 1");
        engine.process_line("body line 2");
        // Third body line (lines_accumulated == 3 > 2) triggers force-emit.
        let issues = engine.process_line("body line 3");
        assert_eq!(issues.len(), 1, "force-emit on max_lines exceeded");
        assert!(engine.flush().is_empty(), "block already consumed");
    }
}
