"""Listagem e purge de domínios orquestrados na VM112 (Spec 017 + 026)."""

from __future__ import annotations

import json
import re
import shutil
import subprocess
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any, Callable

from app.config import settings
from app.services import activity_log, carbonio, carbonio_cache, domain_registry, purge_jobs
from app.services.domain_site_layout import PLATFORM_DOMAINS, SITES_ROOT
from app.services.infrastructure import _mail_host
from app.services.traefik_purge import (
    _collect_purge_hosts,
    purge_traefik_routers,
    purge_traefik_sni,
    rollback_traefik_backup,
    validate_traefik_after_purge,
)
from app.services.purge_snapshot import build_audit_snapshot
from app.services.purge_openpanel import purge_openpanel
from app.services.purge_pmg import purge_pmg

_LOG_DIR = Path("/var/log/ibytera-mail-portal/sessions")
_WIZARD_PORTAL_USERS_DIR = Path("/var/lib/ligbox-wizard/portal_users")
_TENANT_BRANDING = Path("/opt/ligbox-wizard/backend/app/services/tenant_branding.py")
_DEPLOY_SCRIPTS = Path("/opt/ligbox-deploy/scripts/admin-login-check")
_TRAEFIK_CERT_EXPORT = Path("/opt/zextras/ssl/letsencrypt/traefik-export")
_NGINX_INCLUDES = Path("/opt/zextras/conf/nginx/includes")
_PURGE_BLOCKLIST = PLATFORM_DOMAINS | frozenset({"itecnologys.com"})


def _domain_slug(domain: str) -> str:
    return domain.lower().strip().replace(".", "-")


def _list_carbonio_domains() -> list[str]:
    return carbonio.list_all_domains()


def _list_site_domains() -> list[str]:
    if not SITES_ROOT.is_dir():
        return []
    return [
        p.name.lower()
        for p in SITES_ROOT.iterdir()
        if p.is_dir() and (p / "domain.json").is_file()
    ]


def _portal_users_for_domain(domain: str, users_dir: Path | None = None) -> list[dict[str, Any]]:
    domain = domain.lower().strip()
    base = users_dir or Path(settings.portal_users_dir)
    if not base.is_dir():
        return []
    found: list[dict[str, Any]] = []
    for f in base.glob("*.json"):
        try:
            data = json.loads(f.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        email = (data.get("planned_corporate_email") or "").lower().strip()
        if email.endswith(f"@{domain}"):
            found.append({
                "login_id": data.get("login_id") or f.stem,
                "planned_corporate_email": email,
                "path": str(f),
            })
    return found


def _build_orchestrated_domains(query: str = "") -> list[dict[str, Any]]:
    carbonio_set = set(_list_carbonio_domains())
    names = sorted(carbonio_set | set(_list_site_domains()))
    users_by_domain: dict[str, list[dict[str, Any]]] = {}
    users_dir = Path(settings.portal_users_dir)
    if users_dir.is_dir():
        for f in users_dir.glob("*.json"):
            try:
                data = json.loads(f.read_text(encoding="utf-8"))
            except (json.JSONDecodeError, OSError):
                continue
            email = (data.get("planned_corporate_email") or "").lower().strip()
            if "@" not in email:
                continue
            dom = email.split("@", 1)[1]
            users_by_domain.setdefault(dom, []).append({
                "login_id": data.get("login_id") or f.stem,
                "planned_corporate_email": email,
                "path": str(f),
            })
    if _WIZARD_PORTAL_USERS_DIR.is_dir():
        for f in _WIZARD_PORTAL_USERS_DIR.glob("*.json"):
            try:
                data = json.loads(f.read_text(encoding="utf-8"))
            except (json.JSONDecodeError, OSError):
                continue
            email = (data.get("planned_corporate_email") or "").lower().strip()
            if "@" not in email:
                continue
            dom = email.split("@", 1)[1]
            entry = {
                "login_id": data.get("login_id") or f.stem,
                "planned_corporate_email": email,
                "path": str(f),
            }
            if entry not in users_by_domain.get(dom, []):
                users_by_domain.setdefault(dom, []).append(entry)
    q = (query or "").strip().lower()
    items: list[dict[str, Any]] = []
    for domain in names:
        if domain in PLATFORM_DOMAINS:
            continue
        rec = domain_registry.get_domain_record(domain) or {}
        admin_email = rec.get("portal_admin_email") or ""
        exists = domain in carbonio_set
        item = {
            "domain": domain,
            "mail_host": _mail_host(domain),
            "portal_admin_email": admin_email,
            "carbonio_exists": exists,
            "site_folder_exists": (SITES_ROOT / domain / "domain.json").is_file(),
            "account_count": None,
            "accounts_preview": [],
            "portal_users": users_by_domain.get(domain, []),
            "updated_at": rec.get("updated_at"),
        }
        if q:
            blob = " ".join([
                domain,
                admin_email,
                " ".join(u.get("login_id", "") for u in item["portal_users"]),
            ]).lower()
            if q not in blob:
                continue
        items.append(item)
    return items


def list_orchestrated_domains(query: str = "") -> list[dict[str, Any]]:
    q = (query or "").strip().lower()
    if not q:
        cached = carbonio_cache.get(carbonio_cache.CACHE_KEY_ORCH_LIST)
        if cached is not None:
            return cached
    items = _build_orchestrated_domains(q)
    if not q:
        carbonio_cache.set(
            carbonio_cache.CACHE_KEY_ORCH_LIST,
            items,
            carbonio_cache.TTL_ORCH_LIST,
        )
    return items


def list_orchestrated_domains_meta(query: str = "") -> dict[str, Any]:
    items = list_orchestrated_domains(query)
    age = carbonio_cache.entry_age_sec(carbonio_cache.CACHE_KEY_ALL_DOMAINS)
    return {
        "domains": items,
        "cached": age is not None,
        "cache_age_sec": age,
    }


def get_domain_detail(domain: str) -> dict[str, Any]:
    domain = domain.lower().strip()
    detail_key = f"domain_detail:{domain}"
    cached = carbonio_cache.get(detail_key)
    if cached is not None:
        return dict(cached)

    matches = [i for i in list_orchestrated_domains() if i["domain"] == domain]
    if not matches:
        raise ValueError(f"Domínio {domain} não encontrado na orquestração")
    base = dict(matches[0])
    from app.services.infrastructure import get_status

    rec = domain_registry.get_domain_record(domain) or {}
    aliases = list(rec.get("mail_aliases") or [])
    base["domain_record"] = rec
    carbonio_exists = bool(base.get("carbonio_exists"))

    def fetch_accounts() -> list[str]:
        if not carbonio_exists:
            return []
        try:
            return carbonio.list_accounts(domain)
        except carbonio.CarbonioError:
            return []

    def fetch_infra() -> dict[str, Any]:
        return get_status(domain, aliases)

    def fetch_cf() -> dict[str, Any]:
        try:
            from app.services.cloudflare import CloudflareDNS

            return CloudflareDNS().get_zone_by_name(domain)
        except Exception as e:
            return {"error": str(e)}

    with ThreadPoolExecutor(max_workers=3) as pool:
        fut_acc = pool.submit(fetch_accounts)
        fut_inf = pool.submit(fetch_infra)
        fut_cf = pool.submit(fetch_cf)
        base["accounts"] = fut_acc.result()
        base["infra_status"] = fut_inf.result()
        base["cloudflare_zone"] = fut_cf.result()

    carbonio_cache.set(detail_key, base, carbonio_cache.TTL_DOMAIN_DETAIL)
    return base


def _delete_carbonio_accounts(domain: str) -> list[str]:
    logs: list[str] = []
    accounts: list[str] = []
    try:
        accounts = carbonio.list_accounts(domain)
    except carbonio.CarbonioError:
        pass
    for email in accounts:
        code, _out, _err = carbonio._zmprov_run("da", email, log_cmd=True)
        logs.append(f"da {email}: rc={code}")
    return logs


def _delete_carbonio_domain_only(domain: str) -> str | None:
    if carbonio.domain_exists(domain, use_cache=False):
        code, _out, _err = carbonio._zmprov_run("dd", domain, log_cmd=True)
        carbonio.invalidate_domain_list_cache()
        return f"dd {domain}: rc={code}"
    return None


def _purge_portal_users(domain: str) -> list[str]:
    removed: list[str] = []
    for users_dir in (Path(settings.portal_users_dir), _WIZARD_PORTAL_USERS_DIR):
        for u in _portal_users_for_domain(domain, users_dir):
            try:
                Path(u["path"]).unlink(missing_ok=True)
                label = f"{u['login_id']}@{users_dir.name}"
                if label not in removed:
                    removed.append(label)
            except OSError as e:
                removed.append(f"ERR:{u['login_id']}:{e}")
    return removed


def _purge_site_folder(domain: str) -> bool:
    path = SITES_ROOT / domain
    if path.is_dir():
        shutil.rmtree(path)
        return True
    return False


def _purge_cloudflare_zone(domain: str) -> str:
    try:
        from app.services.cloudflare import CloudflareDNS, CloudflareError

        cf = CloudflareDNS()
        zone = cf.get_zone_by_name(domain)
        if not zone:
            return "no_zone"
        zid = zone.get("id")
        with cf._client() as c:
            r = c.delete(f"https://api.cloudflare.com/client/v4/zones/{zid}")
            data = r.json()
            if data.get("success"):
                return f"deleted:{zid}"
            raise CloudflareError(str(data.get("errors")))
    except Exception as e:
        return f"error:{e}"


def _purge_tenant_branding(domain: str) -> str:
    if not _TENANT_BRANDING.is_file():
        return "skip"
    text = _TENANT_BRANDING.read_text(encoding="utf-8")
    needle = f'"{domain}"'
    if needle not in text:
        return "absent"
    new_text = re.sub(rf'^\s*"{re.escape(domain)}".*\n', "", text, flags=re.MULTILINE)
    _TENANT_BRANDING.write_text(new_text, encoding="utf-8")
    return "removed"


def _purge_deploy_script_refs(domain: str) -> str:
    mail_host = _mail_host(domain)
    changed = []
    for name in ("apply-admin-nginx-overrides.py", "sync-traefik-admin-certs.sh"):
        path = _DEPLOY_SCRIPTS / name
        if not path.is_file():
            continue
        text = path.read_text(encoding="utf-8")
        if mail_host not in text:
            continue
        new_text = re.sub(rf'^\s*"{re.escape(mail_host)}".*\n', "", text, flags=re.MULTILINE)
        new_text = new_text.replace(f" {mail_host}", "")
        new_text = new_text.replace(f"'{mail_host}'", "")
        path.write_text(new_text, encoding="utf-8")
        changed.append(name)
    return ",".join(changed) if changed else "none"


def _purge_traefik_export_certs(domain: str) -> str:
    slug = _domain_slug(domain)
    pattern = f"mail-{slug}*"
    removed = 0
    if _TRAEFIK_CERT_EXPORT.is_dir():
        for f in _TRAEFIK_CERT_EXPORT.glob(pattern):
            f.unlink(missing_ok=True)
            removed += 1
    return str(removed)


def _remove_nginx_server_block(text: str, server_name: str) -> tuple[str, bool]:
    pattern = re.compile(
        rf"server\s*\{{\s*\n\s*server_name\s+{re.escape(server_name)}\b",
        re.MULTILINE,
    )
    match = pattern.search(text)
    if not match:
        return text, False
    start = match.start()
    brace = 0
    i = text.find("{", start)
    while i < len(text):
        if text[i] == "{":
            brace += 1
        elif text[i] == "}":
            brace -= 1
            if brace == 0:
                end = i + 1
                while end < len(text) and text[end] in "\r\n":
                    end += 1
                return text[:start] + text[end:], True
        i += 1
    return text, False


def _purge_nginx_vhosts(domain: str, hosts: list[str]) -> str:
    if not _NGINX_INCLUDES.is_dir():
        return "skip"
    targets = [
        _NGINX_INCLUDES / "nginx.conf.web.https",
        _NGINX_INCLUDES / "nginx.conf.web.admin",
        _NGINX_INCLUDES / "nginx.conf.web.http",
        _NGINX_INCLUDES / "nginx.conf.mail.imap",
        _NGINX_INCLUDES / "nginx.conf.mail.imaps",
        _NGINX_INCLUDES / "nginx.conf.mail.pop3",
        _NGINX_INCLUDES / "nginx.conf.mail.pop3s",
        _NGINX_INCLUDES / "nginx.conf.map.key",
        _NGINX_INCLUDES / "nginx.conf.map.crt",
    ]
    edited = 0
    for fp in targets:
        if not fp.is_file():
            continue
        text = fp.read_text(encoding="utf-8")
        orig = text
        for host in hosts:
            while True:
                text, removed = _remove_nginx_server_block(text, host)
                if not removed:
                    break
        for host in hosts:
            text = re.sub(rf"^{re.escape(host)} .*\n", "", text, flags=re.MULTILINE)
        if text != orig:
            fp.with_suffix(fp.suffix + f".bak-purge-{_domain_slug(domain)}").write_text(orig, encoding="utf-8")
            fp.write_text(text, encoding="utf-8")
            edited += 1
    if edited == 0:
        return "none"
    proc = subprocess.run(
        ["/opt/zextras/common/sbin/nginx", "-t", "-c", "/opt/zextras/conf/nginx.conf"],
        capture_output=True,
        text=True,
        timeout=60,
    )
    if proc.returncode != 0:
        return f"nginx_test_fail:{(proc.stderr or proc.stdout)[:120]}"
    subprocess.run(
        ["su", "-", "zextras", "-c", "/opt/zextras/common/sbin/nginx -s reload"],
        capture_output=True,
        timeout=60,
    )
    return f"updated:{edited}"


def _purge_session_logs(domain: str) -> int:
    count = 0
    if not _LOG_DIR.is_dir():
        return 0
    for f in _LOG_DIR.glob("*.jsonl"):
        try:
            if domain in f.read_text(encoding="utf-8", errors="ignore").lower():
                f.unlink()
                count += 1
        except OSError:
            pass
    return count


def _execute_purge(
    domain: str,
    report: Callable[[str, str, str | None], None] | None = None,
    *,
    job_id: str | None = None,
    requested_by: str | None = None,
) -> dict[str, Any]:
    def _step(step_id: str, status: str, detail: str | None = None) -> None:
        if report:
            report(step_id, status, detail)
        if status == "running":
            activity_log.info(f"PURGE [{step_id}] {detail or '…'}", source="admin")
        elif status == "done":
            activity_log.ok(f"PURGE [{step_id}] {detail or 'OK'}", source="admin")
        elif status == "error":
            activity_log.error(f"PURGE [{step_id}] {detail or 'erro'}", source="admin")

    rec = domain_registry.get_domain_record(domain) or {}
    aliases = list(rec.get("mail_aliases") or [])
    hosts = _collect_purge_hosts(domain, aliases)
    result: dict[str, Any] = {"domain": domain}
    traefik_backup: str | None = None

    _step("audit_snapshot", "running")
    snapshot = build_audit_snapshot(domain, job_id=job_id, requested_by=requested_by)
    result["audit_snapshot"] = snapshot
    ac = snapshot.get("carbonio", {}).get("account_count", 0)
    _step("audit_snapshot", "done", f"{ac} conta(s), inventário gravado")

    _step("carbonio_accounts", "running")
    result["carbonio_accounts"] = _delete_carbonio_accounts(domain)
    _step("carbonio_accounts", "done", "; ".join(result["carbonio_accounts"]) or "nenhuma conta")

    _step("carbonio_domain", "running")
    dd = _delete_carbonio_domain_only(domain)
    result["carbonio_domain"] = dd or "domínio já ausente"
    _step("carbonio_domain", "done", result["carbonio_domain"])

    _step("portal_users", "running")
    result["portal_users_removed"] = _purge_portal_users(domain)
    _step("portal_users", "done", ", ".join(result["portal_users_removed"]) or "nenhum")

    _step("site_folder", "running")
    result["site_folder_removed"] = _purge_site_folder(domain)
    _step("site_folder", "done", "removido" if result["site_folder_removed"] else "já ausente")

    _step("cloudflare", "running")
    result["cloudflare"] = _purge_cloudflare_zone(domain)
    _step("cloudflare", "done", result["cloudflare"])

    _step("session_logs", "running")
    result["session_logs_removed"] = _purge_session_logs(domain)
    _step("session_logs", "done", str(result["session_logs_removed"]))

    _step("tenant_branding", "running")
    result["tenant_branding"] = _purge_tenant_branding(domain)
    _step("tenant_branding", "done", result["tenant_branding"])

    _step("deploy_scripts", "running")
    result["deploy_scripts"] = _purge_deploy_script_refs(domain)
    _step("deploy_scripts", "done", result["deploy_scripts"])

    _step("traefik_export_certs", "running")
    result["traefik_export_certs"] = _purge_traefik_export_certs(domain)
    _step("traefik_export_certs", "done", result["traefik_export_certs"])

    _step("nginx_vhosts", "running")
    result["nginx_vhosts"] = _purge_nginx_vhosts(domain, hosts)
    nginx_ok = not str(result["nginx_vhosts"]).startswith("nginx_test_fail")
    _step("nginx_vhosts", "done" if nginx_ok else "error", result["nginx_vhosts"])

    try:
        _step("traefik_sni", "running")
        result["traefik_sni"] = purge_traefik_sni(hosts)
        sni_ok = not str(result["traefik_sni"]).startswith("sni_fail")
        _step("traefik_sni", "done" if sni_ok else "error", result["traefik_sni"])

        _step("traefik_routers", "running")
        tr = purge_traefik_routers(domain, hosts)
        result["traefik_routers"] = tr.get("detail") or ("traefik_ok" if tr.get("ok") else "traefik_fail")
        traefik_backup = tr.get("backup")
        result["traefik_backup"] = traefik_backup
        tr_ok = bool(tr.get("ok"))
        _step("traefik_routers", "done" if tr_ok else "error", result["traefik_routers"])

        _step("traefik_validate", "running")
        validation = validate_traefik_after_purge(domain)
        result["traefik_validate"] = validation
        val_detail = json.dumps(validation.get("checks") or {}, ensure_ascii=False)[:400]
        val_ok = bool(validation.get("ok"))
        if not val_ok and traefik_backup:
            rb = rollback_traefik_backup(traefik_backup)
            result["traefik_rollback"] = rb
            validation = validate_traefik_after_purge(domain)
            result["traefik_validate_after_rollback"] = validation
        _step("traefik_validate", "done" if val_ok else "error", val_detail)

        if not sni_ok or not tr_ok or not val_ok:
            raise RuntimeError(
                f"Traefik purge incompleto (sni={result['traefik_sni']}, "
                f"routers={result['traefik_routers']}, validate={val_detail})"
            )
    except Exception as e:
        result["traefik_error"] = str(e)
        if report:
            _step("traefik_validate", "error", str(e))
        raise

    _step("openpanel_dns", "running")
    op = purge_openpanel(domain)
    result["openpanel"] = op
    _step("openpanel_dns", "done", op.get("dns", "ok"))
    _step("openpanel_user", "running")
    _step("openpanel_user", "done", op.get("user", "ok"))

    _step("pmg_domain", "running")
    pmg = purge_pmg(domain)
    result["pmg"] = pmg
    _step("pmg_domain", "done", pmg.get("domains", "ok"))
    _step("pmg_transport", "running")
    _step("pmg_transport", "done", pmg.get("transport", "ok"))

    result["carbonio"] = result.get("carbonio_accounts", []) + (
        [result["carbonio_domain"]] if result.get("carbonio_domain") else []
    )
    return result


def run_purge_job(job_id: str) -> None:
    job = purge_jobs.get_job(job_id)
    if not job:
        return
    domain = job["domain"]
    report = purge_jobs.make_reporter(job_id)
    try:
        if domain in _PURGE_BLOCKLIST:
            raise ValueError(f"Domínio {domain} está na blocklist de purge")
        activity_log.info(f"PURGE job {job_id} iniciado: {domain}", source="admin")
        result = _execute_purge(domain, report=report, job_id=job_id)
        purge_jobs.complete_job(job_id, result)
        carbonio.invalidate_domain_list_cache()
        carbonio_cache.invalidate_domain(domain)
        activity_log.ok(f"PURGE job {job_id} concluído: {domain}", source="admin")
    except Exception as e:
        purge_jobs.fail_job(job_id, str(e))
        activity_log.error(f"PURGE job {job_id} falhou: {e}", source="admin")


def assert_purge_allowed(domain: str) -> None:
    domain = domain.lower().strip()
    if domain in _PURGE_BLOCKLIST:
        raise ValueError(f"Domínio {domain} está na blocklist de purge")


def purge_domain(domain: str) -> dict[str, Any]:
    domain = domain.lower().strip()
    assert_purge_allowed(domain)
    activity_log.info(f"PURGE iniciado: {domain}", source="admin")
    job = purge_jobs.create_job(domain)
    job_id = job["job_id"]
    report = purge_jobs.make_reporter(job_id)
    try:
        result = _execute_purge(domain, report=report)
        purge_jobs.complete_job(job_id, result)
        carbonio.invalidate_domain_list_cache()
        carbonio_cache.invalidate_domain(domain)
        activity_log.ok(f"PURGE concluído: {domain}", source="admin")
        job_data = purge_jobs.get_job(job_id) or {}
        steps = purge_jobs.steps_for_desk(job_data.get("steps") or [])
        return {"domain": domain, "steps": steps, **result}
    except Exception as e:
        purge_jobs.fail_job(job_id, str(e))
        raise


def delete_carbonio_account(email: str) -> dict:
    """Remove uma conta Carbonio (zmprov da) — Spec 022."""
    email = email.lower().strip()
    if "@" not in email:
        raise ValueError("e-mail inválido")
    domain = email.split("@", 1)[1]
    assert_purge_allowed(domain)
    if not carbonio.account_exists(email):
        return {"ok": True, "email": email, "message": "Conta já não existia", "skipped": True}
    code, out, err = carbonio._zmprov_run("da", email, log_cmd=True)
    if code != 0 and not carbonio._is_missing_account(err, out):
        raise carbonio.CarbonioError(err or out or f"zmprov da falhou para {email}")
    carbonio.invalidate_domain_cache(domain)
    return {"ok": True, "email": email, "message": f"Conta {email} removida do Carbonio", "rc": code}
