"""``audit_discord_roles`` (no subcommand) — run a fresh audit."""

from __future__ import annotations

import json as jsonlib
from typing import TYPE_CHECKING

from django.core.management.base import CommandError

from ....models import (
    InvocationRefusal,
    OrphanAction,
    RunTrigger,
)
from ._invocation import (
    _begin_cli_invocation,
    _link_invocation,
    _rate_limit_or_record,
    _record_refusal,
)

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


def run_main(cmd: Command, opts: CommandOptions, *, verbosity: int) -> None:
    """Default entry point: build + apply a fresh audit run."""
    # Gate FIRST, record acceptance AFTER. Both refusal paths (no
    # permission, destructive action without --yes) must persist
    # an AuditInvocation row so the audit-the-auditor trail is
    # complete, but per the AuditInvocation.accepted contract
    # those rows do NOT count toward the rate-limit window — only
    # accepted=True rows do. This eliminates the prior quota-leak
    # where a refused destructive attempt consumed a slot.
    user, argv = _begin_cli_invocation(
        action_label="start an audit",
        extra_no_user_hint=(
            "Run the command as an OS user whose login name matches "
            "a Django username with the permission, or grant the "
            "permission to the matching Django account. "
            "Alternatively, set AA_DISCORD_AUDIT_CLI_ACTOR to a "
            "Django username in your local settings."
        ),
    )

    action_raw = opts.get("action")
    action = OrphanAction(action_raw) if action_raw else None
    if not opts.get("yes") and action is not None and action.is_destructive:
        _record_refusal(user, argv, InvocationRefusal.CONFIRMATION_DECLINED)
        raise CommandError(
            f"refusing to start audit with destructive action "
            f"{action.value} without --yes"
        )

    invocation = _rate_limit_or_record(user, argv)

    from ....audit import apply_plan, build_plan
    from ....client import make_audited_client

    client = make_audited_client()
    plan = build_plan(
        client,
        triggered_by=RunTrigger.CLI,
        triggered_by_user=user,
        cli_argv=argv,
        was_confirmation_bypassed=bool(opts.get("yes")),
        cli_override=action,
        user_id_filter=opts.get("user_id"),
        limit=opts.get("limit"),
    )
    # Link the invocation to the spawned run (snapshot pk for
    # post-prune traceability).
    _link_invocation(invocation, plan.run_id)

    # Track the active run so the signal handler can flip it.
    # The reset deliberately lives in ``handle``'s outer
    # ``finally`` AFTER ``_flush_signal_to_run`` — see the
    # comment there for why an inner reset breaks SIGTERM flush.
    cmd.track_active_run(plan.run_id)
    result = apply_plan(plan, client)

    # Optional notification fan-out.
    if not opts.get("no_notify"):
        from ....notifications import fan_out_for_run

        fan_out_for_run(plan.run_id, source="cli audit")

    if opts.get("json"):
        cmd.stdout.write(
            jsonlib.dumps(
                {
                    "run_id": result.run_id,
                    "state": result.state.value,
                    "applied_count": len(result.applied),
                    "error_message": result.error_message,
                }
            )
        )
    elif verbosity >= 1:
        cmd.stdout.write(
            cmd.style.SUCCESS(
                f"Audit run #{result.run_id} "
                f"finished in state {result.state.value}"
            )
        )
