"""Operational checks exposed via API/panel: deliverability, RBL, e2e connectivity."""

from __future__ import annotations

import json
import logging
import os
import re
import socket
import ssl
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from urllib import error as urllib_error
from urllib import request as urllib_request

from .settings import get_settings

logger = logging.getLogger(__name__)

# Public DNSBLs used only for *outbound IP reputation self-checks* (not inbound reject lists).
# Operators should run these from the mail host with a stable resolver.
DEFAULT_DNSBL_ZONES = (
    "zen.spamhaus.org",
    "bl.spamcop.net",
    "b.barracudacentral.org",
    "dnsbl.sorbs.net",
)

RBL_ENV_KEY = "LIMRISTEM_MAIL_RSPAMD_DNSBL_ENABLED"
RBL_ZONES_ENV_KEY = "LIMRISTEM_MAIL_RSPAMD_DNSBL_ZONES"


def utc_now_iso() -> str:
    return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def _run(cmd: list[str], timeout: int = 15) -> tuple[int, str, str]:
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False)
        return result.returncode, result.stdout or "", result.stderr or ""
    except FileNotFoundError:
        return 127, "", f"{cmd[0]} not found"
    except subprocess.TimeoutExpired:
        return 124, "", "timeout"


def dig_txt(name: str) -> list[str]:
    code, out, _ = _run(["dig", "+short", "TXT", name], timeout=10)
    if code != 0:
        return []
    return [line.strip().strip('"') for line in out.splitlines() if line.strip()]


def dig_mx(name: str) -> list[str]:
    code, out, _ = _run(["dig", "+short", "MX", name], timeout=10)
    if code != 0:
        return []
    return [line.strip() for line in out.splitlines() if line.strip()]


def dig_a(name: str) -> list[str]:
    code, out, _ = _run(["dig", "+short", "A", name], timeout=10)
    records = [line.strip() for line in out.splitlines() if line.strip()]
    code6, out6, _ = _run(["dig", "+short", "AAAA", name], timeout=10)
    records.extend([line.strip() for line in out6.splitlines() if line.strip()])
    return records


def reverse_dns(ip: str) -> str:
    code, out, _ = _run(["host", ip], timeout=10)
    if code != 0:
        return ""
    return out.strip()


def reverse_octets(ip: str) -> str | None:
    parts = ip.split(".")
    if len(parts) != 4:
        return None  # IPv6 DNSBL needs nibble form; skip for MVP simplicity
    try:
        if not all(0 <= int(p) <= 255 for p in parts):
            return None
    except ValueError:
        return None
    return ".".join(reversed(parts))


def query_dnsbl(ip: str, zone: str) -> dict[str, Any]:
    rev = reverse_octets(ip)
    if not rev:
        return {"zone": zone, "ip": ip, "listed": None, "detail": "only IPv4 DNSBL queries supported here"}
    qname = f"{rev}.{zone}"
    code, out, err = _run(["dig", "+short", "A", qname], timeout=8)
    answers = [line.strip() for line in out.splitlines() if line.strip()]
    listed = bool(answers) and code == 0
    return {
        "zone": zone,
        "ip": ip,
        "query": qname,
        "listed": listed,
        "answers": answers,
        "detail": err.strip() if code not in {0, 1} else None,
    }


def check_public_ip_reputation(ip: str, zones: list[str] | None = None) -> dict[str, Any]:
    zone_list = zones or list(DEFAULT_DNSBL_ZONES)
    env_zones = os.getenv(RBL_ZONES_ENV_KEY, "").strip()
    if env_zones and not zones:
        zone_list = [z for z in re.split(r"[\s,]+", env_zones) if z]
    results = []
    with ThreadPoolExecutor(max_workers=min(6, max(1, len(zone_list)))) as pool:
        futures = {pool.submit(query_dnsbl, ip, zone): zone for zone in zone_list}
        for fut in as_completed(futures):
            try:
                results.append(fut.result())
            except Exception as exc:  # pragma: no cover
                results.append({"zone": futures[fut], "ip": ip, "listed": None, "detail": str(exc)})
    listed_count = sum(1 for item in results if item.get("listed") is True)
    return {
        "ip": ip,
        "checked_at": utc_now_iso(),
        "zones_checked": len(results),
        "listed_count": listed_count,
        "status": "listed" if listed_count else "clean",
        "results": sorted(results, key=lambda item: str(item.get("zone"))),
        "ptr": reverse_dns(ip),
        "note": "Self-check only: listing status depends on your resolver and DNSBL access policy.",
    }


def run_deliverability_check(
    domain: str | None = None,
    mail_host: str | None = None,
    public_ip: str | None = None,
    dkim_selector: str | None = None,
) -> dict[str, Any]:
    settings = get_settings()
    domain = (domain or settings.primary_domain).strip().rstrip(".").lower()
    mail_host = (mail_host or settings.hostname).strip().rstrip(".").lower()
    public_ip = (public_ip or os.getenv("LIMRISTEM_MAIL_PUBLIC_IP") or "").strip()
    dkim_selector = (dkim_selector or os.getenv("LIMRISTEM_MAIL_DKIM_SELECTOR") or "default").strip().lower()

    script = settings.runtime_bin_dir / "deliverability-check.sh"
    args = [str(script), domain, mail_host]
    if public_ip:
        args.append(public_ip)
        args.append(dkim_selector)
    elif dkim_selector:
        args.extend(["", dkim_selector])

    code, out, err = _run(args, timeout=60)
    dns = {
        "mx": dig_mx(domain),
        "mail_a": dig_a(mail_host),
        "spf": dig_txt(domain),
        "dmarc": dig_txt(f"_dmarc.{domain}"),
        "dkim": dig_txt(f"{dkim_selector}._domainkey.{domain}"),
        "mta_sts": dig_txt(f"_mta-sts.{domain}"),
        "tls_rpt": dig_txt(f"_smtp._tls.{domain}"),
    }
    ip_rep = check_public_ip_reputation(public_ip) if public_ip and re.fullmatch(r"\d+\.\d+\.\d+\.\d+", public_ip) else None
    return {
        "checked_at": utc_now_iso(),
        "domain": domain,
        "mail_host": mail_host,
        "public_ip": public_ip or None,
        "dkim_selector": dkim_selector,
        "script_exit_code": code,
        "script_output": out,
        "script_error": err.strip() or None,
        "dns": dns,
        "ip_reputation": ip_rep,
        "status": "ok" if code == 0 else "degraded",
    }


def _tcp_connect(host: str, port: int, timeout: float = 5.0) -> dict[str, Any]:
    started = datetime.now(UTC)
    try:
        with socket.create_connection((host, port), timeout=timeout) as sock:
            elapsed = (datetime.now(UTC) - started).total_seconds()
            peer = sock.getpeername()
            return {"ok": True, "host": host, "port": port, "peer": f"{peer[0]}:{peer[1]}", "latency_ms": int(elapsed * 1000)}
    except OSError as exc:
        return {"ok": False, "host": host, "port": port, "error": str(exc)}


def _tls_probe(host: str, port: int, *, starttls_smtp: bool = False, timeout: float = 8.0) -> dict[str, Any]:
    try:
        raw = socket.create_connection((host, port), timeout=timeout)
        if starttls_smtp:
            raw.recv(1024)
            raw.sendall(b"EHLO limristem-probe\r\n")
            raw.recv(2048)
            raw.sendall(b"STARTTLS\r\n")
            resp = raw.recv(1024).decode("utf-8", errors="replace")
            if not resp.startswith("220"):
                raw.close()
                return {"ok": False, "host": host, "port": port, "error": f"STARTTLS refused: {resp.strip()[:120]}"}
        context = ssl.create_default_context()
        with context.wrap_socket(raw, server_hostname=host) as ssock:
            cert = ssock.getpeercert()
            cipher = ssock.cipher()
            return {
                "ok": True,
                "host": host,
                "port": port,
                "tls_version": ssock.version(),
                "cipher": cipher[0] if cipher else None,
                "subject": dict(x[0] for x in cert.get("subject", ())) if cert else None,
                "issuer": dict(x[0] for x in cert.get("issuer", ())) if cert else None,
                "notAfter": cert.get("notAfter") if cert else None,
            }
    except Exception as exc:
        return {"ok": False, "host": host, "port": port, "error": str(exc)}


def run_e2e_connectivity_tests(mail_host: str | None = None) -> dict[str, Any]:
    """Local/public connectivity probes (not a full mail transaction with credentials)."""
    settings = get_settings()
    host = (mail_host or settings.hostname).strip().rstrip(".").lower()
    ports = {
        "smtp_25": _tcp_connect(host, 25),
        "submission_587": _tcp_connect(host, 587),
        "smtps_465": _tcp_connect(host, 465),
        "imap_143": _tcp_connect(host, 143),
        "imaps_993": _tcp_connect(host, 993),
        "https_443": _tcp_connect(host, 443),
    }
    tls = {
        "smtp_starttls_25": _tls_probe(host, 25, starttls_smtp=True),
        "submission_starttls_587": _tls_probe(host, 587, starttls_smtp=True),
        "smtps_465": _tls_probe(host, 465, starttls_smtp=False),
        "imaps_993": _tls_probe(host, 993, starttls_smtp=False),
        "https_443": _tls_probe(host, 443, starttls_smtp=False),
    }
    health = {"ok": False, "detail": None}
    try:
        url = f"https://{host}/health" if settings.ssl_mode != "plain" else f"http://127.0.0.1:{settings.api_port}/health"
        with urllib_request.urlopen(url, timeout=8) as resp:  # noqa: S310
            body = resp.read(4096).decode("utf-8", errors="replace")
            health = {"ok": resp.status == 200, "status": resp.status, "body": body[:500]}
    except Exception as exc:
        health = {"ok": False, "error": str(exc)}

    ok_ports = sum(1 for item in ports.values() if item.get("ok"))
    ok_tls = sum(1 for item in tls.values() if item.get("ok"))
    return {
        "checked_at": utc_now_iso(),
        "mail_host": host,
        "ports": ports,
        "tls": tls,
        "health": health,
        "summary": {
            "ports_ok": ok_ports,
            "ports_total": len(ports),
            "tls_ok": ok_tls,
            "tls_total": len(tls),
            "status": "ok" if ok_ports >= 4 and health.get("ok") else "degraded",
        },
        "note": "Does not authenticate as a mailbox user; use a client for full login tests.",
    }


def rbl_inbound_config() -> dict[str, Any]:
    enabled = (os.getenv(RBL_ENV_KEY) or "no").strip().lower() in {"1", "true", "yes", "on"}
    zones_raw = os.getenv(RBL_ZONES_ENV_KEY, "zen.spamhaus.org bl.spamcop.net").strip()
    zones = [z for z in re.split(r"[\s,]+", zones_raw) if z]
    return {
        "enabled": enabled,
        "zones": zones,
        "env_enabled_key": RBL_ENV_KEY,
        "env_zones_key": RBL_ZONES_ENV_KEY,
        "warning": (
            "Inbound DNSBL can false-positive on shared/public resolvers. "
            "Prefer Rspamd modules with a dedicated resolver. Enable only if you understand the risk."
        ),
    }


def write_rspamd_rbl_config(*, enabled: bool, zones: list[str]) -> Path:
    """Write optional Rspamd multimap/rbl snippet. Requires manage-rbl.sh or root for live path."""
    settings = get_settings()
    conf_dir = Path("/etc/rspamd/local.d")
    # Prefer writing a staged file under package config if /etc is not writable.
    staged = settings.base_dir / "config" / "rspamd-rbl-enabled.inc"
    lines = [
        "# Generated by Limristem eMail operations",
        f"# enabled={str(enabled).lower()}",
        "",
    ]
    if enabled and zones:
        # Use rbl module style entries when possible.
        lines.append("# See also: https://rspamd.com/doc/modules/rbl.html")
        for zone in zones:
            safe = re.sub(r"[^A-Za-z0-9._-]", "", zone)
            if not safe:
                continue
            symbol = "RBL_" + safe.replace(".", "_").upper()[:40]
            lines.append(f"{symbol} {{")
            lines.append(f'  rbl = "{safe}";')
            lines.append("  symbol = \"" + symbol + "\";")
            lines.append("  score = 3.0;")
            lines.append("}")
            lines.append("")
    content = "\n".join(lines) + "\n"
    staged.parent.mkdir(parents=True, exist_ok=True)
    staged.write_text(content, encoding="utf-8")
    os.chmod(staged, 0o640)
    live = conf_dir / "limristem_rbl.conf"
    try:
        if conf_dir.is_dir() and os.access(conf_dir, os.W_OK):
            live.write_text(content if enabled else "# RBL disabled by Limristem eMail\n", encoding="utf-8")
            os.chmod(live, 0o640)
    except OSError as exc:
        logger.warning("Unable to write live Rspamd RBL config: %s", exc)
    return staged
