from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import secrets
import socket
import threading
import time
from typing import Any, Callable
from urllib.parse import parse_qs, urlencode, urlparse

_OBSERVER_ASSET_DIR = Path(__file__).resolve().parent / "observer"


@dataclass(frozen=True)
class PairingState:
    token: str
    expires_at: datetime
    claimed_by: str | None = None


class PairingController:
    def __init__(self, ttl_seconds: int = 900) -> None:
        self._ttl_seconds = max(60, int(ttl_seconds))
        self._lock = threading.Lock()
        self._state = self._new_state()

    def _new_state(self) -> PairingState:
        return PairingState(
            token=secrets.token_urlsafe(18),
            expires_at=datetime.now(timezone.utc) + timedelta(seconds=self._ttl_seconds),
        )

    def rotate(self) -> PairingState:
        with self._lock:
            self._state = self._new_state()
            return self._state

    def current(self) -> PairingState:
        with self._lock:
            if datetime.now(timezone.utc) >= self._state.expires_at:
                self._state = self._new_state()
            return self._state

    def validate(self, token: str, client_ip: str) -> bool:
        token = token.strip()
        with self._lock:
            state = self._state
            if datetime.now(timezone.utc) >= state.expires_at:
                self._state = self._new_state()
                return False
            if token != state.token:
                return False
            if state.claimed_by is None:
                self._state = PairingState(token=state.token, expires_at=state.expires_at, claimed_by=client_ip)
                return True
            return state.claimed_by == client_ip


class ObserverServer:
    def __init__(
        self,
        *,
        status_provider: Callable[[], dict[str, Any]],
        host: str = "0.0.0.0",
        port: int = 4582,
        token_ttl_seconds: int = 900,
    ) -> None:
        self._status_provider = status_provider
        self.host = host
        self.port = int(port)
        self._session_id = secrets.token_urlsafe(10)
        self._pairing = PairingController(ttl_seconds=token_ttl_seconds)
        self._httpd: ThreadingHTTPServer | None = None
        self._thread: threading.Thread | None = None

    def start(self) -> None:
        handler_cls = self._build_handler()
        self._httpd = ThreadingHTTPServer((self.host, self.port), handler_cls)
        self._thread = threading.Thread(target=self._httpd.serve_forever, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        if self._httpd is not None:
            self._httpd.shutdown()
            self._httpd.server_close()
        if self._thread is not None:
            self._thread.join(timeout=2.0)
        self._httpd = None
        self._thread = None

    def pairing_urls(self) -> dict[str, str]:
        state = self._pairing.current()
        host = self._detect_lan_host()
        return {
            "status_url": f"http://{host}:{self.port}/observer/status?token={state.token}",
            "observer_url": f"http://{host}:{self.port}/observer?token={state.token}",
            "expires_at": state.expires_at.isoformat(),
        }

    def _detect_lan_host(self) -> str:
        if self.host not in {"0.0.0.0", "::"}:
            return self.host
        probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            probe.connect(("8.8.8.8", 80))
            host = probe.getsockname()[0]
            return host or "127.0.0.1"
        except OSError:
            return "127.0.0.1"
        finally:
            probe.close()

    @staticmethod
    def _is_loopback(client_ip: str) -> bool:
        return client_ip in {"127.0.0.1", "::1", "localhost"}

    def _build_handler(self) -> type[BaseHTTPRequestHandler]:
        server = self

        class Handler(BaseHTTPRequestHandler):
            def do_GET(self) -> None:  # noqa: N802
                parsed = urlparse(self.path)
                path = parsed.path
                query = parse_qs(parsed.query)
                client_ip = self.client_address[0]

                if path == "/pair":
                    if not server._is_loopback(client_ip):
                        self._json_response({"error": "pairing endpoint is local-only"}, status=403)
                        return
                    state = server._pairing.rotate()
                    urls = server.pairing_urls()
                    self._json_response(
                        {
                            "mode": "observer",
                            "token": state.token,
                            "expires_at": state.expires_at.isoformat(),
                            "observer_url": urls["observer_url"],
                            "status_url": urls["status_url"],
                        }
                    )
                    return

                token = (query.get("token", [""])[0] or "").strip()
                protected = {
                    "/observer",
                    "/observer/status",
                    "/observer/findings",
                    "/observer/diagnostics",
                    "/observer/stream",
                    "/status",
                    "/companion",
                }
                if path in protected and not server._pairing.validate(token, client_ip):
                    self._json_response({"error": "invalid or expired token"}, status=401)
                    return

                if path in {"/observer/status", "/status"}:
                    payload = server._status_provider()
                    payload["server_time"] = datetime.now(timezone.utc).isoformat()
                    payload["session_id"] = server._session_id
                    self._json_response(payload)
                    return

                if path in {"/observer", "/companion"}:
                    self._html_response(_render_observer_html(token))
                    return

                if path == "/observer/findings":
                    payload = server._status_provider()
                    findings = payload.get("findings", {})
                    self._json_response(
                        {
                            "state": findings.get("state", "CLEAR"),
                            "summary": findings.get("summary", "none"),
                            "drift": findings.get("drift", "none"),
                            "total": findings.get("total", 0),
                            "severity_counts": findings.get("severity_counts", {}),
                            "server_time": datetime.now(timezone.utc).isoformat(),
                        }
                    )
                    return

                if path == "/observer/diagnostics":
                    status = server._status_provider()
                    findings = status.get("findings", {})
                    params = {
                        "session_id": server._session_id,
                        "scan_phase": str(status.get("scan", {}).get("phase", "unknown")),
                        "platform": str(status.get("platform", "desktop")),
                        "findings_state": str(findings.get("state", "CLEAR")),
                    }
                    self._json_response(
                        {
                            "observer_link": "ACTIVE",
                            "channel": "Local Secure Session",
                            "support_ticket_url": f"https://support.auditwalk.com/tickets/new?{urlencode(params)}",
                            "support_root_url": "https://support.auditwalk.com/tickets",
                            "session_id": server._session_id,
                            "server_time": datetime.now(timezone.utc).isoformat(),
                        }
                    )
                    return

                if path == "/observer/stream":
                    self._stream_status_events()
                    return

                if path == "/observer/assets/observer.css":
                    self._asset_response("observer.css", "text/css; charset=utf-8")
                    return

                if path == "/observer/assets/observer.js":
                    self._asset_response("observer.js", "application/javascript; charset=utf-8")
                    return

                if path == "/":
                    self._json_response(
                        {
                            "service": "auditwalk-observer",
                            "mode": "observer-read-only",
                            "pairing_endpoint": "/pair",
                        }
                    )
                    return

                self._json_response({"error": "not found"}, status=404)

            def _json_response(self, payload: dict[str, Any], status: int = 200) -> None:
                blob = json.dumps(payload).encode("utf-8")
                self.send_response(status)
                self.send_header("Content-Type", "application/json; charset=utf-8")
                self.send_header("Cache-Control", "no-store")
                self.send_header("X-Content-Type-Options", "nosniff")
                self.send_header("Content-Length", str(len(blob)))
                self.end_headers()
                self.wfile.write(blob)

            def _html_response(self, body: str, status: int = 200) -> None:
                blob = body.encode("utf-8")
                self.send_response(status)
                self.send_header("Content-Type", "text/html; charset=utf-8")
                self.send_header("Cache-Control", "no-store")
                self.send_header(
                    "Content-Security-Policy",
                    "default-src 'self'; connect-src 'self'; style-src 'self'; script-src 'self' 'unsafe-inline';",
                )
                self.send_header("Referrer-Policy", "no-referrer")
                self.send_header("X-Content-Type-Options", "nosniff")
                self.send_header("Content-Length", str(len(blob)))
                self.end_headers()
                self.wfile.write(blob)

            def _asset_response(self, name: str, content_type: str) -> None:
                try:
                    blob = _observer_asset(name).encode("utf-8")
                except FileNotFoundError:
                    self._json_response({"error": "asset not found"}, status=404)
                    return
                self.send_response(200)
                self.send_header("Content-Type", content_type)
                self.send_header("Cache-Control", "no-store")
                self.send_header("X-Content-Type-Options", "nosniff")
                self.send_header("Content-Length", str(len(blob)))
                self.end_headers()
                self.wfile.write(blob)

            def _stream_status_events(self) -> None:
                self.send_response(200)
                self.send_header("Content-Type", "text/event-stream; charset=utf-8")
                self.send_header("Cache-Control", "no-store")
                self.send_header("Connection", "keep-alive")
                self.send_header("X-Content-Type-Options", "nosniff")
                self.end_headers()
                try:
                    while True:
                        payload = server._status_provider()
                        payload["server_time"] = datetime.now(timezone.utc).isoformat()
                        payload["session_id"] = server._session_id
                        event = f"data: {json.dumps(payload)}\\n\\n"
                        self.wfile.write(event.encode("utf-8"))
                        self.wfile.flush()
                        time.sleep(1.0)
                except (BrokenPipeError, ConnectionResetError):
                    return

            def log_message(self, *_args: Any) -> None:
                return

        return Handler


def _observer_asset(name: str) -> str:
    path = (_OBSERVER_ASSET_DIR / name).resolve()
    if path.parent != _OBSERVER_ASSET_DIR.resolve():
        raise FileNotFoundError(name)
    return path.read_text(encoding="utf-8")


def _render_observer_html(token: str) -> str:
    template = _observer_asset("observer.html")
    return template.replace("__TOKEN_JSON__", json.dumps(token))
