"""``--diff`` subcommand: compare current state to a historical run."""

from __future__ import annotations

import json as jsonlib
from typing import TYPE_CHECKING

from ....models import AuditFinding
from ._invocation import _begin_readonly_cli_invocation, _get_run_or_raise

if TYPE_CHECKING:
    from ..audit_discord_roles import Command


def _enumerate_current_classifications(
    client: object,
) -> dict[int, tuple[str, str]]:
    """Walk the live guild + classify; return ``{uid: (cat, act)}``."""
    # Read-only mirror of build_plan's enumeration loop, minus
    # the AuditRun creation. Lifted out of ``run_diff`` so the
    # function body stays under ruff's PLR0915 cap. The map is
    # keyed by uid because the caller deltacompares against the
    # persisted AuditFinding rows by ``member_id``.
    from itertools import chain

    from allianceauth.services.modules.discord.app_settings import (
        DISCORD_GUILD_ID,
    )
    from allianceauth.services.modules.discord.discord_client import (
        models as _client_models,
    )

    from ....app_settings import AA_DISCORD_AUDIT_POLICY
    from ....client import extract_member_user_id
    from ....constants import PAGE_SIZE
    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,
    )

    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)

    members_iter = chain.from_iterable(
        client.guild_members(
            guild_id=DISCORD_GUILD_ID,
            limit=PAGE_SIZE,
        )
    )
    out: dict[int, tuple[str, str]] = {}
    for raw_member in members_iter:
        uid = extract_member_user_id(raw_member)
        if uid is None:
            continue
        member = _client_models.GuildMember.from_dict(raw_member)
        category, _ = 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,
        )
        if category is None:
            continue
        action = resolve_action(category, policy=policy, cli_override=None)
        out[uid] = (category.value, action.value)
    return out


def _compute_diff_buckets(
    current: dict[int, tuple[str, str]],
    historical: dict[int, tuple[str, str]],
) -> tuple[
    list[int],
    list[int],
    list[tuple[int, str, str]],
    list[tuple[int, str, str]],
    list[int],
]:
    """Bucket uids into added / removed / changed / unchanged lanes."""
    current_uids = set(current.keys())
    historical_uids = set(historical.keys())
    added = sorted(current_uids - historical_uids)
    removed = sorted(historical_uids - current_uids)
    category_changed: list[tuple[int, str, str]] = []
    action_changed: list[tuple[int, str, str]] = []
    unchanged: list[int] = []
    for uid in sorted(current_uids & historical_uids):
        cur_cat, cur_act = current[uid]
        hist_cat, hist_act = historical[uid]
        if cur_cat != hist_cat:
            category_changed.append((uid, hist_cat, cur_cat))
        elif cur_act != hist_act:
            action_changed.append((uid, hist_act, cur_act))
        else:
            unchanged.append(uid)
    return added, removed, category_changed, action_changed, unchanged


def run_diff(cmd: Command, run_id: int, *, json_output: bool) -> None:
    """Compare current would-be findings to a historical run's findings."""
    # Read-only diagnostic. Uses the *current* AA_DISCORD_AUDIT_POLICY,
    # not the historical run's policy_snapshot — the documented
    # intent is "what would change if we ran today under today's
    # settings", not "replay the historical configuration".
    _begin_readonly_cli_invocation(action_label="diff against an existing run")

    historical_run = _get_run_or_raise(run_id)

    from ....client import make_audited_client

    client = make_audited_client()
    current = _enumerate_current_classifications(client)
    historical: dict[int, tuple[str, str]] = {
        f.member_id: (f.category, f.action)
        for f in AuditFinding.objects.filter(run=historical_run)
    }
    added, removed, category_changed, action_changed, unchanged = (
        _compute_diff_buckets(current, historical)
    )

    if json_output:
        cmd.stdout.write(
            jsonlib.dumps(
                {
                    "compared_to_run_id": run_id,
                    "added": [
                        {
                            "member_id": uid,
                            "category": current[uid][0],
                            "action": current[uid][1],
                        }
                        for uid in added
                    ],
                    "removed": [
                        {
                            "member_id": uid,
                            "category": historical[uid][0],
                            "action": historical[uid][1],
                        }
                        for uid in removed
                    ],
                    "category_changed": [
                        {"member_id": uid, "from": h, "to": c}
                        for uid, h, c in category_changed
                    ],
                    "action_changed": [
                        {"member_id": uid, "from": h, "to": c}
                        for uid, h, c in action_changed
                    ],
                    "unchanged_count": len(unchanged),
                }
            )
        )
        return

    cmd.stdout.write(f"Comparing current state to AuditRun id={run_id}\n")
    cmd.stdout.write(f"  added: {len(added)}\n")
    for uid in added:
        cat, act = current[uid]
        cmd.stdout.write(f"    + {uid} {cat}/{act}\n")
    cmd.stdout.write(f"  removed: {len(removed)}\n")
    for uid in removed:
        cat, act = historical[uid]
        cmd.stdout.write(f"    - {uid} (was {cat}/{act})\n")
    cmd.stdout.write(f"  category_changed: {len(category_changed)}\n")
    for uid, hist_cat, cur_cat in category_changed:
        cmd.stdout.write(f"    ~ {uid} {hist_cat} -> {cur_cat}\n")
    cmd.stdout.write(f"  action_changed: {len(action_changed)}\n")
    for uid, hist_act, cur_act in action_changed:
        cmd.stdout.write(f"    ~ {uid} action {hist_act} -> {cur_act}\n")
    cmd.stdout.write(f"  unchanged: {len(unchanged)}\n")
