"""anu_v3.goal_loop_planner — Track3 goal-driven loop generalization (minimal).

Authority: task-2553+17.md §1 / §2 / §4 (Track3) + §12 9-R.7.

The chair supplies only ``goal + boundary + policy_profile``; this module
binds them to a track and generates the standard ANU loop plan::

    lint -> refine(9-R) -> re-lint -> impl -> audit -> adjudication
         -> callback -> next

and resolves next_action / HOLD condition / final-packet schema.

9-R.7 split rule (non-self-referential / no bootstrap paradox):
  If any of the Track2 mandatory-11 features would be made unimplementable
  or contaminated by a Track3 generalization, that **generalization
  sub-feature only** is split out as a follow-up candidate (all 11 Track2
  features preserved, no silent drop). If even after splitting a Track2
  mandatory feature is unavoidably damaged -> HOLD_FOR_CHAIR.

NOTE (9-R.7 / §10): the construction of this very batch is ANU **manual**
coordination — the not-yet-built coordinator never manages its own
build/adjudication. This planner is for *future* batches; the task-2553
series here is fixture/verification data only.
"""

from __future__ import annotations

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

LOOP_STAGES: Tuple[str, ...] = (
    "lint",
    "refine_9r",
    "re_lint",
    "impl",
    "audit",
    "adjudication",
    "callback",
    "next",
)

# Track2 mandatory-11 feature keys (task-2553+17.md §4 1..11). Used by the
# split rule to guarantee zero silent drop.
TRACK2_MANDATORY_11: Tuple[str, ...] = (
    "batch_id_generation",
    "callback_4tuple_registry",
    "dependency_matrix",
    "expected_files_overlap",
    "forbidden_write_overlap",
    "shared_artifact_contamination",
    "final_authority_packet",
    "duplicate_pending_classifier",
    "track_loop_state_13",
    "batch_next_action",
    "consolidated_final_summary",
)


@dataclass
class PolicyProfile:
    name: str
    retry_ceiling: int = 2
    auto_micro_fix: bool = True
    hold_on_high_or_critical: bool = True
    final_packet_schema: str = "anu_v3.track_final_packet.v1"


@dataclass
class GoalRequest:
    """Chair-supplied: goal + boundary + policy_profile only."""

    goal_id: str
    goal_statement: str
    boundary: List[str] = field(default_factory=list)
    policy_profile: PolicyProfile = field(
        default_factory=lambda: PolicyProfile("default")
    )

    def bind_track(self, track_id: str) -> "TrackGoalBinding":
        return TrackGoalBinding(
            track_id=track_id,
            goal=self,
            policy_profile=self.policy_profile,
        )


@dataclass
class TrackGoalBinding:
    track_id: str
    goal: GoalRequest
    policy_profile: PolicyProfile


@dataclass
class LoopPlan:
    track_id: str
    goal_id: str
    stages: List[str]
    retry_ceiling: int
    hold_conditions: List[str]
    final_packet_schema: str
    split_followups: List[str] = field(default_factory=list)
    status: str = "PLANNED"  # PLANNED | FOLLOW_UP_SPLIT | HOLD_FOR_CHAIR

    def to_dict(self) -> Dict[str, object]:
        return {
            "track_id": self.track_id,
            "goal_id": self.goal_id,
            "stages": list(self.stages),
            "retry_ceiling": self.retry_ceiling,
            "hold_conditions": list(self.hold_conditions),
            "final_packet_schema": self.final_packet_schema,
            "split_followups": list(self.split_followups),
            "status": self.status,
        }


# Standard HOLD conditions (task-2553+17.md §8).
DEFAULT_HOLD_CONDITIONS: Tuple[str, ...] = (
    "frozen_anchor_change_required",
    "github_write_required",
    "track1_domain_touch_required",
    "codex_unresolved_high_or_critical",
    "anu_codex_repeated_conflict",
    "goal_unreachable",
    "track3_irreducible_fundamental_conflict",
    "critical7",
)


def generate_loop_plan(
    binding: TrackGoalBinding,
    track2_conflicts: Sequence[str] = (),
) -> LoopPlan:
    """Generate the loop plan for a goal-bound track and apply 9-R.7.

    ``track2_conflicts`` lists Track2 mandatory feature keys that this
    generalization conflicts with. Each conflicting key is split into a
    follow-up (Track2 feature preserved). If the list is non-empty AND
    contains a feature that cannot be preserved by splitting (signalled by
    the sentinel ``"<irreducible>"`` prefix) -> HOLD_FOR_CHAIR.
    """
    pp = binding.policy_profile
    plan = LoopPlan(
        track_id=binding.track_id,
        goal_id=binding.goal.goal_id,
        stages=list(LOOP_STAGES),
        retry_ceiling=pp.retry_ceiling,
        hold_conditions=list(DEFAULT_HOLD_CONDITIONS),
        final_packet_schema=pp.final_packet_schema,
    )
    irreducible = [c for c in track2_conflicts if c.startswith("<irreducible>")]
    splittable = [
        c for c in track2_conflicts if not c.startswith("<irreducible>")
    ]
    unknown = [
        c for c in splittable if c not in TRACK2_MANDATORY_11
    ]
    if unknown:
        raise ValueError(f"unknown Track2 feature keys: {unknown}")
    if irreducible:
        plan.status = "HOLD_FOR_CHAIR"
        plan.split_followups = []
    elif splittable:
        plan.status = "FOLLOW_UP_SPLIT"
        plan.split_followups = sorted(splittable)
    return plan


def resolve_next_action(plan: LoopPlan, current_stage: str) -> str:
    """Resolve the next loop stage (or terminal next-action)."""
    if plan.status == "HOLD_FOR_CHAIR":
        return "HOLD_FOR_CHAIR"
    if current_stage not in plan.stages:
        raise ValueError(f"unknown stage {current_stage!r}")
    idx = plan.stages.index(current_stage)
    if idx + 1 < len(plan.stages):
        return plan.stages[idx + 1]
    return "LOOP_COMPLETE"


def resolve_hold_condition(plan: LoopPlan, signals: Dict[str, bool]) -> str:
    """Return the first triggered HOLD condition, else "NONE"."""
    for cond in plan.hold_conditions:
        if signals.get(cond):
            return cond
    return "NONE"


def resolve_final_packet_schema(plan: LoopPlan) -> str:
    return plan.final_packet_schema


def track2_mandatory_preserved(plans: Sequence[LoopPlan]) -> bool:
    """Zero silent drop guarantee: no plan may HOLD without recording it,
    and split follow-ups must reference only known Track2 keys."""
    for p in plans:
        for f in p.split_followups:
            if f not in TRACK2_MANDATORY_11:
                return False
    return True
