# -*- coding: utf-8 -*-
"""anu_v3.batch_runtime_join_policy — +29 standalone batch join policy.

DISTINCT from the frozen anchor anu_v3.batch_join_policy (byte-0, §9). This is
a NEW standalone module for task-2553+29; the frozen file is neither imported
nor edited (9-R.3 file-level contract, §8 forbidden-target collision avoided by
using a separate path).

Join rules implemented:
  * 구현목표 11 / regression 11 — one track HOLD does not block independent
    DONE tracks (tracks are joined by independence, not by worst-case).
  * regression 2 — a track whose result is ready but whose fallback is still
    PENDING is NON-blocking: it still contributes its terminal outcome.
  * 구현목표 12 / regression 14 — closeout eligibility is *derived* from the
    batch state (never from chat memory) and only ever PROPOSED, never
    confirmed (§7: closeout 확정 금지).
  * regression 12 — any cross-task artifact contamination -> BATCH_HOLD.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List, Sequence

BATCH_ACCEPT = "BATCH_ACCEPT"
BATCH_HOLD = "BATCH_HOLD"
WAIT_FOR_FALLBACK = "WAIT_FOR_FALLBACK"

# terminal outcomes that count a track as independently settled
_SETTLED = {"MERGED", "PASS", "DONE"}


@dataclass
class TrackJoinView:
    task_id: str
    terminal_outcome: str           # MERGED | PASS | DONE | HOLD | PENDING | UNKNOWN
    classification: str
    hold_for_chair: bool
    fallback_state: str             # PENDING | FIRED | CANCELLED | NONE
    has_result: bool


@dataclass
class JoinResult:
    independent_done_tracks: List[str] = field(default_factory=list)
    held_tracks: List[str] = field(default_factory=list)
    waiting_tracks: List[str] = field(default_factory=list)
    batch_next_action: str = BATCH_ACCEPT
    blocking_relations: List[str] = field(default_factory=list)


def join(
    views: Sequence[TrackJoinView],
    *,
    contamination: Sequence[object] = (),
) -> JoinResult:
    """Apply the independence join policy across all tracks."""
    res = JoinResult()

    if contamination:
        # regression 12 — contamination is batch-level fatal.
        res.batch_next_action = BATCH_HOLD
        for v in views:
            if v.terminal_outcome in _SETTLED and not v.hold_for_chair:
                res.independent_done_tracks.append(v.task_id)
            else:
                res.held_tracks.append(v.task_id)
        res.blocking_relations.append("cross_task_contamination -> BATCH_HOLD")
        return res

    for v in views:
        if v.hold_for_chair or v.terminal_outcome == "HOLD":
            res.held_tracks.append(v.task_id)
            continue
        if v.terminal_outcome in _SETTLED:
            # regression 2 — result ready: fallback still PENDING does NOT block.
            res.independent_done_tracks.append(v.task_id)
            continue
        if not v.has_result and v.fallback_state == "PENDING":
            res.waiting_tracks.append(v.task_id)
            continue
        res.waiting_tracks.append(v.task_id)

    # regression 11 — a HOLD track never demotes an independent DONE track.
    # No cross-track edges exist in this batch (disjoint expected_files), so
    # held tracks are recorded but create no blocking relation against DONE.
    if res.held_tracks and res.independent_done_tracks:
        res.blocking_relations.append(
            "held=%s do NOT block independent done=%s (join by independence)"
            % (res.held_tracks, res.independent_done_tracks)
        )

    if res.held_tracks:
        res.batch_next_action = BATCH_HOLD
    elif res.waiting_tracks:
        res.batch_next_action = WAIT_FOR_FALLBACK
    else:
        res.batch_next_action = BATCH_ACCEPT
    return res


def derive_closeout_proposal(batch_state: Dict[str, object]) -> Dict[str, object]:
    """Derive (PROPOSE, never confirm) closeout eligibility from batch_state.

    구현목표 12 / regression 14: input is the batch_state dict ONLY — no chat
    memory, no external signal. §7: confirmed is hard-pinned False.
    """
    tracks = batch_state.get("tracks", {}) or {}
    contamination = batch_state.get("contamination", []) or []
    next_action = batch_state.get("batch_next_action")

    all_settled = bool(tracks) and all(
        (rec.get("terminal_outcome") in _SETTLED) and not rec.get("hold_for_chair")
        for rec in tracks.values()  # type: ignore[union-attr]
    )
    eligible = all_settled and not contamination and next_action == BATCH_ACCEPT

    if not tracks:
        reason = "no tracks in batch_state"
    elif contamination:
        reason = "cross-task contamination present -> ineligible"
    elif not all_settled:
        unsettled = [
            tid for tid, rec in tracks.items()  # type: ignore[union-attr]
            if rec.get("terminal_outcome") not in _SETTLED or rec.get("hold_for_chair")
        ]
        reason = f"tracks not all settled: {unsettled}"
    else:
        reason = (
            "all tracks settled (MERGED/PASS/DONE), no contamination — "
            "closeout PROPOSED; chair confirmation still required (§7)."
        )

    return {
        "eligible": bool(eligible),
        "derived_from": "batch_state",
        "confirmed": False,  # §7 — registry never confirms closeout
        "reason": reason,
    }
