"""RBS REGAL — Advanced Role & Permission System.

Beyond the legacy "admin/staff" pair, this gives:
  • A catalog of modules × actions (Vyapar/Marg/Tally style).
  • 5 preset roles (Admin / Manager / Accountant / Cashier / Viewer).
  • Custom roles persisted in `roles` collection.
  • Per-user role assignment with optional fine-grained overrides.
  • `require_permission(perm)` FastAPI dependency factory for endpoint guards.

Permission strings follow `module.action` (e.g. `invoicing.create`, `users.manage`).
Admin role always passes (super-user). Legacy `"staff"` role maps to Cashier preset.
"""
from typing import Optional, List
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from bson import ObjectId
from auth import get_current_user, require_admin

router = APIRouter(prefix="/api/permissions", tags=["permissions"])

# ---- Catalog ------------------------------------------------------------
# Each module declares the actions it supports.  Use this as the single
# source of truth for the front-end matrix UI.
MODULE_CATALOG = {
    "invoicing":   {"label": "Sales & Invoicing", "actions": ["view", "create", "edit", "delete", "void", "print", "export"]},
    "purchases":   {"label": "Purchases",         "actions": ["view", "create", "edit", "delete", "print", "export"]},
    "items":       {"label": "Items / Stock",     "actions": ["view", "create", "edit", "delete", "import", "export", "bulk_update"]},
    "parties":     {"label": "Parties",           "actions": ["view", "create", "edit", "delete", "import", "export"]},
    "payments":    {"label": "Payments",          "actions": ["view", "create", "edit", "delete", "export"]},
    "expenses":    {"label": "Expenses",          "actions": ["view", "create", "edit", "delete"]},
    "cash_bank":   {"label": "Cash & Bank",       "actions": ["view", "create", "edit", "delete", "transfer"]},
    "godowns":     {"label": "Godowns / Warehouse","actions": ["view", "create", "edit", "delete", "transfer"]},
    "accounting":  {"label": "Accounting Reports","actions": ["view", "export"]},
    "gst":         {"label": "GST Filing & Returns","actions": ["view", "export", "file"]},
    "reports":     {"label": "Business Reports",  "actions": ["view", "export"]},
    "ecommerce":   {"label": "E-commerce Sync",   "actions": ["view", "configure", "sync"]},
    "ai":          {"label": "AI Assistant",      "actions": ["view", "use"]},
    "users":       {"label": "User Management",   "actions": ["view", "create", "edit", "delete", "assign_role"]},
    "companies":   {"label": "Companies / Firms", "actions": ["view", "create", "edit", "delete"]},
    "settings":    {"label": "Settings & Branding","actions": ["view", "edit"]},
    "trash":       {"label": "Trash & Restore",   "actions": ["view", "restore", "purge"]},
}


def _all_perms() -> List[str]:
    out = []
    for mod, cfg in MODULE_CATALOG.items():
        for act in cfg["actions"]:
            out.append(f"{mod}.{act}")
    return out


# ---- Preset Roles -------------------------------------------------------
def _viewer_perms() -> List[str]:
    return [f"{m}.view" for m in MODULE_CATALOG.keys() if "view" in MODULE_CATALOG[m]["actions"]] + ["ai.use"]


def _cashier_perms() -> List[str]:
    return _viewer_perms() + [
        "invoicing.create", "invoicing.print",
        "parties.create",
        "payments.create",
        "expenses.create",
    ]


def _accountant_perms() -> List[str]:
    return _cashier_perms() + [
        "invoicing.edit", "invoicing.void", "invoicing.export",
        "payments.edit", "payments.export",
        "expenses.edit", "expenses.delete",
        "cash_bank.create", "cash_bank.edit", "cash_bank.transfer",
        "accounting.export",
        "gst.export", "gst.file",
        "reports.export",
        "items.import", "items.export",
        "parties.import", "parties.export",
    ]


def _manager_perms() -> List[str]:
    out = _accountant_perms() + [
        "invoicing.delete",
        "purchases.create", "purchases.edit", "purchases.delete", "purchases.print", "purchases.export",
        "items.create", "items.edit", "items.bulk_update",
        "parties.edit", "parties.delete",
        "payments.delete",
        "cash_bank.delete",
        "godowns.create", "godowns.edit", "godowns.transfer",
        "ecommerce.configure", "ecommerce.sync",
        "trash.restore",
        "settings.edit",
    ]
    return sorted(set(out))


PRESET_ROLES = {
    "admin":      {"label": "Admin / Owner",  "preset": True, "permissions": ["*"]},  # super
    "manager":    {"label": "Manager",        "preset": True, "permissions": _manager_perms()},
    "accountant": {"label": "Accountant",     "preset": True, "permissions": _accountant_perms()},
    "cashier":    {"label": "Cashier",        "preset": True, "permissions": _cashier_perms()},
    "viewer":     {"label": "Viewer (Read-only)", "preset": True, "permissions": _viewer_perms()},
}

# Legacy "staff" role → behaves like cashier.
LEGACY_ROLE_ALIAS = {"staff": "cashier"}


# ---- Helpers ------------------------------------------------------------
async def _load_role_perms(db, role_name: str) -> List[str]:
    role_name = (role_name or "").lower()
    role_name = LEGACY_ROLE_ALIAS.get(role_name, role_name)
    if role_name in PRESET_ROLES:
        return PRESET_ROLES[role_name]["permissions"]
    doc = await db.roles.find_one({"name": role_name})
    if doc:
        return list(doc.get("permissions") or [])
    return []


async def get_user_permissions(db, user: dict) -> List[str]:
    """Resolve effective permission list for a user."""
    role_perms = await _load_role_perms(db, user.get("role", "staff"))
    # If user has explicit overrides stored on their record, union them in
    overrides = []
    try:
        u = await db.users.find_one({"_id": ObjectId(user["id"])})
        overrides = list((u or {}).get("permission_overrides") or [])
    except Exception:
        pass
    if "*" in role_perms or user.get("role") == "admin":
        return ["*"]
    return sorted(set(role_perms + overrides))


def _matches(perm: str, granted: List[str]) -> bool:
    if "*" in granted:
        return True
    if perm in granted:
        return True
    # Wildcard: "invoicing.*" matches any action
    mod = perm.split(".", 1)[0]
    return f"{mod}.*" in granted


def require_permission(perm: str):
    """Dependency factory — guards routes with a permission string."""
    async def _dep(request: Request, user: dict = Depends(get_current_user)):
        db = request.app.state.db
        granted = await get_user_permissions(db, user)
        if not _matches(perm, granted):
            raise HTTPException(status_code=403, detail=f"Missing permission: {perm}")
        return user
    return _dep


# ---- Endpoints ----------------------------------------------------------
class RoleIn(BaseModel):
    name: str
    label: Optional[str] = None
    permissions: List[str] = []


@router.get("/catalog")
async def get_catalog(user=Depends(get_current_user)):
    """Return modules + actions for the Role Matrix UI."""
    return {
        "modules": [
            {"key": k, "label": v["label"], "actions": v["actions"], "permissions": [f"{k}.{a}" for a in v["actions"]]}
            for k, v in MODULE_CATALOG.items()
        ],
        "all_permissions": _all_perms(),
    }


@router.get("/roles")
async def list_roles(request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    custom = []
    async for d in db.roles.find({}, {"_id": 0, "name": 1, "label": 1, "permissions": 1, "created_at": 1, "updated_at": 1}):
        custom.append({**d, "preset": False})
    presets = [
        {"name": k, "label": v["label"], "preset": True, "permissions": v["permissions"]}
        for k, v in PRESET_ROLES.items()
    ]
    return {"roles": presets + custom}


@router.post("/roles")
async def create_role(payload: RoleIn, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    name = payload.name.strip().lower()
    if not name or " " in name:
        raise HTTPException(400, "Role name must be a single lowercase word (e.g. accountant_lite)")
    if name in PRESET_ROLES:
        raise HTTPException(409, f"'{name}' is a preset role — cannot override")
    if await db.roles.find_one({"name": name}):
        raise HTTPException(409, "Role with this name already exists")
    valid = set(_all_perms())
    perms = [p for p in payload.permissions if p in valid or p.endswith(".*")]
    doc = {
        "name": name,
        "label": payload.label or name.title(),
        "permissions": perms,
        "created_at": datetime.now(timezone.utc).isoformat(),
        "updated_at": datetime.now(timezone.utc).isoformat(),
        "created_by": user["email"],
    }
    await db.roles.insert_one(doc)
    doc.pop("_id", None)
    return {**doc, "preset": False}


@router.put("/roles/{name}")
async def update_role(name: str, payload: RoleIn, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    name = name.lower()
    if name in PRESET_ROLES:
        raise HTTPException(409, f"'{name}' is a preset — clone it under a new name instead")
    existing = await db.roles.find_one({"name": name})
    if not existing:
        raise HTTPException(404, "Role not found")
    valid = set(_all_perms())
    perms = [p for p in payload.permissions if p in valid or p.endswith(".*")]
    await db.roles.update_one(
        {"name": name},
        {"$set": {
            "label": payload.label or existing.get("label", name.title()),
            "permissions": perms,
            "updated_at": datetime.now(timezone.utc).isoformat(),
            "updated_by": user["email"],
        }},
    )
    return {"ok": True}


@router.delete("/roles/{name}")
async def delete_role(name: str, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    name = name.lower()
    if name in PRESET_ROLES:
        raise HTTPException(409, "Preset roles cannot be deleted")
    in_use = await db.users.count_documents({"role": name})
    if in_use > 0:
        raise HTTPException(409, f"Cannot delete — {in_use} user(s) currently assigned this role")
    r = await db.roles.delete_one({"name": name})
    if r.deleted_count == 0:
        raise HTTPException(404, "Role not found")
    return {"ok": True}


@router.get("/me")
async def my_permissions(request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    perms = await get_user_permissions(db, user)
    return {
        "role": user.get("role", "staff"),
        "permissions": perms,
        "is_super": "*" in perms,
    }


class AssignRoleIn(BaseModel):
    role: str


@router.put("/users/{uid}/role")
async def assign_role(uid: str, payload: AssignRoleIn, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    role = payload.role.lower().strip()
    role = LEGACY_ROLE_ALIAS.get(role, role)
    valid_names = set(PRESET_ROLES.keys()) | {d["name"] async for d in db.roles.find({}, {"name": 1})}
    if role not in valid_names:
        raise HTTPException(400, f"Unknown role: {role}")
    if uid == user["id"] and role != "admin":
        raise HTTPException(409, "You cannot demote yourself")
    try:
        oid = ObjectId(uid)
    except Exception:
        raise HTTPException(400, "Invalid user id")
    r = await db.users.update_one({"_id": oid}, {"$set": {"role": role, "updated_at": datetime.now(timezone.utc).isoformat()}})
    if r.matched_count == 0:
        raise HTTPException(404, "User not found")
    return {"ok": True, "role": role}


class OverrideIn(BaseModel):
    permissions: List[str] = []


@router.put("/users/{uid}/overrides")
async def set_overrides(uid: str, payload: OverrideIn, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    valid = set(_all_perms())
    perms = sorted(set(p for p in payload.permissions if p in valid))
    try:
        oid = ObjectId(uid)
    except Exception:
        raise HTTPException(400, "Invalid user id")
    r = await db.users.update_one({"_id": oid}, {"$set": {"permission_overrides": perms}})
    if r.matched_count == 0:
        raise HTTPException(404, "User not found")
    return {"ok": True, "permission_overrides": perms}


@router.get("/users/{uid}/effective")
async def user_effective(uid: str, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    try:
        u = await db.users.find_one({"_id": ObjectId(uid)})
    except Exception:
        raise HTTPException(400, "Invalid user id")
    if not u:
        raise HTTPException(404, "User not found")
    fake = {"id": str(u["_id"]), "role": u.get("role", "staff")}
    perms = await get_user_permissions(db, fake)
    return {
        "id": str(u["_id"]),
        "email": u["email"],
        "name": u.get("name"),
        "role": u.get("role"),
        "permission_overrides": u.get("permission_overrides", []),
        "effective_permissions": perms,
    }
