import logging
import uuid

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

from app.core.deps import get_db, get_current_staff
from app.models.mstr_role import MstrRole
from app.models.user import User
from app.constants import error_codes
from app.constants.role_keys import ADMIN_ROLE_KEYS, ROLE_KEY_HIERARCHY, get_assignable_role_keys

logger = logging.getLogger(__name__)

router = APIRouter()


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

class CreateRoleRequest(BaseModel):
    name: str
    roleKey: str

    @field_validator("name")
    @classmethod
    def name_must_not_be_empty(cls, v):
        v = v.strip()
        if not v:
            raise ValueError("Role name must not be empty")
        return v

    @field_validator("roleKey")
    @classmethod
    def role_key_must_be_valid(cls, v):
        v = v.strip().upper().replace(" ", "_")
        if v not in ROLE_KEY_HIERARCHY:
            raise ValueError(f"roleKey must be one of {', '.join(ROLE_KEY_HIERARCHY)}")
        return v


class UpdateRoleRequest(BaseModel):
    name: Optional[str] = None
    roleKey: Optional[str] = None

    @field_validator("name")
    @classmethod
    def name_must_not_be_empty(cls, v):
        if v is not None:
            v = v.strip()
            if not v:
                raise ValueError("Role name must not be empty")
        return v

    @field_validator("roleKey")
    @classmethod
    def role_key_must_be_valid(cls, v):
        if v is not None:
            v = v.strip().upper().replace(" ", "_")
            if v not in ROLE_KEY_HIERARCHY:
                raise ValueError(f"roleKey must be one of {', '.join(ROLE_KEY_HIERARCHY)}")
        return v


# ── Serializer ─────────────────────────────────────────────────────────────────

def role_to_dict(role: MstrRole) -> dict:
    return {
        "id": str(role.id),
        "name": role.name,
        "roleKey": role.role_key,
        "createdAt": role.created_at.isoformat(),
    }


def ensure_role_key_assignable(requested_key: str, current_role_key: str) -> None:
    if requested_key not in get_assignable_role_keys(current_role_key):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={
                "error": error_codes.ROLE_KEY_NOT_ALLOWED,
                "message": "You cannot assign this role key",
            },
        )


async def get_role_staff_count(db: AsyncSession, role_id: uuid.UUID) -> int:
    result = await db.execute(
        select(func.count()).select_from(User).where(User.role_id == role_id)
    )
    return result.scalar()


def staff_in_use_message(staff_count: int) -> str:
    staff_word = "staff member is" if staff_count == 1 else "staff members are"
    return f"{staff_count} {staff_word} already on this role. Please reassign them first."


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

@router.get("")
async def list_roles(
    db: AsyncSession = Depends(get_db),
    current_staff: dict = Depends(get_current_staff),
):
    # Only show roles strictly below the viewer's own tier — the same set they're
    # allowed to assign (see get_assignable_role_keys). This keeps SUPER_ADMIN out of
    # everyone's list, including a fellow Super Admin's, and ADMIN out of an Admin's.
    assignable_keys = get_assignable_role_keys(current_staff.get("role", ""))
    query = select(MstrRole).where(MstrRole.role_key.in_(assignable_keys)).order_by(MstrRole.name)

    result = await db.execute(query)
    roles = result.scalars().all()
    return {"roles": [role_to_dict(r) for r in roles]}


@router.post("", status_code=status.HTTP_201_CREATED)
async def create_role(
    body: CreateRoleRequest,
    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 roles"},
        )
    ensure_role_key_assignable(body.roleKey, current_staff.get("role"))

    existing = await db.execute(select(MstrRole).where(MstrRole.name == body.name))
    if existing.scalar_one_or_none():
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={"error": error_codes.ROLE_ALREADY_EXISTS, "message": "Role already exists"},
        )

    role = MstrRole(
        id=uuid.uuid4(),
        name=body.name,
        role_key=body.roleKey,
    )
    db.add(role)
    await db.commit()
    await db.refresh(role)
    logger.info("Role created: %s", role.name)
    return role_to_dict(role)


@router.put("/{role_id}")
async def update_role(
    role_id: str,
    body: UpdateRoleRequest,
    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 roles"},
        )
    if body.roleKey is not None:
        ensure_role_key_assignable(body.roleKey, current_staff.get("role"))

    result = await db.execute(select(MstrRole).where(MstrRole.id == uuid.UUID(role_id)))
    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"},
        )

    if body.name is not None and body.name != role.name:
        existing = await db.execute(select(MstrRole).where(MstrRole.name == body.name))
        if existing.scalar_one_or_none():
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail={"error": error_codes.ROLE_ALREADY_EXISTS, "message": "Role already exists"},
            )
        role.name = body.name
    if body.roleKey is not None:
        role.role_key = body.roleKey

    await db.commit()
    await db.refresh(role)
    logger.info("Role updated: %s", role.name)
    return role_to_dict(role)


@router.get("/{role_id}/staff-count")
async def get_role_staff_count_route(
    role_id: str,
    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 view role usage"},
        )

    result = await db.execute(select(MstrRole).where(MstrRole.id == uuid.UUID(role_id)))
    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"},
        )

    staff_count = await get_role_staff_count(db, role.id)
    return {"staffCount": staff_count}


@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_role(
    role_id: str,
    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 delete roles"},
        )

    result = await db.execute(select(MstrRole).where(MstrRole.id == uuid.UUID(role_id)))
    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"},
        )

    staff_count = await get_role_staff_count(db, role.id)
    if staff_count > 0:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={"error": error_codes.ROLE_IN_USE, "message": staff_in_use_message(staff_count)},
        )

    await db.delete(role)
    await db.commit()
    logger.info("Role deleted: %s", role_id)
