"""Review state machine.

This module defines valid state transitions and provides methods to
execute state changes with proper validation and cleanup.

State Diagram:
    ┌─────────────┐
    │ needs_review│◄──────────────────┐
    └──────┬──────┘                   │
           │ start                    │ cancel
           ▼                          │
    ┌─────────────┐                   │
    │ in_progress │───────────────────┤
    └──────┬──────┘                   │
           │ signoff                  │
           ▼                          │
    ┌─────────────┐   code changes    │
    │  reviewed   │───────────────────┘
    └─────────────┘   (triggers re-review via start)

Note: The 'needs_update' status mentioned in some docstrings is not
implemented. All status values are: needs_review, in_progress, reviewed.
"""

from __future__ import annotations

from datetime import datetime, timezone
from enum import Enum
from typing import TYPE_CHECKING

from .errors import AlreadyInStateError, InvalidStateTransitionError

if TYPE_CHECKING:
    from .registry import ModuleStatus


class Status(Enum):
    """Valid review statuses."""

    NEEDS_REVIEW = "needs_review"
    IN_PROGRESS = "in_progress"
    REVIEWED = "reviewed"

    @classmethod
    def from_string(cls, value: str) -> "Status":
        """Convert string to Status enum."""
        for status in cls:
            if status.value == value:
                return status
        raise ValueError(f"Invalid status: {value}")


# Valid state transitions
VALID_TRANSITIONS: dict[Status, set[Status]] = {
    Status.NEEDS_REVIEW: {Status.IN_PROGRESS},
    Status.IN_PROGRESS: {Status.REVIEWED, Status.NEEDS_REVIEW},  # signoff or cancel
    Status.REVIEWED: {Status.IN_PROGRESS, Status.NEEDS_REVIEW},  # re-review or cancel
}


def validate_transition(
    current: Status,
    target: Status,
    force: bool = False,
) -> None:
    """Validate a state transition.

    Args:
        current: Current status.
        target: Desired status.
        force: If True, skip validation.

    Raises:
        AlreadyInStateError: If already in target state (and not forcing).
        InvalidStateTransitionError: If transition is not valid.
    """
    if force:
        return

    if current == target:
        raise AlreadyInStateError("module", target.value)

    valid_targets = VALID_TRANSITIONS.get(current, set())
    if target not in valid_targets:
        raise InvalidStateTransitionError(
            current.value,
            target.value,
            [s.value for s in valid_targets],
        )


def start_review(
    module: "ModuleStatus",
    current_commit: str,
    issue_url: str | None = None,
    assignee: str | None = None,
    force: bool = False,
) -> None:
    """Transition module to in_progress state.

    Args:
        module: The module to update.
        current_commit: Current git commit hash.
        issue_url: URL or reference to the review issue.
        assignee: Username assigned to the review.
        force: Skip state validation.

    Raises:
        InvalidStateTransitionError: If transition is not valid.
    """
    current = Status.from_string(module.status)
    validate_transition(current, Status.IN_PROGRESS, force=force)

    module.status = Status.IN_PROGRESS.value
    module.review_issue = issue_url
    module.assigned_reviewer = assignee
    module.review_started_commit = current_commit


def signoff(
    module: "ModuleStatus",
    current_commit: str,
    reviewer: str,
    force: bool = False,
) -> None:
    """Transition module to reviewed state.

    Args:
        module: The module to update.
        current_commit: Current git commit hash.
        reviewer: Reviewer username(s), comma-separated.
        force: Skip state validation.

    Raises:
        InvalidStateTransitionError: If transition is not valid.
    """
    current = Status.from_string(module.status)
    validate_transition(current, Status.REVIEWED, force=force)

    module.status = Status.REVIEWED.value
    module.last_reviewed_commit = current_commit
    module.last_reviewed_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")

    # Merge reviewers - normalize by stripping @ prefix for consistency
    new_reviewers = []
    for r in reviewer.split(","):
        r = r.strip()
        if r:
            # Store with @ prefix for display consistency
            if not r.startswith("@"):
                r = f"@{r}"
            new_reviewers.append(r)

    existing = set(module.reviewers)
    module.reviewers = sorted(existing.union(new_reviewers))

    # Clear in-progress fields
    module.assigned_reviewer = None
    module.review_started_commit = None


def cancel_review(
    module: "ModuleStatus",
    force: bool = False,
) -> str:
    """Cancel a review, returning to appropriate previous state.

    Args:
        module: The module to update.
        force: Skip state validation.

    Returns:
        The new status value.

    Raises:
        InvalidStateTransitionError: If transition is not valid.
    """
    current = Status.from_string(module.status)

    # Determine target state based on history
    if module.last_reviewed_commit:
        # Was previously reviewed - restore to reviewed state
        target = Status.REVIEWED
    else:
        # Never reviewed - go to needs_review
        target = Status.NEEDS_REVIEW

    validate_transition(current, target, force=force)

    # Clear in-progress fields
    module.review_issue = None
    module.review_started_commit = None
    module.assigned_reviewer = None

    module.status = target.value
    return target.value


def restart_review(
    module: "ModuleStatus",
    current_commit: str,
) -> None:
    """Restart a review at current commit without closing issue.

    This updates the start commit but keeps the review in progress.

    Args:
        module: The module to update.
        current_commit: New start commit.
    """
    if module.status != Status.IN_PROGRESS.value:
        raise InvalidStateTransitionError(
            module.status,
            "restart",
            ["Only in_progress reviews can be restarted"],
        )

    module.review_started_commit = current_commit
