from __future__ import annotations

import time
from datetime import datetime
from pathlib import Path
from typing import Any

from one_run.commands.events_common import event_matches, format_event_line, normalize_values
from one_run.commands.events_stream import consume_event_file, ensure_offset
from one_run.constants import DEFAULT_RUNS_DIR
from one_run.errors import OneRunError
from one_run.listeners import build_routes_from_manifest, load_handler, manifest_interval_sec
from one_run.utils.duration import parse_duration


def _iter_event_files(runs_root: Path, run_ids: list[str] | None = None) -> list[Path]:
    if not runs_root.exists():
        return []
    if run_ids:
        files: list[Path] = []
        for run_id in run_ids:
            path = runs_root / run_id / "events.jsonl"
            if not path.exists():
                raise OneRunError(f"event file not found: {path}")
            files.append(path)
        return files
    files: list[Path] = []
    for run_dir in runs_root.iterdir():
        if not run_dir.is_dir():
            continue
        events_path = run_dir / "events.jsonl"
        if events_path.exists():
            files.append(events_path)
    return sorted(files)


def listen_events(
    *,
    run_ids: list[str] | None = None,
    watch: bool = True,
    jsonl: bool = False,
    output: str = "table",
    interval_sec: float | None = None,
    events: list[str] | None = None,
    states: list[str] | None = None,
    tags: list[str] | None = None,
    handler: str | None = None,
    manifest_path: str | None = None,
    once: bool = False,
    history: bool = False,
    since: str | None = None,
) -> int:
    project_root = Path.cwd().resolve()
    runs_root = project_root / DEFAULT_RUNS_DIR
    allowed_events = normalize_values(events)
    allowed_states = normalize_values(states)
    required_tags = normalize_values(tags)
    tag_cache: dict[str, set[str]] = {}
    offsets: dict[Path, int] = {}
    callback = load_handler(handler)
    routes = build_routes_from_manifest(manifest_path)
    manifest_interval = manifest_interval_sec(manifest_path)
    poll_interval = interval_sec if interval_sec is not None else (manifest_interval if manifest_interval is not None else 1.0)
    render_jsonl = jsonl or output == "jsonl"
    if since and not history:
        raise OneRunError("--since requires --history")
    since_ts: datetime | None = None
    if since:
        try:
            since_ts = datetime.now() - parse_duration(since)
        except ValueError as exc:
            raise OneRunError(str(exc)) from exc

    if once:
        watch = False

    def _event_ts(payload: dict[str, Any]) -> datetime | None:
        raw = payload.get("timestamp")
        if not isinstance(raw, str):
            return None
        normalized = raw.replace("Z", "+00:00")
        try:
            parsed = datetime.fromisoformat(normalized)
        except ValueError:
            return None
        if parsed.tzinfo is not None:
            return parsed.replace(tzinfo=None)
        return parsed

    def _consume_new(path: Path) -> None:
        offset = ensure_offset(path, offsets=offsets, history=history)

        def _on_line(_: Path, text: str, payload: dict[str, Any] | None) -> None:
            if payload is None:
                return
            if since_ts is not None:
                event_ts = _event_ts(payload)
                if event_ts is None or event_ts < since_ts:
                    return
            if not event_matches(
                payload,
                allowed_events=allowed_events,
                allowed_states=allowed_states,
                required_tags=required_tags,
                project_root=project_root,
                tag_cache=tag_cache,
            ):
                return
            if routes:
                for route in routes:
                    if not event_matches(
                        payload,
                        allowed_events=route.allowed_events,
                        allowed_states=route.allowed_states,
                        required_tags=route.required_tags,
                        project_root=project_root,
                        tag_cache=tag_cache,
                    ):
                        continue
                    if render_jsonl:
                        print(text)
                    else:
                        print(f"[listener:{route.label}] {format_event_line(payload)}")
                    if route.callback is not None:
                        route.callback(payload)
            else:
                if render_jsonl:
                    print(text)
                else:
                    print(format_event_line(payload))
                if callback is not None:
                    callback(payload)

        offsets[path] = consume_event_file(path, offset=offset, on_line=_on_line)

    try:
        if watch:
            print(f"[listener] started watching at {datetime.now().strftime('%H:%M:%S')}")
        while True:
            for path in _iter_event_files(runs_root, run_ids):
                _consume_new(path)
            if not watch:
                return 0
            time.sleep(max(poll_interval, 0.1))
    except KeyboardInterrupt:
        print("[listener] stopped")
        return 0
