# #region rbac_permission_catalog [C:2] [TYPE Module] [SEMANTICS rbac, permission, discovery, route, sync]
#
# @BRIEF Discovers declared RBAC permissions from API routes/plugins and synchronizes them into auth database.

from collections.abc import Iterable
from functools import lru_cache
from pathlib import Path
import re

from sqlalchemy.orm import Session

from ..core.logger import belief_scope, logger
from ..models.auth import Permission

# #region HAS_PERMISSION_PATTERN [TYPE Constant]
# @BRIEF Regex pattern for extracting has_permission("resource", "ACTION") declarations.
HAS_PERMISSION_PATTERN = re.compile(
    r"""has_permission\(\s*['"]([^'"]+)['"]\s*,\s*['"]([A-Z]+)['"]\s*\)"""
)
# #endregion HAS_PERMISSION_PATTERN

# #region ROUTES_DIR [TYPE Constant]
# @BRIEF Absolute directory path where API route RBAC declarations are defined.
ROUTES_DIR = Path(__file__).resolve().parent.parent / "api" / "routes"
# #endregion ROUTES_DIR


# #region _iter_route_files [C:4] [TYPE Function]
# @BRIEF Iterates API route files that may contain RBAC declarations.
# @PRE  ROUTES_DIR points to backend/src/api/routes.
# @POST  Yields Python files excluding test and cache directories.
def _iter_route_files() -> Iterable[Path]:
    with belief_scope("rbac_permission_catalog._iter_route_files"):
        if not ROUTES_DIR.exists():
            return []

        files = []
        for file_path in ROUTES_DIR.rglob("*.py"):
            path_parts = set(file_path.parts)
            if "__tests__" in path_parts or "__pycache__" in path_parts:
                continue
            files.append(file_path)
        return files
# #endregion _iter_route_files


# #region _discover_route_permissions [C:4] [TYPE Function]
# @BRIEF Extracts explicit has_permission declarations from API route source code.
# @PRE  Route files are readable UTF-8 text files.
# @POST  Returns unique set of (resource, action) pairs declared in route guards.
def _discover_route_permissions() -> set[tuple[str, str]]:
    with belief_scope("rbac_permission_catalog._discover_route_permissions"):
        discovered: set[tuple[str, str]] = set()
        for route_file in _iter_route_files():
            try:
                source = route_file.read_text(encoding="utf-8")
            except OSError as read_error:
                logger.explore(
                    "Failed to read route file",
                    extra={"src": "rbac_permission_catalog", "payload": {"file": str(route_file)}, "error": str(read_error)},
                )
                continue

            for resource, action in HAS_PERMISSION_PATTERN.findall(source):
                normalized_resource = str(resource or "").strip()
                normalized_action = str(action or "").strip().upper()
                if normalized_resource and normalized_action:
                    discovered.add((normalized_resource, normalized_action))
        return discovered
# #endregion _discover_route_permissions


# #region _discover_route_permissions_cached [C:4] [TYPE Function]
# @BRIEF Cache route permission discovery because route source files are static during normal runtime.
# @PRE  None.
# @POST  Returns stable discovered route permission pairs without repeated filesystem scans.
@lru_cache(maxsize=1)
def _discover_route_permissions_cached() -> tuple[tuple[str, str], ...]:
    with belief_scope("rbac_permission_catalog._discover_route_permissions_cached"):
        return tuple(sorted(_discover_route_permissions()))
# #endregion _discover_route_permissions_cached


# #region _discover_plugin_execute_permissions [C:4] [TYPE Function]
# @BRIEF Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry.
# @PRE  plugin_loader is optional and may expose get_all_plugin_configs.
# @POST  Returns unique plugin EXECUTE permissions if loader is available.
def _discover_plugin_execute_permissions(plugin_loader=None) -> set[tuple[str, str]]:
    with belief_scope("rbac_permission_catalog._discover_plugin_execute_permissions"):
        discovered: set[tuple[str, str]] = set()
        if plugin_loader is None:
            return discovered

        try:
            plugin_configs = plugin_loader.get_all_plugin_configs()
        except Exception as plugin_error:
            logger.explore(
                "Failed to read plugin configs for RBAC discovery",
                extra={"src": "rbac_permission_catalog", "error": str(plugin_error)},
            )
            return discovered

        for plugin_config in plugin_configs:
            plugin_id = str(getattr(plugin_config, "id", "") or "").strip()
            if plugin_id:
                discovered.add((f"plugin:{plugin_id}", "EXECUTE"))
        return discovered
# #endregion _discover_plugin_execute_permissions


# #region _discover_plugin_execute_permissions_cached [C:4] [TYPE Function]
# @BRIEF Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple.
# @PRE  plugin_ids is a deterministic tuple of plugin ids.
# @POST  Returns stable permission tuple without repeated plugin catalog expansion.
@lru_cache(maxsize=8)
def _discover_plugin_execute_permissions_cached(
    plugin_ids: tuple[str, ...],
) -> tuple[tuple[str, str], ...]:
    with belief_scope("rbac_permission_catalog._discover_plugin_execute_permissions_cached"):
        return tuple((f"plugin:{plugin_id}", "EXECUTE") for plugin_id in plugin_ids)
# #endregion _discover_plugin_execute_permissions_cached


# #region discover_declared_permissions [C:4] [TYPE Function]
# @BRIEF Builds canonical RBAC permission catalog from routes and plugin registry.
# @PRE  plugin_loader may be provided for dynamic task plugin permission discovery.
# @POST  Returns union of route-declared and dynamic plugin EXECUTE permissions.
def discover_declared_permissions(plugin_loader=None) -> set[tuple[str, str]]:
    with belief_scope("rbac_permission_catalog.discover_declared_permissions"):
        permissions = set(_discover_route_permissions_cached())
        plugin_ids = tuple(
            sorted(
                {
                    str(getattr(plugin_config, "id", "") or "").strip()
                    for plugin_config in (plugin_loader.get_all_plugin_configs() if plugin_loader else [])
                    if str(getattr(plugin_config, "id", "") or "").strip()
                }
            )
        )
        permissions.update(_discover_plugin_execute_permissions_cached(plugin_ids))
        return permissions
# #endregion discover_declared_permissions


# #region sync_permission_catalog [C:4] [TYPE Function]
# @BRIEF Persists missing RBAC permission pairs into auth database.
# @PRE  db is a valid SQLAlchemy session bound to auth database.
# @PRE  declared_permissions is an iterable of (resource, action) tuples.
# @POST  Missing permissions are inserted; existing permissions remain untouched.
# @SIDE_EFFECT  Commits auth database transaction when new permissions are added.
def sync_permission_catalog(
    db: Session,
    declared_permissions: Iterable[tuple[str, str]],
) -> int:
    with belief_scope("rbac_permission_catalog.sync_permission_catalog"):
        normalized_declared: set[tuple[str, str]] = set()
        for resource, action in declared_permissions:
            normalized_resource = str(resource or "").strip()
            normalized_action = str(action or "").strip().upper()
            if normalized_resource and normalized_action:
                normalized_declared.add((normalized_resource, normalized_action))

        existing_permissions = db.query(Permission).all()
        existing_pairs = {(perm.resource, perm.action.upper()) for perm in existing_permissions}

        missing_pairs = sorted(normalized_declared - existing_pairs)
        for resource, action in missing_pairs:
            db.add(Permission(resource=resource, action=action))

        if missing_pairs:
            db.commit()

        return len(missing_pairs)
# #endregion sync_permission_catalog

# #endregion rbac_permission_catalog
