"""``--abandon`` subcommand: mark a run as ABANDONED."""

from __future__ import annotations

from typing import TYPE_CHECKING

from django.core.management.base import CommandError

from ...._log import get_logger
from ....audit._apply import observe_run_terminal
from ....models import AuditRunState
from ._invocation import (
    _begin_cli_invocation_accepted,
    _get_run_or_raise,
    _link_invocation,
    emit_lost_cas_diagnostic,
)

logger = get_logger(__name__)

if TYPE_CHECKING:  # pragma: no cover
    from ..audit_discord_roles import Command


def run_abandon(cmd: Command, run_id: int) -> None:
    """
    Mark a stuck run dead.

    Refuses on terminal states (SUCCEEDED / FAILED / BUDGET_EXCEEDED)
    so the append-only audit-trail is preserved; idempotent on
    ABANDONED so a re-click is harmless.
    """
    # Record an AuditInvocation so the abandon itself leaves a trail.
    invocation = _begin_cli_invocation_accepted(
        action_label="abandon an audit",
    )

    run = _get_run_or_raise(run_id)
    if run.state == AuditRunState.ABANDONED:
        cmd.stdout.write(
            cmd.style.WARNING(
                f"AuditRun id={run_id} is already ABANDONED "
                f"(idempotent no-op)."
            )
        )
        return
    if not AuditRunState(run.state).is_abandonable:
        # Terminal-state refusal — the run is sealed; the operator
        # likely typed the wrong run id. Naming the actual state
        # in the message lets the operator correlate with their
        # intent without consulting the DB.
        raise CommandError(
            f"AuditRun id={run_id} is in terminal state {run.state}; "
            f"--abandon refuses to overwrite a sealed run "
            f"(append-only audit-trail). Resumable / never-started "
            f"runs (pending / running / interrupted) are the "
            f"legitimate --abandon target."
        )
    from ....persistence import transition_run_state

    # Defense-in-depth via CAS: if a concurrent flip lands a terminal
    # state between the upfront check and this write, ``expected``
    # rejects it (rowcount=0) and we surface the race honestly instead
    # of clobbering the trail. The matrix permits
    # PENDING/RUNNING/INTERRUPTED -> ABANDONED, so a legitimate flip
    # still succeeds.
    flipped = transition_run_state(
        run,
        AuditRunState.ABANDONED,
        error_message="manually abandoned",
        expected=(
            AuditRunState.PENDING,
            AuditRunState.RUNNING,
            AuditRunState.INTERRUPTED,
        ),
    )
    _link_invocation(invocation, run.pk)
    if flipped:
        # Observe the abandon as a terminal completion via the shared
        # ``observe_run_terminal`` helper. The duration spans original
        # ``started_at`` -> now, mirroring the resume path's reading:
        # it is the wall-clock the run row spent alive, not the
        # operator's --abandon-keystroke latency.
        observe_run_terminal(run, AuditRunState.ABANDONED)
        cmd.stdout.write(
            cmd.style.SUCCESS(f"AuditRun id={run_id} marked ABANDONED.")
        )
    else:
        emit_lost_cas_diagnostic(
            cmd,
            run,
            context="--abandon",
            intended="abandoned",
            source="concurrent flip after the upfront terminal-state check",
        )
