"""Service de correspondance prod ↔ hors-prod + validations post-patching.

Détection automatique par signature de hostname :
  - 2ème caractère = environnement SANEF (p=prod, r=recette, t=test, i=preprod, v=validation, d=dev, o=preprod, s=prod)
  - Signature = hostname avec le 2ème char remplacé par "_"
  - Tous les hostnames avec la même signature sont candidats correspondants.

Exceptions (ls-*, sp-*, etc.) : ne sont pas traitées automatiquement.
"""
import logging
from sqlalchemy import text
from collections import defaultdict

log = logging.getLogger(__name__)

# Lettres prod (un prod pour une signature)
PROD_CHARS = {"p", "s"}  # p=Production, s=Production secours (à valider)

# Lettres hors-prod avec label
NONPROD_CHARS = {
    "r": "Recette", "t": "Test", "i": "Pre-production",
    "v": "Validation", "d": "Developpement", "o": "Pre-production",
}

# Préfixes qui ne suivent PAS la nomenclature
EXCEPTION_PREFIXES = ("ls-", "sp")


def _signature(hostname):
    """Retourne (signature, env_char) ou (None, None) si non analysable."""
    hn = (hostname or "").lower().strip()
    if not hn or len(hn) < 3:
        return None, None
    # Préfixes d'exception
    for pref in EXCEPTION_PREFIXES:
        if hn.startswith(pref):
            return None, None
    # Format standard : X{env_char}YYYYYY
    env_char = hn[1]
    if env_char not in PROD_CHARS and env_char not in NONPROD_CHARS:
        return None, None
    signature = hn[0] + "_" + hn[2:]
    return signature, env_char


def detect_correspondances(db, dry_run=False):
    """Parcourt tous les serveurs, groupe par signature, crée les liens auto.
    Ne touche pas aux liens 'manual' ou 'exception' existants.
    Retourne un dict de stats."""
    stats = {"signatures": 0, "prod_found": 0, "nonprod_found": 0,
             "links_created": 0, "links_kept_manual": 0, "orphan_nonprod": 0,
             "ambiguous": 0, "exceptions": 0}

    # Tous les serveurs actifs (exclut stock/obsolete)
    rows = db.execute(text("""SELECT id, hostname FROM servers
        WHERE etat NOT IN ('Stock','Obsolète','EOL') ORDER BY hostname""")).fetchall()

    by_signature = defaultdict(list)  # signature -> [(server_id, env_char, hostname)]
    for r in rows:
        sig, env = _signature(r.hostname)
        if sig is None:
            stats["exceptions"] += 1
            continue
        by_signature[sig].append((r.id, env, r.hostname))

    stats["signatures"] = len(by_signature)

    if dry_run:
        # Préparer plan
        plan = []
        for sig, members in by_signature.items():
            prods = [m for m in members if m[1] in PROD_CHARS]
            nps = [m for m in members if m[1] in NONPROD_CHARS]
            if len(prods) == 1 and nps:
                plan.append({"signature": sig, "prod": prods[0][2],
                             "nonprods": [(n[2], NONPROD_CHARS[n[1]]) for n in nps]})
            elif len(prods) > 1 and nps:
                stats["ambiguous"] += 1
            elif not prods and nps:
                stats["orphan_nonprod"] += len(nps)
        stats["plan"] = plan[:50]
        return stats

    for sig, members in by_signature.items():
        prods = [m for m in members if m[1] in PROD_CHARS]
        nonprods = [m for m in members if m[1] in NONPROD_CHARS]
        stats["prod_found"] += len(prods)
        stats["nonprod_found"] += len(nonprods)

        if not prods and nonprods:
            stats["orphan_nonprod"] += len(nonprods)
            continue

        if len(prods) > 1:
            stats["ambiguous"] += 1
            # On n'auto-détecte pas quand plusieurs prods (ambigu)
            continue

        if len(prods) == 1 and nonprods:
            prod_id = prods[0][0]
            for np_id, np_env, np_host in nonprods:
                env_label = NONPROD_CHARS.get(np_env, "Autre")
                # Insert si pas déjà présent + pas 'manual' ou 'exception'
                existing = db.execute(text("""SELECT id, source FROM server_correspondance
                    WHERE prod_server_id=:p AND nonprod_server_id=:n"""),
                    {"p": prod_id, "n": np_id}).fetchone()
                if existing:
                    if existing.source in ("manual", "exception"):
                        stats["links_kept_manual"] += 1
                    # sinon déjà auto, on skip
                    continue
                try:
                    db.execute(text("""INSERT INTO server_correspondance
                        (prod_server_id, nonprod_server_id, environment_code, source)
                        VALUES (:p, :n, :env, 'auto')"""),
                        {"p": prod_id, "n": np_id, "env": env_label})
                    stats["links_created"] += 1
                except Exception as e:
                    db.rollback()

    db.commit()
    return stats


def get_servers_for_builder(db, search="", app="", domain="", env=""):
    """Retourne tous les serveurs matchant les filtres, avec leurs correspondances existantes.
    Exclut les serveurs en stock / obsolete (décommissionnés, EOL)."""
    where = ["s.etat NOT IN ('Stock','Obsolète','EOL')"]
    params = {}
    if search:
        where.append("s.hostname ILIKE :s"); params["s"] = f"%{search}%"
    if app:
        where.append("s.application_name = :app"); params["app"] = app
    if domain:
        where.append("d.name = :dom"); params["dom"] = domain
    if env:
        where.append("e.name = :env"); params["env"] = env
    wc = " AND ".join(where)

    return db.execute(text(f"""
        SELECT s.id, s.hostname, s.application_name,
               e.name as env_name, d.name as domain_name, z.name as zone_name,
               (SELECT COUNT(*) FROM server_correspondance sc WHERE sc.prod_server_id = s.id) as n_as_prod,
               (SELECT COUNT(*) FROM server_correspondance sc WHERE sc.nonprod_server_id = s.id) as n_as_nonprod
        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
        LEFT JOIN domains d ON de.domain_id = d.id
        LEFT JOIN zones z ON s.zone_id = z.id
        WHERE {wc}
        ORDER BY e.name, s.hostname
        LIMIT 500
    """), params).fetchall()


def bulk_create_correspondance(db, prod_ids, nonprod_ids, env_labels, user_id):
    """Crée toutes les correspondances prod × non-prod.
    env_labels est un dict {nonprod_id: env_label} optionnel."""
    created = 0
    skipped = 0
    for pid in prod_ids:
        for npid in nonprod_ids:
            if pid == npid:
                continue
            existing = db.execute(text("""SELECT id FROM server_correspondance
                WHERE prod_server_id=:p AND nonprod_server_id=:n"""),
                {"p": pid, "n": npid}).fetchone()
            if existing:
                skipped += 1
                continue
            env = (env_labels or {}).get(str(npid)) or (env_labels or {}).get(npid) or ""
            db.execute(text("""INSERT INTO server_correspondance
                (prod_server_id, nonprod_server_id, environment_code, source, created_by)
                VALUES (:p, :n, :env, 'manual', :uid)"""),
                {"p": pid, "n": npid, "env": env, "uid": user_id})
            created += 1
    db.commit()
    return {"created": created, "skipped": skipped}


def get_correspondance_view(db, search="", app="", env=""):
    """Vue hiérarchique des correspondances groupées par application.
    Exclut les serveurs en stock/obsolete."""
    where = ["s.etat NOT IN ('Stock','Obsolète','EOL')"]
    params = {}
    if search:
        where.append("s.hostname ILIKE :s"); params["s"] = f"%{search}%"
    if app:
        where.append("s.application_name = :app"); params["app"] = app
    if env:
        where.append("e.name = :env"); params["env"] = env
    else:
        # Par défaut : tout ce qui ressemble à prod (Production ou code prod)
        where.append("(e.name ILIKE '%production%' OR e.code ILIKE '%prod%')")

    wc = " AND ".join(where)

    prods = db.execute(text(f"""
        SELECT s.id, s.hostname, s.application_name, e.name as env_name,
               d.name as domain_name
        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
        LEFT JOIN domains d ON de.domain_id = d.id
        WHERE {wc}
        ORDER BY s.application_name, s.hostname
    """), params).fetchall()

    results = []
    for p in prods:
        corrs = db.execute(text("""SELECT sc.id as corr_id, sc.environment_code, sc.source, sc.note,
            ns.id as np_id, ns.hostname as np_hostname,
            (SELECT pv.status FROM patch_validation pv WHERE pv.server_id = ns.id
                ORDER BY pv.patch_date DESC LIMIT 1) as last_validation_status,
            (SELECT pv.validated_at FROM patch_validation pv WHERE pv.server_id = ns.id
                ORDER BY pv.patch_date DESC LIMIT 1) as last_validated_at,
            (SELECT pv.patch_date FROM patch_validation pv WHERE pv.server_id = ns.id
                ORDER BY pv.patch_date DESC LIMIT 1) as last_patch_date
            FROM server_correspondance sc
            JOIN servers ns ON sc.nonprod_server_id = ns.id
            WHERE sc.prod_server_id = :pid
            ORDER BY sc.environment_code, ns.hostname"""), {"pid": p.id}).fetchall()

        # Validation status agrégé du prod
        # Compter statuts des hors-prod liés
        n_total = len(corrs)
        n_ok = sum(1 for c in corrs if c.last_validation_status in ("validated_ok", "forced"))
        n_pending = sum(1 for c in corrs if c.last_validation_status == "en_attente")
        n_ko = sum(1 for c in corrs if c.last_validation_status == "validated_ko")

        if n_total == 0:
            global_status = "no_nonprod"  # gris
        elif n_ko > 0:
            global_status = "ko"
        elif n_pending > 0:
            global_status = "pending"
        elif n_ok == n_total:
            global_status = "all_ok"
        else:
            global_status = "partial"

        results.append({
            "prod_id": p.id, "prod_hostname": p.hostname,
            "application": p.application_name, "domain": p.domain_name,
            "env": p.env_name,
            "correspondants": [dict(c._mapping) for c in corrs],
            "n_total": n_total, "n_ok": n_ok, "n_pending": n_pending, "n_ko": n_ko,
            "global_status": global_status,
        })

    return results


def get_server_links(db, server_id):
    """Pour un serveur donné, retourne ses liens :
    - as_prod : liste des hors-prod qui lui sont liés (si ce serveur est prod)
    - as_nonprod : liste des prod auxquels il est lié (si ce serveur est non-prod)
    Chaque item : {hostname, env_name, environment_code, source, corr_id}
    """
    as_prod = db.execute(text("""SELECT sc.id as corr_id, sc.environment_code, sc.source,
        ns.id, ns.hostname, e.name as env_name
        FROM server_correspondance sc
        JOIN servers ns ON sc.nonprod_server_id = ns.id
        LEFT JOIN domain_environments de ON ns.domain_env_id = de.id
        LEFT JOIN environments e ON de.environment_id = e.id
        WHERE sc.prod_server_id = :id ORDER BY e.name, ns.hostname"""),
        {"id": server_id}).fetchall()

    as_nonprod = db.execute(text("""SELECT sc.id as corr_id, sc.environment_code, sc.source,
        ps.id, ps.hostname, e.name as env_name
        FROM server_correspondance sc
        JOIN servers ps ON sc.prod_server_id = ps.id
        LEFT JOIN domain_environments de ON ps.domain_env_id = de.id
        LEFT JOIN environments e ON de.environment_id = e.id
        WHERE sc.nonprod_server_id = :id ORDER BY ps.hostname"""),
        {"id": server_id}).fetchall()

    return {
        "as_prod": [dict(r._mapping) for r in as_prod],
        "as_nonprod": [dict(r._mapping) for r in as_nonprod],
    }


def get_links_bulk(db, server_ids):
    """Pour une liste d'IDs, retourne un dict {server_id: {as_prod: [...], as_nonprod: [...]}}.
    Optimisé pour affichage en liste (/servers)."""
    if not server_ids:
        return {}
    placeholders = ",".join(str(i) for i in server_ids if str(i).isdigit())
    if not placeholders:
        return {}
    result = {sid: {"as_prod": [], "as_nonprod": []} for sid in server_ids}

    # Prod → non-prods
    rows = db.execute(text(f"""SELECT sc.prod_server_id as sid, sc.environment_code,
        ns.hostname, e.name as env_name
        FROM server_correspondance sc
        JOIN servers ns ON sc.nonprod_server_id = ns.id
        LEFT JOIN domain_environments de ON ns.domain_env_id = de.id
        LEFT JOIN environments e ON de.environment_id = e.id
        WHERE sc.prod_server_id IN ({placeholders})
        ORDER BY ns.hostname""")).fetchall()
    for r in rows:
        if r.sid in result:
            result[r.sid]["as_prod"].append({"hostname": r.hostname,
                "env_name": r.env_name, "environment_code": r.environment_code})

    # Non-prod → prods
    rows = db.execute(text(f"""SELECT sc.nonprod_server_id as sid,
        ps.hostname, e.name as env_name
        FROM server_correspondance sc
        JOIN servers ps ON sc.prod_server_id = ps.id
        LEFT JOIN domain_environments de ON ps.domain_env_id = de.id
        LEFT JOIN environments e ON de.environment_id = e.id
        WHERE sc.nonprod_server_id IN ({placeholders})
        ORDER BY ps.hostname""")).fetchall()
    for r in rows:
        if r.sid in result:
            result[r.sid]["as_nonprod"].append({"hostname": r.hostname, "env_name": r.env_name})
    return result


def get_orphan_nonprod(db):
    """Retourne les hors-prod sans prod associée (exclut stock/obsolete)."""
    rows = db.execute(text("""
        SELECT s.id, s.hostname, s.application_name, e.name as env_name,
               d.name as domain_name
        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
        LEFT JOIN domains d ON de.domain_id = d.id
        WHERE e.name IS NOT NULL AND e.name NOT ILIKE '%production%'
          AND s.etat NOT IN ('Stock','Obsolète','EOL')
          AND NOT EXISTS (SELECT 1 FROM server_correspondance sc WHERE sc.nonprod_server_id = s.id)
        ORDER BY s.application_name, s.hostname
        LIMIT 500
    """)).fetchall()
    return rows


def create_manual_link(db, prod_id, nonprod_id, env_code, note, user_id):
    """Crée un lien manuel."""
    existing = db.execute(text("""SELECT id FROM server_correspondance
        WHERE prod_server_id=:p AND nonprod_server_id=:n"""),
        {"p": prod_id, "n": nonprod_id}).fetchone()
    if existing:
        db.execute(text("""UPDATE server_correspondance SET source='manual',
            environment_code=:env, note=:note, updated_at=NOW() WHERE id=:id"""),
            {"env": env_code, "note": note, "id": existing.id})
    else:
        db.execute(text("""INSERT INTO server_correspondance (prod_server_id,
            nonprod_server_id, environment_code, source, note, created_by)
            VALUES (:p, :n, :env, 'manual', :note, :uid)"""),
            {"p": prod_id, "n": nonprod_id, "env": env_code, "note": note, "uid": user_id})
    db.commit()


def delete_link(db, corr_id):
    db.execute(text("DELETE FROM server_correspondance WHERE id=:id"), {"id": corr_id})
    db.commit()


# ─── Patch validation ───

def create_validation_entry(db, server_id, campaign_id=None, campaign_type="manual"):
    """Crée une entrée 'en_attente' après patching."""
    db.execute(text("""INSERT INTO patch_validation (server_id, campaign_id, campaign_type,
        patch_date, status) VALUES (:sid, :cid, :ct, NOW(), 'en_attente')"""),
        {"sid": server_id, "cid": campaign_id, "ct": campaign_type})
    db.commit()


def mark_validation(db, validation_ids, status, validator_contact_id, validator_name,
                    forced_reason, notes, user_id):
    """Marque N validations. status dans (validated_ok, validated_ko, forced)."""
    placeholders = ",".join(str(i) for i in validation_ids if str(i).isdigit())
    if not placeholders:
        return 0
    db.execute(text(f"""UPDATE patch_validation SET
        status=:s, validated_by_contact_id=:cid, validated_by_name=:n,
        validated_at=NOW(), marked_by_user_id=:uid,
        forced_reason=:fr, notes=:nt, updated_at=NOW()
        WHERE id IN ({placeholders})"""),
        {"s": status, "cid": validator_contact_id, "n": validator_name,
         "uid": user_id, "fr": forced_reason, "nt": notes})
    db.commit()
    return len(placeholders.split(","))


def get_pending_validations(db, env="", campaign_id=None, status="en_attente", limit=500):
    """Liste les validations filtrées."""
    where = ["1=1"]
    params = {}
    if status:
        where.append("pv.status = :st"); params["st"] = status
    if campaign_id:
        where.append("pv.campaign_id = :cid"); params["cid"] = campaign_id
    if env:
        where.append("e.name = :env"); params["env"] = env
    wc = " AND ".join(where)
    return db.execute(text(f"""
        SELECT pv.id, pv.server_id, s.hostname, s.application_name,
               e.name as env_name, d.name as domain_name,
               pv.campaign_id, pv.campaign_type, pv.patch_date, pv.status,
               pv.validated_by_name, pv.validated_at,
               pv.forced_reason, pv.notes,
               EXTRACT(day FROM NOW() - pv.patch_date) as days_pending
        FROM patch_validation pv
        JOIN servers s ON pv.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
        LEFT JOIN domains d ON de.domain_id = d.id
        WHERE {wc}
        ORDER BY pv.patch_date DESC
        LIMIT {int(limit)}
    """), params).fetchall()


def get_validation_history(db, server_id):
    return db.execute(text("""
        SELECT pv.id, pv.campaign_id, pv.campaign_type, pv.patch_date, pv.status,
               pv.validated_by_name, pv.validated_at, pv.forced_reason, pv.notes,
               u.display_name as marked_by
        FROM patch_validation pv
        LEFT JOIN users u ON pv.marked_by_user_id = u.id
        WHERE pv.server_id = :sid
        ORDER BY pv.patch_date DESC
    """), {"sid": server_id}).fetchall()


def can_patch_prod(db, prod_server_id):
    """Retourne (bool, list_of_pending_hostnames) : peut-on patcher le prod ?
    OK si tous les hors-prod liés ont validated_ok ou forced sur leur dernier patching."""
    corrs = db.execute(text("""SELECT ns.id, 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": prod_server_id}).fetchall()
    if not corrs:
        return True, []  # pas de hors-prod = OK (ou selon règle, à ajuster)
    blockers = [c.hostname for c in corrs if c.last_status not in ("validated_ok", "forced")]
    return (len(blockers) == 0), blockers
