from __future__ import annotations

import html
import json
import os
import secrets
import shutil
from http import cookies
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from datetime import datetime
from urllib.parse import parse_qs, urlparse
import cgi

from .certificates import format_datetime_pt, validate_pfx
from .processes import get_sync_process_detail, list_sync_processes
from .process_views import render_processes_page
from .db import MysqlCli
from .security import decrypt_certificate_value
from .security import SessionSigner, verify_password
from .settings import (
    MAX_SYNC_MAX_WORKERS,
    MIN_SYNC_MAX_WORKERS,
    get_sync_max_workers,
    set_sync_max_workers,
)
from .sync import start_sync_queue_dispatcher, trigger_sync
from .tenants import (
    get_municipio_certificado,
    list_municipios,
    replace_municipio_certificate,
    update_municipio_fields,
    update_municipio_certificate_password,
    upsert_municipio_with_certificate,
)


STATIC_DIR = Path(__file__).resolve().parent / "static"
MAX_UPLOAD_BYTES = 2 * 1024 * 1024
UFS = [
    "AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG",
    "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO",
]
STATUS_LABELS = {
    "idle": "Parado",
    "pending": "Pendente",
    "running": "Processando",
    "ready": "Pronto",
    "error": "Erro",
    "disabled": "Desativado",
    "sem_estado": "Sem estado",
}
AMBIENTE_LABELS = {
    "producao": "Produção",
    "producao_restrita": "Produção restrita",
}


def esc(value: object) -> str:
    return html.escape(str(value or ""), quote=True)


def uf_options(selected: str = "RS") -> str:
    selected_uf = (selected or "RS").upper()
    return "".join(f"<option value='{uf}'{' selected' if uf == selected_uf else ''}>{uf}</option>" for uf in UFS)


def select_options(options: list[tuple[str, str]], selected: str) -> str:
    return "".join(
        f"<option value='{esc(value)}'{' selected' if value == selected else ''}>{esc(label)}</option>"
        for value, label in options
    )


def _format_bytes(value: int) -> str:
    size = float(value)
    for unit in ("B", "KB", "MB", "GB", "TB"):
        if size < 1024 or unit == "TB":
            return f"{size:.1f} {unit}" if unit != "B" else f"{int(size)} B"
        size /= 1024
    return f"{size:.1f} TB"


def _read_meminfo() -> dict[str, int]:
    path = Path("/proc/meminfo")
    if not path.exists():
        return {}
    values: dict[str, int] = {}
    for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
        parts = line.split()
        if len(parts) >= 2:
            values[parts[0].rstrip(":")] = int(parts[1]) * 1024
    return values


def _metric_level(percent: int | None) -> str:
    if percent is None:
        return "unknown"
    if percent >= 90:
        return "danger"
    if percent >= 70:
        return "warning"
    return "ok"


def server_health_cards() -> str:
    cards = []
    try:
        load_1m, _load_5m, _load_15m = os.getloadavg()
        cores = os.cpu_count() or 1
        percent = min(999, round((load_1m / cores) * 100))
        cards.append(("CPU", f"{percent}%", f"load 1m {load_1m:.2f} / {cores} vCPU", _metric_level(percent)))
    except (AttributeError, OSError):
        cards.append(("CPU", "N/D", "não disponível", "unknown"))

    meminfo = _read_meminfo()
    if meminfo:
        total = meminfo.get("MemTotal", 0)
        available = meminfo.get("MemAvailable", meminfo.get("MemFree", 0))
        used_percent = round(((total - available) / total) * 100) if total else 0
        cards.append((
            "Memória",
            f"{used_percent}%",
            f"livre {_format_bytes(available)} de {_format_bytes(total)}",
            _metric_level(used_percent),
        ))
    else:
        cards.append(("Memória", "N/D", "não disponível", "unknown"))

    try:
        disk = shutil.disk_usage(Path.cwd())
        used_percent = round((disk.used / disk.total) * 100) if disk.total else 0
        cards.append(("Disco", f"{used_percent}%", f"livre {_format_bytes(disk.free)} de {_format_bytes(disk.total)}", _metric_level(used_percent)))
    except OSError:
        cards.append(("Disco", "N/D", "não disponível", "unknown"))

    return "".join(
        f"""
        <div class="metric metric-{esc(level)}">
          <span>{esc(label)}</span>
          <strong>{esc(value)}</strong>
          <small>{esc(detail)}</small>
        </div>
        """
        for label, value, detail, level in cards
    )


def server_health_panel() -> str:
    return f"""
    <section class="side-monitor" aria-label="Uso do servidor">
      <h2>Monitoramento</h2>
      <div id="server-health-cards" class="metric-list">
        {server_health_cards()}
      </div>
      <div class="metric-legend" aria-label="Legenda de uso">
        <span><i class="dot dot-ok"></i>Tranquilo</span>
        <span><i class="dot dot-warning"></i>Atenção</span>
        <span><i class="dot dot-danger"></i>No limite</span>
      </div>
    </section>
    """


def _parse_datetime_pt(value: str) -> datetime:
    if not value:
        return datetime.min
    for fmt in ("%d/%m/%Y %H:%M", "%d/%m/%Y"):
        try:
            return datetime.strptime(value, fmt)
        except ValueError:
            pass
    return datetime.min


def certificate_validation_script(form_id: str, result_id: str) -> str:
    return f"""
        <script>
          (() => {{
            const form = document.getElementById("{form_id}");
            const certButton = form.querySelector("[data-validate-cert]");
            const certResult = document.getElementById("{result_id}");
            if (!form || !certButton || !certResult) return;
            certButton.addEventListener("click", async () => {{
              const data = new FormData();
              data.append("csrf", form.elements.csrf.value);
              if (form.elements.municipio_id) data.append("municipio_id", form.elements.municipio_id.value);
              if (form.elements.pfx.files[0]) data.append("pfx", form.elements.pfx.files[0]);
              data.append("pfx_password", form.elements.pfx_password.value);
              certButton.disabled = true;
              certResult.className = "hint";
              certResult.textContent = "Validando...";
              try {{
                const response = await fetch("/certificados/validar", {{ method: "POST", body: data }});
                const payload = await response.json();
                certResult.className = response.ok ? "ok inline-message" : "error inline-message";
                certResult.textContent = payload.message || "Não foi possível validar.";
              }} catch (error) {{
                certResult.className = "error inline-message";
                certResult.textContent = "Falha ao validar certificado.";
              }} finally {{
                certButton.disabled = false;
              }}
            }});
          }})();
        </script>
    """


def server_health_script() -> str:
    return """
        <script>
          (() => {
            const target = document.getElementById("server-health-cards");
            if (!target) return;
            async function refreshServerHealth() {
              try {
                const response = await fetch("/server/health", {
                  headers: { "Accept": "application/json" },
                  cache: "no-store"
                });
                if (!response.ok) return;
                const payload = await response.json();
                if (payload.html) target.innerHTML = payload.html;
              } catch (error) {
                return;
              }
            }
            window.setInterval(refreshServerHealth, 10000);
          })();
        </script>
    """


def icon_svg(name: str) -> str:
    icons = {
        "list": "<rect x='4' y='5' width='16' height='3' rx='1'></rect><rect x='4' y='11' width='16' height='3' rx='1'></rect><rect x='4' y='17' width='16' height='3' rx='1'></rect>",
        "plus": "<path d='M12 5v14'></path><path d='M5 12h14'></path>",
        "logout": "<path d='M10 17l5-5-5-5'></path><path d='M15 12H3'></path><path d='M21 19V5'></path>",
        "invoice": "<path d='M7 3h8l4 4v14l-2-1-2 1-2-1-2 1-2-1-2 1V3z'></path><path d='M15 3v5h5'></path><path d='M9 11h6'></path><path d='M9 15h7'></path>",
        "process": "<path d='M4 19V9'></path><path d='M10 19V5'></path><path d='M16 19v-7'></path><path d='M22 19V3'></path><path d='M2 19h22'></path>",
        "control": "<path d='M4 6h10'></path><path d='M18 6h2'></path><circle cx='16' cy='6' r='2'></circle><path d='M4 12h2'></path><path d='M10 12h10'></path><circle cx='8' cy='12' r='2'></circle><path d='M4 18h10'></path><path d='M18 18h2'></path><circle cx='16' cy='18' r='2'></circle>",
    }
    return f"""
    <svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true">
      {icons.get(name, "")}
    </svg>
    """


def sidebar_link(href: str, label: str, icon: str, active: bool = False) -> str:
    class_name = "active" if active else ""
    return f"""<a class="{class_name}" href="{href}">{icon_svg(icon)}<span>{esc(label)}</span></a>"""


def layout(title: str, body: str, user: str | None = None) -> bytes:
    shell_open = "<main>"
    shell_close = "</main>"
    nav = ""
    if user:
        is_new = title == "Adicionar município"
        is_municipios = title in ("Municípios", "Editar município")
        is_control = title == "Controle"
        is_processes = title == "Acompanhar processos"
        shell_open = f"""
        <div class="app-shell">
          <aside class="sidebar">
            <section class="side-card">
              <h2>Menu</h2>
              <nav class="side-nav">
                {sidebar_link("/", "Municípios", "list", is_municipios)}
                {sidebar_link("/municipios/novo", "Novo", "plus", is_new)}
                {sidebar_link("/processos", "Acompanhar processos", "process", is_processes)}
                {sidebar_link("/controle", "Controle", "control", is_control)}
              </nav>
            </section>
            {server_health_panel()}
          </aside>
          <main class="content">
        """
        shell_close = "</main></div>"
        nav = f"""
          <div class="header-actions">
            <span class="user-pill">Painel administrativo</span>
            <form method="post" action="/logout"><button>{icon_svg("logout")}<span>Sair</span></button></form>
          </div>
        """
    scripts = server_health_script() if user else ""
    return f"""<!doctype html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{esc(title)} - NF Nacional</title>
  <link rel="icon" type="image/png" href="/static/favicon.png">
  <link rel="stylesheet" href="/static/app.css">
  <link rel="stylesheet" href="/static/components.css">
</head>
<body>
  <header>
    <div class="brand">
      <img src="/static/masper-logo.png" alt="Masper">
      <span class="brand-divider"></span>
      <span class="brand-icon">{icon_svg("invoice")}</span>
      <div>
        <strong>NOTA NACIONAL</strong>
      </div>
    </div>
    {nav}
  </header>
  {shell_open}{body}{shell_close}
  {'<script src="/static/app.js" defer></script>' if user else ''}
  {scripts}
</body>
</html>""".encode("utf-8")


class AppHandler(BaseHTTPRequestHandler):
    server_version = "NFNacionalPanel/0.1"

    @property
    def signer(self) -> SessionSigner:
        return self.server.signer  # type: ignore[attr-defined]

    @property
    def db(self) -> MysqlCli:
        return self.server.db  # type: ignore[attr-defined]

    def send_html(self, content: bytes, status: int = 200, headers: dict[str, str] | None = None) -> None:
        self.send_response(status)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("X-Frame-Options", "DENY")
        self.send_header("X-Content-Type-Options", "nosniff")
        self.send_header("Referrer-Policy", "same-origin")
        for key, value in (headers or {}).items():
            self.send_header(key, value)
        self.end_headers()
        self.wfile.write(content)

    def send_json(self, payload: dict[str, object], status: int = 200) -> None:
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("X-Frame-Options", "DENY")
        self.send_header("X-Content-Type-Options", "nosniff")
        self.send_header("Referrer-Policy", "same-origin")
        self.end_headers()
        self.wfile.write(json.dumps(payload, ensure_ascii=False).encode("utf-8"))

    def redirect(self, path: str) -> None:
        self.send_response(303)
        self.send_header("Location", path)
        self.end_headers()

    def get_cookie(self, name: str) -> str | None:
        raw = self.headers.get("Cookie", "")
        jar = cookies.SimpleCookie(raw)
        morsel = jar.get(name)
        return morsel.value if morsel else None

    def current_user(self) -> str | None:
        signed = self.get_cookie("nfn_session")
        if not signed:
            return None
        return self.signer.unsign(signed)

    def require_user(self) -> str | None:
        user = self.current_user()
        if not user:
            self.redirect("/login")
            return None
        return user

    def csrf_token(self) -> str:
        existing = self.get_cookie("nfn_csrf")
        if existing and self.signer.unsign(existing):
            return self.signer.unsign(existing) or ""
        return secrets.token_urlsafe(24)

    def set_csrf_header(self, token: str) -> dict[str, str]:
        return {"Set-Cookie": f"nfn_csrf={self.signer.sign(token)}; HttpOnly; SameSite=Lax; Path=/"}

    def check_csrf(self, token: str) -> bool:
        signed = self.get_cookie("nfn_csrf")
        return bool(signed and self.signer.unsign(signed) == token)

    def do_GET(self) -> None:
        parsed = urlparse(self.path)
        if parsed.path == "/favicon.ico":
            self.path = "/static/favicon.png"
            return self.serve_static()
        if parsed.path.startswith("/static/"):
            return self.serve_static()
        if parsed.path == "/login":
            return self.login_page()
        user = self.require_user()
        if not user:
            return
        if parsed.path == "/server/health":
            return self.server_health(user)
        if parsed.path == "/":
            return self.dashboard(user, parsed.query)
        if parsed.path == "/controle":
            return self.control_page(user, parsed.query)
        if parsed.path == "/municipios/novo":
            return self.new_municipio_page(user)
        if parsed.path in ("/municipios/editar", "/municipios/certificado"):
            return self.edit_municipio_page(user, parsed.query)
        if parsed.path == "/processos":
            return self.processes_page(user)
        if parsed.path == "/processos/detalhes":
            return self.process_detail(parsed.query)
        self.send_error(404)

    def server_health(self, user: str) -> None:
        self.send_json({"html": server_health_cards()})

    def processes_page(self, user: str) -> None:
        rows = list_sync_processes(self.db)
        self.send_html(layout("Acompanhar processos", render_processes_page(rows), user))

    def process_detail(self, query: str) -> None:
        try:
            job_id = int(parse_qs(query).get("job_id", ["0"])[0])
            if job_id <= 0:
                raise ValueError("Processo invalido.")
            detail = get_sync_process_detail(self.db, job_id)
        except ValueError as exc:
            self.send_json({"message": str(exc)}, status=404)
            return
        except Exception as exc:
            self.send_json({"message": f"Falha ao consultar processo: {exc}"}, status=500)
            return
        self.send_json(detail)

    def control_page(self, user: str, query: str = "", error: str = "") -> None:
        token = self.csrf_token()
        params = parse_qs(query)
        current_workers = get_sync_max_workers(self.db)
        msg = f"<p class='error'>{esc(error)}</p>" if error else ""
        done = (
            "<p class='ok'>Configuração salva. O dispatcher aplicará o novo limite aos próximos jobs.</p>"
            if params.get("ok", [""])[0] == "1"
            else ""
        )
        body = f"""
        <div class="title-row">
          <h1>Controle</h1>
        </div>
        {msg}{done}
        <fieldset class="panel-fieldset">
          <legend>Sincronização</legend>
          <form method="post" action="/controle" class="form-grid">
            <input type="hidden" name="csrf" value="{esc(token)}">
            <label>Workers paralelos
              <input
                name="sync_max_workers"
                type="number"
                min="{MIN_SYNC_MAX_WORKERS}"
                max="{MAX_SYNC_MAX_WORKERS}"
                value="{current_workers}"
                required
              >
            </label>
            <div class="summary wide">
              <h2>Para que serve</h2>
              <p>
                Define quantos municípios podem consultar e gravar notas simultaneamente.
                Um valor maior acelera a fila, mas aumenta o uso de CPU, memória, conexões
                MySQL e chamadas ao ADN. O padrão recomendado é 3.
              </p>
              <p>
                Ao reduzir o limite, sincronizações já iniciadas terminam normalmente;
                os próximos jobs respeitam o novo valor.
              </p>
            </div>
            <div class="form-actions">
              <button type="submit">Salvar controle</button>
            </div>
          </form>
        </fieldset>
        """
        self.send_html(layout("Controle", body, user), headers=self.set_csrf_header(token))

    def update_control(self, user: str) -> None:
        length = min(int(self.headers.get("Content-Length", "0")), 10_000)
        data = parse_qs(self.rfile.read(length).decode("utf-8"))
        if not self.check_csrf(data.get("csrf", [""])[0]):
            return self.control_page(user, error="Sessão expirada. Tente novamente.")
        try:
            set_sync_max_workers(self.db, data.get("sync_max_workers", [""])[0])
        except Exception as exc:
            return self.control_page(user, error=str(exc))
        self.redirect("/controle?ok=1")

    def do_POST(self) -> None:
        if self.path == "/login":
            return self.login_post()
        user = self.require_user()
        if not user:
            return
        if self.path == "/logout":
            self.send_response(303)
            self.send_header("Location", "/login")
            self.send_header("Set-Cookie", "nfn_session=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax")
            self.end_headers()
            return
        if self.path == "/municipios":
            return self.create_municipio(user)
        if self.path == "/controle":
            return self.update_control(user)
        if self.path == "/municipios/sync":
            return self.trigger_municipio_sync(user)
        if self.path == "/certificados/validar":
            return self.validate_certificate(user)
        if self.path == "/municipios/editar":
            return self.update_municipio(user)
        self.send_error(404)

    def serve_static(self) -> None:
        name = urlparse(self.path).path[len("/static/") :]
        if "/" in name or ".." in name:
            self.send_error(404)
            return
        path = STATIC_DIR / name
        if not path.exists():
            self.send_error(404)
            return
        content_types = {
            ".png": "image/png",
            ".css": "text/css; charset=utf-8",
            ".js": "text/javascript; charset=utf-8",
        }
        ctype = content_types.get(path.suffix, "application/octet-stream")
        self.send_response(200)
        self.send_header("Content-Type", ctype)
        cache_control = "public, max-age=3600" if path.suffix == ".png" else "no-cache"
        self.send_header("Cache-Control", cache_control)
        self.end_headers()
        self.wfile.write(path.read_bytes())

    def login_page(self, error: str = "") -> None:
        token = self.csrf_token()
        msg = f"<p class='error'>{esc(error)}</p>" if error else ""
        body = f"""
        <section class="login">
          <h1>NOTA NACIONAL</h1>
          {msg}
          <form method="post" action="/login">
            <input type="hidden" name="csrf" value="{esc(token)}">
            <label>Usuário<input name="username" autocomplete="username" required></label>
            <label>Senha<input name="password" type="password" autocomplete="current-password" required></label>
            <button type="submit">Entrar</button>
          </form>
        </section>
        """
        self.send_html(layout("Login", body), headers=self.set_csrf_header(token))

    def login_post(self) -> None:
        length = min(int(self.headers.get("Content-Length", "0")), 10_000)
        data = parse_qs(self.rfile.read(length).decode("utf-8"))
        if not self.check_csrf(data.get("csrf", [""])[0]):
            return self.login_page("Sessão expirada. Tente novamente.")
        username = data.get("username", [""])[0]
        password = data.get("password", [""])[0]
        expected_user = os.environ.get("NFSE_ADMIN_USER", "admin")
        expected_hash = os.environ.get("NFSE_ADMIN_PASSWORD_HASH", "")
        if not expected_hash:
            return self.login_page("Configure NFSE_ADMIN_PASSWORD_HASH antes de usar o painel.")
        if username == expected_user and verify_password(password, expected_hash):
            signed = self.signer.sign(username)
            self.send_response(303)
            self.send_header("Location", "/")
            self.send_header("Set-Cookie", f"nfn_session={signed}; HttpOnly; SameSite=Lax; Path=/")
            self.end_headers()
            return
        self.login_page("Usuário ou senha inválidos.")

    def dashboard(self, user: str, query: str = "") -> None:
        token = self.csrf_token()
        params = parse_qs(query)
        search = params.get("q", [""])[0].strip()
        uf_filter = params.get("uf", [""])[0].strip().upper()
        status_filter = params.get("status", [""])[0].strip()
        cert_filter = params.get("cert", [""])[0].strip()
        sort = params.get("sort", ["codigo_siafi"])[0].strip()
        direction = params.get("dir", ["asc"])[0].strip().lower()
        rows = list_municipios(self.db)
        if search:
            needle = search.lower()
            rows = [
                item
                for item in rows
                if (
                    needle in item.get("nome", "").lower()
                    or needle in (item.get("codigo_ibge") or "")
                    or needle in item.get("codigo_siafi", "")
                )
            ]
        if uf_filter:
            rows = [item for item in rows if item.get("uf", "").upper() == uf_filter]
        if status_filter:
            rows = [item for item in rows if item.get("status", "") == status_filter]
        if cert_filter == "validos":
            rows = [item for item in rows if item.get("certificado_expirado") != "1"]
        elif cert_filter == "expirados":
            rows = [item for item in rows if item.get("certificado_expirado") == "1"]

        sort_keys = {
            "codigo_ibge": lambda item: item.get("codigo_ibge") or "",
            "codigo_siafi": lambda item: item.get("codigo_siafi", ""),
            "nome": lambda item: item.get("nome", "").lower(),
            "uf": lambda item: item.get("uf", ""),
            "last_nsu": lambda item: int(item.get("last_nsu") or "0"),
            "status": lambda item: item.get("status", ""),
            "certificado": lambda item: _parse_datetime_pt(item.get("valido_ate", "")),
            "last_success_at": lambda item: _parse_datetime_pt(item.get("last_success_at", "")),
        }
        if sort not in sort_keys:
            sort = "codigo_siafi"
        reverse = direction == "desc"
        rows = sorted(rows, key=sort_keys[sort], reverse=reverse)

        total_rows = len(rows)
        trs = ""
        for item in rows:
            raw_status = item["status"]
            status = esc(raw_status)
            status_label = STATUS_LABELS.get(raw_status, raw_status)
            ambiente_label = AMBIENTE_LABELS.get(item["ambiente"], item["ambiente"])
            cert_expirado = item.get("certificado_expirado") == "1"
            cert_label = "Expirado" if cert_expirado else item["valido_ate"]
            cert_class = "cert-expired" if cert_expirado else ""
            sync_em_andamento = raw_status in ("pending", "running")
            sync_disabled = " disabled" if cert_expirado or sync_em_andamento else ""
            sync_label = "Processando" if sync_em_andamento else "Sincronizar manual"
            trs += f"""
            <tr>
              <td>{esc(item.get('codigo_ibge') or '-')}</td>
              <td>{esc(item['codigo_siafi'])}</td>
              <td>{esc(item['nome'])}</td>
              <td>{esc(item['uf'])}</td>
              <td>{esc(ambiente_label)}</td>
              <td>{esc(item['last_nsu'])}</td>
              <td>{esc(item.get('last_success_at'))}</td>
              <td><span class="status status-{status}">{esc(status_label)}</span></td>
              <td><span class="{cert_class}">{esc(cert_label)}</span></td>
              <td>
                <div class="action-stack">
                  <form method="post" action="/municipios/sync" class="inline-form">
                    <input type="hidden" name="csrf" value="{esc(token)}">
                    <input type="hidden" name="municipio_id" value="{esc(item['id'])}">
                    <button type="submit"{sync_disabled}>{sync_label}</button>
                  </form>
                  <a class="button secondary" href="/municipios/editar?municipio_id={esc(item['id'])}">Editar</a>
                </div>
              </td>
            </tr>"""
        if not trs:
            trs = "<tr data-empty><td colspan='10'>Nenhum município cadastrado.</td></tr>"
        body = f"""
        <div class="title-row">
          <h1>Municípios</h1>
          <a class="button" href="/municipios/novo">Novo</a>
        </div>
        <fieldset class="panel-fieldset">
          <legend>Filtros</legend>
          <form method="get" action="/" class="filters">
          <label>Busca<input name="q" value="{esc(search)}" placeholder="Nome, SIAFI ou IBGE opcional"></label>
          <label>UF<select name="uf">
            <option value="">Todas</option>
            {select_options([(uf, uf) for uf in UFS], uf_filter)}
          </select></label>
          <label>Status<select name="status">
            {select_options([('', 'Todos'), *[(key, value) for key, value in STATUS_LABELS.items()]], status_filter)}
          </select></label>
          <label>Certificado<select name="cert">
            {select_options([('', 'Todos'), ('validos', 'Válidos'), ('expirados', 'Expirados')], cert_filter)}
          </select></label>
          <label>Ordenar<select name="sort">
            {select_options([('codigo_siafi', 'SIAFI'), ('codigo_ibge', 'IBGE'), ('nome', 'Nome'), ('uf', 'UF'), ('last_nsu', 'Último NSU'), ('last_success_at', 'Última sync'), ('status', 'Status'), ('certificado', 'Certificado')], sort)}
          </select></label>
          <label>Direção<select name="dir">
            {select_options([('asc', 'Crescente'), ('desc', 'Decrescente')], 'desc' if reverse else 'asc')}
          </select></label>
          <div class="filter-actions">
            <button type="submit">Aplicar</button>
            <a class="button secondary" href="/">Limpar</a>
          </div>
          <p class="filter-count">{total_rows} município(s)</p>
          </form>
        </fieldset>
        <fieldset class="panel-fieldset">
          <legend>Municípios</legend>
          <table class=data-table data-page-size=25>
            <thead><tr><th>IBGE</th><th>SIAFI</th><th>Nome</th><th>UF</th><th>Ambiente</th><th>Último NSU</th><th>Última sync</th><th>Status</th><th>Certificado até</th><th>Ações</th></tr></thead>
            <tbody>{trs}</tbody>
          </table>
        </fieldset>
        """
        self.send_html(layout("Municípios", body, user), headers=self.set_csrf_header(token))

    def new_municipio_page(self, user: str, error: str = "", ok: str = "") -> None:
        self.municipio_form_page(user, mode="create", error=error, ok=ok)

    def edit_municipio_page(self, user: str, query: str, error: str = "", ok: str = "") -> None:
        params = parse_qs(query)
        try:
            municipio_id = int(params.get("municipio_id", ["0"])[0])
            municipio = get_municipio_certificado(self.db, municipio_id)
        except Exception as exc:
            self.send_error(404, str(exc))
            return
        self.municipio_form_page(user, mode="edit", municipio=municipio, error=error, ok=ok)

    def municipio_form_page(
        self,
        user: str,
        *,
        mode: str,
        municipio: dict[str, str] | None = None,
        error: str = "",
        ok: str = "",
    ) -> None:
        token = self.csrf_token()
        msg = f"<p class='error'>{esc(error)}</p>" if error else ""
        done = f"<p class='ok'>{esc(ok)}</p>" if ok else ""
        is_edit = mode == "edit"
        municipio = municipio or {}
        form_id = "municipio-form"
        action = "/municipios/editar" if is_edit else "/municipios"
        title = "Editar município" if is_edit else "Adicionar município"
        submit = "Salvar alterações" if is_edit else "Cadastrar município"
        cert_required = "" if is_edit else " required"
        password_required = "" if is_edit else " required"
        codigo_field = (
            f"""
            <label>Codigo IBGE (opcional)<input name="codigo_ibge" maxlength="7" pattern="[0-9]{7}" value="{esc(municipio.get('codigo_ibge'))}"></label>
            <label>Código SIAFI<input value="{esc(municipio.get('codigo_siafi'))}" disabled></label>
            <input type="hidden" name="municipio_id" value="{esc(municipio.get('id'))}">
            """
            if is_edit
            else """
            <label>Codigo IBGE (opcional)<input name="codigo_ibge" maxlength="7" pattern="[0-9]{7}"></label>
            <label>Código SIAFI<input name="codigo_siafi" maxlength="10" pattern="[0-9]{1,10}" required></label>
            """
        )
        cert_expirado = municipio.get("certificado_expirado") == "1"
        cert_label = "Expirado" if cert_expirado else municipio.get("valido_ate", "")
        cert_status = (
            "<p class='error inline-message'>Certificado expirado. Envie um novo certificado para liberar a sincronização.</p>"
            if cert_expirado
            else ""
        )
        current_cert = ""
        if is_edit:
            subject = municipio.get("subject_name") or "Não informado"
            documento = municipio.get("documento_federal") or "Não informado"
            ibge = municipio.get("codigo_ibge") or ""
            ibge_summary = f" / IBGE {esc(ibge)}" if ibge else ""
            current_cert = f"""
            <section class="summary">
              <h2>Certificado associado</h2>
              <div class="summary-grid">
                <p><span>Municipio</span><strong>{esc(municipio.get('nome'))} - SIAFI {esc(municipio.get('codigo_siafi'))}{ibge_summary} / {esc(municipio.get('uf'))}</strong></p>
                <p><span>Arquivo</span><strong>{esc(municipio.get('certificado_nome_original'))}</strong></p>
                <p><span>Vencimento</span><strong class="{'cert-expired' if cert_expirado else ''}">{esc(cert_label)}</strong></p>
                <p><span>Documento</span><strong>{esc(documento)}</strong></p>
                <p class="wide"><span>Sujeito</span><strong>{esc(subject)}</strong></p>
              </div>
              {cert_status}
            </section>
            """
        password_hint = (
            "<p class='hint'>Sem novo arquivo, a senha informada atualiza o certificado atual.</p>" if is_edit else ""
        )
        body = f"""
        <div class="title-row">
          <h1>{title}</h1>
          <a class="button secondary" href="/">Voltar</a>
        </div>
        {msg}{done}
        {current_cert}
        <form method="post" action="{action}" enctype="multipart/form-data" class="form-grid" id="{form_id}">
          <input type="hidden" name="csrf" value="{esc(token)}">
          {codigo_field}
          <label>Nome<input name="nome" value="{esc(municipio.get('nome'))}" required></label>
          <label>UF<select name="uf" required>{uf_options(municipio.get('uf', 'RS'))}</select></label>
          <input type="hidden" name="ambiente" value="producao">
          <fieldset class="cert-box">
            <legend>Certificado</legend>
            <label>{'Novo certificado A1' if is_edit else 'Certificado A1'} (.pfx/.p12)<input name="pfx" type="file" accept=".pfx,.p12"{cert_required}></label>
            <label>Senha do PFX<input name="pfx_password" type="password"{password_required}></label>
            <button type="button" data-validate-cert>Confirmar autenticidade</button>
            <p class="hint" id="cert-validation-result"></p>
            {password_hint}
          </fieldset>
          <div class="form-actions">
            <button type="submit">{submit}</button>
          </div>
        </form>
        {certificate_validation_script(form_id, "cert-validation-result")}
        """
        self.send_html(layout(title, body, user), headers=self.set_csrf_header(token))

    def create_municipio(self, user: str) -> None:
        ctype, pdict = cgi.parse_header(self.headers.get("Content-Type", ""))
        if ctype != "multipart/form-data":
            return self.new_municipio_page(user, "Formulário inválido.")
        length = int(self.headers.get("Content-Length", "0"))
        if length > MAX_UPLOAD_BYTES:
            return self.new_municipio_page(user, "Arquivo muito grande.")
        form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ={"REQUEST_METHOD": "POST"})
        csrf = form.getfirst("csrf", "")
        if not self.check_csrf(csrf):
            return self.new_municipio_page(user, "Sessão expirada. Tente novamente.")
        try:
            pfx_item = form["pfx"]
            pfx_data = pfx_item.file.read()
            filename = Path(getattr(pfx_item, "filename", "certificado.pfx")).name
            if not filename.lower().endswith((".pfx", ".p12")):
                raise ValueError("Envie um arquivo .pfx ou .p12.")
            password = form.getfirst("pfx_password", "")
            cert_info = validate_pfx(pfx_data, password)
            municipio_id = upsert_municipio_with_certificate(
                self.db,
                codigo_ibge=form.getfirst("codigo_ibge", ""),
                codigo_siafi=form.getfirst("codigo_siafi", ""),
                nome=form.getfirst("nome", ""),
                uf=form.getfirst("uf", ""),
                ambiente="producao",
                pfx_data=pfx_data,
                pfx_password=password,
                original_filename=filename,
                cert_info=cert_info,
            )
        except Exception as exc:
            return self.new_municipio_page(user, str(exc))
        self.redirect("/")

    def update_municipio(self, user: str) -> None:
        ctype, _pdict = cgi.parse_header(self.headers.get("Content-Type", ""))
        if ctype != "multipart/form-data":
            self.send_error(400, "Formulário inválido.")
            return
        length = int(self.headers.get("Content-Length", "0"))
        if length > MAX_UPLOAD_BYTES:
            self.send_error(400, "Arquivo muito grande.")
            return
        form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ={"REQUEST_METHOD": "POST"})
        municipio_id = int(form.getfirst("municipio_id", "0") or "0")
        if not self.check_csrf(form.getfirst("csrf", "")):
            return self.edit_municipio_page(user, f"municipio_id={municipio_id}", error="Sessão expirada. Tente novamente.")
        try:
            if municipio_id <= 0:
                raise ValueError("Município inválido.")
            nome = form.getfirst("nome", "").strip()
            uf = form.getfirst("uf", "").strip().upper()
            codigo_ibge = form.getfirst("codigo_ibge", "").strip()
            if not nome:
                raise ValueError("Nome do município é obrigatório.")
            if uf not in UFS:
                raise ValueError("UF inválida.")

            pfx_item = form["pfx"] if "pfx" in form else None
            filename = Path(getattr(pfx_item, "filename", "")).name if pfx_item is not None else ""
            password = form.getfirst("pfx_password", "")
            if filename:
                if not filename.lower().endswith((".pfx", ".p12")):
                    raise ValueError("Envie um arquivo .pfx ou .p12.")
                if not password:
                    raise ValueError("Informe a senha do PFX.")
                pfx_data = pfx_item.file.read()
                cert_info = validate_pfx(pfx_data, password)
                replace_municipio_certificate(
                    self.db,
                    municipio_id=municipio_id,
                    pfx_data=pfx_data,
                    pfx_password=password,
                    original_filename=filename,
                    cert_info=cert_info,
                )
            elif password:
                update_municipio_certificate_password(
                    self.db,
                    municipio_id=municipio_id,
                    pfx_password=password,
                )
            update_municipio_fields(
                self.db,
                municipio_id=municipio_id,
                nome=nome,
                uf=uf,
                codigo_ibge=codigo_ibge,
            )
        except Exception as exc:
            return self.edit_municipio_page(user, f"municipio_id={municipio_id}", error=str(exc))
        self.edit_municipio_page(user, f"municipio_id={municipio_id}", ok="Município atualizado com sucesso.")

    def trigger_municipio_sync(self, user: str) -> None:
        length = min(int(self.headers.get("Content-Length", "0")), 10_000)
        data = parse_qs(self.rfile.read(length).decode("utf-8"))
        if not self.check_csrf(data.get("csrf", [""])[0]):
            self.send_error(403)
            return
        try:
            municipio_id = int(data.get("municipio_id", ["0"])[0])
            if municipio_id <= 0:
                raise ValueError("Município inválido.")
            trigger_sync(self.db, municipio_id)
        except Exception as exc:
            self.send_error(400, str(exc))
            return
        self.redirect("/")

    def validate_certificate(self, user: str) -> None:
        ctype, _pdict = cgi.parse_header(self.headers.get("Content-Type", ""))
        if ctype != "multipart/form-data":
            return self.send_json({"ok": False, "message": "Formulário inválido."}, status=400)
        length = int(self.headers.get("Content-Length", "0"))
        if length > MAX_UPLOAD_BYTES:
            return self.send_json({"ok": False, "message": "Arquivo muito grande."}, status=400)
        form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ={"REQUEST_METHOD": "POST"})
        if not self.check_csrf(form.getfirst("csrf", "")):
            return self.send_json({"ok": False, "message": "Sessão expirada. Tente novamente."}, status=403)
        try:
            pfx_item = form["pfx"] if "pfx" in form else None
            filename = Path(getattr(pfx_item, "filename", "")).name if pfx_item is not None else ""
            password = form.getfirst("pfx_password", "")
            if filename:
                if not filename.lower().endswith((".pfx", ".p12")):
                    raise ValueError("Envie um arquivo .pfx ou .p12.")
                pfx_data = pfx_item.file.read()
            else:
                municipio_id = int(form.getfirst("municipio_id", "0") or "0")
                if municipio_id <= 0:
                    raise ValueError("Selecione um arquivo .pfx ou .p12 para validar.")
                if not password:
                    raise ValueError("Informe a senha do PFX.")
                municipio = get_municipio_certificado(self.db, municipio_id)
                if not municipio.get("certificado_id"):
                    raise ValueError("Município não possui certificado ativo.")
                pfx_data = decrypt_certificate_value(
                    municipio["certificado_criptografado"],
                    codigo_siafi=municipio["codigo_siafi"],
                    key_id=municipio.get("certificado_key_id"),
                )
            cert_info = validate_pfx(pfx_data, password)
        except Exception as exc:
            return self.send_json({"ok": False, "message": str(exc)}, status=400)
        valido_ate = format_datetime_pt(cert_info.not_valid_after)
        return self.send_json({"ok": True, "message": f"Certificado válido e dentro do prazo. Vencimento: {valido_ate}."})


def run() -> None:
    host = os.environ.get("NFSE_WEB_HOST", "127.0.0.1")
    port = int(os.environ.get("NFSE_WEB_PORT", "8080"))
    server = ThreadingHTTPServer((host, port), AppHandler)
    server.signer = SessionSigner.from_env()  # type: ignore[attr-defined]
    server.db = MysqlCli.from_env()  # type: ignore[attr-defined]
    print(f"NF Nacional painel em http://{host}:{port}")
    start_sync_queue_dispatcher(server.db.database)
    server.serve_forever()


if __name__ == "__main__":
    run()
