"""
Credenciales de Google, organizadas por módulo.

La indirección `Credential → ModuleCredential → Module` existe para que
incorporar una capacidad nueva —otra API de Google, otro proveedor— sea crear
una fila de catálogo, no migrar el esquema. Con un solo módulo en el primer
corte parece de más; sin ella, el segundo módulo obliga a tocar todas las
tablas que hoy asumen «la credencial» en singular.
"""

from django.conf import settings
from django.db import models

from apps.core.models import BaseModel


class GoogleProject(BaseModel):
    """
    Proyecto de Google Cloud declarado por la cuenta.

    El identificador **no es secreto** —aparece en la consola de Google y en las
    URLs de sus pantallas— así que se guarda en claro. Cifrarlo daría una falsa
    sensación de protección y complicaría mostrarlo, que es justamente para lo
    que sirve: para que el usuario reconozca de qué proyecto le estamos hablando.
    """

    account = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='google_projects'
    )
    project_id = models.CharField(max_length=63)
    display_name = models.CharField(max_length=100, blank=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['account', 'project_id'], name='one_project_per_identifier_and_account'
            )
        ]

    def __str__(self) -> str:
        return self.display_name or self.project_id


class Module(BaseModel):
    """
    Catálogo de capacidades que requieren credencial. Se puebla por migración.

    Las APIs y los permisos que cada módulo necesita viven acá y no en el código
    porque son exactamente lo que hay que mostrarle al usuario durante la guía:
    la pantalla los lee de la fila en vez de repetirlos en un texto que se
    desactualiza en silencio.
    """

    SEARCH_CONSOLE = 'SEARCH_CONSOLE'

    code = models.CharField(max_length=50, unique=True)
    name = models.CharField(max_length=100)
    required_apis = models.JSONField(default=list)
    required_scopes = models.JSONField(default=list)

    def __str__(self) -> str:
        return self.code


class CredentialKind(models.TextChoices):
    SERVICE_ACCOUNT = 'SERVICE_ACCOUNT', 'Service account'
    # Cableado y no implementado: existe para que agregar acceso delegado no
    # obligue a migrar la columna.
    DELEGATED_USER = 'DELEGATED_USER', 'Delegated user'


class CredentialStatus(models.TextChoices):
    UNVERIFIED = 'UNVERIFIED', 'Unverified'
    VERIFIED = 'VERIFIED', 'Verified'
    INVALID = 'INVALID', 'Invalid'
    REVOKED = 'REVOKED', 'Revoked'


class CredentialError(models.TextChoices):
    """
    Motivos de fallo que la interfaz distingue entre sí.

    Son cinco y no uno genérico porque cada uno se resuelve en una pantalla
    distinta de Google: mezclarlos deja al usuario sin saber adónde ir, que es
    exactamente el problema que el producto viene a evitar.

    Las etiquetas son las del panel interno. Lo que lee quien usa el producto
    sale del catálogo del cliente, bajo `credentialError.<CODE>.*`, en su idioma.
    """

    INVALID_KEY = 'INVALID_KEY', 'The key is not valid'
    API_NOT_ENABLED = 'API_NOT_ENABLED', 'The API is not enabled in the project'
    PROJECT_MISMATCH = 'PROJECT_MISMATCH', 'The key does not belong to the declared project'
    NO_PROPERTIES = 'NO_PROPERTIES', 'The service account has no authorized properties'
    PROVIDER_UNAVAILABLE = 'PROVIDER_UNAVAILABLE', 'Google did not answer'


class Credential(BaseModel):
    """
    Clave de cuenta de servicio. El material va cifrado y no sale nunca.

    `client_email` y `key_fingerprint` están en claro a propósito: son lo que hay
    que mostrar para que el usuario autorice la cuenta en Search Console y para
    que reconozca cuál de sus claves es ésta, y ninguno de los dos permite firmar
    nada.
    """

    account = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='credentials'
    )
    google_project = models.ForeignKey(
        GoogleProject, on_delete=models.CASCADE, related_name='credentials'
    )
    kind = models.CharField(
        max_length=20, choices=CredentialKind.choices, default=CredentialKind.SERVICE_ACCOUNT
    )
    client_email = models.EmailField()
    encrypted_key = models.BinaryField()
    key_fingerprint = models.CharField(max_length=64)
    private_key_id = models.CharField(max_length=64, blank=True)
    status = models.CharField(
        max_length=20, choices=CredentialStatus.choices, default=CredentialStatus.UNVERIFIED
    )
    last_checked_at = models.DateTimeField(null=True, blank=True)
    last_error_code = models.CharField(
        max_length=30, choices=CredentialError.choices, blank=True, default=''
    )
    last_error_detail = models.TextField(blank=True, default='')
    #: Propiedades que la última comprobación pudo leer, con su nivel de permiso.
    #:
    #: Se guarda el resultado en vez de volver a pedírselo a Google cada vez que
    #: alguien abre la pantalla: listar sitios gasta cupo, y el cupo es el
    #: recurso escaso del producto. Es una foto fechada por `last_checked_at`, y
    #: la pantalla la muestra siempre junto a esa fecha.
    accessible_properties = models.JSONField(default=list, blank=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self) -> str:
        return f'{self.client_email} ({self.get_status_display()})'

    @property
    def is_usable(self) -> bool:
        """Sólo una credencial activa y comprobada habilita trabajo contra Google."""
        return self.is_active and self.status == CredentialStatus.VERIFIED


class ModuleCredential(BaseModel):
    """
    Asignación de una credencial a un módulo.

    El dominio nunca guarda con qué credencial se lo consulta: la resuelve por
    módulo en el momento de usarla. Así, reemplazar una credencial no toca
    ninguna fila de dominios (FR-012).
    """

    account = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='module_credentials'
    )
    module = models.ForeignKey(Module, on_delete=models.PROTECT, related_name='assignments')
    credential = models.ForeignKey(
        Credential, on_delete=models.CASCADE, related_name='module_assignments'
    )
    is_active = models.BooleanField(default=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['account', 'module'],
                condition=models.Q(is_active=True),
                name='one_active_credential_per_account_and_module',
            )
        ]

    def __str__(self) -> str:
        return f'{self.module.code} → {self.credential.client_email}'
