"""Service prerequis — verification automatique des serveurs d'une campagne
Check basique (TCP) + check approfondi (SSH) si accessible"""
import socket
import paramiko
import os
from sqlalchemy import text

# Mode demo : tout passe OK sans verifier (hors reseau SANEF)
DEMO_MODE = True  # Passer a False en production SANEF

# Seuils espace disque (Mo)
DISK_ROOT_MIN_MB = 1200  # 1.2 Go minimum sur /
DISK_VAR_MIN_MB = 800    # 800 Mo minimum sur /var ou /var/log

# SSH config (pour check approfondi)
SSH_KEY = "/opt/patchcenter/keys/id_rsa_cybglobal.pem"  # Copier la cle ici
SSH_USER = "cybsecope"
SSH_TIMEOUT = 10
DNS_SUFFIXES = ["", ".sanef.groupe", ".sanef-rec.fr", ".sanef.fr"]


def check_prereqs_campaign(db, campaign_id):
    """Verifie les prereqs de tous les serveurs pending d'une campagne."""
    sessions = db.execute(text("""
        SELECT ps.id, s.hostname, s.os_family, s.etat, s.licence_support,
            s.machine_type, s.satellite_url AS satellite_host, s.ssh_method,
            d.code as domain_code, z.name as zone
        FROM patch_sessions ps
        JOIN servers s ON ps.server_id = s.id
        LEFT JOIN domain_environments de ON s.domain_env_id = de.id
        LEFT JOIN domains d ON de.domain_id = d.id
        LEFT JOIN zones z ON s.zone_id = z.id
        WHERE ps.campaign_id = :cid AND ps.status = 'pending'
    """), {"cid": campaign_id}).fetchall()

    checked = 0
    for s in sessions:
        result = _check_server(s)
        _save_result(db, s.id, result)
        checked += 1

    auto_excluded = _auto_exclude(db, campaign_id)
    db.commit()
    return checked, auto_excluded


def _resolve_host(hostname):
    """Trouve un FQDN resolvable et joignable sur port 22"""
    for suffix in DNS_SUFFIXES:
        target = hostname + suffix
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(5)
            r = sock.connect_ex((target, 22))
            sock.close()
            if r == 0:
                return target
        except (socket.gaierror, socket.timeout, OSError):
            continue
    return None


def _ssh_connect(target):
    """Tente une connexion SSH par cle. Retourne le client ou None."""
    if not os.path.exists(SSH_KEY):
        return None
    for loader in [paramiko.RSAKey.from_private_key_file, paramiko.Ed25519Key.from_private_key_file]:
        try:
            key = loader(SSH_KEY)
            client = paramiko.SSHClient()
            client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            client.connect(target, port=22, username=SSH_USER, pkey=key,
                           timeout=SSH_TIMEOUT, look_for_keys=False, allow_agent=False)
            return client
        except Exception:
            continue
    return None


def _ssh_cmd(client, cmd, timeout=8):
    """Execute une commande SSH et retourne stdout"""
    try:
        stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout)
        return stdout.read().decode("utf-8", errors="replace").strip()
    except Exception:
        return ""


def _check_disk(client):
    """Verifie l'espace disque via SSH. Retourne (root_mb, var_mb, ok)"""
    output = _ssh_cmd(client, "df -BM --output=target,avail 2>/dev/null | grep -E '^/ |^/var' | head -5")
    root_mb = None
    var_mb = None
    for line in output.split("\n"):
        parts = line.split()
        if len(parts) >= 2:
            mount = parts[0]
            try:
                avail = int(parts[1].replace("M", ""))
            except ValueError:
                continue
            if mount == "/":
                root_mb = avail
            elif mount in ("/var", "/var/log"):
                var_mb = avail if var_mb is None or avail < (var_mb or 9999) else var_mb

    # Si /var pas monte separement, il est dans /
    if var_mb is None and root_mb is not None:
        var_mb = root_mb

    ok = True
    if root_mb is not None and root_mb < DISK_ROOT_MIN_MB:
        ok = False
    if var_mb is not None and var_mb < DISK_VAR_MIN_MB:
        ok = False

    return root_mb, var_mb, ok


def _check_satellite_ssh(client):
    """Verifie la connectivite Satellite via SSH"""
    output = _ssh_cmd(client, "subscription-manager identity 2>/dev/null | grep -i 'org' || echo 'not_registered'")
    if "not_registered" in output or not output:
        return "ko"
    return "ok"


def _check_server(s):
    """Verifie un serveur. Check basique + approfondi si SSH OK."""
    result = {
        "ssh": "pending", "satellite": "pending", "rollback": None,
        "disk_root_mb": None, "disk_var_mb": None, "disk_ok": None,
        "eligible": True, "exclude_reason": None, "exclude_detail": None,
    }

    # Mode demo : tout OK
    if DEMO_MODE:
        result["ssh"] = "ok"
        result["satellite"] = "ok" if s.os_family == 'linux' else "na"
        result["rollback"] = "snapshot" if s.machine_type == 'vm' else "na"
        result["disk_root_mb"] = 5000
        result["disk_var_mb"] = 3000
        result["disk_ok"] = True
        # Quand meme exclure les EOL et decom
        if s.licence_support == 'obsolete':
            result["eligible"] = False
            result["exclude_reason"] = "obsolete"
            result["exclude_detail"] = "Licence EOL"
        elif s.etat != 'Production':
            result["eligible"] = False
            result["exclude_reason"] = "non_patchable"
            result["exclude_detail"] = f"Etat: {s.etat}"
        return result

    # 1. Eligibilite de base
    if s.licence_support == 'obsolete':
        result["eligible"] = False
        result["exclude_reason"] = "obsolete"
        result["exclude_detail"] = "Licence EOL — serveur non supporte"
        return result

    if s.etat != 'Production':
        result["eligible"] = False
        result["exclude_reason"] = "non_patchable"
        result["exclude_detail"] = f"Etat: {s.etat}"
        return result

    # 2. Connectivite TCP port 22
    target = _resolve_host(s.hostname)
    if not target:
        result["ssh"] = "ko"
        result["eligible"] = False
        result["exclude_reason"] = "creneau_inadequat"
        result["exclude_detail"] = f"Port 22 injoignable ({s.hostname})"
        return result

    result["ssh"] = "ok"

    # 3. Rollback
    if s.machine_type == 'vm':
        result["rollback"] = "snapshot"
    else:
        result["rollback"] = "na"

    # 4. Check approfondi via SSH (si cle dispo)
    client = _ssh_connect(target)
    if client:
        try:
            # Espace disque
            root_mb, var_mb, disk_ok = _check_disk(client)
            result["disk_root_mb"] = root_mb
            result["disk_var_mb"] = var_mb
            result["disk_ok"] = disk_ok

            if not disk_ok:
                detail_parts = []
                if root_mb is not None and root_mb < DISK_ROOT_MIN_MB:
                    detail_parts.append(f"/ = {root_mb}Mo (min {DISK_ROOT_MIN_MB}Mo)")
                if var_mb is not None and var_mb < DISK_VAR_MIN_MB:
                    detail_parts.append(f"/var = {var_mb}Mo (min {DISK_VAR_MIN_MB}Mo)")
                result["eligible"] = False
                result["exclude_reason"] = "creneau_inadequat"
                result["exclude_detail"] = "Espace disque insuffisant: " + ", ".join(detail_parts)

            # Satellite reel (Linux)
            if s.os_family == 'linux':
                result["satellite"] = _check_satellite_ssh(client)
            else:
                result["satellite"] = "na"
        finally:
            try:
                client.close()
            except Exception:
                pass
    else:
        # Pas de connexion SSH approfondie — fallback sur les donnees en base
        if s.os_family == 'linux':
            result["satellite"] = "ok" if s.satellite_host else "ko"
        else:
            result["satellite"] = "na"
        result["disk_ok"] = None  # Non verifie

    return result


def _save_result(db, session_id, result):
    """Sauvegarde les resultats prereqs"""
    all_ok = (result["eligible"]
              and result["ssh"] == "ok"
              and result.get("disk_ok") is not False)
    db.execute(text("""
        UPDATE patch_sessions SET
            prereq_ssh = :ssh, prereq_satellite = :sat,
            rollback_method = COALESCE(rollback_method, :rb),
            prereq_disk_root = :dr, prereq_disk_log = :dv,
            prereq_disk_root_mb = :drm, prereq_disk_var_mb = :dvm,
            prereq_disk_ok = :dok,
            prereq_validated = :valid, prereq_date = now()
        WHERE id = :id
    """), {
        "id": session_id, "ssh": result["ssh"], "sat": result["satellite"],
        "rb": result["rollback"],
        "dr": result["disk_root_mb"], "dv": result["disk_var_mb"],
        "drm": result["disk_root_mb"], "dvm": result["disk_var_mb"],
        "dok": result["disk_ok"],
        "valid": all_ok,
    })


def _auto_exclude(db, campaign_id):
    """Exclut les serveurs non eligibles apres verification"""
    non_eligible = db.execute(text("""
        SELECT ps.id, s.hostname, s.licence_support, s.etat,
            ps.prereq_ssh, ps.prereq_disk_ok
        FROM patch_sessions ps
        JOIN servers s ON ps.server_id = s.id
        WHERE ps.campaign_id = :cid AND ps.status = 'pending'
          AND (s.licence_support = 'obsolete'
               OR s.etat != 'Production'
               OR ps.prereq_ssh = 'ko'
               OR ps.prereq_disk_ok = false)
    """), {"cid": campaign_id}).fetchall()

    count = 0
    for s in non_eligible:
        if s.licence_support == 'obsolete':
            reason, detail = "obsolete", "Licence EOL — auto-exclu"
        elif s.etat != 'Production':
            reason, detail = "non_patchable", f"Etat {s.etat} — auto-exclu"
        elif s.prereq_disk_ok is False:
            reason, detail = "creneau_inadequat", "Espace disque insuffisant — auto-exclu"
        else:
            reason, detail = "creneau_inadequat", "SSH injoignable — auto-exclu"

        db.execute(text("""
            UPDATE patch_sessions SET
                status = 'excluded', exclusion_reason = :r,
                exclusion_detail = :d, excluded_by = 'system', excluded_at = now()
            WHERE id = :id
        """), {"id": s.id, "r": reason, "d": detail})
        count += 1

    if count > 0:
        total = db.execute(text(
            "SELECT COUNT(*) FROM patch_sessions WHERE campaign_id = :cid AND status NOT IN ('excluded','cancelled')"
        ), {"cid": campaign_id}).scalar()
        db.execute(text("UPDATE campaigns SET total_servers = :c WHERE id = :cid"),
                   {"c": total, "cid": campaign_id})

    return count


def check_single_prereq(db, session_id):
    """Verifie les prereqs d'un seul serveur"""
    s = db.execute(text("""
        SELECT ps.id, s.hostname, s.os_family, s.etat, s.licence_support,
            s.machine_type, s.satellite_url AS satellite_host, s.ssh_method,
            d.code as domain_code, z.name as zone
        FROM patch_sessions ps
        JOIN servers s ON ps.server_id = s.id
        LEFT JOIN domain_environments de ON s.domain_env_id = de.id
        LEFT JOIN domains d ON de.domain_id = d.id
        LEFT JOIN zones z ON s.zone_id = z.id
        WHERE ps.id = :sid
    """), {"sid": session_id}).fetchone()

    if not s:
        return None

    result = _check_server(s)
    _save_result(db, session_id, result)
    db.commit()
    return result
