"""Endpoints de módulos y credenciales."""

from drf_spectacular.utils import extend_schema
from rest_framework import serializers, status
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.response import Response

from apps.core.api import BaseApiView
from apps.core.errors import NotFound, ValidationFailed
from apps.credentials.models import Credential, CredentialStatus, Module
from apps.credentials.services import shared_resource_account, upload_credential, verify_credential
from apps.credentials.validation import InvalidKeyFile


class ModuleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Module
        fields = ['id', 'code', 'name', 'required_apis', 'required_scopes']


class CredentialSerializer(serializers.ModelSerializer):
    """
    Lo publicable de una credencial.

    Los campos se enumeran uno por uno y `encrypted_key` no está entre ellos.
    Es a propósito: con un serializador que expusiera todo el modelo, agregar
    una columna mañana la publicaría sin que nadie lo decida (principio III).
    """

    project_id = serializers.CharField(source='google_project.project_id', read_only=True)

    class Meta:
        model = Credential
        fields = [
            'id',
            'kind',
            'client_email',
            'project_id',
            'key_fingerprint',
            'private_key_id',
            'status',
            'last_checked_at',
            'last_error_code',
            'last_error_detail',
            'is_active',
            'created_at',
        ]


class CredentialUploadSerializer(serializers.Serializer):
    """Lo que se manda para cargar una credencial."""

    project_id = serializers.CharField()
    key_file = serializers.CharField(
        help_text='Contenido del archivo .json, o el archivo mismo si se envia como multipart.'
    )


class VerificationSerializer(serializers.Serializer):
    ok = serializers.BooleanField()
    status = serializers.CharField()
    error_code = serializers.CharField(allow_blank=True)
    message = serializers.CharField(allow_blank=True)
    hint = serializers.CharField(allow_blank=True)
    properties = serializers.ListField(child=serializers.DictField(), required=False)


class ModuleListView(BaseApiView):
    @extend_schema(responses=ModuleSerializer(many=True), tags=['credentials'])
    def get(self, request):
        return Response(ModuleSerializer(Module.objects.all(), many=True).data)


class CredentialListView(BaseApiView):
    # Acepta tanto el archivo subido como el contenido pegado en JSON: desde un
    # pipeline lo natural es lo segundo, desde la pantalla lo primero.
    parser_classes = [JSONParser, MultiPartParser, FormParser]

    @extend_schema(responses=CredentialSerializer(many=True), tags=['credentials'])
    def get(self, request):
        credentials = Credential.objects.select_related('google_project')
        return Response(CredentialSerializer(credentials, many=True).data)

    @extend_schema(
        request=CredentialUploadSerializer,
        responses={201: CredentialSerializer, 422: None},
        tags=['credentials'],
        description=(
            'Carga una credencial de cuenta de servicio. Enviar una nueva reemplaza a la '
            'anterior sin tocar ninguna fila de dominios.'
        ),
    )
    def post(self, request):
        material = _received_material(request)

        try:
            credential = upload_credential(
                shared_resource_account(request.user),
                project_id=request.data.get('project_id', ''),
                key_file=material,
            )
        except InvalidKeyFile as exc:
            # `reason` es el código del rechazo y viaja además del mensaje: es lo
            # que deja al cliente ramificar sin leer prosa (RT-08).
            raise ValidationFailed(
                exc.message,
                field=exc.field or 'key_file',
                reason=exc.code,
                params=exc.params,
            ) from exc

        return Response(CredentialSerializer(credential).data, status=status.HTTP_201_CREATED)


def _received_material(request) -> bytes:
    """
    Toma el archivo venga como venga, y explica qué falta si no vino.

    Los dos caminos existen porque los dos usos son reales: la pantalla sube un
    archivo, un pipeline pega el contenido. Obligar al pipeline a armar un
    multipart para mandar dos kilobytes de texto sería fricción sin motivo.
    """
    upload_file = request.FILES.get('key_file')
    if upload_file is not None:
        return upload_file.read()

    content = request.data.get('key_file')
    if isinstance(content, dict):
        import json

        return json.dumps(content).encode()
    if isinstance(content, str) and content.strip():
        return content.encode()

    raise ValidationFailed(
        'Falta el archivo de clave. Mandalo como archivo en «key_file» o pegá su contenido '
        'en el mismo campo.',
        field='key_file',
    )


class CredentialDetailView(BaseApiView):
    @extend_schema(responses={204: None}, tags=['credentials'])
    def delete(self, request, credential_id):
        """
        Revoca la credencial. Los dominios y su historial quedan intactos.

        Revocar no borra: la fila queda con su fecha para poder explicar después
        por qué el monitoreo se detuvo a partir de un momento dado.
        """
        credential = _company_credential(credential_id)
        credential.status = CredentialStatus.REVOKED
        credential.is_active = False
        credential.save(update_fields=['status', 'is_active', 'updated_at'])
        return Response(status=status.HTTP_204_NO_CONTENT)


class CredentialVerifyView(BaseApiView):
    @extend_schema(request=None, responses=VerificationSerializer, tags=['credentials'])
    def post(self, request, credential_id):
        """
        Comprueba la credencial contra Google.

        Devuelve 200 aunque la comprobación falle: la llamada se atendió bien y
        su resultado es un dato, no un error de la API. Un 4xx acá haría que un
        cliente trate «tu clave no sirve» como «tu pedido estaba mal armado».
        """
        result = verify_credential(_company_credential(credential_id))
        return Response(VerificationSerializer(result).data)


def _company_credential(credential_id) -> Credential:
    credential = (
        Credential.objects.filter(id=credential_id)
        .select_related('google_project')
        .first()
    )
    if credential is None:
        raise NotFound('Esa credencial no existe en esta compañía.')
    return credential
