from __future__ import annotations

from datetime import datetime
from pathlib import Path
import re
import socket
import threading
import time
from typing import Any, Callable, Optional

from .adapter import Adapter, AdapterOutputWriter


_BUFFER_SIZE = 10000


class NullWriter(AdapterOutputWriter):
    def write(self, output: str) -> None:
        return


def _is_wildcard_host(host: str) -> bool:
    return (host or "").strip() in {"", "0.0.0.0", "::", "::0"}


def _resolve_primary_ipv4() -> str | None:
    # UDP "connect" does not send packets but lets us query the preferred local route address.
    for target in ("8.8.8.8", "1.1.1.1"):
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
                probe.connect((target, 80))
                candidate = probe.getsockname()[0]
        except OSError:
            continue
        if candidate and not candidate.startswith("127."):
            return candidate

    try:
        for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET, socket.SOCK_DGRAM):
            candidate = info[4][0]
            if candidate and not candidate.startswith("127."):
                return candidate
    except OSError:
        return None
    return None


def _resolve_display_host(host: str) -> str:
    trimmed = (host or "").strip()
    if not _is_wildcard_host(trimmed):
        return trimmed or "127.0.0.1"
    return _resolve_primary_ipv4() or "127.0.0.1"


def _normalize_log_directory(log_directory: str) -> Path | None:
    raw = (log_directory or "").strip()
    if not raw:
        return None
    path = Path(raw).expanduser()
    try:
        path.mkdir(parents=True, exist_ok=True)
    except OSError:
        return None
    if not path.is_dir():
        return None
    return path


def _sanitize_peer_for_filename(peer_ip: str) -> str:
    sanitized = re.sub(r"[^0-9A-Za-z]+", "_", (peer_ip or "").strip())
    sanitized = sanitized.strip("_")
    return sanitized or "unknown"


def _format_bytes_for_log(payload: bytes) -> str:
    text = payload.decode("utf-8", errors="replace")
    escaped = (
        text.replace("\\", "\\\\")
        .replace("\r", "\\r")
        .replace("\n", "\\n")
    )
    return f'text="{escaped}" hex="{payload.hex(" ")}"'


class ConnectionTranscript:
    def __init__(self, directory: Path, peer_ip: str, peer_port: int) -> None:
        self._lock = threading.Lock()
        timestamp = datetime.now()
        date_token = timestamp.strftime("%Y%m%d-%H%M%S-%f")
        ip_token = _sanitize_peer_for_filename(peer_ip)
        base_name = f"elm327-{ip_token}-{date_token}"
        self.path = directory / f"{base_name}.log"
        for suffix in range(1000):
            candidate = directory / (f"{base_name}.log" if suffix == 0 else f"{base_name}-{suffix}.log")
            try:
                self._handle = candidate.open("x", encoding="utf-8")
                self.path = candidate
                break
            except FileExistsError:
                continue
        else:
            raise OSError("ELM327 transcript filename collision")
        self._write_line("# ELM327 connection transcript")
        self._write_line(f"peer={peer_ip}:{peer_port}")
        self._write_line(f"started_at={timestamp.isoformat(timespec='seconds')}")

    @classmethod
    def create(cls, directory: Path | None, peer_ip: str, peer_port: int) -> "ConnectionTranscript | None":
        if directory is None:
            return None
        try:
            return cls(directory, peer_ip, peer_port)
        except OSError as exc:
            print(f"DEBUG: ELM327 transcript disabled (cannot open log file): {exc}", flush=True)
            return None

    def _write_line(self, line: str) -> None:
        with self._lock:
            self._handle.write(f"{line}\n")
            self._handle.flush()

    def note_rx(self, payload: bytes) -> None:
        if payload:
            self._write_line(f"[{datetime.now().isoformat(timespec='milliseconds')}] RX {_format_bytes_for_log(payload)}")

    def note_tx(self, payload: bytes) -> None:
        if payload:
            self._write_line(f"[{datetime.now().isoformat(timespec='milliseconds')}] TX {_format_bytes_for_log(payload)}")

    def note_command(self, command: str) -> None:
        trimmed = command.strip()
        if trimmed:
            self._write_line(f"[{datetime.now().isoformat(timespec='milliseconds')}] CMD {trimmed}")

    def note_event(self, message: str) -> None:
        self._write_line(f"[{datetime.now().isoformat(timespec='milliseconds')}] EVENT {message}")

    def close(self) -> None:
        with self._lock:
            self._handle.flush()
            self._handle.close()


class SocketWriter(AdapterOutputWriter):
    def __init__(
        self,
        sock: socket.socket,
        server: "TCPServer",
        transcript: ConnectionTranscript | None = None,
    ) -> None:
        self._socket = sock
        self._server = server
        self._transcript = transcript

    def write(self, output: str) -> None:
        if not self._socket:
            return
        data = output.encode("utf-8")
        try:
            self._socket.sendall(data)
            self._server.note_tx(len(data))
            if self._transcript is not None:
                self._transcript.note_tx(data)
        except OSError:
            self._socket = None


def _readline(
    sock: socket.socket,
    allowed: set[str],
    echo: bool,
    transcript: ConnectionTranscript | None = None,
) -> tuple[str, int]:
    line = bytearray()
    byte_count = 0
    while True:
        try:
            chunk = sock.recv(_BUFFER_SIZE)
        except socket.timeout:
            return "", 0
        if not chunk:
            raise ConnectionError("socket closed")
        filtered = bytearray(b for b in chunk if chr(b) in allowed)
        if filtered and transcript is not None:
            transcript.note_rx(bytes(filtered))
        if echo and filtered:
            sock.sendall(filtered)
            if transcript is not None:
                transcript.note_tx(bytes(filtered))
        byte_count += len(filtered)
        for byte in filtered:
            if byte in (0x0D, 0x0A):
                return line.decode("utf-8", errors="ignore"), byte_count
            line.append(byte)


def _strip_leading_newlines(command: str) -> str:
    if command.startswith("\n") or command.startswith("\r"):
        return command[1:]
    return command


class TCPServer:
    def __init__(
        self,
        adapter: Adapter,
        host: str,
        port: int,
        event_sink: Optional[Callable[[dict[str, Any]], None]] = None,
        log_directory: str = "",
    ) -> None:
        self.adapter = adapter
        self.host = host
        self.port = port
        self._event_sink = event_sink
        self._log_directory = _normalize_log_directory(log_directory)
        self.server: socket.socket | None = None
        self._stop = threading.Event()
        self._thread: threading.Thread | None = None
        self._client: socket.socket | None = None
        self._client_peer: str | None = None
        self._client_count = 0
        self._last_rx: float | None = None
        self._last_tx: float | None = None
        self._rx_bytes = 0
        self._tx_bytes = 0
        self._write_lock = threading.Lock()
        self.adapter.output = NullWriter()
        if log_directory and self._log_directory is None:
            print(f"DEBUG: ELM327 transcript logging disabled (invalid directory): {log_directory}", flush=True)

    def _emit_event(self, kind: str, **payload: Any) -> None:
        if self._event_sink is None:
            return
        event_payload: dict[str, Any] = {"kind": kind, "timestamp": time.time()}
        event_payload.update(payload)
        try:
            self._event_sink(event_payload)
        except Exception:
            pass

    def start(self) -> None:
        if self._thread is not None:
            return
        self._stop.clear()
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # Retry binding in case port is still being released
        for attempt in range(10):
            try:
                server.bind((self.host, self.port))
                break
            except OSError as e:
                print(f"DEBUG: ELM327 bind attempt {attempt + 1} failed: {e}", flush=True)
                if attempt == 9:
                    raise
                time.sleep(0.1)
        server.listen(1)
        server.settimeout(0.5)
        self.server = server
        thread = threading.Thread(target=self._serve, daemon=True)
        self._thread = thread
        thread.start()
        print(f"DEBUG: ELM327 TCP server started on {self.host}:{self.port}", flush=True)

    def stop(self) -> None:
        self._stop.set()
        if self._client is not None:
            try:
                self._client.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            try:
                self._client.close()
            except OSError:
                pass
            self._client = None
            self._client_peer = None
        if self.server is not None:
            try:
                self.server.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            try:
                self.server.close()
            except OSError:
                pass
            self.server = None
        if self._thread is not None:
            self._thread.join(timeout=1.0)
            self._thread = None
        self.adapter.output = NullWriter()
        print("DEBUG: ELM327 TCP server stopped", flush=True)

    def note_rx(self, byte_count: int) -> None:
        if byte_count <= 0:
            return
        self._last_rx = time.time()
        self._rx_bytes += byte_count
        self._emit_event("transport_rx", bytes=byte_count, peer=self._client_peer or "unknown")

    def note_tx(self, byte_count: int) -> None:
        if byte_count <= 0:
            return
        self._last_tx = time.time()
        self._tx_bytes += byte_count
        self._emit_event("transport_tx", bytes=byte_count, peer=self._client_peer or "unknown")

    def _serve(self) -> None:
        while not self._stop.is_set():
            if self.server is None:
                return
            try:
                client, addr = self.server.accept()
            except socket.timeout:
                continue
            except OSError:
                return
            self._client = client
            peer_ip = str(addr[0])
            peer_port = int(addr[1])
            self._client_peer = f"{peer_ip}:{peer_port}"
            self._client_count += 1
            self._emit_event("connection_opened", peer=self._client_peer)
            transcript = ConnectionTranscript.create(self._log_directory, peer_ip, peer_port)
            if transcript is not None:
                transcript.note_event("connection_opened")
            with client:
                client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                client.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
                client.settimeout(0.5)
                self.adapter.output = SocketWriter(client, self, transcript=transcript)
                while not self._stop.is_set():
                    try:
                        command, byte_count = _readline(
                            client,
                            self.adapter.valid_character_set,
                            self.adapter.echo,
                            transcript=transcript,
                        )
                        if byte_count:
                            self.note_rx(byte_count)
                        if not command:
                            continue
                        command = _strip_leading_newlines(command)
                        if transcript is not None and command:
                            transcript.note_command(command)
                        if command.startswith("q"):
                            break
                        self._emit_event("adapter_command", command=command, peer=self._client_peer or "unknown")
                        self.adapter.feed_command(command)
                    except Exception as exc:
                        if transcript is not None:
                            transcript.note_event(f"connection_error {exc}")
                        self._emit_event("connection_error", message="socket read failed", peer=self._client_peer or "unknown")
                        break
            self.adapter.output = NullWriter()
            self._client = None
            self._emit_event("connection_closed", peer=self._client_peer or "unknown")
            if transcript is not None:
                transcript.note_event("connection_closed")
                transcript.close()
            self._client_peer = None

    def ui_snapshot(self) -> dict:
        listen_port = self.port
        if self.server is not None:
            try:
                listen_port = int(self.server.getsockname()[1])
            except (TypeError, ValueError, OSError):
                listen_port = self.port
        listen_host = (self.host or "").strip() or "0.0.0.0"
        return {
            "connected": self._client is not None,
            "clients": self._client_count,
            "last_rx": self._last_rx,
            "last_tx": self._last_tx,
            "rx_bytes": self._rx_bytes,
            "tx_bytes": self._tx_bytes,
            "listen_host": listen_host,
            "listen_port": listen_port,
            "listen_display_host": _resolve_display_host(listen_host),
            "listen_is_wildcard": _is_wildcard_host(listen_host),
        }
