"""Servicio de códigos de invitación reutilizables."""

import logging
import random
import string
from datetime import datetime, timezone
from typing import List, Optional

import httpx
from fastapi import HTTPException
from sqlalchemy.orm import Session

from core.config import settings
from .dto import (
    ConsumeInviteCodeResponseDTO,
    CreateInviteCodeDTO,
    InviteCodeResponseDTO,
    ValidateInviteCodeResponseDTO,
)
from .models import InviteCode

logger = logging.getLogger(__name__)

# Charset sin caracteres ambiguos (0/O/I/l/1)
_CODE_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
_CODE_LENGTH = 8


def _generate_code() -> str:
    return "".join(random.choices(_CODE_CHARSET, k=_CODE_LENGTH))


def _is_valid(invite: InviteCode) -> bool:
    """True si el código es canjeable en este momento."""
    if not invite.is_active:
        return False
    if invite.max_uses is not None and invite.uses_count >= invite.max_uses:
        return False
    if invite.expires_at and invite.expires_at < datetime.now(timezone.utc):
        return False
    return True


class InviteCodeService:
    def __init__(self, db: Session):
        self.db = db

    async def create_code(
        self,
        data: CreateInviteCodeDTO,
        created_by: str,
        tenant_slug: str,
        user_access_token: str = "",
    ) -> InviteCodeResponseDTO:
        from api.v1.modules.core.tenants.models import Tenant

        tenant = (
            self.db.query(Tenant)
            .filter(Tenant.slug == tenant_slug, Tenant.is_active == True)
            .first()
        )
        if not tenant:
            raise HTTPException(status_code=404, detail="Tenant no encontrado")

        if data.company_id is not None:
            # Implementa la validación de company con tu modelo de compañía.
            company = None
            if not company:
                raise HTTPException(
                    status_code=404,
                    detail="Empresa no encontrada o no pertenece al tenant",
                )

        # Genera código único; reintenta si hay colisión (improbable)
        for _ in range(5):
            code = _generate_code()
            if not self.db.query(InviteCode).filter(InviteCode.code == code).first():
                break
        else:
            raise HTTPException(
                status_code=500,
                detail="No se pudo generar un código único, intenta de nuevo",
            )

        invite = InviteCode(
            code=code,
            tenant_slug=tenant_slug,
            company_id=data.company_id,
            role=data.role,
            max_uses=data.max_uses,
            uses_count=0,
            expires_at=data.expires_at,
            created_by=created_by,
            is_active=True,
        )
        self.db.add(invite)
        self.db.commit()
        self.db.refresh(invite)

        # Registrar el mismo código en el Hub para que funcione en workspace /join
        await self._sync_code_to_hub(
            code=code,
            tenant_slug=tenant_slug,
            data=data,
            user_access_token=user_access_token,
        )

        return InviteCodeResponseDTO.model_validate(invite)

    async def _sync_code_to_hub(
        self,
        code: str,
        tenant_slug: str,
        data: CreateInviteCodeDTO,
        user_access_token: str,
    ) -> None:
        """
        Crea el mismo código en Hub's workspace_invite_codes con allowed_systems=['a76'].
        Best-effort: si falla, el código sigue válido en A76 pero no en workspace.
        """
        if not user_access_token:
            logger.warning(
                "[invite_code] Sin token para sincronizar '%s' al Hub — "
                "el código NO funcionará en workspace /join",
                code,
            )
            return

        hub_url = getattr(settings, "HUB_URL", "").rstrip("/")
        if not hub_url:
            logger.warning("[invite_code] HUB_URL no configurado — código '%s' no sincronizado", code)
            return

        payload: dict = {
            "code": code,
            "allowed_systems": [],
            "role": data.role,
        }
        if data.max_uses is not None:
            payload["max_uses"] = data.max_uses
        if data.expires_at is not None:
            payload["expires_at"] = data.expires_at.isoformat()

        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.post(
                    f"{hub_url}/api/v1/hub/invite-codes/{tenant_slug}",
                    json=payload,
                    headers={"Authorization": f"Bearer {user_access_token}"},
                )
            if resp.status_code in (200, 201):
                logger.info(
                    "[invite_code] Código '%s' sincronizado al Hub (tenant=%s)", code, tenant_slug
                )
            elif resp.status_code == 409:
                logger.info(
                    "[invite_code] Código '%s' ya existe en Hub (tenant=%s) — OK", code, tenant_slug
                )
            else:
                logger.error(
                    "[invite_code] Hub sync falló code='%s' status=%s body=%s",
                    code, resp.status_code, resp.text[:300],
                )
        except Exception as exc:
            logger.error("[invite_code] Hub sync excepción code='%s': %s", code, exc)

    def list_codes(
        self,
        tenant_slug: str,
        company_id: Optional[int] = None,
        include_inactive: bool = False,
    ) -> List[InviteCodeResponseDTO]:
        q = self.db.query(InviteCode).filter(InviteCode.tenant_slug == tenant_slug)

        if company_id is not None:
            q = q.filter(InviteCode.company_id == company_id)

        if not include_inactive:
            q = q.filter(InviteCode.is_active == True)

        invites = q.order_by(InviteCode.created_at.desc()).all()
        return [InviteCodeResponseDTO.model_validate(i) for i in invites]

    def revoke_code(self, code: str, tenant_slug: str) -> None:
        invite = (
            self.db.query(InviteCode)
            .filter(InviteCode.code == code, InviteCode.tenant_slug == tenant_slug)
            .first()
        )
        if not invite:
            raise HTTPException(status_code=404, detail="Código de invitación no encontrado")

        invite.is_active = False
        self.db.commit()

    def validate(self, code: str) -> ValidateInviteCodeResponseDTO:
        """Valida el código sin consumirlo. Devuelve 403 genérico si no es válido."""
        invite = self.db.query(InviteCode).filter(InviteCode.code == code).first()

        if not invite or not _is_valid(invite):
            raise HTTPException(
                status_code=403,
                detail="Código de invitación inválido o expirado",
            )

        remaining: Optional[int] = None
        if invite.max_uses is not None:
            remaining = invite.max_uses - invite.uses_count

        return ValidateInviteCodeResponseDTO(
            code=invite.code,
            tenant_slug=invite.tenant_slug,
            company_id=invite.company_id,
            role=invite.role,
            remaining_uses=remaining,
            expires_at=invite.expires_at,
        )

    def consume(
        self,
        code: str,
        keycloak_user_id: str,
        tenant_id: int,
    ) -> ConsumeInviteCodeResponseDTO:
        """
        Canjea el código:
        - Incrementa uses_count.
        - Si company_id está definido, crea UserTenant (usuario ↔ empresa).
        - Desactiva el código si se agotaron los usos.
        """
        invite = self.db.query(InviteCode).filter(InviteCode.code == code).first()

        if not invite or not _is_valid(invite):
            raise HTTPException(
                status_code=403,
                detail="Código de invitación inválido o expirado",
            )

        if invite.company_id is not None:
            self._ensure_user_tenant(
                keycloak_user_id=keycloak_user_id,
                tenant_id=tenant_id,
                company_id=invite.company_id,
                role=invite.role,
            )

        invite.uses_count += 1
        if invite.max_uses is not None and invite.uses_count >= invite.max_uses:
            invite.is_active = False

        self.db.commit()

        logger.info(
            "[invite_code] canjeado code=%s user=%s company_id=%s uses=%d/%s",
            invite.code,
            keycloak_user_id,
            invite.company_id,
            invite.uses_count,
            invite.max_uses or "∞",
        )

        return ConsumeInviteCodeResponseDTO(
            success=True,
            message="Código canjeado correctamente",
            tenant_slug=invite.tenant_slug,
            company_id=invite.company_id,
            role=invite.role,
        )

    def _ensure_user_tenant(
        self,
        keycloak_user_id: str,
        tenant_id: int,
        company_id: int,
        role: str,
    ) -> None:
        """Crea la fila UserTenant si el usuario aún no tiene acceso a la empresa."""
        from sqlalchemy.exc import IntegrityError
        from api.v1.modules.core.user_tenant.models import UserTenant

        existing = (
            self.db.query(UserTenant)
            .filter(
                UserTenant.keycloak_user_id == keycloak_user_id,
                UserTenant.tenant_id == tenant_id,
                UserTenant.company_id == company_id,
            )
            .first()
        )
        if existing:
            if not existing.is_active:
                existing.is_active = True
                existing.role = role
                self.db.commit()
            return

        user_tenant = UserTenant(
            keycloak_user_id=keycloak_user_id,
            tenant_id=tenant_id,
            company_id=company_id,
            role=role,
            is_active=True,
        )
        self.db.add(user_tenant)
        try:
            self.db.commit()
        except IntegrityError:
            self.db.rollback()
            logger.warning(
                "[invite_code] race: UserTenant ya existe user=%s company=%d",
                keycloak_user_id,
                company_id,
            )
