<?php
/* ============================================================================
   WBMCZ — ACCESS CONTROL (detailed)
   ----------------------------------------------------------------------------
   Three levels of control, all in one place:

     MODULE   a section of the panel            users, products, orders …
     ACTION   what may be done inside it        users.view, users.edit, users.delete
     COLUMN   what may be seen inside it        users.col.balance, users.col.kyc

   A permission key is simply "module.action" or "module.col.<name>".

   Where a staff member's access comes from
   ----------------------------------------
     1. the role attached to the account          roles.perms_json
     2. plus anything given to that person alone  users.perm_extra
     3. minus anything taken back from them       users.perm_denied

   So a role is the starting point, and each person can be given a little more
   or a little less afterwards without touching anyone else.

   The ceiling
   -----------
   Nobody can hand out access they do not hold themselves. When an admin builds
   a role or ticks boxes for an employee, the form only offers what that admin
   has, and the server intersects the request with the ceiling again before
   saving — so it holds even if someone edits the page in their browser.

   Who may create whom
   -------------------
     Super Admin  →  Admin and Employee, with any access
     Admin        →  Employee only, with at most their own access
     Employee     →  nobody

   Main entry points
   -----------------
     acl_can('users.edit')            true / false
     acl_col('users', 'balance')      may this account see that column?
     acl_guard('users.view')          stop the page with a friendly screen
     acl_ceiling()                    the set this account may hand out
     acl_level()                      superadmin | admin | employee
   ============================================================================ */

if (!function_exists('acl_modules')) {

/* ---------------------------------------------------------------------------
   THE REGISTRY
   Every module, the actions inside it, and the columns its list shows.
   'legacy' ties the module back to the old single permission key, so every
   page written before this file carries on working untouched.
   --------------------------------------------------------------------------- */
function acl_modules() {
    static $m = null;
    if ($m !== null) return $m;

    $m = [

    'dashboard' => [
        'label' => 'Dashboard', 'icon' => 'fa-gauge-high', 'group' => 'Overview', 'legacy' => '',
        'actions' => ['view' => 'Open the dashboard', 'export' => 'Export figures'],
        /* one key per block of the dashboard, in the order they appear on it */
        'columns' => [
            'waiting'   => 'Waiting for somebody (the queue tiles)',
            'earning'   => 'Money — income, expenses, company income',
            'orders'    => 'Orders — total, completed, in progress',
            'users'     => 'People — members, sellers, referrals, staff',
            'wallet'    => 'Wallets — held, bonus, seller cash & dues',
            'charts'    => 'Where the money comes from',
            'recent'    => 'Latest orders',
        ],
    ],

    'activity_log' => [
        'label' => 'Activity Log', 'icon' => 'fa-clock-rotate-left', 'group' => 'Overview', 'legacy' => 'manage_staff_roles',
        'actions' => ['view' => 'Read the audit trail', 'export' => 'Export CSV', 'purge' => 'Clear old entries'],
        'columns' => ['who' => 'Who did it', 'what' => 'What was done', 'module' => 'Module', 'ip' => 'IP address', 'device' => 'Device / browser', 'when' => 'Date & time'],
    ],

    'tasks' => [
        'label' => 'Activity & Task', 'icon' => 'fa-list-check', 'group' => 'Overview', 'legacy' => 'manage_tasks',
        'actions' => [
            'view' => 'See the board', 'view_all' => 'See everyone\'s tasks (not just their own)',
            'access' => 'Hand out board access to other staff',
            'create' => 'Create a task', 'assign' => 'Assign to anyone', 'update' => 'Update status & progress',
            'handover' => 'Hand a task to a colleague', 'due' => 'Change the deadline',
            'note' => 'Add notes', 'delete' => 'Delete a task', 'export' => 'Export CSV',
        ],
        'columns' => [
            'code' => 'Task ID', 'about' => 'Who it is about', 'details' => 'Task details',
            'assigned' => 'Assigned to', 'priority' => 'Priority', 'due' => 'Due date',
            'status' => 'Status', 'created' => 'Created on & by', 'history' => 'Task history',
        ],
    ],

    'users' => [
        'label' => 'User Management', 'icon' => 'fa-users', 'group' => 'Management', 'legacy' => 'manage_users',
        'actions' => [
            'view' => 'See the user list (All Users)',
            /* the list is split into its own pages, so access can be given for
               one audience without handing over the rest */
            'view_user'     => 'See the Users list',
            'view_free'     => 'See the Free Users list',
            'view_pro'      => 'See the Pro Users list',
            'view_seller'   => 'See the Sellers list',
            'view_employee' => 'See the Employees list',
            'view_admin'    => 'See the Admins list',
            'view_deleted'  => 'See the Deleted Users list',
            'create' => 'Add a user', 'edit' => 'Edit a user',
            'delete' => 'Delete a user', 'restore' => 'Restore a deleted user',
            'purge' => 'Erase a deleted account for good', 'block' => 'Block / unblock',
            'password' => 'Reset a password', 'wallet' => 'Wallet credit / debit / transfer',
            'kyc' => 'Approve or reject KYC', 'upgrade' => 'Handle upgrade requests',
            'idcard' => 'Issue or renew an ID card', 'note' => 'Write notes',
            'notify' => 'Send a notification', 'import' => 'Bulk import', 'export' => 'Export CSV',
        ],
        'columns' => [
            'profile'      => 'Profile photo',
            'details'      => 'Name, user ID, mobile, email',
            'address'      => 'Village / post office / block / landmark',
            'region'       => 'State, district, pin code',
            'type'         => 'User type, date & time',
            'created_by'   => 'Created by',
            'kyc'          => 'KYC status',
            'subscription' => 'Subscription',
            'balance'      => 'Wallet balance',
            'transfer'     => 'Transfer money',
            'quick'        => 'Quick response (call / WhatsApp / email)',
            'notes'        => 'Notes',
            'update'       => 'Update buttons',
            'action'       => 'Action buttons',
        ],
    ],

    'kyc' => [
        'label' => 'KYC Verification', 'icon' => 'fa-shield-halved', 'group' => 'Management', 'legacy' => 'manage_users',
        'actions' => ['view' => 'See pending KYC', 'approve' => 'Approve', 'reject' => 'Reject', 'recheck' => 'Send back for re-check', 'fields' => 'Set up KYC questions', 'export' => 'Export CSV'],
        'columns' => ['user' => 'User', 'type' => 'KYC type', 'documents' => 'Document images', 'numbers' => 'Aadhaar / PAN numbers', 'submitted' => 'Submitted / updated', 'status' => 'Status', 'review' => 'Review buttons'],
    ],

    'leads' => [
        'label' => 'Lead Management', 'icon' => 'fa-bullseye', 'group' => 'Management', 'legacy' => 'manage_leads',
        'actions' => [
            'view' => 'See leads', 'view_all' => 'See every lead (not just their own)', 'create' => 'Add a lead',
            'edit' => 'Edit a lead', 'assign' => 'Assign to staff', 'followup' => 'Log a follow-up',
            'convert' => 'Convert to customer', 'close' => 'Close / mark lost', 'delete' => 'Delete',
            'import' => 'Bulk import', 'export' => 'Export CSV',
        ],
        'columns' => ['lead' => 'Lead name', 'contact' => 'Contact number & email', 'location' => 'Location', 'interest' => 'Interest', 'source' => 'Source', 'status' => 'Status', 'followup' => 'Next follow-up', 'owner' => 'Owner', 'added_by' => 'Added by', 'action' => 'Action buttons'],
    ],

    'sellers' => [
        'label' => 'Seller Management', 'icon' => 'fa-store', 'group' => 'Management', 'legacy' => 'manage_sellers',
        'actions' => ['view' => 'See sellers', 'create' => 'Add a seller', 'edit' => 'Edit', 'approve' => 'Approve a seller', 'reject' => 'Reject', 'block' => 'Block / unblock', 'categories' => 'Set their categories', 'commission' => 'Set commission', 'delete' => 'Delete', 'export' => 'Export CSV'],
        'columns' => ['seller' => 'Seller name & ID', 'contact' => 'Contact details', 'business' => 'Business / agency details', 'categories' => 'Categories', 'kyc' => 'KYC & documents', 'bank' => 'Bank details', 'earning' => 'Earning & commission', 'status' => 'Status', 'action' => 'Action buttons'],
    ],

    'staff' => [
        'label' => 'Staff & Roles', 'icon' => 'fa-users-gear', 'group' => 'Management', 'legacy' => 'manage_staff_roles',
        'actions' => [
            'view' => 'See staff accounts', 'create_employee' => 'Create an employee',
            'create_admin' => 'Create an admin', 'edit' => 'Edit a staff account',
            'access' => 'Give or take back access', 'role_view' => 'See roles',
            'role_create' => 'Create a role', 'role_edit' => 'Edit a role', 'role_delete' => 'Delete a role',
            'fields' => 'Design the staff creation form', 'password' => 'Reset a staff password',
            'block' => 'Block / unblock staff', 'delete' => 'Remove a staff account', 'export' => 'Export CSV',
        ],
        'columns' => ['staff' => 'Staff name & ID', 'contact' => 'Contact details', 'extra' => 'Extra form fields', 'level' => 'Level (admin / employee)', 'role' => 'Assigned role', 'access' => 'Access summary', 'created_by' => 'Created by', 'status' => 'Status', 'action' => 'Action buttons'],
    ],

    'products' => [
        'label' => 'Product Management', 'icon' => 'fa-box-open', 'group' => 'Service', 'legacy' => 'manage_services',
        'actions' => [
            'view' => 'See products', 'create' => 'Add a product', 'edit' => 'Edit a product',
            'price' => 'Change price & MRP', 'share' => 'Change share percentages',
            'kyc' => 'Set required KYC fields', 'area' => 'Set serviceable area',
            'approve' => 'Approve pending products', 'status' => 'Turn a product on or off',
            'note' => 'Write notes', 'delete' => 'Delete', 'import' => 'Bulk CSV import',
            'subcat' => 'Create & edit sub categories', 'export' => 'Export CSV',
        ],
        'columns' => [
            'number' => 'Product number, date & time', 'category' => 'Category & sub category', 'name' => 'Product name',
            'mrp' => 'MRP', 'discount' => 'Discount price', 'seller' => 'Seller details',
            'seller_share' => 'Seller share', 'sub_share' => 'Discount share (subscription)',
            'referral_share' => 'Referral share', 'staff_share' => 'Staff share', 'company_share' => 'Company share',
            'quick' => 'Quick response', 'update' => 'Update buttons', 'notes' => 'Notes', 'action' => 'Action buttons',
        ],
    ],

    'gov_products' => [
        'label' => 'Government Portals', 'icon' => 'fa-landmark', 'group' => 'Service', 'legacy' => 'manage_services',
        'actions' => ['view' => 'See government portals', 'create' => 'Add a portal', 'edit' => 'Edit', 'links' => 'Change the portal links', 'kyc' => 'Set required KYC fields', 'area' => 'Set serviceable area', 'status' => 'Turn a portal on or off', 'note' => 'Write notes', 'delete' => 'Delete', 'import' => 'Bulk CSV import', 'subcat' => 'Create & edit sub categories', 'export' => 'Export CSV'],
        'columns' => ['number' => 'Portal number, date & time', 'category' => 'Category & sub category', 'name' => 'Portal name', 'links' => 'Portal links', 'customer_data' => 'Customer data (KYC & area)', 'update' => 'Update buttons', 'notes' => 'Notes', 'action' => 'Action buttons'],
    ],

    'services' => [
        'label' => 'Category & Service', 'icon' => 'fa-screwdriver-wrench', 'group' => 'Service', 'legacy' => 'manage_services',
        'actions' => ['view' => 'See categories', 'create' => 'Add a category', 'edit' => 'Edit', 'reorder' => 'Re-order', 'approve' => 'Approve a pending service', 'reject' => 'Reject', 'delete' => 'Delete'],
        'columns' => ['name' => 'Category name', 'icon' => 'Icon & colour', 'parent' => 'Parent category', 'count' => 'Product count', 'status' => 'Status', 'action' => 'Action buttons'],
    ],

    'subscriptions' => [
        'label' => 'Subscription Management', 'icon' => 'fa-crown', 'group' => 'Service', 'legacy' => 'manage_subscriptions',
        'actions' => ['view' => 'See plans', 'create' => 'Create a plan', 'edit' => 'Edit a plan', 'price_up' => 'Raise a price', 'price_down' => 'Lower a price', 'product_add' => 'Add products to a plan', 'product_remove' => 'Remove products from a plan', 'status' => 'Show or hide a plan', 'default' => 'Set the default plan', 'delete' => 'Delete a plan', 'history' => 'Read the plan history'],
        'columns' => ['name' => 'Plan name & badge', 'price' => 'Price & MRP', 'duration' => 'Duration', 'discount' => 'Member discount', 'products' => 'Products in the plan', 'members' => 'Subscriber count', 'status' => 'Status', 'action' => 'Action buttons'],
    ],

    'orders' => [
        'label' => 'Order Management', 'icon' => 'fa-box', 'group' => 'Management', 'legacy' => 'manage_orders',
        'actions' => ['view' => 'See orders', 'view_all' => 'See every order', 'edit' => 'Edit an order', 'status' => 'Change the status', 'assign' => 'Assign to staff', 'deliver' => 'Mark delivered', 'cancel' => 'Cancel', 'refund' => 'Refund', 'delete' => 'Delete', 'export' => 'Export CSV'],
        'columns' => ['order' => 'Order number & date', 'customer' => 'Customer details', 'service' => 'Service & seller', 'amount' => 'Amount', 'payment' => 'Payment details', 'documents' => 'Uploaded documents', 'status' => 'Status', 'action' => 'Action buttons'],
    ],

    'wallet' => [
        'label' => 'Wallet Management', 'icon' => 'fa-wallet', 'group' => 'Money', 'legacy' => 'manage_transactions',
        'actions' => ['view' => 'See wallets', 'history' => 'See full history', 'credit' => 'Add money', 'debit' => 'Deduct money', 'transfer' => 'Transfer between wallets', 'bank' => 'Verify bank details', 'reverse' => 'Reverse a transaction', 'export' => 'Export CSV'],
        'columns' => ['user' => 'User', 'description' => 'Description', 'type' => 'Type', 'amount' => 'Amount', 'balance' => 'Running balance', 'bank' => 'Bank details', 'status' => 'Status', 'date' => 'Date & time', 'action' => 'Action buttons'],
    ],

    'payouts' => [
        'label' => 'Payout & Earning', 'icon' => 'fa-money-bill-transfer', 'group' => 'Money', 'legacy' => 'manage_transactions',
        'actions' => ['view' => 'See payout requests', 'approve' => 'Approve a payout', 'reject' => 'Reject', 'pay' => 'Mark as paid', 'hold' => 'Hold or release', 'commission' => 'Set commission', 'reports' => 'Open earning reports', 'export' => 'Export CSV'],
        'columns' => ['request' => 'Request number & date', 'user' => 'Who asked', 'amount' => 'Amount', 'bank' => 'Bank / UPI details', 'commission' => 'Commission breakdown', 'status' => 'Status', 'action' => 'Action buttons'],
    ],

    'refer' => [
        'label' => 'Refer & Earn', 'icon' => 'fa-share-nodes', 'group' => 'Money', 'legacy' => 'manage_settings',
        'actions' => ['view' => 'See referrals', 'settings' => 'Change referral settings', 'payout' => 'Pay referral earnings', 'adjust' => 'Adjust a referral', 'export' => 'Export CSV'],
        'columns' => ['referrer' => 'Referrer', 'referred' => 'Who they referred', 'level' => 'Level', 'earning' => 'Earning', 'status' => 'Status', 'date' => 'Date'],
    ],

    'tickets' => [
        'label' => 'Helpdesk', 'icon' => 'fa-headset', 'group' => 'Management', 'legacy' => 'manage_tickets',
        'actions' => ['view' => 'See tickets', 'view_all' => 'See every ticket', 'reply' => 'Reply', 'assign' => 'Assign to staff', 'status' => 'Change the status', 'escalate' => 'Escalate', 'close' => 'Close', 'delete' => 'Delete', 'export' => 'Export CSV'],
        'columns' => ['ticket' => 'Ticket ID & date', 'customer' => 'Customer info', 'subject' => 'Subject / issue', 'messages' => 'Conversation', 'attachments' => 'Attachments', 'status' => 'Status', 'assigned' => 'Assigned to', 'action' => 'Action buttons'],
    ],

    'website' => [
        'label' => 'Website Settings', 'icon' => 'fa-globe', 'group' => 'Site', 'legacy' => 'manage_settings',
        'actions' => ['view' => 'Open settings', 'general' => 'General settings', 'carousel' => 'Home page carousel / slider', 'banner' => 'After-login banner', 'logo' => 'Logo & favicon', 'contact' => 'Contact & address', 'seo' => 'SEO / meta', 'content' => 'Page content', 'video' => 'Video section', 'team' => 'Team members', 'language' => 'Language & translation', 'payment' => 'Payment gateway keys', 'requests' => 'Website build requests',
                       'sms' => 'SMS & OTP settings', 'sms_test' => 'Send a test SMS', 'sms_log' => 'Read the SMS log'],
        'columns' => ['title' => 'Title & subtitle', 'image' => 'Image', 'buttons' => 'Buttons & links', 'order' => 'Order', 'status' => 'Status', 'action' => 'Action buttons'],
    ],

    'notifications' => [
        'label' => 'Notifications', 'icon' => 'fa-bell', 'group' => 'Site', 'legacy' => 'manage_users',
        'actions' => ['view' => 'See notifications', 'send' => 'Send a notification', 'broadcast' => 'Send to everyone', 'schedule' => 'Schedule one', 'delete' => 'Delete'],
        'columns' => ['title' => 'Title & message', 'audience' => 'Audience', 'channel' => 'Channel', 'sent' => 'Sent / scheduled', 'status' => 'Status', 'action' => 'Action buttons'],
    ],

    'reports' => [
        'label' => 'Reports', 'icon' => 'fa-chart-pie', 'group' => 'Site', 'legacy' => 'manage_transactions',
        'actions' => ['view' => 'Open reports', 'earning' => 'Earning report', 'sales' => 'Sales report', 'user' => 'User report', 'commission' => 'Commission report', 'export' => 'Export CSV'],
        'columns' => ['period' => 'Period', 'count' => 'Counts', 'amount' => 'Amounts', 'commission' => 'Commission', 'profit' => 'Company profit', 'chart' => 'Charts'],
    ],

    ];

    /* ---------------------------------------------------------------------
       ONE KEY PER CATEGORY

       The catalogue is split one page per category, so each category needs a
       permission of its own — that is what makes "you may open Government
       Portals and nothing else" possible. The list is read from the catalogue
       rather than written out here, so a category created next month gets its
       key without anybody editing this file.
       --------------------------------------------------------------------- */
    foreach (acl_product_categories() as $slug => $title) {
        if ($slug === 'gov_portals') { continue; }   /* it has its own module */
        $m['products']['actions']['view_' . $slug] = 'Open the ' . $title . ' page';
    }

    /* ---------------------------------------------------------------------
       ONE KEY PER DESK ON THE ACTIVITY & TASK BOARD

       The board already has a per-person list of desks, chosen when work is
       handed over. These keys sit alongside it so a ROLE can carry the same
       thing — give the role "Verify Collection" and everyone in it has that
       desk without setting each person up by hand. Whichever says yes wins;
       neither takes anything away from the other.
       --------------------------------------------------------------------- */
    foreach (acl_task_tabs() as $key => $label) {
        $m['tasks']['actions']['tab_' . $key] = 'Open the ' . $label . ' desk';
    }

    return $m;
}

/* The desks on the Activity & Task board, for the per-desk keys above. Read
   from the board itself so a desk added there needs no edit here. */
function acl_task_tabs() {
    static $t = null;
    if ($t !== null) return $t;
    $t = [];
    try {
        if (!function_exists('pipe_all_tabs')) { @require_once __DIR__ . '/pipeline.php'; }
        if (function_exists('pipe_all_tabs')) {
            $roles = function_exists('pipe_roles') ? pipe_roles() : [];
            foreach (pipe_all_tabs() as $k => $tab) {
                /* User and Seller both have a KYC desk and a Lead desk, so the
                   side has to be in the name or the Roles screen shows the same
                   words twice and you cannot tell which one you are ticking. */
                $side = (string)($tab['role'] ?? '');
                $side = isset($roles[$side]['label']) ? $roles[$side]['label'] . ' · ' : '';
                $t[$k] = $side . html_entity_decode((string)($tab['label'] ?? $k), ENT_QUOTES, 'UTF-8');
            }
        }
    } catch (\Throwable $e) { $t = []; }
    return $t;
}

/* The categories, for the per-category product keys above. Read once, and a
   failure here simply means no per-category keys — never a broken page. */
function acl_product_categories() {
    static $c = null;
    if ($c !== null) return $c;
    $c = [];
    try {
        global $db;
        if (isset($db) && is_object($db)) {
            foreach ($db->select("SELECT id, category_key, title FROM service_categories ORDER BY display_order ASC, id ASC") as $r) {
                $slug = preg_replace('/^cat_/', '', strtolower((string)$r['category_key']));
                $slug = trim((string)preg_replace('/[^a-z0-9_]+/', '_', $slug), '_');
                if ($slug === '') { $slug = 'cat' . (int)$r['id']; }
                $c[$slug] = html_entity_decode((string)$r['title'], ENT_QUOTES, 'UTF-8');
            }
        }
    } catch (\Throwable $e) { $c = []; }
    return $c;
}

/* The groups the builder screen shows, in order. */
function acl_groups() {
    return ['Overview' => 'fa-gauge-high', 'Management' => 'fa-users', 'Service' => 'fa-screwdriver-wrench',
            'Money' => 'fa-wallet', 'Site' => 'fa-globe'];
}

/* Every permission key that exists, flat. */
function acl_all_keys() {
    static $k = null;
    if ($k !== null) return $k;
    $k = [];
    foreach (acl_modules() as $mk => $m) {
        foreach (array_keys($m['actions']) as $a) { $k[] = "$mk.$a"; }
        foreach (array_keys($m['columns']) as $c) { $k[] = "$mk.col.$c"; }
    }
    return $k;
}

/* A readable name for one key, for the log and the summary chips. */
function acl_key_label($key) {
    $parts = explode('.', $key, 3);
    $mods  = acl_modules();
    $mk    = $parts[0] ?? '';
    if (!isset($mods[$mk])) return $key;
    $mod = $mods[$mk];
    if (($parts[1] ?? '') === 'col') {
        $c = $parts[2] ?? '';
        return $mod['label'] . ' — column: ' . ($mod['columns'][$c] ?? $c);
    }
    $a = $parts[1] ?? '';
    return $mod['label'] . ' — ' . ($mod['actions'][$a] ?? $a);
}

/* Old single permission key → the modules it now covers. */
function acl_legacy_map() {
    static $map = null;
    if ($map !== null) return $map;
    $map = [];
    foreach (acl_modules() as $mk => $m) {
        $l = $m['legacy'] ?? '';
        if ($l === '') continue;
        $map[$l][] = $mk;
    }
    return $map;
}

/** Every key belonging to one module. */
function acl_module_keys($mk) {
    $mods = acl_modules();
    if (!isset($mods[$mk])) return [];
    $out = [];
    foreach (array_keys($mods[$mk]['actions']) as $a) { $out[] = "$mk.$a"; }
    foreach (array_keys($mods[$mk]['columns']) as $c) { $out[] = "$mk.col.$c"; }
    return $out;
}

/* ---------------------------------------------------------------------------
   SCHEMA — built on demand so a page can never white-screen before the
   migration has been run.
   --------------------------------------------------------------------------- */
function acl_ensure_schema($db) {
    static $done = false;
    if ($done) return true;
    $conn = $db->getConnection();

    $col = function ($t, $c, $def) use ($conn) {
        try {
            $r = $conn->query("SHOW COLUMNS FROM `$t` LIKE '$c'");
            if ($r && $r->num_rows === 0) { $conn->query("ALTER TABLE `$t` ADD COLUMN `$c` $def"); }
        } catch (\Throwable $e) {}
    };
    $col('roles', 'level',           "VARCHAR(20) NOT NULL DEFAULT 'employee'");
    $col('roles', 'perms_json',      "LONGTEXT NULL");
    $col('roles', 'description',     "VARCHAR(255) NULL");
    $col('roles', 'status',          "TINYINT(1) NOT NULL DEFAULT 1");
    $col('roles', 'created_by',      "INT NULL");
    $col('roles', 'created_by_name', "VARCHAR(150) NULL");
    $col('roles', 'updated_at',      "TIMESTAMP NULL DEFAULT NULL");

    $col('users', 'staff_level', "VARCHAR(20) NULL");
    $col('users', 'perm_extra',  "TEXT NULL");
    $col('users', 'perm_denied', "TEXT NULL");

    foreach ([
        "CREATE TABLE IF NOT EXISTS staff_form_fields (
            id INT AUTO_INCREMENT PRIMARY KEY,
            field_key VARCHAR(60) NOT NULL,
            label VARCHAR(150) NOT NULL,
            field_type VARCHAR(20) NOT NULL DEFAULT 'text',
            options TEXT NULL,
            placeholder VARCHAR(200) NULL,
            help_text VARCHAR(255) NULL,
            applies_to VARCHAR(20) NOT NULL DEFAULT 'both',
            is_required TINYINT(1) NOT NULL DEFAULT 0,
            is_system TINYINT(1) NOT NULL DEFAULT 0,
            maps_to VARCHAR(40) NULL,
            sort_order INT NOT NULL DEFAULT 0,
            status TINYINT(1) NOT NULL DEFAULT 1,
            created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
            UNIQUE KEY uniq_sff (field_key)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

        "CREATE TABLE IF NOT EXISTS staff_field_values (
            id INT AUTO_INCREMENT PRIMARY KEY,
            user_id INT NOT NULL,
            field_key VARCHAR(60) NOT NULL,
            field_value TEXT NULL,
            updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            UNIQUE KEY uniq_sfv (user_id, field_key)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

        "CREATE TABLE IF NOT EXISTS staff_access_log (
            id BIGINT AUTO_INCREMENT PRIMARY KEY,
            target_type VARCHAR(20) NOT NULL DEFAULT 'user',
            target_id INT NOT NULL,
            target_name VARCHAR(150) NULL,
            action VARCHAR(30) NOT NULL DEFAULT 'update',
            granted TEXT NULL,
            revoked TEXT NULL,
            remark VARCHAR(500) NULL,
            done_by INT NULL,
            done_by_name VARCHAR(150) NULL,
            done_by_level VARCHAR(20) NULL,
            ip_address VARCHAR(45) NULL,
            created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
            KEY idx_sal_target (target_type, target_id, created_at)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
    ] as $q) { try { $conn->query($q); } catch (\Throwable $e) {} }

    /* ------------------------------------------------------------------------
       A dump-import that stopped early leaves a table readable but unwritable:
       the id column has no AUTO_INCREMENT, so MySQL has no number to give a new
       row and every save is refused. Put it right the first time it is noticed.
       Checked once per session, because it can only ever be wrong once.
       --------------------------------------------------------------------- */
    if (empty($_SESSION['wb_ai_checked'])) {
        foreach (['roles', 'users'] as $__t) { acl_fix_autoincrement($db, $__t); }
        $_SESSION['wb_ai_checked'] = 1;
    }

    $done = true;
    return true;
}

/**
 * Give a table's numbered primary key its AUTO_INCREMENT back, if it lost it.
 * Does nothing at all when the table is already healthy.
 *
 * @return string '' when nothing needed doing, otherwise what was repaired
 */
function acl_fix_autoincrement($db, $table) {
    $conn = $db->getConnection();
    try {
        $r = $conn->query("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'");
        $pk = [];
        while ($r && ($k = $r->fetch_assoc())) { $pk[] = $k['Column_name']; }
        if (count($pk) !== 1) return '';

        $col = $pk[0];
        $r   = $conn->query("SHOW COLUMNS FROM `$table` LIKE '" . $conn->real_escape_string($col) . "'");
        $c   = $r ? $r->fetch_assoc() : null;
        if (!$c) return '';
        if (stripos((string)$c['Type'], 'int') === false) return '';
        if (stripos((string)$c['Extra'], 'auto_increment') !== false) return '';

        $next = 1;
        $x = $conn->query("SELECT COALESCE(MAX(`$col`), 0) + 1 AS n FROM `$table`");
        if ($x && ($v = $x->fetch_assoc())) { $next = max(1, (int)$v['n']); }

        $conn->query("ALTER TABLE `$table` MODIFY `$col` " . $c['Type'] . " NOT NULL AUTO_INCREMENT");
        try { $conn->query("ALTER TABLE `$table` AUTO_INCREMENT = $next"); } catch (\Throwable $e) {}
        return "$table.$col";
    } catch (\Throwable $e) { return ''; }
}

/* ---------------------------------------------------------------------------
   The staff tier of an account.
     superadmin  the owner: no role attached, or a role that holds everything
     admin       a role marked as an admin role
     employee    everyone else inside the panel
   --------------------------------------------------------------------------- */
function acl_level($userId = null) {
    global $db;
    static $cache = [];
    $uid = $userId !== null ? (int)$userId : (int)($_SESSION['user_id'] ?? 0);
    if ($uid <= 0) return 'guest';
    if (isset($cache[$uid])) return $cache[$uid];

    try {
        $u = $db->select("SELECT user_type, role_id, staff_level FROM users WHERE id = ?", [$uid]);
    } catch (\Throwable $e) { $u = []; }
    if (empty($u) || $u[0]['user_type'] !== 'admin') return $cache[$uid] = 'guest';

    /* an explicit level always wins */
    $lvl = strtolower(trim((string)($u[0]['staff_level'] ?? '')));
    if (in_array($lvl, ['superadmin', 'admin', 'employee'], true)) return $cache[$uid] = $lvl;

    /* no role at all = the owner account */
    if (empty($u[0]['role_id'])) return $cache[$uid] = 'superadmin';

    try {
        $r = $db->select("SELECT permissions, level FROM roles WHERE id = ?", [(int)$u[0]['role_id']]);
    } catch (\Throwable $e) { $r = []; }
    if (empty($r)) return $cache[$uid] = 'employee';

    $p = json_decode((string)$r[0]['permissions'], true);
    if (is_array($p) && in_array('all', $p, true)) return $cache[$uid] = 'superadmin';

    $rl = strtolower(trim((string)($r[0]['level'] ?? '')));
    return $cache[$uid] = ($rl === 'admin' ? 'admin' : 'employee');
}

function acl_is_super($userId = null) { return acl_level($userId) === 'superadmin'; }

/* ---------------------------------------------------------------------------
   THE EFFECTIVE SET
   role permissions  +  what this person alone was given  −  what was taken back
   A super admin gets ['*'], which means everything.
   --------------------------------------------------------------------------- */
function acl_effective($userId = null) {
    global $db;
    static $cache = [];
    $uid = $userId !== null ? (int)$userId : (int)($_SESSION['user_id'] ?? 0);
    if ($uid <= 0) return [];
    if (isset($cache[$uid])) return $cache[$uid];

    if (acl_level($uid) === 'superadmin') { return $cache[$uid] = ['*']; }

    try {
        $u = $db->select("SELECT role_id, perm_extra, perm_denied FROM users WHERE id = ?", [$uid]);
    } catch (\Throwable $e) { $u = []; }
    if (empty($u)) return $cache[$uid] = [];

    $set = [];

    /* 1. the role */
    if (!empty($u[0]['role_id'])) {
        try {
            $r = $db->select("SELECT permissions, perms_json FROM roles WHERE id = ? AND (status IS NULL OR status = 1)", [(int)$u[0]['role_id']]);
        } catch (\Throwable $e) { $r = []; }
        if (!empty($r)) {
            $detailed = json_decode((string)($r[0]['perms_json'] ?? ''), true);
            if (is_array($detailed) && !empty($detailed)) {
                $set = $detailed;
            } else {
                /* a role saved before this system existed: open every module it
                   already held, so nobody loses access the day this goes live */
                $old = json_decode((string)$r[0]['permissions'], true);
                $set = acl_expand_legacy(is_array($old) ? $old : []);
            }
        }
    }

    /* 2. given to this person alone */
    $extra = json_decode((string)($u[0]['perm_extra'] ?? ''), true);
    if (is_array($extra)) { $set = array_merge($set, $extra); }

    /* 3. taken back from this person alone */
    $denied = json_decode((string)($u[0]['perm_denied'] ?? ''), true);
    if (is_array($denied) && !empty($denied)) { $set = array_diff($set, $denied); }

    return $cache[$uid] = array_values(array_unique($set));
}

/** An old-style permission list turned into the full detailed set. */
function acl_expand_legacy($old) {
    if (in_array('all', (array)$old, true)) return ['*'];
    $map = acl_legacy_map();
    $out = [];
    foreach ((array)$old as $k) {
        foreach ($map[$k] ?? [] as $mk) { $out = array_merge($out, acl_module_keys($mk)); }
    }
    return array_values(array_unique($out));
}

/* Forget the cached answer — used right after a grant is saved. */
function acl_reset_cache() {
    /* the static caches live inside the functions, so the simplest reliable
       reset is a redirect; this flag lets a page know it should not trust a
       value it read earlier in the same request. */
    $GLOBALS['__acl_dirty'] = true;
}

/* ---------------------------------------------------------------------------
   THE CHECKS
   --------------------------------------------------------------------------- */

/** Does this account hold this permission key? */
function acl_can($key, $userId = null) {
    if ($key === '' || $key === null) return true;
    $set = acl_effective($userId);
    if (in_array('*', $set, true)) return true;
    foreach ((array)$key as $k) {
        if ($k === '' || in_array($k, $set, true)) return true;
    }
    return false;
}

/** Every one of these keys, not just one of them. */
function acl_can_all($keys, $userId = null) {
    $set = acl_effective($userId);
    if (in_array('*', $set, true)) return true;
    foreach ((array)$keys as $k) { if (!in_array($k, $set, true)) return false; }
    return true;
}

/** May this account see this column? */
function acl_col($module, $column, $userId = null) {
    return acl_can("$module.col.$column", $userId);
}

/** Does the account hold anything at all inside a module? */
function acl_has_module($mk, $userId = null) {
    $set = acl_effective($userId);
    if (in_array('*', $set, true)) return true;
    $pre = $mk . '.';
    foreach ($set as $k) { if (strpos($k, $pre) === 0) return true; }
    return false;
}

/* ---------------------------------------------------------------------------
   THE CEILING — what this account is allowed to hand out
   --------------------------------------------------------------------------- */
function acl_ceiling($userId = null) {
    $set = acl_effective($userId);
    if (in_array('*', $set, true)) return acl_all_keys();
    return $set;
}

/** Keep only what the giver actually holds. Nothing else can ever be saved. */
function acl_filter_grant($requested, $userId = null) {
    $ceiling = acl_ceiling($userId);
    $valid   = acl_all_keys();
    $out = [];
    foreach ((array)$requested as $k) {
        $k = trim((string)$k);
        if ($k === '') continue;
        if (!in_array($k, $valid, true)) continue;     // not a real key
        if (!in_array($k, $ceiling, true)) continue;   // above the giver's own access
        $out[] = $k;
    }
    return array_values(array_unique($out));
}

/* ---------------------------------------------------------------------------
   WHO MAY CREATE WHOM
   --------------------------------------------------------------------------- */
function acl_creatable_levels($userId = null) {
    $lvl = acl_level($userId);
    if ($lvl === 'superadmin') return ['admin' => 'Admin', 'employee' => 'Employee'];
    if ($lvl === 'admin' && acl_can('staff.create_employee', $userId)) return ['employee' => 'Employee'];
    return [];
}
function acl_can_create_level($level, $userId = null) {
    return array_key_exists($level, acl_creatable_levels($userId));
}

/**
 * May this account manage that staff account?
 * A super admin manages everyone. An admin manages the employees only —
 * never another admin, and never themselves out of existence.
 */
function acl_can_manage_staff($targetUserId, $userId = null) {
    $me  = $userId !== null ? (int)$userId : (int)($_SESSION['user_id'] ?? 0);
    $lvl = acl_level($me);
    if ($lvl === 'superadmin') return true;
    if ($lvl !== 'admin') return false;
    if (!acl_can('staff.access', $me)) return false;
    return acl_level($targetUserId) === 'employee';
}

/* ---------------------------------------------------------------------------
   THE STAFF CREATION FORM — designed by the super admin
   --------------------------------------------------------------------------- */

/**
 * The boxes the form starts with.
 *   key, label, type, shows on, required, saved into a users column, hint, locked
 * The last flag marks the four boxes an account cannot be made without —
 * those can be renamed and re-ordered but never hidden or deleted. The rest are
 * ordinary boxes: keep them, rename them, hide them or throw them away.
 */
function acl_system_fields() {
    return [
        ['name',       'Full Name',       'text',     'both',     1, 'name',            'Sanjay Mondal',         1],
        ['mobile',     'Mobile Number',   'tel',      'both',     1, 'mobile',          '9876543210',            1],
        ['email',      'Email Address',   'email',    'both',     1, 'email',           'name@company.com',      1],
        ['password',   'Login Password',  'password', 'both',     1, 'password',        'At least 6 characters', 1],
        ['address',    'Full Address',    'textarea', 'both',     0, '',                'House, street, area',   0],
        ['whatsapp',   'WhatsApp Number', 'tel',      'both',     0, 'whatsapp_number', '9876543210',            0],
        ['gender',     'Gender',          'select',   'both',     0, 'gender',          '',                      0],
        ['joining',    'Joining Date',    'date',     'employee', 0, '',                '',                      0],
        ['designation','Designation',     'text',     'both',     0, '',                'Support Executive',     0],
    ];
}

/** Write the system fields in, once. Returns how many rows were added. */
function acl_seed_fields($db) {
    acl_ensure_schema($db);
    $conn = $db->getConnection();
    $n = 0; $order = 0;
    foreach (acl_system_fields() as $f) {
        $order += 10;
        $opts = $f[0] === 'gender' ? "Male\nFemale\nOther" : null;
        $sys  = (int)($f[7] ?? 0);
        try {
            $st = $conn->prepare(
                "INSERT INTO staff_form_fields
                    (field_key, label, field_type, applies_to, is_required, maps_to, placeholder, options, sort_order, is_system, status)
                 VALUES (?,?,?,?,?,?,?,?,?,?,1)
                 ON DUPLICATE KEY UPDATE is_system = VALUES(is_system), maps_to = VALUES(maps_to)");
            $st->bind_param('ssssisssii', $f[0], $f[1], $f[2], $f[3], $f[4], $f[5], $f[6], $opts, $order, $sys);
            $st->execute();
            if ($st->affected_rows > 0) $n++;
            $st->close();
        } catch (\Throwable $e) {}
    }
    return $n;
}

/** The fields to show on the Add Admin / Add Employee form. */
function acl_form_fields($db, $level = 'employee') {
    try {
        return $db->select(
            "SELECT * FROM staff_form_fields
             WHERE status = 1 AND (applies_to = 'both' OR applies_to = ?)
             ORDER BY sort_order ASC, id ASC", [$level]);
    } catch (\Throwable $e) { return []; }
}

/** The extra (non-column) values saved for one staff account. */
function acl_field_values($db, $userId) {
    $out = [];
    try {
        foreach ($db->select("SELECT field_key, field_value FROM staff_field_values WHERE user_id = ?", [(int)$userId]) as $r) {
            $out[$r['field_key']] = $r['field_value'];
        }
    } catch (\Throwable $e) {}
    return $out;
}

function acl_save_field_value($db, $userId, $key, $value) {
    try {
        $db->insert(
            "INSERT INTO staff_field_values (user_id, field_key, field_value) VALUES (?,?,?)
             ON DUPLICATE KEY UPDATE field_value = VALUES(field_value)",
            [(int)$userId, substr((string)$key, 0, 60), (string)$value]
        );
    } catch (\Throwable $e) {}
}

/* ---------------------------------------------------------------------------
   THE ACCESS LOG — who gave what to whom, and who took it back
   --------------------------------------------------------------------------- */
function acl_log($db, $targetType, $targetId, $targetName, $action, $granted = [], $revoked = [], $remark = '') {
    acl_ensure_schema($db);
    $me   = (int)($_SESSION['user_id'] ?? 0);
    $name = $_SESSION['user_name'] ?? '';
    if ($name === '' && $me) {
        try { $r = $db->select("SELECT name FROM users WHERE id = ?", [$me]); $name = $r[0]['name'] ?? ''; }
        catch (\Throwable $e) {}
    }
    $ip = '0.0.0.0';
    foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $k) {
        if (!empty($_SERVER[$k])) {
            $c = trim(explode(',', $_SERVER[$k])[0]);
            if (filter_var($c, FILTER_VALIDATE_IP)) { $ip = $c; break; }
        }
    }
    try {
        $db->insert(
            "INSERT INTO staff_access_log
                (target_type, target_id, target_name, action, granted, revoked, remark,
                 done_by, done_by_name, done_by_level, ip_address)
             VALUES (?,?,?,?,?,?,?,?,?,?,?)",
            [substr($targetType, 0, 20), (int)$targetId, substr((string)$targetName, 0, 150),
             substr((string)$action, 0, 30),
             !empty($granted) ? json_encode(array_values($granted)) : null,
             !empty($revoked) ? json_encode(array_values($revoked)) : null,
             $remark !== '' ? substr($remark, 0, 500) : null,
             $me ?: null, substr((string)$name, 0, 150), acl_level($me), $ip]
        );
    } catch (\Throwable $e) {}
}

/* ---------------------------------------------------------------------------
   A friendly stop instead of a bare die()
   --------------------------------------------------------------------------- */
function acl_deny($key = '') {
    $what = is_array($key) ? ($key[0] ?? '') : $key;
    $name = $what !== '' ? acl_key_label($what) : 'this section';
    http_response_code(403);
    ?><!DOCTYPE html>
    <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Access denied</title><script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"></head>
    <body class="bg-slate-100 min-h-screen flex items-center justify-center p-6 font-sans">
      <div class="max-w-md w-full bg-white rounded-2xl shadow-lg border border-slate-200 p-8 text-center">
        <div class="w-16 h-16 rounded-2xl bg-rose-50 text-rose-500 flex items-center justify-center text-2xl mx-auto mb-4"><i class="fas fa-lock"></i></div>
        <h1 class="text-xl font-extrabold text-slate-800 mb-2">You do not have access here</h1>
        <p class="text-sm text-slate-500 leading-relaxed mb-6">
          Your account has not been given <b><?php echo htmlspecialchars($name); ?></b>.
          Ask whoever set up your account to add it, and it will appear in your menu straight away.
        </p>
        <a href="index.php" class="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-slate-900 text-white text-sm font-bold"><i class="fas fa-arrow-left"></i> Back to dashboard</a>
      </div>
    </body></html><?php
    exit;
}

function acl_guard($key) { if (!acl_can($key)) { acl_deny($key); } }

/** How many keys of a module this account holds — used for the summary chips. */
function acl_module_count($mk, $userId = null) {
    $set = acl_effective($userId);
    $all = acl_module_keys($mk);
    if (in_array('*', $set, true)) return [count($all), count($all)];
    $have = 0;
    foreach ($all as $k) { if (in_array($k, $set, true)) $have++; }
    return [$have, count($all)];
}

} // end function_exists guard
