"""``--policy-preview`` subcommand: project a hypothetical policy."""

from __future__ import annotations

import json as jsonlib
from collections import Counter
from typing import TYPE_CHECKING, Final

from django.core.management.base import CommandError

from ....models import OrphanAction, OrphanCategory
from ._invocation import _begin_readonly_cli_invocation

# Single source of truth for the per-row transition glyphs in the text
# renderer. Exposed at module-level so the policy-preview test suite
# can pin the contract (changed-transition shows the changed-glyph,
# unchanged shows the unchanged-glyph) without re-stating the glyph
# literal across both sides — a future ASCII-only policy or glyph
# rename then becomes a single-line change here. The glyphs themselves
# are operator-visible text and should not change without notice.
ARROW_CHANGED: Final[str] = (
    "->"  # ASCII arrow: safe in legacy / non-UTF-8 shells
)
ARROW_UNCHANGED: Final[str] = "="

if TYPE_CHECKING:  # pragma: no cover
    from collections.abc import Iterator

    from ....client import AuditedDiscordClient
    from ..audit_discord_roles import Command


def _parse_preview_policy(raw: str) -> dict[OrphanCategory, OrphanAction]:
    """Parse + validate a JSON policy override into a typed dict."""
    # The CLI accepts a JSON object keyed by ``OrphanCategory`` value
    # whose values are ``OrphanAction`` values. Both sides are
    # validated against the enum so a typo in the policy JSON
    # surfaces as a CommandError rather than a silent default to
    # REPORT inside ``resolve_action``.
    try:
        decoded = jsonlib.loads(raw)
    except jsonlib.JSONDecodeError as exc:
        raise CommandError(
            f"--policy-preview value is not valid JSON: {exc}"
        ) from exc
    if not isinstance(decoded, dict):
        raise CommandError(
            "--policy-preview must be a JSON object "
            "{category_value: action_value}"
        )
    out: dict[OrphanCategory, OrphanAction] = {}
    for key, value in decoded.items():
        try:
            category = OrphanCategory(key)
        except ValueError as exc:
            raise CommandError(
                f"--policy-preview: unknown category {key!r}; "
                f"expected one of "
                f"{sorted(c.value for c in OrphanCategory)}"
            ) from exc
        if not isinstance(value, str):
            raise CommandError(
                f"--policy-preview: category {key!r} maps to a "
                f"non-string value ({value!r}); expected an "
                f"OrphanAction value"
            )
        try:
            action = OrphanAction(value)
        except ValueError as exc:
            raise CommandError(
                f"--policy-preview: unknown action {value!r} for "
                f"category {key!r}; expected one of "
                f"{sorted(a.value for a in OrphanAction)}"
            ) from exc
        out[category] = action
    return out


def _enumerate_categorised_uids(
    client: AuditedDiscordClient,
) -> Iterator[tuple[int, OrphanCategory]]:
    """
    Walk the live guild + classify; yield ``(uid, category)``.

    Thin alias over ``iter_classified_members`` kept for the calling-
    site readability — the policy-preview body iterates this twice
    (once against the live policy, once against the preview), and
    naming the operation locally documents the symmetry with
    ``--diff``'s ``_enumerate_current_classifications``.
    """
    from ....audit import iter_classified_members
    from ....policy import assemble_classifier_context

    yield from iter_classified_members(client, assemble_classifier_context())


def run_policy_preview(
    cmd: Command, raw_policy: str, *, json_output: bool
) -> None:
    """Project a hypothetical policy against the live guild state."""
    # Read-only diagnostic. The current policy on disk is unchanged;
    # ``resolve_action`` is applied twice per finding (once with the
    # operator's overrides, once with the live config) and the per-
    # category bucket counts get rendered alongside each other.
    _begin_readonly_cli_invocation(action_label="preview a policy override")

    preview_policy_typed = _parse_preview_policy(raw_policy)

    from ....app_settings import AA_DISCORD_AUDIT_POLICY
    from ....client import make_audited_client
    from ....policy import policy_from_snapshot, resolve_action

    current_policy = policy_from_snapshot(AA_DISCORD_AUDIT_POLICY)
    client = make_audited_client()

    # Per-(category, current_action, preview_action) tally. ``buckets``
    # is keyed by category for predictable iteration order; each
    # bucket carries member-uid counts under the (current -> preview)
    # action transition.
    buckets: dict[OrphanCategory, Counter[tuple[str, str]]] = {
        c: Counter() for c in OrphanCategory
    }
    for _uid, category in _enumerate_categorised_uids(client):
        current_action = resolve_action(
            category, policy=current_policy, cli_override=None
        )
        preview_action = preview_policy_typed.get(category, current_action)
        key = (current_action.value, preview_action.value)
        buckets[category][key] += 1

    if json_output:
        cmd.stdout.write(
            jsonlib.dumps(
                {
                    "preview_policy": {
                        c.value: a.value
                        for c, a in preview_policy_typed.items()
                    },
                    "buckets": [
                        {
                            "category": category.value,
                            "current_action": current,
                            "preview_action": preview,
                            "count": count,
                        }
                        for category, transitions in buckets.items()
                        for (current, preview), count in transitions.items()
                    ],
                }
            )
        )
        return

    cmd.stdout.write("Policy preview against current guild state\n")
    for category, transitions in buckets.items():
        if not transitions:
            continue
        cmd.stdout.write(f"  {category.value}:\n")
        for (current, preview), count in transitions.items():
            arrow = ARROW_CHANGED if current != preview else ARROW_UNCHANGED
            cmd.stdout.write(
                f"    {current} {arrow} {preview}: {count} member(s)\n"
            )
