"""Provisionamento infra: HAProxy SNI (CT114), Traefik, certificado LE, Carbonio."""

from __future__ import annotations

import re
import subprocess
from typing import Any

import httpx

from app.config import settings
from app.services import activity_log, carbonio, dns_verify, carbonio_cache
from app.services.domain_registry import get_domain_record
from app.services.mail_aliases import sanitize_mail_aliases

CERT_NAME = "mail-vm112-multi"
TRAEFIK_SERVICE = "dratcoin-Service"
SNI_MAP = "/root/traefik/haproxy-mail-sni/maps/sni_vm112.lst"
TRAEFIK_DYNAMIC = "/root/traefik/dynamic.yml"
DEPLOY_SCRIPT = "/usr/local/bin/carbonio-cert-deploy.sh"


def _mail_host(domain: str) -> str:
    return f"mail.{domain.lower().strip()}"


def _router_key_for_host(mail_host: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", mail_host.lower()).strip("-")
    return f"mail-{slug}-Router"


def _router_key(domain: str) -> str:
    return _router_key_for_host(_mail_host(domain))


def _collect_mail_hosts(domain: str, mail_aliases: list[str] | None = None) -> list[str]:
    primary = _mail_host(domain)
    hosts = [primary]
    for h in mail_aliases or []:
        hh = h.lower().strip().rstrip(".")
        if hh and hh not in hosts:
            hosts.append(hh)
    return hosts


def _cert_san_hostnames(all_hosts: list[str]) -> list[str]:
    """
    Hostnames para certificado LE — só webmail (mail.* / webmail.*).
    Exclui portal.* e outros aliases que não servem Carbonio directamente.
    """
    cert: list[str] = []
    for h in all_hosts:
        hl = h.lower().strip()
        if hl.startswith("portal."):
            continue
        if hl.startswith("mail.") or hl.startswith("webmail."):
            cert.append(hl)
    return sorted(set(cert))


def _normalize_cert_sans(existing_sans: list[str], cert_hosts: list[str]) -> list[str]:
    """Mantém SANs activos no certificado multi-domínio."""
    result = set(cert_hosts)
    for san in existing_sans:
        s = san.lower().strip()
        if not (s.startswith("mail.") or s.startswith("webmail.")):
            continue
        if s.startswith("portal."):
            continue
        result.add(s)
    return sorted(result)


def _ssh_ct114(command: str, timeout: int = 90) -> tuple[bool, str]:
    host = settings.traefik_ssh_host
    try:
        proc = subprocess.run(
            [
                "ssh",
                "-o",
                "BatchMode=yes",
                "-o",
                "StrictHostKeyChecking=accept-new",
                host,
                command,
            ],
            capture_output=True,
            text=True,
            timeout=timeout,
        )
        out = (proc.stdout or "") + (proc.stderr or "")
        return proc.returncode == 0, out.strip()
    except (subprocess.TimeoutExpired, OSError) as e:
        return False, str(e)


def _read_ct114_file(path: str) -> tuple[bool, str]:
    ok, out = _ssh_ct114(f"cat {path} 2>/dev/null || true")
    return ok, out


def _cert_domains() -> list[str]:
    try:
        proc = subprocess.run(
            ["certbot", "certificates"],
            capture_output=True,
            text=True,
            timeout=60,
        )
        text = proc.stdout or ""
        for block in text.split("Certificate Name:"):
            if CERT_NAME in block:
                m = re.search(r"Domains:\s*(.+)", block)
                if m:
                    return m.group(1).split()
    except (subprocess.TimeoutExpired, OSError):
        pass
    return []


def get_status(domain: str, mail_aliases: list[str] | None = None) -> dict[str, Any]:
    domain = domain.lower().strip()
    cache_key = f"infra_status:{domain}"
    cached = carbonio_cache.get(cache_key)
    if cached is not None:
        return dict(cached)
    mail = _mail_host(domain)
    if mail_aliases is not None:
        aliases = sanitize_mail_aliases(mail_aliases, domain)
    else:
        rec = get_domain_record(domain)
        aliases = list(rec.get("mail_aliases") or []) if rec else []
    all_hosts = _collect_mail_hosts(domain, aliases)
    steps: list[dict[str, Any]] = []

    # 1 Carbonio
    c_ok = carbonio.domain_exists(domain)
    c_msg = (
        "Domínio activo no Carbonio"
        if c_ok
        else "Aguarda «Criar conta agora» (zmprov cd + ca)"
    )
    steps.append({"id": "carbonio_domain", "label": "Carbonio (domínio)", "ok": c_ok, "message": c_msg})

    # 2 DNS público
    try:
        v = dns_verify.verify_mail_dns(domain, settings.mail_public_ip)
        d_ok = bool(v.get("ready"))
        d_msg = "MX e A mail OK" if d_ok else "DNS mail incompleto"
    except Exception as e:
        d_ok = False
        d_msg = str(e)
    steps.append({"id": "dns_mail", "label": "DNS público (MX/A)", "ok": d_ok, "message": d_msg})

    # 3 HAProxy SNI
    ok_ssh, sni_body = _read_ct114_file(SNI_MAP)
    if not ok_ssh:
        s_ok, s_msg = False, f"SSH CT114 indisponível: {sni_body[:120]}"
    else:
        sni_lines = {ln.strip() for ln in sni_body.splitlines() if ln.strip()}
        missing = [h for h in all_hosts if h not in sni_lines]
        s_ok = len(missing) == 0
        s_msg = (
            f"{len(all_hosts)} hostname(s) no SNI"
            if s_ok
            else f"Faltam no SNI: {', '.join(missing)}"
        )
    steps.append({"id": "haproxy_sni", "label": "HAProxy SNI (CT114)", "ok": s_ok, "message": s_msg})

    # 4 Traefik router
    ok_ssh, dyn = _read_ct114_file(TRAEFIK_DYNAMIC)
    if not ok_ssh:
        t_ok, t_msg = False, f"SSH CT114: {dyn[:120]}"
    else:
        missing_t = [h for h in all_hosts if f"Host(`{h}`)" not in dyn]
        t_ok = len(missing_t) == 0
        t_msg = (
            f"Routers Traefik ({len(all_hosts)} hostnames)"
            if t_ok
            else f"Faltam routers: {', '.join(missing_t)}"
        )
    steps.append({"id": "traefik_router", "label": "Traefik HTTPS (CT114)", "ok": t_ok, "message": t_msg})

    # 5 Cert SAN (só hostnames mail.* — não portal.*)
    cert_hosts = _cert_san_hostnames(all_hosts)
    sans = _cert_domains()
    missing_cert = [h for h in cert_hosts if h not in sans]
    cert_ok = len(missing_cert) == 0
    steps.append(
        {
            "id": "cert_san",
            "label": "Certificado LE (VM112)",
            "ok": cert_ok,
            "message": (
                f"SAN inclui {len(cert_hosts)} hostname(s) webmail"
                if cert_ok
                else f"Faltam no cert: {', '.join(missing_cert)}"
            ),
        }
    )

    # 6 Webmail HTTPS
    try:
        with httpx.Client(timeout=12.0, verify=False) as client:
            r = client.get(f"https://{mail}/", follow_redirects=True)
        w_ok = r.status_code < 500
        w_msg = f"HTTPS {r.status_code}" if w_ok else f"HTTPS erro {r.status_code}"
    except Exception as e:
        w_ok = False
        w_msg = str(e)[:80]
    steps.append({"id": "webmail_https", "label": "Webmail HTTPS", "ok": w_ok, "message": w_msg})

    all_ok = all(s["ok"] for s in steps)
    result = {
        "domain": domain,
        "mail_host": mail,
        "mail_aliases": aliases,
        "mail_hosts": all_hosts,
        "steps": steps,
        "ready": all_ok,
    }
    carbonio_cache.set(cache_key, result, carbonio_cache.TTL_INFRA_STATUS)
    return result


def provision(
    domain: str,
    step_id: str | None = None,
    mail_aliases: list[str] | None = None,
) -> dict[str, Any]:
    domain = domain.lower().strip()
    if mail_aliases is not None:
        aliases = sanitize_mail_aliases(mail_aliases, domain)
    else:
        rec = get_domain_record(domain)
        aliases = list(rec.get("mail_aliases") or []) if rec else []
    all_hosts = _collect_mail_hosts(domain, aliases)
    results: list[dict[str, Any]] = []

    def run(step: str, fn) -> None:
        if step_id and step_id != step:
            return
        activity_log.info(f"Infra: iniciar {step}", source="traefik")
        try:
            msg = fn()
            activity_log.ok(f"Infra {step}: {msg}", source="traefik")
            results.append({"id": step, "ok": True, "message": msg})
        except Exception as e:
            activity_log.error(f"Infra {step}: {e}", source="traefik")
            results.append({"id": step, "ok": False, "message": str(e)})

    def do_sni() -> str:
        ok, body = _read_ct114_file(SNI_MAP)
        if not ok:
            raise RuntimeError(body)
        added: list[str] = []
        for host in all_hosts:
            if host in body.splitlines():
                continue
            cmd = f"grep -qxF '{host}' {SNI_MAP} || echo '{host}' >> {SNI_MAP}"
            ok2, out = _ssh_ct114(cmd, timeout=60)
            if not ok2:
                raise RuntimeError(out)
            added.append(host)
        if added:
            ok3, out = _ssh_ct114(
                "cd /root/traefik && docker compose restart mail-sni-proxy 2>&1 | tail -2",
                timeout=120,
            )
            if not ok3:
                raise RuntimeError(out)
        return f"SNI: {len(all_hosts)} hostname(s)" + (f" (+{len(added)} novos)" if added else " (já OK)")

    def do_traefik() -> str:
        ok, body = _read_ct114_file(TRAEFIK_DYNAMIC)
        if not ok:
            raise RuntimeError(body)
        added: list[str] = []
        for host in all_hosts:
            if f"Host(`{host}`)" in body:
                continue
            r_key = _router_key_for_host(host)
            remote = f"""python3 <<'PY'
from pathlib import Path
p = Path("{TRAEFIK_DYNAMIC}")
text = p.read_text()
mail = "{host}"
rkey = "{r_key}"
svc = "{TRAEFIK_SERVICE}"
if f"Host(`{{mail}}`)" in text:
    print("exists")
else:
    block = f'''
    {{rkey}}:
      rule: Host(`{{mail}}`)
      service: {{svc}}
      entryPoints:
      - websecure
      tls:
        certResolver: letsencrypt
      middlewares:
      - default-headers
'''
    idx = text.find("  services:")
    if idx < 0:
        raise SystemExit("services: não encontrado")
    p.write_text(text[:idx] + block + text[idx:])
    print("added")
PY"""
            ok2, out = _ssh_ct114(remote, timeout=90)
            if not ok2:
                raise RuntimeError(out)
            added.append(host)
        if added:
            ok3, out2 = _ssh_ct114(
                "cd /root/traefik && docker compose restart traefik 2>&1 | tail -3",
                timeout=120,
            )
            if not ok3:
                raise RuntimeError(out2)
        return f"Traefik: {len(all_hosts)} hostname(s)" + (f" (+{len(added)} routers)" if added else " (já OK)")

    def do_cert() -> str:
        cert_hosts = _cert_san_hostnames(all_hosts)
        sans = _cert_domains()
        missing = [h for h in cert_hosts if h not in sans]
        if not missing:
            return f"SAN já inclui {len(cert_hosts)} hostname(s) webmail"

        try:
            from app.services.cloudflare import CloudflareDNS

            zone = CloudflareDNS().get_zone_by_name(domain)
            if zone and zone.get("status") != "active":
                ns = ", ".join(zone.get("name_servers") or [])
                raise RuntimeError(
                    f"Zona Cloudflare «{domain}» ainda não activa (status: {zone.get('status')}). "
                    f"Altere os nameservers no registrador para: {ns}"
                )
        except RuntimeError:
            raise
        except Exception:
            pass

        new_domains = _normalize_cert_sans(sans, cert_hosts)
        dom_args = " ".join(f"-d {d}" for d in new_domains)
        creds = settings.certbot_cloudflare_credentials
        cmd = (
            f"certbot certonly --dns-cloudflare "
            f"--dns-cloudflare-credentials {creds} "
            f"--dns-cloudflare-propagation-seconds 90 "
            f"--cert-name {CERT_NAME} --expand {dom_args} "
            f"--non-interactive --agree-tos"
        )
        proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=600)
        if proc.returncode != 0:
            err = (proc.stderr or proc.stdout or "certbot falhou")[:500]
            raise RuntimeError(
                f"{err} — confirme NS Cloudflare do domínio e aguarde propagação DNS."
            )
        lineage = f"/etc/letsencrypt/live/{CERT_NAME}"
        deploy = subprocess.run(
            f"RENEWED_LINEAGE={lineage} {DEPLOY_SCRIPT}",
            shell=True,
            capture_output=True,
            text=True,
            timeout=180,
        )
        if deploy.returncode != 0:
            raise RuntimeError((deploy.stderr or deploy.stdout or "deploy falhou")[:300])
        return f"certificado expandido ({len(new_domains)} SANs) e deploy Carbonio"

    run("haproxy_sni", do_sni)
    run("traefik_router", do_traefik)
    if step_id in (None, "cert_san", "carbonio_cert"):
        if not step_id or step_id == "cert_san":
            run("cert_san", do_cert)

    status = get_status(domain, aliases)
    return {"domain": domain, "results": results, "status": status, "mail_aliases": aliases}
