"""
Alta y rotacion de claves de API desde la linea de comandos.

    python manage.py create_api_key --account "Ventanilla Notarial"
    python manage.py create_api_key --account "Cobranza" --scopes sms:send
    python manage.py create_api_key --set-scopes <prefijo> mail:send messages:read
    python manage.py create_api_key --rotate <prefijo>
    python manage.py create_api_key --revoke <prefijo>

La clave se imprime una sola vez. No queda guardada en claro en ninguna parte:
si se pierde, hay que rotarla.
"""

from __future__ import annotations

from django.core.management.base import BaseCommand, CommandError

from authentication.models.account import Account
from keys.models.api_keys import DEFAULT_SCOPES, SCOPES, ApiKeys


class Command(BaseCommand):
    help = "Emite, rota o revoca una clave de API."

    def add_arguments(self, parser):
        parser.add_argument(
            "--account",
            help="Nombre de la cuenta a la que se emite la clave.",
        )
        parser.add_argument(
            "--rotate",
            help="Prefijo de una clave existente a la que generar un secreto nuevo.",
        )
        parser.add_argument(
            "--revoke",
            help="Prefijo de una clave que dejara de funcionar de inmediato.",
        )
        parser.add_argument(
            "--inactive",
            action="store_true",
            help="Crear la clave desactivada, para activarla despues.",
        )
        parser.add_argument(
            "--scopes",
            nargs="+",
            metavar="ALCANCE",
            help=(
                "Operaciones que autoriza la clave. Por defecto, todas: "
                f"{' '.join(DEFAULT_SCOPES)}"
            ),
        )
        parser.add_argument(
            "--allowed-senders",
            nargs="+",
            metavar="REMITENTE",
            help=(
                "Direcciones o dominios desde los que puede enviar correo. "
                "Si no se indica, se aplica la politica global."
            ),
        )
        parser.add_argument(
            "--set-scopes",
            nargs="+",
            metavar=("PREFIJO", "ALCANCE"),
            help="Cambia los alcances de una clave existente.",
        )

    def handle(self, *args, **options):
        if options["rotate"]:
            return self._rotar(options["rotate"])
        if options["revoke"]:
            return self._revocar(options["revoke"])
        if options["set_scopes"]:
            prefijo, *alcances = options["set_scopes"]
            return self._cambiar_alcances(prefijo, alcances)
        if not options["account"]:
            raise CommandError(
                "Indica --account, --rotate, --revoke o --set-scopes."
            )
        return self._emitir(
            options["account"],
            activa=not options["inactive"],
            scopes=self._validar(options["scopes"]),
            allowed_senders=options["allowed_senders"],
        )

    # -- Operaciones --------------------------------------------------------

    def _emitir(
        self,
        nombre_cuenta: str,
        *,
        activa: bool,
        scopes: list[str] | None = None,
        allowed_senders: list[str] | None = None,
    ):
        cuenta = Account.objects.filter(name=nombre_cuenta).first()
        if cuenta is None:
            disponibles = ", ".join(
                Account.objects.values_list("name", flat=True)[:20]
            )
            raise CommandError(
                f"No existe la cuenta '{nombre_cuenta}'. "
                f"Cuentas disponibles: {disponibles or 'ninguna'}"
            )

        registro, clave = ApiKeys.issue(
            cuenta,
            active=activa,
            scopes=scopes,
            allowed_senders=allowed_senders,
        )

        self.stdout.write(self.style.SUCCESS("\nClave de API creada.\n"))
        self.stdout.write(f"  Cuenta    : {cuenta.name}")
        self.stdout.write(f"  Prefijo   : {registro.key_prefix}")
        self.stdout.write(f"  Activa    : {'sí' if registro.active else 'no'}")
        self.stdout.write(f"  Alcances  : {', '.join(registro.scopes)}")
        if registro.allowed_senders:
            self.stdout.write(
                f"  Remitentes: {', '.join(registro.allowed_senders)}"
            )
        self._mostrar_clave(clave)

    def _rotar(self, prefijo: str):
        registro = self._buscar(prefijo)
        clave = registro.rotate()
        self.stdout.write(
            self.style.SUCCESS(f"\nClave rotada para «{registro.account.name}».\n")
        )
        self._mostrar_clave(clave)
        self.stdout.write(
            "\n  La clave anterior ha dejado de funcionar en este momento.\n"
        )

    def _revocar(self, prefijo: str):
        registro = self._buscar(prefijo)
        registro.revoke()
        self.stdout.write(
            self.style.SUCCESS(
                f"\nClave de «{registro.account.name}» revocada. Deja de funcionar "
                "en la siguiente petición.\n"
            )
        )

    def _cambiar_alcances(self, prefijo: str, alcances: list[str]):
        registro = self._buscar(prefijo)
        registro.scopes = self._validar(alcances) or []
        registro.save(update_fields=["scopes"])
        self.stdout.write(
            self.style.SUCCESS(
                f"\nAlcances de «{registro.account.name}» "
                f"({registro.key_prefix[:8]}…): {', '.join(registro.scopes) or 'ninguno'}\n"
            )
        )
        if not registro.scopes:
            self.stdout.write(
                self.style.WARNING(
                    "  Sin alcances, la clave autentica pero no puede hacer nada.\n"
                )
            )

    # -- Ayudas -------------------------------------------------------------

    def _validar(self, alcances: list[str] | None) -> list[str] | None:
        """
        Rechaza los alcances que no existen.

        Sin esto, un `--scopes mail:enviar` se guardaria tal cual y la clave
        quedaria sin permisos por una errata, con un 403 dificil de explicar.
        """
        if alcances is None:
            return None
        desconocidos = [a for a in alcances if a not in SCOPES]
        if desconocidos:
            raise CommandError(
                f"Alcance desconocido: {', '.join(desconocidos)}. "
                f"Validos: {', '.join(SCOPES)}"
            )
        return alcances

    def _buscar(self, prefijo: str) -> ApiKeys:
        registro = ApiKeys.objects.filter(key_prefix__startswith=prefijo).first()
        if registro is None:
            raise CommandError(f"No hay ninguna clave con el prefijo '{prefijo}'.")
        return registro

    def _mostrar_clave(self, clave: str) -> None:
        self.stdout.write(
            self.style.WARNING("\n  Clave (no se vuelve a mostrar):\n")
        )
        self.stdout.write(f"    {clave}\n")
