"""Contrôle d'accès centralisé de l'application.

La politique s'applique à toutes les routes enregistrées. Les décorateurs locaux
restent utiles pour documenter les cas particuliers, mais une route oubliée ne
retombe plus sur le simple fait d'être connecté.
"""
from functools import wraps

from flask import abort, request, redirect, url_for, jsonify, current_app
from flask_login import current_user


ROLE_ALIASES = {
    # Alias historique conservé uniquement pour l'affichage/compatibilité.
    # La sécurité utilise le slug RBAC réel ``super_admin``.
    "super_admin": "admin",
    "admin": "admin",
    "chef": "responsable_gmao",
    "responsable": "responsable_gmao",
    "responsable_gmao": "responsable_gmao",
    "tech": "technicien",
    "technician": "technicien",
    "technicien": "technicien",
    "assistant_prevention": "assistant_prevention",
    "requester": "demandeur",
    "user": "demandeur",
    "demandeur": "demandeur",
    "viewer": "lecture",
    "lecture": "lecture",
}

ROLE_LABELS = {
    "super_admin": "Super administrateur",
    "admin": "Administrateur",
    "responsable_gmao": "Responsable GMAO",
    "technicien": "Technicien",
    "assistant_prevention": "Assistant de prévention",
    "demandeur": "Demandeur",
    "lecture": "Lecture seule",
}

# LEGACY_SEED_ONLY : utilisé uniquement par les migrations historiques et le
# bootstrap des tests. Le runtime n'en fait jamais un fallback d'autorisation.
PERMISSIONS_BY_ROLE = {
    # LEGACY_SEED_ONLY : le rôle admin est désormais un rôle explicite. Le
    # wildcard appartient exclusivement à super_admin (voir migration).
    "super_admin": {"*"},
    "admin": {
        "documents.view",
        "dashboard.view", "intervention.view", "intervention.create", "intervention.edit",
        "intervention.assign", "intervention.change_status", "intervention.close", "intervention.reject",
        "intervention.delete", "intervention.postpone", "intervention.comment",
        "patrimoine.view", "patrimoine.create", "patrimoine.edit", "patrimoine.move",
        "patrimoine.change_quantity", "patrimoine.retire", "patrimoine.delete",
        "planning.view", "planning.create", "planning.edit", "planning.delete", "planning.manage",
        "stock.view", "stock.edit", "stock.receive", "stock.issue", "stock.transfer", "stock.inventory",
        "stock.adjust", "stock.destroy", "stock.transvasement", "stock.dilute", "stock.configure",
        "contract.view", "contract.manage", "prevention.view", "prevention.manage", "export.use",
        "housing.private", "gmao_config.view", "gmao_config.configure", "watchdog_dnd.view",
        "watchdog_dnd.configure", "integration.ent.view", "integration.ent.configure",
        "integration.outlook.view", "integration.outlook.configure", "integration.pronote.view",
        "integration.pronote.configure", "integration.yeastar.view", "integration.yeastar.configure",
        "user.view", "user.create", "user.edit", "user.delete", "user.manage", "role.view",
        "role.create", "role.edit", "role.archive", "role.manage", "audit.view", "audit.export",
        "system.view", "system.configure", "system.admin",
    },
    "responsable_gmao": {
        "documents.view",
        "dashboard.view", "intervention.view", "intervention.create", "intervention.manage",
        "patrimoine.view", "patrimoine.manage", "planning.view", "planning.manage",
        "stock.view", "stock.manage", "contract.view", "contract.manage",
        "prevention.view", "prevention.manage", "export.use",
        "housing.private",
    },
    "technicien": {
        "documents.view",
        "dashboard.view", "intervention.view", "intervention.create", "intervention.manage",
        "patrimoine.view", "patrimoine.manage", "planning.view", "planning.manage",
        "stock.view", "stock.manage", "contract.view", "prevention.view", "export.use",
    },
    "assistant_prevention": {
        "documents.view",
        "dashboard.view", "intervention.view", "intervention.create", "patrimoine.view",
        "planning.view", "prevention.view", "prevention.manage", "export.use",
    },
    "demandeur": {"dashboard.view", "intervention.view", "intervention.create", "documents.view"},
    "lecture": {"dashboard.view", "intervention.view", "patrimoine.view", "planning.view", "contract.view", "documents.view"},
}

PUBLIC_ENDPOINTS = {
    "auth.login", "health.health", "static",
    "setup_wizard.index", "setup_wizard.api_progress", "setup_wizard.api_save_step",
    "setup_wizard.api_complete", "setup_wizard.api_test_ent", "setup_wizard.api_test_pronote",
}

ADMIN_BLUEPRINTS = {
    "admin", "ai_config", "gmao_config", "logs", "status", "yeastar",
    "outlook_auth", "outlook_dashboard", "outlook_pages", "outlook_sync", "ent", "pronote",
}
PATRIMOINE_BLUEPRINTS = {
    "equipments", "equipments_meters", "equipments_documents", "equipments_restrictions", "housing",
    "equipments_scheduled", "buildings", "zones", "rooms", "room_types", "room_profiles", "configuration", "wizard", "lots",
}
PLANNING_BLUEPRINTS = {"planning", "scheduler", "interventions_planning"}
STOCK_BLUEPRINTS = {"parts", "meters", "cleaning"}
CONTRACT_BLUEPRINTS = {"companies", "contracts", "services", "c4"}
PREVENTION_BLUEPRINTS = {"trainings", "constraints", "prevention"}

# Catalogue réellement utilisé par les routes et les fonctions métier.
GRANULAR_PERMISSION_CODES = {
    code for codes in PERMISSIONS_BY_ROLE.values() for code in codes if code != "*"
} | {
    "documents.view",
    "gmao_config.view", "gmao_config.configure",
    "watchdog_dnd.view", "watchdog_dnd.configure",
    "integration.ent.view", "integration.ent.configure",
    "integration.outlook.view", "integration.outlook.configure",
    "integration.pronote.view", "integration.pronote.configure",
    "integration.yeastar.view", "integration.yeastar.configure",
    "user.view", "user.create", "user.edit", "user.delete", "user.manage",
    "role.view", "role.create", "role.edit", "role.archive", "role.manage",
    "audit.view", "audit.export", "system.view", "system.configure",
}

PERMISSION_LABELS = {
    "gmao_config": "Configuration GMAO",
    "watchdog_dnd": "Watchdog DND",
    "integration.ent": "Intégration ENT",
    "integration.outlook": "Intégration Outlook",
    "integration.pronote": "Intégration Pronote",
    "integration.yeastar": "Intégration Yeastar",
    "user": "Utilisateurs",
    "role": "Rôles et permissions",
    "audit": "Journaux d'audit",
    "documents": "Centre documentaire",
    "system": "Système",
}

ACTION_LABELS = {
    "view": ("Consulter", "Permet de consulter les informations du domaine."),
    "create": ("Créer", "Permet d'enregistrer de nouvelles informations dans le domaine."),
    "edit": ("Modifier", "Permet de modifier les informations existantes du domaine."),
    "delete": ("Supprimer", "Permet de supprimer ou retirer les éléments concernés."),
    "manage": ("Administrer", "Permet d'administrer les éléments et paramètres du domaine."),
    "configure": ("Configurer", "Permet de modifier la configuration du domaine."),
    "export": ("Exporter", "Permet d'exporter les informations du domaine."),
    "validate": ("Valider", "Permet de valider l'opération concernée."),
    "reject": ("Refuser", "Permet de refuser l'opération concernée."),
    "assign": ("Affecter", "Permet d'affecter l'opération à une personne ou une entreprise."),
    "change_status": ("Changer le statut", "Permet de modifier le statut de l'opération."),
    "close": ("Clôturer", "Permet de clôturer l'opération et d'enregistrer son état final."),
    "move": ("Déplacer", "Permet de déplacer les éléments concernés entre les emplacements."),
    "receive": ("Réceptionner", "Permet d'enregistrer les réceptions et l'entrée en stock."),
    "issue": ("Sortir du stock", "Permet d'enregistrer une sortie de stock, notamment vers un agent."),
    "transfer": ("Transférer", "Permet de transférer une quantité entre emplacements."),
    "inventory": ("Réaliser un inventaire", "Permet d'enregistrer un comptage physique et ses écarts."),
    "adjust": ("Corriger un stock", "Permet d'enregistrer une correction de stock tracée."),
    "destroy": ("Détruire", "Permet d'enregistrer la destruction de produits."),
    "transvasement": ("Transvaser", "Permet d'enregistrer un transvasement de produit."),
    "dilute": ("Préparer une dilution", "Permet d'enregistrer la préparation d'un produit dilué."),
    "archive": ("Archiver", "Permet d'archiver le domaine concerné sans effacer son historique."),
    "private": ("Consulter les données privées", "Permet de consulter les données personnelles protégées."),
    "all": ("Accès complet", "Accès complet à l'application, réservé au super administrateur."),
}

def permission_metadata(code):
    """Retourne un libellé et une description compréhensibles pour l'UI."""
    if code == "*":
        return "Accès complet à l'application", ACTION_LABELS["all"][1]
    module, _, action = code.partition(".")
    if code == "documents.view":
        return "Consulter le centre documentaire", "Permet de rechercher et consulter les documents autorisés des interventions, équipements et produits."
    domain = PERMISSION_LABELS.get(module, module.replace("_", " ").capitalize())
    label, description = ACTION_LABELS.get(action, (action.replace("_", " ").capitalize(), f"Permet d'effectuer l'action « {action.replace('_', ' ')} » dans le domaine {domain}."))
    return f"{label} — {domain}", description.replace("le domaine", f"le domaine « {domain} »")


def canonical_role(role):
    return ROLE_ALIASES.get((role or "").strip().lower(), "lecture")


def has_permission(permission, user=None):
    """Retourne le droit effectif, en mode fail-closed.

    La matrice SQL est la seule source de vérité à l'exécution : tous les
    rôles actifs sont réunis, puis les exceptions individuelles sont appliquées
    avec priorité aux refus. Aucune valeur de ``User.role`` n'est consultée.
    """
    user = user or current_user
    if not getattr(user, "is_authenticated", False):
        return False
    try:
        from .models.rbac import RolePermission
        from datetime import datetime, timezone
        roles = [link.role for link in user.role_links if link.role and link.role.is_active]
        if any(role.slug == "super_admin" for role in roles):
            return True
        if roles:
            granted = {
                link.permission.code
                for role in roles
                for link in role.permission_links
                if link.effect == "allow" and link.permission and link.permission.is_active
            }
        else:
            granted = set()

        now = datetime.now(timezone.utc)
        allowed_overrides = set()
        denied = set()
        for override in user.permission_links:
            expiry = override.expires_at
            if expiry and expiry.tzinfo is None:
                expiry = expiry.replace(tzinfo=timezone.utc)
            if expiry and expiry < now:
                continue
            if not override.permission or not override.permission.is_active:
                continue
            if override.effect == "deny":
                denied.add(override.permission.code)
            elif override.effect == "allow":
                allowed_overrides.add(override.permission.code)

        granted.update(allowed_overrides)
        if permission in denied:
            return False
        if permission in granted or "*" in granted:
            return True
        module = permission.rsplit(".", 1)[0] if "." in permission else permission
        return f"{module}.manage" in granted and f"{module}.manage" not in denied
    except Exception:
        current_app.logger.exception("Erreur RBAC lors du calcul de %s pour l'utilisateur %s", permission, getattr(user, "id", None))
        return False


def permission_required(permission):
    def decorator(view):
        @wraps(view)
        def wrapped(*args, **kwargs):
            if not has_permission(permission):
                abort(403)
            return view(*args, **kwargs)
        return wrapped
    return decorator


def required_permission(endpoint, method):
    """Déduit la permission minimale d'une route enregistrée."""
    if not endpoint:
        return None
    blueprint = endpoint.split(".", 1)[0]
    mutating = method not in {"GET", "HEAD", "OPTIONS"}

    if blueprint == "cleaning":
        explicit = {
            "cleaning.receive": "stock.receive",
            "cleaning.issue": "stock.issue",
            "cleaning.transfer": "stock.transfer",
            "cleaning.transvasement_page": "stock.transvasement",
            "cleaning.inventory": "stock.inventory",
            "cleaning.forecast_config": "stock.configure",
            "cleaning.locations": "stock.configure",
            "cleaning.material_assign": "patrimoine.edit",
            "cleaning.product_new": "stock.configure",
            "cleaning.product_edit": "stock.configure",
            "cleaning.reference_new": "stock.configure",
            "cleaning.packaging_new": "stock.configure",
        }
        if endpoint in explicit:
            return explicit[endpoint]
        action = "view"
        if mutating:
            action = "create" if any(token in endpoint for token in ("new", "receive", "issue", "transfer", "transvasement", "inventory")) else "edit"
            if "delete" in endpoint:
                action = "delete"
            if "validate" in endpoint:
                action = "validate"
        return f"stock.{action}"

    if blueprint == "gmao_config":
        return "gmao_config.configure" if mutating else "gmao_config.view"
    if blueprint == "yeastar":
        return "watchdog_dnd.configure" if "dnd" in endpoint and mutating else ("watchdog_dnd.view" if "dnd" in endpoint else "integration.yeastar.configure" if mutating else "integration.yeastar.view")
    if blueprint == "logs":
        return "watchdog_dnd.view" if "watchdog" in endpoint or endpoint == "logs.index" else "audit.view"
    if blueprint in {"ent", "outlook_pages", "outlook_dashboard", "outlook_sync"}:
        integration = "ent" if blueprint == "ent" else "outlook"
        return f"integration.{integration}.configure" if mutating else f"integration.{integration}.view"
    if blueprint == "pronote":
        return "integration.pronote.configure" if mutating else "integration.pronote.view"
    if blueprint == "admin":
        if endpoint.startswith("admin.user") or endpoint in {"admin.list_users", "admin.users"}:
            return "user.manage" if mutating else "user.view"
        if endpoint.startswith("admin.role") or endpoint == "admin.permissions":
            return "role.manage" if mutating else "role.view"
        if endpoint == "admin.audit_logs":
            return "audit.view"
        if endpoint.startswith("admin.settings"):
            return "system.configure" if mutating else "system.view"
    if blueprint in ADMIN_BLUEPRINTS:
        return "system.admin"
    if blueprint == "auth":
        return "system.admin" if endpoint not in {"auth.profile", "auth.change_own_password", "auth.logout"} else None
    if blueprint == "setup_wizard":
        return "system.admin"
    if blueprint in PATRIMOINE_BLUEPRINTS:
        return "patrimoine.edit" if mutating else "patrimoine.view"
    if blueprint in PLANNING_BLUEPRINTS:
        return "planning.edit" if mutating else "planning.view"
    if blueprint == "interventions":
        explicit = {
            "interventions.change_status": "intervention.change_status",
            "interventions.refuse": "intervention.reject",
            "interventions.soft_delete": "intervention.delete",
            "interventions.postpone": "intervention.postpone",
            "interventions.add_comment": "intervention.comment",
            "interventions.save_work_details": "intervention.edit",
            "interventions.update_location": "intervention.edit",
        }
        if endpoint in explicit:
            return explicit[endpoint]
        if endpoint in {"interventions.create", "interventions.create_for_group"}:
            return "intervention.create"
        return "intervention.edit" if mutating else "intervention.view"
    if blueprint == "documents":
        if endpoint in {"documents.index", "documents.fds", "documents.download"}:
            return "documents.view"
        if "intervention" in endpoint:
            return "intervention.manage" if mutating else "intervention.view"
        if "equipment" in endpoint:
            return "patrimoine.manage" if mutating else "patrimoine.view"
        return "system.admin"
    if blueprint in STOCK_BLUEPRINTS:
        return "stock.edit" if mutating else "stock.view"
    if blueprint in CONTRACT_BLUEPRINTS:
        return "contract.manage" if mutating else "contract.view"
    if blueprint in PREVENTION_BLUEPRINTS:
        return "prevention.manage" if mutating else "prevention.view"
    if blueprint == "exports":
        return "export.use"
    if blueprint in {"dashboard", "notifications", "messagerie", "chatbot"}:
        return "dashboard.view"
    return "system.admin" if mutating else "dashboard.view"


def enforce_route_permission():
    """Garde global appelé avant chaque requête."""
    endpoint = request.endpoint
    if endpoint in PUBLIC_ENDPOINTS or endpoint == "static":
        return None
    # L'API REST applique sa clé ou sa session dans son propre décorateur.
    if endpoint and endpoint.startswith("api_v1."):
        return None
    if not current_user.is_authenticated:
        if request.path.startswith('/api/') or request.is_json or request.accept_mimetypes.best == 'application/json':
            return jsonify({'error': 'Authentification requise'}), 401
        return redirect(url_for('auth.login', next=request.full_path.rstrip('?')))
    permission = required_permission(endpoint, request.method)
    if permission and not has_permission(permission):
        abort(403)
    return None
