"""
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
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"


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):
        # Sync if Redis has newer snooze or completed state
        if redis_data.get("snoozed_until") and not data.get("snoozed_until"):
            data["snoozed_until"] = redis_data["snoozed_until"]
        if redis_data.get("completed") and not data.get("completed"):
            data["completed"] = True
            data["completed_at"] = redis_data.get("completed_at")
        for k in ("backup_tested", "backup_key_saved", "backup_configured", "ssl_configured", "mfa_configured"):
            if redis_data.get(k) and not data.get(k):
                data[k] = redis_data[k]

    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."""
    saved_disk = False
    try:
        path = _state_file_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(json.dumps(state, indent=2), encoding="utf-8")
        try:
            os.chmod(path, 0o660)
        except OSError:
            pass
        saved_disk = True
    except (OSError, PermissionError) as exc:
        logger.warning("save_wizard_state: failed to write disk state: %s", exc)

    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 reset_wizard() -> Dict[str, Any]:
    """Riavvia il wizard per consentire all'amministratore di ripercorrere la procedura guidata."""
    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) -> 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.
    """
    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())

    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 host or host == "localhost":
        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 public_ip and 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."
            )

    result["ready_for_letsencrypt"] = result["a_matches"] and bool(result["a_record"])
    result["all_network_ok"] = result["a_matches"] and result["ptr_ok"] and (not public_ipv6 or result["aaaa_matches"] or not result["aaaa_record"])
    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 or not passphrase.strip():
        return pem_str
    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.strip().encode("utf-8")),
        )
        return enc_bytes.decode("utf-8")
    except Exception as exc:
        logger.warning("encrypt_private_key_pem failed: %s", exc)
        return pem_str


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) -> 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
    """
    state = get_wizard_state()
    dns_res = verify_dns_for_hostname()

    # 1. DNS & PTR
    dns_ok = bool(dns_res.get("a_matches") and dns_res.get("ptr_ok", False))
    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_res.get("a_matches") else "error"),
        "message": (
            "Record A e PTR Reverse DNS perfettamente allineati e corretti."
            if dns_ok
            else ("Record PTR non coincidente o mancante: le email potrebbero essere respinte da Gmail/Outlook/Yahoo." if dns_res.get("a_matches") else "Record A non configurato o disallineato.")
        ),
    }

    # 2. SSL
    from .admin_ops import load_ssl_config
    ssl_cfg = {}
    try:
        ssl_cfg = load_ssl_config()
    except Exception as exc:
        logger.debug("evaluate_production_readiness: load_ssl_config failed: %s", exc)
    ssl_mode = ssl_cfg.get("mode", "")
    cert_exists = ssl_cfg.get("cert_exists", False)
    ssl_ready = (ssl_cfg.get("status") == "ready") and cert_exists
    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 configurato.")
        ),
    }

    # 3. Backup Remoto Cifrato & Testato
    from .admin_ops import load_backup_storages
    storages = []
    try:
        storages = load_backup_storages()
    except Exception as exc:
        logger.debug("evaluate_production_readiness: load_backup_storages failed: %s", exc)
    has_remote_encrypted = False
    for st in storages:
        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. Obbligatorio per conformità NIS2 e sicurezza del pannello."
        ),
    }

    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": (
            "✅ Server Pronto per la Produzione (Conforme GDPR & NIS2)"
            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
