import logging
import re
import uuid
from datetime import datetime, timedelta, timezone
from typing import Final

from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from pydantic import BaseModel, EmailStr, field_validator

from app.core.deps import get_db, get_current_staff
from app.core.auth import verify_pin, create_access_token
from app.core.limiter import limiter
from app.models.user import User
from app.models.mstr_role import MstrRole
from app.models.active_session import ActiveSession
from app.models.patient_lock import PatientLock
from app.core.config import settings
from app.constants import error_codes

logger = logging.getLogger(__name__)

PIN_LENGTH: Final = 6
MAX_FAILED_ATTEMPTS: Final = 5

router = APIRouter()


# ── Models ────────────────────────────────────────────

class LoginRequest(BaseModel):
    email: EmailStr
    pin: str

    @field_validator("pin")
    @classmethod
    def pin_must_be_6_digits(cls, v):
        if not re.match(rf"^\d{{{PIN_LENGTH}}}$", v):
            raise ValueError(f"PIN must be exactly {PIN_LENGTH} numeric digits")
        return v


class LoginResponse(BaseModel):
    accessToken: str
    staff: dict


# ── Routes ──────────────────────────────

@router.post("/login", response_model=LoginResponse)
@limiter.limit("10/minute")
async def login(
    request: Request,
    body: LoginRequest,
    db: AsyncSession = Depends(get_db),
):
    # Step 1 — Find user by email
    result = await db.execute(select(User).where(User.email == body.email))
    user = result.scalar_one_or_none()

    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail={"error": error_codes.INVALID_CREDENTIALS, "message": "Invalid Email or PIN"}
        )

    # Step 2 — Check active
    if not user.is_active:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={"error": error_codes.ACCOUNT_INACTIVE, "message": "Account is deactivated"}
        )

    # Step 3 — Check not locked
    if user.locked_at is not None:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={"error": error_codes.ACCOUNT_LOCKED, "message": "Account locked. Contact admin.", "lockedAt": str(user.locked_at)}
        )

    # Step 4 — Verify PIN
    if not verify_pin(body.pin, user.pin_hash):
        user.failed_pin_attempts += 1
        if user.failed_pin_attempts >= MAX_FAILED_ATTEMPTS:
            user.locked_at = datetime.now(timezone.utc)
            await db.commit()
            logger.warning("Account locked after %d failed attempts: %s", MAX_FAILED_ATTEMPTS, body.email)
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail={"error": error_codes.ACCOUNT_LOCKED, "message": f"Account locked after {MAX_FAILED_ATTEMPTS} failed attempts. Contact admin."}
            )
        await db.commit()
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail={"error": error_codes.INVALID_CREDENTIALS, "message": "Invalid Email or PIN"}
        )

    # Step 5 — Check concurrent session limit
    count_result = await db.execute(
        select(func.count()).select_from(ActiveSession).where(
            ActiveSession.expires_at > datetime.now(timezone.utc)
        )
    )
    active_count = count_result.scalar()
    if active_count >= settings.MAX_CONCURRENT_SESSIONS:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail={"error": error_codes.MAX_SESSIONS_REACHED, "message": "Maximum concurrent sessions reached. Try again later."}
        )

    # Step 6 — Reset failed attempts
    user.failed_pin_attempts = 0
    await db.flush()

    # Step 7 — Upsert active_session slot
    expires_at = datetime.now(timezone.utc) + timedelta(minutes=settings.SESSION_LOCK_TTL_MINUTES)
    existing_slot = await db.execute(
        select(ActiveSession).where(ActiveSession.staff_id == user.id)
    )
    slot = existing_slot.scalar_one_or_none()
    if slot:
        slot.expires_at = expires_at
    else:
        db.add(ActiveSession(
            id=uuid.uuid4(),
            staff_id=user.id,
            session_id=None,
            expires_at=expires_at,
        ))
    await db.commit()

    # Step 8 — Resolve the staff's role_key (embedded in the JWT for the session's lifetime —
    # a later change to the role's role_key won't retroactively affect an already-issued token)
    role_result = await db.execute(select(MstrRole).where(MstrRole.id == user.role_id))
    role = role_result.scalar_one()

    # Step 9 — Issue JWT
    token = create_access_token(
        staff_id=str(user.id),
        role=role.role_key,
        department_id=str(user.department_id),
    )

    logger.info("Login successful: %s", body.email)

    return LoginResponse(
        accessToken=token,
        staff={
            "id": str(user.id),
            "name": user.name,
            "role": role.role_key,
            "email": user.email,
            "department": {"id": str(user.department_id)},
        }
    )


@router.post("/logout")
async def logout(
    db: AsyncSession = Depends(get_db),
    current_staff: dict = Depends(get_current_staff),
):
    staff_id = uuid.UUID(current_staff["sub"])

    # Release active session slot
    slot = await db.execute(
        select(ActiveSession).where(ActiveSession.staff_id == staff_id)
    )
    for row in slot.scalars().all():
        await db.delete(row)

    # Release any patient lock held by this staff
    lock = await db.execute(
        select(PatientLock).where(PatientLock.staff_id == staff_id)
    )
    for lock_row in lock.scalars().all():
        await db.delete(lock_row)

    await db.commit()
    logger.info("Logout: %s", staff_id)
    return {"success": True}
