from datetime import datetime, timezone

from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession

from staff.orm import Staff
from auth.utils import hash_password
from auth.models import default_permissions_for_role
from shared.audit import log_action, diff
from shared.exceptions import NotFoundError, AuthorizationError
import uuid


VALID_ROLES = ("sysadmin", "admin", "editor", "user")


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _to_dict(staff: Staff) -> dict:
    return {
        "id":          staff.id,
        "email":       staff.email,
        "name":        staff.name,
        "role":        staff.role,
        "is_active":   staff.is_active,
        "permissions": staff.permissions,
    }


def _to_response(staff: Staff) -> dict:
    return _to_dict(staff)


async def list_staff(db: AsyncSession, search: str = None, role_filter: str = None) -> dict:
    stmt = select(Staff)
    if role_filter:
        stmt = stmt.where(Staff.role == role_filter)
    if search:
        s = f"%{search.lower()}%"
        stmt = stmt.where(
            or_(
                func.lower(Staff.name).like(s),
                func.lower(Staff.email).like(s),
            )
        )
    stmt = stmt.order_by(Staff.name)
    result = await db.execute(stmt)
    rows = result.scalars().all()
    return {"staff": [_to_response(r) for r in rows], "total": len(rows)}


async def get_staff(db: AsyncSession, staff_id: str) -> dict:
    result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
    staff = result.scalar_one_or_none()
    if staff is None:
        raise NotFoundError("Staff member not found")
    return _to_response(staff)


async def get_staff_me(db: AsyncSession, user_sub: str) -> dict:
    return await get_staff(db, user_sub)


async def create_staff(
    db: AsyncSession,
    data: dict,
    current_user_role: str,
    actor_id: str,
    actor_name: str,
) -> dict:
    role = data.get("role", "user")
    if role not in VALID_ROLES:
        raise AuthorizationError(f"Invalid role: {role}")
    if current_user_role == "admin" and role == "sysadmin":
        raise AuthorizationError("Admin cannot create sysadmin accounts")

    existing = await db.execute(
        select(Staff).where(Staff.email == data["email"]).limit(1)
    )
    if existing.scalar_one_or_none() is not None:
        raise AuthorizationError("A staff member with this email already exists")

    permissions = data.get("permissions")
    if permissions is None and role in ("editor", "user"):
        permissions = default_permissions_for_role(role)

    uid = str(uuid.uuid4())
    now = _now()
    staff = Staff(
        id=uid,
        firestore_id=None,
        email=data["email"],
        name=data["name"],
        role=role,
        hashed_password=hash_password(data["password"]),
        is_active=True,
        permissions=permissions,
        ui_prefs={},
        created_at=now,
        updated_at=now,
    )
    db.add(staff)
    await db.flush()

    await log_action(
        db,
        actor_id=actor_id,
        actor_name=actor_name,
        action="CREATE",
        entity_type="staff",
        entity_id=uid,
        entity_label=data["email"],
        meta={"role": role},
    )
    await db.commit()
    await db.refresh(staff)
    return _to_response(staff)


async def update_staff(
    db: AsyncSession,
    staff_id: str,
    data: dict,
    current_user_role: str,
    current_user_id: str,
    actor_id: str,
    actor_name: str,
) -> dict:
    result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
    staff = result.scalar_one_or_none()
    if staff is None:
        raise NotFoundError("Staff member not found")

    if current_user_role == "admin" and staff.role == "sysadmin":
        raise AuthorizationError("Admin cannot modify sysadmin accounts")
    if current_user_role == "admin" and data.get("role") == "sysadmin":
        raise AuthorizationError("Admin cannot promote to sysadmin")

    old = _to_dict(staff)

    if data.get("email") is not None:
        dup = await db.execute(
            select(Staff).where(Staff.email == data["email"], Staff.id != staff_id).limit(1)
        )
        if dup.scalar_one_or_none() is not None:
            raise AuthorizationError("A staff member with this email already exists")
        staff.email = data["email"]

    if data.get("name") is not None:
        staff.name = data["name"]
    if data.get("role") is not None:
        if data["role"] not in VALID_ROLES:
            raise AuthorizationError(f"Invalid role: {data['role']}")
        staff.role = data["role"]
    if data.get("is_active") is not None:
        staff.is_active = data["is_active"]
    if "permissions" in data:
        staff.permissions = data["permissions"]

    staff.updated_at = _now()
    await db.flush()

    changes = diff(old, _to_dict(staff))
    action = "PERMISSION_CHANGE" if "permissions" in data and len(changes) == 1 else "UPDATE"
    await log_action(
        db,
        actor_id=actor_id,
        actor_name=actor_name,
        action=action,
        entity_type="staff",
        entity_id=staff_id,
        entity_label=staff.email,
        changes=changes or None,
    )
    await db.commit()
    await db.refresh(staff)
    return _to_response(staff)


async def update_staff_password(
    db: AsyncSession,
    staff_id: str,
    new_password: str,
    current_user_role: str,
    actor_id: str,
    actor_name: str,
) -> dict:
    result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
    staff = result.scalar_one_or_none()
    if staff is None:
        raise NotFoundError("Staff member not found")
    if current_user_role == "admin" and staff.role == "sysadmin":
        raise AuthorizationError("Admin cannot modify sysadmin accounts")

    staff.hashed_password = hash_password(new_password)
    staff.updated_at = _now()
    await db.flush()

    await log_action(
        db,
        actor_id=actor_id,
        actor_name=actor_name,
        action="UPDATE",
        entity_type="staff",
        entity_id=staff_id,
        entity_label=staff.email,
        meta={"detail": "password changed"},
    )
    await db.commit()
    return {"message": "Password updated successfully"}


async def get_preferences(db: AsyncSession, staff_id: str) -> dict:
    result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
    staff = result.scalar_one_or_none()
    if staff is None:
        raise NotFoundError("Staff member not found")
    return staff.ui_prefs or {}


async def update_preferences(db: AsyncSession, staff_id: str, page_key: str, prefs: dict) -> dict:
    result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
    staff = result.scalar_one_or_none()
    if staff is None:
        raise NotFoundError("Staff member not found")

    current = dict(staff.ui_prefs or {})
    current[page_key] = {**current.get(page_key, {}), **prefs}
    staff.ui_prefs = current
    staff.updated_at = _now()
    await db.commit()
    return current


async def delete_staff(
    db: AsyncSession,
    staff_id: str,
    current_user_role: str,
    current_user_id: str,
    actor_id: str,
    actor_name: str,
) -> dict:
    if staff_id == current_user_id:
        raise AuthorizationError("Cannot delete your own account")

    result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
    staff = result.scalar_one_or_none()
    if staff is None:
        raise NotFoundError("Staff member not found")

    if current_user_role == "admin" and staff.role == "sysadmin":
        raise AuthorizationError("Admin cannot delete sysadmin accounts")

    label = staff.email
    await log_action(
        db,
        actor_id=actor_id,
        actor_name=actor_name,
        action="DELETE",
        entity_type="staff",
        entity_id=staff_id,
        entity_label=label,
    )
    await db.delete(staff)
    await db.commit()
    return {"message": "Staff member deleted"}
