import logging
import uuid
import re
from typing import Final, Optional, List

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

from app.core.deps import get_db, get_current_staff
from app.core.auth import hash_pin
from app.models.user import User
from app.models.mstr_department import MstrDepartment
from app.models.mstr_role import MstrRole
from app.models.active_session import ActiveSession
from app.models.patient_lock import PatientLock
from app.constants import error_codes
from app.constants.role_keys import ADMIN_ROLE_KEYS

logger = logging.getLogger(__name__)

PIN_LENGTH: Final = 6
MAX_NAME_LENGTH: Final = 100
MAX_EMAIL_LENGTH: Final = 255

router = APIRouter()


# ── Request Models ─────────────────────────────────────────────────────────────

class CreateStaffRequest(BaseModel):
    name: str
    roleId: str
    email: EmailStr
    pin: str
    departmentId: str

    @field_validator("departmentId")
    @classmethod
    def validate_department_id(cls, v):
        try:
            uuid.UUID(v)
        except ValueError:
            raise ValueError("departmentId must be a valid UUID")
        return v

    @field_validator("roleId")
    @classmethod
    def validate_role_id(cls, v):
        try:
            uuid.UUID(v)
        except ValueError:
            raise ValueError("roleId must be a valid UUID")
        return v

    @field_validator("name")
    @classmethod
    def name_not_empty(cls, v):
        if not v.strip():
            raise ValueError("Name cannot be empty")
        if len(v) > MAX_NAME_LENGTH:
            raise ValueError(f"Name cannot exceed {MAX_NAME_LENGTH} characters")
        return v.strip()

    @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 UpdateStaffRequest(BaseModel):
    name: Optional[str] = None
    roleId: Optional[str] = None
    departmentId: Optional[str] = None
    email: Optional[EmailStr] = None
    pin: Optional[str] = None
    isActive: Optional[bool] = None
    unlock: Optional[bool] = None

    @field_validator("name")
    @classmethod
    def name_not_empty(cls, v):
        if v is not None and not v.strip():
            raise ValueError("Name cannot be empty")
        return v.strip() if v else v

    @field_validator("roleId")
    @classmethod
    def validate_role_id(cls, v):
        if v is not None:
            try:
                uuid.UUID(v)
            except ValueError:
                raise ValueError("roleId must be a valid UUID")
        return v

    @field_validator("departmentId")
    @classmethod
    def validate_department_id(cls, v):
        if v is not None:
            try:
                uuid.UUID(v)
            except ValueError:
                raise ValueError("departmentId must be a valid UUID")
        return v

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


# ── Response Models ────────────────────────────────────────────────────────────

class StaffItem(BaseModel):
    id: str
    name: str
    roleId: str
    roleName: str
    roleKey: str
    email: str
    departmentId: str
    isActive: bool
    failedPinAttempts: int
    lockedAt: Optional[str] = None
    createdAt: str
    updatedAt: Optional[str] = None


class StaffListResponse(BaseModel):
    staff: List[StaffItem]


class CreateStaffResponse(BaseModel):
    id: str
    name: str
    roleId: str
    roleName: str
    roleKey: str
    email: str
    departmentId: str
    isActive: bool
    createdAt: str


class UpdateStaffResponse(BaseModel):
    id: str
    name: str
    roleId: str
    roleName: str
    roleKey: str
    email: str
    departmentId: str
    isActive: bool
    failedPinAttempts: int
    lockedAt: Optional[str] = None
    createdAt: str


# ── Helpers ──────────────────────────────────────────────────

async def release_active_session_and_locks(user: User, db: AsyncSession) -> None:
    """Frees up a deactivated staff member's active session slot and any patient locks
    they're holding, so other staff aren't blocked by a now-inactive account."""
    active_result = await db.execute(
        select(ActiveSession).where(ActiveSession.staff_id == user.id)
    )
    active_slot = active_result.scalar_one_or_none()
    if active_slot:
        await db.delete(active_slot)

    lock_result = await db.execute(
        select(PatientLock).where(PatientLock.staff_id == user.id)
    )
    for lock in lock_result.scalars().all():
        await db.delete(lock)


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

@router.post("", status_code=status.HTTP_201_CREATED, response_model=CreateStaffResponse)
async def create_staff(
    request: CreateStaffRequest,
    db: AsyncSession = Depends(get_db),
    current_staff: dict = Depends(get_current_staff),
):
    if current_staff.get("role") not in ADMIN_ROLE_KEYS:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={"error": error_codes.FORBIDDEN, "message": "Only admins can create staff"}
        )
    # Step 1 — Check department exists
    dept_result = await db.execute(
        select(MstrDepartment).where(
            MstrDepartment.id == uuid.UUID(request.departmentId)
        )
    )
    department = dept_result.scalar_one_or_none()
    if not department:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={"error": error_codes.DEPARTMENT_NOT_FOUND, "message": "Department not found"}
        )

    # Step 2 — Check role exists
    role_result = await db.execute(
        select(MstrRole).where(MstrRole.id == uuid.UUID(request.roleId))
    )
    role = role_result.scalar_one_or_none()
    if not role:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={"error": error_codes.ROLE_NOT_FOUND, "message": "Role not found"}
        )

    # Step 3 — Check email is unique
    email_result = await db.execute(
        select(User).where(User.email == request.email)
    )
    existing = email_result.scalar_one_or_none()
    if existing:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={"error": error_codes.EMAIL_ALREADY_EXISTS, "message": "A staff account with this email already exists"}
        )

    # Step 4 — Hash PIN and insert
    new_user = User(
        id=uuid.uuid4(),
        department_id=uuid.UUID(request.departmentId),
        name=request.name,
        email=request.email,
        pin_hash=hash_pin(request.pin),
        role_id=role.id,
        is_active=True,
        failed_pin_attempts=0,
        locked_at=None,
    )
    db.add(new_user)
    await db.commit()
    await db.refresh(new_user)

    return CreateStaffResponse(
        id=str(new_user.id),
        name=new_user.name,
        roleId=str(role.id),
        roleName=role.name,
        roleKey=role.role_key,
        email=new_user.email,
        departmentId=str(new_user.department_id),
        isActive=new_user.is_active,
        createdAt=new_user.created_at.isoformat(),
    )


@router.get("", response_model=StaffListResponse)
async def list_staff(
    sort_by: str = Query("createdAt", pattern="^(name|department|role|status|createdAt)$"),
    order: str = Query("desc", pattern="^(asc|desc)$"),
    db: AsyncSession = Depends(get_db),
    current_staff: dict = Depends(get_current_staff),
):
    if current_staff.get("role") not in ADMIN_ROLE_KEYS:
        raise HTTPException(
            status_code=403,
            detail={"error": error_codes.FORBIDDEN, "message": "Only admins can view staff list"}
        )
    admin_department_id = uuid.UUID(current_staff["departmentId"])
    result = await db.execute(select(User).where(User.department_id == admin_department_id))
    staff_list = list(result.scalars().all())

    dept_result = await db.execute(select(MstrDepartment))
    dept_map: dict[str, str] = {str(d.id): d.name for d in dept_result.scalars().all()}

    role_result = await db.execute(select(MstrRole))
    role_map: dict[str, MstrRole] = {str(r.id): r for r in role_result.scalars().all()}

    def role_name(s: User) -> str:
        role = role_map.get(str(s.role_id))
        return role.name if role else ""

    def status_order(s: User) -> int:
        if s.locked_at is not None:
            return 2
        return 0 if s.is_active else 1

    reverse = order == "desc"
    sort_key_map = {
        "name":       lambda s: (s.name or "").lower(),
        "department": lambda s: dept_map.get(str(s.department_id), "").lower(),
        "role":       lambda s: role_name(s).lower(),
        "status":     lambda s: status_order(s),
        "createdAt":  lambda s: s.created_at,
    }
    staff_list.sort(key=sort_key_map[sort_by], reverse=reverse)

    return StaffListResponse(
        staff=[
            StaffItem(
                id=str(s.id),
                name=s.name,
                roleId=str(s.role_id),
                roleName=role_map[str(s.role_id)].name,
                roleKey=role_map[str(s.role_id)].role_key,
                email=s.email,
                departmentId=str(s.department_id),
                isActive=s.is_active,
                failedPinAttempts=s.failed_pin_attempts,
                lockedAt=s.locked_at.isoformat() if s.locked_at else None,
                createdAt=s.created_at.isoformat(),
            )
            for s in staff_list
        ]
    )


@router.patch("/{staff_id}", response_model=UpdateStaffResponse)
async def update_staff(
    staff_id: uuid.UUID,
    request: UpdateStaffRequest,
    db: AsyncSession = Depends(get_db),
    current_staff: dict = Depends(get_current_staff),
):
    if current_staff.get("role") not in ADMIN_ROLE_KEYS:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={"error": error_codes.FORBIDDEN, "message": "Only admins can update staff accounts"}
        )

    result = await db.execute(
        select(User).where(User.id == staff_id)
    )
    user = result.scalar_one_or_none()

    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={"error": error_codes.STAFF_NOT_FOUND, "message": "Staff account not found"}
        )

    if request.name is not None:
        user.name = request.name

    if request.email is not None:
        email_result = await db.execute(
            select(User).where(User.email == request.email, User.id != staff_id)
        )
        if email_result.scalar_one_or_none():
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail={"error": error_codes.EMAIL_ALREADY_EXISTS, "message": "A staff account with this email already exists"}
            )
        user.email = request.email

    if request.roleId is not None:
        role_result = await db.execute(
            select(MstrRole).where(MstrRole.id == uuid.UUID(request.roleId))
        )
        if not role_result.scalar_one_or_none():
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail={"error": error_codes.ROLE_NOT_FOUND, "message": "Role not found"}
            )
        user.role_id = uuid.UUID(request.roleId)

    if request.departmentId is not None:
        dept_result = await db.execute(
            select(MstrDepartment).where(MstrDepartment.id == uuid.UUID(request.departmentId))
        )
        if not dept_result.scalar_one_or_none():
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail={"error": error_codes.DEPARTMENT_NOT_FOUND, "message": "Department not found"}
            )
        user.department_id = uuid.UUID(request.departmentId)

    if request.pin is not None:
        user.pin_hash = hash_pin(request.pin)
        logger.info("PIN reset for %s", user.email)

    if request.isActive is not None:
        user.is_active = request.isActive
        logger.info("Staff %s isActive set to %s", user.email, request.isActive)

        # On deactivation: release active session slot and patient lock
        if not request.isActive:
            await release_active_session_and_locks(user, db)

    if request.unlock is True:
        user.locked_at = None
        user.failed_pin_attempts = 0
        logger.info("Account unlocked for %s", user.email)

    await db.commit()
    await db.refresh(user)

    role_result = await db.execute(select(MstrRole).where(MstrRole.id == user.role_id))
    role = role_result.scalar_one()

    return UpdateStaffResponse(
        id=str(user.id),
        name=user.name,
        roleId=str(role.id),
        roleName=role.name,
        roleKey=role.role_key,
        email=user.email,
        departmentId=str(user.department_id),
        isActive=user.is_active,
        failedPinAttempts=user.failed_pin_attempts,
        lockedAt=user.locked_at.isoformat() if user.locked_at else None,
        createdAt=user.created_at.isoformat(),
    )
