"""dmesg parsing, marker insertion, and collection — kernel observability core.

parse_dmesg_line: Parse raw dmesg output lines into DmesgLine models.
insert_marker: Write a vmcluster marker string to /dev/kmsg via SSH.
collect_dmesg: SSH into a node, run dmesg, parse/filter/classify output.
"""

from __future__ import annotations

import logging
import re
from datetime import datetime, timezone

from vmcluster_mcp.models import DmesgLine, DmesgResult

logger = logging.getLogger("vmcluster_mcp.observe.dmesg")
from vmcluster_mcp.observe.classifier import classify_line

# Priority integer → syslog level name (priority & 7)
LEVEL_NAMES: dict[int, str] = {
    0: "emerg",
    1: "alert",
    2: "crit",
    3: "err",
    4: "warning",
    5: "notice",
    6: "info",
    7: "debug",
}

# Priority integer → syslog facility name (priority >> 3)
FACILITY_NAMES: dict[int, str] = {
    0: "kern",
    1: "user",
    2: "mail",
    3: "daemon",
    4: "auth",
    5: "syslog",
}

# Raw format: <priority>[  seconds.microseconds] text
_RAW_RE = re.compile(r"^<(\d+)>\[\s*(\d+\.\d+)\]\s*(.*)")

# ISO timestamp format: <priority>YYYY-MM-DDTHH:MM:SS,usec+offset text
_ISO_RE = re.compile(r"^<(\d+)>(\d{4}-\d{2}-\d{2}T[\d:,]+[+-]\d{4})\s+(.*)")


def parse_dmesg_line(line: str) -> DmesgLine | None:
    """Parse a single raw dmesg line into a DmesgLine model.

    Supports both raw bracket format and ISO 8601 timestamp format.
    Returns None if the line does not match either format.
    """
    raw_match = _RAW_RE.match(line)
    if raw_match:
        priority = int(raw_match.group(1))
        timestamp = float(raw_match.group(2))
        text = raw_match.group(3)
    else:
        iso_match = _ISO_RE.match(line)
        if iso_match:
            priority = int(iso_match.group(1))
            # Replace comma with period for microseconds (ISO 8601 variant)
            iso_ts = iso_match.group(2).replace(",", ".")
            timestamp = datetime.fromisoformat(iso_ts).timestamp()
            text = iso_match.group(3)
        else:
            return None

    level = LEVEL_NAMES.get(priority & 7, "debug")
    facility = FACILITY_NAMES.get(priority >> 3, "unknown")
    classification = classify_line(text)

    return DmesgLine(
        timestamp=timestamp,
        level=level,  # type: ignore[arg-type]
        facility=facility,
        text=text,
        classified_as=classification,
    )


async def insert_marker(
    ssh_manager,
    ip: str,
    port: int,
    user: str,
    key_path: str,
    marker: str,
) -> None:
    """Write a pre-generated marker string to /dev/kmsg on a remote node via SSH."""
    await ssh_manager.execute(
        ip=ip,
        port=port,
        user=user,
        key_path=key_path,
        command=f'echo "{marker}" > /dev/kmsg',
    )


# Syslog level numeric values (lower = more severe)
LEVEL_VALUES: dict[str, int] = {
    "emerg": 0,
    "alert": 1,
    "crit": 2,
    "err": 3,
    "warning": 4,
    "notice": 5,
    "info": 6,
    "debug": 7,
}

# Maximum LEVEL_VALUES value to include for each filter preset
FILTER_THRESHOLDS: dict[str, int] = {
    "all": 7,
    "warn+": 4,
    "err+": 3,
}

_ERROR_LEVELS = frozenset({"err", "crit", "alert", "emerg"})


async def collect_dmesg(
    ssh_manager,
    ip: str,
    port: int,
    user: str,
    key_path: str,
    since_marker: str | None = None,
    filter_level: str = "all",
) -> DmesgResult:
    """Collect and parse dmesg output from a remote node.

    Tries ``dmesg -r --time-format=iso`` first; falls back to ``dmesg -r`` on
    non-zero exit code.  Lines are parsed, optionally sliced after
    *since_marker*, filtered by *filter_level*, and classified.

    Args:
        ssh_manager: SSHConnectionManager instance.
        ip: Node IP address.
        port: SSH port.
        user: SSH username.
        key_path: Path to SSH private key.
        since_marker: If set, only lines appearing *after* the first line
            whose text contains the marker string are returned.  If the marker
            is not found, an empty DmesgResult is returned.
        filter_level: One of ``"all"``, ``"warn+"``, or ``"err+"``.

    Returns:
        A populated DmesgResult.
    """
    threshold = FILTER_THRESHOLDS.get(filter_level, 7)

    # Try ISO format first; fall back to plain raw format
    result = await ssh_manager.execute(
        ip=ip, port=port, user=user, key_path=key_path,
        command="dmesg -r --time-format=iso",
    )
    if result.exit_code != 0:
        logger.debug("ISO dmesg failed (exit %s) on %s — retrying with dmesg -r", result.exit_code, ip)
        result = await ssh_manager.execute(
            ip=ip, port=port, user=user, key_path=key_path,
            command="dmesg -r",
        )

    raw_lines = result.stdout.splitlines() if result.stdout else []

    # Parse all lines; skip unparseable ones
    parsed: list[DmesgLine] = []
    for raw in raw_lines:
        line = parse_dmesg_line(raw)
        if line is not None:
            parsed.append(line)

    # Marker-based slicing
    if since_marker is not None:
        marker_idx: int | None = None
        for idx, line in enumerate(parsed):
            if since_marker in line.text:
                marker_idx = idx
                break
        if marker_idx is None:
            return DmesgResult(
                lines=[],
                oops_detected=False,
                panic_detected=False,
                warning_count=0,
                error_count=0,
            )
        parsed = parsed[marker_idx + 1:]

    # Level filtering
    filtered = [
        line for line in parsed
        if LEVEL_VALUES.get(line.level, 7) <= threshold
    ]

    # Derive summary fields
    oops_detected = any(l.classified_as == "oops" for l in filtered)
    panic_detected = any(l.classified_as == "panic" for l in filtered)
    warning_count = sum(1 for l in filtered if l.classified_as == "warn")
    error_count = sum(1 for l in filtered if l.level in _ERROR_LEVELS)

    return DmesgResult(
        lines=filtered,
        oops_detected=oops_detected,
        panic_detected=panic_detected,
        warning_count=warning_count,
        error_count=error_count,
    )
