<?php
// include/PermissionManager.php

class PermissionManager
{
    private mysqli $conn;

    public function __construct(mysqli $conn)
    {
        $this->conn = $conn;
    }

    // ──────────────────────────────────────────────────────────────────────────
    // ROLE QUERIES
    // ──────────────────────────────────────────────────────────────────────────

    /**
     * Get the role record attached to a user (via users.role_id).
     */
    public function getUserRole(int $userId): ?array
    {
        $stmt = $this->conn->prepare("
            SELECT r.id, r.name, r.display_name, r.description, r.is_system
            FROM   roles r
            JOIN   users u ON u.role_id = r.id
            WHERE  u.id = ?
        ");
        $stmt->bind_param('i', $userId);
        $stmt->execute();
        $row = $stmt->get_result()->fetch_assoc();
        $stmt->close();
        return $row ?: null;
    }

    /**
     * Get all permissions belonging to a role.
     */
    public function getRolePermissions(int $roleId): array
    {
        $stmt = $this->conn->prepare("
            SELECT p.id, p.name, p.display_name, p.module
            FROM   permissions p
            JOIN   role_permissions rp ON rp.permission_id = p.id
            WHERE  rp.role_id = ?
        ");
        $stmt->bind_param('i', $roleId);
        $stmt->execute();
        $rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
        $stmt->close();
        return $rows;
    }

    // ──────────────────────────────────────────────────────────────────────────
    // USER PERMISSION QUERIES
    // ──────────────────────────────────────────────────────────────────────────

    /**
     * Get the effective permission list for a user.
     *
     * Logic:
     *   1. Start with all permissions from the user's role.
     *   2. Apply user_permissions overrides:
     *        granted = 1  → add the permission (even if not in role)
     *        granted = 0  → remove the permission (explicit revoke)
     *
     * Returns an array of permission records (associative arrays).
     */
    public function getUserPermissions(int $userId): array
    {
        // Step 1 – role-based permissions
        $role = $this->getUserRole($userId);
        $rolePerms = $role ? $this->getRolePermissions($role['id']) : [];
        $effective = array_column($rolePerms, null, 'name'); // keyed by name

        // Step 2 – individual overrides
        $stmt = $this->conn->prepare("
            SELECT p.name, p.id, p.display_name, p.module, up.granted
            FROM   user_permissions up
            JOIN   permissions p ON p.id = up.permission_id
            WHERE  up.user_id = ?
        ");
        $stmt->bind_param('i', $userId);
        $stmt->execute();
        $overrides = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
        $stmt->close();

        foreach ($overrides as $o) {
            if ((int)$o['granted'] === 1) {
                $effective[$o['name']] = $o; // grant extra
            } else {
                unset($effective[$o['name']]); // revoke
            }
        }

        return array_values($effective);
    }

    /**
     * Check whether a user has a specific permission (by name).
     */
    public function userHasPermission(int $userId, string $permissionName): bool
    {
        $perms = $this->getUserPermissions($userId);
        foreach ($perms as $p) {
            if ($p['name'] === $permissionName) {
                return true;
            }
        }
        return false;
    }

    /**
     * Primary permission check used by AuthMiddleware::requirePermission().
     *
     * Acts as a named alias for userHasPermission() so that AuthMiddleware
     * and any other callers have a consistent, short method name.
     *
     * $resourceId and $departmentId are accepted for signature compatibility
     * but are not used in this flat role-based permission model.
     */
    public function hasPermission(
        int    $userId,
        string $permissionName,
        ?int   $resourceId   = null,
        ?int   $departmentId = null
    ): bool {
        return $this->userHasPermission($userId, $permissionName);
    }

    /**
     * Check whether a user has ANY of the given permissions.
     */
    public function userHasAnyPermission(int $userId, array $permissionNames): bool
    {
        $perms = array_column($this->getUserPermissions($userId), 'name');
        return (bool) array_intersect($permissionNames, $perms);
    }

    /**
     * Check whether a user has ALL of the given permissions.
     */
    public function userHasAllPermissions(int $userId, array $permissionNames): bool
    {
        $perms = array_column($this->getUserPermissions($userId), 'name');
        return count(array_intersect($permissionNames, $perms)) === count($permissionNames);
    }

    /**
     * Check whether a user belongs to a specific department.
     * Uses the department_id column on the users table.
     */
    public function hasDepartmentAccess(int $userId, int $departmentId): bool
    {
        $stmt = $this->conn->prepare("
            SELECT id FROM users
            WHERE  id = ? AND department_id = ? AND status = 'Active'
        ");
        $stmt->bind_param('ii', $userId, $departmentId);
        $stmt->execute();
        $stmt->store_result();
        $found = $stmt->num_rows > 0;
        $stmt->close();
        return $found;
    }

    // ──────────────────────────────────────────────────────────────────────────
    // SESSION HELPERS
    // ──────────────────────────────────────────────────────────────────────────

    /**
     * Load a user's effective permissions into $_SESSION['permissions'].
     * Call this once after login so page-level guards can use sessionCan()
     * without hitting the database on every request.
     */
    public function loadSessionPermissions(int $userId): void
    {
        $perms = $this->getUserPermissions($userId);
        $_SESSION['permissions'] = array_column($perms, 'name');
    }

    /**
     * Convenience: check a permission directly from the session array.
     * Usage: PermissionManager::sessionCan('manage_rotations')
     */
    public static function sessionCan(string $permissionName): bool
    {
        return in_array($permissionName, $_SESSION['permissions'] ?? [], true);
    }

    /**
     * Convenience: check multiple permissions from session (ANY match).
     */
    public static function sessionCanAny(array $permissionNames): bool
    {
        return (bool) array_intersect($permissionNames, $_SESSION['permissions'] ?? []);
    }

    /**
     * Convenience: check multiple permissions from session (ALL must match).
     */
    public static function sessionCanAll(array $permissionNames): bool
    {
        $session = $_SESSION['permissions'] ?? [];
        return count(array_intersect($permissionNames, $session)) === count($permissionNames);
    }

    // ──────────────────────────────────────────────────────────────────────────
    // GRANT / REVOKE  (admin actions)
    // ──────────────────────────────────────────────────────────────────────────

    /**
     * Grant a specific permission to a user (individual override, granted = 1).
     *
     * @param int $targetUserId  The user receiving the permission.
     * @param int $permissionId  The permission to grant.
     * @param int $grantedBy     Admin/actor performing the action.
     */
    public function grantPermission(int $targetUserId, int $permissionId, int $grantedBy): bool
    {
        $stmt = $this->conn->prepare("
            INSERT INTO user_permissions (user_id, permission_id, granted, granted_by)
            VALUES (?, ?, 1, ?)
            ON DUPLICATE KEY UPDATE granted = 1, granted_by = ?, granted_at = NOW()
        ");
        $stmt->bind_param('iiii', $targetUserId, $permissionId, $grantedBy, $grantedBy);
        $ok = $stmt->execute();
        $stmt->close();

        if ($ok) {
            $this->logAudit(
                $grantedBy,
                'grant_permission',
                'user',
                $targetUserId,
                null,
                json_encode(['permission_id' => $permissionId])
            );
        }

        return $ok;
    }

    /**
     * Revoke a specific permission from a user (individual override, granted = 0).
     */
    public function revokePermission(int $targetUserId, int $permissionId, int $revokedBy): bool
    {
        $stmt = $this->conn->prepare("
            INSERT INTO user_permissions (user_id, permission_id, granted, granted_by)
            VALUES (?, ?, 0, ?)
            ON DUPLICATE KEY UPDATE granted = 0, granted_by = ?, granted_at = NOW()
        ");
        $stmt->bind_param('iiii', $targetUserId, $permissionId, $revokedBy, $revokedBy);
        $ok = $stmt->execute();
        $stmt->close();

        if ($ok) {
            $this->logAudit(
                $revokedBy,
                'revoke_permission',
                'user',
                $targetUserId,
                null,
                json_encode(['permission_id' => $permissionId])
            );
        }

        return $ok;
    }

    /**
     * Remove any individual override for a user/permission pair,
     * effectively falling back to the role-based default.
     */
    public function clearPermissionOverride(int $targetUserId, int $permissionId): bool
    {
        $stmt = $this->conn->prepare("
            DELETE FROM user_permissions
            WHERE user_id = ? AND permission_id = ?
        ");
        $stmt->bind_param('ii', $targetUserId, $permissionId);
        $ok = $stmt->execute();
        $stmt->close();
        return $ok;
    }

    /**
     * Assign a role to a user, updating users.role_id.
     */
    public function assignRole(int $targetUserId, int $roleId, int $assignedBy): bool
    {
        // Capture old role for audit
        $old = $this->getUserRole($targetUserId);

        $stmt = $this->conn->prepare("UPDATE users SET role_id = ? WHERE id = ?");
        $stmt->bind_param('ii', $roleId, $targetUserId);
        $ok = $stmt->execute();
        $stmt->close();

        if ($ok) {
            $this->logAudit(
                $assignedBy,
                'assign_role',
                'user',
                $targetUserId,
                $old ? json_encode(['role_id' => $old['id']]) : null,
                json_encode(['role_id' => $roleId])
            );
        }

        return $ok;
    }

    /**
     * Sync a role's permissions: replaces all existing role_permissions rows
     * with the supplied permission ID list.
     *
     * @param int   $roleId        The role to update.
     * @param int[] $permissionIds New full set of permission IDs for the role.
     * @param int   $actorId       Admin performing the action.
     */
    public function syncRolePermissions(int $roleId, array $permissionIds, int $actorId): bool
    {
        // Fetch old permissions for audit
        $old = $this->getRolePermissions($roleId);

        $this->conn->begin_transaction();

        try {
            // Delete existing
            $del = $this->conn->prepare("DELETE FROM role_permissions WHERE role_id = ?");
            $del->bind_param('i', $roleId);
            $del->execute();
            $del->close();

            // Insert new
            if (!empty($permissionIds)) {
                $ins = $this->conn->prepare(
                    "INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)"
                );
                foreach ($permissionIds as $permId) {
                    $permId = (int) $permId;
                    $ins->bind_param('ii', $roleId, $permId);
                    $ins->execute();
                }
                $ins->close();
            }

            $this->conn->commit();

            $this->logAudit(
                $actorId,
                'sync_role_permissions',
                'role',
                $roleId,
                json_encode(array_column($old, 'id')),
                json_encode($permissionIds)
            );

            return true;

        } catch (Exception $e) {
            $this->conn->rollback();
            return false;
        }
    }

    // ──────────────────────────────────────────────────────────────────────────
    // AUDIT LOG
    // ──────────────────────────────────────────────────────────────────────────

    /**
     * Write a row to permission_audit_logs.
     *
     * @param int         $actorId       Who performed the action.
     * @param string      $action        e.g. 'login', 'grant_permission', 'revoke_permission'
     * @param string      $resourceType  e.g. 'user', 'role', 'permission'
     * @param int|null    $resourceId    Primary key of the affected resource.
     * @param string|null $oldValue      JSON or plain text of the previous state.
     * @param string|null $newValue      JSON or plain text of the new state.
     */
    public function logAudit(
        int     $actorId,
        string  $action,
        string  $resourceType,
        ?int    $resourceId = null,
        ?string $oldValue   = null,
        ?string $newValue   = null
    ): void {
        $ip        = $_SERVER['REMOTE_ADDR']     ?? null;
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;

        $stmt = $this->conn->prepare("
            INSERT INTO permission_audit_logs
                (user_id, action, resource_type, resource_id, old_value, new_value, ip_address, user_agent)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        ");
        $stmt->bind_param(
            'ississss',
            $actorId, $action, $resourceType, $resourceId,
            $oldValue, $newValue, $ip, $userAgent
        );
        $stmt->execute();
        $stmt->close();
    }

    // ──────────────────────────────────────────────────────────────────────────
    // ALL ROLES / PERMISSIONS  (for admin UIs)
    // ──────────────────────────────────────────────────────────────────────────

    public function getAllRoles(): array
    {
        $result = $this->conn->query("SELECT * FROM roles ORDER BY display_name");
        return $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
    }

    public function getAllPermissions(): array
    {
        $result = $this->conn->query("SELECT * FROM permissions ORDER BY module, display_name");
        return $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
    }

    public function getPermissionsByModule(): array
    {
        $all     = $this->getAllPermissions();
        $grouped = [];
        foreach ($all as $p) {
            $grouped[$p['module'] ?? 'General'][] = $p;
        }
        return $grouped;
    }

    /**
     * Get a single role by ID.
     */
    public function getRoleById(int $roleId): ?array
    {
        $stmt = $this->conn->prepare("SELECT * FROM roles WHERE id = ?");
        $stmt->bind_param('i', $roleId);
        $stmt->execute();
        $row = $stmt->get_result()->fetch_assoc();
        $stmt->close();
        return $row ?: null;
    }

    /**
     * Get a single permission by name.
     */
    public function getPermissionByName(string $name): ?array
    {
        $stmt = $this->conn->prepare("SELECT * FROM permissions WHERE name = ?");
        $stmt->bind_param('s', $name);
        $stmt->execute();
        $row = $stmt->get_result()->fetch_assoc();
        $stmt->close();
        return $row ?: null;
    }
}