"""Role management for the admin portal.

This module backs the "Role Management" page under `/admin/cms/roles`. The
primary administrator can invite additional administrators by email and assign
one of three roles:

  - ``super-admin``: full access, including managing other admins.
  - ``admin``: full access to the admin dashboard and CMS.
  - ``support``: read-only / support-scope access (frontend decides gating).

Why this design?
~~~~~~~~~~~~~~~~
The project already reuses WHMCS as the user database and does NOT want a
second auth system. So instead of creating new login credentials, we persist
an *allowlist* of emails-plus-roles in Atoms Cloud (the ``user_roles`` table).

At login time, ``routers/whmcs.py::_admin_email_allowlist()`` reads BOTH the
``ADMIN_EMAILS`` env var AND this table (via
:func:`db_admin_role_map`) to decide whether to mint a JWT with ``role:
"admin"``. Users still sign in with their normal WHMCS credentials — this
table only controls elevation.

Who can call these endpoints?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:func:`require_super_admin` enforces that only users whose JWT carries
``role == "admin"`` AND whose email appears either in ``ADMIN_EMAILS`` or is
stored in this table with role ``super-admin`` can manage roles. The first
admin configured via ``ADMIN_EMAILS`` is therefore the implicit super-admin
and can bootstrap additional admins without editing env variables.
"""

from __future__ import annotations

import logging
import os
from datetime import datetime
from typing import Dict, List, Optional

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

from core.database import get_db
from dependencies.auth import get_current_user
from models.user_roles import User_roles
from schemas.auth import UserResponse

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/admin/roles", tags=["admin-roles"])


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

ALLOWED_ROLES = ("super-admin", "admin", "support")


# ---------------------------------------------------------------------------
# Helpers shared with routers/whmcs.py
# ---------------------------------------------------------------------------

def _env_admin_emails() -> set:
    """Lowercase set of admin emails from ``ADMIN_EMAILS`` / ``ADMIN_EMAIL``.

    Duplicated intentionally from ``routers/whmcs.py`` to avoid a circular
    import when that module calls back into this one.
    """
    raw = os.environ.get("ADMIN_EMAILS") or os.environ.get("ADMIN_EMAIL") or ""
    normalised = raw.replace(";", ",").replace("\n", ",")
    return {p.strip().lower() for p in normalised.split(",") if p.strip()}


async def db_admin_role_map(db: AsyncSession) -> Dict[str, str]:
    """Return ``{email_lowercase: role}`` for every entry persisted in DB.

    Used by the JWT minting logic (``routers/whmcs.py``) to elevate users
    without restarting the backend. Only returns rows whose role is one of
    :data:`ALLOWED_ROLES`; anything else is ignored defensively.
    """
    try:
        result = await db.execute(select(User_roles))
        rows = result.scalars().all()
    except Exception as exc:  # pragma: no cover - table missing on first boot
        logger.warning("db_admin_role_map: could not read user_roles: %s", exc)
        return {}

    mapping: Dict[str, str] = {}
    for row in rows:
        email = (row.target_user_id or "").strip().lower()
        role = (row.role or "").strip().lower()
        if email and role in ALLOWED_ROLES:
            mapping[email] = role
    return mapping


async def resolve_effective_role(db: AsyncSession, email: Optional[str]) -> str:
    """Resolve the effective admin role for a login email.

    Combines the env allowlist (treated as ``super-admin``) with DB entries.
    Env-configured emails always win if both are present, so the platform
    owner can never be locked out by a rogue DB edit.
    """
    if not email:
        return "user"
    key = email.strip().lower()
    if key in _env_admin_emails():
        return "super-admin"
    db_map = await db_admin_role_map(db)
    return db_map.get(key, "user")


# ---------------------------------------------------------------------------
# Permission guard
# ---------------------------------------------------------------------------

async def require_super_admin(
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
) -> UserResponse:
    """Only env-configured admins or DB-stored ``super-admin``s pass.

    A regular ``admin`` or ``support`` user cannot manage roles — that would
    let an admin promote themselves to super-admin. Keeping the guard strict
    protects the primary admin's authority.
    """
    email = (getattr(current_user, "email", "") or "").strip().lower()
    if not email:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Authentication required.",
        )

    if email in _env_admin_emails():
        return current_user

    db_role = (await db_admin_role_map(db)).get(email)
    if db_role == "super-admin":
        return current_user

    raise HTTPException(
        status_code=status.HTTP_403_FORBIDDEN,
        detail="Only the primary administrator can manage roles.",
    )


# ---------------------------------------------------------------------------
# Pydantic schemas
# ---------------------------------------------------------------------------

class RoleAssignment(BaseModel):
    """Response shape for a single role entry."""

    id: Optional[int] = None
    email: str
    role: str
    source: str = Field(
        "db",
        description="Either 'env' (from ADMIN_EMAILS) or 'db' (from user_roles table).",
    )
    invited_by: Optional[str] = None
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None


class RoleListResponse(BaseModel):
    items: List[RoleAssignment]
    total: int


class InviteAdminRequest(BaseModel):
    email: EmailStr
    role: str = Field(..., description="One of: super-admin, admin, support")


class UpdateRoleRequest(BaseModel):
    role: str


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _validate_role(role: str) -> str:
    role_norm = (role or "").strip().lower()
    if role_norm not in ALLOWED_ROLES:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Invalid role. Must be one of: {', '.join(ALLOWED_ROLES)}",
        )
    return role_norm


def _row_to_assignment(row: User_roles) -> RoleAssignment:
    return RoleAssignment(
        id=row.id,
        email=(row.target_user_id or "").strip().lower(),
        role=(row.role or "").strip().lower(),
        source="db",
        invited_by=row.permissions or None,
        created_at=row.created_at,
        updated_at=row.updated_at,
    )


# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------

@router.get("", response_model=RoleListResponse)
async def list_roles(
    _: UserResponse = Depends(require_super_admin),
    db: AsyncSession = Depends(get_db),
):
    """List all admin role assignments (env + DB merged)."""
    items: List[RoleAssignment] = []

    # Env-configured admins are shown first and marked read-only (source=env).
    for email in sorted(_env_admin_emails()):
        items.append(
            RoleAssignment(
                email=email,
                role="super-admin",
                source="env",
                invited_by="ADMIN_EMAILS env",
            )
        )

    # DB-backed admins.
    result = await db.execute(select(User_roles).order_by(User_roles.id.asc()))
    for row in result.scalars().all():
        role = (row.role or "").strip().lower()
        if role not in ALLOWED_ROLES:
            continue
        email = (row.target_user_id or "").strip().lower()
        # Hide DB rows that duplicate env entries (env wins).
        if email in _env_admin_emails():
            continue
        items.append(_row_to_assignment(row))

    return RoleListResponse(items=items, total=len(items))


@router.post("", response_model=RoleAssignment, status_code=status.HTTP_201_CREATED)
async def invite_admin(
    payload: InviteAdminRequest,
    current_user: UserResponse = Depends(require_super_admin),
    db: AsyncSession = Depends(get_db),
):
    """Invite a new admin (or update an existing DB assignment)."""
    role = _validate_role(payload.role)
    email = payload.email.strip().lower()

    if email in _env_admin_emails():
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="This email is already configured via ADMIN_EMAILS and cannot be managed here.",
        )

    # Upsert by email — one role per person.
    existing = await db.execute(
        select(User_roles).where(User_roles.target_user_id == email)
    )
    row = existing.scalar_one_or_none()
    invited_by = (getattr(current_user, "email", "") or "").strip().lower() or None

    if row is None:
        row = User_roles(
            target_user_id=email,
            role=role,
            permissions=invited_by,
        )
        db.add(row)
    else:
        row.role = role
        row.permissions = invited_by

    await db.commit()
    await db.refresh(row)
    logger.info("Role assignment upserted: %s -> %s by %s", email, role, invited_by)
    return _row_to_assignment(row)


@router.patch("/{role_id}", response_model=RoleAssignment)
async def update_role(
    role_id: int,
    payload: UpdateRoleRequest,
    current_user: UserResponse = Depends(require_super_admin),
    db: AsyncSession = Depends(get_db),
):
    """Change the role of an existing DB-backed assignment."""
    role = _validate_role(payload.role)
    result = await db.execute(select(User_roles).where(User_roles.id == role_id))
    row = result.scalar_one_or_none()
    if row is None:
        raise HTTPException(status_code=404, detail="Role assignment not found.")

    row.role = role
    row.permissions = (getattr(current_user, "email", "") or "").strip().lower() or row.permissions
    await db.commit()
    await db.refresh(row)
    return _row_to_assignment(row)


@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_role(
    role_id: int,
    _: UserResponse = Depends(require_super_admin),
    db: AsyncSession = Depends(get_db),
):
    """Revoke an admin role (DB entries only; env entries are immutable here)."""
    result = await db.execute(select(User_roles).where(User_roles.id == role_id))
    row = result.scalar_one_or_none()
    if row is None:
        raise HTTPException(status_code=404, detail="Role assignment not found.")

    await db.execute(delete(User_roles).where(User_roles.id == role_id))
    await db.commit()
    return None