"""``--from-fixture`` subcommand: build_plan against a JSON fixture."""

from __future__ import annotations

import json as jsonlib
from pathlib import Path
from typing import TYPE_CHECKING, cast

from django.core.management.base import CommandError

from ....models import RunTrigger
from ._fixture_client import _FixtureClient
from ._invocation import (
    _begin_cli_invocation,
    _link_invocation,
    _rate_limit_or_record,
)

if TYPE_CHECKING:  # pragma: no cover
    from ....client import AuditedDiscordClient
    from ..audit_discord_roles import Command


def _resolve_fixture_path(path: str) -> Path:
    """
    Validate ``path`` is a readable ``.json`` regular file; return resolved.

    ``--from-fixture`` is operator-trusted (gated by ``run_audit``
    perm + shell access), so a hostile path here is not a
    privilege-escalation vector — but a typo like
    ``--from-fixture /etc/passwd`` would otherwise crash deep in
    classifier code with a confusing TypeError. The suffix check +
    ``Path.resolve()`` + ``is_file()`` short-circuit turns operator-
    error into a single-line ``CommandError`` instead.
    """
    fixture_path = Path(path)
    if fixture_path.suffix != ".json":
        raise CommandError(f"fixture {path!r} must have a .json suffix")
    try:
        fixture_path = fixture_path.resolve(strict=True)
    except (OSError, RuntimeError) as exc:
        raise CommandError(
            f"failed to resolve fixture path {path!r}: {exc}"
        ) from exc
    if not fixture_path.is_file():
        raise CommandError(f"fixture {path!r} is not a regular file")
    return fixture_path


def run_from_fixture(cmd: Command, path: str, *, json_output: bool) -> None:
    """Run build_plan against a JSON fixture; skip apply_plan."""
    # Repro entry point. The fixture-shaped client is fed into
    # the same build_plan code path real audits use, so a bug
    # in classification reproduces against the snapshot the
    # operator attached to the ticket. apply_plan is skipped:
    # the fixture is offline data, so applying would have
    # nothing to act on, and a future maintainer extending
    # apply_plan must not be tripped up by a "synthetic" run
    # making real Discord API calls.
    user, argv = _begin_cli_invocation(action_label="run audit from fixture")
    invocation = _rate_limit_or_record(user, argv)

    fixture_path = _resolve_fixture_path(path)

    try:
        with fixture_path.open(encoding="utf-8") as fp:
            data = jsonlib.load(fp)
    except OSError as exc:
        raise CommandError(f"failed to open fixture {path!r}: {exc}") from exc
    except jsonlib.JSONDecodeError as exc:
        raise CommandError(
            f"fixture {path!r} is not valid JSON: {exc}"
        ) from exc

    if not isinstance(data, dict):
        raise CommandError(f"fixture {path!r} root must be a JSON object")
    roles = data.get("guild_roles")
    members_pages = data.get("guild_members")
    if not isinstance(roles, list) or not isinstance(members_pages, list):
        raise CommandError(
            f"fixture {path!r} must define 'guild_roles' (list) "
            f"and 'guild_members' (list of pages)"
        )

    # _FixtureClient's __init__ coerces each role/member dict to the
    # shape build_plan expects (``int(r["id"])``, ``str(r["name"])``);
    # a malformed entry (``{"id": null}``, missing key, …) raises
    # TypeError / ValueError / KeyError. Surface those as a one-line
    # CommandError so the operator UX matches the JSONDecodeError /
    # OSError branches above instead of bubbling a raw traceback.
    try:
        client = _FixtureClient(roles=roles, members_pages=members_pages)
    except (TypeError, ValueError, KeyError) as exc:
        raise CommandError(
            f"fixture {path!r} structural error: {exc}"
        ) from exc

    from ....audit import build_plan

    # Two-step cast through ``object`` because ``_FixtureClient`` is a
    # structural subset of ``AuditedDiscordClient`` (only the methods
    # ``build_plan`` reaches for); pyright refuses the direct narrowing
    # and asks for the explicit ``object`` round-trip.
    plan = build_plan(
        cast("AuditedDiscordClient", cast("object", client)),
        triggered_by=RunTrigger.CLI,
        triggered_by_user=user,
        cli_argv=argv,
    )
    _link_invocation(invocation, plan.run_id)

    if json_output:
        cmd.stdout.write(
            jsonlib.dumps(
                {
                    "run_id": plan.run_id,
                    "findings_count": len(plan.findings),
                    "fixture": path,
                    "applied": False,
                }
            )
        )
        return
    cmd.stdout.write(
        cmd.style.SUCCESS(
            f"AuditRun id={plan.run_id} created from fixture "
            f"{path!r} with {len(plan.findings)} finding(s); "
            f"apply_plan skipped."
        )
    )
