"""Service QuickWin — gestion des campagnes + exclusions par serveur"""
import json
from sqlalchemy import text
from .quickwin_log_service import log_info, log_warn, log_error, log_success

# Exclusions generales par defaut (reboot packages + middleware/apps)
DEFAULT_GENERAL_EXCLUDES = (
    "dbus* dracut* glibc* grub2* kernel* kexec-tools* "
    "libselinux* linux-firmware* microcode_ctl* mokutil* "
    "net-snmp* NetworkManager* network-scripts* nss* openssl-libs* "
    "polkit* selinux-policy* shim* systemd* tuned*"
)


def get_server_configs(db, server_ids=None):
    """Retourne les configs QuickWin pour les serveurs (ou tous)"""
    if server_ids:
        rows = db.execute(text("""
            SELECT qc.*, s.hostname, s.os_family, s.tier,
                   d.name as domaine, e.name as environnement,
                   z.name as zone
            FROM quickwin_server_config qc
            JOIN servers s ON qc.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 environments e ON de.environment_id = e.id
            LEFT JOIN zones z ON s.zone_id = z.id
            WHERE qc.server_id = ANY(:ids)
            ORDER BY s.hostname
        """), {"ids": server_ids}).fetchall()
    else:
        rows = db.execute(text("""
            SELECT qc.*, s.hostname, s.os_family, s.tier,
                   d.name as domaine, e.name as environnement,
                   z.name as zone
            FROM quickwin_server_config qc
            JOIN servers s ON qc.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 environments e ON de.environment_id = e.id
            LEFT JOIN zones z ON s.zone_id = z.id
            ORDER BY s.hostname
        """)).fetchall()
    return rows


def upsert_server_config(db, server_id, general_excludes=None, specific_excludes="", notes=""):
    """Cree ou met a jour la config QuickWin d'un serveur.
    Si general_excludes est vide lors de la creation, applique DEFAULT_GENERAL_EXCLUDES."""
    existing = db.execute(text(
        "SELECT id FROM quickwin_server_config WHERE server_id = :sid"
    ), {"sid": server_id}).fetchone()
    if existing:
        ge = general_excludes if general_excludes is not None else DEFAULT_GENERAL_EXCLUDES
        db.execute(text("""
            UPDATE quickwin_server_config
            SET general_excludes = :ge, specific_excludes = :se, notes = :n, updated_at = now()
            WHERE server_id = :sid
        """), {"sid": server_id, "ge": ge, "se": specific_excludes, "n": notes})
    else:
        ge = general_excludes if general_excludes else DEFAULT_GENERAL_EXCLUDES
        db.execute(text("""
            INSERT INTO quickwin_server_config (server_id, general_excludes, specific_excludes, notes)
            VALUES (:sid, :ge, :se, :n)
        """), {"sid": server_id, "ge": ge, "se": specific_excludes, "n": notes})
    db.commit()


def delete_server_config(db, config_id):
    db.execute(text("DELETE FROM quickwin_server_config WHERE id = :id"), {"id": config_id})
    db.commit()


def get_eligible_servers(db):
    """Serveurs Linux production, patch_os_owner=secops"""
    return db.execute(text("""
        SELECT s.id, s.hostname, s.os_family, s.os_version, s.machine_type,
               s.tier, s.etat, s.patch_excludes, s.is_flux_libre, s.is_podman,
               d.name as domaine, d.code as domain_code,
               e.name as environnement, e.code as env_code,
               COALESCE(qc.general_excludes, '') as qw_general_excludes,
               COALESCE(qc.specific_excludes, '') as qw_specific_excludes
        FROM servers s
        LEFT JOIN domain_environments de ON s.domain_env_id = de.id
        LEFT JOIN domains d ON de.domain_id = d.id
        LEFT JOIN environments e ON de.environment_id = e.id
        LEFT JOIN quickwin_server_config qc ON qc.server_id = s.id
        WHERE s.os_family = 'linux'
          AND s.etat = 'Production'
          AND s.patch_os_owner = 'secops'
        ORDER BY e.display_order, d.display_order, s.hostname
    """)).fetchall()


# -- Runs --

def list_runs(db):
    return db.execute(text("""
        SELECT r.*,
               u.display_name as created_by_name,
               (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id) as total_entries,
               (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.status = 'patched') as patched_count,
               (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.status = 'failed') as failed_count,
               (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.branch = 'hprod') as hprod_count,
               (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.branch = 'prod') as prod_count
        FROM quickwin_runs r
        LEFT JOIN users u ON r.created_by = u.id
        ORDER BY r.year DESC, r.week_number DESC, r.id DESC
    """)).fetchall()


def get_run(db, run_id):
    return db.execute(text("""
        SELECT r.*, u.display_name as created_by_name
        FROM quickwin_runs r LEFT JOIN users u ON r.created_by = u.id
        WHERE r.id = :id
    """), {"id": run_id}).fetchone()


def get_run_entries(db, run_id):
    return db.execute(text("""
        SELECT qe.*, s.hostname, s.fqdn, s.os_family, s.machine_type,
               d.name as domaine, e.name as environnement
        FROM quickwin_entries qe
        JOIN servers s ON qe.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 environments e ON de.environment_id = e.id
        WHERE qe.run_id = :rid
        ORDER BY qe.branch, s.hostname
    """), {"rid": run_id}).fetchall()


def create_run(db, year, week_number, label, user_id, server_ids, notes=""):
    """Cree un run QuickWin avec les serveurs selectionnes.
    Classe auto en hprod/prod selon l'environnement du serveur."""
    row = db.execute(text("""
        INSERT INTO quickwin_runs (year, week_number, label, created_by, notes)
        VALUES (:y, :w, :l, :uid, :n) RETURNING id
    """), {"y": year, "w": week_number, "l": label, "uid": user_id, "n": notes}).fetchone()
    run_id = row.id

    # Lire les reboot packages globaux (source: app_secrets)
    from .secrets_service import get_secret
    reboot_pkgs = get_secret(db, "patching_reboot_packages") or DEFAULT_GENERAL_EXCLUDES

    for sid in server_ids:
        srv = db.execute(text("""
            SELECT s.id, e.name as env_name, COALESCE(s.patch_excludes, '') as pe
            FROM servers s
            LEFT JOIN domain_environments de ON s.domain_env_id = de.id
            LEFT JOIN environments e ON de.environment_id = e.id
            WHERE s.id = :sid
        """), {"sid": sid}).fetchone()
        if not srv:
            continue
        branch = "prod" if srv.env_name and "production" in srv.env_name.lower() else "hprod"
        # QuickWin : reboot globaux + exclusions iTop du serveur
        db.execute(text("""
            INSERT INTO quickwin_entries (run_id, server_id, branch, general_excludes, specific_excludes)
            VALUES (:rid, :sid, :br, :ge, :se)
        """), {"rid": run_id, "sid": sid, "br": branch, "ge": reboot_pkgs, "se": srv.pe})

    db.commit()
    return run_id


def delete_run(db, run_id):
    db.execute(text("DELETE FROM quickwin_entries WHERE run_id = :rid"), {"rid": run_id})
    db.execute(text("DELETE FROM quickwin_runs WHERE id = :rid"), {"rid": run_id})
    db.commit()


def get_available_servers(db, run_id, search="", domains=None, envs=None, zones=None):
    """Serveurs eligibles PAS encore dans ce run, avec multi-filtres.
    domains/envs/zones: listes de noms (multi-select)."""
    rows = db.execute(text("""
        SELECT s.id, s.hostname, s.os_family, s.machine_type, s.domain_ltd,
               d.name as domaine, e.name as environnement, e.code as env_code,
               z.name as zone
        FROM servers s
        LEFT JOIN domain_environments de ON s.domain_env_id = de.id
        LEFT JOIN domains d ON de.domain_id = d.id
        LEFT JOIN environments e ON de.environment_id = e.id
        LEFT JOIN zones z ON s.zone_id = z.id
        WHERE s.os_family = 'linux'
          AND s.etat = 'Production'
          AND s.patch_os_owner = 'secops'
          AND s.id NOT IN (SELECT server_id FROM quickwin_entries WHERE run_id = :rid)
        ORDER BY d.name, e.name, s.hostname
    """), {"rid": run_id}).fetchall()
    filtered = rows
    if search:
        filtered = [r for r in filtered if search.lower() in r.hostname.lower()]
    if domains:
        filtered = [r for r in filtered if (r.domaine or '') in domains]
    if envs:
        filtered = [r for r in filtered if (r.environnement or '') in envs]
    if zones:
        filtered = [r for r in filtered if (r.zone or '') in zones]
    return filtered


def get_available_filters(db, run_id):
    """Retourne les domaines, envs et zones disponibles (avec compteurs)."""
    rows = db.execute(text("""
        SELECT d.name as domaine, e.name as environnement, z.name as zone
        FROM servers s
        LEFT JOIN domain_environments de ON s.domain_env_id = de.id
        LEFT JOIN domains d ON de.domain_id = d.id
        LEFT JOIN environments e ON de.environment_id = e.id
        LEFT JOIN zones z ON s.zone_id = z.id
        WHERE s.os_family = 'linux'
          AND s.etat = 'Production'
          AND s.patch_os_owner = 'secops'
          AND s.id NOT IN (SELECT server_id FROM quickwin_entries WHERE run_id = :rid)
    """), {"rid": run_id}).fetchall()
    domains = sorted(set(r.domaine for r in rows if r.domaine))
    envs = sorted(set(r.environnement for r in rows if r.environnement))
    zones = sorted(set(r.zone for r in rows if r.zone))
    # Compteurs
    dom_counts = {}
    for r in rows:
        k = r.domaine or '?'
        dom_counts[k] = dom_counts.get(k, 0) + 1
    env_counts = {}
    for r in rows:
        k = r.environnement or '?'
        env_counts[k] = env_counts.get(k, 0) + 1
    zone_counts = {}
    for r in rows:
        k = r.zone or '?'
        zone_counts[k] = zone_counts.get(k, 0) + 1
    return domains, envs, zones, dom_counts, env_counts, zone_counts


def add_entries_to_run(db, run_id, server_ids, user=None):
    """Ajoute des serveurs a un run existant. Determine auto hprod/prod.
    Retourne le nombre de serveurs ajoutes."""
    existing = set(r.server_id for r in db.execute(text(
        "SELECT server_id FROM quickwin_entries WHERE run_id = :rid"
    ), {"rid": run_id}).fetchall())

    by = user.get("display_name", user.get("username", "")) if user else ""
    from .secrets_service import get_secret
    reboot_pkgs = get_secret(db, "patching_reboot_packages") or DEFAULT_GENERAL_EXCLUDES

    added = 0
    hostnames = []
    for sid in server_ids:
        if sid in existing:
            continue
        srv = db.execute(text("""
            SELECT s.id, s.hostname, e.name as env_name, COALESCE(s.patch_excludes, '') as pe
            FROM servers s
            LEFT JOIN domain_environments de ON s.domain_env_id = de.id
            LEFT JOIN environments e ON de.environment_id = e.id
            WHERE s.id = :sid
        """), {"sid": sid}).fetchone()
        if not srv:
            continue
        branch = "prod" if srv.env_name and "production" in srv.env_name.lower() else "hprod"
        db.execute(text("""
            INSERT INTO quickwin_entries (run_id, server_id, branch, general_excludes, specific_excludes)
            VALUES (:rid, :sid, :br, :ge, :se)
        """), {"rid": run_id, "sid": sid, "br": branch, "ge": reboot_pkgs, "se": srv.pe})
        added += 1
        hostnames.append(srv.hostname)
    if added:
        log_info(db, run_id, "servers", f"{added} serveur(s) ajoute(s)",
                 detail=", ".join(hostnames[:20]) + ("..." if len(hostnames) > 20 else ""),
                 created_by=by)
    db.commit()
    return added


def remove_entries_from_run(db, run_id, entry_ids, user=None):
    """Supprime des entries d'un run. Retourne le nombre de suppressions."""
    if not entry_ids:
        return 0
    by = user.get("display_name", user.get("username", "")) if user else ""
    # Log hostnames avant suppression
    rows = db.execute(text("""
        SELECT s.hostname FROM quickwin_entries qe
        JOIN servers s ON qe.server_id = s.id
        WHERE qe.run_id = :rid AND qe.id = ANY(:ids)
    """), {"rid": run_id, "ids": entry_ids}).fetchall()
    hostnames = [r.hostname for r in rows]
    result = db.execute(text(
        "DELETE FROM quickwin_entries WHERE run_id = :rid AND id = ANY(:ids)"
    ), {"rid": run_id, "ids": entry_ids})
    removed = result.rowcount
    if removed:
        log_warn(db, run_id, "servers", f"{removed} serveur(s) supprime(s)",
                 detail=", ".join(hostnames[:20]) + ("..." if len(hostnames) > 20 else ""),
                 created_by=by)
    db.commit()
    return removed


def get_campaign_scope(db, run_id):
    """Retourne domaines, envs et zones presents dans le run avec compteurs."""
    rows = db.execute(text("""
        SELECT d.name as domaine, e.name as environnement, z.name as zone, qe.status
        FROM quickwin_entries qe
        JOIN servers s ON qe.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 environments e ON de.environment_id = e.id
        LEFT JOIN zones z ON s.zone_id = z.id
        WHERE qe.run_id = :rid
    """), {"rid": run_id}).fetchall()
    dom_counts = {}
    env_counts = {}
    zone_counts = {}
    dom_active = {}  # non-excluded count
    zone_active = {}  # non-excluded count
    for r in rows:
        d = r.domaine or '?'
        e = r.environnement or '?'
        z = r.zone or '?'
        dom_counts[d] = dom_counts.get(d, 0) + 1
        env_counts[e] = env_counts.get(e, 0) + 1
        zone_counts[z] = zone_counts.get(z, 0) + 1
        if r.status != 'excluded':
            dom_active[d] = dom_active.get(d, 0) + 1
            zone_active[z] = zone_active.get(z, 0) + 1
    domains = sorted(k for k in dom_counts if k != '?')
    envs = sorted(k for k in env_counts if k != '?')
    zones = sorted(k for k in zone_counts if k != '?')
    return {
        "domains": domains, "envs": envs, "zones": zones,
        "dom_counts": dom_counts, "env_counts": env_counts, "zone_counts": zone_counts,
        "dom_active": dom_active, "zone_active": zone_active,
    }


def apply_scope(db, run_id, keep_domains=None, keep_zones=None, user=None):
    """Applique le perimetre: serveurs hors domaines/zones selectionnes -> excluded.
    Serveurs dans le perimetre -> pending (si etaient excluded)."""
    by = user.get("display_name", user.get("username", "")) if user else ""

    # Recuperer toutes les entries avec leur domaine/zone
    entries = db.execute(text("""
        SELECT qe.id, qe.status, s.hostname, d.name as domaine, z.name as zone
        FROM quickwin_entries qe
        JOIN servers s ON qe.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 qe.run_id = :rid
    """), {"rid": run_id}).fetchall()

    included = 0
    excluded = 0
    for e in entries:
        in_scope = True
        if keep_domains and (e.domaine or '') not in keep_domains:
            in_scope = False
        if keep_zones and (e.zone or '') not in keep_zones:
            in_scope = False

        if in_scope and e.status == 'excluded':
            db.execute(text("UPDATE quickwin_entries SET status='pending', updated_at=now() WHERE id=:id"),
                       {"id": e.id})
            included += 1
        elif not in_scope and e.status != 'excluded':
            db.execute(text("UPDATE quickwin_entries SET status='excluded', updated_at=now() WHERE id=:id"),
                       {"id": e.id})
            excluded += 1

    scope_desc = []
    if keep_domains:
        scope_desc.append(f"domaines: {', '.join(keep_domains)}")
    if keep_zones:
        scope_desc.append(f"zones: {', '.join(keep_zones)}")
    log_info(db, run_id, "scope",
             f"Perimetre applique: {included} inclus, {excluded} exclus",
             detail=" | ".join(scope_desc) if scope_desc else "Tous",
             created_by=by)
    db.commit()
    return included, excluded


def update_entry_status(db, entry_id, status, patch_output="", packages_count=0,
                        packages="", reboot_required=False, notes=""):
    db.execute(text("""
        UPDATE quickwin_entries SET
            status = :st, patch_output = :po, patch_packages_count = :pc,
            patch_packages = :pp, reboot_required = :rb, notes = :n,
            patch_date = CASE WHEN :st IN ('patched','failed') THEN now() ELSE patch_date END,
            updated_at = now()
        WHERE id = :id
    """), {"id": entry_id, "st": status, "po": patch_output, "pc": packages_count,
           "pp": packages, "rb": reboot_required, "n": notes})
    db.commit()

    # Création auto d'une entrée patch_validation (en_attente) pour les serveurs patchés
    if status == "patched":
        row = db.execute(text("SELECT server_id, run_id FROM quickwin_entries WHERE id=:id"),
                         {"id": entry_id}).fetchone()
        if row:
            # Éviter les doublons (même run + même server dans la dernière heure)
            existing = db.execute(text("""SELECT id FROM patch_validation
                WHERE server_id=:sid AND campaign_id=:cid AND campaign_type='quickwin'
                  AND patch_date >= NOW() - INTERVAL '1 hour'"""),
                {"sid": row.server_id, "cid": row.run_id}).fetchone()
            if not existing:
                db.execute(text("""INSERT INTO patch_validation (server_id, campaign_id,
                    campaign_type, patch_date, status)
                    VALUES (:sid, :cid, 'quickwin', NOW(), 'en_attente')"""),
                    {"sid": row.server_id, "cid": row.run_id})
                db.commit()


def update_entry_field(db, entry_id, field, value):
    """Mise a jour d'un champ unique (pour inline edit)"""
    allowed = ("general_excludes", "specific_excludes", "notes", "status",
               "snap_done", "prereq_ok", "prereq_detail", "dryrun_output")
    if field not in allowed:
        return False
    db.execute(text(f"UPDATE quickwin_entries SET {field} = :val, updated_at = now() WHERE id = :id"),
               {"val": value, "id": entry_id})
    db.commit()
    return True


def can_start_prod(db, run_id):
    """Verifie que tous les hprod sont termines avant d'autoriser le prod"""
    pending = db.execute(text("""
        SELECT COUNT(*) as cnt FROM quickwin_entries
        WHERE run_id = :rid AND branch = 'hprod' AND status IN ('pending', 'in_progress')
    """), {"rid": run_id}).fetchone()
    return pending.cnt == 0


def check_prod_validations(db, run_id):
    """Vérifie que chaque prod du run a ses non-prod liés validés (via server_correspondance + patch_validation).
    Retourne (ok, blockers) où blockers = liste [{prod_hostname, nonprod_hostname, status}].
    Ignore les prods sans non-prod lié (OK par défaut)."""
    rows = db.execute(text("""
        SELECT qe.id as entry_id, ps.id as prod_id, ps.hostname as prod_host
        FROM quickwin_entries qe JOIN servers ps ON qe.server_id = ps.id
        WHERE qe.run_id = :rid AND qe.branch = 'prod' AND qe.status NOT IN ('excluded','skipped','patched')
    """), {"rid": run_id}).fetchall()

    blockers = []
    for r in rows:
        corrs = db.execute(text("""SELECT ns.hostname,
            (SELECT pv.status FROM patch_validation pv WHERE pv.server_id = ns.id
                ORDER BY pv.patch_date DESC LIMIT 1) as last_status
            FROM server_correspondance sc JOIN servers ns ON sc.nonprod_server_id = ns.id
            WHERE sc.prod_server_id = :pid"""), {"pid": r.prod_id}).fetchall()
        for c in corrs:
            if c.last_status not in ("validated_ok", "forced"):
                blockers.append({"prod_hostname": r.prod_host,
                                 "nonprod_hostname": c.hostname,
                                 "status": c.last_status or "aucun_patching"})
    return (len(blockers) == 0), blockers


def get_run_stats(db, run_id):
    return db.execute(text("""
        SELECT
            COUNT(*) FILTER (WHERE status != 'excluded') as total,
            COUNT(*) FILTER (WHERE branch = 'hprod' AND status != 'excluded') as hprod_total,
            COUNT(*) FILTER (WHERE branch = 'prod' AND status != 'excluded') as prod_total,
            COUNT(*) FILTER (WHERE status = 'patched') as patched,
            COUNT(*) FILTER (WHERE status = 'failed') as failed,
            COUNT(*) FILTER (WHERE status = 'pending') as pending,
            COUNT(*) FILTER (WHERE status = 'pending' AND branch = 'hprod') as hprod_pending,
            COUNT(*) FILTER (WHERE status = 'excluded') as excluded,
            COUNT(*) FILTER (WHERE status = 'skipped') as skipped,
            COUNT(*) FILTER (WHERE branch = 'hprod' AND status = 'patched') as hprod_patched,
            COUNT(*) FILTER (WHERE branch = 'prod' AND status = 'patched') as prod_patched,
            COUNT(*) FILTER (WHERE reboot_required AND status != 'excluded') as reboot_count
        FROM quickwin_entries WHERE run_id = :rid
    """), {"rid": run_id}).fetchone()


def advance_run_status(db, run_id, target_status, user=None):
    """Avance le statut du run vers l'etape suivante"""
    VALID_TRANSITIONS = {
        "draft": "prereq",
        "prereq": "snapshot",
        "snapshot": "patching",
        "patching": "result",
        "result": "completed",
    }
    run = db.execute(text("SELECT status FROM quickwin_runs WHERE id = :id"),
                     {"id": run_id}).fetchone()
    if not run:
        return False
    by = user.get("display_name", user.get("username", "")) if user else ""
    if target_status == "draft":
        db.execute(text("UPDATE quickwin_runs SET status = :st, updated_at = now() WHERE id = :id"),
                   {"st": target_status, "id": run_id})
        log_info(db, run_id, "workflow", f"Retour a l'etape brouillon", created_by=by)
        db.commit()
        return True
    expected = VALID_TRANSITIONS.get(run.status)
    if expected != target_status:
        log_warn(db, run_id, "workflow",
                 f"Transition refusee: {run.status} -> {target_status}", created_by=by)
        db.commit()
        return False
    db.execute(text("UPDATE quickwin_runs SET status = :st, updated_at = now() WHERE id = :id"),
               {"st": target_status, "id": run_id})
    log_info(db, run_id, "workflow", f"Passage a l'etape: {target_status}", created_by=by)
    db.commit()
    return True


def get_step_stats(db, run_id, branch=None):
    """Stats par etape pour un run (optionnel: filtre par branch)"""
    where = "run_id = :rid"
    params = {"rid": run_id}
    if branch:
        where += " AND branch = :br"
        params["br"] = branch
    return db.execute(text(f"""
        SELECT
            COUNT(*) as total,
            COUNT(*) FILTER (WHERE status NOT IN ('excluded','skipped')) as active,
            COUNT(*) FILTER (WHERE prereq_ok = true) as prereq_ok,
            COUNT(*) FILTER (WHERE prereq_ok = false) as prereq_ko,
            COUNT(*) FILTER (WHERE prereq_ok IS NULL AND status NOT IN ('excluded','skipped')) as prereq_pending,
            COUNT(*) FILTER (WHERE snap_done = true) as snap_ok,
            COUNT(*) FILTER (WHERE snap_done = false AND status NOT IN ('excluded','skipped')) as snap_pending,
            COUNT(*) FILTER (WHERE status = 'patched') as patched,
            COUNT(*) FILTER (WHERE status = 'failed') as failed,
            COUNT(*) FILTER (WHERE status = 'pending') as pending,
            COUNT(*) FILTER (WHERE reboot_required = true) as reboot_count
        FROM quickwin_entries WHERE {where}
    """), params).fetchone()


def check_prereqs(db, run_id, branch, user=None):
    """Lance les verifications prerequis pour tous les serveurs d'une branche.
    Etapes par serveur: DNS resolution, SSH, espace disque, satellite."""
    from .quickwin_prereq_service import check_server_prereqs

    entries = db.execute(text("""
        SELECT qe.id, s.hostname, s.domain_ltd, s.ssh_method,
               e.code as env_code
        FROM quickwin_entries qe
        JOIN servers s ON qe.server_id = s.id
        LEFT JOIN domain_environments de ON s.domain_env_id = de.id
        LEFT JOIN environments e ON de.environment_id = e.id
        WHERE qe.run_id = :rid AND qe.branch = :br
          AND qe.status NOT IN ('excluded','skipped')
        ORDER BY s.hostname
    """), {"rid": run_id, "br": branch}).fetchall()

    by = user.get("display_name", user.get("username", "")) if user else ""
    log_info(db, run_id, "prereq",
             f"Lancement check prerequis {branch} ({len(entries)} serveurs)", created_by=by)

    results = []
    ok_count = 0
    for e in entries:
        try:
            r = check_server_prereqs(
                hostname=e.hostname,
                db=db,
                domain_ltd=e.domain_ltd,
                env_code=e.env_code,
                ssh_method=e.ssh_method or "ssh_key",
            )
        except Exception as ex:
            r = {"dns_ok": False, "ssh_ok": False, "satellite_ok": False,
                 "disk_ok": False, "detail": str(ex), "skip": True}

        prereq_ok = (r.get("dns_ok", False) and r.get("ssh_ok", False)
                     and r.get("satellite_ok", False) and r.get("disk_ok", True))
        db.execute(text("""
            UPDATE quickwin_entries SET
                prereq_ok = :ok, prereq_ssh = :ssh, prereq_satellite = :sat, prereq_disk = :disk,
                prereq_detail = :detail, prereq_date = now(), updated_at = now()
            WHERE id = :id
        """), {
            "id": e.id, "ok": prereq_ok,
            "ssh": r.get("ssh_ok", False), "sat": r.get("satellite_ok", False),
            "disk": r.get("disk_ok", True), "detail": r.get("detail", ""),
        })
        # Log par serveur
        detail_str = r.get("detail", "")
        if prereq_ok:
            ok_count += 1
            log_success(db, run_id, "prereq", f"OK: {e.hostname}",
                        detail=detail_str, entry_id=e.id, hostname=e.hostname)
        else:
            log_error(db, run_id, "prereq", f"KO: {e.hostname}",
                      detail=detail_str, entry_id=e.id, hostname=e.hostname)
        results.append({"hostname": e.hostname, "ok": prereq_ok, "detail": detail_str})

    ko_count = len(results) - ok_count
    log_info(db, run_id, "prereq",
             f"Fin check {branch}: {ok_count} OK, {ko_count} KO sur {len(results)}", created_by=by)
    db.commit()
    return results


def mark_snapshot(db, entry_id, done=True):
    """Marque un serveur comme snapshot fait/pas fait"""
    db.execute(text("""
        UPDATE quickwin_entries SET snap_done = :done,
        snap_date = CASE WHEN :done THEN now() ELSE NULL END,
        updated_at = now() WHERE id = :id
    """), {"id": entry_id, "done": done})
    db.commit()


def mark_all_snapshots(db, run_id, branch, done=True):
    """Marque tous les serveurs d'une branche comme snapshot fait"""
    db.execute(text("""
        UPDATE quickwin_entries SET snap_done = :done,
        snap_date = CASE WHEN :done THEN now() ELSE NULL END,
        updated_at = now()
        WHERE run_id = :rid AND branch = :br AND status NOT IN ('excluded','skipped')
    """), {"rid": run_id, "br": branch, "done": done})
    db.commit()


def build_yum_commands(db, run_id, branch):
    """Construit les commandes yum pour chaque serveur d'une branche.
    Inclut tous les serveurs actifs (non excluded/skipped) avec snap fait."""
    entries = db.execute(text("""
        SELECT qe.id, s.hostname, qe.general_excludes, qe.specific_excludes
        FROM quickwin_entries qe
        JOIN servers s ON qe.server_id = s.id
        WHERE qe.run_id = :rid AND qe.branch = :br
          AND qe.status NOT IN ('excluded','skipped')
          AND qe.snap_done = true
        ORDER BY s.hostname
    """), {"rid": run_id, "br": branch}).fetchall()

    commands = []
    for e in entries:
        excludes = (e.general_excludes or "") + " " + (e.specific_excludes or "")
        parts = [p.strip() for p in excludes.split() if p.strip()]
        exclude_args = " ".join(f"--exclude={p}" for p in parts)
        cmd = f"yum update -y {exclude_args}".strip()
        db.execute(text("UPDATE quickwin_entries SET patch_command = :cmd WHERE id = :id"),
                   {"cmd": cmd, "id": e.id})
        commands.append({"id": e.id, "hostname": e.hostname, "command": cmd})
    db.commit()
    return commands


def inject_yum_history(db, data):
    """Injecte l'historique yum dans quickwin_server_config.
    data = [{"server": "hostname", "yum_commands": [...]}]"""
    updated = 0
    inserted = 0
    for item in data:
        hostname = item.get("server", item.get("server_name", "")).strip()
        if not hostname:
            continue
        srv = db.execute(text("SELECT id FROM servers WHERE hostname = :h"), {"h": hostname}).fetchone()
        if not srv:
            continue
        cmds = json.dumps(item.get("yum_commands", item.get("last_yum_commands", [])), ensure_ascii=False)
        existing = db.execute(text(
            "SELECT id FROM quickwin_server_config WHERE server_id = :sid"
        ), {"sid": srv.id}).fetchone()
        if existing:
            db.execute(text("""
                UPDATE quickwin_server_config SET last_yum_commands = :cmds::jsonb, updated_at = now()
                WHERE server_id = :sid
            """), {"sid": srv.id, "cmds": cmds})
            updated += 1
        else:
            db.execute(text("""
                INSERT INTO quickwin_server_config (server_id, last_yum_commands)
                VALUES (:sid, :cmds::jsonb)
            """), {"sid": srv.id, "cmds": cmds})
            inserted += 1
    db.commit()
    return updated, inserted


# Correspondance HPROD ↔ PROD : logique déplacée vers server_correspondance (global)
# Les fonctions obsolètes ont été supprimées : compute_correspondance, get_correspondance,
# get_available_prod_entries, set_prod_pair, clear_all_pairs.
# La colonne prod_pair_entry_id de quickwin_entries est laissée en place pour compatibilité
# mais n'est plus utilisée. Les liens sont désormais dans server_correspondance.
