"""``--explain`` subcommand: classify one Discord member, no writes."""

from __future__ import annotations

import json as jsonlib
from dataclasses import dataclass
from typing import TYPE_CHECKING, NamedTuple

from django.core.management.base import CommandError

from ._invocation import _begin_readonly_cli_invocation

if TYPE_CHECKING:  # pragma: no cover
    from allianceauth.services.modules.discord.discord_client.models import (
        GuildMember,
    )

    from ....models import (
        OrphanAction,
        OrphanCategory,
    )
    from ....policy import ClassifierContext
    from ..audit_discord_roles import Command


class _ExplainInputs(NamedTuple):
    """Per-uid signals derived for traceability in the explain output."""

    in_bot_uids: bool
    has_discord_user: bool
    discord_user_has_access: bool | None
    has_identity: bool


@dataclass(frozen=True, slots=True)
class _ExplainResult:
    """
    Resolved explain output, passed to both emitters as one object.

    Bundles every value produced by ``run_explain`` so the two emitter
    functions share a single parameter instead of a repeated keyword list.
    """

    member: GuildMember
    member_id: int
    category: OrphanCategory | None
    action: OrphanAction | None
    aa_user_pk: int | None
    inputs: _ExplainInputs


def run_explain(cmd: Command, member_id: int, *, json_output: bool) -> None:
    """Show classification + resolved action for one member; no writes."""
    # Read-only diagnostic. Permission gate still applies because
    # introspecting "what would the audit do for this uid"
    # discloses who is in the guild and how they map to AA users.
    # No AuditRun and no AuditInvocation are persisted: --explain
    # is not an audit, so polluting the trail with diagnostic
    # introspections would obscure the real run history.
    _begin_readonly_cli_invocation(action_label="explain a Discord member")

    from ....app_settings import AA_DISCORD_AUDIT_POLICY
    from ....policy import (
        assemble_classifier_context,
        policy_from_snapshot,
        resolve_action,
    )

    member = _fetch_explain_member(member_id)
    ctx = assemble_classifier_context()
    policy = policy_from_snapshot(AA_DISCORD_AUDIT_POLICY)

    category, aa_user = ctx.classify(member)
    action: OrphanAction | None = (
        resolve_action(category, policy=policy, cli_override=None)
        if category is not None
        else None
    )
    result = _ExplainResult(
        member=member,
        member_id=member_id,
        category=category,
        action=action,
        aa_user_pk=aa_user.pk if aa_user is not None else None,
        inputs=_derive_explain_inputs(member_id, ctx=ctx),
    )

    if json_output:
        _emit_explain_json(cmd, result)
    else:
        _emit_explain_text(cmd, result)


def _fetch_explain_member(member_id: int) -> GuildMember:
    """Fetch one Discord guild member; raise ``CommandError`` on miss."""
    from allianceauth.services.modules.discord.app_settings import (
        DISCORD_GUILD_ID,
    )
    from allianceauth.services.modules.discord.discord_client import (
        models as _client_models,
    )

    from ....client import make_audited_client

    client = make_audited_client()
    try:
        raw_member = client.guild_member(
            guild_id=DISCORD_GUILD_ID, user_id=member_id
        )
    except CommandError:
        # A CommandError is already operator-facing — never re-wrap it
        # in the generic "failed to fetch" message below (that would
        # bury the real reason behind a misleading prefix).
        raise
    except Exception as exc:
        # Surface any HTTP / connection failure as CommandError so
        # the operator sees a single line instead of a traceback.
        raise CommandError(
            f"failed to fetch Discord member id={member_id}: {exc}"
        ) from exc
    if raw_member is None:
        raise CommandError(
            f"Discord member id={member_id} not found in guild "
            f"{DISCORD_GUILD_ID}"
        )
    return _client_models.GuildMember.from_dict(raw_member)


def _derive_explain_inputs(
    member_id: int,
    *,
    ctx: ClassifierContext,
) -> _ExplainInputs:
    """
    Compute the raw-signal inputs section for the explain output.

    The inputs report the facts we can read directly about this uid
    (in bot_uids? linked DiscordUser? identity row?), not which
    precedence branch ``classify()`` took. Two disagreeing inputs
    (e.g. uid in both ``bot_uids`` and the ``DiscordUser`` table) are
    visible at a glance — useful when an operator opens ``--explain``
    to diagnose an unexpected classification.
    """
    discord_user = ctx.discord_users_by_uid.get(member_id)
    discord_user_has_access: bool | None
    if discord_user is not None:
        du_user_id = discord_user.user_id
        discord_user_has_access = (
            du_user_id in ctx.users_with_access
            if du_user_id is not None
            else False
        )
    else:
        discord_user_has_access = None
    return _ExplainInputs(
        in_bot_uids=member_id in ctx.bot_uids,
        has_discord_user=discord_user is not None,
        discord_user_has_access=discord_user_has_access,
        has_identity=ctx.identities_by_uid.get(member_id) is not None,
    )


def _emit_explain_json(cmd: Command, result: _ExplainResult) -> None:
    """Render the JSON form of the explain output."""
    cmd.stdout.write(
        jsonlib.dumps(
            {
                "member_id": result.member_id,
                "member_username": getattr(result.member.user, "username", ""),
                "category": (
                    result.category.value if result.category else None
                ),
                "resolved_action": (
                    result.action.value if result.action else None
                ),
                "would_create_finding": result.category is not None,
                "aa_user_pk": result.aa_user_pk,
                "inputs": result.inputs._asdict(),
            }
        )
    )


def _emit_explain_text(cmd: Command, result: _ExplainResult) -> None:
    """Render the human-readable form of the explain output."""
    cls_label = (
        result.category.value
        if result.category
        else "active (no orphan finding)"
    )
    action_label = result.action.value if result.action else "-"
    member_username = getattr(result.member.user, "username", "<unknown>")
    access_repr = (
        "-"
        if result.inputs.discord_user_has_access is None
        else str(result.inputs.discord_user_has_access)
    )
    cmd.stdout.write(
        f"Discord member id={result.member_id}"
        f" username={member_username!r}\n"
        f"classification: {cls_label}\n"
        f"resolved_action: {action_label}\n"
        f"inputs:\n"
        f"  in_bot_uids: {result.inputs.in_bot_uids}\n"
        f"  has_discord_user: {result.inputs.has_discord_user}\n"
        f"  discord_user_has_access: {access_repr}\n"
        f"  has_identity: {result.inputs.has_identity}\n"
    )
