"""W21B-w3 Commit 3 — active-only audit helper (pure, read-only).

Partitions the current ``background_render`` output into the *active*
visual set versus *stale* leftovers so canary / visual-review tooling can
focus on the checkpoint-active backgrounds without ever touching stale
rows or files.

Contract (locked with Codex, brief §Commit 3):
  - **Read-only / behaviour-free.** No DB write, no PNG create/delete, no
    checkpoint mutation. Stale items are *reported*, never cleaned. The
    helper must not mutate its inputs.
  - **Single SOT for ``expected``.** The caller passes the same
    ``expected_bg_ids`` that ``background_render.verify_completion()``
    derives (master_plan + background_prompt canonical). The helper never
    recomputes expected — that avoids a second source of truth.
  - **active set** = ``expected_bg_ids ∩ {bg : render_groups[bg].status
    == 'ok'}``. reuse bgs keep ``status='ok'`` so they are active.
  - **stale** = asset rows / PNG files whose bg_id is NOT in the active
    set (this includes the leftover assets of expected-but-failed /
    reuse_target_missing bgs, which are no longer active).
  - **active-missing** (an active bg with no asset row, or an asset row
    whose file does not resolve) is a *diagnostic*, NOT stale — the bg is
    current, just incomplete.
"""
from __future__ import annotations

from typing import Any, Dict, Iterable, List, Set


_ACTIVE_STATUS = "ok"


def build_active_only_audit(
    *,
    expected_bg_ids: Iterable[str],
    render_groups: Dict[str, Dict[str, Any]],
    asset_bg_ids: Iterable[str],
    asset_bg_ids_with_files: Iterable[str],
    file_bg_ids: Iterable[str],
) -> Dict[str, Any]:
    """Return the active / stale partition + a summary.

    Parameters
    ----------
    expected_bg_ids:
        Canonical expected bg ids (same source as ``verify_completion``).
    render_groups:
        ``background_render`` checkpoint ``data.groups`` — each entry is
        read for ``status`` / ``is_reuse`` / ``render_action`` /
        ``reuse_target_bg_id`` / ``png_path`` only.
    asset_bg_ids:
        All ``chain_bg`` ImageAsset row bg_ids (variant_type), as scanned
        read-only by the caller.
    asset_bg_ids_with_files:
        Subset of ``asset_bg_ids`` whose ``file_path`` resolves on disk.
    file_bg_ids:
        PNG file stems present in the background_chain image dir.
    """
    expected_set: Set[str] = {b for b in expected_bg_ids if b}
    asset_set: Set[str] = {b for b in asset_bg_ids if b}
    asset_with_files_set: Set[str] = {b for b in asset_bg_ids_with_files if b}
    file_set: Set[str] = {b for b in file_bg_ids if b}

    active: List[Dict[str, Any]] = []
    active_bg_ids: Set[str] = set()
    active_missing: List[str] = []
    active_missing_files: List[str] = []
    reuse_count = 0
    new_render_count = 0
    active_found_count = 0

    for bg_id in sorted(expected_set):
        g = render_groups.get(bg_id) or {}
        if g.get("status") != _ACTIVE_STATUS:
            # Not active — its leftover asset/file (if any) is handled by
            # the stale partition below.
            continue
        is_reuse = bool(g.get("is_reuse")) or (
            g.get("render_action") == "reuse_existing_plate"
        )
        has_asset = bg_id in asset_set
        has_file = bg_id in asset_with_files_set
        active.append({
            "bg_id": bg_id,
            "status": g.get("status"),
            "is_reuse": is_reuse,
            "render_action": g.get("render_action", "render_new_plate"),
            "reuse_target_bg_id": g.get("reuse_target_bg_id", "") or "",
            "png_path": g.get("png_path", "") or "",
            "has_asset": has_asset,
            "has_file": has_file,
        })
        active_bg_ids.add(bg_id)
        if is_reuse:
            reuse_count += 1
        else:
            new_render_count += 1
        if has_asset and has_file:
            active_found_count += 1
        elif not has_asset:
            active_missing.append(bg_id)
        else:
            # asset row exists but file did not resolve.
            active_missing_files.append(bg_id)

    # Stale = asset rows / PNG files not part of the active set. Reported
    # only — never deleted (no-destructive-cleanup lock).
    stale_asset_bg_ids = sorted(asset_set - active_bg_ids)
    stale_file_bg_ids = sorted(file_set - active_bg_ids)

    return {
        "active": active,
        "active_missing": sorted(active_missing),
        "active_missing_files": sorted(active_missing_files),
        "stale_asset_bg_ids": stale_asset_bg_ids,
        "stale_file_bg_ids": stale_file_bg_ids,
        "summary": {
            "active_count": len(active),
            "active_found_count": active_found_count,
            "reuse_count": reuse_count,
            "new_render_count": new_render_count,
            "active_missing_count": len(active_missing),
            "active_missing_files_count": len(active_missing_files),
            "stale_asset_count": len(stale_asset_bg_ids),
            "stale_file_count": len(stale_file_bg_ids),
        },
    }
