"""Small Basic Auth web UI for managing mailcat domain routes."""

from __future__ import annotations

import argparse
import base64
import html
import json
import os
import secrets
import shutil
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse

from mail_routes import DOMAIN_ROUTES_FILE, domain_lookup, domain_routes, normalize_domain_route

DEFAULT_CONFIG_FILE = Path(__file__).parent / "web_config.json"
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 4321


def validate_routes(raw_routes: list[dict]) -> list[dict]:
    """Return normalized route dictionaries and reject duplicate domains."""
    if not isinstance(raw_routes, list):
        raise ValueError("Routes must be a list")

    normalized: list[dict] = []
    seen: dict[str, int] = {}
    for index, raw_rule in enumerate(raw_routes):
        domains, mailbox = normalize_domain_route(raw_rule)
        for domain in domains:
            if domain in seen:
                other = seen[domain] + 1
                raise ValueError(f"Domain {domain!r} is already used by route {other}")
            seen[domain] = index
        normalized.append({"domains": domains, "mailbox": mailbox})
    return normalized


def load_routes(path: Path = DOMAIN_ROUTES_FILE) -> list[dict]:
    raw = json.loads(path.read_text(encoding="utf-8"))
    return validate_routes(raw)


def save_routes(routes: list[dict], path: Path = DOMAIN_ROUTES_FILE) -> list[dict]:
    normalized = validate_routes(routes)
    tmp_path = path.with_name(f".{path.name}.tmp")
    backup_path = path.with_suffix(f"{path.suffix}.bak")
    tmp_path.write_text(json.dumps(normalized, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    if path.exists():
        shutil.copy2(path, backup_path)
    os.replace(tmp_path, path)
    domain_routes.cache_clear()
    domain_lookup.cache_clear()
    return normalized


def load_web_config(path: Path) -> tuple[str, str]:
    config = {}
    if path.exists():
        config = json.loads(path.read_text(encoding="utf-8"))

    username = os.environ.get("MAILCAT_WEB_USER") or config.get("username")
    password = os.environ.get("MAILCAT_WEB_PASSWORD") or config.get("password")
    if not username or not password:
        raise SystemExit(
            f"Missing web credentials. Create {path} or set MAILCAT_WEB_USER and MAILCAT_WEB_PASSWORD."
        )
    return str(username), str(password)


def basic_auth_ok(header: str | None, username: str, password: str) -> bool:
    if not header or not header.startswith("Basic "):
        return False
    try:
        decoded = base64.b64decode(header[6:], validate=True).decode("utf-8")
        supplied_user, supplied_password = decoded.split(":", 1)
    except Exception:
        return False
    return secrets.compare_digest(supplied_user, username) and secrets.compare_digest(
        supplied_password, password
    )


def page_html() -> bytes:
    return f"""<!doctype html>
<html lang="nl">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Mailcat routes</title>
  <style>
    :root {{
      color-scheme: light;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background: #f5f6f8;
      color: #20242a;
    }}
    body {{ margin: 0; }}
    header {{
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 16px;
      padding: 14px 20px;
      background: #17324d;
      color: #fff;
    }}
    h1 {{ font-size: 18px; line-height: 1.2; margin: 0; font-weight: 650; }}
    main {{ padding: 18px 20px 28px; max-width: 1280px; margin: 0 auto; }}
    .toolbar {{
      display: grid;
      grid-template-columns: minmax(180px, 1fr) auto auto;
      gap: 10px;
      align-items: center;
      margin-bottom: 14px;
    }}
    input, textarea {{
      width: 100%;
      box-sizing: border-box;
      font: inherit;
      border: 1px solid #b9c1cc;
      border-radius: 6px;
      padding: 8px 10px;
      background: #fff;
      color: #20242a;
    }}
    textarea {{ min-height: 72px; resize: vertical; }}
    button {{
      border: 1px solid #245782;
      background: #245782;
      color: #fff;
      border-radius: 6px;
      padding: 8px 12px;
      font: inherit;
      cursor: pointer;
      white-space: nowrap;
    }}
    button.secondary {{ background: #fff; color: #245782; }}
    button.danger {{ background: #8c2634; border-color: #8c2634; }}
    button:disabled {{ opacity: .55; cursor: default; }}
    .layout {{ display: grid; grid-template-columns: 1fr 360px; gap: 18px; align-items: start; }}
    table {{ width: 100%; border-collapse: collapse; background: #fff; border: 1px solid #d7dce2; }}
    th, td {{ text-align: left; vertical-align: top; border-bottom: 1px solid #e2e6eb; padding: 8px 10px; }}
    th {{ background: #edf1f5; font-size: 13px; color: #34495e; }}
    tr.selected {{ background: #eaf4ff; }}
    td.index {{ width: 56px; color: #596775; }}
    td.actions {{ width: 96px; text-align: right; }}
    .panel {{ background: #fff; border: 1px solid #d7dce2; padding: 14px; }}
    .panel h2 {{ margin: 0 0 12px; font-size: 15px; }}
    label {{ display: block; margin: 10px 0 6px; font-size: 13px; color: #34495e; }}
    .form-actions {{ display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }}
    .status {{ min-height: 20px; font-size: 13px; color: #34495e; }}
    .error {{ color: #8c2634; }}
    .domains {{ line-height: 1.45; }}
    .mailbox {{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }}
    @media (max-width: 860px) {{
      .layout {{ grid-template-columns: 1fr; }}
      .toolbar {{ grid-template-columns: 1fr; }}
      main {{ padding: 14px; }}
    }}
  </style>
</head>
<body>
  <header>
    <h1>Mailcat routes</h1>
    <div id="count"></div>
  </header>
  <main>
    <div class="toolbar">
      <input id="search" type="search" placeholder="Zoek domein of map">
      <button id="newButton" type="button" class="secondary">Nieuwe regel</button>
      <button id="reloadButton" type="button" class="secondary">Herladen</button>
    </div>
    <div class="layout">
      <table>
        <thead><tr><th>#</th><th>Domeinen</th><th>Map</th><th></th></tr></thead>
        <tbody id="routesBody"></tbody>
      </table>
      <section class="panel">
        <h2 id="formTitle">Nieuwe regel</h2>
        <form id="routeForm">
          <input id="routeIndex" type="hidden">
          <label for="domains">Domeinen, een per regel of komma-gescheiden</label>
          <textarea id="domains" required></textarea>
          <label for="mailbox">Doelmap zonder INBOX.</label>
          <input id="mailbox" required placeholder="Diensten.AI.OpenAI">
          <div class="form-actions">
            <button id="saveButton" type="submit">Opslaan</button>
            <button id="deleteButton" type="button" class="danger" disabled>Verwijderen</button>
            <button id="clearButton" type="button" class="secondary">Leegmaken</button>
          </div>
        </form>
        <p id="status" class="status"></p>
      </section>
    </div>
  </main>
  <script>
    let routes = [];
    let selectedIndex = null;
    const $ = (id) => document.getElementById(id);

    function splitDomains(value) {{
      return value.split(/[\\n,]/).map((item) => item.trim()).filter(Boolean);
    }}

    function setStatus(text, isError = false) {{
      $("status").textContent = text;
      $("status").className = isError ? "status error" : "status";
    }}

    function clearForm() {{
      selectedIndex = null;
      $("routeIndex").value = "";
      $("domains").value = "";
      $("mailbox").value = "";
      $("formTitle").textContent = "Nieuwe regel";
      $("deleteButton").disabled = true;
      renderRoutes();
    }}

    function editRoute(index) {{
      const route = routes[index];
      selectedIndex = index;
      $("routeIndex").value = String(index);
      $("domains").value = route.domains.join("\\n");
      $("mailbox").value = route.mailbox;
      $("formTitle").textContent = `Regel ${{index + 1}} bewerken`;
      $("deleteButton").disabled = false;
      renderRoutes();
    }}

    async function requestJson(url, options = {{}}) {{
      const response = await fetch(url, {{
        headers: {{ "Content-Type": "application/json", ...(options.headers || {{}}) }},
        ...options,
      }});
      const payload = await response.json().catch(() => ({{ error: response.statusText }}));
      if (!response.ok) throw new Error(payload.error || response.statusText);
      return payload;
    }}

    async function loadRoutes() {{
      const payload = await requestJson("/api/routes");
      routes = payload.routes;
      $("count").textContent = `${{routes.length}} regels`;
      renderRoutes();
      setStatus("Geladen.");
    }}

    function renderRoutes() {{
      const needle = $("search").value.trim().toLowerCase();
      const rows = routes
        .map((route, index) => ({{ route, index }}))
        .filter(({{ route }}) => !needle || route.mailbox.toLowerCase().includes(needle) || route.domains.join(" ").toLowerCase().includes(needle))
        .map(({{ route, index }}) => `
          <tr class="${{selectedIndex === index ? "selected" : ""}}">
            <td class="index">${{index + 1}}</td>
            <td class="domains">${{route.domains.map(escapeHtml).join("<br>")}}</td>
            <td class="mailbox">${{escapeHtml(route.mailbox)}}</td>
            <td class="actions"><button type="button" class="secondary" onclick="editRoute(${{index}})">Bewerk</button></td>
          </tr>`);
      $("routesBody").innerHTML = rows.join("");
    }}

    function escapeHtml(value) {{
      return value.replace(/[&<>"']/g, (ch) => ({{ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }}[ch]));
    }}

    $("routeForm").addEventListener("submit", async (event) => {{
      event.preventDefault();
      const route = {{ domains: splitDomains($("domains").value), mailbox: $("mailbox").value.trim() }};
      const indexValue = $("routeIndex").value;
      try {{
        const method = indexValue === "" ? "POST" : "PUT";
        const url = indexValue === "" ? "/api/routes" : `/api/routes/${{indexValue}}`;
        const payload = await requestJson(url, {{ method, body: JSON.stringify(route) }});
        routes = payload.routes;
        clearForm();
        setStatus("Opgeslagen.");
      }} catch (error) {{
        setStatus(error.message, true);
      }}
    }});

    $("deleteButton").addEventListener("click", async () => {{
      if (selectedIndex === null) return;
      if (!confirm(`Regel ${{selectedIndex + 1}} verwijderen?`)) return;
      try {{
        const payload = await requestJson(`/api/routes/${{selectedIndex}}`, {{ method: "DELETE" }});
        routes = payload.routes;
        clearForm();
        setStatus("Verwijderd.");
      }} catch (error) {{
        setStatus(error.message, true);
      }}
    }});

    $("search").addEventListener("input", renderRoutes);
    $("newButton").addEventListener("click", clearForm);
    $("clearButton").addEventListener("click", clearForm);
    $("reloadButton").addEventListener("click", loadRoutes);
    loadRoutes().catch((error) => setStatus(error.message, true));
  </script>
</body>
</html>
""".encode("utf-8")


class RouteHandler(BaseHTTPRequestHandler):
    routes_file = DOMAIN_ROUTES_FILE
    username = ""
    password = ""

    def log_message(self, fmt: str, *args: object) -> None:
        print(f"{self.address_string()} - {fmt % args}")

    def _require_auth(self) -> bool:
        if basic_auth_ok(self.headers.get("Authorization"), self.username, self.password):
            return True
        self.send_response(HTTPStatus.UNAUTHORIZED)
        self.send_header("WWW-Authenticate", 'Basic realm="mailcat"')
        self.send_header("Content-Length", "0")
        self.end_headers()
        return False

    def _send(self, status: HTTPStatus, body: bytes, content_type: str) -> None:
        self.send_response(status)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _json(self, status: HTTPStatus, payload: dict) -> None:
        self._send(
            status,
            json.dumps(payload, indent=2, ensure_ascii=False).encode("utf-8"),
            "application/json; charset=utf-8",
        )

    def _error(self, status: HTTPStatus, message: str) -> None:
        self._json(status, {"error": message})

    def _read_route_body(self) -> dict:
        length = int(self.headers.get("Content-Length", "0"))
        body = self.rfile.read(length)
        payload = json.loads(body.decode("utf-8"))
        if not isinstance(payload, dict):
            raise ValueError("Request body must contain one route object")
        return payload

    def _route_index(self) -> int | None:
        parts = [part for part in urlparse(self.path).path.split("/") if part]
        if len(parts) != 3 or parts[:2] != ["api", "routes"]:
            return None
        try:
            return int(parts[2])
        except ValueError:
            return None

    def do_GET(self) -> None:
        if not self._require_auth():
            return
        path = urlparse(self.path).path
        if path == "/":
            self._send(HTTPStatus.OK, page_html(), "text/html; charset=utf-8")
            return
        if path == "/api/routes":
            try:
                self._json(HTTPStatus.OK, {"routes": load_routes(self.routes_file)})
            except Exception as exc:
                self._error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
            return
        self._error(HTTPStatus.NOT_FOUND, "Not found")

    def do_POST(self) -> None:
        if not self._require_auth():
            return
        if urlparse(self.path).path != "/api/routes":
            self._error(HTTPStatus.NOT_FOUND, "Not found")
            return
        try:
            routes = load_routes(self.routes_file)
            routes.append(self._read_route_body())
            self._json(HTTPStatus.CREATED, {"routes": save_routes(routes, self.routes_file)})
        except json.JSONDecodeError:
            self._error(HTTPStatus.BAD_REQUEST, "Invalid JSON")
        except ValueError as exc:
            self._error(HTTPStatus.BAD_REQUEST, str(exc))
        except Exception as exc:
            self._error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))

    def do_PUT(self) -> None:
        if not self._require_auth():
            return
        index = self._route_index()
        if index is None:
            self._error(HTTPStatus.NOT_FOUND, "Not found")
            return
        try:
            routes = load_routes(self.routes_file)
            if index < 0 or index >= len(routes):
                raise ValueError("Route index out of range")
            routes[index] = self._read_route_body()
            self._json(HTTPStatus.OK, {"routes": save_routes(routes, self.routes_file)})
        except json.JSONDecodeError:
            self._error(HTTPStatus.BAD_REQUEST, "Invalid JSON")
        except ValueError as exc:
            self._error(HTTPStatus.BAD_REQUEST, str(exc))
        except Exception as exc:
            self._error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))

    def do_DELETE(self) -> None:
        if not self._require_auth():
            return
        index = self._route_index()
        if index is None:
            self._error(HTTPStatus.NOT_FOUND, "Not found")
            return
        try:
            routes = load_routes(self.routes_file)
            if index < 0 or index >= len(routes):
                raise ValueError("Route index out of range")
            del routes[index]
            self._json(HTTPStatus.OK, {"routes": save_routes(routes, self.routes_file)})
        except ValueError as exc:
            self._error(HTTPStatus.BAD_REQUEST, str(exc))
        except Exception as exc:
            self._error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))


def main() -> None:
    parser = argparse.ArgumentParser(description="Run the mailcat route management web UI.")
    parser.add_argument("--host", default=DEFAULT_HOST)
    parser.add_argument("--port", type=int, default=DEFAULT_PORT)
    parser.add_argument("--config", type=Path, default=DEFAULT_CONFIG_FILE)
    parser.add_argument("--routes-file", type=Path, default=DOMAIN_ROUTES_FILE)
    args = parser.parse_args()

    username, password = load_web_config(args.config)
    RouteHandler.routes_file = args.routes_file
    RouteHandler.username = username
    RouteHandler.password = password

    server = ThreadingHTTPServer((args.host, args.port), RouteHandler)
    print(f"mailcat route web listening on http://{html.escape(args.host)}:{args.port}/")
    server.serve_forever()


if __name__ == "__main__":
    main()
