"""Log forwarding to Elasticsearch and Logstash.

Each forwarder runs a daemon thread that drains a queue and ships batches
to the remote. If the remote is unreachable, batches are retried with
exponential backoff (3 attempts) then dropped — local log files are always
the authoritative copy.

No external dependencies: uses ``urllib.request`` for Elasticsearch and
``socket`` for Logstash TCP.
"""

from __future__ import annotations

import base64
import json
import queue
import socket
import sys
import threading
import time
import urllib.error
import urllib.request
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from konstant.config import ForwardConfig

_BATCH_SIZE = 50
_FLUSH_INTERVAL = 5.0  # seconds
_MAX_RETRIES = 3
_SENTINEL = None  # pushed to signal shutdown


class LogForwarder(ABC):
    """Base class — buffer lines in a queue, flush in a background thread."""

    def __init__(self, config: ForwardConfig) -> None:
        self._config = config
        self._queue: queue.Queue[str | None] = queue.Queue(maxsize=5000)
        self._thread = threading.Thread(
            target=self._drain_loop,
            daemon=True,
            name=f"konstant-fwd-{config.type}",
        )
        self._thread.start()

    def push(self, line: str) -> None:
        try:
            self._queue.put_nowait(line)
        except queue.Full:
            pass  # drop rather than block the log reader

    def close(self) -> None:
        self._queue.put(_SENTINEL)

    def _drain_loop(self) -> None:
        batch: list[str] = []
        last_flush = time.monotonic()
        while True:
            try:
                item = self._queue.get(timeout=1.0)
            except queue.Empty:
                item = ...  # timeout, check if we should flush
            if item is _SENTINEL:
                if batch:
                    self._flush_with_retry(batch)
                break
            if item is not ...:
                batch.append(item)  # type: ignore[arg-type]
            now = time.monotonic()
            if len(batch) >= _BATCH_SIZE or (batch and now - last_flush >= _FLUSH_INTERVAL):
                self._flush_with_retry(batch)
                batch = []
                last_flush = now

    def _flush_with_retry(self, batch: list[str]) -> None:
        delay = 1.0
        for attempt in range(_MAX_RETRIES):
            try:
                self._send_batch(batch)
                return
            except Exception:
                if attempt < _MAX_RETRIES - 1:
                    time.sleep(delay)
                    delay *= 2
        print(
            f"[konstant] forwarder: dropped {len(batch)} lines after {_MAX_RETRIES} retries",
            file=sys.stderr,
            flush=True,
        )

    @abstractmethod
    def _send_batch(self, batch: list[str]) -> None: ...


# ------------------------------------------------------------------
# Elasticsearch — HTTP _bulk API
# ------------------------------------------------------------------


class ElasticsearchForwarder(LogForwarder):
    """Ships log lines to Elasticsearch via the ``_bulk`` API."""

    def _send_batch(self, batch: list[str]) -> None:
        url = self._config.url.rstrip("/") + "/_bulk"
        ndjson_lines: list[str] = []
        for line in batch:
            action = json.dumps({"index": {"_index": self._config.index}})
            ndjson_lines.append(action)
            ndjson_lines.append(line if isinstance(line, str) else json.dumps(line))
        body = "\n".join(ndjson_lines) + "\n"

        req = urllib.request.Request(
            url,
            data=body.encode("utf-8"),
            method="POST",
            headers={"Content-Type": "application/x-ndjson"},
        )
        self._apply_auth(req)
        with urllib.request.urlopen(req, timeout=10) as resp:
            payload = resp.read()
        if not payload:
            return
        try:
            parsed = json.loads(payload.decode("utf-8"))
        except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
            return
        if isinstance(parsed, dict) and parsed.get("errors"):
            raise RuntimeError("Elasticsearch bulk indexing returned errors=true")

    def _apply_auth(self, req: urllib.request.Request) -> None:
        if self._config.api_key:
            req.add_header("Authorization", f"ApiKey {self._config.api_key}")
        elif self._config.username:
            creds = f"{self._config.username}:{self._config.password}"
            encoded = base64.b64encode(creds.encode()).decode()
            req.add_header("Authorization", f"Basic {encoded}")


# ------------------------------------------------------------------
# Logstash — TCP with json_lines codec
# ------------------------------------------------------------------


class LogstashForwarder(LogForwarder):
    """Ships log lines over a persistent TCP socket (Logstash ``tcp`` input
    with ``json_lines`` codec)."""

    def __init__(self, config: ForwardConfig) -> None:
        self._sock: socket.socket | None = None
        super().__init__(config)

    def _connect(self) -> socket.socket:
        host, _, port_str = self._config.url.rstrip("/").rpartition(":")
        host = host.replace("tcp://", "").replace("//", "").strip("/")
        port = int(port_str) if port_str else 5044
        sock = socket.create_connection((host, port), timeout=10)
        return sock

    def _send_batch(self, batch: list[str]) -> None:
        if self._sock is None:
            self._sock = self._connect()
        payload = "".join(line + "\n" for line in batch)
        try:
            self._sock.sendall(payload.encode("utf-8"))
        except (BrokenPipeError, ConnectionResetError, OSError):
            self._sock = None
            raise

    def close(self) -> None:
        super().close()
        if self._sock is not None:
            try:
                self._sock.close()
            except OSError:
                pass


# ------------------------------------------------------------------
# Factory
# ------------------------------------------------------------------


def create_forwarder(config: ForwardConfig) -> LogForwarder | None:
    """Return the appropriate forwarder if forwarding is enabled, else ``None``."""
    if not config.enabled:
        return None
    if config.type == "logstash":
        return LogstashForwarder(config)
    return ElasticsearchForwarder(config)
