"""ImageHub — task pipeline state machine (project-workflow §3/§4, single-user MVP).

A cohesive, HTTP-free domain module: the per-task transitions that move a task through a
dataset's ordered pipeline stages (Label -> Review_1 -> Review_n -> Ground Truth). The HTTP
routes in ``imagehub_routes`` stay a thin transport layer; the rules live here so they are
unit-testable with plain data (no DB, no FastAPI), mirroring ``imagehub_segmentation``.

Mapped from ``docs/workflows/project-workflow-spec.md``:
  * TP1 finalize advances a Label task to the next stage (or Ground Truth if none follow);
  * TP2 review accept (with/without corrections) advances; TP3 reject returns to the first stage;
  * §9 RS1 only a Ground Truth task may be a reference standard.

Multi-labeler assignment, issues, comments, time and evaluation are later phases; this module is
deliberately limited to the pipeline + the bits the Data Page MVP needs.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

# State vocabularies — must match the CHECK constraints in migration 021.
PIPELINE_STATES = ("inLabel", "inReview", "groundTruth", "issue")
QUEUE_STATUSES = ("assigned", "saved", "pendingFinalization", "skipped")
REVIEW_DECISIONS = ("accept", "acceptWithCorrections", "reject")


class TaskPipelineError(Exception):
    """A pipeline rule was violated; ``status`` is the HTTP code the caller should map to."""

    def __init__(self, message: str, status: int = 422) -> None:
        super().__init__(message)
        self.status = status


@dataclass(frozen=True)
class StageInfo:
    """The minimum a transition needs to know about a pipeline stage."""

    id: str
    kind: str  # 'label' | 'review'
    seq: int


@dataclass(frozen=True)
class Transition:
    """The computed result of an event: the task's new pipeline position + an audit label."""

    pipeline_state: str
    current_stage_id: Optional[str]
    queue_status: str
    action: str


def order_stages(stages: list[StageInfo]) -> list[StageInfo]:
    """Pipeline order: by ``seq`` then ``id`` (stable, deterministic)."""
    return sorted(stages, key=lambda s: (s.seq, s.id))


def first_stage(stages: list[StageInfo]) -> StageInfo:
    ordered = order_stages(stages)
    if not ordered:
        raise TaskPipelineError(
            "Bộ dữ liệu chưa có giai đoạn nào trong quy trình.", status=409
        )
    return ordered[0]


def stage_after(stages: list[StageInfo], stage_id: Optional[str]) -> Optional[StageInfo]:
    """The stage following ``stage_id`` in pipeline order, or None if it is the last / unknown."""
    if stage_id is None:
        return None
    ordered = order_stages(stages)
    for i, s in enumerate(ordered):
        if s.id == stage_id:
            return ordered[i + 1] if i + 1 < len(ordered) else None
    return None


def state_for_stage(stage: StageInfo) -> str:
    """A review stage holds tasks ``inReview``; any other (label/pre-label) holds them ``inLabel``."""
    return "inReview" if stage.kind == "review" else "inLabel"


def initial_transition(stages: list[StageInfo]) -> Transition:
    """Where a brand-new task starts: the first stage, ``assigned`` and unworked."""
    s0 = first_stage(stages)
    return Transition(state_for_stage(s0), s0.id, "assigned", "Tạo công việc")


def _advance(current_stage_id: Optional[str], stages: list[StageInfo]) -> Transition:
    """Move to the next stage, or to terminal Ground Truth when none follows."""
    nxt = stage_after(stages, current_stage_id)
    if nxt is None:
        return Transition("groundTruth", None, "assigned", "")
    return Transition(state_for_stage(nxt), nxt.id, "assigned", "")


def compute_finalize(
    pipeline_state: str, current_stage_id: Optional[str], stages: list[StageInfo]
) -> Transition:
    """TP1: finalizing a Label task advances it to the next stage (or Ground Truth)."""
    if pipeline_state != "inLabel":
        raise TaskPipelineError(
            "Chỉ có thể hoàn tất công việc đang ở giai đoạn gán nhãn.", status=409
        )
    t = _advance(current_stage_id, stages)
    action = "Hoàn tất → Ground Truth" if t.pipeline_state == "groundTruth" else "Hoàn tất gán nhãn"
    return Transition(t.pipeline_state, t.current_stage_id, t.queue_status, action)


def compute_review(
    pipeline_state: str, current_stage_id: Optional[str], stages: list[StageInfo], decision: str
) -> Transition:
    """TP2 accept / TP3 reject from a Review stage."""
    if pipeline_state != "inReview":
        raise TaskPipelineError(
            "Chỉ có thể duyệt công việc đang ở giai đoạn rà soát.", status=409
        )
    if decision not in REVIEW_DECISIONS:
        raise TaskPipelineError("Quyết định duyệt không hợp lệ.", status=422)
    if decision == "reject":
        s0 = first_stage(stages)
        return Transition("inLabel", s0.id, "assigned", "Từ chối — trả lại để chỉnh sửa")
    t = _advance(current_stage_id, stages)
    label = "Chấp nhận (có chỉnh sửa)" if decision == "acceptWithCorrections" else "Chấp nhận"
    if t.pipeline_state == "groundTruth":
        label += " → Ground Truth"
    return Transition(t.pipeline_state, t.current_stage_id, t.queue_status, label)


def validate_set_reference(pipeline_state: str, value: bool) -> None:
    """RS1: only a Ground Truth task may be flagged as a reference standard."""
    if value and pipeline_state != "groundTruth":
        raise TaskPipelineError(
            "Chỉ tác vụ Ground Truth mới được đặt làm chuẩn tham chiếu.", status=409
        )
