"""
Wizard Iniziale per Limristem eMail.
Gestione dello stato di primo avvio, verifica conformita europea (GDPR),
pre-check DNS per Let's Encrypt, download chiave pubblica di firma backup
e meccanismo di snooze (promemoria a 7 giorni).
"""

from __future__ import annotations

import json
import logging
import os
import re
import secrets
import string
import subprocess
import tempfile
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from .alerts import get_public_ip, get_public_ipv6
from .settings import get_settings

logger = logging.getLogger(__name__)

WIZARD_STATE_FILENAME = "wizard-state.json"
WIZARD_REDIS_KEY = "limristem_mail:wizard_state"
_WIZARD_DNS_CACHE: Dict[str, Any] = {"data": None, "ts": 0.0, "host": ""}
_WIZARD_DNS_CACHE_TTL: float = 60.0


def _state_file_path() -> Path:
    settings = get_settings()
    state_dir = Path(getattr(settings, "state_dir", "/var/lib/limristem-mail/state"))
    try:
        state_dir.mkdir(parents=True, exist_ok=True)
    except (OSError, PermissionError) as exc:
        logger.debug("Unable to create state dir %s: %s", state_dir, exc)
    return state_dir / WIZARD_STATE_FILENAME


def _load_redis_state() -> Optional[Dict[str, Any]]:
    try:
        from .cache import get_cache, safe_cache_get
        cache = get_cache()
        raw = safe_cache_get(cache, WIZARD_REDIS_KEY)
        if raw:
            if isinstance(raw, bytes):
                raw = raw.decode("utf-8")
            data = json.loads(raw)
            if isinstance(data, dict):
                return data
    except Exception as exc:
        logger.debug("_load_redis_state failed: %s", exc)
    return None


def _save_redis_state(state: Dict[str, Any]) -> bool:
    try:
        from .cache import get_cache, safe_cache_set
        cache = get_cache()
        return safe_cache_set(cache, WIZARD_REDIS_KEY, json.dumps(state))
    except Exception as exc:
        logger.debug("_save_redis_state failed: %s", exc)
        return False


def get_wizard_state() -> Dict[str, Any]:
    """Carica lo stato attuale del wizard dal file persistente o da Redis."""
    default_state = {
        "completed": False,
        "completed_at": None,
        "snoozed_until": None,
        "step": 1,
        "dns_verified": False,
        "ssl_configured": False,
        "backup_configured": False,
        "backup_tested": False,
        "backup_key_saved": False,
        "mfa_configured": False,
        "pending_private_key_token": None,
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    data = None
    try:
        path = _state_file_path()
        if path.is_file():
            content = path.read_text(encoding="utf-8")
            loaded = json.loads(content)
            if isinstance(loaded, dict):
                data = loaded
    except (OSError, PermissionError, json.JSONDecodeError) as exc:
        logger.warning("get_wizard_state: failed to read disk state: %s", exc)

    redis_data = _load_redis_state()
    if data is None:
        data = redis_data
    elif isinstance(redis_data, dict):
        # A failed cache write must not resurrect an old "completed" or backup
        # success flag after the operator reset it on disk.
        try:
            if int(redis_data.get("updated_at_ns", 0)) > int(data.get("updated_at_ns", 0)):
                data = redis_data
        except (TypeError, ValueError):
            pass

    if not isinstance(data, dict):
        return default_state

    # Assicura la presenza di tutte le chiavi predefinite
    for k, v in default_state.items():
        if k not in data:
            data[k] = v
    return data


def save_wizard_state(state: Dict[str, Any]) -> bool:
    """Salva lo stato del wizard su disco e su Redis cache."""
    state["updated_at_ns"] = time.time_ns()
    saved_disk = False
    temporary_path = None
    try:
        path = _state_file_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, prefix=".wizard-state-", delete=False) as handle:
            temporary_path = Path(handle.name)
            handle.write(json.dumps(state, indent=2))
            handle.flush()
            os.fsync(handle.fileno())
            os.fchmod(handle.fileno(), 0o660)
        os.replace(temporary_path, path)
        saved_disk = True
    except (OSError, PermissionError) as exc:
        logger.warning("save_wizard_state: failed to write disk state: %s", exc)
    finally:
        if temporary_path is not None:
            temporary_path.unlink(missing_ok=True)

    saved_redis = _save_redis_state(state)
    return saved_disk or saved_redis


def should_show_wizard() -> bool:
    """
    Determina se il wizard iniziale deve essere mostrato all'amministratore.
    - Se completato: False
    - Se posticipato (snooze di 7 giorni) e la data e futura: False
    - Altrimenti: True
    """
    state = get_wizard_state()
    if state.get("completed"):
        return False

    snoozed_until_str = state.get("snoozed_until")
    if snoozed_until_str:
        try:
            snoozed_until = datetime.fromisoformat(snoozed_until_str)
            if datetime.now(timezone.utc) < snoozed_until:
                return False
        except (ValueError, TypeError) as exc:
            logger.debug("should_show_wizard: invalid snoozed_until date: %s", exc)

    return True


def snooze_wizard(days: int = 7) -> Dict[str, Any]:
    """Posticipa la comparsa del wizard per il numero di giorni specificato (default: 7)."""
    state = get_wizard_state()
    until = datetime.now(timezone.utc) + timedelta(days=days)
    state["snoozed_until"] = until.isoformat()
    save_wizard_state(state)
    logger.info("Wizard snoozed until %s", state["snoozed_until"])
    return state


def complete_wizard() -> Dict[str, Any]:
    """Segna il wizard come completato."""
    state = get_wizard_state()
    state["completed"] = True
    state["completed_at"] = datetime.now(timezone.utc).isoformat()
    state["snoozed_until"] = None
    save_wizard_state(state)
    logger.info("Wizard completed at %s", state["completed_at"])
    return state


def clear_wizard_dns_cache() -> None:
    global _WIZARD_DNS_CACHE
    _WIZARD_DNS_CACHE = {"data": None, "ts": 0.0, "key": ()}


def reset_wizard() -> Dict[str, Any]:
    """Riavvia il wizard per consentire all'amministratore di ripercorrere la procedura guidata."""
    clear_wizard_dns_cache()
    state = get_wizard_state()
    state["completed"] = False
    state["completed_at"] = None
    state["snoozed_until"] = None
    state["step"] = 1
    save_wizard_state(state)
    logger.info("Wizard state reset by administrator")
    return state


def verify_dns_for_hostname(hostname: Optional[str] = None, force_refresh: bool = False) -> Dict[str, Any]:
    """
    Verifica live dei parametri di rete e record DNS (A, AAAA e PTR Reverse DNS)
    per garantire la corretta deliverability delle email e la pronta emissione
    di certificati Let's Encrypt.
    """
    global _WIZARD_DNS_CACHE
    settings = get_settings()
    host = (hostname or getattr(settings, "hostname", "")).strip().rstrip(".").lower()
    public_ip = (str(getattr(settings, "public_ip", "") or os.getenv("LIMRISTEM_MAIL_PUBLIC_IP", "")).strip() or get_public_ip())
    public_ipv6 = (str(getattr(settings, "public_ipv6", "") or os.getenv("LIMRISTEM_MAIL_PUBLIC_IPV6", "")).strip() or get_public_ipv6())
    now = time.time()
    cache_key = (host, public_ip, public_ipv6)
    if not force_refresh and _WIZARD_DNS_CACHE["data"] is not None:
        if _WIZARD_DNS_CACHE.get("key") == cache_key and (now - _WIZARD_DNS_CACHE["ts"]) < _WIZARD_DNS_CACHE_TTL:
            return dict(_WIZARD_DNS_CACHE["data"])

    result = {
        "hostname": host,
        "public_ip": public_ip,
        "public_ipv6": public_ipv6,
        "a_record": "",
        "aaaa_record": "",
        "a_matches": False,
        "aaaa_matches": False,
        "ptr_v4_record": "",
        "ptr_v6_record": "",
        "ptr_v4_matches": False,
        "ptr_v6_matches": False,
        "ptr_ok": False,
        "ready_for_letsencrypt": False,
        "all_network_ok": False,
        "errors": [],
        "warnings": [],
        "recommendations": [],
    }

    if not re.fullmatch(r"(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?", host):
        result["errors"].append("Hostname non valido o impostato su localhost.")
        return result

    # 1. Verifica Record A IPv4
    for resolver in ("@1.1.1.1", "@8.8.8.8"):
        try:
            res = subprocess.run(["dig", "+short", host, "A", resolver], capture_output=True, text=True, timeout=3)
            out = res.stdout.strip()
            if out:
                lines = [line.strip() for line in out.splitlines() if line.strip() and not line.startswith(";")]
                if lines:
                    result["a_record"] = lines[0]
                    if public_ip and public_ip in lines:
                        result["a_matches"] = True
                    break
        except (subprocess.CalledProcessError, FileNotFoundError, OSError) as exc:
            logger.debug("verify_dns_for_hostname dig A failed: %s", exc)
        except Exception as exc:
            logger.warning("verify_dns_for_hostname: suppressed %s: %s", type(exc).__name__, exc)

    # 2. Verifica Record AAAA IPv6
    for resolver in ("@1.1.1.1", "@8.8.8.8"):
        try:
            res = subprocess.run(["dig", "+short", host, "AAAA", resolver], capture_output=True, text=True, timeout=3)
            out = res.stdout.strip()
            if out:
                lines = [line.strip() for line in out.splitlines() if line.strip() and not line.startswith(";")]
                if lines:
                    result["aaaa_record"] = lines[0]
                    if public_ipv6 and public_ipv6.lower() in [l.lower() for l in lines]:
                        result["aaaa_matches"] = True
                    break
        except (subprocess.CalledProcessError, FileNotFoundError, OSError) as exc:
            logger.debug("verify_dns_for_hostname dig AAAA failed: %s", exc)
        except Exception as exc:
            logger.warning("verify_dns_for_hostname: suppressed %s: %s", type(exc).__name__, exc)

    # 3. Verifica Record PTR Reverse DNS (IPv4)
    if public_ip:
        for resolver in ("@1.1.1.1", "@8.8.8.8"):
            try:
                res = subprocess.run(["dig", "+short", "-x", public_ip, resolver], capture_output=True, text=True, timeout=3)
                out = res.stdout.strip()
                if out:
                    lines = [line.strip().rstrip(".").lower() for line in out.splitlines() if line.strip() and not line.startswith(";")]
                    if lines:
                        result["ptr_v4_record"] = host if host in lines else lines[0]
                        if host in lines or result["ptr_v4_record"] == host:
                            result["ptr_v4_matches"] = True
                        break
            except Exception as exc:
                logger.debug("verify_dns_for_hostname dig PTR IPv4 failed: %s", exc)

    # 4. Verifica Record PTR Reverse DNS (IPv6)
    if public_ipv6:
        for resolver in ("@1.1.1.1", "@8.8.8.8"):
            try:
                res = subprocess.run(["dig", "+short", "-x", public_ipv6, resolver], capture_output=True, text=True, timeout=3)
                out = res.stdout.strip()
                if out:
                    lines = [line.strip().rstrip(".").lower() for line in out.splitlines() if line.strip() and not line.startswith(";")]
                    if lines:
                        result["ptr_v6_record"] = host if host in lines else lines[0]
                        if host in lines or result["ptr_v6_record"] == host:
                            result["ptr_v6_matches"] = True
                        break
            except Exception as exc:
                logger.debug("verify_dns_for_hostname dig PTR IPv6 failed: %s", exc)

    # Valutazione DNS A / AAAA
    if not result["a_record"]:
        result["errors"].append(
            f"Nessun record DNS 'A' rilevato per '{host}'. "
            f"Crea un record A presso il tuo DNS provider: {host} -> {public_ip or '<IP pubblico>'}"
        )
    elif not public_ip:
        result["errors"].append("IP pubblico del server non rilevato: impossibile verificare il record A.")
    elif not result["a_matches"]:
        result["errors"].append(
            f"Il record DNS 'A' per '{host}' punta a '{result['a_record']}', ma l'IP del server e '{public_ip}'. "
            f"Let's Encrypt fallira finche il record A non puntera a {public_ip}."
        )
    else:
        result["a_matches"] = True

    if public_ipv6 and result["aaaa_record"] and not result["aaaa_matches"]:
        result["warnings"].append(
            f"Il record DNS 'AAAA' punta a '{result['aaaa_record']}', mentre l'IPv6 locale e '{public_ipv6}'."
        )

    # Valutazione PTR
    ptr_v4_ok = result["ptr_v4_matches"] if public_ip else True
    ptr_v6_ok = result["ptr_v6_matches"] if public_ipv6 else True
    result["ptr_ok"] = ptr_v4_ok and ptr_v6_ok

    if public_ip and not result["ptr_v4_matches"]:
        if result["ptr_v4_record"]:
            result["errors"].append(
                f"Il record Reverse DNS (PTR) per {public_ip} punta a '{result['ptr_v4_record']}', ma deve puntare a '{host}'. "
                "Senza un PTR coincidente, le email inviate a Gmail, Outlook e Yahoo verranno rifiutate o contrassegnate come spam."
            )
        else:
            result["errors"].append(
                f"Nessun record Reverse DNS (PTR) configurato per l'IP {public_ip}. "
                "Imposta il PTR nel pannello del tuo provider di hosting/VPS per farlo puntare all'hostname."
            )

    ipv6_ok = not result["aaaa_record"] or result["aaaa_matches"]
    result["ready_for_letsencrypt"] = result["a_matches"] and bool(result["a_record"]) and ipv6_ok
    result["all_network_ok"] = result["a_matches"] and result["ptr_ok"] and ipv6_ok
    _WIZARD_DNS_CACHE = {"data": dict(result), "ts": now, "key": cache_key}
    return result


def generate_secure_passphrase(length: int = 24) -> str:
    """Genera una passphrase robusta ad alta entropia per cifrare la chiave privata di backup."""
    chars = string.ascii_letters + string.digits + "!@#$%^&*()-_=+"
    return "".join(secrets.choice(chars) for _ in range(length))


def encrypt_private_key_pem(pem_str: str, passphrase: Optional[str] = None) -> str:
    """Cifra una chiave privata PEM con AES-256 (PKCS#8) usando la passphrase indicata."""
    if not passphrase:
        raise ValueError("A passphrase is required to encrypt the private key")
    try:
        from cryptography.hazmat.primitives import serialization
        key = serialization.load_pem_private_key(pem_str.encode("utf-8"), password=None)
        enc_bytes = key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.BestAvailableEncryption(passphrase.encode("utf-8")),
        )
        return enc_bytes.decode("utf-8")
    except Exception as exc:
        logger.warning("encrypt_private_key_pem failed: %s", exc)
        raise ValueError("Unable to encrypt the private key") from exc


def generate_backup_encryption_keypair() -> tuple[str, str]:
    """Genera una coppia di chiavi RSA 2048-bit per la cifratura asimmetrica dei backup (privata, pubblica)."""
    from cryptography.hazmat.primitives.asymmetric import rsa
    from cryptography.hazmat.primitives import serialization
    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    priv_pem = key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    ).decode("utf-8")
    pub_pem = key.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    ).decode("utf-8")
    return priv_pem, pub_pem


def evaluate_production_readiness(
    db=None,
    state: Optional[Dict[str, Any]] = None,
    dns_result: Optional[Dict[str, Any]] = None,
    ssl_config: Optional[Dict[str, Any]] = None,
    storages_list: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
    """
    Valuta lo stato di idoneita alla produzione secondo i criteri:
    1. Parametri di Rete, Record A & Reverse DNS (PTR) tutti verdi e allineati
    2. Certificato SSL / TLS valido e attendibile
    3. Storage di Backup Remoto (SFTP o S3) configurato con crittografia attiva
    4. Connessione dello Storage di Backup testata con successo
    5. Chiave privata di decifratura scaricata e custodita dall'operatore
    6. Autenticazione a Due Fattori (MFA / 2FA) attiva per l'amministratore
    """
    if state is None:
        state = get_wizard_state()
    if dns_result is None:
        dns_result = verify_dns_for_hostname()

    # 1. DNS & PTR
    dns_ok = bool(
        dns_result.get("a_matches")
        and dns_result.get("ptr_ok", False)
        and (not dns_result.get("aaaa_record") or dns_result.get("aaaa_matches"))
    )
    dns_status = {
        "id": "dns_ptr",
        "title": "Parametri di Rete, Record A & Reverse DNS (PTR)",
        "step": 1,
        "status": "ok" if dns_ok else ("warning" if dns_result.get("a_matches") else "error"),
        "message": (
            "Record A e PTR Reverse DNS perfettamente allineati e corretti."
            if dns_ok
            else ("Record PTR o AAAA mancanti o disallineati: verificare la configurazione DNS." if dns_result.get("a_matches") else "Record A non configurato o disallineato.")
        ),
    }

    # 2. SSL
    if ssl_config is None:
        from .admin_ops import load_ssl_config
        try:
            ssl_config = load_ssl_config()
        except Exception as exc:
            logger.debug("evaluate_production_readiness: load_ssl_config failed: %s", exc)
            ssl_config = {}
    ssl_mode = ssl_config.get("mode", "")
    cert_exists = ssl_config.get("cert_exists", False)
    days_left = ssl_config.get("cert_days_left", (ssl_config.get("cert_info") or {}).get("days_left"))
    ssl_ready = (ssl_config.get("status") == "ready") and cert_exists and (days_left is None or days_left > 0)
    ssl_trusted = ssl_ready and (ssl_mode in ("letsencrypt", "manual"))
    mode_label = "Let's Encrypt" if ssl_mode == "letsencrypt" else ("Personalizzato / Manuale" if ssl_mode == "manual" else ssl_mode.title())
    ssl_status = {
        "id": "ssl_tls",
        "title": "Certificato SSL / TLS (Cifratura in Transito)",
        "step": 2,
        "status": "ok" if ssl_trusted else ("warning" if ssl_ready else "error"),
        "message": (
            f"Certificato attivo e fidato ({mode_label})."
            if ssl_trusted
            else ("Certificato auto-firmato attivo (i client email potrebbero mostrare avvisi di sicurezza)." if ssl_ready else "Nessun certificato SSL/TLS valido disponibile.")
        ),
    }

    # 3. Backup Remoto Cifrato & Testato
    if storages_list is None:
        from .admin_ops import load_backup_storages
        try:
            storages_list = load_backup_storages()
        except Exception as exc:
            logger.debug("evaluate_production_readiness: load_backup_storages failed: %s", exc)
            storages_list = []
    has_remote_encrypted = False
    for st in storages_list:
        st_type = str(st.get("type", "")).lower()
        st_enc = str(st.get("encrypt", "no")).lower()
        if st_type in ("sftp", "s3") and st_enc == "yes":
            has_remote_encrypted = True
            break
    backup_tested = bool(state.get("backup_tested", False))
    backup_ok = has_remote_encrypted and backup_tested
    backup_status = {
        "id": "backup_storage",
        "title": "Storage di Backup Remoto Cifrato & Test Connessione",
        "step": 3,
        "status": "ok" if backup_ok else ("warning" if has_remote_encrypted else "error"),
        "message": (
            "Storage remoto cifrato (SFTP/S3) configurato e testato con successo."
            if backup_ok
            else ("Storage remoto cifrato configurato ma non ancora testato con successo." if has_remote_encrypted else "Nessun backup remoto cifrato (SFTP/S3) configurato.")
        ),
    }

    # 4. Custodia Chiave Privata
    key_saved = bool(state.get("backup_key_saved", False))
    key_status = {
        "id": "backup_key",
        "title": "Custodia Chiave Privata di Decifratura",
        "step": 3,
        "status": "ok" if key_saved else "error",
        "message": (
            "Chiave privata di decifratura scaricata e custodita in sicurezza."
            if key_saved
            else "La chiave privata di decifratura non risulta ancora scaricata e confermata."
        ),
    }

    # 5. MFA / 2FA
    mfa_active = False
    try:
        from .mfa import admin_mfa_status
        from .db import SessionLocal
        session_db = db or SessionLocal()
        try:
            mfa_info = admin_mfa_status(session_db)
            mfa_active = bool(mfa_info.get("mfa_enabled"))
        finally:
            if not db:
                session_db.close()
    except Exception as exc:
        logger.debug("evaluate_production_readiness mfa check: %s", exc)

    mfa_status = {
        "id": "mfa_2fa",
        "title": "Autenticazione a Due Fattori (MFA / 2FA)",
        "step": 4,
        "status": "ok" if mfa_active else "error",
        "message": (
            "Autenticazione a Due Fattori (TOTP) attiva e verificata."
            if mfa_active
            else "2FA non attivo. Abilita un secondo fattore per proteggere l'accesso amministrativo."
        ),
    }

    checks = [dns_status, ssl_status, backup_status, key_status, mfa_status]
    has_errors = any(c["status"] == "error" for c in checks)
    has_warnings = any(c["status"] == "warning" for c in checks)
    is_ready = not has_errors and not has_warnings

    return {
        "is_ready": is_ready,
        "has_errors": has_errors,
        "has_warnings": has_warnings,
        "level": "success" if is_ready else ("warning" if not has_errors else "danger"),
        "headline": (
            "✅ Controlli di configurazione completati: verificare invio, ricezione e ripristino prima della produzione"
            if is_ready
            else "⚠️ Configurazione Incompleta: Server NON Pronto per la Produzione"
        ),
        "checks": checks,
    }


def get_backup_signing_key_content() -> Optional[str]:
    """Recupera il contenuto della chiave pubblica di firma dei backup."""
    try:
        from .backup_key import signing_public_key, escrow_copy_paths
        key = signing_public_key()
        if key:
            return key.strip()
        for p in escrow_copy_paths():
            if p.is_file():
                try:
                    content = p.read_text(encoding="utf-8").strip()
                    if content:
                        return content
                except (OSError, PermissionError):
                    pass
    except Exception as exc:
        logger.debug("get_backup_signing_key_content: backup_key lookup error: %s", exc)

    candidates = [
        Path("/opt/limristem-mail/limristem-mail-backup-signing.pub"),
        Path("/opt/limristem-mail/config/backup-signing.pub"),
        Path("/root/limristem-mail-backup-signing.pub"),
        Path("/etc/limristem-mail/limristem-mail-backup-signing.pub"),
    ]
    for p in candidates:
        if p.is_file():
            try:
                content = p.read_text(encoding="utf-8").strip()
                if content:
                    return content
            except (OSError, PermissionError) as exc:
                logger.debug("Unable to read backup pubkey from %s: %s", p, exc)
    return None
