"""Cliente Python del API FaceID v1.0.0 (EPUNEMI) — kit de integración v2.

Úsalo SOLO en tu backend: la API key es un secreto de servidor y nunca debe
llegar al navegador ni a una app móvil.

    pip install requests

    import os
    from faceid_client import FaceIDClient, FaceIDError

    faceid = FaceIDClient(api_key=os.environ['FACEID_API_KEY'])
    try:
        r = faceid.verificar(foto_bytes, cod_number='12345')
        print(r['is_same_person'], r['similarity_score'])
    except FaceIDError as e:
        if e.no_coincide:
            ...  # el rostro no es el del usuario (e.similarity_score / e.distance si vienen)
        else:
            ...  # sin rostro, sin registro, key inválida (e.status == 403), red...

Todas las respuestas del API tienen la forma:
    {"isSuccess": bool, "message": str, "aData": {...}}
Los errores de negocio llegan con HTTP 200 e isSuccess=false; una key inválida o
una empresa desactivada responden HTTP 403. Este cliente convierte ambos casos en
FaceIDError para que nunca se confundan con un éxito.

Por defecto el cliente elimina `face_embedding` (vector biométrico) de las
respuestas: es un dato personal sensible que tu sistema no necesita. Usa
incluir_embedding=True sólo si sabes por qué lo necesitas.
"""
import os
import re

import requests

FACEID_URL = os.environ.get('FACEID_URL', 'https://faceapi.epunemi.gob.ec')
API_BASE = '/api/faceid/v1.0.0/biometric/'

_RE_DISTANCIA = re.compile(r'Distancia:\s*([0-9.eE+-]+).*?similitud:\s*([0-9.eE+-]+)', re.IGNORECASE)


class FaceIDError(Exception):
    """Error devuelto por FaceID (isSuccess=false, HTTP != 200 o fallo de red).

    Atributos:
        message: texto en español apto para mostrar al usuario.
        status: código HTTP (None si no hubo respuesta). 403 = key inválida o empresa desactivada.
        respuesta: JSON completo devuelto por FaceID (o {}).
        no_coincide: True si el rostro no coincide con el registro (verificación 1:1).
        distance / similarity_score: valores del intento fallido cuando FaceID los informa.
    """

    def __init__(self, message, status=None, respuesta=None):
        super().__init__(message)
        self.message = message
        self.status = status
        self.respuesta = respuesta or {}
        # Código estable del error (NO_COINCIDE, SIN_ROSTRO, SIN_REGISTRO, VARIOS_ROSTROS, CALIDAD_IMAGEN…)
        self.code = ((self.respuesta or {}).get('errors') or {}).get('code')
        self.no_coincide = self.code == 'NO_COINCIDE' if self.code else 'no coinciden' in (message or '')
        self.distance = self.similarity_score = None
        m = _RE_DISTANCIA.search(message or '')
        if m:
            try:
                self.distance, self.similarity_score = float(m.group(1)), float(m.group(2))
            except ValueError:
                pass

    @property
    def status_code(self):
        """Alias de `status` (compatibilidad con el kit v1)."""
        return self.status


def _sin_embedding(valor):
    """Quita recursivamente la clave face_embedding de dicts/listas."""
    if isinstance(valor, dict):
        return {k: _sin_embedding(v) for k, v in valor.items() if k != 'face_embedding'}
    if isinstance(valor, list):
        return [_sin_embedding(v) for v in valor]
    return valor


class FaceIDClient:
    """Cliente del API FaceID.

    Args:
        api_key: token de la empresa entregado por el administrador de FaceID
            (se envía como `Authorization: Bearer <api_key>`).
        base_url: URL de FaceID (por defecto FACEID_URL o https://faceapi.epunemi.gob.ec).
        secret_key: clave administrativa (header `X-Secret-Key`), sólo para
            datos_biometricos(), resetear() y eliminar().
        timeout: segundos máximos por petición. La primera verificación de cada
            worker puede tardar más (carga del modelo): 60 s recomendado.
        incluir_embedding: si es False (defecto) se quita `face_embedding` de las respuestas.
    """

    def __init__(self, api_key, base_url=None, secret_key=None, timeout=60, incluir_embedding=False):
        if not api_key:
            raise ValueError('Falta la API key de FaceID')
        self.base_url = (base_url or FACEID_URL).rstrip('/') + API_BASE
        self.timeout = timeout
        self.incluir_embedding = incluir_embedding
        self._secret_key = secret_key
        self.session = requests.Session()
        self.session.headers['Authorization'] = f'Bearer {api_key}'
        self.session.headers['Accept'] = 'application/json'

    # ------------------------------------------------------------------ util
    def _request(self, method, accion, admin=False, **kwargs):
        headers = kwargs.pop('headers', {}) or {}
        if admin:
            if not self._secret_key:
                raise FaceIDError('Esta operación requiere la clave administrativa (secret_key).')
            headers['X-Secret-Key'] = self._secret_key
        try:
            resp = self.session.request(method, self.base_url + accion, timeout=self.timeout,
                                        headers=headers, **kwargs)
        except requests.Timeout as ex:
            raise FaceIDError('FaceID no respondió a tiempo. Intenta nuevamente.') from ex
        except requests.RequestException as ex:
            raise FaceIDError(f'No se pudo conectar con FaceID: {ex.__class__.__name__}') from ex
        try:
            data = resp.json()
        except ValueError:
            raise FaceIDError(f'Respuesta inválida de FaceID (HTTP {resp.status_code})', resp.status_code)
        if not isinstance(data, dict):
            raise FaceIDError(f'Respuesta inválida de FaceID (HTTP {resp.status_code})', resp.status_code)
        if not self.incluir_embedding:
            data = _sin_embedding(data)
        if resp.status_code != 200 or not data.get('isSuccess'):
            raise FaceIDError(data.get('message') or f'Error HTTP {resp.status_code}', resp.status_code, data)
        return data

    @staticmethod
    def _foto(foto, nombre='captura.jpg'):
        """Acepta bytes, un archivo abierto, un UploadedFile de Django o un UploadFile de FastAPI (ya leído)."""
        if hasattr(foto, 'read'):
            foto = foto.read()
        if not foto:
            raise FaceIDError('No se recibió la foto.')
        return (nombre, foto, 'image/jpeg')

    # ------------------------------------------------------------ endpoints
    def enrolar(self, foto, cod_number, username='', nombre='', apellido='', email='', dni_number='',
                permite_actualizar=False):
        """Registra (o actualiza) el rostro de un usuario. POST save/

        La foto debe tener exactamente un rostro, buena luz y mínimo 200x200 px.
        Si el usuario ya tiene registro activo y permite_actualizar=False → FaceIDError
        ("Ya se encuentra registrado su información biométrica").

        Returns:
            dict: respuesta completa ({'isSuccess': True, 'message': ..., 'aData': {...}}).
        """
        datos = {'cod_number': str(cod_number), 'username': username, 'nombre': nombre,
                 'apellido': apellido, 'email': email, 'dni_number': dni_number}
        if permite_actualizar:
            # Importante: el API trata cualquier valor presente como "sí"; para
            # NO permitir actualizar, el campo no se envía.
            datos['permite_actualizar'] = 'true'
        return self._request('POST', 'save/', data=datos, files={'face_photo': self._foto(foto)})

    def verificar(self, foto, cod_number='', username='', email=''):
        """Verificación 1:1: ¿la foto es del usuario indicado? POST verify_face/

        Se busca el registro activo por cod_number (o username, o email, en ese orden).

        Returns:
            dict aData: is_same_person (True), similarity_score (1 - distancia),
            distance, threshold (0.50 por defecto), model ('ArcFace'), biometric
            (datos del registro) y liveness si está activo en el servidor.

        Raises:
            FaceIDError: si el rostro no coincide (e.no_coincide=True), no hay rostro,
            no existe registro, la key es inválida (e.status=403), etc.
        """
        if not (cod_number or username or email):
            raise ValueError('Indica cod_number, username o email')
        datos = {'cod_number': str(cod_number) if cod_number else '', 'username': username, 'email': email}
        return self._request('POST', 'verify_face/', data=datos,
                             files={'face_photo_file': self._foto(foto)}).get('aData', {})

    def identificar(self, foto):
        """Identificación 1:N: ¿a quién de mi empresa pertenece este rostro? POST identify/

        Returns:
            dict aData: found, ambiguous, criterio ('umbral' | 'margen' | None), distance,
            similarity_score, threshold (0.40), margin (distancia del 2º menos la del 1º),
            empresa, candidates_checked, candidates_without_embedding y biometric
            (None si found=False). found=False NO es error: la persona no está registrada
            o el resultado fue ambiguo.
        """
        return self._request('POST', 'identify/', files={'face_photo_file': self._foto(foto)}).get('aData', {})

    def registros(self, username='', email=''):
        """Registros biométricos activos de un usuario + periodo activo. GET load_faces/

        Returns:
            dict: respuesta completa; los registros vienen en 'aaData' y el resumen
            (data_count, aPeriod) en 'aData'.
        """
        if not (username or email):
            raise ValueError('Indica username o email')
        return self._request('GET', 'load_faces/', params={'username': username, 'email': email})

    def obtener(self, id_registro):
        """Un registro biométrico activo de tu empresa por id. GET load/

        Returns:
            dict aData: id, username, nombre, apellido, email, cod_number, dni_number,
            face_photo, is_verify, date_verify…
        """
        return self._request('GET', 'load/', params={'id': id_registro}).get('aData', {})

    def datos_biometricos(self, username='', email=''):
        """Registro activo más reciente + ruta de la foto (requiere secret_key). GET biometric_data/"""
        if not (username or email):
            raise ValueError('Indica username o email')
        return self._request('GET', 'biometric_data/', admin=True,
                             params={'username': username, 'email': email}).get('aData', {})

    def comparar_urls(self, face_photo_url, url_comparar):
        """Compara dos imágenes publicadas por URL. POST verify_face_public/

        Sólo funciona con hosts permitidos por FaceID (ALLOWED_IMAGE_URL_HOSTS);
        prefiere verificar() con la foto en el cuerpo.
        """
        return self._request('POST', 'verify_face_public/',
                             data={'face_photo_url': face_photo_url, 'url_comparar': url_comparar}).get('aData', {})

    def resetear(self, usernames):
        """Desactiva los registros activos de una lista de usuarios (requiere secret_key). POST reset/

        Returns:
            list: registros desactivados.
        """
        return self._request('POST', 'reset/', admin=True, json={'usernames': list(usernames)}).get('aData', {})

    def eliminar(self, id_registro):
        """Elimina físicamente un registro (requiere secret_key). DELETE delete/"""
        return self._request('DELETE', 'delete/', admin=True, params={'id': id_registro})


if __name__ == '__main__':
    # Prueba rápida (sólo lectura):
    #   FACEID_API_KEY=xxx python faceid_client.py foto.jpg 12345
    import sys
    if len(sys.argv) < 3:
        sys.exit('Uso: FACEID_API_KEY=xxx python faceid_client.py foto.jpg <cod_number>')
    cliente = FaceIDClient(api_key=os.environ['FACEID_API_KEY'])
    with open(sys.argv[1], 'rb') as f:
        foto = f.read()
    try:
        r = cliente.verificar(foto, cod_number=sys.argv[2])
        print(f"Verificado: similitud {r['similarity_score'] * 100:.1f} % (umbral de distancia {r['threshold']})")
    except FaceIDError as e:
        print('No verificado:', e.message)
