from __future__ import annotations

import json
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator

from dont_hallucinate.parser import ParsedFailure, classify_failure
from dont_hallucinate.verifier import VerificationResult, verify_command


@dataclass(slots=True)
class StreamEvent:
    ts: str
    exit_code: int
    command: str
    stderr: str
    shell: str = ""
    actor: str = ""
    cwd: str = ""
    workspace_root: str = ""
    session: int | None = None
    pid: int | None = None


@dataclass(slots=True)
class FailureReport:
    event: StreamEvent
    parsed: ParsedFailure
    verification: VerificationResult


def _parse_line(line: str) -> StreamEvent | None:
    line = line.lstrip("\ufeff")
    try:
        payload = json.loads(line)
    except json.JSONDecodeError:
        return None
    try:
        return StreamEvent(
            ts=str(payload.get("ts", "")),
            exit_code=int(payload.get("exit_code", 0)),
            command=str(payload.get("command", "")),
            stderr=str(payload.get("stderr", "")),
            shell=str(payload.get("shell", "")),
            actor=str(payload.get("actor", "")),
            cwd=str(payload.get("cwd", "")),
            workspace_root=str(payload.get("workspace_root", "")),
            session=payload.get("session"),
            pid=payload.get("pid"),
        )
    except (TypeError, ValueError):
        return None


def tail_jsonl(path: Path, poll_interval: float = 0.2, from_start: bool = False) -> Iterator[StreamEvent]:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.touch(exist_ok=True)

    with path.open("r", encoding="utf-8", errors="replace") as handle:
        if not from_start:
            handle.seek(0, 2)

        while True:
            current_pos = handle.tell()
            line = handle.readline()
            if line:
                event = _parse_line(line.strip())
                if event:
                    yield event
                continue

            try:
                if path.stat().st_size < current_pos:
                    handle.seek(0)
            except FileNotFoundError:
                path.touch(exist_ok=True)
                handle.seek(0, 2)

            time.sleep(poll_interval)


def to_failure_report(event: StreamEvent) -> FailureReport:
    parsed = classify_failure(event.command, event.stderr, event.exit_code)
    if parsed.error_type == "invalid_flag":
        verification = verify_command(event.command, parsed.hallucinated_flag)
    else:
        verification = VerificationResult(
            available_flags=[],
            suggested_flag=None,
            corrected_command=None,
            source="none",
        )
    return FailureReport(event=event, parsed=parsed, verification=verification)
