"""Router QuickWin — Campagnes patching rapide avec exclusions par serveur"""
import json
from datetime import datetime
from sqlalchemy import text
from fastapi import APIRouter, Request, Depends, Query, Form
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit, base_context
from ..services.quickwin_service import (
    get_server_configs, upsert_server_config, delete_server_config,
    get_eligible_servers, list_runs, get_run, get_run_entries,
    create_run, delete_run, update_entry_field,
    can_start_prod, check_prod_validations, get_run_stats, inject_yum_history,
    advance_run_status, get_step_stats, mark_snapshot, mark_all_snapshots,
    build_yum_commands, get_available_servers, get_available_filters,
    add_entries_to_run, remove_entries_from_run,
    get_campaign_scope, apply_scope,
    DEFAULT_GENERAL_EXCLUDES,
)
from ..services.quickwin_log_service import get_logs, get_log_stats, clear_logs
from ..config import APP_NAME

router = APIRouter()
templates = Jinja2Templates(directory="app/templates")


@router.get("/quickwin", response_class=HTMLResponse)
async def quickwin_page(request: Request, db=Depends(get_db)):
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_view(perms, "campaigns") and not can_view(perms, "quickwin"):
        return RedirectResponse(url="/dashboard")

    runs = list_runs(db)
    configs = get_server_configs(db)
    now = datetime.now()

    ctx = base_context(request, db, user)
    ctx.update({
        "app_name": APP_NAME,
        "runs": runs,
        "configs": configs,
        "config_count": len(configs),
        "current_week": now.isocalendar()[1],
        "current_year": now.isocalendar()[0],
        "can_create": can_edit(perms, "campaigns"),
        "msg": request.query_params.get("msg"),
    })
    return templates.TemplateResponse("quickwin.html", ctx)


# -- Config exclusions par serveur --

DEFAULT_REBOOT_PACKAGES = (
    "kernel* glibc* systemd* dbus* polkit* linux-firmware* microcode_ctl* "
    "tuned* dracut* grub2* kexec-tools* libselinux* selinux-policy* shim* "
    "mokutil* net-snmp* NetworkManager* network-scripts* nss* openssl-libs*"
)


@router.get("/quickwin/config", response_class=HTMLResponse)
async def quickwin_config_page(request: Request, db=Depends(get_db)):
    """Page d'édition de la liste globale des packages qui nécessitent un reboot.
    Cette liste est utilisée par QuickWin (en plus des exclusions iTop par serveur)."""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
        return RedirectResponse(url="/dashboard")

    from ..services.secrets_service import get_secret
    current = get_secret(db, "patching_reboot_packages") or DEFAULT_REBOOT_PACKAGES

    ctx = base_context(request, db, user)
    ctx.update({
        "app_name": APP_NAME,
        "reboot_packages": current,
        "default_packages": DEFAULT_REBOOT_PACKAGES,
        "msg": request.query_params.get("msg"),
    })
    return templates.TemplateResponse("quickwin_config.html", ctx)


@router.post("/quickwin/config/save")
async def quickwin_config_save(request: Request, db=Depends(get_db),
                                reboot_packages: str = Form("")):
    """Sauvegarde la liste globale des packages nécessitant un reboot."""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
        return RedirectResponse(url="/dashboard")
    from ..services.secrets_service import set_secret
    set_secret(db, "patching_reboot_packages", reboot_packages.strip(),
               "Packages nécessitant un reboot (QuickWin)")
    return RedirectResponse(url="/quickwin/config?msg=saved", status_code=303)


# -- Runs QuickWin --

@router.post("/quickwin/create")
async def quickwin_create(request: Request, db=Depends(get_db),
                           label: str = Form(""),
                           week_number: int = Form(0),
                           year: int = Form(0),
                           server_ids: str = Form(""),
                           notes: str = Form("")):
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns"):
        return RedirectResponse(url="/quickwin")

    if not label:
        label = f"Quick Win S{week_number:02d} {year}"

    ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()]
    if not ids:
        # Prendre tous les serveurs eligibles (linux, production, secops)
        eligible = get_eligible_servers(db)
        ids = [s.id for s in eligible]

    if not ids:
        return RedirectResponse(url="/quickwin?msg=no_servers", status_code=303)

    try:
        run_id = create_run(db, year, week_number, label, user.get("uid"), ids, notes)
        return RedirectResponse(url=f"/quickwin/{run_id}", status_code=303)
    except Exception as e:
        db.rollback()
        return RedirectResponse(url=f"/quickwin?msg=error", status_code=303)


@router.get("/quickwin/correspondance", response_class=HTMLResponse)
async def quickwin_correspondance_redirect(request: Request, db=Depends(get_db)):
    """Redirige vers la nouvelle correspondance globale."""
    return RedirectResponse(url="/patching/correspondance", status_code=303)


@router.get("/quickwin/{run_id}", response_class=HTMLResponse)
async def quickwin_detail(request: Request, run_id: int, db=Depends(get_db),
                          search: str = Query(""),
                          status: str = Query(""),
                          domain: str = Query(""),
                          prereq_filter: str = Query(""),
                          snap_filter: str = Query(""),
                          hp_page: int = Query(1),
                          p_page: int = Query(1),
                          per_page: int = Query(14),
                          add_search: str = Query(""),
                          add_domains: str = Query(""),
                          add_envs: str = Query(""),
                          add_zones: str = Query(""),
                          show_add: int = Query(0)):
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")

    run = get_run(db, run_id)
    if not run:
        return RedirectResponse(url="/quickwin")

    entries = get_run_entries(db, run_id)
    stats = get_run_stats(db, run_id)
    prod_ok = can_start_prod(db, run_id)
    validations_ok, validations_blockers = check_prod_validations(db, run_id)
    step_stats_hp = get_step_stats(db, run_id, "hprod")
    step_stats_pr = get_step_stats(db, run_id, "prod")

    hprod_all = [e for e in entries if e.branch == "hprod" and e.status != "excluded"]
    prod_all = [e for e in entries if e.branch == "prod" and e.status != "excluded"]

    # Filtres
    def apply_filters(lst):
        filtered = lst
        if search:
            filtered = [e for e in filtered if search.lower() in e.hostname.lower()]
        if status:
            filtered = [e for e in filtered if e.status == status]
        if domain:
            filtered = [e for e in filtered if e.domaine == domain]
        if prereq_filter == "ok":
            filtered = [e for e in filtered if e.prereq_ok is True]
        elif prereq_filter == "ko":
            filtered = [e for e in filtered if e.prereq_ok is False]
        elif prereq_filter == "pending":
            filtered = [e for e in filtered if e.prereq_ok is None]
        if snap_filter == "ok":
            filtered = [e for e in filtered if e.snap_done is True]
        elif snap_filter == "pending":
            filtered = [e for e in filtered if not e.snap_done]
        return filtered

    hprod = apply_filters(hprod_all)
    prod = apply_filters(prod_all)

    # Pagination
    per_page = max(5, min(per_page, 100))

    hp_total = len(hprod)
    hp_total_pages = max(1, (hp_total + per_page - 1) // per_page)
    hp_page = max(1, min(hp_page, hp_total_pages))
    hp_start = (hp_page - 1) * per_page
    hprod_page = hprod[hp_start:hp_start + per_page]

    p_total = len(prod)
    p_total_pages = max(1, (p_total + per_page - 1) // per_page)
    p_page = max(1, min(p_page, p_total_pages))
    p_start = (p_page - 1) * per_page
    prod_page = prod[p_start:p_start + per_page]

    # Campaign scope (domains/zones in this run)
    scope = get_campaign_scope(db, run_id) if run.status in ("draft", "prereq") else {}

    ctx = base_context(request, db, user)
    ctx.update({
        "app_name": APP_NAME,
        "run": run, "entries": entries, "stats": stats,
        "hprod": hprod_page, "prod": prod_page,
        "hprod_total": hp_total, "prod_total": p_total,
        "hp_page": hp_page, "hp_total_pages": hp_total_pages,
        "p_page": p_page, "p_total_pages": p_total_pages,
        "per_page": per_page,
        "prod_ok": prod_ok,
        "validations_ok": validations_ok, "validations_blockers": validations_blockers,
        "step_hp": step_stats_hp, "step_pr": step_stats_pr,
        "scope": scope,
        "filters": {"search": search, "status": status, "domain": domain,
                    "prereq": prereq_filter, "snap": snap_filter},
        "msg": request.query_params.get("msg"),
    })
    return templates.TemplateResponse("quickwin_detail.html", ctx)


@router.post("/quickwin/{run_id}/delete")
async def quickwin_delete(request: Request, run_id: int, db=Depends(get_db)):
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns"):
        return RedirectResponse(url="/quickwin")
    delete_run(db, run_id)
    return RedirectResponse(url="/quickwin?msg=deleted", status_code=303)


@router.post("/quickwin/{run_id}/apply-scope")
async def quickwin_apply_scope(request: Request, run_id: int, db=Depends(get_db),
                                scope_domains: str = Form(""), scope_zones: str = Form("")):
    """Applique le perimetre: domaines + zones selectionnes = inclus, reste = excluded"""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
        return RedirectResponse(url=f"/quickwin/{run_id}")
    domains = [d for d in scope_domains.split(",") if d.strip()] or None
    zones = [z for z in scope_zones.split(",") if z.strip()] or None
    included, excluded = apply_scope(db, run_id, keep_domains=domains, keep_zones=zones, user=user)
    return RedirectResponse(url=f"/quickwin/{run_id}?msg=scope_{included}in_{excluded}ex", status_code=303)


@router.post("/quickwin/{run_id}/add-servers")
async def quickwin_add_servers(request: Request, run_id: int, db=Depends(get_db),
                                server_ids: str = Form("")):
    """Ajoute des serveurs au run (depuis le panneau d'ajout)"""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
        return RedirectResponse(url=f"/quickwin/{run_id}")
    ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()]
    added = add_entries_to_run(db, run_id, ids, user=user) if ids else 0
    return RedirectResponse(url=f"/quickwin/{run_id}?msg=added_{added}", status_code=303)


@router.post("/quickwin/{run_id}/add-filtered")
async def quickwin_add_filtered(request: Request, run_id: int, db=Depends(get_db),
                                 add_domains: str = Form(""), add_envs: str = Form(""),
                                 add_zones: str = Form(""), add_search: str = Form("")):
    """Ajoute tous les serveurs matchant les filtres multi-select"""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
        return RedirectResponse(url=f"/quickwin/{run_id}")
    domains = [d for d in add_domains.split(",") if d.strip()] or None
    envs = [e for e in add_envs.split(",") if e.strip()] or None
    zones = [z for z in add_zones.split(",") if z.strip()] or None
    servers = get_available_servers(db, run_id, search=add_search, domains=domains, envs=envs, zones=zones)
    ids = [s.id for s in servers]
    added = add_entries_to_run(db, run_id, ids, user=user) if ids else 0
    return RedirectResponse(url=f"/quickwin/{run_id}?msg=added_{added}", status_code=303)


@router.post("/quickwin/{run_id}/remove-entries")
async def quickwin_remove_entries(request: Request, run_id: int, db=Depends(get_db),
                                   entry_ids: str = Form("")):
    """Supprime des entries du run"""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
        return RedirectResponse(url=f"/quickwin/{run_id}")
    ids = [int(x) for x in entry_ids.split(",") if x.strip().isdigit()]
    removed = remove_entries_from_run(db, run_id, ids, user=user) if ids else 0
    return RedirectResponse(url=f"/quickwin/{run_id}?msg=removed_{removed}", status_code=303)


# -- Workflow steps --

STEPS = ["draft", "prereq", "snapshot", "patching", "result", "completed"]

@router.post("/quickwin/{run_id}/advance")
async def quickwin_advance(request: Request, run_id: int, db=Depends(get_db),
                            target: str = Form("")):
    """Avance le run vers l'etape suivante"""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
        return RedirectResponse(url=f"/quickwin/{run_id}")
    if target in STEPS:
        advance_run_status(db, run_id, target, user=user)
    return RedirectResponse(url=f"/quickwin/{run_id}?msg=step_{target}", status_code=303)


@router.get("/quickwin/{run_id}/prereq-stream")
async def quickwin_prereq_stream(request: Request, run_id: int, db=Depends(get_db),
                                  branch: str = Query("hprod")):
    """SSE: streame les resultats prereq serveur par serveur"""
    user = get_current_user(request)
    if not user:
        return StreamingResponse(iter([]), media_type="text/event-stream")

    def event_generator():
        from ..services.quickwin_prereq_service import check_server_prereqs
        from ..services.quickwin_log_service import log_info, log_success, log_error
        by = user.get("display_name", user.get("username", "")) if user else ""

        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()

        total = len(entries)
        log_info(db, run_id, "prereq",
                 f"Lancement check prerequis {branch} ({total} serveurs)", created_by=by)
        db.commit()

        yield f"data: {json.dumps({'type':'start','total':total,'branch':branch})}\n\n"

        ok_count = 0
        ko_count = 0
        for idx, e in enumerate(entries):
            yield f"data: {json.dumps({'type':'progress','idx':idx+1,'total':total,'hostname':e.hostname,'status':'checking'})}\n\n"
            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", ""),
            })

            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:
                ko_count += 1
                log_error(db, run_id, "prereq", f"KO: {e.hostname}",
                          detail=detail_str, entry_id=e.id, hostname=e.hostname)
            db.commit()

            result_data = {
                'type': 'result', 'idx': idx+1, 'total': total,
                'hostname': e.hostname, 'ok': prereq_ok,
                'dns': r.get('dns_ok', False), 'ssh': r.get('ssh_ok', False),
                'sat': r.get('satellite_ok', False), 'disk': r.get('disk_ok', True),
                'fqdn': r.get('fqdn', ''), 'detail': detail_str[:200],
            }
            yield f"data: {json.dumps(result_data)}\n\n"

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

        yield f"data: {json.dumps({'type':'done','ok':ok_count,'ko':ko_count,'total':total})}\n\n"

    return StreamingResponse(event_generator(), media_type="text/event-stream")


@router.post("/quickwin/{run_id}/snapshot/mark")
async def quickwin_snapshot_mark(request: Request, run_id: int, db=Depends(get_db),
                                  entry_id: int = Form(0), done: str = Form("true")):
    """Marque un serveur comme snapshot fait/pas fait"""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    if entry_id:
        mark_snapshot(db, entry_id, done == "true")
    return RedirectResponse(url=f"/quickwin/{run_id}?msg=snap_marked", status_code=303)


@router.post("/quickwin/{run_id}/snapshot/mark-all")
async def quickwin_snapshot_mark_all(request: Request, run_id: int, db=Depends(get_db),
                                      branch: str = Form("hprod")):
    """Marque tous les serveurs d'une branche comme snapshot fait"""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
        return RedirectResponse(url=f"/quickwin/{run_id}")
    mark_all_snapshots(db, run_id, branch, True)
    return RedirectResponse(url=f"/quickwin/{run_id}?msg=snap_all", status_code=303)


@router.get("/quickwin/{run_id}/snapshot-stream")
async def quickwin_snapshot_stream(request: Request, run_id: int, db=Depends(get_db),
                                    branch: str = Query("hprod")):
    """SSE: prend les snapshots VM serveur par serveur"""
    user = get_current_user(request)
    if not user:
        return StreamingResponse(iter([]), media_type="text/event-stream")

    def event_generator():
        from ..services.quickwin_snapshot_service import snapshot_server
        from ..services.quickwin_log_service import log_info, log_success, log_error, log_warn

        by = user.get("display_name", user.get("username", "")) if user else ""

        entries = db.execute(text("""
            SELECT qe.id, s.hostname, s.machine_type, s.vcenter_vm_name, qe.branch
            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')
            ORDER BY s.hostname
        """), {"rid": run_id, "br": branch}).fetchall()

        total = len(entries)
        physical_entries = [e for e in entries if e.machine_type == 'physical']
        vm_entries = [e for e in entries if e.machine_type != 'physical']
        physical_count = len(physical_entries)

        log_info(db, run_id, "snapshot",
                 f"Lancement snapshots {branch} ({len(vm_entries)} VMs, {physical_count} physiques ignores)",
                 created_by=by)
        db.commit()

        yield f"data: {json.dumps({'type':'start','total':total,'vms':len(vm_entries),'physical':physical_count,'branch':branch})}\n\n"

        # Marquer les physiques comme snap_done (pas de snap necessaire)
        for e in physical_entries:
            db.execute(text("""
                UPDATE quickwin_entries SET snap_done = true, snap_detail = 'Serveur physique - pas de snapshot VM',
                       updated_at = now() WHERE id = :id
            """), {"id": e.id})
            log_warn(db, run_id, "snapshot",
                     f"Physique ignore: {e.hostname} (verifier backup Commvault)",
                     entry_id=e.id, hostname=e.hostname, created_by=by)
        if physical_entries:
            db.commit()

        ok_count = 0
        ko_count = 0
        snap_name = f"QW_{run_id}_{branch}_{__import__('datetime').datetime.now().strftime('%Y%m%d_%H%M')}"

        for idx, e in enumerate(vm_entries):
            yield f"data: {json.dumps({'type':'progress','idx':idx+1,'total':len(vm_entries),'hostname':e.hostname,'status':'connecting'})}\n\n"

            r = snapshot_server(
                hostname=e.hostname,
                vm_name=e.vcenter_vm_name,
                branch=e.branch,
                db=db,
                snap_name=snap_name,
            )

            snap_ok = r.get("ok", False)
            detail = r.get("detail", "")
            vcenter = r.get("vcenter", "")

            db.execute(text("""
                UPDATE quickwin_entries SET
                    snap_done = :done, snap_detail = :detail, updated_at = now()
                WHERE id = :id
            """), {"id": e.id, "done": snap_ok, "detail": f"[{vcenter}] {detail}" if vcenter else detail})

            if snap_ok:
                ok_count += 1
                log_success(db, run_id, "snapshot", f"OK: {e.hostname} ({vcenter})",
                            detail=detail, entry_id=e.id, hostname=e.hostname, created_by=by)
            else:
                ko_count += 1
                log_error(db, run_id, "snapshot", f"KO: {e.hostname}",
                          detail=detail, entry_id=e.id, hostname=e.hostname, created_by=by)
            db.commit()

            result_data = {
                'type': 'result', 'idx': idx+1, 'total': len(vm_entries),
                'hostname': e.hostname, 'ok': snap_ok,
                'vcenter': vcenter, 'detail': detail[:200],
            }
            yield f"data: {json.dumps(result_data)}\n\n"

        log_info(db, run_id, "snapshot",
                 f"Fin snapshots {branch}: {ok_count} OK, {ko_count} KO sur {len(vm_entries)} VMs",
                 created_by=by)
        db.commit()

        yield f"data: {json.dumps({'type':'done','ok':ok_count,'ko':ko_count,'total':len(vm_entries),'physical':physical_count})}\n\n"

    return StreamingResponse(event_generator(), media_type="text/event-stream")


@router.post("/quickwin/{run_id}/build-commands")
async def quickwin_build_commands(request: Request, run_id: int, db=Depends(get_db),
                                   branch: str = Form("hprod")):
    """Construit les commandes yum pour une branche"""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    commands = build_yum_commands(db, run_id, branch)
    return RedirectResponse(url=f"/quickwin/{run_id}?msg=commands_{len(commands)}&show_cmds={branch}", status_code=303)


@router.get("/api/quickwin/{run_id}/commands/{branch}")
async def quickwin_get_commands(request: Request, run_id: int, branch: str, db=Depends(get_db)):
    """Retourne les commandes generees pour une branche"""
    user = get_current_user(request)
    if not user:
        return JSONResponse({"error": "unauthorized"}, 401)
    entries = db.execute(text("""
        SELECT qe.id, s.hostname, qe.patch_command, s.ssh_method, s.domain_ltd,
               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')
          AND qe.patch_command IS NOT NULL AND qe.patch_command != ''
        ORDER BY s.hostname
    """), {"rid": run_id, "br": branch}).fetchall()
    return JSONResponse([
        {"id": e.id, "hostname": e.hostname, "command": e.patch_command}
        for e in entries
    ])


@router.get("/quickwin/{run_id}/patch-stream")
async def quickwin_patch_stream(request: Request, run_id: int, db=Depends(get_db),
                                 branch: str = Query("hprod")):
    """SSE: execute les commandes yum serveur par serveur via SSH"""
    user = get_current_user(request)
    if not user:
        return StreamingResponse(iter([]), media_type="text/event-stream")

    def event_generator():
        from ..services.quickwin_prereq_service import _get_secret, _resolve_fqdn, _get_ssh_key_path, SSH_TIMEOUT, PSMP_HOST, CYBR_USER, TARGET_USER
        from ..services.quickwin_log_service import log_info, log_success, log_error
        import paramiko

        by = user.get("display_name", user.get("username", "")) if user else ""

        entries = db.execute(text("""
            SELECT qe.id, s.hostname, qe.patch_command, s.ssh_method, s.domain_ltd,
                   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')
              AND qe.patch_command IS NOT NULL AND qe.patch_command != ''
            ORDER BY s.hostname
        """), {"rid": run_id, "br": branch}).fetchall()

        total = len(entries)
        log_info(db, run_id, "patching",
                 f"Lancement patching {branch} ({total} serveurs)", created_by=by)
        db.commit()

        yield f"data: {json.dumps({'type':'start','total':total,'branch':branch})}\n\n"

        ok_count = 0
        ko_count = 0

        for idx, e in enumerate(entries):
            yield f"data: {json.dumps({'type':'progress','idx':idx+1,'total':total,'hostname':e.hostname,'command':e.patch_command,'status':'connecting'})}\n\n"

            # Resoudre FQDN
            fqdn, _dns_err = _resolve_fqdn(e.hostname, e.domain_ltd, e.env_code)
            if not fqdn:
                fqdn = e.hostname

            # Connexion SSH
            client = None
            ssh_err = None
            try:
                ssh_method = e.ssh_method or "ssh_key"
                if ssh_method == "ssh_key":
                    client = paramiko.SSHClient()
                    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                    key_path = _get_ssh_key_path(db)
                    if not key_path:
                        raise Exception("Cle SSH non configuree (ssh_key_file dans Secrets)")
                    client.connect(fqdn, username="root", key_filename=key_path,
                                   timeout=SSH_TIMEOUT, allow_agent=False, look_for_keys=False)
                else:
                    password = _get_secret(db, "ssh_pwd_default_pass")
                    transport = paramiko.Transport((PSMP_HOST, 22))
                    psmp_user = f"{CYBR_USER}@{TARGET_USER}@{fqdn}"
                    transport.connect(username=psmp_user, password=password or "")
                    transport.set_keepalive(30)
                    client = paramiko.SSHClient()
                    client._transport = transport
            except Exception as ex:
                ssh_err = str(ex)

            if ssh_err or not client:
                ko_count += 1
                detail = f"SSH echoue: {ssh_err or 'connexion impossible'}"
                db.execute(text("""
                    UPDATE quickwin_entries SET status='failed', patch_output=:out,
                           patch_date=now(), updated_at=now() WHERE id=:id
                """), {"id": e.id, "out": detail})
                log_error(db, run_id, "patching", f"KO SSH: {e.hostname}",
                          detail=detail, entry_id=e.id, hostname=e.hostname, created_by=by)
                db.commit()
                yield f"data: {json.dumps({'type':'result','idx':idx+1,'total':total,'hostname':e.hostname,'ok':False,'detail':detail[:300]})}\n\n"
                continue

            # Executer la commande
            try:
                yield f"data: {json.dumps({'type':'progress','idx':idx+1,'total':total,'hostname':e.hostname,'status':'executing','command':e.patch_command})}\n\n"

                stdin, stdout, stderr = client.exec_command(e.patch_command, timeout=600)
                output = stdout.read().decode('utf-8', errors='replace')
                err_output = stderr.read().decode('utf-8', errors='replace')
                exit_code = stdout.channel.recv_exit_status()

                full_output = output
                if err_output:
                    full_output += "\n--- STDERR ---\n" + err_output

                # Parser la sortie yum/dnf
                pkg_count = 0
                packages = ""
                summary_lines = []
                upgrade_keywords = ("Updating", "Installing", "Upgrading",
                                    "Mise à niveau", "Installation de", "Mise à jour")
                result_keywords = ("Updated:", "Installed:", "Upgraded:",
                                   "Mis à jour", "Installé", "Mis à niveau")
                skip_keywords = ("vérification", "métadonnées", "Dernière",
                                 "===", "---", "Dépendances résolues",
                                 "Paquet ", "Architecture", "Version",
                                 "Dépôt", "Taille")

                for line in output.splitlines():
                    stripped = line.strip()
                    if not stripped:
                        continue
                    if any(stripped.startswith(k) for k in result_keywords):
                        packages += stripped + "\n"
                    if any(stripped.startswith(k) for k in upgrade_keywords):
                        pkg_count += 1

                # Construire un résumé pour le terminal SSE
                for line in output.splitlines():
                    stripped = line.strip()
                    if not stripped:
                        continue
                    if any(k in stripped for k in skip_keywords):
                        continue
                    summary_lines.append(stripped)

                # Garder les lignes utiles (fin = résumé transaction)
                if len(summary_lines) > 15:
                    summary_lines = summary_lines[-15:]
                summary = "\n".join(summary_lines)

                # Rien à faire ?
                nothing = any(k in output for k in ("Rien à faire", "Nothing to do", "No packages marked"))
                if nothing and pkg_count == 0:
                    summary = "Rien à faire — tous les paquets sont à jour."

                reboot = "reboot" in output.lower() or "kernel" in output.lower()

                if exit_code == 0:
                    ok_count += 1
                    status = "patched"
                    log_success(db, run_id, "patching",
                                f"OK: {e.hostname} ({pkg_count} paquets)",
                                detail=full_output[:500], entry_id=e.id,
                                hostname=e.hostname, created_by=by)
                else:
                    ko_count += 1
                    status = "failed"
                    log_error(db, run_id, "patching",
                              f"KO: {e.hostname} (exit {exit_code})",
                              detail=full_output[:500], entry_id=e.id,
                              hostname=e.hostname, created_by=by)

                db.execute(text("""
                    UPDATE quickwin_entries SET status=:st, patch_output=:out,
                           patch_packages_count=:pc, patch_packages=:pp,
                           reboot_required=:rb, patch_date=now(), updated_at=now()
                    WHERE id=:id
                """), {"id": e.id, "st": status, "out": full_output[:5000],
                       "pc": pkg_count, "pp": packages[:2000], "rb": reboot})
                db.commit()

                yield f"data: {json.dumps({'type':'result','idx':idx+1,'total':total,'hostname':e.hostname,'ok':exit_code==0,'exit_code':exit_code,'packages':pkg_count,'reboot':reboot,'detail':summary[:1000]})}\n\n"

            except Exception as ex:
                ko_count += 1
                detail = f"Erreur execution: {ex}"
                db.execute(text("""
                    UPDATE quickwin_entries SET status='failed', patch_output=:out,
                           patch_date=now(), updated_at=now() WHERE id=:id
                """), {"id": e.id, "out": detail})
                log_error(db, run_id, "patching", f"KO: {e.hostname}",
                          detail=detail, entry_id=e.id, hostname=e.hostname, created_by=by)
                db.commit()
                yield f"data: {json.dumps({'type':'result','idx':idx+1,'total':total,'hostname':e.hostname,'ok':False,'detail':detail[:300]})}\n\n"
            finally:
                try:
                    client.close()
                except Exception:
                    pass

        log_info(db, run_id, "patching",
                 f"Fin patching {branch}: {ok_count} OK, {ko_count} KO sur {total}", created_by=by)
        db.commit()
        yield f"data: {json.dumps({'type':'done','ok':ok_count,'ko':ko_count,'total':total})}\n\n"

    return StreamingResponse(event_generator(), media_type="text/event-stream")


@router.post("/quickwin/{run_id}/mark-patched")
async def quickwin_mark_patched(request: Request, run_id: int, db=Depends(get_db),
                                 entry_id: int = Form(0),
                                 patch_status: str = Form("patched"),
                                 packages_count: int = Form(0),
                                 reboot_required: str = Form("false"),
                                 patch_output: str = Form("")):
    """Marque un serveur comme patche ou echoue"""
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    if entry_id:
        from sqlalchemy import text
        reboot = reboot_required == "true"
        db.execute(text("""
            UPDATE quickwin_entries SET status = :st, patch_packages_count = :pc,
            reboot_required = :rb, patch_output = :po, patch_date = now(), updated_at = now()
            WHERE id = :id
        """), {"id": entry_id, "st": patch_status, "pc": packages_count,
               "rb": reboot, "po": patch_output})
        db.commit()
    return RedirectResponse(url=f"/quickwin/{run_id}?msg=marked", status_code=303)


# -- Logs --

@router.get("/quickwin/{run_id}/logs", response_class=HTMLResponse)
async def quickwin_logs_page(request: Request, run_id: int, db=Depends(get_db),
                              level: str = Query(""),
                              step: str = Query(""),
                              hostname: str = Query("")):
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    run = get_run(db, run_id)
    if not run:
        return RedirectResponse(url="/quickwin")
    logs = get_logs(db, run_id, level=level or None, step=step or None,
                    hostname=hostname or None)
    log_stats = get_log_stats(db, run_id)
    stats_dict = {r.level: r.cnt for r in log_stats}
    ctx = base_context(request, db, user)
    ctx.update({
        "app_name": APP_NAME, "run": run, "logs": logs,
        "log_stats": stats_dict,
        "total_logs": sum(stats_dict.values()),
        "filters": {"level": level, "step": step, "hostname": hostname},
        "msg": request.query_params.get("msg"),
    })
    return templates.TemplateResponse("quickwin_logs.html", ctx)


@router.post("/quickwin/{run_id}/logs/clear")
async def quickwin_logs_clear(request: Request, run_id: int, db=Depends(get_db),
                               clear_step: str = Form("")):
    user = get_current_user(request)
    if not user:
        return RedirectResponse(url="/login")
    perms = get_user_perms(db, user)
    if not can_edit(perms, "campaigns") and not can_edit(perms, "quickwin"):
        return RedirectResponse(url=f"/quickwin/{run_id}/logs")
    clear_logs(db, run_id, step=clear_step or None)
    return RedirectResponse(url=f"/quickwin/{run_id}/logs?msg=cleared", status_code=303)


# -- API JSON --

@router.post("/api/quickwin/entry/update")
async def quickwin_entry_update(request: Request, db=Depends(get_db)):
    user = get_current_user(request)
    if not user:
        return JSONResponse({"error": "unauthorized"}, 401)
    body = await request.json()
    entry_id = body.get("id")
    field = body.get("field")
    value = body.get("value")
    if not entry_id or not field:
        return JSONResponse({"error": "id and field required"}, 400)
    ok = update_entry_field(db, entry_id, field, value)
    return JSONResponse({"ok": ok})


@router.post("/api/quickwin/inject-yum-history")
async def quickwin_inject_yum(request: Request, db=Depends(get_db)):
    user = get_current_user(request)
    if not user:
        return JSONResponse({"error": "unauthorized"}, 401)
    body = await request.json()
    if not isinstance(body, list):
        return JSONResponse({"error": "expected list"}, 400)
    updated, inserted = inject_yum_history(db, body)
    return JSONResponse({"ok": True, "updated": updated, "inserted": inserted})


@router.get("/api/quickwin/prod-check/{run_id}")
async def quickwin_prod_check(request: Request, run_id: int, db=Depends(get_db)):
    """Verifie si le prod peut demarrer (tous hprod termines)"""
    user = get_current_user(request)
    if not user:
        return JSONResponse({"error": "unauthorized"}, 401)
    ok = can_start_prod(db, run_id)
    return JSONResponse({"can_start_prod": ok})


# Correspondance par-run supprimée — utiliser /patching/correspondance (global)
@router.get("/quickwin/{run_id}/correspondance")
async def quickwin_correspondance_deprecated(request: Request, run_id: int, db=Depends(get_db)):
    return RedirectResponse(url="/patching/correspondance", status_code=303)
