import logging
import os
import json
import subprocess
from pathlib import Path

from fastapi import HTTPException

from .limristem_mail_backend import BackendCommandError, execute_command
from .settings import get_settings

settings = get_settings()
logger = logging.getLogger(__name__)


SCRIPT_COMMAND_ALIASES = {
    "manage-queue.sh": "queue",
    "manage-bans.sh": "bans",
    "manage-limits.sh": "limits",
    "manage-backups.sh": "backup",
    "manage-firewall.sh": "firewall",
    "manage-api-credentials.sh": "api",
    "manage-settings.sh": "settings",
    "manage-ssl.sh": "ssl",
    "manage-rbl.sh": "rbl",
    "manage-topology.sh": "topology",
}


def requires_privileged_script_path(script_name: str, *args: str) -> bool:
    if script_name == "manage-backups.sh":
        return not bool(args) or args[0] not in {"show", "list", "list-schedules", "list-storages"}
    if script_name == "manage-firewall.sh":
        return not bool(args) or args[0] != "show"
    if script_name == "manage-queue.sh":
        return not bool(args) or args[0] != "list"
    if script_name == "manage-ssl.sh":
        return not bool(args) or args[0] != "show"
    if script_name == "manage-settings.sh":
        return not bool(args) or args[0] not in {"show", "check-update"}
    if script_name == "manage-rbl.sh":
        return not bool(args) or args[0] != "show"
    if script_name == "manage-topology.sh":
        return not bool(args) or args[0] != "show"
    # set/set-many/apply rewrite the root-owned env file and /etc/{postfix,rspamd,fail2ban}.
    # In-process they would run as the service account: the env write fails with EACCES
    # and every config write is silently swallowed by ProtectSystem=strict.
    if script_name == "manage-limits.sh":
        return not bool(args) or args[0] != "show"
    # Even "list" needs root: fail2ban-client talks to a 0660 root:root socket, so an
    # in-process listing always came back empty and "ban" always failed outright.
    if script_name == "manage-bans.sh":
        return True
    # regenerate rewrites the (root-owned) env file and restarts the unit, so it has
    # to run through sudo instead of in-process under the service account.
    if script_name == "manage-api-credentials.sh":
        return not bool(args) or args[0] != "show"
    # Always write Maildir as root so ownership is vmail.
    if script_name in {"manage-mailbox-import.sh", "manage-mailbox-fs.sh"}:
        return True
    return False


def normalize_privileged_operation_error(detail: str, script_name: str, returncode: int, args: tuple[str, ...] = ()) -> str:
    cleaned_detail = detail.strip()
    normalized = cleaned_detail.lower()
    if "sudo:" in normalized and (
        "a terminal is required" in normalized
        or "a password is required" in normalized
        or "askpass" in normalized
    ):
        return "Privileged system operations require the installed passwordless sudoers rule for the limristem-mail user."
    if "no new privileges" in normalized:
        return "Privileged system operations are unavailable in the current environment."
    if script_name == "manage-queue.sh" and cleaned_detail:
        return cleaned_detail
    if "read-only file system" in normalized:
        if "/etc/systemd/system" in cleaned_detail or "limristem-mail-backup-schedule-" in cleaned_detail:
            return "Backup settings were saved, but this environment cannot update system backup files because the filesystem is read-only."
        if "cannot create directory" in normalized or "/var/backups/" in cleaned_detail:
            return "The current environment cannot write backup data because the filesystem is read-only."
        return "The current environment cannot complete this operation because the filesystem is read-only."
    if "is not writable in the current environment" in normalized:
        if "/etc/systemd/system" in cleaned_detail or "limristem-mail-backup-schedule-" in cleaned_detail:
            return "Backup settings were saved, but this environment cannot update system backup files because the filesystem is read-only."
        if "backup destination" in normalized or "/var/backups/" in cleaned_detail:
            return "The current environment cannot write backup data because the filesystem is read-only."
        return "The current environment cannot complete this operation because required paths are not writable."
    if not cleaned_detail and script_name == "manage-backups.sh" and args and args[0] == "run":
        return (
            "Backup failed before reporting a specific error. "
            "Check the backup destination, storage settings, and required backup tools, then retry."
        )
    if returncode < 0 and not detail:
        return f"{script_name} terminated by signal {abs(returncode)}"
    return cleaned_detail or f"{script_name} failed with status {returncode}"


def build_root_command(script_name: str, *args: str) -> list[str]:
    # Always the helper script itself: templates/sudoers/limristem-mail-admin lists
    # $BASE_DIR/bin/manage-*.sh and nothing else, so a command built around the
    # limristem-mail launcher could never be authorised ("sudo: a password is required").
    return ["sudo", "-n", str(settings.runtime_bin_dir / script_name), *args]


def run_root_script(script_name: str, *args: str, input: str | None = None) -> str:
    cli_command = SCRIPT_COMMAND_ALIASES.get(script_name)
    if cli_command and not requires_privileged_script_path(script_name, *args):
        try:
            return execute_command(cli_command, *args)
        except BackendCommandError as exc:
            detail = normalize_privileged_operation_error(exc.detail.strip(), script_name, exc.returncode, args)
            raise HTTPException(status_code=500, detail=detail) from exc
    command = build_root_command(script_name, *args)
    run_kwargs: dict = {"capture_output": True, "text": True, "check": True}
    if input is not None:
        run_kwargs["input"] = input
    try:
        result = subprocess.run(command, **run_kwargs)
    except subprocess.CalledProcessError as exc:
        detail = normalize_privileged_operation_error(
            (exc.stderr or exc.stdout or "").strip(),
            script_name,
            exc.returncode,
            args,
        )
        raise HTTPException(status_code=500, detail=detail) from exc
    return result.stdout


def load_script_json(script_name: str, *args: str, default):
    output = run_root_script(script_name, *args).strip()
    try:
        return json.loads(output or json.dumps(default))
    except json.JSONDecodeError as exc:
        raise HTTPException(status_code=500, detail=f"{script_name} returned invalid JSON output") from exc


def load_queue() -> list[dict]:
    return load_script_json("manage-queue.sh", "list", "--json", default=[])


def load_bans() -> list[dict]:
    return load_script_json("manage-bans.sh", "list", "--json", default=[])


def load_limits() -> dict[str, str]:
    return load_script_json("manage-limits.sh", "show", "--json", default={})


def load_backup_config() -> dict[str, str]:
    return load_script_json("manage-backups.sh", "show", "--json", default={})


def load_backup_runs() -> list[dict]:
    return load_script_json("manage-backups.sh", "list", "--json", default=[])


def load_backup_schedules() -> list[dict]:
    return load_script_json("manage-backups.sh", "list-schedules", "--json", default=[])


def load_backup_storages() -> list[dict]:
    return load_script_json("manage-backups.sh", "list-storages", "--json", default=[])


def save_backup_schedule(payload_json: str) -> dict:
    return load_script_json("manage-backups.sh", "save-schedule", payload_json, "--json", default={})


def save_backup_storage(payload_json: str) -> dict:
    return load_script_json("manage-backups.sh", "save-storage", payload_json, "--json", default={})


def test_backup_storage(storage_id: str) -> dict:
    return load_script_json("manage-backups.sh", "test-storage", storage_id, "--json", default={})


def load_firewall_config() -> dict[str, str]:
    return load_script_json("manage-firewall.sh", "show", "--json", default={})


def load_api_credentials() -> dict[str, str]:
    return load_script_json("manage-api-credentials.sh", "show", "--json", default={})


def _enrich_ssl_payload(payload: dict) -> dict:
    if not isinstance(payload, dict):
        return {}
    cert_info = payload.get("cert_info") or {}
    subject = cert_info.get("subject", "")
    issuer = cert_info.get("issuer", "")
    not_after = cert_info.get("not_valid_after", "")

    # Extract CN from subject
    cn = ""
    if subject:
        for part in subject.split(","):
            part = part.strip()
            if part.startswith("CN="):
                cn = part[3:].strip()
                break
    if not cn:
        cn = payload.get("hostname", "")
    payload["cert_common_name"] = cn

    # Extract issuer organization or CN
    issuer_name = ""
    if issuer:
        for part in issuer.split(","):
            part = part.strip()
            if part.startswith("O="):
                issuer_name = part[2:].strip()
                break
            elif part.startswith("CN="):
                issuer_name = part[3:].strip()
                break
    if not issuer_name:
        if payload.get("mode") == "letsencrypt":
            issuer_name = "Let's Encrypt"
        elif payload.get("mode") == "selfsigned":
            issuer_name = "Auto-firmato (Self-Signed)"
        else:
            issuer_name = issuer or "Non disponibile"
    payload["cert_issuer"] = issuer_name

    # Format expiration date and days left
    days_left = cert_info.get("days_left")
    formatted_valid_to = ""
    if not_after:
        try:
            from datetime import datetime, timezone
            clean_date = not_after.rstrip("Z")
            dt = datetime.fromisoformat(clean_date)
            formatted_valid_to = dt.strftime("%Y-%m-%d %H:%M UTC")
            if days_left is None:
                now = datetime.now(dt.tzinfo if dt.tzinfo else timezone.utc)
                days_left = max(0, (dt - now).days)
        except Exception as exc:
            logger.debug("_enrich_ssl_payload date parse failed: %s", exc)
            formatted_valid_to = not_after
    payload["cert_valid_to"] = formatted_valid_to
    payload["cert_days_left"] = days_left
    return payload


def load_ssl_config() -> dict[str, str]:
    # 1. Prefer root script manage-ssl.sh show --json via sudo (runs as root, can inspect /etc/letsencrypt)
    try:
        payload = load_script_json("manage-ssl.sh", "show", "--json")
        if isinstance(payload, dict) and payload.get("status"):
            return _enrich_ssl_payload(payload)
    except Exception as exc:
        logger.debug("load_ssl_config: root script manage-ssl.sh failed: %s", exc)

    # 2. Try in-process backend command
    try:
        payload = json.loads(execute_command("ssl", "show", "--json", allow_fallback=False))
        if isinstance(payload, dict) and payload.get("status"):
            return _enrich_ssl_payload(payload)
    except Exception as exc:
        logger.debug("load_ssl_config: backend execute_command failed: %s", exc)

    # 3. Fallback based on env settings
    from .settings import get_settings as _get_settings

    current_settings = _get_settings()
    hostname = current_settings.hostname
    primary_domain = current_settings.primary_domain
    ssl_mode = (current_settings.ssl_mode or "selfsigned").strip().lower()
    if ssl_mode == "manual":
        cert_path = os.getenv("LIMRISTEM_MAIL_TLS_CERT_PATH", "/etc/ssl/limristem-mail/manual.crt")
        key_path = os.getenv("LIMRISTEM_MAIL_TLS_KEY_PATH", "/etc/ssl/limristem-mail/manual.key")
    elif ssl_mode == "letsencrypt":
        cert_path = f"/etc/letsencrypt/live/{hostname}/fullchain.pem"
        key_path = f"/etc/letsencrypt/live/{hostname}/privkey.pem"
    elif ssl_mode == "plain":
        cert_path = ""
        key_path = ""
    else:
        cert_path = "/etc/ssl/limristem-mail/limristem-mail.crt"
        key_path = "/etc/ssl/limristem-mail/limristem-mail.key"

    cert_exists = False
    if cert_path:
        try:
            cert_exists = Path(cert_path).is_file()
        except (PermissionError, OSError):
            cert_exists = bool(ssl_mode == "letsencrypt" or os.path.exists(cert_path))
    key_exists = False
    if key_path:
        try:
            key_exists = Path(key_path).is_file()
        except (PermissionError, OSError):
            key_exists = bool(ssl_mode == "letsencrypt" or os.path.exists(key_path))

    result = {
        "mode": ssl_mode,
        "hostname": hostname,
        "le_email": os.getenv("LIMRISTEM_MAIL_LE_EMAIL", f"postmaster@{primary_domain}"),
        "cert_path": cert_path,
        "key_path": key_path,
        "cert_exists": cert_exists,
        "key_exists": key_exists,
        "status": "ready"
        if cert_exists and key_exists
        else ("plain" if ssl_mode == "plain" else "missing"),
        "nginx_enabled": os.getenv("LIMRISTEM_MAIL_ENABLE_NGINX", "no").strip().lower() in {"1", "true", "yes", "on"},
        "mta_sts_enabled": os.getenv("LIMRISTEM_MAIL_ENABLE_MTA_STS", "yes").strip().lower() in {"1", "true", "yes", "on"},
    }

    try:
        from cryptography import x509
        from cryptography.hazmat.backends import default_backend

        def parse_cert(path):
            try:
                cert = x509.load_pem_x509_certificate(Path(path).read_bytes(), default_backend())
                from datetime import datetime, timezone
                not_after = cert.not_valid_after_utc if hasattr(cert, "not_valid_after_utc") else cert.not_valid_after.replace(tzinfo=timezone.utc)
                not_before = cert.not_valid_before_utc if hasattr(cert, "not_valid_before_utc") else cert.not_valid_before.replace(tzinfo=timezone.utc)
                now = datetime.now(timezone.utc)
                days_left = max(0, (not_after - now).days)
                return {
                    "subject": cert.subject.rfc4514_string(),
                    "issuer": cert.issuer.rfc4514_string(),
                    "not_valid_before": not_before.isoformat(),
                    "not_valid_after": not_after.isoformat(),
                    "serial_number": str(cert.serial_number),
                    "days_left": days_left,
                }
            except Exception as exc:
                logger.debug("parse_cert: suppressed %s: %s", type(exc).__name__, exc)
                return None

        if result["cert_exists"] and cert_path:
            result["cert_info"] = parse_cert(cert_path)

        postfix_cert_path = ""
        try:
            postfix_cert_path = subprocess.check_output(["postconf", "-h", "smtpd_tls_cert_file"], text=True, stderr=subprocess.DEVNULL).strip()
        except Exception as exc:
            logger.debug("load_ssl_config: postconf smtpd_tls_cert_file failed: %s", exc)
        result["postfix_cert_path"] = postfix_cert_path
        if postfix_cert_path and Path(postfix_cert_path).is_file():
            result["postfix_cert_info"] = parse_cert(postfix_cert_path)

        dovecot_cert_path = ""
        for setting in ("ssl_server/cert_file", "ssl_server_cert_file"):
            try:
                out = subprocess.check_output(["doveconf", "-h", setting], text=True, stderr=subprocess.DEVNULL).strip()
                if out.startswith("<"):
                    out = out[1:]
                out = out.strip()
                if out:
                    dovecot_cert_path = out
                    break
            except (subprocess.CalledProcessError, FileNotFoundError, OSError) as exc:
                logger.debug("doveconf %s query failed: %s", setting, exc)
        if not dovecot_cert_path:
            try:
                cfg = Path("/etc/dovecot/conf.d/10-ssl-limristem-mail.conf")
                if cfg.is_file():
                    for line in cfg.read_text().splitlines():
                        if line.strip().startswith("ssl_server_cert_file") and "=" in line:
                            dovecot_cert_path = line.split("=", 1)[1].strip().strip("<").strip()
                            break
            except Exception as exc:
                logger.warning("load_ssl_config: suppressed %s: %s", type(exc).__name__, exc)
        result["dovecot_cert_path"] = dovecot_cert_path
        if dovecot_cert_path and Path(dovecot_cert_path).is_file():
            result["dovecot_cert_info"] = parse_cert(dovecot_cert_path)

        # check if certs match
        result["certs_match"] = True
        if result.get("postfix_cert_path") and result["postfix_cert_path"] != cert_path:
            result["certs_match"] = False
        if result.get("dovecot_cert_path") and result["dovecot_cert_path"] != cert_path:
            result["certs_match"] = False

    except Exception as exc:
        logger.warning("Unable to parse SSL cert info: %s", exc)

    return _enrich_ssl_payload(result)


def load_panel_settings() -> dict[str, object]:
    return load_script_json("manage-settings.sh", "show", "--json", default={})


def check_for_updates(*, apply_if_enabled: bool = False) -> dict[str, object]:
    command = ["check-update"]
    if apply_if_enabled:
        command.append("--apply-if-enabled")
    command.append("--json")
    return load_script_json("manage-settings.sh", *command, default={})


def apply_update_now() -> dict[str, object]:
    return load_script_json("manage-settings.sh", "apply-update", "--json", default={})


def regenerate_api_credentials() -> dict[str, str]:
    return load_script_json("manage-api-credentials.sh", "regenerate", "--json", default={})


def panel_template_dir() -> Path:
    installed_panel_dir = settings.base_dir / "bin" / "panel"
    if installed_panel_dir.is_dir():
        return installed_panel_dir
    return Path(__file__).resolve().parent / "templates"


def load_webmail_status() -> dict:
    """Read Webmail Roundcube status via manage-webmail.sh status --json."""
    try:
        return load_script_json("manage-webmail.sh", "status", "--json", default={"installed": False, "active": False})
    except Exception as exc:
        logger.warning("Failed loading webmail status: %s", exc)
        return {"installed": False, "active": False, "version": None, "auto_update": "no"}


def run_manage_webmail(action: str, *extra_args: str) -> dict:
    """Execute action on manage-webmail.sh and return JSON status."""
    return load_script_json("manage-webmail.sh", action, *extra_args, "--json", default={"installed": False})
