# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Role & Permission Skill (RBAC)

Granular role-based access control with per-skill and per-tool permissions.
Permissions stored in ~/.familiar/data/rbac.json

Dependencies: None (pure stdlib)
"""

import json
import logging
from datetime import datetime
from pathlib import Path


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


try:
    from familiar.core.utils import atomic_write_json, safe_load_json
except ImportError:
    atomic_write_json = None
    safe_load_json = None

logger = logging.getLogger(__name__)


def _get_data_file():
    """Get data file path (re-evaluates for tenant scoping)."""
    return _get_data_dir() / "rbac.json"


DATA_FILE = _get_data_file()  # Default for backward compat

# Built-in role definitions with default permissions
BUILTIN_ROLES = {
    "admin": {
        "description": "Full access: all skills, all tools, user management",
        "permissions": ["*:*"],
        "inherits": [],
    },
    "editor": {
        "description": "Write access to assigned skills, no user management",
        "permissions": [
            "contacts:*",
            "documents:*",
            "bookkeeping:*",
            "nonprofit:*",
            "meetings:*",
            "knowledge_base:*",
            "tasks:*",
            "reports:*",
            "websearch:*",
            "filereader:*",
            "transcription:*",
            "smart_search:*",
            "services:*",
            "video:*",
            "notifications:send_notification",
        ],
        "inherits": [],
    },
    "staff": {
        "description": "Standard access: most skills, own data",
        "permissions": [
            "contacts:*",
            "documents:*",
            "bookkeeping:*",
            "nonprofit:*",
            "meetings:*",
            "knowledge_base:*",
            "tasks:*",
            "reports:*",
            "websearch:*",
            "filereader:*",
            "transcription:*",
            "smart_search:*",
            "services:*",
            "video:*",
            "notifications:*",
            "workflows:run_workflow",
            "workflows:list_workflows",
            "workflows:workflow_status",
        ],
        "inherits": [],
    },
    "readonly": {
        "description": "View only: read tools, search, no write actions",
        "permissions": [
            "contacts:search_contacts",
            "contacts:get_contact",
            "contacts:list_contacts",
            "nonprofit:search_donors",
            "nonprofit:get_donor",
            "bookkeeping:list_transactions",
            "bookkeeping:financial_summary",
            "bookkeeping:fund_balances",
            "bookkeeping:budget_status",
            "reports:donor_report",
            "reports:grant_report",
            "reports:financial_snapshot",
            "smart_search:*",
            "knowledge_base:search_knowledge",
            "knowledge_base:list_knowledge",
            "tasks:list_tasks",
        ],
        "inherits": [],
    },
    "auditor": {
        "description": "Read-only + audit trail + compliance + encryption status",
        "permissions": [
            "audit:*",
            "phi_detection:scan_for_phi",
            "phi_detection:classification_report",
            "phi_detection:phi_safe_mode",
            "encryption:encryption_status",
            "reports:*",
            "smart_search:*",
        ],
        "inherits": ["readonly"],
    },
    "volunteer": {
        "description": "Limited: contacts and tasks only",
        "permissions": [
            "contacts:search_contacts",
            "contacts:get_contact",
            "contacts:add_contact",
            "contacts:list_contacts",
            "tasks:list_tasks",
            "tasks:add_task",
            "tasks:complete_task",
            "smart_search:quick_lookup",
        ],
        "inherits": [],
    },
}


def _load_data():
    if safe_load_json:
        return safe_load_json(
            _get_data_file(), default=lambda: {"roles": {}, "assignments": {}, "version": 1}
        )
    if _get_data_file().exists():
        try:
            with open(_get_data_file(), encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError):
            pass
    return {"roles": {}, "assignments": {}, "version": 1}


def _save_data(data):
    if atomic_write_json:
        atomic_write_json(_get_data_file(), data)
    else:
        _get_data_file().parent.mkdir(parents=True, exist_ok=True)
        with open(_get_data_file(), "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)


def _get_role_permissions(role_name, _visited=None):
    """Resolve all permissions for a role including inheritance (with cycle detection)."""
    if _visited is None:
        _visited = set()
    if role_name in _visited:
        logger.warning(
            f"Circular role inheritance detected: {role_name} already in chain {_visited}"
        )
        return set()
    _visited.add(role_name)

    # Check custom roles first
    db = _load_data()
    custom_roles = db.get("roles", {})

    role_def = custom_roles.get(role_name) or BUILTIN_ROLES.get(role_name)
    if not role_def:
        return set()

    perms = set(role_def.get("permissions", []))

    # Resolve inheritance (passing visited set to detect cycles)
    for parent_role in role_def.get("inherits", []):
        perms |= _get_role_permissions(parent_role, _visited)

    return perms


def _check_perm(user_perms, skill, tool):
    """Check if a set of permissions allows access to skill:tool."""
    if "*:*" in user_perms:
        return True
    if f"{skill}:*" in user_perms:
        return True
    if f"{skill}:{tool}" in user_perms:
        return True
    return False


# === Tool Handlers ===


def create_role(data):
    """Create a new custom role with specified permissions."""
    name = data.get("name", "").strip().lower()
    description = data.get("description", "").strip()
    permissions = data.get("permissions", [])
    inherits = data.get("inherits", [])

    if not name:
        return "Please provide a role name."
    if not permissions and not inherits:
        return "Please provide permissions (e.g. ['bookkeeping:*', 'tasks:add_task']) or inherits."

    if name in BUILTIN_ROLES:
        return f"Cannot override built-in role: {name}. Create a role with a different name."

    db = _load_data()
    db["roles"][name] = {
        "description": description,
        "permissions": permissions,
        "inherits": inherits,
        "created_at": datetime.now().isoformat(),
    }
    _save_data(db)

    total_perms = _get_role_permissions(name)
    return f"✅ Role created: {name} ({len(total_perms)} effective permissions)"


def assign_role(data):
    """Assign a role to a user."""
    user_id = data.get("user_id", "").strip()
    role = data.get("role", "").strip().lower()

    if not user_id or not role:
        return "Please provide user_id and role."

    all_roles = set(BUILTIN_ROLES.keys())
    db = _load_data()
    all_roles |= set(db.get("roles", {}).keys())

    if role not in all_roles:
        return f"Role not found: {role}. Available: {', '.join(sorted(all_roles))}"

    db.setdefault("assignments", {})[user_id] = {
        "role": role,
        "assigned_at": datetime.now().isoformat(),
        "assigned_by": data.get("assigned_by", "system"),
    }
    _save_data(db)

    perms = _get_role_permissions(role)
    return f"✅ Role '{role}' assigned to user {user_id} ({len(perms)} permissions)"


def check_permission(data):
    """Check if a user has permission for a specific skill:tool."""
    user_id = data.get("user_id", "").strip()
    skill = data.get("skill", "").strip()
    tool = data.get("tool", "").strip()
    role_override = data.get("role", "").strip().lower()

    if not skill or not tool:
        return "Please provide skill and tool to check."

    # Get user's role
    if role_override:
        role = role_override
    elif user_id:
        db = _load_data()
        assignment = db.get("assignments", {}).get(user_id, {})
        role = assignment.get("role", "readonly")
    else:
        return "Please provide user_id or role."

    perms = _get_role_permissions(role)
    allowed = _check_perm(perms, skill, tool)

    if allowed:
        return f"✅ ALLOWED: {role} can access {skill}:{tool}"
    else:
        return f"❌ DENIED: {role} cannot access {skill}:{tool}"


def list_roles(data):
    """List all roles (built-in and custom) with their permissions."""
    db = _load_data()
    show_perms = data.get("show_permissions", False)

    lines = ["🔑 Roles:\n", "  BUILT-IN:"]
    for name, role_def in BUILTIN_ROLES.items():
        perms = _get_role_permissions(name)
        lines.append(f"    {name}: {role_def['description']} ({len(perms)} perms)")
        if show_perms:
            for p in sorted(role_def["permissions"])[:10]:
                lines.append(f"      • {p}")
            if len(role_def["permissions"]) > 10:
                lines.append(f"      ... and {len(role_def['permissions']) - 10} more")

    custom = db.get("roles", {})
    if custom:
        lines.append("\n  CUSTOM:")
        for name, role_def in custom.items():
            perms = _get_role_permissions(name)
            lines.append(f"    {name}: {role_def.get('description', '')} ({len(perms)} perms)")
            if show_perms:
                for p in sorted(role_def.get("permissions", []))[:10]:
                    lines.append(f"      • {p}")

    # Show assignments
    assignments = db.get("assignments", {})
    if assignments:
        lines.append(f"\n  ASSIGNMENTS ({len(assignments)}):")
        for uid, info in assignments.items():
            lines.append(f"    {uid} → {info['role']} (since {info.get('assigned_at', '')[:10]})")

    return "\n".join(lines)


def manage_permissions(data):
    """Add or remove specific permissions from a custom role."""
    role_name = data.get("role", "").strip().lower()
    action = data.get("action", "view").lower()
    permissions = data.get("permissions", [])

    if not role_name:
        return "Please provide a role name."

    if role_name in BUILTIN_ROLES and action != "view":
        return f"Cannot modify built-in role: {role_name}. Create a custom role instead."

    db = _load_data()

    if action == "view":
        perms = _get_role_permissions(role_name)
        if not perms:
            return f"Role not found: {role_name}"
        lines = [f"🔑 Permissions for '{role_name}' ({len(perms)} total):\n"]
        for p in sorted(perms):
            lines.append(f"  • {p}")
        return "\n".join(lines)

    if role_name not in db.get("roles", {}):
        return f"Custom role not found: {role_name}. Only custom roles can be modified."

    role_def = db["roles"][role_name]

    if action == "add":
        if not permissions:
            return "Please provide permissions to add (e.g. ['bookkeeping:log_income'])."
        current = set(role_def.get("permissions", []))
        current |= set(permissions)
        role_def["permissions"] = sorted(current)
        _save_data(db)
        return f"✅ Added {len(permissions)} permission(s) to {role_name}. Total: {len(current)}"

    if action == "remove":
        if not permissions:
            return "Please provide permissions to remove."
        current = set(role_def.get("permissions", []))
        current -= set(permissions)
        role_def["permissions"] = sorted(current)
        _save_data(db)
        return (
            f"✅ Removed {len(permissions)} permission(s) from {role_name}. Total: {len(current)}"
        )

    return "Unknown action. Use: view, add, remove"


# === Tool Definitions ===

TOOLS = [
    {
        "name": "create_role",
        "description": (
            "Create a new custom role with specified skill:tool permissions and optional inheritance from existing roles. "
            "Permissions use the format 'skill:tool', 'skill:*' for all tools in a skill, or '*:*' for full access. "
            "Cannot override built-in roles (admin, editor, staff, readonly, auditor, volunteer). "
            "Returns the total effective permission count including inherited permissions."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Role name (lowercase)"},
                "description": {
                    "type": "string",
                    "description": "Human-readable description of the role's purpose",
                },
                "permissions": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Permission strings: 'skill:tool', 'skill:*', or '*:*'",
                },
                "inherits": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Roles to inherit permissions from",
                },
            },
            "required": ["name"],
        },
        "handler": create_role,
        "category": "rbac",
    },
    {
        "name": "assign_role",
        "description": (
            "Assign a role to a user by their user ID. "
            "The role must be a built-in role (admin, editor, staff, readonly, auditor, volunteer) or a custom role. "
            "Each user can have only one role; assigning a new role replaces the previous one. "
            "Records who made the assignment and when."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "user_id": {"type": "string", "description": "User ID"},
                "role": {"type": "string", "description": "Role name"},
                "assigned_by": {
                    "type": "string",
                    "default": "system",
                    "description": "ID of the user making the assignment",
                },
            },
            "required": ["user_id", "role"],
        },
        "handler": assign_role,
        "category": "rbac",
    },
    {
        "name": "check_permission",
        "description": (
            "Check if a user or role has permission for a specific skill:tool combination. "
            "Provide either a user_id to look up their assigned role, or a role name directly. "
            "Resolves inherited permissions and wildcard patterns. "
            "Returns ALLOWED or DENIED with the effective role name."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "user_id": {"type": "string", "description": "User ID to look up role for"},
                "role": {"type": "string", "description": "Role to check (overrides user lookup)"},
                "skill": {"type": "string", "description": "Skill name"},
                "tool": {"type": "string", "description": "Tool name"},
            },
            "required": ["skill", "tool"],
        },
        "handler": check_permission,
        "category": "rbac",
    },
    {
        "name": "list_roles",
        "description": (
            "List all roles, both built-in and custom, with descriptions and effective permission counts. "
            "Optionally shows individual permissions for each role. "
            "Also displays current user-to-role assignments with assignment dates. "
            "Built-in roles include admin, editor, staff, readonly, auditor, and volunteer."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "show_permissions": {
                    "type": "boolean",
                    "default": False,
                    "description": "Show individual permissions",
                },
            },
        },
        "handler": list_roles,
        "category": "rbac",
    },
    {
        "name": "manage_permissions",
        "description": (
            "View, add, or remove specific permissions from a custom role. "
            "Built-in roles cannot be modified; create a custom role with inheritance instead. "
            "Use 'view' to see all effective permissions, 'add' to grant new ones, or 'remove' to revoke. "
            "Permission strings use the format 'skill:tool' (e.g. 'bookkeeping:log_income')."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "role": {"type": "string", "description": "Role name"},
                "action": {
                    "type": "string",
                    "enum": ["view", "add", "remove"],
                    "default": "view",
                    "description": "Action to perform: view, add, or remove permissions",
                },
                "permissions": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Permissions to add/remove (e.g. 'bookkeeping:log_income')",
                },
            },
            "required": ["role"],
        },
        "handler": manage_permissions,
        "category": "rbac",
    },
]
