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

from __future__ import annotations

import json as jsonlib
from typing import TYPE_CHECKING, NamedTuple

from django.core.management.base import CommandError

from ._invocation import _begin_readonly_cli_invocation

if TYPE_CHECKING:
    from collections.abc import Mapping

    from allianceauth.services.modules.discord.discord_client.models import (
        DiscordUser,
        GuildMember,
    )

    from ....models import (
        DiscordIdentity,
        OrphanAction,
        OrphanCategory,
    )
    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


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 (
        build_bot_uids,
        build_discord_users_by_uid,
        build_identities_by_uid,
        build_users_with_discord_access,
        classify,
        policy_from_snapshot,
        resolve_action,
    )

    member = _fetch_explain_member(member_id)
    bot_uids = build_bot_uids()
    users_with_access = build_users_with_discord_access()
    discord_users_by_uid = build_discord_users_by_uid()
    identities_by_uid = build_identities_by_uid()
    policy = policy_from_snapshot(AA_DISCORD_AUDIT_POLICY)

    category, aa_user = classify(
        member,
        bot_uids=bot_uids,
        users_with_access=users_with_access,
        discord_users_by_uid=discord_users_by_uid,
        identities_by_uid=identities_by_uid,
    )
    inputs = _derive_explain_inputs(
        member_id,
        bot_uids=bot_uids,
        users_with_access=users_with_access,
        discord_users_by_uid=discord_users_by_uid,
        identities_by_uid=identities_by_uid,
    )
    action: OrphanAction | None = (
        resolve_action(category, policy=policy, cli_override=None)
        if category is not None
        else None
    )
    aa_user_pk = aa_user.pk if aa_user is not None else None

    if json_output:
        _emit_explain_json(
            cmd,
            member=member,
            member_id=member_id,
            category=category,
            action=action,
            aa_user_pk=aa_user_pk,
            inputs=inputs,
        )
    else:
        _emit_explain_text(
            cmd,
            member=member,
            member_id=member_id,
            category=category,
            action=action,
            inputs=inputs,
        )


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 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,
    *,
    bot_uids: frozenset[int],
    users_with_access: frozenset[int],
    discord_users_by_uid: Mapping[int, DiscordUser],
    identities_by_uid: Mapping[int, DiscordIdentity],
) -> _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 = discord_users_by_uid.get(member_id)
    has_discord_user = discord_user is not None
    discord_user_has_access: bool | None
    if has_discord_user:
        du_user_id = getattr(discord_user, "user_id", None)
        discord_user_has_access = (
            du_user_id in 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 bot_uids,
        has_discord_user=has_discord_user,
        discord_user_has_access=discord_user_has_access,
        has_identity=identities_by_uid.get(member_id) is not None,
    )


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


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