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

from __future__ import annotations

from typing import TYPE_CHECKING, Final

from django.core.management.base import CommandError

from ....models import AuditRunState
from ._invocation import (
    _begin_cli_invocation,
    _get_run_or_raise,
    _link_invocation,
    _record_acceptance,
)

if TYPE_CHECKING:
    from ..audit_discord_roles import Command


# Terminal states are sealed: ``--abandon`` must NOT overwrite their
# ``finished_at`` / ``error_message``. Append-only audit-trail is the
# load-bearing security guarantee of this module; a typo
# (``--abandon 4242`` instead of ``4243``) would otherwise corrupt
# the trail of an already-completed run. Resumable / never-started
# states (PENDING, RUNNING, INTERRUPTED) are the legitimate target.
_ABANDONABLE: Final[frozenset[str]] = frozenset(
    {
        AuditRunState.PENDING.value,
        AuditRunState.RUNNING.value,
        AuditRunState.INTERRUPTED.value,
    }
)


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.
    user, argv = _begin_cli_invocation(action_label="abandon an audit")
    invocation = _record_acceptance(user, argv)

    run = _get_run_or_raise(run_id)
    if run.state == AuditRunState.ABANDONED.value:
        cmd.stdout.write(
            cmd.style.WARNING(
                f"AuditRun id={run_id} is already ABANDONED "
                f"(idempotent no-op)."
            )
        )
        return
    if run.state not in _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 flip_run_state

    # Defense-in-depth via CAS: if a concurrent flip lands a
    # terminal state between the upfront check and this write,
    # ``expected_state`` 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 = flip_run_state(
        run,
        AuditRunState.ABANDONED,
        error_message="manually abandoned",
        expected_state=(
            AuditRunState.PENDING,
            AuditRunState.RUNNING,
            AuditRunState.INTERRUPTED,
        ),
    )
    _link_invocation(invocation, run.pk)
    if flipped:
        cmd.stdout.write(
            cmd.style.SUCCESS(f"AuditRun id={run_id} marked ABANDONED.")
        )
    else:
        run.refresh_from_db()
        cmd.stdout.write(
            cmd.style.WARNING(
                f"AuditRun id={run_id} --abandon lost the race; "
                f"final state {run.state} (set by a concurrent "
                f"flip after the upfront terminal-state check)."
            )
        )
