"""Log capture — subprocess stdout/stderr to rotating log files with timestamps."""

from __future__ import annotations

import json
import logging
import os
import threading
import time
from abc import ABC, abstractmethod
from collections import deque
from datetime import datetime, timezone
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import IO, TYPE_CHECKING, Callable

if TYPE_CHECKING:
    from konstant.config import LoggingConfig
    from konstant.forwarder import LogForwarder

MAX_BYTES = 10 * 1024 * 1024  # 10 MB
BACKUP_COUNT = 5


# ------------------------------------------------------------------
# Formatter strategy
# ------------------------------------------------------------------


class LineFormatter(ABC):
    @abstractmethod
    def format_line(self, line: str, service_name: str, stream: str) -> str: ...


class TextLineFormatter(LineFormatter):
    def format_line(self, line: str, service_name: str, stream: str) -> str:
        ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        return f"[{ts}] {line}"


class JsonLineFormatter(LineFormatter):
    def format_line(self, line: str, service_name: str, stream: str) -> str:
        level = "ERROR" if stream == "stderr" else "INFO"
        record = {
            "@timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
            "service": service_name,
            "stream": stream,
            "message": line,
            "level": level,
        }
        return json.dumps(record, ensure_ascii=False)


def _make_formatter(fmt: str) -> LineFormatter:
    if fmt == "json":
        return JsonLineFormatter()
    return TextLineFormatter()


# ------------------------------------------------------------------
# LogCapture
# ------------------------------------------------------------------


class LogCapture:
    """Captures subprocess stdout/stderr streams and writes them to a rotating log file."""

    def __init__(
        self,
        service_name: str,
        log_dir: Path,
        logging_config: LoggingConfig | None = None,
        forwarder: LogForwarder | None = None,
    ) -> None:
        self.service_name = service_name
        self.log_dir = log_dir
        self.log_dir.mkdir(parents=True, exist_ok=True)
        self._log_path = self.log_dir / f"{service_name}.log"
        self._handler = RotatingFileHandler(
            self._log_path,
            maxBytes=MAX_BYTES,
            backupCount=BACKUP_COUNT,
            encoding="utf-8",
        )
        self._handler.setFormatter(logging.Formatter("%(message)s"))
        self._logger = logging.getLogger(f"konstant.{service_name}")
        self._logger.setLevel(logging.INFO)
        self._logger.propagate = False
        self._logger.handlers = [self._handler]
        self._threads: list[threading.Thread] = []

        fmt = logging_config.format if logging_config else "text"
        self._formatter = _make_formatter(fmt)
        self._forwarder = forwarder

    @property
    def log_path(self) -> Path:
        return self._log_path

    def capture_stream(self, stream: IO[str] | None, label: str = "") -> None:
        """Start a daemon thread that reads lines from *stream* and writes them to the log."""
        if stream is None:
            return
        t = threading.Thread(
            target=self._reader_loop,
            args=(stream, label),
            daemon=True,
            name=f"konstant-log-{self.service_name}-{label}",
        )
        t.start()
        self._threads.append(t)

    def _reader_loop(self, stream: IO[str], label: str) -> None:
        try:
            for raw_line in iter(stream.readline, ""):
                line = raw_line.rstrip("\n\r")
                formatted = self._formatter.format_line(line, self.service_name, label)
                self._logger.info("%s", formatted)
                if self._forwarder is not None:
                    self._forwarder.push(formatted)
        except ValueError:
            pass  # stream closed
        finally:
            try:
                stream.close()
            except Exception:
                pass

    def close(self) -> None:
        self._handler.close()
        if self._forwarder is not None:
            self._forwarder.close()


# ------------------------------------------------------------------
# Log reading helpers
# ------------------------------------------------------------------


def tail_log(log_path: Path, lines: int = 50) -> list[str]:
    """Return the last *lines* lines from a log file."""
    if not log_path.exists():
        return []
    with open(log_path, "r", encoding="utf-8", errors="replace") as f:
        return list(deque(f, maxlen=lines))


def follow_log(
    log_path: Path,
    poll_interval: float = 0.5,
    formatter: Callable[[str], str] | None = None,
) -> None:
    """Stream new lines appended to a log file. Blocks until interrupted.

    If *formatter* is provided, each line is passed through it before printing.
    """
    if not log_path.exists():
        log_path.parent.mkdir(parents=True, exist_ok=True)
        log_path.touch()
    with open(log_path, "r", encoding="utf-8", errors="replace") as f:
        f.seek(0, os.SEEK_END)
        while True:
            line = f.readline()
            if line:
                if formatter is not None:
                    print(formatter(line), flush=True)
                else:
                    print(line, end="", flush=True)
            else:
                time.sleep(poll_interval)


def pretty_print_log_line(raw: str) -> str:
    """If *raw* is a JSON log line, extract and format for human display.

    Returns the original line unchanged if it's not valid JSON.
    """
    raw = raw.rstrip("\n\r")
    try:
        obj = json.loads(raw)
    except (json.JSONDecodeError, ValueError):
        return raw
    ts = obj.get("@timestamp", "")
    level = obj.get("level", "INFO")
    msg = obj.get("message", raw)
    stream = obj.get("stream", "")
    prefix = f"[{ts}]"
    if stream == "stderr" or level == "ERROR":
        return f"{prefix} [ERR] {msg}"
    return f"{prefix} {msg}"
