"""
Endpoints API para autenticación
"""

from core.database import get_core_db
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Query, Response, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session

from .dto import (
    ExchangeCodeRequestDTO,
    LoginChoiceResponseDTO,
    LoginRequestDTO,
    LogoutRequestDTO,
    RefreshTokenRequestDTO,
    RegisterRequestDTO,
    RegisterResponseDTO,
    SetCookieRequestDTO,
    SSOExchangeRequestDTO,
    SwitchTenantRequestDTO,
    TokenResponseDTO,
    UserInfoResponseDTO,
)
from .service import AuthService

import logging
from typing import Optional
from pydantic import BaseModel

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/auth", tags=["Authentication"])
security = HTTPBearer()


@router.get("/register/check")
async def check_register(
    invite_token: str = Query(..., description="Token de invitación"),
    tenant_slug: str = Query(..., description="Slug del tenant"),
    email: str = Query(..., description="Email del usuario invitado"),
    db: Session = Depends(get_core_db),
):
    """
    Valida un token de invitación y verifica si el email ya existe en Keycloak.
    No consume el token. Responde con user_exists y datos básicos del usuario si ya existe.
    """
    from api.v1.modules.core.invites.service import InviteService
    import httpx
    from core.config import settings

    invite_service = InviteService(db)
    # Valida token (lanza 403 si es inválido)
    invite_result = invite_service.validate(invite_token, tenant_slug, email)

    # Intentar verificar si el email ya existe en el Hub usando service account
    user_exists = False
    user_info: dict = {}
    if settings.HUB_ADMIN_EMAIL and settings.HUB_ADMIN_PASSWORD:
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                # Login con service account
                login_resp = await client.post(
                    f"{settings.HUB_URL}api/v1/auth/login",
                    json={"username": settings.HUB_ADMIN_EMAIL, "password": settings.HUB_ADMIN_PASSWORD},
                )
                if login_resp.status_code == 200:
                    svc_token = login_resp.json().get("access_token", "")
                    if svc_token:
                        # Buscar admin por email
                        admins_resp = await client.get(
                            f"{settings.HUB_URL}api/v1/hub/admins",
                            params={"email": email},
                            headers={"Authorization": f"Bearer {svc_token}"},
                        )
                        if admins_resp.status_code == 200:
                            admins = admins_resp.json()
                            if isinstance(admins, list):
                                matches = [a for a in admins if a.get("email", "").lower() == email.lower()]
                            elif isinstance(admins, dict) and "items" in admins:
                                matches = [a for a in admins["items"] if a.get("email", "").lower() == email.lower()]
                            else:
                                matches = []
                            if matches:
                                user_exists = True
                                a = matches[0]
                                user_info = {
                                    "username": a.get("username", ""),
                                    "first_name": a.get("first_name", ""),
                                    "last_name": a.get("last_name", ""),
                                }
        except Exception as exc:
            import logging
            logging.getLogger(__name__).warning("register/check Hub lookup failed: %s", exc)

    return {
        "email": invite_result.email,
        "role": invite_result.role,
        "user_exists": user_exists,
        **user_info,
    }


@router.post("/register", response_model=RegisterResponseDTO, status_code=201)
async def register(
    register_data: RegisterRequestDTO, db: Session = Depends(get_core_db)
):
    """
    Registra un nuevo usuario en Keycloak

    El usuario debe proporcionar:
    - username: Nombre de usuario único
    - email: Email único
    - password: Contraseña (mínimo 8 caracteres)
    - first_name: Nombre
    - last_name: Apellido
    - tenant_slug: Slug del tenant al que pertenece

    El usuario se crea automáticamente en Keycloak con:
    - Cuenta habilitada
    - Rol 'user' asignado por defecto
    - Atributos de tenant
    """
    service = AuthService(db)
    return await service.register(register_data)


@router.post("/login", response_model=None)
async def login(
    login_data: LoginRequestDTO, 
    request: Request, # Inject Request
    db: Session = Depends(get_core_db)
):
    """
    Autentica usuario con Keycloak y retorna tokens JWT
    
    El usuario debe proporcionar:
    - username: Usuario o email
    - password: Contraseña
    - tenant_slug: Slug del tenant al que pertenece
    """
    service = AuthService(db)
    import logging
    logger = logging.getLogger(__name__)
    return await service.login(
        login_data=login_data,
        ip_address=request.client.host,
        user_agent=request.headers.get("user-agent")
    )


@router.post("/switch-tenant", response_model=TokenResponseDTO)
async def switch_tenant(
    data: SwitchTenantRequestDTO,
    db: Session = Depends(get_core_db),
    credentials: HTTPAuthorizationCredentials = Depends(security),
):
    """
    Cambia el tenant activo de un usuario ya autenticado y retorna nuevos tokens JWT.

    Requiere:
    - Authorization: Bearer <access_token>  (para identificar al usuario)
    - Body: { tenant_slug, refresh_token }
    """
    service = AuthService(db)
    # Obtener info del usuario desde el access token actual
    user_info = await service.get_user_info(credentials.credentials)

    keycloak_user_id = user_info.sub
    # El realm se puede inferir del token; usamos el campo tenant_id para buscar el realm actual,
    # pero lo más directo es dejar que Keycloak lo resuelva usando la config global.
    # Todos los tenants comparten el mismo realm en esta arquitectura.
    from api.v1.modules.core.tenants.models import Tenant
    from core.database import get_core_db as _gcdb
    # Obtener el realm del tenant destino (o default)
    tenant = db.query(Tenant).filter(Tenant.slug == data.tenant_slug, Tenant.is_active).first()
    if not tenant:
        raise HTTPException(status_code=403, detail="Access denied")

    return await service.switch_tenant(
        keycloak_user_id=keycloak_user_id,
        keycloak_realm=tenant.keycloak_realm,
        tenant_slug=data.tenant_slug,
        refresh_token=data.refresh_token,
    )


@router.post("/refresh", response_model=TokenResponseDTO)
async def refresh_token(
    refresh_data: RefreshTokenRequestDTO, db: Session = Depends(get_core_db)
):
    """
    Refresca el access token usando el refresh token
    """
    service = AuthService(db)
    return await service.refresh_token(refresh_data)


@router.get("/me", response_model=UserInfoResponseDTO)
async def get_current_user_info(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    db: Session = Depends(get_core_db),
):
    """
    Obtiene información del usuario actual desde el token
    """
    service = AuthService(db)
    return await service.get_user_info(credentials.credentials)


@router.post("/lazy-link", status_code=200)
async def lazy_link(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    db: Session = Depends(get_core_db),
):
    """
    Vincula un invite pendiente al usuario autenticado (lazy-link).
    Se llama después de un SSO login desde el workspace para crear el UserTenant
    si hay un invite_token pendiente para el email del usuario.
    """
    service = AuthService(db)
    try:
        await service._link_pending_invite(
            credentials.credentials,  # username_or_email = token (fallback)
            access_token=credentials.credentials,
        )
    except Exception:
        pass
    try:
        claims = service._decode_kc_user_from_token(credentials.credentials)
        service._backfill_company_roles(claims.get("sub", ""))
    except Exception:
        pass
    return {"ok": True}


@router.post("/logout")
async def logout(
    logout_data: LogoutRequestDTO,
    request: Request, # Inject request for IP/User-Agent
    db: Session = Depends(get_core_db),
    # Make current_user optional to avoid 401 on expired tokens
    # We will try to use it if available, otherwise use DTO
    # Note: Depends(get_current_user) raises HTTPException if invalid, so we cannot make it optional easily without changing dependency.
    # Instead, we will rely on DTO username since user explicitly asked for this simplified flow.
    # But if we want to support both, we can't use strict dependency here if we expect it to work on expired tokens.
    # So we remove the strict dependency for now as per "simplified" request.
):
    """
    Cierra sesión invalidando el refresh token
    """
    # Extract info for logging (optional, but harmless to keep providing context if needed, 
    # but strictly speaking we can revert to just calling service)
    # The original file likely didn't have IP extraction here unless I added it. 
    # I'll keep it simple.

    service = AuthService(db)
    return await service.logout(logout_data)


@router.post("/exchange-code", response_model=TokenResponseDTO)
async def exchange_code(
    exchange_data: ExchangeCodeRequestDTO, db: Session = Depends(get_core_db)
):
    """
    Intercambia un authorization code de OAuth2 por tokens

    Este endpoint es útil cuando el frontend usa el flujo de autorización
    con proveedores externos (Microsoft, Google, etc.) a través de Keycloak.

    El código se obtiene después de que el usuario se autentica con el proveedor
    externo y Keycloak lo redirige al frontend con el código en los query params.
    """
    service = AuthService(db)
    return await service.exchange_code(exchange_data)


@router.post("/set-cookie")
async def set_cookie(
    cookie_data: SetCookieRequestDTO,
    response: Response,
    db: Session = Depends(get_core_db),
):
    """
    Establece cookies HttpOnly con los tokens de autenticación

    Este endpoint se llama desde el frontend después de una autenticación
    SSO exitosa para establecer las cookies de sesión necesarias para
    la validación server-side en los layouts protegidos.

    Las cookies se configuran como:
    - HttpOnly: No accesibles desde JavaScript (mayor seguridad)
    - Secure: Solo se envían por HTTPS (en producción)
    - SameSite=Lax: Protección contra CSRF
    - Max-Age: Tiempo de vida del token
    """
    # Validar que los tokens sean válidos decodificándolos
    service = AuthService(db)
    try:
        # Validar el access token
        user_info = await service.get_user_info(cookie_data.access_token)

        # Establecer las cookies
        # Access token cookie
        response.set_cookie(
            key="access_token",
            value=cookie_data.access_token,
            httponly=True,  # No accesible desde JavaScript
            secure=False,  # TODO: Cambiar a True en producción con HTTPS
            samesite="lax",  # Protección CSRF
            max_age=3600,  # 1 hora (ajustar según configuración del token)
            path="/",
        )

        # Refresh token cookie
        response.set_cookie(
            key="refresh_token",
            value=cookie_data.refresh_token,
            httponly=True,
            secure=False,  # TODO: Cambiar a True en producción con HTTPS
            samesite="lax",
            max_age=86400,  # 24 horas (ajustar según configuración del token)
            path="/",
        )

        return {
            "success": True,
            "message": "Cookies establecidas correctamente",
            "user": user_info,
        }

    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Error validando tokens: {str(e)}")


@router.post("/sso-exchange", response_model=TokenResponseDTO)
async def sso_exchange(
    body: SSOExchangeRequestDTO,
    response: Response,
    db: Session = Depends(get_core_db),
):
    """
    Canjea un relay token de un solo uso (generado por el Hub) por KC tokens.
    Llamado server-side desde la página /auth/sso del frontend.
    Establece cookies HttpOnly con los tokens y devuelve el resultado.
    """
    service = AuthService(db)
    tokens = await service.sso_exchange(body.relay_token)

    _is_prod = False  # TODO: leer de settings.ENVIRONMENT == "production"
    response.set_cookie(
        key="access_token",
        value=tokens.access_token,
        httponly=True,
        secure=_is_prod,
        samesite="lax",
        max_age=3600,
        path="/",
    )
    response.set_cookie(
        key="refresh_token",
        value=tokens.refresh_token,
        httponly=True,
        secure=_is_prod,
        samesite="lax",
        max_age=86400,
        path="/",
    )
    return tokens


# ---------------------------------------------------------------------------
# Dev-only local auth — solo disponible cuando DEV_LOCAL_AUTH=True
# ---------------------------------------------------------------------------
@router.post("/dev-login")
async def dev_login():
    """
    Genera un token local firmado con SECRET_KEY para desarrollo sin Keycloak/Hub.
    Disponible únicamente cuando DEV_LOCAL_AUTH=True en el entorno.
    """
    from datetime import datetime, timezone, timedelta
    from jose import jwt as jose_jwt
    from core.config import settings

    if not settings.DEV_LOCAL_AUTH:
        raise HTTPException(status_code=404, detail="Not found")

    now = datetime.now(timezone.utc)
    payload = {
        "sub": "dev-local-user",
        "email": settings.DEV_LOCAL_AUTH_EMAIL,
        "preferred_username": settings.DEV_LOCAL_AUTH_EMAIL.split("@")[0],
        "name": settings.DEV_LOCAL_AUTH_NAME,
        "tenant_id": settings.DEV_LOCAL_AUTH_TENANT_ID,
        "tenant_slug": "dev",
        "company_id": settings.DEV_LOCAL_AUTH_COMPANY_ID,
        "roles": ["super_admin"],
        "permissions": [],
        "allowed_systems": ["fixed_asset", "inventory"],
        "dev_local": True,
        "iat": now,
        "exp": now + timedelta(hours=8),
    }
    token = jose_jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
    return {"access_token": token, "token_type": "bearer"}


@router.get("/my-companies")
async def get_my_companies(
    current_user: dict = Depends(get_current_user),
    db: Session = Depends(get_core_db),
):
    """
    Retorna las compañías accesibles para el usuario actual.

    Modelo del CRM: una compañía por tenant (1:1) — el ``company_id`` coincide con
    el ``tenant_id``. Cada agente de carga (tenant) opera como una empresa. Se
    garantiza el vínculo usuario↔tenant↔company; los permisos de la empresa se
    resuelven en ``/permissions/me`` (bootstrap de super_admin al primer usuario).

    En dev-local retorna una compañía ficticia para que el dashboard funcione.
    """
    from core.config import settings

    if settings.DEV_LOCAL_AUTH and current_user.get("dev_local"):
        return [{
            "id": settings.DEV_LOCAL_AUTH_COMPANY_ID,
            "name": "Empresa Dev Local",
            "tenant_id": settings.DEV_LOCAL_AUTH_TENANT_ID,
            "rfc": None,
            "logo": None,
            "is_active": True,
        }]

    from core.security import (
        resolve_effective_tenant_id_from_user,
        _ensure_user_tenant_for_company,
    )
    from api.v1.modules.core.tenants.models import Tenant
    from sqlalchemy import text

    user_id = current_user.get("sub") or current_user.get("id")
    tenant_id = resolve_effective_tenant_id_from_user(current_user)

    # 1) Usuario CON tenant en el token (flujo normal): autocrea una compañía por
    #    defecto en el primer acceso y AUTO-LIGA al usuario a TODAS las compañías de
    #    su tenant. Así cualquier usuario del mismo tenant (misma organización del
    #    Workspace) entra y ve la(s) compañía(s) sin gestión manual. El ROL no se
    #    asigna aquí: es solo membresía; los permisos se otorgan aparte (un admin
    #    asigna el rol; el primer usuario recibe super_admin vía /permissions/me).
    if tenant_id:
        tenant_id = int(tenant_id)
        company_ids = [
            int(r[0])
            for r in db.execute(
                text("SELECT id FROM a76.company WHERE tenant_id = :tid ORDER BY id"),
                {"tid": tenant_id},
            ).fetchall()
        ]
        if not company_ids:
            tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
            default_name = (
                (tenant.name if tenant else None)
                or current_user.get("tenant_slug")
                or "Mi empresa"
            )
            created = db.execute(
                text("INSERT INTO a76.company (tenant_id, name) VALUES (:tid, :name) RETURNING id"),
                {"tid": tenant_id, "name": default_name},
            ).fetchone()
            db.execute(text("SELECT setval('a76.company_id_seq', (SELECT MAX(id) FROM a76.company))"))
            db.commit()
            company_ids = [int(created[0])]
            logger.info("Compañía por defecto creada para tenant=%s: id=%s", tenant_id, created[0])

        # Auto-ligado por tenant (solo membresía, sin rol).
        if user_id:
            for cid in company_ids:
                try:
                    _ensure_user_tenant_for_company(db, str(user_id), tenant_id, cid)
                except Exception as exc:
                    logger.warning("auto-ligado de compañía %s falló (no bloquea): %s", cid, exc)

    # 2) Compañías por MEMBRESÍA (user_tenants ∪ user_company_roles) → funciona
    #    también para hub_admin sin tenant en el token: verá las compañías que creó
    #    o a las que fue asignado. La membresía la determina el CRM, no el Hub.
    if not user_id:
        return []
    rows = db.execute(
        text(
            """
            SELECT c.id, c.name, c.rfc, c.logo, c.tenant_id, t.name, t.slug
            FROM a76.company c
            LEFT JOIN core.tenants t ON t.id = c.tenant_id
            WHERE c.id IN (
                SELECT company_id FROM core.user_tenants
                    WHERE keycloak_user_id = :uid AND is_active AND company_id IS NOT NULL
                UNION
                SELECT company_id FROM core.user_company_roles
                    WHERE user_id = :uid AND is_active
            )
            ORDER BY c.id
            """
        ),
        {"uid": str(user_id)},
    ).fetchall()

    return [
        {
            "id": int(r[0]),
            "name": r[1] or "Empresa",
            "tenant_id": int(r[4]),
            "tenant_name": r[5],
            "tenant_slug": r[6],
            "rfc": r[2],
            "logo": r[3],
            "is_active": True,
        }
        for r in rows
    ]


class _CreateCompanyDTO(BaseModel):
    name: str
    tenant_id: int
    rfc: Optional[str] = None


async def _sync_tenants_from_hub(request: Request, db: Session) -> None:
    """
    Auto-sync Workspace→CRM: trae los tenants del Workspace (Hub GET /hub/tenants) y
    los da de alta/actualiza en core.tenants con su MISMO ID del Workspace. Así los
    tenants creados en el Workspace aparecen solos en el CRM para asignarles compañías.
    Best-effort: usa el token KC de la sesión (valkey); si no está fresco o el Hub no
    responde, no bloquea (se devuelven los tenants ya sincronizados).
    """
    import httpx
    from sqlalchemy import text as _text
    from core.config import settings
    from core import session_store
    from api.v1.modules.core.tenants.models import Tenant, TenantType

    sid = request.cookies.get("crm_sid") if request else None
    kc_token = None
    if sid:
        sess = session_store.get_session(sid)
        kc_token = (sess or {}).get("access_token")
    if not kc_token:
        return

    try:
        async with httpx.AsyncClient(timeout=8.0) as client:
            r = await client.get(
                f"{settings.HUB_URL}api/v1/hub/tenants",
                headers={"Authorization": f"Bearer {kc_token}"},
            )
        if r.status_code != 200:
            logger.info("sync-tenants: Hub devolvió %s — sin sincronizar", r.status_code)
            return
        payload = r.json()
        items = payload.get("tenants", []) if isinstance(payload, dict) else (payload or [])
        for t in items:
            tid = t.get("id")
            if tid is None:
                continue
            name = t.get("name") or t.get("display_name") or t.get("slug")
            slug = t.get("slug") or f"tenant-{tid}"
            existing = db.query(Tenant).filter(Tenant.id == int(tid)).first()
            if existing:
                if name and existing.name != name:
                    existing.name = name
            else:
                db.add(Tenant(
                    id=int(tid), name=name or slug, slug=slug,
                    keycloak_realm=slug, type=TenantType.SHARED, is_active=True,
                ))
        db.commit()
        db.execute(_text("SELECT setval('core.tenants_id_seq', (SELECT MAX(id) FROM core.tenants))"))
        db.commit()
    except Exception as exc:
        logger.warning("sync-tenants desde Hub falló (no bloquea): %s", exc)
        try:
            db.rollback()
        except Exception:
            pass


@router.get("/assignable-tenants")
async def assignable_tenants(
    request: Request,
    current_user: dict = Depends(get_current_user),
    db: Session = Depends(get_core_db),
):
    """
    Tenants disponibles para asignar una compañía. El tenant lo crea el Workspace;
    aquí solo se elige. hub_admin ve TODOS (auto-sincronizados del Hub); un usuario
    con tenant ve el suyo.
    """
    from api.v1.modules.core.tenants.models import Tenant
    from core.security import resolve_effective_tenant_id_from_user, is_hub_admin

    if is_hub_admin(current_user):
        # Sincroniza automáticamente los tenants del Workspace antes de listar.
        await _sync_tenants_from_hub(request, db)
        rows = db.query(Tenant).filter(Tenant.is_active == True).order_by(Tenant.id).all()  # noqa: E712
        return [{"id": t.id, "name": t.name, "slug": t.slug} for t in rows]

    tid = resolve_effective_tenant_id_from_user(current_user)
    if tid:
        t = db.query(Tenant).filter(Tenant.id == int(tid), Tenant.is_active == True).first()  # noqa: E712
        return [{"id": t.id, "name": t.name, "slug": t.slug}] if t else []
    return []


@router.post("/companies", status_code=201)
async def create_company(
    data: _CreateCompanyDTO,
    current_user: dict = Depends(get_current_user),
    db: Session = Depends(get_core_db),
):
    """
    Da de alta una compañía (a76.company) bajo un tenant del Workspace y asigna al
    usuario como miembro. hub_admin puede crear en cualquier tenant; un usuario con
    tenant solo en el suyo. El rol super_admin se otorga al seleccionarla (/permissions/me).
    """
    from sqlalchemy import text as _text
    from api.v1.modules.core.tenants.models import Tenant
    from core.security import (
        resolve_effective_tenant_id_from_user,
        is_hub_admin,
        _ensure_user_tenant_for_company,
    )

    name = (data.name or "").strip()
    if len(name) < 2:
        raise HTTPException(status_code=422, detail="El nombre de la compañía es obligatorio.")

    tid = int(data.tenant_id)
    tenant = db.query(Tenant).filter(Tenant.id == tid, Tenant.is_active == True).first()  # noqa: E712
    if not tenant:
        raise HTTPException(status_code=404, detail="Tenant no encontrado.")

    # Autorización: hub_admin (atestado por el Hub) puede crear en cualquier tenant;
    # un usuario ligado a un tenant, solo en el suyo.
    if not is_hub_admin(current_user):
        own = resolve_effective_tenant_id_from_user(current_user)
        if own is None or int(own) != tid:
            raise HTTPException(status_code=403, detail="No puedes crear compañías en ese tenant.")

    created = db.execute(
        _text("INSERT INTO a76.company (tenant_id, name, rfc) VALUES (:t, :n, :r) RETURNING id"),
        {"t": tid, "n": name, "r": (data.rfc or None)},
    ).fetchone()
    db.execute(_text("SELECT setval('a76.company_id_seq', (SELECT MAX(id) FROM a76.company))"))
    db.commit()
    cid = int(created[0])

    user_id = current_user.get("sub") or current_user.get("id")
    if user_id:
        try:
            _ensure_user_tenant_for_company(db, str(user_id), tid, cid)
        except Exception as exc:
            logger.warning("create_company: no se pudo asegurar membresía (no bloquea): %s", exc)

    return {"id": cid, "name": name, "tenant_id": tid, "rfc": data.rfc, "logo": None, "is_active": True}
