<?php
// JSON API: do not print PHP notices/warnings to stdout (breaks clients' JSON.parse).
ini_set('display_errors', '0');
ini_set('log_errors', '1');
error_reporting(E_ALL);

/**
 * MULTI-TENANT ATTENDANCE SYSTEM - API BACKEND
 * File: api.php
 * Security: JWT-like sessions, bcrypt, device binding, geo-validation
 */

// Ignore favicon requests to save resources
if (strpos($_SERVER['REQUEST_URI'] ?? '', 'favicon.ico') !== false) {
    http_response_code(404);
    exit;
}


// ─── CONFIGURATION ───────────────────────────────────────────────────────────
$is_remote_server = (strpos($_SERVER['HTTP_HOST'] ?? '', 'tsbd.com.au') !== false);
define('DB_HOST', $is_remote_server ? 'localhost' : 'webhosting3010.is.cc');
define('DB_NAME', 'skinsion_Hr_dba');
define('DB_USER', 'skinsion_Hr_Man');
define('DB_PASS', 'Xu1AwM8%g;-E16_m');
define('DB_CHARSET', 'utf8mb4');

define('SECRET_KEY', '55515151');
define('SESSION_LIFETIME', 3600 * 8); // 8 hours
define('QR_TOKEN_LIFETIME', 120);      // 2 minutes
define('UPLOAD_DIR', __DIR__ . '/uploads/selfies/');
define('MAX_DISTANCE_METERS', 100);
define('GEOFENCE_TOLERANCE_METERS', 35);
/** Bump when adding incremental DDL; wrap new migrations in `if ($currentSchemaVersion < N)`. */
define('HRPRO_SCHEMA_VERSION', 2);

// ─── SMTP CONFIGURATION ──────────────────────────────────────────────────────
define('SMTP_HOST', 'mail.officetech.com.eg'); // مثال: mail.domain.com أو smtp.gmail.com
define('SMTP_PORT', 587); // 587 for TLS, 465 for SSL
define('SMTP_USER', 'no-reply@officetech.com.eg');
define('SMTP_PASS', 'YOUR_SMTP_PASSWORD_HERE');
define('SMTP_FROM_EMAIL', 'no-reply@officetech.com.eg');

/** Web Push (VAPID): save keys in Master (Outbound mail → Web Push) or set env HRPRO_VAPID_PUBLIC / HRPRO_VAPID_PRIVATE / HRPRO_VAPID_SUBJECT; run `composer install` in /hr for minishlink/web-push. */
define('ADMIN_PUSH_VAPID_PUBLIC', getenv('HRPRO_VAPID_PUBLIC') ?: '');
define('ADMIN_PUSH_VAPID_PRIVATE', getenv('HRPRO_VAPID_PRIVATE') ?: '');
define('ADMIN_PUSH_SUBJECT', getenv('HRPRO_VAPID_SUBJECT') ?: 'mailto:admin@localhost');


// ─── CORS & HEADERS ──────────────────────────────────────────────────────────
header('Content-Type: application/json; charset=utf-8');
// كان السابق: Access-Control-Allow-Origin: * (أي موقع يقدر يستدعي الـ API من المتصفح).
// تم التقييد إلى https://tsbd.com.au فقط — المتصفح يرفض طلبات CORS من أي Origin آخر.
header('Access-Control-Allow-Origin: https://tsbd.com.au');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-ID');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }

// ─── DATABASE ────────────────────────────────────────────────────────────────
function db(): PDO {
    static $pdo = null;
    if ($pdo) return $pdo;
    try {
        $dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
        $pdo = new PDO($dsn, DB_USER, DB_PASS, [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ]);

        try {
            $pdo->exec('CREATE TABLE IF NOT EXISTS hrpro_schema_version (
                id TINYINT UNSIGNED NOT NULL PRIMARY KEY DEFAULT 1,
                version INT UNSIGNED NOT NULL DEFAULT 0
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
            $pdo->exec('INSERT IGNORE INTO hrpro_schema_version (id, version) VALUES (1, 0)');
        } catch (Exception $e) { /* ignore */ }

        $currentSchemaVersion = 0;
        try {
            $rv = $pdo->query('SELECT version FROM hrpro_schema_version WHERE id = 1 LIMIT 1');
            if ($rv) {
                $vr = $rv->fetch(PDO::FETCH_ASSOC);
                $currentSchemaVersion = (int) ($vr['version'] ?? 0);
            }
        } catch (Exception $e) { /* ignore */ }

        if ($currentSchemaVersion >= HRPRO_SCHEMA_VERSION) {
            ensureLeaveRequestPermissionKindColumn($pdo);
            ensureEmployeeBirthDateColumn($pdo);
            adminMessagesEnsureDeletedColumn($pdo);
            return $pdo;
        }

        if ($currentSchemaVersion < 1) {

        // Auto-Migration: Add missing column 'must_change_password' if not exists
        try {
            $pdo->exec("ALTER TABLE employees ADD COLUMN must_change_password TINYINT(1) DEFAULT 0 AFTER password");
        } catch (Exception $e) { /* likely already exists */ }

        // leave_policies: deduction day equivalents (full / half / quarter)
        foreach ([
            "ALTER TABLE leave_policies ADD COLUMN deduct_full_day DECIMAL(5,2) NOT NULL DEFAULT 1.00",
            "ALTER TABLE leave_policies ADD COLUMN deduct_half_day DECIMAL(5,2) NOT NULL DEFAULT 0.50",
            "ALTER TABLE leave_policies ADD COLUMN deduct_quarter_day DECIMAL(5,2) NOT NULL DEFAULT 0.25",
        ] as $sql) {
            try {
                $pdo->exec($sql);
            } catch (Exception $e) { /* column exists */ }
        }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS admin_notifications (
                id INT AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(64) NOT NULL,
                type VARCHAR(48) NOT NULL DEFAULT 'general',
                title VARCHAR(255) NOT NULL,
                body TEXT NULL,
                action_url VARCHAR(512) NULL,
                is_read TINYINT(1) NOT NULL DEFAULT 0,
                is_deleted TINYINT(1) NOT NULL DEFAULT 0,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_admin_notif_company_read (company_id, is_deleted, is_read),
                INDEX idx_admin_notif_company_created (company_id, created_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }
        try {
            $pdo->exec("ALTER TABLE admin_notifications ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0");
        } catch (Exception $e) { /* column exists */ }
        try {
            $pdo->exec("CREATE INDEX idx_admin_notif_company_deleted_read ON admin_notifications (company_id, is_deleted, is_read)");
        } catch (Exception $e) { /* index exists */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS admin_push_subscriptions (
                id INT AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(64) NOT NULL,
                endpoint_hash CHAR(64) NOT NULL,
                subscription_json TEXT NOT NULL,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                UNIQUE KEY uq_admin_push_company_endpoint (company_id, endpoint_hash),
                INDEX idx_admin_push_company (company_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS employee_push_subscriptions (
                id INT AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(64) NOT NULL,
                employee_id INT NOT NULL,
                endpoint_hash CHAR(64) NOT NULL,
                subscription_json TEXT NOT NULL,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                UNIQUE KEY uq_emp_push_company_endpoint (company_id, endpoint_hash),
                INDEX idx_emp_push_company (company_id),
                INDEX idx_emp_push_employee (employee_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS company_news_ticker (
                id INT AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(64) NOT NULL,
                message TEXT NOT NULL,
                is_active TINYINT(1) NOT NULL DEFAULT 1,
                sort_order INT NOT NULL DEFAULT 0,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_news_company_active (company_id, is_active, sort_order),
                INDEX idx_news_company_created (company_id, created_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }

        // employee_team_members: many-to-many mapping between supervisors and team members
        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS employee_team_members (
                id INT AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(36) NOT NULL,
                supervisor_id INT(10) UNSIGNED NOT NULL,
                employee_id INT(10) UNSIGNED NOT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                UNIQUE KEY uq_company_supervisor_employee (company_id, supervisor_id, employee_id),
                INDEX idx_team_company_supervisor (company_id, supervisor_id),
                INDEX idx_team_company_employee (company_id, employee_id),
                CONSTRAINT employee_team_members_ibfk_company
                  FOREIGN KEY (company_id) REFERENCES companies (company_id) ON DELETE CASCADE,
                CONSTRAINT employee_team_members_ibfk_supervisor
                  FOREIGN KEY (supervisor_id) REFERENCES employees (id) ON DELETE CASCADE,
                CONSTRAINT employee_team_members_ibfk_employee
                  FOREIGN KEY (employee_id) REFERENCES employees (id) ON DELETE CASCADE
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS leave_manual_deductions (
                id INT AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(64) NOT NULL,
                employee_id INT NOT NULL,
                deduction_date DATE NOT NULL,
                leave_type VARCHAR(32) NOT NULL,
                days DECIMAL(5,2) NOT NULL,
                reason VARCHAR(500) DEFAULT NULL,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_lmd_emp_date (employee_id, deduction_date),
                INDEX idx_lmd_company_date (company_id, deduction_date)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS trial_requests (
                id INT AUTO_INCREMENT PRIMARY KEY,
                company_name VARCHAR(255) NOT NULL,
                phone VARCHAR(40) NOT NULL,
                email VARCHAR(255) NOT NULL,
                ip_address VARCHAR(45) DEFAULT NULL,
                user_agent VARCHAR(512) DEFAULT NULL,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_trial_created (created_at),
                INDEX idx_trial_ip_created (ip_address, created_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS company_sub_admins (
                id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(36) NOT NULL,
                username VARCHAR(100) NOT NULL,
                password_hash VARCHAR(255) NOT NULL,
                display_name VARCHAR(120) NOT NULL DEFAULT '',
                is_active TINYINT(1) NOT NULL DEFAULT 1,
                created_by_super_admin_id INT UNSIGNED NULL,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                UNIQUE KEY uq_sub_admin_username (username),
                INDEX idx_sub_admin_company (company_id, is_active),
                CONSTRAINT fk_sub_admin_company
                  FOREIGN KEY (company_id) REFERENCES companies (company_id) ON DELETE CASCADE
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS company_sub_admin_permissions (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                sub_admin_id INT UNSIGNED NOT NULL,
                module_key VARCHAR(64) NOT NULL,
                can_read TINYINT(1) NOT NULL DEFAULT 0,
                can_create TINYINT(1) NOT NULL DEFAULT 0,
                can_update TINYINT(1) NOT NULL DEFAULT 0,
                can_delete TINYINT(1) NOT NULL DEFAULT 0,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                UNIQUE KEY uq_sub_admin_module (sub_admin_id, module_key),
                INDEX idx_sub_admin_perm_sub (sub_admin_id),
                CONSTRAINT fk_sub_admin_perm_sub
                  FOREIGN KEY (sub_admin_id) REFERENCES company_sub_admins (id) ON DELETE CASCADE
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS system_smtp_settings (
                id TINYINT UNSIGNED NOT NULL PRIMARY KEY DEFAULT 1,
                smtp_host VARCHAR(255) NOT NULL DEFAULT '',
                smtp_port INT UNSIGNED NOT NULL DEFAULT 587,
                smtp_encryption VARCHAR(8) NOT NULL DEFAULT 'tls',
                smtp_username VARCHAR(255) NOT NULL DEFAULT '',
                smtp_password_blob VARCHAR(1024) NOT NULL DEFAULT '',
                from_email VARCHAR(255) NOT NULL DEFAULT '',
                from_name VARCHAR(255) NOT NULL DEFAULT 'HR Pro',
                reply_to_email VARCHAR(255) NULL DEFAULT NULL,
                updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
            $pdo->exec("INSERT IGNORE INTO system_smtp_settings (id) VALUES (1)");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS system_webpush_settings (
                id TINYINT UNSIGNED NOT NULL PRIMARY KEY DEFAULT 1,
                vapid_public_key VARCHAR(255) NOT NULL DEFAULT '',
                vapid_private_blob VARCHAR(1024) NOT NULL DEFAULT '',
                vapid_subject VARCHAR(255) NOT NULL DEFAULT 'mailto:admin@localhost',
                updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
            $pdo->exec("INSERT IGNORE INTO system_webpush_settings (id) VALUES (1)");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS smtp_test_log (
                id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
                tested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                recipient VARCHAR(255) NOT NULL DEFAULT '',
                status ENUM('sent','failed') NOT NULL DEFAULT 'failed',
                error_detail TEXT NULL,
                smtp_host VARCHAR(255) NOT NULL DEFAULT '',
                smtp_port INT UNSIGNED NOT NULL DEFAULT 0,
                smtp_encryption VARCHAR(10) NOT NULL DEFAULT ''
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */ }

        try {
            $pdo->exec('CREATE INDEX idx_audit_login_purge ON audit_log (action(32), actor_type, company_id)');
        } catch (Exception $e) { /* index may already exist */ }

        foreach ([
            'ALTER TABLE companies ADD COLUMN account_close_at DATE NULL DEFAULT NULL COMMENT \'Access allowed through this calendar day\'',
            'ALTER TABLE companies ADD COLUMN contact_person_name VARCHAR(255) NULL DEFAULT NULL',
            'ALTER TABLE companies ADD COLUMN contact_person_phone VARCHAR(40) NULL DEFAULT NULL',
            'ALTER TABLE companies ADD COLUMN employee_welcome_email_body MEDIUMTEXT NULL DEFAULT NULL COMMENT \'Custom welcome/credentials email template for employees\'',
            'ALTER TABLE companies ADD COLUMN mobile_app_url VARCHAR(2048) NULL DEFAULT NULL COMMENT \'Custom employee mobile app / PWA link for this company\'',
        ] as $sql) {
            try {
                $pdo->exec($sql);
            } catch (Exception $e) { /* column exists */
            }
        }

        foreach ([
            'ALTER TABLE branches ADD COLUMN photo_url VARCHAR(512) NULL DEFAULT NULL',
            'ALTER TABLE branches ADD COLUMN map_link VARCHAR(512) NULL DEFAULT NULL',
        ] as $sql) {
            try {
                $pdo->exec($sql);
            } catch (Exception $e) { /* column exists */
            }
        }
        try {
            $pdo->exec('ALTER TABLE branches DROP COLUMN map_link');
        } catch (Exception $e) { /* column missing or not supported */
        }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS employee_documents (
                id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(36) NOT NULL,
                employee_id INT UNSIGNED NOT NULL,
                file_path VARCHAR(512) NOT NULL,
                original_name VARCHAR(255) NOT NULL,
                mime_type VARCHAR(120) DEFAULT NULL,
                file_size INT UNSIGNED DEFAULT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_empdoc_emp (employee_id, company_id),
                INDEX idx_empdoc_co (company_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */
        }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS company_official_holidays (
                id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(36) NOT NULL,
                holiday_date DATE NOT NULL,
                title VARCHAR(200) NOT NULL,
                notes VARCHAR(500) DEFAULT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                UNIQUE KEY uq_company_official_holiday (company_id, holiday_date),
                INDEX idx_coh_company_date (company_id, holiday_date)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */
        }

        foreach ([
            'ALTER TABLE leave_policies ADD COLUMN salary_deduct_enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT \'Final report: allow marking unpaid day as salary deduction\'',
            'ALTER TABLE leave_policies ADD COLUMN salary_deduct_allow_with_balance TINYINT(1) NOT NULL DEFAULT 0 COMMENT \'Allow salary deduction mark even if leave balance remains\'',
        ] as $sql) {
            try {
                $pdo->exec($sql);
            } catch (Exception $e) { /* exists */
            }
        }

        foreach ([
            'ALTER TABLE leave_policies ADD COLUMN grace_late_minutes INT UNSIGNED NOT NULL DEFAULT 5 COMMENT \'Minutes after official start before lateness counts\'',
            'ALTER TABLE leave_policies ADD COLUMN grace_early_leave_minutes INT UNSIGNED NOT NULL DEFAULT 5 COMMENT \'Minutes before official end ignored for early checkout\'',
            'ALTER TABLE leave_policies ADD COLUMN late_minutes_quarter SMALLINT UNSIGNED NULL DEFAULT NULL COMMENT \'NULL = use default 15\'',
            'ALTER TABLE leave_policies ADD COLUMN late_minutes_half SMALLINT UNSIGNED NULL DEFAULT NULL',
            'ALTER TABLE leave_policies ADD COLUMN late_minutes_full SMALLINT UNSIGNED NULL DEFAULT NULL',
            'ALTER TABLE leave_policies ADD COLUMN early_leave_minutes_quarter SMALLINT UNSIGNED NULL DEFAULT NULL',
            'ALTER TABLE leave_policies ADD COLUMN early_leave_minutes_half SMALLINT UNSIGNED NULL DEFAULT NULL',
            'ALTER TABLE leave_policies ADD COLUMN early_leave_minutes_full SMALLINT UNSIGNED NULL DEFAULT NULL',
            'ALTER TABLE leave_policies ADD COLUMN auto_deduct_leave_type VARCHAR(16) NOT NULL DEFAULT \'annual\'',
            'ALTER TABLE leave_policies ADD COLUMN auto_deduct_max_per_day DECIMAL(5,2) NOT NULL DEFAULT 1.00',
            'ALTER TABLE leave_policies ADD COLUMN auto_deduct_combine_late_early TINYINT(1) NOT NULL DEFAULT 0',
            'ALTER TABLE leave_policies ADD COLUMN auto_deduct_ignore_codes VARCHAR(400) NULL DEFAULT NULL COMMENT \'Comma list: late,early_leave,...\'',
            'ALTER TABLE leave_policies ADD COLUMN permission_monthly_limit INT UNSIGNED NULL DEFAULT NULL COMMENT \'Approved permission requests allowed per employee per month\'',
        ] as $sql) {
            try {
                $pdo->exec($sql);
            } catch (Exception $e) { /* exists */
            }
        }

        foreach ([
            'ALTER TABLE leave_requests ADD COLUMN permission_from CHAR(5) NULL DEFAULT NULL COMMENT \'HH:MM for permission requests\'',
            'ALTER TABLE leave_requests ADD COLUMN permission_to CHAR(5) NULL DEFAULT NULL COMMENT \'HH:MM for permission requests\'',
            'ALTER TABLE leave_requests ADD COLUMN permission_kind VARCHAR(16) NULL DEFAULT NULL COMMENT \'mission=internal field mission; NULL=standard permission\'',
        ] as $sql) {
            try {
                $pdo->exec($sql);
            } catch (Exception $e) { /* exists */
            }
        }
        // leave_type column (older installs); then widen for 'permission' and other codes.
        try {
            $pdo->exec("ALTER TABLE leave_requests ADD COLUMN leave_type VARCHAR(32) NOT NULL DEFAULT 'annual' AFTER end_date");
        } catch (Exception $e) { /* exists */
        }
        try {
            $pdo->exec("ALTER TABLE leave_requests MODIFY COLUMN leave_type VARCHAR(32) NOT NULL DEFAULT 'annual'");
        } catch (Exception $e) { /* ignore */
        }

        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS employee_salary_deductions (
                id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                company_id VARCHAR(36) NOT NULL,
                employee_id INT UNSIGNED NOT NULL,
                deduction_date DATE NOT NULL,
                days DECIMAL(5,2) NOT NULL DEFAULT 1.00,
                reason VARCHAR(500) DEFAULT NULL,
                force_despite_balance TINYINT(1) NOT NULL DEFAULT 0,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                UNIQUE KEY uq_esd_emp_date (company_id, employee_id, deduction_date),
                INDEX idx_esd_company (company_id),
                INDEX idx_esd_employee (employee_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Exception $e) { /* ignore */
        }

        try {
            $pdo->exec('ALTER TABLE departments ADD COLUMN is_active TINYINT(1) NOT NULL DEFAULT 1');
        } catch (Exception $e) { /* column exists */
        }

        foreach ([
            'ALTER TABLE employees ADD COLUMN show_employee_report TINYINT(1) NOT NULL DEFAULT 0 COMMENT \'Employee report screen visibility\'',
            'ALTER TABLE employees ADD COLUMN show_public_holidays TINYINT(1) NOT NULL DEFAULT 0 COMMENT \'Public holidays screen visibility\'',
        ] as $sql) {
            try {
                $pdo->exec($sql);
            } catch (Exception $e) { /* exists */
            }
        }

        }

        try {
            $pdo->prepare('UPDATE hrpro_schema_version SET version = ? WHERE id = 1')->execute([HRPRO_SCHEMA_VERSION]);
        } catch (Exception $e) { /* ignore */ }

    } catch (PDOException $e) {
        // If PDO was created successfully, do not fail the whole request because
        // of a migration/DDL issue. Keep serving using the live connection.
        if ($pdo instanceof PDO) {
            error_log('DB auto-migration warning: ' . $e->getMessage());
            ensureLeaveRequestPermissionKindColumn($pdo);
            ensureEmployeeBirthDateColumn($pdo);
            adminMessagesEnsureDeletedColumn($pdo);
            return $pdo;
        }
        respond(500, 'Database connection failed');
    }
    ensureLeaveRequestPermissionKindColumn($pdo);
    ensureEmployeeBirthDateColumn($pdo);
    adminMessagesEnsureDeletedColumn($pdo);
    return $pdo;
}

/** True when subscription / account-close alerts stay hidden (more than 30 full days before close date). */
function shouldSuppressPreCloseSubscriptionAlerts(PDO $pdo, string $companyId): bool {
    $stmt = $pdo->prepare('SELECT account_close_at FROM companies WHERE company_id = ?');
    $stmt->execute([$companyId]);
    $r = $stmt->fetch();
    if (!$r || empty($r['account_close_at']) || $r['account_close_at'] === '0000-00-00') {
        return false;
    }
    $close = (string) $r['account_close_at'];
    $closeT = strtotime($close . ' 12:00:00');
    $todayT = strtotime('today');
    if ($closeT === false || $todayT === false) {
        return false;
    }
    $daysLeft = (int) floor(($closeT - $todayT) / 86400);
    if ($daysLeft < 0) {
        return false;
    }

    return $daysLeft > 30;
}

function adminNotificationsHasDeletedColumn(PDO $pdo): bool {
    try {
        $st = $pdo->query("SHOW COLUMNS FROM admin_notifications LIKE 'is_deleted'");
        return ($st && $st->fetch()) ? true : false;
    } catch (Exception $e) {
        return false;
    }
}

function adminNotificationsEnsureDeletedColumn(PDO $pdo): bool {
    if (adminNotificationsHasDeletedColumn($pdo)) {
        return true;
    }
    try {
        $pdo->exec("ALTER TABLE admin_notifications ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0");
    } catch (Exception $e) { /* column may already exist */ }
    try {
        $pdo->exec("CREATE INDEX idx_admin_notif_company_deleted_read ON admin_notifications (company_id, is_deleted, is_read)");
    } catch (Exception $e) { /* index may already exist */ }
    return adminNotificationsHasDeletedColumn($pdo);
}

/**
 * Format a UTC Unix timestamp (from MySQL UNIX_TIMESTAMP on TIMESTAMP columns) as Africa/Cairo wall time for display.
 */
function hrproUnixTimestampToCairo(int $unixTs): string {
    if ($unixTs <= 0) {
        return '';
    }
    try {
        $dt = new DateTime('@' . $unixTs);
        $dt->setTimezone(new DateTimeZone('Africa/Cairo'));
        return $dt->format('Y-m-d H:i:s');
    } catch (Exception $e) {
        return '';
    }
}

/**
 * Normalize a request date to strict Y-m-d or return $fallback (defense-in-depth for SQL date parameters).
 */
function hrproRequireYmd(mixed $in, string $fallback): string {
    if (!is_string($in)) {
        return $fallback;
    }
    $s = trim($in);

    return preg_match('/^\d{4}-\d{2}-\d{2}$/', $s) ? $s : $fallback;
}

// ─── HELPERS ─────────────────────────────────────────────────────────────────
/** Clamp policy deduction “day” value for DB (0–99.99). */
function policyDeductionDecimal($v, float $default): float {
    if ($v === null || $v === '') {
        return $default;
    }
    if (!is_numeric($v)) {
        return $default;
    }
    $n = round((float) $v, 2);
    if ($n < 0) {
        return 0.0;
    }
    if ($n > 99.99) {
        return 99.99;
    }
    return $n;
}

/** Approved leave calendar days overlapping [yearStart, yearEnd] per leave_type. */
function leaveApprovedUsedInRange(PDO $pdo, int $employeeId, string $companyId, string $yearStart, string $yearEnd): array {
    $out = ['annual' => 0.0, 'casual' => 0.0, 'sick' => 0.0];
    $stmt = $pdo->prepare("
        SELECT leave_type,
            SUM(DATEDIFF(LEAST(end_date, ?), GREATEST(start_date, ?)) + 1) AS used
        FROM leave_requests
        WHERE employee_id = ? AND company_id = ? AND status = 'approved'
          AND leave_type IN ('annual','casual','sick')
          AND start_date <= ? AND end_date >= ?
        GROUP BY leave_type
    ");
    $stmt->execute([$yearEnd, $yearStart, $employeeId, $companyId, $yearEnd, $yearStart]);
    while ($b = $stmt->fetch()) {
        $lt = $b['leave_type'] ?? '';
        if ($lt !== '' && isset($out[$lt])) {
            $out[$lt] = (float) $b['used'];
        }
    }
    return $out;
}

/** Manual admin deductions (fractional days) in [yearStart, yearEnd]. */
function leaveManualUsedInRange(PDO $pdo, int $employeeId, string $companyId, string $yearStart, string $yearEnd): array {
    $out = ['annual' => 0.0, 'casual' => 0.0, 'sick' => 0.0];
    $stmt = $pdo->prepare("
        SELECT leave_type, SUM(days) AS used
        FROM leave_manual_deductions
        WHERE employee_id = ? AND company_id = ?
          AND deduction_date BETWEEN ? AND ?
        GROUP BY leave_type
    ");
    $stmt->execute([$employeeId, $companyId, $yearStart, $yearEnd]);
    while ($b = $stmt->fetch()) {
        $lt = $b['leave_type'] ?? '';
        if ($lt !== '' && isset($out[$lt])) {
            $out[$lt] = (float) $b['used'];
        }
    }
    return $out;
}

/** First calendar workday of $year (English weekday names vs policy weekend list). */
function leaveLedgerFirstWorkdayOfYear(int $year, array $weekendDaysEn): string
{
    $d = new DateTime(sprintf('%04d-01-01', $year));
    for ($i = 0; $i < 14; $i++) {
        if (!in_array($d->format('l'), $weekendDaysEn, true)) {
            return $d->format('Y-m-d');
        }
        $d->modify('+1 day');
    }

    return sprintf('%04d-01-01', $year);
}

/** Work dates between start and end inclusive, excluding weekend day names. */
function leaveLedgerWorkdaysBetween(string $start, string $end, array $weekendDaysEn): array
{
    $out = [];
    $cur = new DateTime($start);
    $endDt = new DateTime($end);
    if ($cur > $endDt) {
        return $out;
    }
    $endDt->modify('+1 day');
    while ($cur < $endDt) {
        if (!in_array($cur->format('l'), $weekendDaysEn, true)) {
            $out[] = $cur->format('Y-m-d');
        }
        $cur->modify('+1 day');
    }

    return $out;
}

/** Distinct allowed deduction day fractions from policy row (full / half / quarter). */
/** Every leave type with entitlement > 0 has rem <= 0 (no remaining paid leave). */
function finalReportAllLeaveRemDepleted(array $balancesOut): bool {
    $hadEntitlement = false;
    foreach (['annual', 'casual', 'sick'] as $lt) {
        $b = $balancesOut[$lt] ?? null;
        if (!$b || !is_array($b)) {
            continue;
        }
        $tot = (float) ($b['total'] ?? 0);
        if ($tot <= 0) {
            continue;
        }
        $hadEntitlement = true;
        if ((float) ($b['rem'] ?? 0) > 0.001) {
            return false;
        }
    }
    return true;
}

function policyDeductionFractionOptions(array $policyRow): array {
    $a = policyDeductionDecimal($policyRow['deduct_full_day'] ?? null, 1.0);
    $b = policyDeductionDecimal($policyRow['deduct_half_day'] ?? null, 0.5);
    $c = policyDeductionDecimal($policyRow['deduct_quarter_day'] ?? null, 0.25);
    $opts = array_values(array_unique([round($a, 2), round($b, 2), round($c, 2)]));
    sort($opts);
    return $opts;
}

/** Scheduled on-site hours from policy times (for overtime suggestion). */
function finalReportExpectedWorkingHours(string $schIn, string $schOut): float {
    $si = substr(preg_replace('/\s+/', '', (string) $schIn), 0, 8);
    $so = substr(preg_replace('/\s+/', '', (string) $schOut), 0, 8);
    if (strlen($si) < 5) {
        $si = '09:00';
    }
    if (strlen($so) < 5) {
        $so = '17:00';
    }
    $si = substr($si, 0, 5);
    $so = substr($so, 0, 5);
    $t1 = strtotime('2000-01-01 ' . $si . ':00');
    $t2 = strtotime('2000-01-01 ' . $so . ':00');
    if (!$t1 || !$t2 || $t2 <= $t1) {
        return 8.0;
    }
    return round(($t2 - $t1) / 3600, 2);
}

/** Stable codes for UI filter + GET reports/final-issues. */
function finalReportSuggestionCodes(): array {
    return ['absent', 'late', 'early_leave', 'incomplete_checkout', 'overtime', 'location_alert', 'distance_far', 'external_no_mission'];
}

function finalReportParseHmToTs(string $dateStr, $hmRaw): ?int
{
    $hm = substr(preg_replace('/\s+/', '', (string) $hmRaw), 0, 5);
    if (!preg_match('/^\d{2}:\d{2}$/', $hm)) {
        return null;
    }
    $ts = strtotime($dateStr . ' ' . $hm . ':00');
    if (!$ts) {
        return null;
    }

    return (int) $ts;
}

/**
 * Sum of approved permission minutes that overlap the late window.
 *
 * @param list<array<string,mixed>> $approvedPermissions
 */
function finalReportCoveredLateMinutes(string $dateStr, int $lateStartTs, int $lateEndTs, array $approvedPermissions): int
{
    if ($lateEndTs <= $lateStartTs || $approvedPermissions === []) {
        return 0;
    }
    $ranges = [];
    foreach ($approvedPermissions as $perm) {
        if (!is_array($perm)) {
            continue;
        }
        $fromTs = finalReportParseHmToTs($dateStr, $perm['permission_from'] ?? null);
        $toTs = finalReportParseHmToTs($dateStr, $perm['permission_to'] ?? null);
        if ($fromTs === null || $toTs === null || $toTs <= $fromTs) {
            continue;
        }
        $s = max($lateStartTs, $fromTs);
        $e = min($lateEndTs, $toTs);
        if ($e > $s) {
            $ranges[] = [$s, $e];
        }
    }
    if ($ranges === []) {
        return 0;
    }
    usort($ranges, static function (array $a, array $b): int {
        return $a[0] <=> $b[0];
    });
    $merged = [];
    foreach ($ranges as $rg) {
        if ($merged === [] || $rg[0] > $merged[count($merged) - 1][1]) {
            $merged[] = $rg;
            continue;
        }
        $merged[count($merged) - 1][1] = max($merged[count($merged) - 1][1], $rg[1]);
    }
    $coveredSecs = 0;
    foreach ($merged as $m) {
        $coveredSecs += max(0, (int) ($m[1] - $m[0]));
    }

    return max(0, (int) floor($coveredSecs / 60));
}

/**
 * Net late minutes after grace and approved permission overlap.
 *
 * @param array<string,mixed> $row
 */
function finalReportNetLateMinutes(array $row, int $graceMinutes): int
{
    $dateStr = (string) ($row['date'] ?? '');
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateStr)) {
        return 0;
    }
    $ciRaw = $row['check_in'] ?? null;
    if (!$ciRaw) {
        return 0;
    }
    $schInRaw = (string) ($row['sch_in'] ?? '09:00:00');
    $schHM = substr(preg_replace('/\s+/', '', $schInRaw), 0, 5);
    if (strlen($schHM) < 5) {
        $schHM = '09:00';
    }
    $officialTs = finalReportParseHmToTs($dateStr, $schHM);
    $ciTs = strtotime((string) $ciRaw);
    if ($officialTs === null || !$ciTs) {
        return 0;
    }
    $graceEndTs = $officialTs + max(0, $graceMinutes) * 60;
    if ($ciTs <= $graceEndTs) {
        return 0;
    }
    $lateMin = (int) floor(($ciTs - $graceEndTs) / 60);
    if ($lateMin < 1) {
        return 0;
    }
    $approvedPermissions = $row['approved_permissions'] ?? [];
    if (!is_array($approvedPermissions)) {
        $approvedPermissions = [];
    }
    $coveredMin = finalReportCoveredLateMinutes($dateStr, $graceEndTs, (int) $ciTs, $approvedPermissions);

    return max(0, $lateMin - $coveredMin);
}

/**
 * Non-binding issue hints for a single calendar row (manager review).
 * @param array<string,mixed> $row
 * @return list<string>
 */
function finalReportRowSuggestions(array $row): array {
    $codes = [];
    $st = $row['status'] ?? '';
    if ($st === 'weekend' || $st === 'leave' || $st === 'official_holiday') {
        return [];
    }
    if ($st === 'absent') {
        return ['absent'];
    }
    if ($st !== 'present') {
        return [];
    }
    $schIn = substr((string) ($row['sch_in'] ?? '09:00:00'), 0, 5);
    $schOut = substr((string) ($row['sch_out'] ?? '17:00:00'), 0, 5);
    $ci = $row['check_in'] ?? null;
    $co = $row['check_out'] ?? null;
    if ($ci) {
        $graceLate = max(0, (int) ($row['grace_late_minutes'] ?? 0));
        if (finalReportNetLateMinutes($row, $graceLate) > 0) {
            $codes[] = 'late';
        }
    }
    if ($co) {
        $cot = substr($co, 11, 5);
        if ($cot < $schOut) {
            $codes[] = 'early_leave';
        }
    }
    if ($ci && !$co) {
        $codes[] = 'incomplete_checkout';
    }
    $exp = finalReportExpectedWorkingHours((string) ($row['sch_in'] ?? '09:00:00'), (string) ($row['sch_out'] ?? '17:00:00'));
    if (!empty($row['work_hours']) && (float) $row['work_hours'] > $exp + 0.5) {
        $codes[] = 'overtime';
    }
    $inLoc = $row['in_location_status'] ?? null;
    $outLoc = $row['out_location_status'] ?? null;
    if ($inLoc === 'alert' || $outLoc === 'alert') {
        $codes[] = 'location_alert';
    }
    $inD = isset($row['in_distance_m']) ? (float) $row['in_distance_m'] : 0.0;
    $outD = isset($row['out_distance_m']) ? (float) $row['out_distance_m'] : 0.0;
    $mx = max($inD, $outD);
    if ($mx >= 250 && $inLoc !== 'alert' && $outLoc !== 'alert') {
        $codes[] = 'distance_far';
    }
    if (($row['logged_work_nature'] ?? '') === 'external') {
        $vd = trim((string) ($row['visit_details'] ?? ''));
        if ($vd === '') {
            $codes[] = 'external_no_mission';
        }
    }

    return array_values(array_unique($codes));
}

/** @param array<int, array<string,mixed>> $report */
function finalReportAttachSuggestions(array &$report): void {
    foreach ($report as $i => $row) {
        $s = finalReportRowSuggestions($row);
        $report[$i]['suggestions'] = $s;
        $report[$i]['has_issues'] = count($s) > 0;
    }
}

/** @return list<string> */
function leaveSettlementParseIgnoreCodes(?string $csv): array {
    $csv = trim((string) $csv);
    if ($csv === '') {
        return [];
    }
    $out = [];
    foreach (explode(',', $csv) as $p) {
        $p = trim($p);
        if ($p !== '') {
            $out[] = $p;
        }
    }

    return array_values(array_unique($out));
}

/**
 * @param array<string,mixed> $empRow employee row joined with leave_policies columns
 * @return array<string,mixed>
 */
function leaveSettlementPolicyFromEmployeeRow(array $empRow): array
{
    $dq = policyDeductionDecimal($empRow['deduct_quarter_day'] ?? null, 0.25);
    $dh = policyDeductionDecimal($empRow['deduct_half_day'] ?? null, 0.5);
    $df = policyDeductionDecimal($empRow['deduct_full_day'] ?? null, 1.0);

    return [
        'grace_late_minutes' => max(0, (int) ($empRow['grace_late_minutes'] ?? 5)),
        'grace_early_leave_minutes' => max(0, (int) ($empRow['grace_early_leave_minutes'] ?? 5)),
        'late_minutes_quarter' => $empRow['late_minutes_quarter'] !== null && $empRow['late_minutes_quarter'] !== '' ? (int) $empRow['late_minutes_quarter'] : null,
        'late_minutes_half' => $empRow['late_minutes_half'] !== null && $empRow['late_minutes_half'] !== '' ? (int) $empRow['late_minutes_half'] : null,
        'late_minutes_full' => $empRow['late_minutes_full'] !== null && $empRow['late_minutes_full'] !== '' ? (int) $empRow['late_minutes_full'] : null,
        'early_leave_minutes_quarter' => $empRow['early_leave_minutes_quarter'] !== null && $empRow['early_leave_minutes_quarter'] !== '' ? (int) $empRow['early_leave_minutes_quarter'] : null,
        'early_leave_minutes_half' => $empRow['early_leave_minutes_half'] !== null && $empRow['early_leave_minutes_half'] !== '' ? (int) $empRow['early_leave_minutes_half'] : null,
        'early_leave_minutes_full' => $empRow['early_leave_minutes_full'] !== null && $empRow['early_leave_minutes_full'] !== '' ? (int) $empRow['early_leave_minutes_full'] : null,
        'auto_deduct_leave_type' => (static function ($v): string {
            $v = strtolower(trim((string) $v));

            return in_array($v, ['annual', 'casual', 'sick'], true) ? $v : 'annual';
        })($empRow['auto_deduct_leave_type'] ?? 'annual'),
        'auto_deduct_max_per_day' => min(99.99, max(0.01, (float) ($empRow['auto_deduct_max_per_day'] ?? 1.0))),
        'auto_deduct_combine_late_early' => (int) ($empRow['auto_deduct_combine_late_early'] ?? 0) === 1,
        'auto_deduct_ignore_codes' => leaveSettlementParseIgnoreCodes($empRow['auto_deduct_ignore_codes'] ?? ''),
        'deduct_quarter_day' => $dq,
        'deduct_half_day' => $dh,
        'deduct_full_day' => $df,
    ];
}

/**
 * Non-binding settlement suggestion for one calendar day.
 *
 * Business rule: only **absence** (no attendance on a workday) gets an automatic
 * leave-balance suggestion. Lateness, early leave, and incomplete checkout are not
 * auto-suggested for leave debit (salary / other channels); the manager may still
 * enter a manual leave deduction on the settlement screen.
 *
 * @param array<string,mixed> $row
 * @param array<string,mixed> $sp from leaveSettlementPolicyFromEmployeeRow
 * @return array<string,mixed>|null
 */
function suggestLeaveSettlementDay(array $row, array $sp): ?array
{
    $st = (string) ($row['status'] ?? '');
    if ($st === 'weekend' || $st === 'leave' || $st === 'official_holiday') {
        return null;
    }

    if ($st !== 'absent') {
        return null;
    }

    $lt = (string) $sp['auto_deduct_leave_type'];
    $df = (float) $sp['deduct_full_day'];
    $maxDay = (float) $sp['auto_deduct_max_per_day'];

    return [
        'leave_type' => $lt,
        'days' => round(min($maxDay, $df), 2),
        'reason_code' => 'absent',
        'reason_text' => 'Absent (no attendance)',
        'breakdown' => ['late_minutes' => 0, 'early_leave_minutes' => 0, 'penalty_minutes' => 0],
    ];
}

/**
 * Employees + joined policy/department/branch for bulk final-report paths (chunked IN).
 *
 * @param list<int> $employeeIds
 * @return array<int, array<string,mixed>> keyed by employee id
 */
function hrproFetchEmployeesForFinalReportByIds(PDO $pdo, array $employeeIds, string $companyId): array
{
    $ids = [];
    foreach ($employeeIds as $id) {
        $id = (int) $id;
        if ($id > 0) {
            $ids[$id] = $id;
        }
    }
    if ($ids === []) {
        return [];
    }
    $select = 'SELECT e.*, lp.policy_name, lp.weekend_days, lp.annual_leave_days, lp.casual_leave_days, lp.sick_leave_days,
                  lp.deduct_full_day, lp.deduct_half_day, lp.deduct_quarter_day,
                  lp.start_time, lp.end_time,
                  lp.salary_deduct_enabled, lp.salary_deduct_allow_with_balance,
                  lp.grace_late_minutes, lp.grace_early_leave_minutes,
                  lp.late_minutes_quarter, lp.late_minutes_half, lp.late_minutes_full,
                  lp.early_leave_minutes_quarter, lp.early_leave_minutes_half, lp.early_leave_minutes_full,
                  lp.auto_deduct_leave_type, lp.auto_deduct_max_per_day, lp.auto_deduct_combine_late_early, lp.auto_deduct_ignore_codes,
                  d.department_name, b.name AS branch_name
           FROM employees e
           LEFT JOIN leave_policies lp ON e.policy_id = lp.id
           LEFT JOIN departments d ON e.department_id = d.id
           LEFT JOIN branches b ON e.branch_id = b.id
           WHERE e.company_id = ? AND e.id IN (';
    $base = count($ids);
    $chunkSize = 400;
    $out = [];
    $idList = array_values($ids);
    for ($off = 0; $off < $base; $off += $chunkSize) {
        $chunk = array_slice($idList, $off, $chunkSize);
        $placeholders = implode(',', array_fill(0, count($chunk), '?'));
        $stmt = $pdo->prepare($select . $placeholders . ')');
        $params = array_merge([$companyId], $chunk);
        $stmt->execute($params);
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $out[(int) $row['id']] = $row;
        }
    }

    return $out;
}

/** Employee + joined policy/department/branch for final report & leave settlement. */
function hrproFetchEmployeeForFinalReport(int $eid, string $companyId): ?array
{
    $map = hrproFetchEmployeesForFinalReportByIds(db(), [$eid], $companyId);

    return $map[$eid] ?? null;
}

/**
 * Grace minutes from the employee's joined leave policy row for final-report $policy payloads.
 * Defaults match leaveSettlementPolicyFromEmployeeRow().
 *
 * @param array<string,mixed> $employee
 * @return array{grace_late_minutes:int, grace_early_leave_minutes:int}
 */
function hrproFinalReportPolicyGraceFromEmployee(array $employee): array
{
    return [
        'grace_late_minutes' => max(0, (int) ($employee['grace_late_minutes'] ?? 5)),
        'grace_early_leave_minutes' => max(0, (int) ($employee['grace_early_leave_minutes'] ?? 5)),
    ];
}

/**
 * Build leave settlement preview payload (shared logic for GET preview).
 *
 * @return array<string,mixed>|array{error:string,http?:int}
 */
function hrproLeaveSettlementPreviewData(array $auth, int $eid, string $from, string $to): array
{
    $employee = hrproFetchEmployeeForFinalReport($eid, $auth['company_id']);
    if (!$employee) {
        return ['error' => 'Employee not found', 'http' => 404];
    }

    $settlementPolicy = leaveSettlementPolicyFromEmployeeRow($employee);

    $policyRowForOpts = [
        'deduct_full_day' => $employee['deduct_full_day'] ?? null,
        'deduct_half_day' => $employee['deduct_half_day'] ?? null,
        'deduct_quarter_day' => $employee['deduct_quarter_day'] ?? null,
    ];
    $deductOptions = policyDeductionFractionOptions($policyRowForOpts);

    $policy = [
        'name' => $employee['policy_name'] ?? 'Default',
        'weekend_days' => json_decode($employee['weekend_days'] ?? '["Friday"]', true),
        'annual' => (int) ($employee['annual_leave_days'] ?? 0),
        'casual' => (int) ($employee['casual_leave_days'] ?? 0),
        'sick' => (int) ($employee['sick_leave_days'] ?? 0),
        'start_time' => $employee['start_time'] ?? '09:00:00',
        'end_time' => $employee['end_time'] ?? '17:00:00',
        'deduct_full_day' => policyDeductionDecimal($employee['deduct_full_day'] ?? null, 1.0),
        'deduct_half_day' => policyDeductionDecimal($employee['deduct_half_day'] ?? null, 0.5),
        'deduct_quarter_day' => policyDeductionDecimal($employee['deduct_quarter_day'] ?? null, 0.25),
        'deduct_options' => $deductOptions,
        'salary_deduct_enabled' => (int) ($employee['salary_deduct_enabled'] ?? 1),
        'salary_deduct_allow_with_balance' => (int) ($employee['salary_deduct_allow_with_balance'] ?? 0),
    ];
    $policy = array_merge($policy, hrproFinalReportPolicyGraceFromEmployee($employee));

    $balanceYear = (int) substr((string) $from, 0, 4);
    if ($balanceYear < 1990 || $balanceYear > 2120) {
        $balanceYear = (int) date('Y');
    }
    $yearStart = sprintf('%04d-01-01', $balanceYear);
    $yearEnd = sprintf('%04d-12-31', $balanceYear);
    $pdo = db();
    $usedApproved = leaveApprovedUsedInRange($pdo, $eid, $auth['company_id'], $yearStart, $yearEnd);
    $usedManual = leaveManualUsedInRange($pdo, $eid, $auth['company_id'], $yearStart, $yearEnd);

    $balancesOut = [];
    foreach (['annual', 'casual', 'sick'] as $lt) {
        $tot = (float) $policy[$lt];
        $uA = round($usedApproved[$lt], 2);
        $uM = round($usedManual[$lt], 2);
        $u = round($uA + $uM, 2);
        $balancesOut[$lt] = [
            'total' => (int) round($tot),
            'used_approved' => $uA,
            'used_manual' => $uM,
            'used' => $u,
            'rem' => round($tot - $u, 2),
        ];
    }
    $policy['_balance_exhausted'] = finalReportAllLeaveRemDepleted($balancesOut);

    $stmtM = $pdo->prepare('SELECT id, deduction_date, leave_type, days, reason FROM leave_manual_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date BETWEEN ? AND ? ORDER BY id ASC');
    $stmtM->execute([$eid, $auth['company_id'], $from, $to]);
    $manualByDate = [];
    while ($md = $stmtM->fetch()) {
        $ds = $md['deduction_date'];
        if (!isset($manualByDate[$ds])) {
            $manualByDate[$ds] = [];
        }
        $manualByDate[$ds][] = [
            'id' => (int) $md['id'],
            'leave_type' => $md['leave_type'],
            'days' => (float) $md['days'],
            'reason' => $md['reason'],
        ];
    }

    $report = buildFinalReportDayRowsForEmployee($pdo, $employee, $policy, $auth['company_id'], $from, $to, $manualByDate);

    $days = [];
    $totalsProposed = ['annual' => 0.0, 'casual' => 0.0, 'sick' => 0.0];
    foreach ($report as $row) {
        $sys = suggestLeaveSettlementDay($row, $settlementPolicy);
        $mgr = null;
        if ($sys !== null) {
            $mgr = [
                'leave_type' => $sys['leave_type'],
                'days' => $sys['days'],
                'reason' => '',
            ];
            $lt = (string) $sys['leave_type'];
            if (isset($totalsProposed[$lt])) {
                $totalsProposed[$lt] = round($totalsProposed[$lt] + (float) $sys['days'], 2);
            }
        }
        $line = $row;
        $line['system_settlement'] = $sys;
        $line['manager_edit'] = $mgr;
        $days[] = $line;
    }

    $stmtFp = $pdo->prepare('SELECT MAX(timestamp) AS mx FROM attendance WHERE employee_id = ? AND company_id = ? AND DATE(timestamp) BETWEEN ? AND ?');
    $stmtFp->execute([$eid, $auth['company_id'], $from, $to]);
    $fpRow = $stmtFp->fetch();
    $previewFingerprint = md5((string) ($fpRow['mx'] ?? '') . '|' . $eid . '|' . $from . '|' . $to);

    $employeeProfile = [
        'name' => $employee['name'],
        'employee_code' => $employee['employee_code'],
        'department' => $employee['department_name'] ?? '',
        'branch' => $employee['branch_name'] ?? '',
        'job_title' => $employee['job_title'] ?? '',
        'work_nature' => $employee['work_nature'] ?? 'internal',
        'phone' => $employee['phone'] ?? '',
        'email' => $employee['email'] ?? '',
        'policy_name' => $policy['name'],
    ];

    return [
        'from' => $from,
        'to' => $to,
        'employee_id' => $eid,
        'days' => $days,
        'balances' => $balancesOut,
        'balance_year' => $balanceYear,
        'policy' => $policy,
        'settlement_policy' => $settlementPolicy,
        'employee_profile' => $employeeProfile,
        'totals_proposed' => $totalsProposed,
        'preview_fingerprint' => $previewFingerprint,
        'policy_missing' => empty($employee['policy_id']) || empty($employee['policy_name']),
    ];
}

/**
 * Collect automatic settlement rows (absent + policy suggestion, no manual row yet) for bulk preview/apply.
 *
 * @return list<array{employee_id:int, employee_name:string, employee_code:string, deduction_date:string, leave_type:string, days:float}>
 */
function hrproBulkSettlementCollectRows(PDO $pdo, string $companyId, string $from, string $to, ?int $departmentId): array
{
    $sql = 'SELECT e.id FROM employees e WHERE e.company_id = ? AND e.is_active = 1';
    $params = [$companyId];
    if ($departmentId !== null && $departmentId > 0) {
        $sql .= ' AND e.department_id = ?';
        $params[] = $departmentId;
    }
    $sql .= ' ORDER BY e.name ASC';
    $st = $pdo->prepare($sql);
    $st->execute($params);
    $empIds = [];
    while ($r = $st->fetch()) {
        $empIds[] = (int) $r['id'];
    }
    $empMap = hrproFetchEmployeesForFinalReportByIds($pdo, $empIds, $companyId);
    $out = [];
    foreach ($empIds as $eid) {
        $employee = $empMap[$eid] ?? null;
        if (!$employee) {
            continue;
        }
        $policyRowForOpts = [
            'deduct_full_day' => $employee['deduct_full_day'] ?? null,
            'deduct_half_day' => $employee['deduct_half_day'] ?? null,
            'deduct_quarter_day' => $employee['deduct_quarter_day'] ?? null,
        ];
        $deductOptions = policyDeductionFractionOptions($policyRowForOpts);
        $policy = [
            'name' => $employee['policy_name'] ?? 'Default',
            'weekend_days' => json_decode($employee['weekend_days'] ?? '["Friday"]', true),
            'annual' => (int) ($employee['annual_leave_days'] ?? 0),
            'casual' => (int) ($employee['casual_leave_days'] ?? 0),
            'sick' => (int) ($employee['sick_leave_days'] ?? 0),
            'start_time' => $employee['start_time'] ?? '09:00:00',
            'end_time' => $employee['end_time'] ?? '17:00:00',
            'deduct_full_day' => policyDeductionDecimal($employee['deduct_full_day'] ?? null, 1.0),
            'deduct_half_day' => policyDeductionDecimal($employee['deduct_half_day'] ?? null, 0.5),
            'deduct_quarter_day' => policyDeductionDecimal($employee['deduct_quarter_day'] ?? null, 0.25),
            'deduct_options' => $deductOptions,
            'salary_deduct_enabled' => (int) ($employee['salary_deduct_enabled'] ?? 1),
            'salary_deduct_allow_with_balance' => (int) ($employee['salary_deduct_allow_with_balance'] ?? 0),
        ];
        $policy = array_merge($policy, hrproFinalReportPolicyGraceFromEmployee($employee));
        $stmtM = $pdo->prepare('SELECT id, deduction_date, leave_type, days, reason FROM leave_manual_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date BETWEEN ? AND ? ORDER BY id ASC');
        $stmtM->execute([$eid, $companyId, $from, $to]);
        $manualByDate = [];
        while ($md = $stmtM->fetch()) {
            $ds = $md['deduction_date'];
            if (!isset($manualByDate[$ds])) {
                $manualByDate[$ds] = [];
            }
            $manualByDate[$ds][] = [
                'id' => (int) $md['id'],
                'leave_type' => $md['leave_type'],
                'days' => (float) $md['days'],
                'reason' => $md['reason'],
            ];
        }
        $report = buildFinalReportDayRowsForEmployee($pdo, $employee, $policy, $companyId, $from, $to, $manualByDate);
        finalReportAttachSuggestions($report);
        $sp = leaveSettlementPolicyFromEmployeeRow($employee);
        foreach ($report as $row) {
            $mds = $row['manual_deductions'] ?? [];
            if ($mds !== []) {
                continue;
            }
            $sys = suggestLeaveSettlementDay($row, $sp);
            if ($sys === null) {
                continue;
            }
            $out[] = [
                'employee_id' => $eid,
                'employee_name' => (string) ($employee['name'] ?? ''),
                'employee_code' => (string) ($employee['employee_code'] ?? ''),
                'deduction_date' => (string) ($row['date'] ?? ''),
                'leave_type' => (string) $sys['leave_type'],
                'days' => round((float) $sys['days'], 2),
            ];
        }
    }

    return $out;
}

/**
 * @param list<array{employee_id:int, deduction_date:string, leave_type:string, days:float}> $rows
 */
function hrproBulkSettlementValidateBalances(PDO $pdo, string $companyId, array $rowsByEmployee): ?string
{
    $bulkIds = [];
    foreach (array_keys($rowsByEmployee) as $empIdStr) {
        $bulkIds[] = (int) $empIdStr;
    }
    $empMap = hrproFetchEmployeesForFinalReportByIds($pdo, $bulkIds, $companyId);

    foreach ($rowsByEmployee as $empIdStr => $empRows) {
        $empId = (int) $empIdStr;
        $employee = $empMap[$empId] ?? null;
        if (!$employee) {
            return 'Employee not found: ' . $empId;
        }
        $from = null;
        $to = null;
        foreach ($empRows as $r) {
            $d = $r['deduction_date'];
            if ($from === null || $d < $from) {
                $from = $d;
            }
            if ($to === null || $d > $to) {
                $to = $d;
            }
        }
        if ($from === null || $to === null) {
            continue;
        }
        $balanceYear = (int) substr((string) $from, 0, 4);
        if ($balanceYear < 1990 || $balanceYear > 2120) {
            $balanceYear = (int) date('Y');
        }
        $yearStart = sprintf('%04d-01-01', $balanceYear);
        $yearEnd = sprintf('%04d-12-31', $balanceYear);
        $usedApproved = leaveApprovedUsedInRange($pdo, $empId, $companyId, $yearStart, $yearEnd);
        $usedManual = leaveManualUsedInRange($pdo, $empId, $companyId, $yearStart, $yearEnd);
        $policy = [
            'annual' => (int) ($employee['annual_leave_days'] ?? 0),
            'casual' => (int) ($employee['casual_leave_days'] ?? 0),
            'sick' => (int) ($employee['sick_leave_days'] ?? 0),
        ];
        $sumNew = ['annual' => 0.0, 'casual' => 0.0, 'sick' => 0.0];
        foreach ($empRows as $r) {
            $lt = (string) ($r['leave_type'] ?? '');
            if (!isset($sumNew[$lt])) {
                continue;
            }
            $sumNew[$lt] = round($sumNew[$lt] + (float) ($r['days'] ?? 0), 2);
        }
        foreach (['annual', 'casual', 'sick'] as $lt) {
            $tot = (float) $policy[$lt];
            $uA = round($usedApproved[$lt], 2);
            $uM = round($usedManual[$lt], 2);
            $newS = $sumNew[$lt] ?? 0.0;
            $remAfter = round($tot - $uA - $uM - $newS, 2);
            if ($remAfter < -0.01) {
                $nm = (string) ($employee['name'] ?? (string) $empId);

                return 'Insufficient leave balance for ' . $lt . ' (employee: ' . $nm . ', would remain ' . $remAfter . ')';
            }
        }
    }

    return null;
}

/**
 * Daily rows for final report (attendance / leave / weekend) + suggestions.
 *
 * @param array<string,mixed> $employee joined row with policy_* fields
 * @param array<string,mixed> $policy normalized policy (weekend_days, start_time, end_time, …)
 * @param array<string, array<int, array<string, mixed>>> $manualByDate
 * @return array<int, array<string,mixed>>
 */
/**
 * Leave-balance deduction preview for a late check-in day (uses policy grace + late minute thresholds + day fractions).
 *
 * @param array<string,mixed> $row  one final-report day row (after suggestions attached)
 * @param array<string,mixed> $sp   leaveSettlementPolicyFromEmployeeRow()
 * @return array<string,mixed>|null
 */
function finalReportComputeLateLeaveDeductionPreview(array $row, array $sp): ?array
{
    if (($row['status'] ?? '') !== 'present') {
        return null;
    }
    $codes = $row['suggestions'] ?? [];
    if (!is_array($codes) || !in_array('late', $codes, true)) {
        return null;
    }
    $ciRaw = $row['check_in'] ?? null;
    if (!$ciRaw) {
        return null;
    }
    $grace = max(0, (int) ($sp['grace_late_minutes'] ?? 0));
    $tQ = $sp['late_minutes_quarter'];
    $tH = $sp['late_minutes_half'];
    $tF = $sp['late_minutes_full'];
    if ($tQ === null || $tQ === '') {
        $tQ = 15;
    } else {
        $tQ = max(0, (int) $tQ);
    }
    if ($tH === null || $tH === '') {
        $tH = max($tQ + 1, 30);
    } else {
        $tH = max(0, (int) $tH);
    }
    if ($tF === null || $tF === '') {
        $tF = max($tH + 1, 60);
    } else {
        $tF = max(0, (int) $tF);
    }
    if ($tH <= $tQ) {
        $tH = $tQ + 1;
    }
    if ($tF <= $tH) {
        $tF = $tH + 1;
    }

    $lateMin = finalReportNetLateMinutes($row, $grace);
    if ($lateMin < 1) {
        return null;
    }

    $df = (float) ($sp['deduct_full_day'] ?? 1.0);
    $dh = (float) ($sp['deduct_half_day'] ?? 0.5);
    $dq = (float) ($sp['deduct_quarter_day'] ?? 0.25);
    $lt = strtolower(trim((string) ($sp['auto_deduct_leave_type'] ?? 'annual')));
    if (!in_array($lt, ['annual', 'casual', 'sick'], true)) {
        $lt = 'annual';
    }

    $tier = null;
    $days = null;
    if ($lateMin >= $tF) {
        $tier = 'full';
        $days = $df;
    } elseif ($lateMin >= $tH) {
        $tier = 'half';
        $days = $dh;
    } elseif ($lateMin >= $tQ) {
        $tier = 'quarter';
        $days = $dq;
    } else {
        return null;
    }

    return [
        'days' => round((float) $days, 2),
        'leave_type' => $lt,
        'late_minutes' => $lateMin,
        'tier' => $tier,
        'reason_code' => 'late_policy',
    ];
}

/**
 * @param array<int, array<string,mixed>> $report
 */
function finalReportAttachLateLeavePreview(array &$report, array $sp): void
{
    foreach ($report as $i => $row) {
        $report[$i]['late_leave_deduct_preview'] = finalReportComputeLateLeaveDeductionPreview($row, $sp);
    }
}

/**
 * Backward-safe guard: some old databases may miss this column
 * even if schema version says up-to-date.
 */
function ensureLeavePolicyPermissionLimitColumn(PDO $pdo): void
{
    static $checked = false;
    if ($checked) return;
    try {
        $pdo->exec("ALTER TABLE leave_policies ADD COLUMN permission_monthly_limit INT UNSIGNED NULL DEFAULT NULL COMMENT 'Approved permission requests allowed per employee per month'");
    } catch (Exception $e) {
        // Column already exists or ALTER not needed.
    }
    $checked = true;
}

/**
 * Backward-safe: permission_kind was added after HRPRO_SCHEMA_VERSION reached 2;
 * installs that skip the version-0 migration block never got this column without ALTER here.
 */
function ensureLeaveRequestPermissionKindColumn(PDO $pdo): void
{
    static $checked = false;
    if ($checked) {
        return;
    }
    try {
        $pdo->exec("ALTER TABLE leave_requests ADD COLUMN permission_kind VARCHAR(16) NULL DEFAULT NULL COMMENT 'mission=internal field mission; NULL=standard permission'");
    } catch (Exception $e) {
        // Column already exists.
    }
    $checked = true;
}

/** Employees.birth_date for admin-entered date of birth. */
function ensureEmployeeBirthDateColumn(PDO $pdo): void
{
    static $checked = false;
    if ($checked) {
        return;
    }
    try {
        $pdo->exec('ALTER TABLE employees ADD COLUMN birth_date DATE NULL DEFAULT NULL COMMENT \'Date of birth (full date)\'');
    } catch (Exception $e) {
        // Column already exists.
    }
    $checked = true;
}

/** Employee inbox: soft-archive (same semantics as admin_notifications.is_deleted). */
function adminMessagesHasDeletedColumn(PDO $pdo): bool
{
    try {
        $st = $pdo->query("SHOW COLUMNS FROM admin_messages LIKE 'is_deleted'");
        return ($st && $st->fetch()) ? true : false;
    } catch (Exception $e) {
        return false;
    }
}

/**
 * Ensure admin_messages.is_deleted exists when possible; returns whether SELECTs may use it.
 * If ALTER is not allowed on the server, callers must fall back to queries without is_deleted.
 */
function adminMessagesEnsureDeletedColumn(PDO $pdo): bool
{
    if (adminMessagesHasDeletedColumn($pdo)) {
        return true;
    }
    try {
        $pdo->exec("ALTER TABLE admin_messages ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Archived by employee'");
    } catch (Exception $e) {
        error_log('admin_messages.is_deleted ALTER: ' . $e->getMessage());
    }
    return adminMessagesHasDeletedColumn($pdo);
}

/**
 * @param array<string,mixed> $preview from finalReportComputeLateLeaveDeductionPreview
 * @param array<string,mixed> $empRow  employee joined policy row (deduct_* columns)
 */
function hrproInsertLeaveManualDeductFromLatePreview(PDO $pdo, string $companyId, int $empId, string $dedDate, array $preview, array $empRow): bool
{
    $policyRowForOpts = [
        'deduct_full_day' => $empRow['deduct_full_day'] ?? null,
        'deduct_half_day' => $empRow['deduct_half_day'] ?? null,
        'deduct_quarter_day' => $empRow['deduct_quarter_day'] ?? null,
    ];
    $allowed = policyDeductionFractionOptions($policyRowForOpts);
    $days = (float) ($preview['days'] ?? -1);
    $ok = false;
    foreach ($allowed as $a) {
        if (abs($days - (float) $a) < 0.001) {
            $ok = true;
            break;
        }
    }
    if (!$ok) {
        return false;
    }
    $lt = (string) ($preview['leave_type'] ?? '');
    if (!in_array($lt, ['annual', 'casual', 'sick'], true)) {
        return false;
    }
    $lateMin = (int) ($preview['late_minutes'] ?? 0);
    $tier = (string) ($preview['tier'] ?? '');
    $reason = sprintf('Late check-in — policy tier %s (%d min after grace)', $tier, $lateMin);
    if (strlen($reason) > 500) {
        $reason = substr($reason, 0, 500);
    }
    $pdo->prepare('INSERT INTO leave_manual_deductions (company_id, employee_id, deduction_date, leave_type, days, reason) VALUES (?,?,?,?,?,?)')
        ->execute([$companyId, $empId, $dedDate, $lt, round($days, 2), $reason]);
    $newId = (int) $pdo->lastInsertId();
    auditLog('admin', $companyId, 'LEAVE_MANUAL_DEDUCT', ['id' => $newId, 'employee_id' => $empId, 'date' => $dedDate, 'leave_type' => $lt, 'days' => $days], $companyId);

    return true;
}

/**
 * @return array{employee: array<string,mixed>, report: array<int, array<string,mixed>>, policy: array<string,mixed>}|null
 */
function hrproBuildFinalReportWindowRows(PDO $pdo, string $companyId, int $eid, string $from, string $to): ?array
{
    $employee = hrproFetchEmployeeForFinalReport($eid, $companyId);
    if (!$employee) {
        return null;
    }
    $policyRowForOpts = [
        'deduct_full_day' => $employee['deduct_full_day'] ?? null,
        'deduct_half_day' => $employee['deduct_half_day'] ?? null,
        'deduct_quarter_day' => $employee['deduct_quarter_day'] ?? null,
    ];
    $deductOptions = policyDeductionFractionOptions($policyRowForOpts);
    $policy = [
        'name' => $employee['policy_name'] ?? 'Default',
        'weekend_days' => json_decode($employee['weekend_days'] ?? '["Friday"]', true),
        'annual' => (int) ($employee['annual_leave_days'] ?? 0),
        'casual' => (int) ($employee['casual_leave_days'] ?? 0),
        'sick' => (int) ($employee['sick_leave_days'] ?? 0),
        'start_time' => $employee['start_time'] ?? '09:00:00',
        'end_time' => $employee['end_time'] ?? '17:00:00',
        'deduct_full_day' => policyDeductionDecimal($employee['deduct_full_day'] ?? null, 1.0),
        'deduct_half_day' => policyDeductionDecimal($employee['deduct_half_day'] ?? null, 0.5),
        'deduct_quarter_day' => policyDeductionDecimal($employee['deduct_quarter_day'] ?? null, 0.25),
        'deduct_options' => $deductOptions,
        'salary_deduct_enabled' => (int) ($employee['salary_deduct_enabled'] ?? 1),
        'salary_deduct_allow_with_balance' => (int) ($employee['salary_deduct_allow_with_balance'] ?? 0),
    ];
    $policy = array_merge($policy, hrproFinalReportPolicyGraceFromEmployee($employee));
    $balanceYear = (int) substr((string) $from, 0, 4);
    if ($balanceYear < 1990 || $balanceYear > 2120) {
        $balanceYear = (int) date('Y');
    }
    $yearStart = sprintf('%04d-01-01', $balanceYear);
    $yearEnd = sprintf('%04d-12-31', $balanceYear);
    $usedApproved = leaveApprovedUsedInRange($pdo, $eid, $companyId, $yearStart, $yearEnd);
    $usedManual = leaveManualUsedInRange($pdo, $eid, $companyId, $yearStart, $yearEnd);
    $balancesOut = [];
    foreach (['annual', 'casual', 'sick'] as $lt) {
        $tot = (float) $policy[$lt];
        $uA = round($usedApproved[$lt], 2);
        $uM = round($usedManual[$lt], 2);
        $u = round($uA + $uM, 2);
        $balancesOut[$lt] = [
            'total' => (int) round($tot),
            'used_approved' => $uA,
            'used_manual' => $uM,
            'used' => $u,
            'rem' => round($tot - $u, 2),
        ];
    }
    $policy['_balance_exhausted'] = finalReportAllLeaveRemDepleted($balancesOut);

    $stmtM = $pdo->prepare('SELECT id, deduction_date, leave_type, days, reason FROM leave_manual_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date BETWEEN ? AND ? ORDER BY id ASC');
    $stmtM->execute([$eid, $companyId, $from, $to]);
    $manualByDate = [];
    while ($md = $stmtM->fetch()) {
        $ds = $md['deduction_date'];
        if (!isset($manualByDate[$ds])) {
            $manualByDate[$ds] = [];
        }
        $manualByDate[$ds][] = [
            'id' => (int) $md['id'],
            'leave_type' => $md['leave_type'],
            'days' => (float) $md['days'],
            'reason' => $md['reason'],
        ];
    }
    $report = buildFinalReportDayRowsForEmployee($pdo, $employee, $policy, $companyId, $from, $to, $manualByDate);

    return ['employee' => $employee, 'report' => $report, 'policy' => $policy];
}

function buildFinalReportDayRowsForEmployee(PDO $pdo, array $employee, array $policy, string $companyId, string $from, string $to, array $manualByDate): array {
    $eid = (int) $employee['id'];
    $stmtA = $pdo->prepare("
                SELECT
                    DATE(timestamp) as date,
                    MIN(CASE WHEN type='check_in' THEN timestamp ELSE NULL END) as check_in,
                    MAX(CASE WHEN type='check_out' THEN timestamp ELSE NULL END) as check_out,
                    MAX(CASE WHEN type='check_in' THEN notes ELSE NULL END) as notes,
                    MAX(CASE WHEN type='check_in' THEN visit_details ELSE NULL END) as visit_details
                FROM attendance
                WHERE employee_id = ? AND company_id = ? AND DATE(timestamp) BETWEEN ? AND ?
                GROUP BY DATE(timestamp)
            ");
    $stmtA->execute([$eid, $companyId, $from, $to]);
    $attendance = [];
    while ($r = $stmtA->fetch()) {
        $attendance[$r['date']] = $r;
    }

    $stmtRaw = $pdo->prepare("
                SELECT DATE(timestamp) AS d, type, timestamp, location_status, distance_meters,
                       selfie_path, device_id, IFNULL(qr_token,'') AS qr_token, logged_work_nature, notes, visit_details
                FROM attendance
                WHERE employee_id = ? AND company_id = ? AND DATE(timestamp) BETWEEN ? AND ?
                ORDER BY timestamp ASC
            ");
    $stmtRaw->execute([$eid, $companyId, $from, $to]);
    $firstLastByDate = [];
    while ($rw = $stmtRaw->fetch()) {
        $ds = $rw['d'];
        if (!isset($firstLastByDate[$ds])) {
            $firstLastByDate[$ds] = ['first_in' => null, 'last_out' => null];
        }
        if ($rw['type'] === 'check_in') {
            $firstLastByDate[$ds]['first_in'] = $rw;
        }
        if ($rw['type'] === 'check_out') {
            $firstLastByDate[$ds]['last_out'] = $rw;
        }
    }

    $stmtL = $pdo->prepare("
                SELECT start_date, end_date, reason, leave_type
                FROM leave_requests
                WHERE employee_id = ? AND company_id = ? AND status = 'approved'
                AND leave_type IN ('annual','casual','sick')
                AND (start_date <= ? AND end_date >= ?)
            ");
    $stmtL->execute([$eid, $companyId, $to, $from]);
    $leaves = $stmtL->fetchAll();

    $stmtP = $pdo->prepare("
                SELECT start_date, end_date, reason, permission_from, permission_to, permission_kind
                FROM leave_requests
                WHERE employee_id = ? AND company_id = ? AND status = 'approved'
                AND leave_type = 'permission'
                AND (start_date <= ? AND end_date >= ?)
            ");
    $stmtP->execute([$eid, $companyId, $to, $from]);
    $permissionsByDate = [];
    while ($p = $stmtP->fetch()) {
        $s = new DateTime((string) $p['start_date']);
        $e = new DateTime((string) $p['end_date']);
        $wStart = new DateTime($from);
        $wEnd = new DateTime($to);
        if ($s < $wStart) {
            $s = clone $wStart;
        }
        if ($e > $wEnd) {
            $e = clone $wEnd;
        }
        if ($e < $s) {
            continue;
        }
        $curPerm = clone $s;
        while ($curPerm <= $e) {
            $ds = $curPerm->format('Y-m-d');
            if (!isset($permissionsByDate[$ds])) {
                $permissionsByDate[$ds] = [];
            }
            $permissionsByDate[$ds][] = [
                'permission_from' => $p['permission_from'] ?? null,
                'permission_to' => $p['permission_to'] ?? null,
                'reason' => $p['reason'] ?? '',
                'permission_kind' => $p['permission_kind'] ?? null,
            ];
            $curPerm->modify('+1 day');
        }
    }

    $stmtH = $pdo->prepare('SELECT holiday_date, title FROM company_official_holidays WHERE company_id = ? AND holiday_date BETWEEN ? AND ?');
    $stmtH->execute([$companyId, $from, $to]);
    $holidayByDate = [];
    while ($h = $stmtH->fetch()) {
        $holidayByDate[(string) $h['holiday_date']] = (string) $h['title'];
    }

    $stmtSal = $pdo->prepare('SELECT id, deduction_date, days, reason, force_despite_balance FROM employee_salary_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date BETWEEN ? AND ? ORDER BY id ASC');
    $stmtSal->execute([$eid, $companyId, $from, $to]);
    $salaryByDate = [];
    while ($s = $stmtSal->fetch()) {
        $ds = (string) $s['deduction_date'];
        if (!isset($salaryByDate[$ds])) {
            $salaryByDate[$ds] = [];
        }
        $salaryByDate[$ds][] = [
            'id' => (int) $s['id'],
            'days' => (float) $s['days'],
            'reason' => $s['reason'],
            'force_despite_balance' => (int) ($s['force_despite_balance'] ?? 0),
        ];
    }

    $salaryEnabled = !isset($policy['salary_deduct_enabled']) || (int) $policy['salary_deduct_enabled'] === 1;
    $salaryAllowWithBal = isset($policy['salary_deduct_allow_with_balance']) && (int) $policy['salary_deduct_allow_with_balance'] === 1;
    $balanceExhausted = !empty($policy['_balance_exhausted']);
    $salaryGate = $salaryAllowWithBal || $balanceExhausted;

    $report = [];
    $current = new DateTime($from);
    $end = new DateTime($to);
    $end->modify('+1 day');

    $weekendDays = $policy['weekend_days'] ?? [];
    if (!is_array($weekendDays)) {
        $weekendDays = ['Friday'];
    }

    while ($current < $end) {
        $dStr = $current->format('Y-m-d');
        $dayNameEn = $current->format('l');
        $isWeekend = in_array($dayNameEn, $weekendDays);

        $holidayTitle = $holidayByDate[$dStr] ?? null;
        $dayData = [
            'date' => $dStr,
            'check_in' => null,
            'check_out' => null,
            'sch_in' => $policy['start_time'],
            'sch_out' => $policy['end_time'],
            'grace_late_minutes' => max(0, (int) ($policy['grace_late_minutes'] ?? 0)),
            'status' => $isWeekend ? 'weekend' : 'absent',
            'note' => '',
            'visit_details' => '',
        ];

        if ($isWeekend) {
            // keep weekend
        } elseif (isset($attendance[$dStr])) {
            $dayData['check_in'] = $attendance[$dStr]['check_in'];
            $dayData['check_out'] = $attendance[$dStr]['check_out'];
            $dayData['status'] = 'present';

            $aNotes = $attendance[$dStr]['notes'] ?? '';
            $vNotes = $attendance[$dStr]['visit_details'] ?? '';
            $combined = [];
            if ($aNotes) {
                $combined[] = $aNotes;
            }
            if ($vNotes) {
                $combined[] = $vNotes;
            }
            $dayData['note'] = implode(' | ', $combined);
            if ($holidayTitle !== null && $holidayTitle !== '') {
                $dayData['note'] = trim($dayData['note'] !== '' ? $dayData['note'] . ' | ' . $holidayTitle : $holidayTitle);
            }
            $dayData['visit_details'] = trim((string) $vNotes);

            $fl = $firstLastByDate[$dStr] ?? null;
            $cin = $fl['first_in'] ?? null;
            $cout = $fl['last_out'] ?? null;
            $dayData['in_location_status'] = $cin ? ($cin['location_status'] ?? null) : null;
            $dayData['out_location_status'] = $cout ? ($cout['location_status'] ?? null) : null;
            $dayData['in_distance_m'] = ($cin && isset($cin['distance_meters'])) ? (float) $cin['distance_meters'] : null;
            $dayData['out_distance_m'] = ($cout && isset($cout['distance_meters'])) ? (float) $cout['distance_meters'] : null;
            $dayData['has_selfie_in'] = $cin && !empty($cin['selfie_path']);
            $dayData['has_selfie_out'] = $cout && !empty($cout['selfie_path']);
            $qt = $cin ? ($cin['qr_token'] ?? '') : '';
            if ($cin && (($cin['device_id'] ?? '') === 'manual')) {
                $dayData['registration_method'] = 'manual';
            } elseif ($qt === '' || strcasecmp((string) $qt, 'DIRECT') === 0) {
                $dayData['registration_method'] = 'direct';
            } else {
                $dayData['registration_method'] = 'qr';
            }
            $dayData['logged_work_nature'] = $cin ? ($cin['logged_work_nature'] ?? null) : null;
            if ($dayData['check_in'] && $dayData['check_out']) {
                $t1 = strtotime($dayData['check_in']);
                $t2 = strtotime($dayData['check_out']);
                $dayData['work_hours'] = $t2 > $t1 ? round(($t2 - $t1) / 3600, 2) : null;
            } else {
                $dayData['work_hours'] = null;
            }
        } else {
            $onLeave = false;
            foreach ($leaves as $l) {
                if ($dStr >= $l['start_date'] && $dStr <= $l['end_date']) {
                    $dayData['status'] = 'leave';
                    $dayData['note'] = ($l['leave_type'] ? '[' . $l['leave_type'] . '] ' : '') . $l['reason'];
                    $onLeave = true;
                    break;
                }
            }
            if (!$onLeave && $holidayTitle !== null && $holidayTitle !== '') {
                $dayData['status'] = 'official_holiday';
                $dayData['note'] = $holidayTitle;
            }
        }
        $dayData['manual_deductions'] = $manualByDate[$dStr] ?? [];
        $dayData['can_manual_deduct'] = !$isWeekend
            && $dayData['status'] !== 'leave'
            && $dayData['status'] !== 'official_holiday'
            && ($dayData['status'] === 'present' || $dayData['status'] === 'absent');
        $dayData['salary_deductions'] = $salaryByDate[$dStr] ?? [];
        $dayData['can_mark_salary_deduct'] = $salaryEnabled
            && $salaryGate
            && !$isWeekend
            && ($dayData['status'] === 'absent')
            && count($dayData['salary_deductions']) === 0;
        $dayData['approved_permissions'] = $permissionsByDate[$dStr] ?? [];
        $report[] = $dayData;
        $current->modify('+1 day');
    }

    finalReportAttachSuggestions($report);
    $spLocal = leaveSettlementPolicyFromEmployeeRow($employee);
    finalReportAttachLateLeavePreview($report, $spLocal);

    return $report;
}

function respond(int $code, $data, string $message = ''): void {
    http_response_code($code);
    if (is_string($data)) {
        echo json_encode(['success' => $code < 400, 'message' => $data]);
    } else {
        $out = ['success' => $code < 400, 'data' => $data];
        if ($message) $out['message'] = $message;
        echo json_encode($out);
    }
    exit;
}

/**
 * UTF-8 → UTF-16 LE for Excel (double-click opens Arabic correctly on Windows). Uses mbstring or iconv.
 */
function hrproUtf8ToUtf16Le(string $utf8): ?string
{
    if (function_exists('mb_convert_encoding')) {
        return mb_convert_encoding($utf8, 'UTF-16LE', 'UTF-8');
    }
    if (function_exists('iconv')) {
        $conv = @iconv('UTF-8', 'UTF-16LE//IGNORE', $utf8);
        if ($conv !== false) {
            return $conv;
        }
    }

    return null;
}

function hrproCsvCanEmitUtf16Le(): bool
{
    return hrproUtf8ToUtf16Le('ن') !== null;
}

/**
 * Emit one CSV row for Excel on Windows: UTF-16 LE preserves Arabic in columns when opened by double-click.
 * Semicolon delimiter — Excel Arabic locale uses ; as list separator (comma would show one cell per row).
 */
function hrproCsvWriteRowExcel($out, array $fields, bool $asUtf16Le, string $delimiter = ';'): void {
    $tmp = fopen('php://temp', 'r+');
    if ($tmp === false) {
        return;
    }
    fputcsv($tmp, $fields, $delimiter);
    rewind($tmp);
    $utf8 = stream_get_contents($tmp);
    fclose($tmp);
    if ($utf8 === false || $utf8 === '') {
        return;
    }
    if ($asUtf16Le) {
        $le = hrproUtf8ToUtf16Le($utf8);
        if ($le !== null) {
            fwrite($out, $le);

            return;
        }
    }
    fwrite($out, $utf8);
}

/** Selfie column: yes/no only — no file paths or extensions in CSV. */
function hrproCsvSelfiePresent(?string $path): string
{
    return ($path !== null && trim((string) $path) !== '') ? 'نعم' : 'لا';
}

function input(): array {
    static $cached = null;
    if ($cached !== null) {
        return $cached;
    }
    $raw = file_get_contents('php://input');
    if ($raw === false || $raw === '') {
        $cached = is_array($_POST) ? $_POST : [];
        return $cached;
    }
    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        $cached = is_array($_POST) ? $_POST : [];
        return $cached;
    }
    $cached = $decoded;
    return $cached;
}

function generateToken(array $payload): string {
    $header  = base64url_encode(json_encode(['typ'=>'JWT','alg'=>'HS256']));
    $payload = base64url_encode(json_encode(array_merge($payload, ['exp' => time() + SESSION_LIFETIME])));
    $sig     = base64url_encode(hash_hmac('sha256', "$header.$payload", SECRET_KEY, true));
    return "$header.$payload.$sig";
}

function verifyToken(string $token): ?array {
    $parts = explode('.', $token);
    if (count($parts) !== 3) return null;
    [$header, $payload, $sig] = $parts;
    $expected = base64url_encode(hash_hmac('sha256', "$header.$payload", SECRET_KEY, true));
    if (!hash_equals($expected, $sig)) return null;
    $data = json_decode(base64url_decode($payload), true);
    if (!$data || $data['exp'] < time()) return null;
    return $data;
}

function base64url_encode(string $data): string {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function base64url_decode(string $data): string {
    return base64_decode(strtr($data, '-_', '+/') . str_repeat('=', (4 - strlen($data) % 4) % 4));
}

function getAuth(): array {
    $headers = getallheaders();
    $auth    = $headers['Authorization'] ?? $headers['authorization'] ?? '';
    if (!str_starts_with($auth, 'Bearer ')) respond(401, 'Missing or invalid token');
    $token   = substr($auth, 7);
    $payload = verifyToken($token);
    if (!$payload) respond(401, 'Token expired or invalid');
    if (!empty($payload['company_id']) && in_array($payload['role'] ?? '', ['admin', 'employee'], true)) {
        $stmt = db()->prepare('SELECT timezone, subscription_status, account_close_at FROM companies WHERE company_id = ?');
        $stmt->execute([$payload['company_id']]);
        $company = $stmt->fetch();
        if (!$company) {
            respond(403, 'Company not found');
        }
        if (($company['subscription_status'] ?? '') === 'suspended') {
            respond(403, 'Account suspended');
        }
        $closeAt = $company['account_close_at'] ?? null;
        if ($closeAt && $closeAt !== '0000-00-00' && (string) $closeAt !== '' && date('Y-m-d') > (string) $closeAt) {
            respond(403, 'Subscription period has ended');
        }
        if (!empty($company['timezone'])) {
            $payload['timezone'] = $company['timezone'];
        }
    } elseif (!empty($payload['company_id'])) {
        try {
            $stmt = db()->prepare('SELECT timezone FROM companies WHERE company_id = ?');
            $stmt->execute([$payload['company_id']]);
            $company = $stmt->fetch();
            if ($company && !empty($company['timezone'])) {
                $payload['timezone'] = $company['timezone'];
            }
        } catch (Exception $e) {}
    }

    // Attendance timestamps use MySQL NOW()/CURRENT_TIMESTAMP after this — always from companies.timezone
    // (refreshed here each request), not the employee phone clock. JWT may carry an old timezone; DB wins.
    if (!empty($payload['timezone'])) {
        date_default_timezone_set($payload['timezone']);
        try {
            $tz = new DateTimeZone($payload['timezone']);
            $pdo = db();
            $iana = $payload['timezone'];
            // Prefer IANA zone for DST alignment when mysql.time_zone_* tables exist on the host.
            try {
                $q = $pdo->quote($iana);
                $pdo->exec('SET time_zone = ' . $q);
            } catch (Exception $eNamed) {
                $dt = new DateTime('now', $tz);
                $offset = $dt->format('P'); // e.g. +02:00 — fixed offset; no DST transitions
                $pdo->exec("SET time_zone = '{$offset}'");
            }
        } catch (Exception $e) {}
    }
    return $payload;
}

function requireRole(string ...$roles): array {
    $auth = getAuth();
    if (!in_array($auth['role'], $roles)) respond(403, 'Forbidden');
    return $auth;
}

function getAuthOptional(): ?array {
    $headers = getallheaders();
    $auth = $headers['Authorization'] ?? $headers['authorization'] ?? '';
    if (!is_string($auth) || !str_starts_with($auth, 'Bearer ')) return null;
    $token = substr($auth, 7);
    $payload = verifyToken($token);
    return is_array($payload) ? $payload : null;
}

/**
 * Metadata for admin permission modules (single source for keys + Sub Admin UI labels).
 *
 * @return array<int, array{key: string, label_ar: string, label_en: string, group: string}>
 */
function adminPermissionModuleDefinitions(): array
{
    return [
        ['key' => 'dashboard', 'label_ar' => 'لوحة التحكم والإعدادات', 'label_en' => 'Dashboard & company settings', 'group' => 'settings'],
        ['key' => 'employees', 'label_ar' => 'الموظفون', 'label_en' => 'Employees', 'group' => 'hr'],
        ['key' => 'branches', 'label_ar' => 'الفروع', 'label_en' => 'Branches', 'group' => 'hr'],
        ['key' => 'departments', 'label_ar' => 'الأقسام', 'label_en' => 'Departments', 'group' => 'hr'],
        ['key' => 'attendance', 'label_ar' => 'الحضور والانصراف', 'label_en' => 'Attendance', 'group' => 'attendance'],
        ['key' => 'final_report', 'label_ar' => 'التقرير النهائي للحضور', 'label_en' => 'Final attendance report', 'group' => 'reports'],
        ['key' => 'final_decision', 'label_ar' => 'التقرير النهائي — القرار', 'label_en' => 'Final decision report', 'group' => 'reports'],
        ['key' => 'leave_settlement', 'label_ar' => 'تقرير تسوية الإجازات', 'label_en' => 'Leave settlement report', 'group' => 'reports'],
        ['key' => 'deductions_report', 'label_ar' => 'تقرير الخصومات', 'label_en' => 'Deductions report', 'group' => 'reports'],
        ['key' => 'leave_overview', 'label_ar' => 'نظرة عامة على الإجازات', 'label_en' => 'Leave overview', 'group' => 'reports'],
        ['key' => 'leave', 'label_ar' => 'طلبات الإجازات', 'label_en' => 'Leave requests', 'group' => 'leave_policies'],
        ['key' => 'policies', 'label_ar' => 'سياسات الإجازات', 'label_en' => 'Leave policies', 'group' => 'leave_policies'],
        ['key' => 'official_holidays', 'label_ar' => 'العطلات الرسمية', 'label_en' => 'Official holidays', 'group' => 'leave_policies'],
        ['key' => 'company_news', 'label_ar' => 'إعلانات الشركة', 'label_en' => 'Company news', 'group' => 'comms'],
        ['key' => 'employee_messages', 'label_ar' => 'مراسلة الموظفين', 'label_en' => 'Employee messages (admin)', 'group' => 'comms'],
        ['key' => 'qr', 'label_ar' => 'رمز QR للحضور', 'label_en' => 'Attendance QR', 'group' => 'tools'],
    ];
}

function adminModuleKeys(): array
{
    return array_column(adminPermissionModuleDefinitions(), 'key');
}

function defaultAdminPermissionMap(): array {
    $mods = adminModuleKeys();
    $out = [];
    foreach ($mods as $m) {
        $out[$m] = ['read' => 0, 'create' => 0, 'update' => 0, 'delete' => 0];
    }
    return $out;
}

function fetchSubAdminPermissionMap(int $subAdminId): array {
    $map = defaultAdminPermissionMap();
    $st = db()->prepare('SELECT module_key, can_read, can_create, can_update, can_delete FROM company_sub_admin_permissions WHERE sub_admin_id=?');
    $st->execute([$subAdminId]);
    foreach ($st->fetchAll() as $r) {
        $mk = (string)($r['module_key'] ?? '');
        if (!isset($map[$mk])) continue;
        $map[$mk] = [
            'read' => (int)($r['can_read'] ?? 0),
            'create' => (int)($r['can_create'] ?? 0),
            'update' => (int)($r['can_update'] ?? 0),
            'delete' => (int)($r['can_delete'] ?? 0),
        ];
    }
    return $map;
}

function isGeneralAdmin(array $auth): bool {
    return ($auth['role'] ?? '') === 'admin' && (($auth['admin_type'] ?? 'general') === 'general');
}

function adminActionFromMethod(string $method): string {
    $m = strtoupper($method);
    if ($m === 'GET') return 'read';
    if ($m === 'POST') return 'create';
    if ($m === 'PUT' || $m === 'PATCH') return 'update';
    if ($m === 'DELETE') return 'delete';
    return 'read';
}

function resolveAdminModuleByMatch(string $match): ?string {
    $m = strtolower(trim($match));
    $table = [
        'dashboard/stats' => 'dashboard',
        'employees' => 'employees',
        'employees/reset-device' => 'employees',
        'employees/reset-password' => 'employees',
        'employees/documents' => 'employees',
        'employees/documents-item' => 'employees',
        'employees/documents-download' => 'employees',
        'branches' => 'branches',
        'departments' => 'departments',
        'attendance' => 'attendance',
        'attendance/summary' => 'attendance',
        'attendance/direct' => 'attendance',
        'attendance/note' => 'attendance',
        'admin/attendance/manual' => 'attendance',
        'leave' => 'leave',
        'leave-policies' => 'policies',
        'official-holidays' => 'official_holidays',
        'official-holidays/item' => 'official_holidays',
        'admin/company-news' => 'company_news',
        'admin/company-news/item' => 'company_news',
        'company-news' => 'company_news',
        'admin/welcome-email-template' => 'dashboard',
        'admin/mobile-app-url' => 'dashboard',
        'admin/notification-email' => 'dashboard',
        'admin/timezone' => 'dashboard',
        'admin/settings/logo' => 'dashboard',
        'admin/change-password' => 'dashboard',
        'admin/push-vapid-public' => 'dashboard',
        'admin/push-subscribe' => 'dashboard',
        'admin/push-unsubscribe' => 'dashboard',
        'admin/push-test' => 'dashboard',
        'admin/notifications' => 'dashboard',
        'admin/notifications/count' => 'dashboard',
        'admin/notifications/mark-read' => 'dashboard',
        'admin/notifications/delete' => 'dashboard',
        'admin/notifications/purge' => 'dashboard',
        'qr/generate' => 'qr',
        'reports/final' => 'final_report',
        'reports/final-decision' => 'final_decision',
        'reports/leave-deduct' => 'leave_settlement',
        'reports/leave-deduct/item' => 'leave_settlement',
        'reports/salary-deduct' => 'deductions_report',
        'reports/salary-deduct/item' => 'deductions_report',
        'reports/leave-overview' => 'leave_overview',
        'reports/export' => 'attendance',
        'messages' => 'employee_messages',
        'admin/branches-activity' => 'dashboard',
        'admin/branch-monitor-detail' => 'dashboard',
        'admin/contacts' => 'employees',
    ];
    if (isset($table[$m])) return $table[$m];
    if (str_starts_with($m, 'reports/final')) return 'final_report';
    if (str_starts_with($m, 'reports/leave-settlement')) return 'leave_settlement';
    if (str_starts_with($m, 'reports/leave-deduct')) return 'leave_settlement';
    if (str_starts_with($m, 'reports/salary-deduct')) return 'deductions_report';
    if (str_starts_with($m, 'reports/leave-overview') || str_starts_with($m, 'reports/leave-ledger')) return 'leave_overview';
    if (str_starts_with($m, 'reports/employee-deductions-summary')) return 'deductions_report';
    if (str_starts_with($m, 'reports/final-issues')) return 'final_decision';
    return null;
}

function requireAdminPermission(string $moduleKey, string $action): void {
    $auth = getAuth();
    if (($auth['role'] ?? '') !== 'admin') respond(403, 'Forbidden');
    if (($auth['admin_type'] ?? 'general') === 'general') return;
    $subAdminId = (int)($auth['admin_id'] ?? 0);
    if ($subAdminId <= 0) respond(403, 'Admin account not recognized');
    $col = match ($action) {
        'create' => 'can_create',
        'update' => 'can_update',
        'delete' => 'can_delete',
        default => 'can_read'
    };
    $st = db()->prepare("SELECT {$col} AS allowed, can_read FROM company_sub_admin_permissions WHERE sub_admin_id=? AND module_key=? LIMIT 1");
    $st->execute([$subAdminId, $moduleKey]);
    $row = $st->fetch();
    if (!$row) respond(403, 'Permission denied');
    if ((int)($row['can_read'] ?? 0) !== 1) respond(403, 'Permission denied');
    if ((int)($row['allowed'] ?? 0) !== 1) respond(403, 'Permission denied');
}

function haversineDistance(float $lat1, float $lon1, float $lat2, float $lon2): float {
    $R = 6371000; // Earth radius in meters
    $φ1 = deg2rad($lat1); $φ2 = deg2rad($lat2);
    $Δφ = deg2rad($lat2 - $lat1);
    $Δλ = deg2rad($lon2 - $lon1);
    $a = sin($Δφ/2)**2 + cos($φ1)*cos($φ2)*sin($Δλ/2)**2;
    return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
}

/**
 * Allowed distance from branch pin = branch "Allowed Radius (meters)" from admin
 * plus GPS / indoor uncertainty (internal employees get a slightly higher GPS cap).
 */
function branchEffectiveGeofenceRadiusMeters(float $branchRadiusMeters, float $gpsAccuracyMeters, bool $forInternalEmployee): float
{
    $base = $branchRadiusMeters > 0 ? $branchRadiusMeters : MAX_DISTANCE_METERS;
    $gpsCap = $forInternalEmployee ? 300 : 150;
    $dynamicGpsAllowance = min(max($gpsAccuracyMeters, 0), $gpsCap);
    // Branch "Allowed radius" from admin is $base; add GPS / device uncertainty (higher cap for internal / indoor).
    $extra = max(GEOFENCE_TOLERANCE_METERS, $dynamicGpsAllowance);

    return $base + $extra;
}

/**
 * @return array<string,mixed>|null branch row including distance_meters if inside geofence
 */
function branchGeofenceMatch(array $br, float $lat, float $lng, float $gpsAccuracyMeters, bool $forInternalEmployee): ?array
{
    if (!$lat || !$lng || ($lat == 0.0 && $lng == 0.0)) {
        return null;
    }
    $branchLat = (float) $br['latitude'];
    $branchLng = (float) $br['longitude'];

    $distNormal = haversineDistance($lat, $lng, $branchLat, $branchLng);
    $distSwapped = haversineDistance($lat, $lng, $branchLng, $branchLat);
    $dist = min($distNormal, $distSwapped);

    $radius = (float) ($br['radius'] ?? 0);
    $effectiveRadius = branchEffectiveGeofenceRadiusMeters($radius, $gpsAccuracyMeters, $forInternalEmployee);

    if ($dist > $effectiveRadius) {
        return null;
    }
    $out = $br;
    $out['distance_meters'] = $dist;

    return $out;
}

/** Internal employees: respect assigned branch radius first (same site, different GPS point), then any branch. */
function findAttendanceBranchForInternal(string $companyId, float $lat, float $lng, float $gpsAccuracyMeters, ?int $employeeBranchId): ?array
{
    if ($employeeBranchId) {
        $stmt = db()->prepare('SELECT id, name, latitude, longitude, radius FROM branches WHERE company_id = ? AND id = ?');
        $stmt->execute([$companyId, $employeeBranchId]);
        $home = $stmt->fetch();
        if ($home) {
            $hit = branchGeofenceMatch($home, $lat, $lng, $gpsAccuracyMeters, true);
            if ($hit) {
                return $hit;
            }
        }
    }

    return findMatchedBranch($companyId, $lat, $lng, $gpsAccuracyMeters, true);
}

function findMatchedBranch(string $companyId, float $lat, float $lng, float $gpsAccuracyMeters = 0, bool $forInternalEmployee = false): ?array
{
    if (!$lat || !$lng || ($lat == 0.0 && $lng == 0.0)) {
        return null;
    }

    $stmt = db()->prepare('SELECT id, name, latitude, longitude, radius FROM branches WHERE company_id = ?');
    $stmt->execute([$companyId]);
    $branches = $stmt->fetchAll();
    if (!$branches) {
        return null;
    }

    $matchedBranch = null;
    $matchedDistance = null;
    foreach ($branches as $br) {
        $hit = branchGeofenceMatch($br, $lat, $lng, $gpsAccuracyMeters, $forInternalEmployee);
        if ($hit === null) {
            continue;
        }
        $dist = (float) $hit['distance_meters'];
        if ($matchedBranch === null || $dist < $matchedDistance) {
            $matchedBranch = $hit;
            $matchedDistance = $dist;
        }
    }

    if (!$matchedBranch) {
        return null;
    }
    $matchedBranch['distance_meters'] = $matchedDistance;

    return $matchedBranch;
}

function findNearestBranch(string $companyId, float $lat, float $lng): ?array {
    if (!$lat || !$lng || ($lat == 0.0 && $lng == 0.0)) return null;

    $stmt = db()->prepare("SELECT id, name, latitude, longitude FROM branches WHERE company_id = ?");
    $stmt->execute([$companyId]);
    $branches = $stmt->fetchAll();
    if (!$branches) return null;

    $nearest = null;
    $nearestDistance = null;
    foreach ($branches as $br) {
        $branchLat = (float)$br['latitude'];
        $branchLng = (float)$br['longitude'];
        $distNormal = haversineDistance($lat, $lng, $branchLat, $branchLng);
        $distSwapped = haversineDistance($lat, $lng, $branchLng, $branchLat);
        $dist = min($distNormal, $distSwapped);

        if ($nearest === null || $dist < $nearestDistance) {
            $nearest = $br;
            $nearestDistance = $dist;
        }
    }

    if (!$nearest) return null;
    $nearest['distance_meters'] = $nearestDistance;
    return $nearest;
}

function getBranchNameById(string $companyId, ?int $branchId): ?string {
    if (!$branchId) return null;
    $stmt = db()->prepare("SELECT name FROM branches WHERE company_id=? AND id=?");
    $stmt->execute([$companyId, $branchId]);
    $name = $stmt->fetchColumn();
    return $name ?: null;
}

function notifyAdminBranchMismatch(string $companyId, string $employeeName, ?string $homeBranchName, string $actualBranchName, string $attendanceType): void {
    try {
        $typeLabel = $attendanceType === 'check_out' ? 'خروج' : 'دخول';
        $home = $homeBranchName ?: 'غير محدد';
        $subject = "تنبيه بصمة من فرع بديل - {$employeeName}";
        $message = "تنبيه إداري: قام الموظف {$employeeName} بتسجيل {$typeLabel} من فرع بديل.\n\n"
                 . "الفرع الأساسي: {$home}\n"
                 . "الفرع الذي تم التسجيل منه: {$actualBranchName}\n"
                 . "وقت التسجيل: " . date('Y-m-d H:i:s') . "\n";

        adminNotifyInsert($companyId, 'attendance_branch', $subject, $message, '/hr/admin.html?hrpro_pwa=admin&open_tab=attendance');

        $stmt = db()->prepare("SELECT notification_email, name FROM companies WHERE company_id = ?");
        $stmt->execute([$companyId]);
        $company = $stmt->fetch();
        $to = $company['notification_email'] ?? '';
        if (!$to || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
            return;
        }

        sendEmail($to, $subject, $message, $company['name'] ?? 'HR System');
    } catch (Exception $e) {
        // non-fatal
    }
}

/**
 * Legacy attendance copies lived under uploads/employees/…/emp_{id}_{uniq}.ext (not a deliberate profile upload).
 * Paths under uploads/selfies/ may be the first bound profile photo — treat as account photo for UI.
 */
function employeeAccountPhotoUrl(?string $photoUrl): ?string
{
    if ($photoUrl === null || $photoUrl === '') {
        return null;
    }
    $norm = str_replace('\\', '/', $photoUrl);
    if (preg_match('#^uploads/employees/#', $norm) && preg_match('#/emp_\d+_#', $norm)) {
        return null;
    }

    return $photoUrl;
}

/** True when profile has no usable account photo: empty, or legacy attendance copy hidden by employeeAccountPhotoUrl(). */
function employeeNeedsAccountPhotoFromSelfie(?string $photoUrl): bool
{
    $raw = trim((string) ($photoUrl ?? ''));
    if ($raw === '') {
        return true;
    }

    return employeeAccountPhotoUrl($raw) === null;
}

/**
 * Photo path for employee login / auto-login JSON: uses profile when employeeAccountPhotoUrl() accepts it;
 * otherwise latest attendance selfie (and persists it when profile was empty or legacy-only).
 */
function resolveEmployeePhotoForLogin(array $emp): string
{
    $raw = trim((string) ($emp['photo_url'] ?? ''));
    if ($raw !== '') {
        $acct = employeeAccountPhotoUrl($raw);
        if ($acct !== null && $acct !== '') {
            return $acct;
        }
    }
    $pdo = db();
    $st = $pdo->prepare("SELECT selfie_path FROM attendance WHERE employee_id = ? AND company_id = ? AND selfie_path IS NOT NULL AND selfie_path != '' ORDER BY timestamp DESC LIMIT 1");
    $st->execute([(int) $emp['id'], $emp['company_id']]);
    $row = $st->fetch();
    $sp = ($row && !empty($row['selfie_path'])) ? trim((string) $row['selfie_path']) : '';
    if ($sp === '') {
        return '';
    }
    $isLegacyHidden = ($raw !== '' && employeeAccountPhotoUrl($raw) === null);
    if ($raw === '' || $isLegacyHidden) {
        $pdo->prepare('UPDATE employees SET photo_url = ? WHERE id = ? AND company_id = ?')->execute([$sp, (int) $emp['id'], $emp['company_id']]);
    }

    return $sp;
}

function saveEmployeePhoto(string $base64, string $companyId): ?string {
    $dir = 'uploads/employees/' . $companyId . '/';
    if (!is_dir($dir)) mkdir($dir, 0755, true);
    
    if (preg_match('/^data:image\/(\w+);base64,/', $base64, $m)) {
        $data = base64_decode(substr($base64, strpos($base64, ',') + 1));
    } else {
        $data = base64_decode($base64);
    }
    
    if (!$data) return null;
    
    $filename = 'emp_' . uniqid() . '.jpg';
    $path = $dir . $filename;
    
    // Resize & Compress using GD
    $img = @imagecreatefromstring($data);
    if ($img) {
        $width = imagesx($img);
        $height = imagesy($img);
        $max = 300; // Optimal size for welcome screen
        if ($width > $max || $height > $max) {
            if ($width > $height) {
                $newW = $max; $newH = ($height/$width)*$max;
            } else {
                $newH = $max; $newW = ($width/$height)*$max;
            }
            $tmp = imagecreatetruecolor($newW, $newH);
            imagecopyresampled($tmp, $img, 0,0,0,0, $newW, $newH, $width, $height);
            imagejpeg($tmp, $path, 75); // 75% quality for fast loading
            imagedestroy($tmp);
        } else {
            imagejpeg($img, $path, 75);
        }
        imagedestroy($img);
        return $path;
    }
    
    file_put_contents($path, $data);
    return $path;
}

function saveBase64Image(string $base64, string $prefix, string $companyId): ?string {
    $dir = 'uploads/employees/' . $companyId . '/';
    if (!is_dir($dir)) mkdir($dir, 0755, true);

    if (preg_match('/^data:image\/(\w+);base64,/', $base64, $m)) {
        $base64 = substr($base64, strpos($base64, ',') + 1);
        $ext    = in_array($m[1], ['jpeg','jpg','png','webp']) ? $m[1] : 'jpg';
    } else { $ext = 'jpg'; }
    $decoded = base64_decode($base64);
    if (!$decoded) return null;
    $filename = $prefix . '_' . uniqid() . '.' . $ext;
    file_put_contents($dir . $filename, $decoded);
    return $dir . $filename;
}

/** Attendance selfies: relative path under hr/uploads/selfies/{company_id}/ (matches UPLOAD_DIR intent). */
function saveSelfieBase64(string $base64, string $prefix, string $companyId): ?string {
    $safeCid = preg_replace('#[^a-zA-Z0-9\-]#', '_', $companyId);
    $relDir = 'uploads/selfies/' . $safeCid . '/';
    $absDir = rtrim((string) UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $safeCid;
    if (!is_dir($absDir) && !@mkdir($absDir, 0755, true)) {
        error_log('saveSelfieBase64: failed to mkdir ' . $absDir);
        return null;
    }

    if (preg_match('/^data:image\/(\w+);base64,/', $base64, $m)) {
        $base64 = substr($base64, strpos($base64, ',') + 1);
        $ext    = in_array($m[1], ['jpeg', 'jpg', 'png', 'webp'], true) ? $m[1] : 'jpg';
    } else {
        $ext = 'jpg';
    }
    $decoded = base64_decode($base64, true);
    if ($decoded === false || $decoded === '') {
        return null;
    }

    $maxSide = 1024;
    $jpegQuality = 75;
    $img = @imagecreatefromstring($decoded);
    if ($img !== false) {
        $width = imagesx($img);
        $height = imagesy($img);
        if ($width > 0 && $height > 0) {
            $out = $img;
            if ($width > $maxSide || $height > $maxSide) {
                if ($width > $height) {
                    $newW = $maxSide;
                    $newH = (int) round(($height / $width) * $maxSide);
                } else {
                    $newH = $maxSide;
                    $newW = (int) round(($width / $height) * $maxSide);
                }
                $tmp = imagecreatetruecolor($newW, $newH);
                imagecopyresampled($tmp, $img, 0, 0, 0, 0, $newW, $newH, $width, $height);
                imagedestroy($img);
                $out = $tmp;
            }
            $filename = $prefix . '_' . uniqid('', true) . '.jpg';
            $absPath = $absDir . DIRECTORY_SEPARATOR . $filename;
            if (@imagejpeg($out, $absPath, $jpegQuality)) {
                imagedestroy($out);
                return $relDir . $filename;
            }
            imagedestroy($out);
            error_log('saveSelfieBase64: failed to write jpeg ' . $absPath);
        } else {
            imagedestroy($img);
        }
    }

    $filename = $prefix . '_' . uniqid('', true) . '.' . $ext;
    $absPath = $absDir . DIRECTORY_SEPARATOR . $filename;
    if (file_put_contents($absPath, $decoded) === false) {
        error_log('saveSelfieBase64: failed to write ' . $absPath);
        return null;
    }

    return $relDir . $filename;
}

/** Branch photo only: uploads/branches/{company_id}/ */
function saveBranchPhotoBase64(string $base64, string $prefix, string $companyId): ?string {
    $dir = 'uploads/branches/' . $companyId . '/';
    if (!is_dir($dir)) mkdir($dir, 0755, true);

    if (preg_match('/^data:image\/(\w+);base64,/', $base64, $m)) {
        $base64 = substr($base64, strpos($base64, ',') + 1);
        $ext    = in_array($m[1], ['jpeg', 'jpg', 'png', 'webp'], true) ? $m[1] : 'jpg';
    } else {
        $ext = 'jpg';
    }
    $decoded = base64_decode($base64, true);
    if ($decoded === false || $decoded === '') {
        return null;
    }
    $filename = $prefix . '_' . uniqid() . '.' . $ext;
    $path = $dir . $filename;
    file_put_contents($path, $decoded);
    return $path;
}

function sanitizeEmployeeDocumentFileName(string $name): string {
    $b = basename(str_replace(["\0", '\\'], '', $name));
    $b = preg_replace('/[^a-zA-Z0-9._\-\x{0600}-\x{06FF}]+/u', '_', $b);
    $b = trim((string) $b, '._');
    if ($b === '') {
        $b = 'document';
    }
    if (function_exists('mb_strlen') && function_exists('mb_substr')) {
        return mb_strlen($b, 'UTF-8') > 180 ? mb_substr($b, 0, 180, 'UTF-8') : $b;
    }
    return strlen($b) > 180 ? substr($b, 0, 180) : $b;
}

/**
 * Employee attachments: uploads/{company_id}/employee_documents/{employee_id}/file
 * @return array{file_path:string,mime_type:string,file_size:int,original_name:string}|null
 */
function saveEmployeeDocumentFromBase64(string $base64, string $companyId, int $employeeId, string $originalName): ?array {
    $maxBytes = 12 * 1024 * 1024;
    $mime = 'application/octet-stream';
    $raw = $base64;
    // Accept data URLs with optional parameters before ";base64," (e.g. charset), which FileReader/browsers may emit.
    $lower = strtolower($base64);
    if (strpos($lower, 'data:') === 0) {
        $b64pos = stripos($base64, ';base64,');
        if ($b64pos !== false) {
            $header = trim(substr($base64, 5, $b64pos - 5));
            $mime = strtolower(trim(explode(';', $header, 2)[0]));
            $raw = substr($base64, $b64pos + strlen(';base64,'));
        }
    }
    $allowed = [
        'application/pdf',
        'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif', 'image/bmp',
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'application/vnd.ms-excel',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    ];
    // Some browsers report Office/PDF uploads as octet-stream; infer from extension.
    if (!in_array($mime, $allowed, true)) {
        $extGuess = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
        $extToMime = [
            'pdf' => 'application/pdf',
            'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jfif' => 'image/jpeg',
            'webp' => 'image/webp', 'gif' => 'image/gif', 'bmp' => 'image/bmp',
            'doc' => 'application/msword',
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'xls' => 'application/vnd.ms-excel',
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        ];
        if (isset($extToMime[$extGuess])) {
            $mime = $extToMime[$extGuess];
        }
    }
    if (!in_array($mime, $allowed, true)) {
        return null;
    }
    $decoded = base64_decode(preg_replace('/\s+/', '', $raw), true);
    if ($decoded === false || $decoded === '') {
        return null;
    }
    if (strlen($decoded) > $maxBytes) {
        return null;
    }
    $extMap = [
        'application/pdf' => 'pdf',
        'image/jpeg' => 'jpg', 'image/jpg' => 'jpg', 'image/png' => 'png',
        'image/webp' => 'webp', 'image/gif' => 'gif', 'image/bmp' => 'bmp',
        'application/msword' => 'doc',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
        'application/vnd.ms-excel' => 'xls',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
    ];
    $ext = $extMap[$mime] ?? 'bin';
    $safe = sanitizeEmployeeDocumentFileName($originalName);
    if (!preg_match('/\.' . preg_quote($ext, '/') . '$/i', $safe)) {
        $safe .= '.' . $ext;
    }
    // Always write under this API folder (cwd on shared hosting is often not /hr).
    $relDir = 'uploads/' . $companyId . '/employee_documents/' . $employeeId . '/';
    $dir = __DIR__ . '/' . str_replace('\\', '/', $relDir);
    if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
        error_log('saveEmployeeDocumentFromBase64: mkdir failed ' . $dir . ' (company ' . $companyId . ', employee ' . $employeeId . ')');
        return null;
    }
    if (!is_dir($dir)) {
        error_log('saveEmployeeDocumentFromBase64: directory missing after mkdir ' . $dir);
        return null;
    }
    $finalName = pathinfo($safe, PATHINFO_FILENAME) . '_' . uniqid() . '.' . $ext;
    $path = str_replace('\\', '/', $relDir . $finalName);
    $abs = __DIR__ . '/' . $path;
    $written = @file_put_contents($abs, $decoded, LOCK_EX);
    $len = strlen($decoded);
    if ($written === false || $written !== $len) {
        error_log('saveEmployeeDocumentFromBase64: file_put_contents failed or partial ' . $abs);
        @unlink($abs);
        return null;
    }
    if (!is_file($abs)) {
        return null;
    }
    return [
        'file_path' => $path,
        'mime_type' => $mime,
        'file_size' => $len,
        'original_name' => sanitizeEmployeeDocumentFileName($originalName),
    ];
}

function auditLog(string $actorType, string $actorId, string $action, array $details = [], ?string $companyId = null): void {
    try {
        db()->prepare("INSERT INTO audit_log (company_id,actor_type,actor_id,action,details,ip_address) VALUES (?,?,?,?,?,?)")
             ->execute([$companyId, $actorType, $actorId, $action, json_encode($details), $_SERVER['REMOTE_ADDR'] ?? '']);
    } catch (Throwable $e) { /* non-fatal */
        error_log('auditLog: ' . $e->getMessage());
    }
}

function generateUUID(): string {
    return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
        mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff),
        mt_rand(0,0x0fff)|0x4000, mt_rand(0,0x3fff)|0x8000,
        mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff));
}

/** Strong password for super-admin / privileged changes: returns Arabic error message or null if OK. */
function validateStrongPassword(string $pwd): ?string {
    if (strlen($pwd) < 12) {
        return 'Password must be at least 12 characters';
    }
    if (!preg_match('/[A-Z]/', $pwd)) {
        return 'Password must include an uppercase letter';
    }
    if (!preg_match('/[a-z]/', $pwd)) {
        return 'Password must include a lowercase letter';
    }
    if (!preg_match('/\d/', $pwd)) {
        return 'Password must include a digit';
    }
    if (!preg_match('/[^\p{L}\p{N}]/u', $pwd)) {
        return 'Password must include a special character';
    }
    return null;
}

// ─── SMART DEVICE BINDING HELPERS ───────────────────────────────────────────────
/**
 * Extract the FP hash portion from a device_id string.
 * New format: "FP:{fpHash}:R:{randomSeed}"
 * Legacy format: anything without FP: prefix
 */
function extractFpHash(?string $deviceId): ?string {
    if (!$deviceId) return null;
    if (str_starts_with($deviceId, 'FP:')) {
        $parts = explode(':', $deviceId);
        // Format: FP : {hash} : R : {seed}  → index [0]=FP [1]={hash}
        return $parts[1] ?? null;
    }
    return null; // legacy format — no fingerprint to extract
}

/**
 * Smart device check with fingerprint comparison.
 * Returns: ['ok'=>true] on pass, or ['ok'=>false, 'message'=>'...'] on fail.
 * If fingerprint matches but device_id differs (browser changed), auto-updates DB.
 *
 * @param array  $employee      Employee row from DB (must have device_id)
 * @param string $incomingId    The device_id sent by the client
 * @param string $context       'login' | 'attendance' (for audit log)
 */
function smartDeviceCheck(array $employee, ?string $incomingId, string $context = 'login'): array {
    $storedId   = $employee['device_id'];
    $employeeId = $employee['id'];
    $companyId  = $employee['company_id'];

    // 1. No device bound yet — bind on first use
    if ($storedId === null) {
        if ($incomingId) {
            db()->prepare("UPDATE employees SET device_id=?, device_bound_at=NOW() WHERE id=?")
               ->execute([$incomingId, $employeeId]);
            auditLog('employee', $employeeId, 'DEVICE_BOUND', ['device_id' => substr($incomingId,0,25)], $companyId);
        }
        return ['ok' => true, 'device_id' => $incomingId];
    }

    // 2. Exact match — Same browser/device (Re-login logic)
    if ($storedId === $incomingId) {
        return ['ok' => true, 'device_id' => $incomingId];
    }

    // 3. IDs differ — Check hardware fingerprints
    $storedFp   = extractFpHash($storedId);
    $incomingFp = extractFpHash($incomingId);

    // If both have fingerprints and they match, allow update
    if ($storedFp !== null && $incomingFp !== null && $storedFp === $incomingFp) {
        db()->prepare("UPDATE employees SET device_id=?, device_bound_at=NOW() WHERE id=?")
           ->execute([$incomingId, $employeeId]);
        auditLog('employee', $employeeId, 'AUTO_DEVICE_UPDATE_' . strtoupper($context),
            ['old' => substr($storedId,0,25), 'new' => substr($incomingId,0,25)], $companyId);
        return ['ok' => true, 'device_id' => $incomingId, 'auto_updated' => true];
    }

    // 4. Fingerprint mismatch or legacy ID comparison
    // If it's a login context and we can't verify the fingerprint (e.g. legacy ID),
    // we strictly block to prevent unauthorized device switching.
    auditLog('employee', $employeeId, 'LOGIN_DEVICE_MISMATCH',
        ['stored' => substr($storedId,0,25), 'incoming' => substr($incomingId ?? '',0,25)], $companyId);
    
    return [
        'ok' => false, 
        'message' => 'هذا الحساب مرتبط بجهاز آخر. تواصل مع الإدارة لإعادة ضبط الربط.'
    ];
}

// ─── SMTP (system-wide; one sender for all tenants — per-company recipients stay on companies.notification_email) ─
function hrproSmtpSecretKeyBin(): string
{
    return hash('sha256', (defined('SECRET_KEY') ? (string) SECRET_KEY : 'hrpro') . '|smtp_settings_v1', true);
}

function hrproEncryptSmtpPassword(string $plain): string
{
    if ($plain === '') {
        return '';
    }
    if (!function_exists('openssl_encrypt')) {
        return 'p64:' . base64_encode($plain);
    }
    $iv = random_bytes(16);
    $ct = openssl_encrypt($plain, 'AES-256-CBC', hrproSmtpSecretKeyBin(), OPENSSL_RAW_DATA, $iv);
    if ($ct === false) {
        return 'p64:' . base64_encode($plain);
    }

    return 'e1:' . base64_encode($iv . $ct);
}

function hrproDecryptSmtpPassword(string $stored): string
{
    if ($stored === '') {
        return '';
    }
    if (strpos($stored, 'p64:') === 0) {
        $d = base64_decode(substr($stored, 4), true);

        return is_string($d) ? $d : '';
    }
    if (strpos($stored, 'e1:') !== 0) {
        return $stored;
    }
    $bin = base64_decode(substr($stored, 3), true);
    if ($bin === false || strlen($bin) < 17) {
        return '';
    }
    $iv = substr($bin, 0, 16);
    $ct = substr($bin, 16);
    if (!function_exists('openssl_decrypt')) {
        return '';
    }
    $pt = openssl_decrypt($ct, 'AES-256-CBC', hrproSmtpSecretKeyBin(), OPENSSL_RAW_DATA, $iv);

    return $pt !== false ? $pt : '';
}

/** @return array{host:string,port:int,encryption:string,username:string,password:string,from_email:string,from_name:string,reply_to:?string,source:string} */
function hrproGetEffectiveSmtpConfig(): array
{
    $portDef = defined('SMTP_PORT') ? (int) SMTP_PORT : 587;
    $encDef = ($portDef === 465) ? 'ssl' : 'tls';
    $defaults = [
        'host' => defined('SMTP_HOST') ? (string) SMTP_HOST : '',
        'port' => $portDef > 0 ? $portDef : 587,
        'encryption' => $encDef,
        'username' => defined('SMTP_USER') ? (string) SMTP_USER : '',
        'password' => defined('SMTP_PASS') ? (string) SMTP_PASS : '',
        'from_email' => defined('SMTP_FROM_EMAIL') ? (string) SMTP_FROM_EMAIL : '',
        'from_name' => 'HR Pro',
        'reply_to' => null,
        'source' => 'constants',
    ];
    try {
        $st = db()->prepare('SELECT smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password_blob, from_email, from_name, reply_to_email FROM system_smtp_settings WHERE id = 1');
        $st->execute();
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            return $defaults;
        }
        $host = trim((string) ($row['smtp_host'] ?? ''));
        if ($host === '') {
            return $defaults;
        }
        $port = (int) ($row['smtp_port'] ?? 587);
        if ($port < 1 || $port > 65535) {
            $port = 587;
        }
        $enc = strtolower(trim((string) ($row['smtp_encryption'] ?? 'tls')));
        if (!in_array($enc, ['tls', 'ssl', 'none'], true)) {
            $enc = 'tls';
        }
        $from = trim((string) ($row['from_email'] ?? ''));
        if ($from === '' || !filter_var($from, FILTER_VALIDATE_EMAIL)) {
            return $defaults;
        }
        $rt = trim((string) ($row['reply_to_email'] ?? ''));
        $replyTo = ($rt !== '' && filter_var($rt, FILTER_VALIDATE_EMAIL)) ? $rt : null;

        return [
            'host' => $host,
            'port' => $port,
            'encryption' => $enc,
            'username' => trim((string) ($row['smtp_username'] ?? '')),
            'password' => hrproDecryptSmtpPassword((string) ($row['smtp_password_blob'] ?? '')),
            'from_email' => $from,
            'from_name' => trim((string) ($row['from_name'] ?? '')) ?: 'HR Pro',
            'reply_to' => $replyTo,
            'source' => 'database',
        ];
    } catch (Throwable $e) {
        return $defaults;
    }
}

// ─── EMAIL SENDER FUNCTION ───────────────────────────────────────────────────
function hrproEmployeeLoginUrl(): string {
    $host = $_SERVER['HTTP_HOST'] ?? '';
    if ($host === '') {
        return '/hr/login.html';
    }
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (string) ($_SERVER['SERVER_PORT'] ?? '') === '443';
    $scheme = $https ? 'https' : 'http';

    return $scheme . '://' . $host . '/hr/login.html';
}

function encodeEmailSubjectForMail(string $subject): string {
    if (!preg_match('/[^\x20-\x7E]/', $subject)) {
        return $subject;
    }

    return mb_encode_mimeheader($subject, 'UTF-8', 'B', "\r\n");
}

function sendEmail($to, $subject, $message, $companyName) {
    $cfg = hrproGetEffectiveSmtpConfig();
    $fromEmail = $cfg['from_email'] !== '' ? $cfg['from_email'] : 'no-reply@hrpro.com';
    $fromLabel = $cfg['from_name'] ?: 'HR Pro';
    if ($companyName !== '' && $companyName !== $fromLabel) {
        $fromLabel = $fromLabel . ' — ' . $companyName;
    }
    $replyAddr = $cfg['reply_to'] ?: $fromEmail;

    // إذا كانت مكتبة PHPMailer متوفرة (يُفضل استخدامها)
    if (class_exists('PHPMailer\PHPMailer\PHPMailer')) {
        try {
            $mail = new PHPMailer\PHPMailer\PHPMailer(true);
            $mail->isSMTP();
            $mail->Host = $cfg['host'];
            $mail->SMTPAuth = true;
            $mail->Username = $cfg['username'];
            $mail->Password = $cfg['password'];
            if ($cfg['encryption'] === 'ssl') {
                $mail->SMTPSecure = 'ssl';
            } elseif ($cfg['encryption'] === 'none') {
                $mail->SMTPAutoTLS = false;
                $mail->SMTPSecure = '';
            } else {
                $mail->SMTPSecure = 'tls';
            }
            $mail->Port = (int) $cfg['port'];
            $mail->CharSet = 'UTF-8';
            $mail->setFrom($fromEmail, $fromLabel);
            $mail->Sender = $fromEmail; // sets Return-Path to match From, prevents "on behalf of"
            if ($replyAddr !== $fromEmail) {
                $mail->addReplyTo($replyAddr, $cfg['from_name'] ?: 'HR Pro');
            }
            $mail->addAddress($to);
            $mail->Subject = $subject;
            $mail->Body = $message;
            return $mail->send();
        } catch (Exception $e) {
            error_log('Mailer Error: ' . (isset($mail) ? $mail->ErrorInfo : $e->getMessage()));
            return false;
        }
    }

    // دالة mail() التقليدية كبديل في حالة عدم توفر PHPMailer
    $fromHdr = mb_encode_mimeheader($fromLabel, 'UTF-8', 'Q');
    $subjectEncoded = encodeEmailSubjectForMail($subject);
    $headers = "From: $fromHdr <$fromEmail>\r\n" .
               "Reply-To: $replyAddr\r\n" .
               "MIME-Version: 1.0\r\n" .
               "Content-Type: text/plain; charset=UTF-8\r\n" .
               "Content-Transfer-Encoding: 8bit\r\n" .
               "X-Auto-Response-Suppress: All\r\n" .
               "Auto-Submitted: auto-generated\r\n" .
               "X-Mailer: HR-Pro/" . phpversion();

    return @mail($to, $subjectEncoded, $message, $headers);
}

/**
 * Like sendEmail() but returns ['ok' => bool, 'error' => string] so the
 * SMTP test endpoint can surface the real PHPMailer / transport error.
 *
 * @return array{ok:bool,error:string}
 */
function sendEmailDebug(string $to, string $subject, string $message, string $companyName): array
{
    $cfg = hrproGetEffectiveSmtpConfig();
    $fromEmail = $cfg['from_email'] !== '' ? $cfg['from_email'] : 'no-reply@hrpro.com';
    $fromLabel = $cfg['from_name'] ?: 'HR Pro';
    if ($companyName !== '' && $companyName !== $fromLabel) {
        $fromLabel = $fromLabel . ' — ' . $companyName;
    }
    $replyAddr = $cfg['reply_to'] ?: $fromEmail;

    if (class_exists('PHPMailer\PHPMailer\PHPMailer')) {
        try {
            $mail = new PHPMailer\PHPMailer\PHPMailer(true);
            $mail->isSMTP();
            $mail->Host       = $cfg['host'];
            $mail->SMTPAuth   = true;
            $mail->Username   = $cfg['username'];
            $mail->Password   = $cfg['password'];
            if ($cfg['encryption'] === 'ssl') {
                $mail->SMTPSecure = 'ssl';
            } elseif ($cfg['encryption'] === 'none') {
                $mail->SMTPAutoTLS = false;
                $mail->SMTPSecure  = '';
            } else {
                $mail->SMTPSecure = 'tls';
            }
            $mail->Port    = (int) $cfg['port'];
            $mail->CharSet = 'UTF-8';
            $mail->setFrom($fromEmail, $fromLabel);
            $mail->Sender  = $fromEmail; // sets Return-Path to match From, prevents "on behalf of"
            if ($replyAddr !== $fromEmail) {
                $mail->addReplyTo($replyAddr, $cfg['from_name'] ?: 'HR Pro');
            }
            $mail->addAddress($to);
            $mail->Subject = $subject;
            $mail->Body    = $message;
            $mail->send();
            return ['ok' => true, 'error' => ''];
        } catch (Exception $e) {
            $detail = isset($mail) && $mail->ErrorInfo !== '' ? $mail->ErrorInfo : $e->getMessage();
            error_log('Mailer Error (debug): ' . $detail);
            return ['ok' => false, 'error' => $detail];
        }
    }

    // Fallback: plain mail()
    $fromHdr        = mb_encode_mimeheader($fromLabel, 'UTF-8', 'Q');
    $subjectEncoded = encodeEmailSubjectForMail($subject);
    $headers        = "From: $fromHdr <$fromEmail>\r\n" .
                      "Reply-To: $replyAddr\r\n" .
                      "MIME-Version: 1.0\r\n" .
                      "Content-Type: text/plain; charset=UTF-8\r\n" .
                      "Content-Transfer-Encoding: 8bit\r\n" .
                      "X-Auto-Response-Suppress: All\r\n" .
                      "Auto-Submitted: auto-generated\r\n" .
                      "X-Mailer: HR-Pro/" . phpversion();
    $sent = @mail($to, $subjectEncoded, $message, $headers);
    return ['ok' => (bool) $sent, 'error' => $sent ? '' : 'mail() returned false (PHPMailer not installed; check server mail config)'];
}

function leaveTypeLabelAr(string $lt): string
{
    return match ($lt) {
        'annual' => 'إجازة سنوية',
        'casual' => 'إجازة عارضة',
        'sick' => 'إجازة مرضية',
        'permission' => 'إذن',
        default => $lt,
    };
}

/**
 * Approved/pending leave & permission rows overlapping a calendar day (branch monitor).
 *
 * @param string       $companyId companies.company_id (UUID string, not integer)
 * @param array<mixed> $employeeIds
 *
 * @return array<int, list<array<string, mixed>>>
 */
function branchMonitorRequestsForDay(PDO $pdo, string $companyId, string $dayYmd, array $employeeIds): array
{
    ensureLeaveRequestPermissionKindColumn($pdo);
    $employeeIds = array_values(array_unique(array_filter(array_map(static function ($id) {
        return (int) $id;
    }, $employeeIds), static function ($id) {
        return $id > 0;
    })));
    $byEmp = [];
    foreach ($employeeIds as $eid) {
        $byEmp[$eid] = [];
    }
    if ($employeeIds === []) {
        return $byEmp;
    }

    $placeholders = implode(',', array_fill(0, count($employeeIds), '?'));
    $sql = "
        SELECT employee_id, leave_type, status, permission_from, permission_to, permission_kind,
               start_date, end_date, reason
        FROM leave_requests
        WHERE company_id = ?
          AND status IN ('approved', 'pending')
          AND start_date <= ?
          AND end_date >= ?
          AND leave_type IN ('annual','casual','sick','permission')
          AND employee_id IN ($placeholders)
        ORDER BY employee_id,
          CASE WHEN status = 'approved' THEN 0 ELSE 1 END,
          CASE leave_type WHEN 'annual' THEN 1 WHEN 'casual' THEN 2 WHEN 'sick' THEN 3 WHEN 'permission' THEN 4 ELSE 5 END,
          id ASC
    ";
    $params = array_merge([$companyId, $dayYmd, $dayYmd], $employeeIds);
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);

    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        $eid = (int) ($row['employee_id'] ?? 0);
        if (!isset($byEmp[$eid])) {
            continue;
        }
        $lt = strtolower(trim((string) ($row['leave_type'] ?? '')));
        $st = (string) ($row['status'] ?? '');
        if (! in_array($st, ['approved', 'pending'], true)) {
            continue;
        }
        if (in_array($lt, ['annual', 'casual', 'sick'], true)) {
            $byEmp[$eid][] = [
                'kind' => 'leave',
                'leave_type' => $lt,
                'status' => $st,
                'start_date' => $row['start_date'] ?? null,
                'end_date' => $row['end_date'] ?? null,
                'reason' => $row['reason'] ?? null,
            ];
        } elseif ($lt === 'permission') {
            $pk = strtolower(trim((string) ($row['permission_kind'] ?? '')));
            $byEmp[$eid][] = [
                'kind' => 'permission',
                'mission' => ($pk === 'mission'),
                'from' => $row['permission_from'] ?? null,
                'to' => $row['permission_to'] ?? null,
                'status' => $st,
                'start_date' => $row['start_date'] ?? null,
                'end_date' => $row['end_date'] ?? null,
                'reason' => $row['reason'] ?? null,
            ];
        }
    }

    return $byEmp;
}

/**
 * @return array{0:string,1:string} subject, plain body
 */
function buildLeaveRequestNotifyEmail(
    string $companyName,
    string $employeeName,
    string $employeeCode,
    string $reqKind,
    string $startDate,
    string $endDate,
    string $reason,
    ?string $permFrom,
    ?string $permTo,
    string $leaveType
): array {
    $isPerm = $reqKind === 'permission';
    $typeLine = $isPerm
        ? ('نوع الطلب: إذن (فترة زمنية)' . "\nنوع الإجازة في السجل: " . leaveTypeLabelAr($leaveType))
        : ('نوع الطلب: إجازة' . "\nنوع الإجازة: " . leaveTypeLabelAr($leaveType));
    $subject = ($isPerm ? '[HR Pro] طلب إذن جديد — ' : '[HR Pro] طلب إجازة جديد — ')
        . $companyName . ' — ' . $employeeName;
    $body = "══════════════════════════════════════════════════\n"
        . " HR Pro — " . ($isPerm ? 'إشعار طلب إذن' : 'إشعار طلب إجازة') . "\n"
        . "══════════════════════════════════════════════════\n\n"
        . "الشركة: {$companyName}\n"
        . "{$typeLine}\n"
        . "اسم الموظف: {$employeeName}\n"
        . "كود الموظف: {$employeeCode}\n"
        . "من تاريخ: {$startDate}\n"
        . "إلى تاريخ: {$endDate}\n";
    if ($isPerm && $permFrom !== null && $permTo !== null && $permFrom !== '' && $permTo !== '') {
        $body .= "وقت الإذن: من {$permFrom} إلى {$permTo}\n";
    }
    $body .= "\nنص الطلب / السبب:\n"
        . "──────────────────────────────────────────────────\n"
        . $reason . "\n"
        . "──────────────────────────────────────────────────\n\n"
        . "يرجى مراجعة الطلب من لوحة الإدارة:\n"
        . hrproPublicAdminLeaveUrl() . "\n\n"
        . '— ' . "رسالة آلية من نظام HR Pro\n";

    return [$subject, $body];
}

function hrproPublicAdminLeaveUrl(): string
{
    $host = $_SERVER['HTTP_HOST'] ?? '';
    if ($host === '') {
        return '/hr/admin.html?open_tab=leave';
    }
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (string) ($_SERVER['SERVER_PORT'] ?? '') === '443';
    $scheme = $https ? 'https' : 'http';

    return $scheme . '://' . $host . '/hr/admin.html?hrpro_pwa=admin&open_tab=leave';
}

function defaultEmployeeWelcomeEmailTemplate(): string
{
    return "مرحباً {employee_name}،\n\n"
        . "تم إنشاء حسابك في {company_name}، أو تم إعادة تعيين كلمة المرور من الإدارة.\n\n"
        . "بيانات الدخول إلى تطبيق الموظف:\n"
        . "  • اسم المستخدم: {username}\n"
        . "  • كلمة المرور الحالية: {password}\n"
        . "  • كود الموظف: {employee_code}\n\n"
        . "رابط الدخول:\n{login_url}\n\n"
        . "مع التحية،\n{company_name}\n";
}

function applyEmployeeWelcomeTemplate(?string $template, array $vars): string
{
    $tpl = trim((string) $template);
    if ($tpl === '') {
        $tpl = defaultEmployeeWelcomeEmailTemplate();
    }
    $map = [
        '{employee_name}' => (string) ($vars['employee_name'] ?? ''),
        '{username}' => (string) ($vars['username'] ?? ''),
        '{password}' => (string) ($vars['password'] ?? ''),
        '{employee_code}' => (string) ($vars['employee_code'] ?? ''),
        '{company_name}' => (string) ($vars['company_name'] ?? ''),
        '{login_url}' => (string) ($vars['login_url'] ?? hrproEmployeeLoginUrl()),
    ];

    return str_replace(array_keys($map), array_values($map), $tpl);
}

function sendEmployeeCredentialsEmail(string $companyId, string $toEmail, string $employeeName, string $username, string $plainPassword, string $employeeCode, string $context): bool
{
    $toEmail = trim($toEmail);
    if ($toEmail === '' || !filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
        return false;
    }
    $co = db()->prepare('SELECT name, employee_welcome_email_body FROM companies WHERE company_id=?');
    $co->execute([$companyId]);
    $row = $co->fetch();
    if (!$row) {
        return false;
    }
    $companyName = (string) ($row['name'] ?? 'HR Pro');
    $tpl = $row['employee_welcome_email_body'] ?? null;
    $body = applyEmployeeWelcomeTemplate(is_string($tpl) ? $tpl : null, [
        'employee_name' => $employeeName,
        'username' => $username,
        'password' => $plainPassword,
        'employee_code' => $employeeCode,
        'company_name' => $companyName,
        'login_url' => hrproEmployeeLoginUrl(),
    ]);
    $subject = $context === 'password_reset'
        ? ('[HR Pro] تم إعادة تعيين كلمة المرور — ' . $companyName)
        : ('[HR Pro] بيانات حسابك — ' . $companyName);

    return sendEmail($toEmail, $subject, $body, $companyName);
}

/** @return array{public:string,private:string,subject:string,source:string} */
function hrproGetEffectiveVapidConfig(): array
{
    $subj = trim((string) ADMIN_PUSH_SUBJECT);
    if ($subj === '') {
        $subj = 'mailto:admin@localhost';
    }
    $fromEnv = [
        'public' => trim((string) ADMIN_PUSH_VAPID_PUBLIC),
        'private' => trim((string) ADMIN_PUSH_VAPID_PRIVATE),
        'subject' => $subj,
        'source' => 'env',
    ];
    try {
        $st = db()->prepare('SELECT vapid_public_key, vapid_private_blob, vapid_subject FROM system_webpush_settings WHERE id = 1');
        $st->execute();
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            return $fromEnv;
        }
        $pub = trim((string) ($row['vapid_public_key'] ?? ''));
        $blob = trim((string) ($row['vapid_private_blob'] ?? ''));
        if ($pub === '' || $blob === '') {
            return $fromEnv;
        }
        $priv = hrproDecryptSmtpPassword($blob);
        $s = trim((string) ($row['vapid_subject'] ?? ''));
        if ($s === '') {
            $s = 'mailto:admin@localhost';
        }

        return [
            'public' => $pub,
            'private' => $priv,
            'subject' => $s,
            'source' => 'database',
        ];
    } catch (Throwable $e) {
        return $fromEnv;
    }
}

function hrproVapidKeysPresent(): bool
{
    $c = hrproGetEffectiveVapidConfig();

    return $c['public'] !== '' && $c['private'] !== '';
}

function hrproWebPushVendorAvailable(): bool
{
    static $cached = null;
    if ($cached !== null) {
        return $cached;
    }
    $autoload = __DIR__ . '/vendor/autoload.php';
    if (!is_file($autoload)) {
        return $cached = false;
    }
    require_once $autoload;
    $cached = class_exists(\Minishlink\WebPush\WebPush::class);

    return $cached;
}

/** In-app admin feed + optional Web Push to devices subscribed for this company. */
function adminNotifyInsert(string $companyId, string $type, string $title, ?string $body, ?string $actionUrl): void {
    try {
        $url = $actionUrl ?: '/hr/admin.html?hrpro_pwa=admin';
        $stmt = db()->prepare("INSERT INTO admin_notifications (company_id, type, title, body, action_url) VALUES (?,?,?,?,?)");
        $stmt->execute([$companyId, $type, $title, $body, $url]);
    } catch (Exception $e) {
        error_log('adminNotifyInsert: ' . $e->getMessage());
        return;
    }
    adminSendWebPush($companyId, $title, (string)($body ?? ''), $url);
}

/** @param 'admin_push_subscriptions'|'employee_push_subscriptions' $table */
function pushSendToCompanySubscriptions(string $companyId, string $table, string $title, string $body, string $url, ?int $employeeId = null): void {
    if ($table !== 'admin_push_subscriptions' && $table !== 'employee_push_subscriptions') {
        return;
    }
    if (!hrproVapidKeysPresent()) {
        return;
    }
    if (!hrproWebPushVendorAvailable()) {
        return;
    }
    $targetedEmployee = ($table === 'employee_push_subscriptions' && $employeeId !== null);
    try {
        $v = hrproGetEffectiveVapidConfig();
        $auth = [
            'VAPID' => [
                'subject' => $v['subject'],
                'publicKey' => $v['public'],
                'privateKey' => $v['private'],
            ],
        ];
        $webPush = new \Minishlink\WebPush\WebPush($auth);
        if ($table === 'admin_push_subscriptions') {
            $stmt = db()->prepare('SELECT subscription_json FROM admin_push_subscriptions WHERE company_id=?');
            $stmt->execute([$companyId]);
        } elseif ($targetedEmployee) {
            $stmt = db()->prepare('SELECT subscription_json FROM employee_push_subscriptions WHERE company_id=? AND employee_id=?');
            $stmt->execute([$companyId, $employeeId]);
        } else {
            $stmt = db()->prepare('SELECT subscription_json FROM employee_push_subscriptions WHERE company_id=?');
            $stmt->execute([$companyId]);
        }
        $rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
        $payload = json_encode(['title' => $title, 'body' => $body, 'url' => $url], JSON_UNESCAPED_UNICODE);
        foreach ($rows as $json) {
            $arr = json_decode((string)$json, true);
            if (!$arr || empty($arr['endpoint'])) {
                continue;
            }
            $sub = \Minishlink\WebPush\Subscription::create($arr);
            $webPush->queueNotification($sub, $payload, ['TTL' => 86400, 'urgency' => 'high']);
        }
        if ($table === 'admin_push_subscriptions') {
            $del = db()->prepare('DELETE FROM admin_push_subscriptions WHERE company_id=? AND endpoint_hash=?');
        } elseif ($targetedEmployee) {
            $del = db()->prepare('DELETE FROM employee_push_subscriptions WHERE company_id=? AND employee_id=? AND endpoint_hash=?');
        } else {
            $del = db()->prepare('DELETE FROM employee_push_subscriptions WHERE company_id=? AND endpoint_hash=?');
        }
        foreach ($webPush->flush() as $report) {
            if ($report->isSuccess()) {
                continue;
            }
            if (method_exists($report, 'isSubscriptionExpired') && $report->isSubscriptionExpired()) {
                $ep = method_exists($report, 'getEndpoint') ? $report->getEndpoint() : '';
                if ($ep !== '') {
                    $h = hash('sha256', $ep);
                    try {
                        if ($targetedEmployee) {
                            $del->execute([$companyId, $employeeId, $h]);
                        } else {
                            $del->execute([$companyId, $h]);
                        }
                    } catch (Exception $e) { /* ignore */ }
                }
            }
        }
    } catch (Throwable $e) {
        error_log('pushSendToCompanySubscriptions: ' . $e->getMessage());
    }
}

function adminSendWebPush(string $companyId, string $title, string $body, string $url): void {
    pushSendToCompanySubscriptions($companyId, 'admin_push_subscriptions', $title, $body, $url);
}

function employeeSendWebPushCompany(string $companyId, string $title, string $body, string $url): void {
    pushSendToCompanySubscriptions($companyId, 'employee_push_subscriptions', $title, $body, $url);
}

function employeeSendWebPushToEmployee(string $companyId, int $employeeId, string $title, string $body, string $url): void {
    if ($employeeId <= 0) {
        return;
    }
    pushSendToCompanySubscriptions($companyId, 'employee_push_subscriptions', $title, $body, $url, $employeeId);
}

// ─── ROUTER ──────────────────────────────────────────────────────────────────
$input  = input();
$method = $input['_method'] ?? $_SERVER['REQUEST_METHOD'];
$route  = $_GET['route'] ?? '';
$route  = trim($route, '/');
$parts  = explode('/', $route);
$action = $parts[0] ?? '';

$match = $action;
if (isset($parts[1])) {
    if (in_array($action, ['auth', 'qr', 'reports', 'dashboard', 'super'])) {
        if ($action === 'reports' && isset($parts[1])) {
            if ($parts[1] === 'leave-deduct' && isset($parts[2]) && $parts[2] !== '') {
                $match = 'reports/leave-deduct/item';
            } elseif ($parts[1] === 'salary-deduct' && isset($parts[2]) && $parts[2] !== '') {
                $match = 'reports/salary-deduct/item';
            } else {
                $match = $action . '/' . $parts[1];
            }
        } elseif ($action === 'super' && isset($parts[1])) {
            if ($parts[1] === 'company-admins' && isset($parts[2]) && in_array($parts[2], ['password', 'status'], true)) {
                $match = 'super/company-admins/' . $parts[2];
            } else {
                $match = $action . '/' . $parts[1];
            }
        } else {
            $match = $action . '/' . $parts[1];
        }
    } elseif ($action === 'attendance' && isset($parts[1])) {
        if ($parts[1] === 'my')      $match = 'attendance/my';
        if ($parts[1] === 'summary') $match = 'attendance/summary';
        if ($parts[1] === 'direct')  $match = 'attendance/direct';
        if ($parts[1] === 'note')    $match = 'attendance/note';
        if ($parts[1] === 'employee-note') {
            $match = 'attendance/employee-note';
        }
    } elseif ($action === 'team' && isset($parts[1])) {
        if ($parts[1] === 'manager-meta') $match = 'team/manager-meta';
        if ($parts[1] === 'attendance')   $match = 'team/attendance';
        if ($parts[1] === 'note')         $match = 'team/note';
    } elseif ($action === 'employees') {
        if (isset($parts[1]) && ctype_digit((string) $parts[1]) && isset($parts[2]) && $parts[2] === 'policy') {
            $match = 'employees/item/policy';
        } elseif (isset($parts[1]) && $parts[1] === 'me' && isset($parts[2])) {
            if ($parts[2] === 'policy') {
                $match = 'employees/me/policy';
            } elseif ($parts[2] === 'screen-flags') {
                $match = 'employees/me/screen-flags';
            }
        } elseif (isset($parts[2])) {
            if ($parts[2] === 'documents') {
                if (isset($parts[4]) && $parts[4] === 'download' && isset($parts[3]) && $parts[3] !== '' && ctype_digit((string) $parts[3])) {
                    $match = 'employees/documents-download';
                } else {
                    $match = (isset($parts[3]) && $parts[3] !== '') ? 'employees/documents-item' : 'employees/documents';
                }
            } elseif ($parts[2] === 'reset-device') {
                $match = 'employees/reset-device';
            } elseif ($parts[2] === 'reset-password') {
                $match = 'employees/reset-password';
            }
        } elseif (isset($parts[1]) && $parts[1] === 'acknowledge-must-change-password') {
            $match = 'employees/acknowledge-must-change-password';
        } elseif (isset($parts[1]) && $parts[1] === 'change-password') {
            $match = 'employees/change-password';
        } elseif (isset($parts[1]) && $parts[1] === 'profile') {
            $match = 'employees/profile';
        }
    } elseif ($action === 'messages') {
        if ($method === 'PUT' && isset($parts[1])) {
            if ($parts[1] === 'mark-read') {
                $match = 'messages/mark-read';
            } elseif ($parts[1] === 'archive') {
                $match = 'messages/archive';
            } elseif ($parts[1] === 'purge') {
                $match = 'messages/purge';
            } elseif (isset($parts[2]) && $parts[2] === 'read') {
                $match = 'messages/read';
            }
        }
    } elseif ($action === 'user' && isset($parts[1]) && $parts[1] === 'language') {
        $match = 'user/language';
    } elseif ($action === 'admin') {
        if (isset($parts[1]) && $parts[1] === 'change-password') {
            $match = 'admin/change-password';
        } elseif (isset($parts[1]) && $parts[1] === 'welcome-email-template') {
            $match = 'admin/welcome-email-template';
        } elseif (isset($parts[1]) && $parts[1] === 'mobile-app-url') {
            $match = 'admin/mobile-app-url';
        } elseif (isset($parts[1]) && $parts[1] === 'notification-email') {
            $match = 'admin/notification-email';
        } elseif (isset($parts[1]) && $parts[1] === 'timezone') {
            $match = 'admin/timezone';
        } elseif (isset($parts[1]) && $parts[1] === 'settings' && isset($parts[2]) && $parts[2] === 'logo') {
            $match = 'admin/settings/logo';
        } elseif (isset($parts[1]) && $parts[1] === 'attendance' && isset($parts[2]) && $parts[2] === 'manual') {
            $match = 'admin/attendance/manual';
        } elseif (isset($parts[1]) && $parts[1] === 'branches-activity') {
            $match = 'admin/branches-activity';
        } elseif (isset($parts[1]) && $parts[1] === 'branch-monitor-detail') {
            $match = 'admin/branch-monitor-detail';
        } elseif (isset($parts[1]) && $parts[1] === 'contacts') {
            $match = 'admin/contacts';
        } elseif (isset($parts[1]) && $parts[1] === 'notifications') {
            if (isset($parts[2]) && $parts[2] === 'count') {
                $match = 'admin/notifications/count';
            } elseif (isset($parts[2]) && $parts[2] === 'mark-read') {
                $match = 'admin/notifications/mark-read';
            } elseif (isset($parts[2]) && $parts[2] === 'delete') {
                $match = 'admin/notifications/delete';
            } elseif (isset($parts[2]) && $parts[2] === 'purge') {
                $match = 'admin/notifications/purge';
            } else {
                $match = 'admin/notifications';
            }
        } elseif (isset($parts[1]) && $parts[1] === 'push-vapid-public') {
            $match = 'admin/push-vapid-public';
        } elseif (isset($parts[1]) && $parts[1] === 'push-subscribe') {
            $match = 'admin/push-subscribe';
        } elseif (isset($parts[1]) && $parts[1] === 'push-unsubscribe') {
            $match = 'admin/push-unsubscribe';
        } elseif (isset($parts[1]) && $parts[1] === 'push-test') {
            $match = 'admin/push-test';
        } elseif (isset($parts[1]) && $parts[1] === 'company-news') {
            if (!empty($parts[2])) {
                $match = 'admin/company-news/item';
            } else {
                $match = 'admin/company-news';
            }
        }
    } elseif ($action === 'company-news') {
        $match = 'company-news';
    } elseif ($action === 'employee') {
        if (isset($parts[1]) && $parts[1] === 'push-vapid-public') {
            $match = 'employee/push-vapid-public';
        } elseif (isset($parts[1]) && $parts[1] === 'push-subscribe') {
            $match = 'employee/push-subscribe';
        } elseif (isset($parts[1]) && $parts[1] === 'push-unsubscribe') {
            $match = 'employee/push-unsubscribe';
        } elseif (isset($parts[1]) && $parts[1] === 'contacts') {
            $match = 'employee/contacts';
        }
    } elseif ($action === 'departments') {
        $match = 'departments';
    } elseif ($action === 'branches') {
        $match = 'branches';
    } elseif ($action === 'leave-policies') {
        $match = 'leave-policies';
    } elseif ($action === 'leave' && isset($parts[1]) && $parts[1] === 'balance') {
        $match = 'leave/balance';
    } elseif ($action === 'official-holidays') {
        $match = (isset($parts[1]) && $parts[1] !== '') ? 'official-holidays/item' : 'official-holidays';
    }
}

$authOptional = getAuthOptional();
if (is_array($authOptional) && ($authOptional['role'] ?? '') === 'admin' && (($authOptional['admin_type'] ?? 'general') === 'sub')) {
    $moduleKey = resolveAdminModuleByMatch($match);
    if ($moduleKey !== null) {
        requireAdminPermission($moduleKey, adminActionFromMethod((string)$method));
    }
}

try {
    switch ("$method:$match") {

// ═══════════════════════════════════════════════════════════════════════════════
// AUTH
// ═══════════════════════════════════════════════════════════════════════════════
    case 'POST:auth/super-login':
        $d = input();
        $stmt = db()->prepare("SELECT * FROM super_admins WHERE username = ?");
        $stmt->execute([$d['username'] ?? '']);
        $admin = $stmt->fetch();
        if (!$admin || !password_verify($d['password'] ?? '', $admin['password'])) {
            respond(401, 'Invalid credentials');
        }
        $token = generateToken(['role'=>'super_admin','id'=>$admin['id'],'username'=>$admin['username']]);
        auditLog('super_admin', $admin['id'], 'LOGIN');
        respond(200, ['token'=>$token,'role'=>'super_admin','username'=>$admin['username'],'language'=>$admin['language'] ?? 'ar']);

    case 'POST:trial-request':
        try {
            $d = input();
            $name = isset($d['company_name']) ? trim((string) $d['company_name']) : '';
            $phone = isset($d['phone']) ? trim((string) $d['phone']) : '';
            $email = isset($d['email']) ? trim((string) $d['email']) : '';
            $nameLen = function_exists('mb_strlen') ? mb_strlen($name, 'UTF-8') : strlen($name);
            if ($nameLen < 2 || $nameLen > 200) {
                respond(400, 'يرجى إدخال اسم الشركة (حرفان على الأقل)');
            }
            if (strlen($phone) < 6 || strlen($phone) > 40 || !preg_match('/^[0-9+\s().-]{6,40}$/u', $phone)) {
                respond(400, 'يرجى إدخال رقم هاتف صالح');
            }
            if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                respond(400, 'يرجى إدخال بريد إلكتروني صالح');
            }
            $pdo = db();
            $ip = (string) ($_SERVER['REMOTE_ADDR'] ?? '');
            $ua = substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 500);
            if ($ip !== '') {
                $lim = $pdo->prepare('SELECT COUNT(*) FROM trial_requests WHERE ip_address = ? AND created_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE)');
                $lim->execute([$ip]);
                if ((int) $lim->fetchColumn() >= 8) {
                    respond(429, 'طلبات كثيرة من نفس الشبكة. حاول بعد قليل.');
                }
            }
            $pdo->prepare('INSERT INTO trial_requests (company_name, phone, email, ip_address, user_agent) VALUES (?,?,?,?,?)')
                ->execute([$name, $phone, $email, $ip !== '' ? $ip : null, $ua !== '' ? $ua : null]);
            respond(201, ['id' => (int) $pdo->lastInsertId()], 'تم استلام طلبك. سيتواصل فريقنا معك قريباً.');
        } catch (Exception $e) {
            respond(500, 'تعذّر حفظ الطلب. حاول مرة أخرى.');
        }

    case 'POST:auth/admin-login':
        $d = input();
        $username = trim((string)($d['username'] ?? ''));
        $password = (string)($d['password'] ?? '');
        $stmt = db()->prepare("SELECT * FROM companies WHERE admin_username = ?");
        $stmt->execute([$username]);
        $company = $stmt->fetch();
        $adminType = 'general';
        $subAdmin = null;

        if (!$company || !password_verify($password, (string)$company['admin_password'])) {
            $st2 = db()->prepare("SELECT s.*, c.name AS company_name, c.language, c.notification_email, c.timezone, c.company_logo, c.subscription_status, c.account_close_at, c.contact_person_name, c.contact_person_phone, c.mobile_app_url
                                  FROM company_sub_admins s
                                  JOIN companies c ON c.company_id=s.company_id
                                  WHERE s.username=? LIMIT 1");
            $st2->execute([$username]);
            $subAdmin = $st2->fetch();
            if (!$subAdmin || !password_verify($password, (string)$subAdmin['password_hash'])) {
                respond(401, 'Invalid credentials');
            }
            if ((int)($subAdmin['is_active'] ?? 0) !== 1) respond(403, 'Account disabled');
            $company = $subAdmin;
            $adminType = 'sub';
        }

        if (($company['subscription_status'] ?? '') === 'suspended') respond(403, 'Account suspended');
        $closeAt = $company['account_close_at'] ?? null;
        if ($closeAt && $closeAt !== '0000-00-00' && (string) $closeAt !== '' && date('Y-m-d') > (string) $closeAt) {
            respond(403, 'انتهت فترة الاشتراك. تواصل مع الدعم.');
        }
        $timezone = $company['timezone'] ?? 'Africa/Cairo';
        $tokenPayload = [
            'role' => 'admin',
            'company_id' => $company['company_id'],
            'company_name' => $company['company_name'] ?? $company['name'],
            'timezone' => $timezone,
            'admin_type' => $adminType,
            'username' => $username,
        ];
        if ($adminType === 'sub' && !empty($subAdmin['id'])) {
            $tokenPayload['admin_id'] = (int)$subAdmin['id'];
        }
        $token = generateToken($tokenPayload);
        $permissionMap = ($adminType === 'sub' && !empty($subAdmin['id'])) ? fetchSubAdminPermissionMap((int)$subAdmin['id']) : defaultAdminPermissionMap();
        if ($adminType === 'general') {
            foreach ($permissionMap as $mk => $v) {
                $permissionMap[$mk] = ['read' => 1, 'create' => 1, 'update' => 1, 'delete' => 1];
            }
        }
        auditLog('admin', $company['company_id'], 'LOGIN', ['admin_type' => $adminType], $company['company_id']);
        respond(200, [
            'token' => $token,
            'role' => 'admin',
            'admin_type' => $adminType,
            'admin_id' => $adminType === 'sub' ? (int)($subAdmin['id'] ?? 0) : null,
            'permissions' => $permissionMap,
            'company_name' => $company['company_name'] ?? $company['name'],
            'company_id' => $company['company_id'],
            'language' => $company['language'] ?? 'ar',
            'notification_email' => $company['notification_email'] ?? '',
            'timezone' => $timezone,
            'company_logo' => $company['company_logo'] ?? '',
            'subscription_status' => $company['subscription_status'] ?? 'trial',
            'account_close_at' => $company['account_close_at'] ?? null,
            'contact_person_name' => $company['contact_person_name'] ?? null,
            'contact_person_phone' => $company['contact_person_phone'] ?? null,
            'mobile_app_url' => $company['mobile_app_url'] ?? '',
            'username' => $username,
            'display_name' => ($adminType === 'sub') ? ($subAdmin['display_name'] ?? $username) : ($company['name'] ?? 'Admin'),
        ]);

    case 'GET:auth/admin-session':
        $auth = requireRole('admin');
        $cid = (string)($auth['company_id'] ?? '');
        if ($cid === '') respond(401, 'Invalid admin session');
        $st = db()->prepare("SELECT name, language, notification_email, timezone, company_logo, subscription_status, account_close_at, contact_person_name, contact_person_phone, mobile_app_url FROM companies WHERE company_id=? LIMIT 1");
        $st->execute([$cid]);
        $company = $st->fetch();
        if (!$company) respond(404, 'Company not found');
        $adminType = (string)($auth['admin_type'] ?? 'general');
        $adminId = isset($auth['admin_id']) ? (int)$auth['admin_id'] : null;
        $displayName = (string)($auth['username'] ?? 'Admin');
        $usernameOut = (string)($auth['username'] ?? '');
        $permissions = defaultAdminPermissionMap();
        $isActive = 1;
        if ($adminType === 'sub') {
            if (!$adminId || $adminId <= 0) respond(401, 'Invalid sub-admin session');
            $stSub = db()->prepare("SELECT id, username, display_name, is_active FROM company_sub_admins WHERE id=? AND company_id=? LIMIT 1");
            $stSub->execute([$adminId, $cid]);
            $sub = $stSub->fetch();
            if (!$sub) respond(401, 'Sub-admin account not found');
            $isActive = (int)($sub['is_active'] ?? 0);
            if ($isActive !== 1) respond(403, 'Sub-admin account disabled');
            $usernameOut = (string)($sub['username'] ?? $usernameOut);
            $displayName = (string)($sub['display_name'] ?? $usernameOut);
            $permissions = fetchSubAdminPermissionMap((int)$sub['id']);
        } else {
            foreach ($permissions as $mk => $v) {
                $permissions[$mk] = ['read' => 1, 'create' => 1, 'update' => 1, 'delete' => 1];
            }
            $displayName = (string)($company['name'] ?? $displayName);
        }
        respond(200, [
            'role' => 'admin',
            'admin_type' => $adminType,
            'admin_id' => $adminId,
            'is_active' => $isActive,
            'permissions' => $permissions,
            'company_name' => $company['name'] ?? '',
            'company_id' => $cid,
            'language' => $company['language'] ?? 'ar',
            'notification_email' => $company['notification_email'] ?? '',
            'timezone' => $company['timezone'] ?? 'Africa/Cairo',
            'company_logo' => $company['company_logo'] ?? '',
            'subscription_status' => $company['subscription_status'] ?? 'trial',
            'account_close_at' => $company['account_close_at'] ?? null,
            'contact_person_name' => $company['contact_person_name'] ?? null,
            'contact_person_phone' => $company['contact_person_phone'] ?? null,
            'mobile_app_url' => $company['mobile_app_url'] ?? '',
            'username' => $usernameOut,
            'display_name' => $displayName
        ]);

    case 'POST:auth/employee-login':
        $d = input();
        $deviceId = $_SERVER['HTTP_X_DEVICE_ID'] ?? $d['device_id'] ?? null;
        if (!$deviceId) respond(400, 'Device ID required');
        $stmt = db()->prepare("SELECT e.*, d.department_name, c.subscription_status, c.account_close_at, c.timezone, c.company_logo FROM employees e JOIN companies c ON e.company_id=c.company_id LEFT JOIN departments d ON e.department_id=d.id WHERE e.username=? AND e.is_active=1");
        $stmt->execute([$d['username'] ?? '']);
        $emp = $stmt->fetch();
        if (!$emp || !password_verify($d['password'] ?? '', $emp['password'])) respond(401, 'Invalid credentials');
        if ($emp['subscription_status'] === 'suspended') respond(403, 'Company account suspended');
        $empClose = $emp['account_close_at'] ?? null;
        if ($empClose && $empClose !== '0000-00-00' && (string) $empClose !== '' && date('Y-m-d') > (string) $empClose) {
            respond(403, 'Company account closed');
        }

        // Smart device binding check (fingerprint-aware)
        $deviceCheck = smartDeviceCheck($emp, $deviceId, 'login');
        if (!$deviceCheck['ok']) {
            respond(403, $deviceCheck['message']);
        }
        // Use the (possibly auto-updated) device_id
        $finalDeviceId = $deviceCheck['device_id'] ?? $deviceId;

        // Generate persistent remember token
        $rememberToken = bin2hex(random_bytes(32));
        db()->prepare("UPDATE employees SET remember_token=? WHERE id=?")->execute([$rememberToken, $emp['id']]);

        $timezone = $emp['timezone'] ?? 'Africa/Cairo';
        $token = generateToken(['role'=>'employee','id'=>$emp['id'],'company_id'=>$emp['company_id'],'name'=>$emp['name'],'timezone'=>$timezone]);
        auditLog('employee', $emp['id'], 'LOGIN', ['device_id'=>substr($finalDeviceId,0,20)], $emp['company_id']);
        respond(200, [
            'token'         => $token,
            'remember_token'=> $rememberToken,
            'role'          => 'employee',
            'name'          => $emp['name'],
            'photo_url'     => resolveEmployeePhotoForLogin($emp),
            'employee_code' => $emp['employee_code'],
            'work_nature'   => $emp['work_nature'],
            'language'      => $emp['language'] ?? 'ar',
            'timezone'      => $timezone,
            'company_logo'  => $emp['company_logo'] ?? '',
            'device_id'     => $finalDeviceId,
            'phone'         => $emp['phone'] ?? '',
            'email'         => $emp['email'] ?? '',
            'department'    => $emp['department_name'] ?? '',
            'must_change_password' => (int)($emp['must_change_password'] ?? 0)
        ]);

    case 'POST:auth/auto-verify':
        $d = input();
        $deviceId = $_SERVER['HTTP_X_DEVICE_ID'] ?? $d['device_id'] ?? null;
        $rememberToken = $d['remember_token'] ?? null;

        if (!$deviceId || !$rememberToken) respond(400, 'Device ID and Remember Token required');

        // Find employee by remember_token
        $stmt = db()->prepare("SELECT e.*, d.department_name, c.subscription_status, c.account_close_at, c.timezone, c.company_logo FROM employees e JOIN companies c ON e.company_id=c.company_id LEFT JOIN departments d ON e.department_id=d.id WHERE e.remember_token=? AND e.is_active=1");
        $stmt->execute([$rememberToken]);
        $emp = $stmt->fetch();

        if (!$emp) respond(401, 'Invalid session');
        if ($emp['subscription_status'] === 'suspended') respond(403, 'Company account suspended');
        $acClose = $emp['account_close_at'] ?? null;
        if ($acClose && $acClose !== '0000-00-00' && (string) $acClose !== '' && date('Y-m-d') > (string) $acClose) {
            respond(403, 'Company account closed');
        }

        // Verify device fingerprint
        $deviceCheck = smartDeviceCheck($emp, $deviceId, 'auto_login');
        if (!$deviceCheck['ok']) {
            respond(403, $deviceCheck['message']);
        }
        $finalDeviceId = $deviceCheck['device_id'] ?? $deviceId;

        $timezone = $emp['timezone'] ?? 'Africa/Cairo';
        $token = generateToken(['role'=>'employee','id'=>$emp['id'],'company_id'=>$emp['company_id'],'name'=>$emp['name'],'timezone'=>$timezone]);
        
        respond(200, [
            'token'         => $token,
            'name'          => $emp['name'],
            'photo_url'     => resolveEmployeePhotoForLogin($emp),
            'work_nature'   => $emp['work_nature'],
            'language'      => $emp['language'] ?? 'ar',
            'timezone'      => $timezone,
            'company_logo'  => $emp['company_logo'] ?? '',
            'device_id'     => $finalDeviceId,
            'phone'         => $emp['phone'] ?? '',
            'email'         => $emp['email'] ?? '',
            'department'    => $emp['department_name'] ?? '',
            'must_change_password' => (int)($emp['must_change_password'] ?? 0)
        ]);

// ═══════════════════════════════════════════════════════════════════════════════
// SUPER ADMIN – Companies
// ═══════════════════════════════════════════════════════════════════════════════
    case 'GET:companies':
        requireRole('super_admin');
        $rows = db()->query("SELECT c.*, (SELECT COUNT(*) FROM employees e WHERE e.company_id=c.company_id) AS employee_count FROM companies c ORDER BY c.created_at DESC")->fetchAll();
        respond(200, $rows);

    case 'POST:companies':
        requireRole('super_admin');
        $d = input();
        foreach (['name','admin_username','admin_password'] as $f) {
            if (empty($d[$f])) respond(400, "Field '$f' is required");
        }
        $hash = password_hash($d['admin_password'], PASSWORD_BCRYPT, ['cost'=>12]);
        $cid  = generateUUID();
        $closeRaw = isset($d['account_close_at']) ? trim((string) $d['account_close_at']) : '';
        $accountClose = ($closeRaw !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $closeRaw)) ? $closeRaw : null;
        $contactName = isset($d['contact_person_name']) ? trim((string) $d['contact_person_name']) : '';
        $contactPhone = isset($d['contact_person_phone']) ? trim((string) $d['contact_person_phone']) : '';
        $contactName = $contactName !== '' ? substr($contactName, 0, 255) : null;
        $contactPhone = $contactPhone !== '' ? substr($contactPhone, 0, 40) : null;
        $notifRaw = isset($d['notification_email']) ? trim((string) $d['notification_email']) : '';
        $notifEmail = ($notifRaw !== '' && filter_var($notifRaw, FILTER_VALIDATE_EMAIL)) ? substr($notifRaw, 0, 255) : null;
        db()->prepare('INSERT INTO companies (company_id,name,admin_username,admin_password,subscription_status,max_employees,account_close_at,contact_person_name,contact_person_phone,notification_email) VALUES (?,?,?,?,?,?,?,?,?,?)')
            ->execute([$cid, $d['name'], $d['admin_username'], $hash, $d['subscription_status'] ?? 'trial', (int) ($d['max_employees'] ?? 50), $accountClose, $contactName, $contactPhone, $notifEmail]);
        respond(201, ['company_id'=>$cid], 'Company created');

    case 'PUT:companies':
        requireRole('super_admin');
        $cid = $parts[1] ?? null;
        if (!$cid) respond(400, 'Company ID required');
        $d = input();
        $fields = [];
        $vals = [];
        if (!empty($d['name'])) {
            $fields[] = 'name=?';
            $vals[] = $d['name'];
        }
        if (!empty($d['admin_password'])) {
            $fields[] = 'admin_password=?';
            $vals[] = password_hash($d['admin_password'], PASSWORD_BCRYPT, ['cost' => 12]);
        }
        if (!empty($d['admin_username'])) {
            $fields[] = 'admin_username=?';
            $vals[] = $d['admin_username'];
        }
        if (!empty($d['subscription_status'])) {
            $fields[] = 'subscription_status=?';
            $vals[] = $d['subscription_status'];
        }
        if (isset($d['max_employees']) && $d['max_employees'] !== '') {
            $fields[] = 'max_employees=?';
            $vals[] = (int) $d['max_employees'];
        }
        if (array_key_exists('account_close_at', $d)) {
            $cr = trim((string) $d['account_close_at']);
            if ($cr === '' || $cr === '0000-00-00') {
                $fields[] = 'account_close_at=NULL';
            } elseif (preg_match('/^\d{4}-\d{2}-\d{2}$/', $cr)) {
                $fields[] = 'account_close_at=?';
                $vals[] = $cr;
            }
        }
        if (array_key_exists('contact_person_name', $d)) {
            $cn = trim((string) $d['contact_person_name']);
            $fields[] = 'contact_person_name=?';
            $vals[] = $cn === '' ? null : substr($cn, 0, 255);
        }
        if (array_key_exists('contact_person_phone', $d)) {
            $cp = trim((string) $d['contact_person_phone']);
            $fields[] = 'contact_person_phone=?';
            $vals[] = $cp === '' ? null : substr($cp, 0, 40);
        }
        if (array_key_exists('notification_email', $d)) {
            $ne = trim((string) $d['notification_email']);
            $fields[] = 'notification_email=?';
            $vals[] = ($ne === '' || !filter_var($ne, FILTER_VALIDATE_EMAIL)) ? null : substr($ne, 0, 255);
        }
        if (empty($fields)) {
            respond(400, 'Nothing to update');
        }
        $vals[] = $cid;
        db()->prepare('UPDATE companies SET ' . implode(',', $fields) . ' WHERE company_id=?')->execute($vals);
        respond(200, [], 'Company updated');

    case 'DELETE:companies':
        requireRole('super_admin');
        $cid = $parts[1] ?? null;
        if (!$cid) respond(400, 'Company ID required');
        db()->prepare("DELETE FROM companies WHERE company_id=?")->execute([$cid]);
        respond(200, [], 'Company deleted');

    case 'GET:super/permission-modules':
        requireRole('super_admin');
        respond(200, ['modules' => adminPermissionModuleDefinitions()]);

    case 'GET:super/company-admins':
        requireRole('super_admin');
        $d = input();
        $cid = trim((string)($d['company_id'] ?? ''));
        if ($cid === '') respond(400, 'company_id is required');
        $st = db()->prepare("SELECT id, company_id, username, display_name, is_active, created_by_super_admin_id, created_at, updated_at FROM company_sub_admins WHERE company_id=? ORDER BY id DESC");
        $st->execute([$cid]);
        respond(200, $st->fetchAll());

    case 'POST:super/company-admins':
        $auth = requireRole('super_admin');
        $d = input();
        $cid = trim((string)($d['company_id'] ?? ''));
        $username = trim((string)($d['username'] ?? ''));
        $password = (string)($d['password'] ?? '');
        $displayName = trim((string)($d['display_name'] ?? ''));
        if ($cid === '' || $username === '' || $password === '') respond(400, 'company_id, username, password are required');
        if (strlen($password) < 8) respond(400, 'Password must be at least 8 chars');
        $st = db()->prepare("SELECT company_id FROM companies WHERE company_id=?");
        $st->execute([$cid]);
        if (!$st->fetch()) respond(404, 'Company not found');
        $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
        $pdo = db();
        $pdo->beginTransaction();
        try {
            $pdo->prepare("INSERT INTO company_sub_admins (company_id, username, password_hash, display_name, is_active, created_by_super_admin_id) VALUES (?,?,?,?,1,?)")
                ->execute([$cid, $username, $hash, $displayName, (int)($auth['id'] ?? 0)]);
            $newId = (int)$pdo->lastInsertId();
            $permMap = is_array($d['permissions'] ?? null) ? $d['permissions'] : [];
            foreach (adminModuleKeys() as $mk) {
                $p = is_array($permMap[$mk] ?? null) ? $permMap[$mk] : [];
                $pdo->prepare("INSERT INTO company_sub_admin_permissions (sub_admin_id, module_key, can_read, can_create, can_update, can_delete) VALUES (?,?,?,?,?,?)")
                    ->execute([
                        $newId, $mk,
                        !empty($p['read']) ? 1 : 0,
                        !empty($p['create']) ? 1 : 0,
                        !empty($p['update']) ? 1 : 0,
                        !empty($p['delete']) ? 1 : 0
                    ]);
            }
            $pdo->commit();
        } catch (Exception $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            throw $e;
        }
        respond(201, ['id' => $newId], 'Sub-admin created');

    case 'PUT:super/company-admins':
        requireRole('super_admin');
        $d = input();
        $id = (int)($d['sub_admin_id'] ?? 0);
        if ($id <= 0) respond(400, 'sub_admin_id is required');
        $fields = [];
        $vals = [];
        if (array_key_exists('display_name', $d)) {
            $fields[] = 'display_name=?';
            $vals[] = trim((string)$d['display_name']);
        }
        if (array_key_exists('username', $d) && trim((string)$d['username']) !== '') {
            $fields[] = 'username=?';
            $vals[] = trim((string)$d['username']);
        }
        if (!$fields) respond(400, 'Nothing to update');
        $vals[] = $id;
        db()->prepare("UPDATE company_sub_admins SET " . implode(',', $fields) . " WHERE id=?")->execute($vals);
        respond(200, [], 'Sub-admin updated');

    case 'PUT:super/company-admins/password':
        requireRole('super_admin');
        $d = input();
        $id = (int)($d['sub_admin_id'] ?? 0);
        $password = (string)($d['password'] ?? '');
        if ($id <= 0 || $password === '') respond(400, 'sub_admin_id and password are required');
        if (strlen($password) < 8) respond(400, 'Password must be at least 8 chars');
        $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
        db()->prepare("UPDATE company_sub_admins SET password_hash=? WHERE id=?")->execute([$hash, $id]);
        respond(200, [], 'Password updated');

    case 'PUT:super/company-admins/status':
        requireRole('super_admin');
        $d = input();
        $id = (int)($d['sub_admin_id'] ?? 0);
        $active = !empty($d['is_active']) ? 1 : 0;
        if ($id <= 0) respond(400, 'sub_admin_id is required');
        db()->prepare("UPDATE company_sub_admins SET is_active=? WHERE id=?")->execute([$active, $id]);
        respond(200, [], 'Status updated');

    case 'GET:super/company-admin-permissions':
        requireRole('super_admin');
        $d = input();
        $id = (int)($d['sub_admin_id'] ?? 0);
        if ($id <= 0) respond(400, 'sub_admin_id is required');
        respond(200, fetchSubAdminPermissionMap($id));

    case 'PUT:super/company-admin-permissions':
        requireRole('super_admin');
        $d = input();
        $id = (int)($d['sub_admin_id'] ?? 0);
        $permMap = is_array($d['permissions'] ?? null) ? $d['permissions'] : null;
        if ($id <= 0 || $permMap === null) respond(400, 'sub_admin_id and permissions are required');
        db()->prepare("DELETE FROM company_sub_admin_permissions WHERE sub_admin_id=?")->execute([$id]);
        foreach (adminModuleKeys() as $mk) {
            $p = is_array($permMap[$mk] ?? null) ? $permMap[$mk] : [];
            db()->prepare("INSERT INTO company_sub_admin_permissions (sub_admin_id, module_key, can_read, can_create, can_update, can_delete) VALUES (?,?,?,?,?,?)")
                ->execute([
                    $id, $mk,
                    !empty($p['read']) ? 1 : 0,
                    !empty($p['create']) ? 1 : 0,
                    !empty($p['update']) ? 1 : 0,
                    !empty($p['delete']) ? 1 : 0
                ]);
        }
        respond(200, [], 'Permissions updated');

// ═══════════════════════════════════════════════════════════════════════════════
// ADMIN – Departments
// ═══════════════════════════════════════════════════════════════════════════════
    case 'GET:departments':
        $auth = requireRole('admin');
        $rows = db()->prepare("SELECT d.id, d.department_name, d.is_active, d.created_at,
            (SELECT COUNT(*) FROM employees e WHERE e.company_id = d.company_id AND e.department_id = d.id AND e.is_active = 1) AS active_employee_count,
            (SELECT COUNT(*) FROM employees e WHERE e.company_id = d.company_id AND e.department_id = d.id AND e.is_active = 0) AS inactive_employee_count
            FROM departments d WHERE d.company_id=? ORDER BY d.is_active DESC, d.department_name");
        $rows->execute([$auth['company_id']]);
        respond(200, $rows->fetchAll());

    case 'POST:departments':
        $auth = requireRole('admin');
        $d = input();
        if (empty($d['department_name'])) respond(400, 'Department name is required');
        $ia = array_key_exists('is_active', $d) ? ((int) (filter_var($d['is_active'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0)) : 1;
        db()->prepare("INSERT INTO departments (company_id, department_name, is_active) VALUES (?, ?, ?)")
            ->execute([$auth['company_id'], $d['department_name'], $ia]);
        respond(201, ['id' => db()->lastInsertId()], 'Department created');

    case 'PUT:departments':
        $auth = requireRole('admin');
        $did = $parts[1] ?? null;
        if (!$did) respond(400, 'Department ID required');
        $d = input();
        $fields = [];
        $vals = [];
        if (!empty($d['department_name'])) {
            $fields[] = 'department_name=?';
            $vals[] = $d['department_name'];
        }
        if (array_key_exists('is_active', $d)) {
            $fields[] = 'is_active=?';
            $vals[] = (int) (filter_var($d['is_active'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0);
        }
        if (empty($fields)) {
            respond(400, 'Nothing to update');
        }
        $check = db()->prepare("SELECT id FROM departments WHERE id=? AND company_id=?");
        $check->execute([$did, $auth['company_id']]);
        if (!$check->fetch()) {
            respond(404, 'Department not found');
        }
        $vals[] = $did;
        db()->prepare('UPDATE departments SET ' . implode(',', $fields) . ' WHERE id=?')->execute($vals);
        respond(200, [], 'Department updated');

    case 'DELETE:departments':
        $auth = requireRole('admin');
        $did = $parts[1] ?? null;
        if (!$did) respond(400, 'Department ID required');
        $check = db()->prepare("SELECT id FROM departments WHERE id=? AND company_id=?");
        $check->execute([$did, $auth['company_id']]);
        if (!$check->fetch()) respond(404, 'Department not found');
        db()->prepare("DELETE FROM departments WHERE id=?")->execute([$did]);
        respond(200, [], 'Department deleted');

// ═══════════════════════════════════════════════════════════════════════════════
// ADMIN – Branches
// ═══════════════════════════════════════════════════════════════════════════════
    case 'GET:branches':
        $auth = requireRole('admin', 'employee');
        $rows = db()->prepare("SELECT * FROM branches WHERE company_id=? ORDER BY name");
        $rows->execute([$auth['company_id']]);
        respond(200, $rows->fetchAll());

    case 'POST:branches':
        $auth = requireRole('admin');
        $d = input();
        foreach (['name','latitude','longitude'] as $f) {
            if (empty($d[$f])) respond(400, "Field '$f' is required");
        }
        $pdo = db();
        $pdo->prepare('INSERT INTO branches (company_id, name, latitude, longitude, radius, photo_url) VALUES (?, ?, ?, ?, ?, NULL)')
            ->execute([$auth['company_id'], $d['name'], $d['latitude'], $d['longitude'], $d['radius'] ?? 100]);
        $newId = (int) $pdo->lastInsertId();
        if (!empty($d['photo_data'])) {
            $saved = saveBranchPhotoBase64((string) $d['photo_data'], 'branch_' . $newId, $auth['company_id']);
            if ($saved) {
                $pdo->prepare('UPDATE branches SET photo_url=? WHERE id=? AND company_id=?')->execute([$saved, $newId, $auth['company_id']]);
            }
        }
        respond(201, ['id' => $newId], 'Branch created');

    case 'PUT:branches':
        $auth = requireRole('admin');
        $bid = $parts[1] ?? null;
        if (!$bid) respond(400, 'Branch ID required');
        $d = input();
        $fields = []; $vals = [];
        foreach (['name','latitude','longitude','radius'] as $f) {
            if (isset($d[$f])) { $fields[] = "$f=?"; $vals[] = $d[$f]; }
        }
        if (!empty($d['photo_data'])) {
            $saved = saveBranchPhotoBase64((string) $d['photo_data'], 'branch_' . (int) $bid, $auth['company_id']);
            if ($saved) {
                $fields[] = 'photo_url=?';
                $vals[] = $saved;
            }
        }
        if (empty($fields)) respond(400, 'Nothing to update');
        $vals[] = $bid; $vals[] = $auth['company_id'];
        db()->prepare("UPDATE branches SET ".implode(',', $fields)." WHERE id=? AND company_id=?")->execute($vals);
        respond(200, [], 'Branch updated');

    case 'DELETE:branches':
        $auth = requireRole('admin');
        $bid = $parts[1] ?? null;
        if (!$bid) respond(400, 'Branch ID required');
        db()->prepare("DELETE FROM branches WHERE id=? AND company_id=?")->execute([$bid, $auth['company_id']]);
        respond(200, [], 'Branch deleted');

// ═══════════════════════════════════════════════════════════════════════════════
// ADMIN – Official holidays (company calendar; final report treats as non-working)
// ═══════════════════════════════════════════════════════════════════════════════
    case 'GET:official-holidays':
        $auth = requireRole('admin', 'employee');
        $y = $input['year'] ?? $_GET['year'] ?? null;
        $pdo = db();
        if ($y !== null && $y !== '' && ctype_digit((string) $y)) {
            $yi = (int) $y;
            if ($yi < 1990 || $yi > 2120) {
                $yi = (int) date('Y');
            }
            $from = sprintf('%04d-01-01', $yi);
            $to = sprintf('%04d-12-31', $yi);
            $st = $pdo->prepare('SELECT id, holiday_date, title, notes, created_at FROM company_official_holidays WHERE company_id=? AND holiday_date BETWEEN ? AND ? ORDER BY holiday_date ASC');
            $st->execute([$auth['company_id'], $from, $to]);
        } else {
            $st = $pdo->prepare('SELECT id, holiday_date, title, notes, created_at FROM company_official_holidays WHERE company_id=? ORDER BY holiday_date DESC LIMIT 500');
            $st->execute([$auth['company_id']]);
        }
        respond(200, $st->fetchAll());

    case 'POST:official-holidays':
        $auth = requireRole('admin');
        $d = input();
        $start = trim((string) ($d['holiday_date'] ?? $d['start_date'] ?? ''));
        $end = trim((string) ($d['end_date'] ?? ''));
        if ($start === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $start)) {
            respond(400, 'holiday_date or start_date (Y-m-d) required');
        }
        if ($end === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $end)) {
            $end = $start;
        }
        $title = trim((string) ($d['title'] ?? ''));
        if ($title === '') {
            respond(400, 'title required');
        }
        if (strlen($title) > 200) {
            respond(400, 'title too long');
        }
        $notes = isset($d['notes']) ? trim((string) $d['notes']) : '';
        $notes = $notes === '' ? null : (strlen($notes) > 500 ? substr($notes, 0, 500) : $notes);
        $dStart = new DateTime($start);
        $dEnd = new DateTime($end);
        if ($dEnd < $dStart) {
            respond(400, 'end_date must be on or after start date');
        }
        $span = $dStart->diff($dEnd)->days + 1;
        if ($span > 366) {
            respond(400, 'Date range too long (max 366 days)');
        }
        $pdo = db();
        $ins = $pdo->prepare('INSERT INTO company_official_holidays (company_id, holiday_date, title, notes) VALUES (?,?,?,?)');
        $inserted = 0;
        $skipped = [];
        $firstId = null;
        for ($cur = clone $dStart; $cur <= $dEnd; $cur->modify('+1 day')) {
            $ds = $cur->format('Y-m-d');
            try {
                $ins->execute([$auth['company_id'], $ds, $title, $notes]);
                $inserted++;
                if ($firstId === null) {
                    $firstId = (int) $pdo->lastInsertId();
                }
            } catch (Exception $e) {
                $skipped[] = $ds;
            }
        }
        if ($inserted === 0) {
            respond(409, 'All dates in the range already have holidays: ' . implode(', ', $skipped));
        }
        auditLog('admin', $auth['company_id'], 'OFFICIAL_HOLIDAYS_RANGE', ['inserted' => $inserted, 'skipped' => $skipped], $auth['company_id']);
        respond(201, ['inserted' => $inserted, 'skipped' => $skipped, 'id' => $firstId], 'Official holidays saved');

    case 'PUT:official-holidays/item':
        $auth = requireRole('admin');
        $hid = $parts[1] ?? null;
        if (!$hid || !ctype_digit((string) $hid)) {
            respond(400, 'Holiday ID required');
        }
        $d = input();
        $fields = [];
        $vals = [];
        if (isset($d['holiday_date'])) {
            $hdt = trim((string) $d['holiday_date']);
            if ($hdt !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $hdt)) {
                $fields[] = 'holiday_date=?';
                $vals[] = $hdt;
            }
        }
        if (isset($d['title'])) {
            $t = trim((string) $d['title']);
            if ($t !== '') {
                $fields[] = 'title=?';
                $vals[] = strlen($t) > 200 ? substr($t, 0, 200) : $t;
            }
        }
        if (array_key_exists('notes', $d)) {
            $n = trim((string) $d['notes']);
            $fields[] = 'notes=?';
            $vals[] = $n === '' ? null : (strlen($n) > 500 ? substr($n, 0, 500) : $n);
        }
        if (empty($fields)) {
            respond(400, 'Nothing to update');
        }
        $vals[] = (int) $hid;
        $vals[] = $auth['company_id'];
        try {
            db()->prepare('UPDATE company_official_holidays SET ' . implode(',', $fields) . ' WHERE id=? AND company_id=?')->execute($vals);
        } catch (Exception $e) {
            respond(409, 'Duplicate date or invalid update');
        }
        respond(200, [], 'Official holiday updated');

    case 'DELETE:official-holidays/item':
        $auth = requireRole('admin');
        $hid = $parts[1] ?? null;
        if (!$hid || !ctype_digit((string) $hid)) {
            respond(400, 'Holiday ID required');
        }
        $st = db()->prepare('DELETE FROM company_official_holidays WHERE id=? AND company_id=?');
        $st->execute([(int) $hid, $auth['company_id']]);
        if ($st->rowCount() === 0) {
            respond(404, 'Not found');
        }
        respond(200, [], 'Deleted');

// ═══════════════════════════════════════════════════════════════════════════════
// ADMIN – Leave Policies
// ═══════════════════════════════════════════════════════════════════════════════
    case 'GET:leave-policies':
        $auth = requireRole('admin');
        ensureLeavePolicyPermissionLimitColumn(db());
        $rows = db()->prepare("SELECT * FROM leave_policies WHERE company_id=? ORDER BY policy_name");
        $rows->execute([$auth['company_id']]);
        respond(200, $rows->fetchAll());

    case 'POST:leave-policies':
        $auth = requireRole('admin');
        ensureLeavePolicyPermissionLimitColumn(db());
        $d = input();
        if (empty($d['policy_name'])) respond(400, 'Policy name is required');
        
        $weekendDays = isset($d['weekend_days']) ? json_encode($d['weekend_days']) : '[]';
        $df = policyDeductionDecimal($d['deduct_full_day'] ?? null, 1.0);
        $dh = policyDeductionDecimal($d['deduct_half_day'] ?? null, 0.5);
        $dq = policyDeductionDecimal($d['deduct_quarter_day'] ?? null, 0.25);
        $sdEn = array_key_exists('salary_deduct_enabled', $d) ? (((int) $d['salary_deduct_enabled'] === 1 || $d['salary_deduct_enabled'] === true || $d['salary_deduct_enabled'] === '1') ? 1 : 0) : 1;
        $sdAb = array_key_exists('salary_deduct_allow_with_balance', $d) ? (((int) $d['salary_deduct_allow_with_balance'] === 1 || $d['salary_deduct_allow_with_balance'] === true || $d['salary_deduct_allow_with_balance'] === '1') ? 1 : 0) : 0;

        $gl = isset($d['grace_late_minutes']) ? max(0, (int) $d['grace_late_minutes']) : 5;
        $ge = isset($d['grace_early_leave_minutes']) ? max(0, (int) $d['grace_early_leave_minutes']) : 5;
        $adlt = strtolower(trim((string) ($d['auto_deduct_leave_type'] ?? 'annual')));
        if (!in_array($adlt, ['annual', 'casual', 'sick'], true)) {
            $adlt = 'annual';
        }
        $adMax = isset($d['auto_deduct_max_per_day']) ? min(99.99, max(0.01, (float) $d['auto_deduct_max_per_day'])) : 1.0;
        $adComb = array_key_exists('auto_deduct_combine_late_early', $d) ? (((int) $d['auto_deduct_combine_late_early'] === 1 || $d['auto_deduct_combine_late_early'] === true || $d['auto_deduct_combine_late_early'] === '1') ? 1 : 0) : 0;
        $adIgn = isset($d['auto_deduct_ignore_codes']) ? substr(trim((string) $d['auto_deduct_ignore_codes']), 0, 400) : null;
        $permLimit = array_key_exists('permission_monthly_limit', $d) && $d['permission_monthly_limit'] !== '' && $d['permission_monthly_limit'] !== null
            ? max(0, (int) $d['permission_monthly_limit'])
            : null;
        $lq = array_key_exists('late_minutes_quarter', $d) && $d['late_minutes_quarter'] !== '' && $d['late_minutes_quarter'] !== null ? max(0, (int) $d['late_minutes_quarter']) : null;
        $lh = array_key_exists('late_minutes_half', $d) && $d['late_minutes_half'] !== '' && $d['late_minutes_half'] !== null ? max(0, (int) $d['late_minutes_half']) : null;
        $lf = array_key_exists('late_minutes_full', $d) && $d['late_minutes_full'] !== '' && $d['late_minutes_full'] !== null ? max(0, (int) $d['late_minutes_full']) : null;
        $eq = array_key_exists('early_leave_minutes_quarter', $d) && $d['early_leave_minutes_quarter'] !== '' && $d['early_leave_minutes_quarter'] !== null ? max(0, (int) $d['early_leave_minutes_quarter']) : null;
        $eh = array_key_exists('early_leave_minutes_half', $d) && $d['early_leave_minutes_half'] !== '' && $d['early_leave_minutes_half'] !== null ? max(0, (int) $d['early_leave_minutes_half']) : null;
        $ef = array_key_exists('early_leave_minutes_full', $d) && $d['early_leave_minutes_full'] !== '' && $d['early_leave_minutes_full'] !== null ? max(0, (int) $d['early_leave_minutes_full']) : null;

        db()->prepare('INSERT INTO leave_policies (company_id, policy_name, weekend_days, annual_leave_days, casual_leave_days, sick_leave_days, start_time, end_time, deduct_full_day, deduct_half_day, deduct_quarter_day, salary_deduct_enabled, salary_deduct_allow_with_balance, grace_late_minutes, grace_early_leave_minutes, late_minutes_quarter, late_minutes_half, late_minutes_full, early_leave_minutes_quarter, early_leave_minutes_half, early_leave_minutes_full, auto_deduct_leave_type, auto_deduct_max_per_day, auto_deduct_combine_late_early, auto_deduct_ignore_codes, permission_monthly_limit) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
            ->execute([
                $auth['company_id'],
                $d['policy_name'],
                $weekendDays,
                $d['annual_leave_days'] ?? 21,
                $d['casual_leave_days'] ?? 7,
                $d['sick_leave_days'] ?? 15,
                $d['start_time'] ?? '09:00:00',
                $d['end_time'] ?? '17:00:00',
                $df,
                $dh,
                $dq,
                $sdEn,
                $sdAb,
                $gl,
                $ge,
                $lq,
                $lh,
                $lf,
                $eq,
                $eh,
                $ef,
                $adlt,
                $adMax,
                $adComb,
                $adIgn !== '' ? $adIgn : null,
                $permLimit,
            ]);
        respond(201, ['id' => db()->lastInsertId()], 'Policy created');

    case 'PUT:leave-policies':
        $auth = requireRole('admin');
        ensureLeavePolicyPermissionLimitColumn(db());
        $pid = $parts[1] ?? null;
        if (!$pid) respond(400, 'Policy ID required');
        $d = input();
        
        $fields = []; $vals = [];
        if (isset($d['policy_name'])) { $fields[] = "policy_name=?"; $vals[] = $d['policy_name']; }
        if (isset($d['weekend_days'])) { $fields[] = "weekend_days=?"; $vals[] = json_encode($d['weekend_days']); }
        if (isset($d['annual_leave_days'])) { $fields[] = "annual_leave_days=?"; $vals[] = $d['annual_leave_days']; }
        if (isset($d['casual_leave_days'])) { $fields[] = "casual_leave_days=?"; $vals[] = $d['casual_leave_days']; }
        if (isset($d['sick_leave_days'])) { $fields[] = "sick_leave_days=?"; $vals[] = $d['sick_leave_days']; }
        if (isset($d['start_time'])) { $fields[] = "start_time=?"; $vals[] = $d['start_time']; }
        if (isset($d['end_time'])) { $fields[] = "end_time=?"; $vals[] = $d['end_time']; }
        if (array_key_exists('deduct_full_day', $d)) { $fields[] = "deduct_full_day=?"; $vals[] = policyDeductionDecimal($d['deduct_full_day'], 1.0); }
        if (array_key_exists('deduct_half_day', $d)) { $fields[] = "deduct_half_day=?"; $vals[] = policyDeductionDecimal($d['deduct_half_day'], 0.5); }
        if (array_key_exists('deduct_quarter_day', $d)) { $fields[] = "deduct_quarter_day=?"; $vals[] = policyDeductionDecimal($d['deduct_quarter_day'], 0.25); }
        if (array_key_exists('salary_deduct_enabled', $d)) {
            $fields[] = 'salary_deduct_enabled=?';
            $vals[] = ((int) $d['salary_deduct_enabled'] === 1 || $d['salary_deduct_enabled'] === true || $d['salary_deduct_enabled'] === '1') ? 1 : 0;
        }
        if (array_key_exists('salary_deduct_allow_with_balance', $d)) {
            $fields[] = 'salary_deduct_allow_with_balance=?';
            $vals[] = ((int) $d['salary_deduct_allow_with_balance'] === 1 || $d['salary_deduct_allow_with_balance'] === true || $d['salary_deduct_allow_with_balance'] === '1') ? 1 : 0;
        }
        if (array_key_exists('grace_late_minutes', $d)) {
            $fields[] = 'grace_late_minutes=?';
            $vals[] = max(0, (int) $d['grace_late_minutes']);
        }
        if (array_key_exists('grace_early_leave_minutes', $d)) {
            $fields[] = 'grace_early_leave_minutes=?';
            $vals[] = max(0, (int) $d['grace_early_leave_minutes']);
        }
        if (array_key_exists('late_minutes_quarter', $d)) {
            $fields[] = 'late_minutes_quarter=?';
            $vals[] = ($d['late_minutes_quarter'] === '' || $d['late_minutes_quarter'] === null) ? null : max(0, (int) $d['late_minutes_quarter']);
        }
        if (array_key_exists('late_minutes_half', $d)) {
            $fields[] = 'late_minutes_half=?';
            $vals[] = ($d['late_minutes_half'] === '' || $d['late_minutes_half'] === null) ? null : max(0, (int) $d['late_minutes_half']);
        }
        if (array_key_exists('late_minutes_full', $d)) {
            $fields[] = 'late_minutes_full=?';
            $vals[] = ($d['late_minutes_full'] === '' || $d['late_minutes_full'] === null) ? null : max(0, (int) $d['late_minutes_full']);
        }
        if (array_key_exists('early_leave_minutes_quarter', $d)) {
            $fields[] = 'early_leave_minutes_quarter=?';
            $vals[] = ($d['early_leave_minutes_quarter'] === '' || $d['early_leave_minutes_quarter'] === null) ? null : max(0, (int) $d['early_leave_minutes_quarter']);
        }
        if (array_key_exists('early_leave_minutes_half', $d)) {
            $fields[] = 'early_leave_minutes_half=?';
            $vals[] = ($d['early_leave_minutes_half'] === '' || $d['early_leave_minutes_half'] === null) ? null : max(0, (int) $d['early_leave_minutes_half']);
        }
        if (array_key_exists('early_leave_minutes_full', $d)) {
            $fields[] = 'early_leave_minutes_full=?';
            $vals[] = ($d['early_leave_minutes_full'] === '' || $d['early_leave_minutes_full'] === null) ? null : max(0, (int) $d['early_leave_minutes_full']);
        }
        if (array_key_exists('auto_deduct_leave_type', $d)) {
            $adlt = strtolower(trim((string) $d['auto_deduct_leave_type']));
            $fields[] = 'auto_deduct_leave_type=?';
            $vals[] = in_array($adlt, ['annual', 'casual', 'sick'], true) ? $adlt : 'annual';
        }
        if (array_key_exists('auto_deduct_max_per_day', $d)) {
            $fields[] = 'auto_deduct_max_per_day=?';
            $vals[] = min(99.99, max(0.01, (float) $d['auto_deduct_max_per_day']));
        }
        if (array_key_exists('auto_deduct_combine_late_early', $d)) {
            $fields[] = 'auto_deduct_combine_late_early=?';
            $vals[] = ((int) $d['auto_deduct_combine_late_early'] === 1 || $d['auto_deduct_combine_late_early'] === true || $d['auto_deduct_combine_late_early'] === '1') ? 1 : 0;
        }
        if (array_key_exists('auto_deduct_ignore_codes', $d)) {
            $fields[] = 'auto_deduct_ignore_codes=?';
            $ign = substr(trim((string) $d['auto_deduct_ignore_codes']), 0, 400);
            $vals[] = $ign !== '' ? $ign : null;
        }
        if (array_key_exists('permission_monthly_limit', $d)) {
            $fields[] = 'permission_monthly_limit=?';
            $vals[] = ($d['permission_monthly_limit'] === '' || $d['permission_monthly_limit'] === null)
                ? null
                : max(0, (int) $d['permission_monthly_limit']);
        }

        if (empty($fields)) respond(400, 'Nothing to update');
        $vals[] = $pid; $vals[] = $auth['company_id'];
        db()->prepare("UPDATE leave_policies SET ".implode(',', $fields)." WHERE id=? AND company_id=?")->execute($vals);
        respond(200, [], 'Policy updated');

    case 'DELETE:leave-policies':
        $auth = requireRole('admin');
        $pid = $parts[1] ?? null;
        if (!$pid) respond(400, 'Policy ID required');
        db()->prepare("DELETE FROM leave_policies WHERE id=? AND company_id=?")->execute([$pid, $auth['company_id']]);
        respond(200, [], 'Policy deleted');

// ═══════════════════════════════════════════════════════════════════════════════
// ADMIN – Employees
// ═══════════════════════════════════════════════════════════════════════════════
    case 'GET:employees':
        $auth = requireRole('admin');
        $rows = db()->prepare("SELECT
                e.*,
                d.department_name,
                b.name as branch_name,
                lp.policy_name,
                (
                    SELECT GROUP_CONCAT(t.employee_id ORDER BY t.employee_id SEPARATOR ',')
                    FROM employee_team_members t
                    WHERE t.company_id = e.company_id
                      AND t.supervisor_id = e.id
                ) AS team_member_ids_csv
            FROM employees e
            LEFT JOIN departments d ON e.department_id = d.id
            LEFT JOIN branches b ON e.branch_id = b.id
            LEFT JOIN leave_policies lp ON e.policy_id = lp.id
            WHERE e.company_id=?
            ORDER BY e.name");
        $rows->execute([$auth['company_id']]);
        $list = $rows->fetchAll();
        foreach ($list as &$r) {
            $csv = $r['team_member_ids_csv'] ?? null;
            $ids = [];
            if ($csv !== null && $csv !== '') {
                $parts = explode(',', (string)$csv);
                foreach ($parts as $p) {
                    $iv = (int)trim((string)$p);
                    if ($iv > 0) $ids[] = $iv;
                }
            }
            $r['team_member_ids'] = array_values(array_unique($ids));
            unset($r['team_member_ids_csv']);
        }
        respond(200, $list);

    case 'POST:employees':
        $auth = requireRole('admin');
        $d = input();
        $name = trim((string) ($d['name'] ?? ''));
        $username = trim((string) ($d['username'] ?? ''));
        $password = (string) ($d['password'] ?? '');
        if ($name === '') {
            respond(400, 'Full name is required / الاسم الكامل مطلوب');
        }
        if ($username === '') {
            respond(400, 'Username is required / اسم المستخدم مطلوب');
        }
        if ($password === '') {
            respond(400, 'Password is required for new employees / كلمة المرور مطلوبة عند إضافة موظف');
        }
        // Check quota
        $count = db()->prepare("SELECT COUNT(*) FROM employees WHERE company_id=?");
        $count->execute([$auth['company_id']]);
        $quota = db()->prepare("SELECT max_employees FROM companies WHERE company_id=?");
        $quota->execute([$auth['company_id']]);
        $max = $quota->fetchColumn();
        if ($count->fetchColumn() >= $max) respond(403, 'Employee quota reached');

        $code = strtoupper(substr(md5(uniqid()), 0, 8));
        $hash = password_hash($password, PASSWORD_BCRYPT, ['cost'=>12]);
        if ($hash === false) {
            respond(500, 'Could not create password hash / تعذّر إنشاء كلمة المرور');
        }

        $fkOpt = static function ($v): ?int {
            if ($v === null || $v === '' || $v === false) {
                return null;
            }
            if (!is_numeric($v)) {
                return null;
            }
            $i = (int) $v;

            return $i > 0 ? $i : null;
        };
        $deptId = $fkOpt($d['department_id'] ?? null);
        $branchId = $fkOpt($d['branch_id'] ?? null);
        $policyId = $fkOpt($d['policy_id'] ?? null);
        $workNature = $d['work_nature'] ?? 'internal';
        if (!in_array($workNature, ['internal', 'external'], true)) {
            $workNature = 'internal';
        }
        $salRaw = $d['salary'] ?? null;
        $salaryVal = ($salRaw === null || $salRaw === '') ? 0.0 : (is_numeric($salRaw) ? (float) $salRaw : 0.0);
        $hireRaw = trim((string) ($d['hire_date'] ?? ''));
        $hireDate = ($hireRaw !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $hireRaw)) ? $hireRaw : null;
        $birthRaw = trim((string) ($d['birth_date'] ?? ''));
        $birthDate = ($birthRaw !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthRaw)) ? $birthRaw : null;

        $photoPath = null;
        try {
            if (!empty($d['photo_data'])) {
                $photoPath = saveEmployeePhoto($d['photo_data'], $auth['company_id']);
            } elseif (!empty($d['photo_url'])) {
                $photoPath = $d['photo_url'];
            }
        } catch (Throwable $e) {
            error_log('saveEmployeePhoto: ' . $e->getMessage());
            $photoPath = null;
        }

        try {
            db()->prepare("INSERT INTO employees (company_id,department_id,branch_id,policy_id,work_nature,employee_code,name,national_id,phone,email,job_title,salary,hire_date,birth_date,address,username,password,photo_url,must_change_password) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)")
                ->execute([$auth['company_id'], $deptId, $branchId, $policyId, $workNature, $code, $name, $d['national_id'] ?? null, $d['phone'] ?? null, $d['email'] ?? null, $d['job_title'] ?? null, $salaryVal, $hireDate, $birthDate, $d['address'] ?? null, $username, $hash, $photoPath]);
        } catch (PDOException $e) {
            $msg = $e->getMessage();
            $pdoCode = $e->getCode();
            $isIntegrity = ($pdoCode === '23000' || $pdoCode === 23000 || (is_string($pdoCode) && strncmp((string) $pdoCode, '23', 2) === 0));
            if ($isIntegrity || strpos($msg, '1062') !== false || stripos($msg, 'Duplicate') !== false) {
                respond(409, 'Username already exists in your company / اسم المستخدم مستخدم بالفعل لموظف آخر');
            }
            if (strpos($msg, '1452') !== false || stripos($msg, 'foreign key') !== false || stripos($msg, 'FOREIGN KEY') !== false) {
                respond(400, 'Invalid department, branch, or leave policy — تحقق من القسم أو الفرع أو سياسة الإجازات');
            }
            if (stripos($msg, 'Incorrect decimal') !== false || stripos($msg, '1366') !== false || stripos($msg, 'Out of range') !== false) {
                respond(400, 'Invalid salary or numeric field / قيمة راتب أو رقم غير صالحة');
            }
            error_log('POST employees INSERT: ' . $msg);
            throw $e;
        }
        auditLog('admin', $auth['company_id'], 'CREATE_EMPLOYEE', ['name'=>$name], $auth['company_id']);
        $newEmpId = (int) db()->lastInsertId();
        // Optional: assign supervisor team members during create flow.
        if (array_key_exists('team_member_ids', $d)) {
            $rawTeam = is_array($d['team_member_ids']) ? $d['team_member_ids'] : explode(',', (string)$d['team_member_ids']);
            $teamRequested = array_values(array_unique(array_filter(array_map('intval', $rawTeam), function ($x) use ($newEmpId) {
                return $x > 0 && $x !== $newEmpId;
            })));
            if (count($teamRequested)) {
                $ph = implode(',', array_fill(0, count($teamRequested), '?'));
                $stTeam = db()->prepare("SELECT id FROM employees WHERE company_id=? AND id IN ($ph)");
                $stTeam->execute(array_merge([$auth['company_id']], $teamRequested));
                $teamValid = array_map('intval', array_column($stTeam->fetchAll(), 'id'));
                if (count($teamValid)) {
                    $insTeam = db()->prepare("INSERT IGNORE INTO employee_team_members (company_id, supervisor_id, employee_id) VALUES (?,?,?)");
                    foreach ($teamValid as $mid) {
                        $insTeam->execute([$auth['company_id'], $newEmpId, (int)$mid]);
                    }
                }
            }
        }
        $empEmail = isset($d['email']) ? trim((string) $d['email']) : '';
        if ($empEmail !== '' && filter_var($empEmail, FILTER_VALIDATE_EMAIL)) {
            try {
                sendEmployeeCredentialsEmail($auth['company_id'], $empEmail, $name, $username, $password, $code, 'created');
            } catch (Throwable $e) {
                error_log('sendEmployeeCredentialsEmail: ' . $e->getMessage());
            }
        }
        respond(201, ['employee_code'=>$code, 'employee_id' => $newEmpId], 'Employee created');

    case 'PUT:employees':
        $auth = requireRole('admin');
        $eid = $parts[1] ?? null;
        if (!$eid) respond(400, 'Employee ID required');
        $d = input();
        // Verify belongs to company
        $check = db()->prepare("SELECT id FROM employees WHERE id=? AND company_id=?");
        $check->execute([$eid, $auth['company_id']]);
        if (!$check->fetch()) respond(404, 'Employee not found');

        $clearedProfilePhoto = false;
        if (!empty($d['clear_photo']) && ($d['clear_photo'] === true || $d['clear_photo'] === 1 || $d['clear_photo'] === '1')) {
            $stPh = db()->prepare('SELECT photo_url FROM employees WHERE id=? AND company_id=?');
            $stPh->execute([(int) $eid, $auth['company_id']]);
            $oldPh = $stPh->fetchColumn();
            if ($oldPh !== null && $oldPh !== '' && is_string($oldPh)) {
                $rel = str_replace(["\0", '\\'], '', $oldPh);
                if (strpos($rel, '..') === false && preg_match('#^uploads/(employees|selfies)/#', $rel)) {
                    $full = __DIR__ . '/' . str_replace('\\', '/', $rel);
                    if (is_file($full)) {
                        @unlink($full);
                    }
                }
            }
            db()->prepare('UPDATE employees SET photo_url=NULL WHERE id=? AND company_id=?')->execute([(int) $eid, $auth['company_id']]);
            auditLog('admin', $auth['company_id'], 'EMPLOYEE_PHOTO_CLEAR', ['employee_id' => (int) $eid], $auth['company_id']);
            $clearedProfilePhoto = true;
        }

        $teamReplaceDirty = array_key_exists('team_member_ids', $d);
        $teamDeltaProvided = array_key_exists('team_member_add_ids', $d) || array_key_exists('team_member_remove_ids', $d);
        $teamDirty = $teamReplaceDirty || $teamDeltaProvided;
        $teamMode = $teamDeltaProvided ? 'delta' : ($teamReplaceDirty ? 'replace' : 'none');

        $eidInt = (int)$eid;
        $teamParseIds = function ($raw) use ($eidInt): array {
            $rawList = is_array($raw) ? $raw : explode(',', (string)$raw);
            return array_values(array_unique(array_filter(array_map('intval', $rawList), function ($x) use ($eidInt) {
                return $x > 0 && $x !== $eidInt;
            })));
        };
        $teamValidateIds = function (array $requestedIds) use ($auth): array {
            if (!count($requestedIds)) return [];
            $placeholders = implode(',', array_fill(0, count($requestedIds), '?'));
            $st = db()->prepare("SELECT id FROM employees WHERE company_id=? AND id IN ($placeholders)");
            $st->execute(array_merge([$auth['company_id']], $requestedIds));
            return array_map('intval', array_column($st->fetchAll(), 'id'));
        };

        $teamIdsValid = []; // replace mode
        $teamAddValid = []; // delta mode
        $teamRemoveValid = []; // delta mode
        if ($teamMode === 'replace') {
            $teamIdsValid = $teamValidateIds($teamParseIds($d['team_member_ids']));
        } elseif ($teamMode === 'delta') {
            $teamAddValid = $teamValidateIds($teamParseIds($d['team_member_add_ids'] ?? []));
            $teamRemoveValid = $teamValidateIds($teamParseIds($d['team_member_remove_ids'] ?? []));
        }

        $applyTeamUpdate = function () use ($teamDirty, $teamMode, $teamIdsValid, $teamAddValid, $teamRemoveValid, $auth, $eidInt): void {
            if (!$teamDirty) return;
            if ($teamMode === 'replace') {
                db()->prepare("DELETE FROM employee_team_members WHERE company_id=? AND supervisor_id=?")
                    ->execute([$auth['company_id'], $eidInt]);
                if (count($teamIdsValid)) {
                    $ins = db()->prepare("INSERT IGNORE INTO employee_team_members (company_id, supervisor_id, employee_id) VALUES (?,?,?)");
                    foreach ($teamIdsValid as $mid) {
                        $ins->execute([$auth['company_id'], $eidInt, (int)$mid]);
                    }
                }
                return;
            }
            // Delta mode: add/remove only requested differences
            if (count($teamAddValid)) {
                $ins = db()->prepare("INSERT IGNORE INTO employee_team_members (company_id, supervisor_id, employee_id) VALUES (?,?,?)");
                foreach ($teamAddValid as $mid) {
                    $ins->execute([$auth['company_id'], $eidInt, (int)$mid]);
                }
            }
            if (count($teamRemoveValid)) {
                $placeholders = implode(',', array_fill(0, count($teamRemoveValid), '?'));
                $sql = "DELETE FROM employee_team_members
                        WHERE company_id=? AND supervisor_id=? AND employee_id IN ($placeholders)";
                db()->prepare($sql)->execute(array_merge([$auth['company_id'], $eidInt], $teamRemoveValid));
            }
        };

        $fields = []; $vals = [];
        foreach (['department_id','branch_id','policy_id','work_nature','name','national_id','phone','email','job_title','salary','hire_date','birth_date','address','is_active','photo_url','show_employee_report','show_public_holidays'] as $f) {
            if (!isset($d[$f]) && !array_key_exists($f, $d)) {
                continue;
            }
            if ($f === 'is_active') {
                $fields[] = 'is_active=?';
                $raw = $d['is_active'];
                $on = filter_var($raw, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
                if ($on === null) {
                    $on = ((int) $raw === 1 || $raw === '1');
                }
                $vals[] = $on ? 1 : 0;
                continue;
            }
            if ($f === 'show_employee_report' || $f === 'show_public_holidays') {
                $fields[] = "$f=?";
                $raw = $d[$f];
                $on = filter_var($raw, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
                if ($on === null) {
                    $on = ((int) $raw === 1 || $raw === '1');
                }
                $vals[] = $on ? 1 : 0;
                continue;
            }
            $fields[] = "$f=?";
            $vals[] = $d[$f] === '' ? null : $d[$f];
        }
        if (!empty($d['photo_data'])) {
            $photoPath = saveEmployeePhoto($d['photo_data'], $auth['company_id']);
            if ($photoPath) {
                $fields[] = "photo_url=?";
                $vals[] = $photoPath;
            }
        }
        if (!empty($d['password'])) {
            $fields[] = 'password=?';
            $fields[] = 'must_change_password=?';
            $vals[] = password_hash($d['password'], PASSWORD_BCRYPT, ['cost'=>12]);
            $vals[] = 0;
        }
        if (empty($fields)) {
            if ($clearedProfilePhoto) {
                $applyTeamUpdate();
                respond(200, [], 'Profile photo removed; the next check-in selfie can set a new photo automatically.');
            }
            if ($teamDirty) {
                $applyTeamUpdate();
                respond(200, [], 'Employee updated');
            }
            respond(400, 'Nothing to update');
        }
        $vals[] = $eid;
        db()->prepare("UPDATE employees SET ".implode(',', $fields)." WHERE id=?")->execute($vals);
        $applyTeamUpdate();
        respond(200, [], 'Employee updated');

    case 'DELETE:employees':
        $auth = requireRole('admin');
        $eid = $parts[1] ?? null;
        if (!$eid) respond(400, 'Employee ID required');
        $check = db()->prepare("SELECT id FROM employees WHERE id=? AND company_id=?");
        $check->execute([$eid, $auth['company_id']]);
        if (!$check->fetch()) respond(404, 'Employee not found');
        $docDir = __DIR__ . '/uploads/' . $auth['company_id'] . '/employee_documents/' . (int) $eid;
        if (is_dir($docDir)) {
            foreach (glob($docDir . '/*') ?: [] as $f) {
                if (is_file($f)) {
                    @unlink($f);
                }
            }
            @rmdir($docDir);
        }
        try {
            db()->prepare('DELETE FROM employee_documents WHERE employee_id=? AND company_id=?')->execute([(int) $eid, $auth['company_id']]);
        } catch (Exception $e) { /* table may not exist yet */
        }
        db()->prepare("DELETE FROM employees WHERE id=?")->execute([$eid]);
        respond(200, [], 'Employee deleted');

    case 'GET:employees/documents':
        $auth = requireRole('admin');
        $eid = $parts[1] ?? null;
        if (!$eid || !ctype_digit((string) $eid)) {
            respond(400, 'Employee ID required');
        }
        $check = db()->prepare('SELECT id FROM employees WHERE id=? AND company_id=?');
        $check->execute([(int) $eid, $auth['company_id']]);
        if (!$check->fetch()) {
            respond(404, 'Employee not found');
        }
        $st = db()->prepare('SELECT id, file_path, original_name, mime_type, file_size, created_at FROM employee_documents WHERE employee_id=? AND company_id=? ORDER BY id DESC');
        $st->execute([(int) $eid, $auth['company_id']]);
        respond(200, $st->fetchAll());

    case 'GET:employees/documents-download':
        $auth = requireRole('admin');
        $eid = $parts[1] ?? null;
        $docId = $parts[3] ?? null;
        if (!$eid || !ctype_digit((string) $eid) || !$docId || !ctype_digit((string) $docId)) {
            respond(400, 'Employee and document ID required');
        }
        $check = db()->prepare('SELECT id FROM employees WHERE id=? AND company_id=?');
        $check->execute([(int) $eid, $auth['company_id']]);
        if (!$check->fetch()) {
            respond(404, 'Employee not found');
        }
        $st = db()->prepare('SELECT file_path, original_name, mime_type FROM employee_documents WHERE id=? AND employee_id=? AND company_id=?');
        $st->execute([(int) $docId, (int) $eid, $auth['company_id']]);
        $row = $st->fetch();
        if (!$row) {
            respond(404, 'Document not found');
        }
        $rel = str_replace('\\', '/', (string) ($row['file_path'] ?? ''));
        if ($rel === '' || strpos($rel, '..') !== false) {
            respond(400, 'Invalid file path');
        }
        $expectedPrefix = 'uploads/' . $auth['company_id'] . '/employee_documents/' . (int) $eid . '/';
        if (strncmp($rel, $expectedPrefix, strlen($expectedPrefix)) !== 0) {
            respond(403, 'Forbidden');
        }
        $full = __DIR__ . '/' . ltrim($rel, '/');
        if (!is_file($full)) {
            respond(404, 'File not found on server');
        }
        $mime = (string) ($row['mime_type'] ?? 'application/octet-stream');
        if ($mime === '') {
            $mime = 'application/octet-stream';
        }
        $orig = (string) ($row['original_name'] ?? 'document');
        $ascii = preg_replace('/[^\x20-\x7E]/', '_', $orig) ?: 'document';
        $ascii = str_replace(['"', '\\'], '', $ascii);
        if (strlen($ascii) > 180) {
            $ascii = substr($ascii, 0, 180);
        }
        header_remove('Content-Type');
        header('Content-Type: ' . $mime);
        header('Content-Length: ' . (string) filesize($full));
        header('Content-Disposition: inline; filename="' . $ascii . '"; filename*=UTF-8\'\'' . rawurlencode($orig));
        header('Cache-Control: private, max-age=0, must-revalidate');
        header('X-Content-Type-Options: nosniff');
        readfile($full);
        exit;

    case 'POST:employees/documents':
        $auth = requireRole('admin');
        $eid = $parts[1] ?? null;
        if (!$eid || !ctype_digit((string) $eid)) {
            respond(400, 'Employee ID required');
        }
        $check = db()->prepare('SELECT id FROM employees WHERE id=? AND company_id=?');
        $check->execute([(int) $eid, $auth['company_id']]);
        if (!$check->fetch()) {
            respond(404, 'Employee not found');
        }
        $d = input();
        $files = $d['files'] ?? null;
        if (!is_array($files) || count($files) === 0) {
            respond(400, 'files array required: [{ file_name, file_data }]');
        }
        $pdo = db();
        $inserted = [];
        $attempted = 0;
        foreach ($files as $item) {
            if (!is_array($item)) {
                continue;
            }
            $fn = isset($item['file_name']) ? (string) $item['file_name'] : '';
            $fd = isset($item['file_data']) ? (string) $item['file_data'] : '';
            if ($fn === '' || $fd === '') {
                continue;
            }
            $attempted++;
            $saved = saveEmployeeDocumentFromBase64($fd, $auth['company_id'], (int) $eid, $fn);
            if (!$saved) {
                continue;
            }
            $pdo->prepare('INSERT INTO employee_documents (company_id, employee_id, file_path, original_name, mime_type, file_size) VALUES (?,?,?,?,?,?)')
                ->execute([$auth['company_id'], (int) $eid, $saved['file_path'], $saved['original_name'], $saved['mime_type'], $saved['file_size']]);
            $inserted[] = [
                'id' => (int) $pdo->lastInsertId(),
                'file_path' => $saved['file_path'],
                'original_name' => $saved['original_name'],
                'mime_type' => $saved['mime_type'],
                'file_size' => $saved['file_size'],
            ];
        }
        if (count($inserted) === 0) {
            if ($attempted === 0) {
                respond(400, 'No valid files in request (need file_name + file_data per item)');
            }
            $hintPath = 'uploads/' . $auth['company_id'] . '/employee_documents/' . (int) $eid . '/';
            respond(400, 'Files could not be saved. Allowed: PDF, Word, Excel, images (JPEG/PNG/WebP/GIF/BMP; max 12MB each). Target folder: ' . $hintPath . ' next to api.php — check permissions and disk space.');
        }
        auditLog('admin', $auth['company_id'], 'EMPLOYEE_DOCUMENTS_UPLOAD', ['employee_id' => (int) $eid, 'count' => count($inserted)], $auth['company_id']);
        respond(201, ['uploaded' => $inserted], 'Files uploaded');

    case 'PUT:employees/documents-item':
        $auth = requireRole('admin');
        $eid = $parts[1] ?? null;
        $docId = $parts[3] ?? null;
        if (!$eid || !ctype_digit((string) $eid) || !$docId || !ctype_digit((string) $docId)) {
            respond(400, 'Employee and document ID required');
        }
        $check = db()->prepare('SELECT id FROM employees WHERE id=? AND company_id=?');
        $check->execute([(int) $eid, $auth['company_id']]);
        if (!$check->fetch()) {
            respond(404, 'Employee not found');
        }
        $st = db()->prepare('SELECT id FROM employee_documents WHERE id=? AND employee_id=? AND company_id=?');
        $st->execute([(int) $docId, (int) $eid, $auth['company_id']]);
        if (!$st->fetch()) {
            respond(404, 'Document not found');
        }
        $d = input();
        $newName = isset($d['original_name']) ? trim((string) $d['original_name']) : '';
        if ($newName === '') {
            respond(400, 'original_name required');
        }
        $newName = sanitizeEmployeeDocumentFileName($newName);
        if ($newName === '') {
            respond(400, 'Invalid display name');
        }
        db()->prepare('UPDATE employee_documents SET original_name=? WHERE id=? AND company_id=?')
            ->execute([$newName, (int) $docId, $auth['company_id']]);
        auditLog('admin', $auth['company_id'], 'EMPLOYEE_DOCUMENT_RENAME', ['employee_id' => (int) $eid, 'document_id' => (int) $docId], $auth['company_id']);
        respond(200, ['original_name' => $newName], 'Document updated');

    case 'DELETE:employees/documents-item':
        $auth = requireRole('admin');
        $eid = $parts[1] ?? null;
        $docId = $parts[3] ?? null;
        if (!$eid || !ctype_digit((string) $eid) || !$docId || !ctype_digit((string) $docId)) {
            respond(400, 'Employee and document ID required');
        }
        $st = db()->prepare('SELECT id, file_path FROM employee_documents WHERE id=? AND employee_id=? AND company_id=?');
        $st->execute([(int) $docId, (int) $eid, $auth['company_id']]);
        $row = $st->fetch();
        if (!$row) {
            respond(404, 'Document not found');
        }
        $rel = (string) ($row['file_path'] ?? '');
        if ($rel !== '' && strpos($rel, '..') === false) {
            $full = __DIR__ . '/' . ltrim($rel, '/');
            if (is_file($full)) {
                @unlink($full);
            }
        }
        db()->prepare('DELETE FROM employee_documents WHERE id=? AND company_id=?')->execute([(int) $docId, $auth['company_id']]);
        respond(200, [], 'Document deleted');

    case 'POST:employees/reset-device':
        $auth = requireRole('admin');
        $eid = $parts[1] ?? null; // employees/{id}/reset-device
        if (!$eid) respond(400, 'Employee ID required');
        $check = db()->prepare("SELECT id FROM employees WHERE id=? AND company_id=?");
        $check->execute([$eid, $auth['company_id']]);
        if (!$check->fetch()) respond(404, 'Employee not found');
        db()->prepare("UPDATE employees SET device_id=NULL, device_bound_at=NULL WHERE id=?")->execute([$eid]);
        auditLog('admin', $auth['company_id'], 'RESET_DEVICE', ['employee_id'=>$eid], $auth['company_id']);
        respond(200, [], 'Device reset. Employee can log in from a new device.');

    case 'POST:employees/reset-password':
        $auth = requireRole('admin');
        $eid = $parts[1] ?? null; // employees/{id}/reset-password
        if (!$eid) respond(400, 'Employee ID required');
        $d = input();
        if (empty($d['new_password'])) respond(400, 'New password is required');
        $check = db()->prepare("SELECT id FROM employees WHERE id=? AND company_id=?");
        $check->execute([$eid, $auth['company_id']]);
        if (!$check->fetch()) respond(404, 'Employee not found');
        $hash = password_hash($d['new_password'], PASSWORD_BCRYPT, ['cost'=>12]);
        db()->prepare("UPDATE employees SET password=?, must_change_password=0 WHERE id=?")->execute([$hash, $eid]);
        auditLog('admin', $auth['company_id'], 'RESET_PASSWORD', ['employee_id'=>$eid], $auth['company_id']);
        $em = db()->prepare('SELECT name, email, username, employee_code FROM employees WHERE id=? AND company_id=?');
        $em->execute([(int) $eid, $auth['company_id']]);
        $er = $em->fetch();
        if ($er) {
            $mailTo = trim((string) ($er['email'] ?? ''));
            if ($mailTo !== '' && filter_var($mailTo, FILTER_VALIDATE_EMAIL)) {
                sendEmployeeCredentialsEmail(
                    $auth['company_id'],
                    $mailTo,
                    (string) ($er['name'] ?? ''),
                    (string) ($er['username'] ?? ''),
                    (string) $d['new_password'],
                    (string) ($er['employee_code'] ?? ''),
                    'password_reset'
                );
            }
        }
        respond(200, [], 'Employee password has been reset successfully.');

    case 'POST:employees/acknowledge-must-change-password':
        requireRole('employee');
        respond(403, 'Password change by employees is disabled / تغيير كلمة المرور من تطبيق الموظف غير متاح');

    case 'POST:employees/change-password':
        requireRole('employee');
        respond(403, 'Password change by employees is disabled / تغيير كلمة المرور من تطبيق الموظف غير متاح');

    case 'GET:employees/me/policy':
        $auth = requireRole('employee');
        $st = db()->prepare('SELECT lp.policy_name, lp.start_time, lp.end_time, lp.grace_late_minutes, lp.grace_early_leave_minutes
            FROM employees e
            LEFT JOIN leave_policies lp ON e.policy_id = lp.id AND lp.company_id = e.company_id
            WHERE e.id = ? AND e.company_id = ?');
        $st->execute([(int) $auth['id'], $auth['company_id']]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            respond(404, 'Employee not found');
        }
        respond(200, [
            'policy_name' => $row['policy_name'] !== null && $row['policy_name'] !== '' ? (string) $row['policy_name'] : null,
            'start_time' => $row['start_time'] !== null && (string) $row['start_time'] !== '' ? (string) $row['start_time'] : '09:00:00',
            'end_time' => $row['end_time'] !== null && (string) $row['end_time'] !== '' ? (string) $row['end_time'] : '17:00:00',
            'grace_late_minutes' => max(0, (int) ($row['grace_late_minutes'] ?? 5)),
            'grace_early_leave_minutes' => max(0, (int) ($row['grace_early_leave_minutes'] ?? 5)),
        ]);

    case 'GET:employees/item/policy':
        $auth = requireRole('admin');
        $eid = isset($parts[1]) ? (int) $parts[1] : 0;
        if ($eid <= 0) {
            respond(400, 'Invalid employee');
        }
        $st = db()->prepare('SELECT lp.policy_name, lp.start_time, lp.end_time, lp.grace_late_minutes, lp.grace_early_leave_minutes
            FROM employees e
            LEFT JOIN leave_policies lp ON e.policy_id = lp.id AND lp.company_id = e.company_id
            WHERE e.id = ? AND e.company_id = ?');
        $st->execute([$eid, $auth['company_id']]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            respond(404, 'Employee not found');
        }
        respond(200, [
            'policy_name' => $row['policy_name'] !== null && $row['policy_name'] !== '' ? (string) $row['policy_name'] : null,
            'start_time' => $row['start_time'] !== null && (string) $row['start_time'] !== '' ? (string) $row['start_time'] : '09:00:00',
            'end_time' => $row['end_time'] !== null && (string) $row['end_time'] !== '' ? (string) $row['end_time'] : '17:00:00',
            'grace_late_minutes' => max(0, (int) ($row['grace_late_minutes'] ?? 5)),
            'grace_early_leave_minutes' => max(0, (int) ($row['grace_early_leave_minutes'] ?? 5)),
        ]);

    case 'GET:employees/me/screen-flags':
        $auth = requireRole('employee');
        $st = db()->prepare('SELECT COALESCE(show_employee_report, 0) AS show_employee_report, COALESCE(show_public_holidays, 0) AS show_public_holidays FROM employees WHERE id=? AND company_id=?');
        $st->execute([(int) $auth['id'], $auth['company_id']]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            respond(404, 'Employee not found');
        }
        respond(200, [
            'show_employee_report' => (int) $row['show_employee_report'] === 1,
            'show_public_holidays' => (int) $row['show_public_holidays'] === 1,
        ]);

    case 'PUT:employees/profile':
        $auth = requireRole('employee');
        $d = input();
        
        $fields = []; $vals = [];
        if (isset($d['phone'])) { $fields[] = "phone=?"; $vals[] = $d['phone']; }
        if (isset($d['email'])) { $fields[] = "email=?"; $vals[] = $d['email']; }
        
        if (empty($fields) && empty($d['photo_data'])) respond(400, 'Nothing to update');
        
        if (!empty($d['photo_data'])) {
            // Check if photo already exists - don't allow override if it does
            $checkPhoto = db()->prepare("SELECT photo_url FROM employees WHERE id=?");
            $checkPhoto->execute([$auth['id']]);
            $currentPhoto = $checkPhoto->fetchColumn();
            
            if (empty($currentPhoto)) {
                $photoPath = saveEmployeePhoto($d['photo_data'], $auth['company_id']);
                if ($photoPath) {
                    $fields[] = "photo_url=?";
                    $vals[] = $photoPath;
                }
            } else {
                // Photo already exists, don't allow change
            }
        }
        
        if (!empty($fields)) {
            $vals[] = $auth['id'];
            db()->prepare("UPDATE employees SET ".implode(',', $fields)." WHERE id=?")->execute($vals);
            respond(200, [], 'Profile updated successfully');
        } else {
             respond(200, [], 'Nothing updated');
        }

    case 'POST:admin/timezone':
        $auth = requireRole('admin');
        $d = input();
        if (!isset($d['timezone'])) respond(400, 'Timezone is required');
        db()->prepare("UPDATE companies SET timezone = ? WHERE company_id = ?")
            ->execute([$d['timezone'], $auth['company_id']]);
        respond(200, null, 'Timezone updated successfully');

    case 'POST:admin/notification-email':
        requireRole('admin');
        respond(403, 'يتم ضبط بريد تنبيهات الإجازات من لوحة Super Admin فقط (إعدادات الشركة) لتقليل البريد غير المرغوب والتأكد من صلاحية المستلم.');

    case 'GET:admin/welcome-email-template':
        $auth = requireRole('admin');
        $st = db()->prepare('SELECT employee_welcome_email_body FROM companies WHERE company_id=?');
        $st->execute([$auth['company_id']]);
        $tpl = $st->fetchColumn();
        respond(200, [
            'template' => $tpl !== false && $tpl !== null ? (string) $tpl : '',
            'default_template' => defaultEmployeeWelcomeEmailTemplate(),
            'placeholders' => ['{employee_name}', '{username}', '{password}', '{employee_code}', '{company_name}', '{login_url}'],
            'placeholders_text' => '{employee_name} {username} {password} {employee_code} {company_name} {login_url}',
        ]);

    case 'PUT:admin/welcome-email-template':
        $auth = requireRole('admin');
        $d = input();
        if (!array_key_exists('template', $d)) {
            respond(400, 'template is required');
        }
        $tpl = (string) $d['template'];
        if (strlen($tpl) > 20000) {
            respond(400, 'Template too long');
        }
        db()->prepare('UPDATE companies SET employee_welcome_email_body=? WHERE company_id=?')
            ->execute([$tpl === '' ? null : $tpl, $auth['company_id']]);
        respond(200, [], 'Welcome email template saved');

    case 'GET:admin/mobile-app-url':
        $auth = requireRole('admin');
        $st = db()->prepare('SELECT mobile_app_url FROM companies WHERE company_id=?');
        $st->execute([$auth['company_id']]);
        $u = $st->fetchColumn();
        respond(200, [
            'mobile_app_url' => ($u !== false && $u !== null && (string) $u !== '') ? (string) $u : '',
        ]);

    case 'PUT:admin/mobile-app-url':
        $auth = requireRole('admin');
        $d = input();
        if (!array_key_exists('mobile_app_url', $d)) {
            respond(400, 'mobile_app_url is required');
        }
        $raw = trim((string) $d['mobile_app_url']);
        if ($raw === '') {
            db()->prepare('UPDATE companies SET mobile_app_url=NULL WHERE company_id=?')->execute([$auth['company_id']]);
            respond(200, ['mobile_app_url' => ''], 'Saved');
        }
        if (strlen($raw) > 2048) {
            respond(400, 'URL too long');
        }
        if (!preg_match('#^https?://#i', $raw)) {
            $raw = 'https://' . $raw;
        }
        if (filter_var($raw, FILTER_VALIDATE_URL) === false) {
            respond(400, 'Invalid URL');
        }
        db()->prepare('UPDATE companies SET mobile_app_url=? WHERE company_id=?')->execute([$raw, $auth['company_id']]);
        respond(200, ['mobile_app_url' => $raw], 'Saved');

    case 'GET:admin/push-vapid-public':
        requireRole('admin');
        $v = hrproGetEffectiveVapidConfig();
        $keysOk = hrproVapidKeysPresent();
        respond(200, [
            'publicKey' => $keysOk ? $v['public'] : null,
            'webpush_library_loaded' => hrproWebPushVendorAvailable(),
            'vapid_source' => $keysOk ? $v['source'] : null,
        ]);

    case 'POST:admin/push-subscribe':
        $auth = requireRole('admin');
        $d = input();
        $sub = $d['subscription'] ?? null;
        if ($sub === null) {
            respond(400, 'subscription required');
        }
        $arr = is_array($sub) ? $sub : json_decode((string)$sub, true);
        if (!$arr || empty($arr['endpoint'])) {
            respond(400, 'Invalid subscription');
        }
        $hash = hash('sha256', $arr['endpoint']);
        $json = json_encode($arr, JSON_UNESCAPED_UNICODE);
        $stmt = db()->prepare("REPLACE INTO admin_push_subscriptions (company_id, endpoint_hash, subscription_json) VALUES (?,?,?)");
        $stmt->execute([$auth['company_id'], $hash, $json]);
        respond(200, [], 'Push subscription saved');

    case 'POST:admin/push-unsubscribe':
        $auth = requireRole('admin');
        $d = input();
        $endpoint = $d['endpoint'] ?? '';
        if ($endpoint === '') {
            respond(400, 'endpoint required');
        }
        $h = hash('sha256', $endpoint);
        db()->prepare("DELETE FROM admin_push_subscriptions WHERE company_id=? AND endpoint_hash=?")
            ->execute([$auth['company_id'], $h]);
        respond(200, [], 'Unsubscribed');

    case 'POST:admin/push-test':
        $auth = requireRole('admin');
        $cnt = db()->prepare('SELECT COUNT(*) FROM admin_push_subscriptions WHERE company_id=?');
        $cnt->execute([$auth['company_id']]);
        $subs = (int) $cnt->fetchColumn();
        $vapidOk = hrproVapidKeysPresent();
        $vendorOk = hrproWebPushVendorAvailable();
        if (!$vapidOk) {
            respond(400, ['subscribers' => $subs, 'vapid_keys_present' => false, 'webpush_library_loaded' => $vendorOk], 'VAPID keys are not configured');
        }
        if (!$vendorOk) {
            respond(400, ['subscribers' => $subs, 'vapid_keys_present' => true, 'webpush_library_loaded' => false], 'Web Push PHP library not installed (composer install in /hr)');
        }
        if ($subs <= 0) {
            respond(400, ['subscribers' => 0, 'vapid_keys_present' => true, 'webpush_library_loaded' => true], 'No admin push subscriptions for this company. Enable push first.');
        }
        adminSendWebPush(
            $auth['company_id'],
            'HR Pro — اختبار',
            'هذا إشعار تجريبي لتأكيد التفعيل',
            '/hr/admin.html?hrpro_pwa=admin'
        );
        respond(200, ['subscribers' => $subs, 'vapid_keys_present' => true, 'webpush_library_loaded' => true], 'Test push sent to ' . $subs . ' device(s)');

    case 'GET:admin/notifications/count':
        $auth = requireRole('admin');
        $pdo = db();
        $sup = shouldSuppressPreCloseSubscriptionAlerts($pdo, $auth['company_id']) ? 1 : 0;
        $hasDeleted = adminNotificationsHasDeletedColumn($pdo);
        $deletedWhere = $hasDeleted ? ' AND is_deleted=0' : '';
        $stmt = $pdo->prepare("SELECT COUNT(*) FROM admin_notifications WHERE company_id=?" . $deletedWhere . " AND is_read=0 AND ( ? = 0 OR type NOT IN ('subscription','billing','trial_reminder','account_close'))");
        $stmt->execute([$auth['company_id'], $sup]);
        respond(200, ['unread' => (int) $stmt->fetchColumn()]);

    case 'GET:admin/notifications':
        $auth = requireRole('admin');
        $in = input();
        $limit = isset($in['limit']) ? (int) $in['limit'] : (isset($_GET['limit']) ? (int) $_GET['limit'] : 30);
        $includeArchived = !empty($in['include_archived']) || !empty($_GET['include_archived']);
        $archivedOnly = !empty($in['archived_only']) || !empty($_GET['archived_only']);
        if ($limit < 1) {
            $limit = 1;
        }
        if ($limit > 80) {
            $limit = 80;
        }
        $pdo = db();
        $sup = shouldSuppressPreCloseSubscriptionAlerts($pdo, $auth['company_id']) ? 1 : 0;
        $hasDeleted = adminNotificationsHasDeletedColumn($pdo);
        if ($archivedOnly && !$hasDeleted) {
            respond(200, []);
        }
        $deletedWhere = '';
        if ($hasDeleted) {
            if ($archivedOnly) {
                $deletedWhere = ' AND is_deleted=1';
            } elseif (!$includeArchived) {
                $deletedWhere = ' AND is_deleted=0';
            }
        }
        $selectDeleted = $hasDeleted ? 'is_deleted' : '0 AS is_deleted';
        $stmt = $pdo->prepare('SELECT id, type, title, body, action_url, is_read, ' . $selectDeleted . ', created_at FROM admin_notifications WHERE company_id=?' . $deletedWhere . ' AND ( ? = 0 OR type NOT IN (\'subscription\',\'billing\',\'trial_reminder\',\'account_close\')) ORDER BY created_at DESC LIMIT ' . $limit);
        $stmt->execute([$auth['company_id'], $sup]);
        respond(200, $stmt->fetchAll());

    case 'PUT:admin/notifications/mark-read':
        $auth = requireRole('admin');
        $d = input();
        $pdo = db();
        $hasDeleted = adminNotificationsHasDeletedColumn($pdo);
        if (!empty($d['all'])) {
            $sql = "UPDATE admin_notifications SET is_read=1 WHERE company_id=? AND is_read=0";
            if ($hasDeleted) {
                $sql .= " AND is_deleted=0";
            }
            $pdo->prepare($sql)
                ->execute([$auth['company_id']]);
        } elseif (!empty($d['ids']) && is_array($d['ids'])) {
            $sql = "UPDATE admin_notifications SET is_read=1 WHERE company_id=? AND id=?";
            if ($hasDeleted) {
                $sql .= " AND is_deleted=0";
            }
            $upd = $pdo->prepare($sql);
            foreach ($d['ids'] as $rawId) {
                $id = (int)$rawId;
                if ($id > 0) {
                    $upd->execute([$auth['company_id'], $id]);
                }
            }
        } else {
            respond(400, 'Provide all: true or ids: []');
        }
        respond(200, [], 'Notifications updated');

    case 'PUT:admin/notifications/delete':
        $auth = requireRole('admin');
        $d = input();
        $pdo = db();
        if (!adminNotificationsEnsureDeletedColumn($pdo)) {
            respond(500, 'Unable to archive notifications at the moment.');
        }
        if (!empty($d['all'])) {
            $pdo->prepare("UPDATE admin_notifications SET is_deleted=1 WHERE company_id=? AND is_deleted=0")
                ->execute([$auth['company_id']]);
        } elseif (!empty($d['ids']) && is_array($d['ids'])) {
            $upd = $pdo->prepare("UPDATE admin_notifications SET is_deleted=1 WHERE company_id=? AND id=? AND is_deleted=0");
            foreach ($d['ids'] as $rawId) {
                $id = (int)$rawId;
                if ($id > 0) {
                    $upd->execute([$auth['company_id'], $id]);
                }
            }
        } else {
            respond(400, 'Provide all: true or ids: []');
        }
        respond(200, [], 'Notifications archived');

    case 'DELETE:admin/notifications/purge':
    case 'PUT:admin/notifications/purge':
        $auth = requireRole('admin');
        $d = input();
        $pdo = db();
        if (!adminNotificationsEnsureDeletedColumn($pdo)) {
            respond(500, 'Unable to delete archived notifications at the moment.');
        }
        if (!empty($d['all'])) {
            $pdo->prepare("DELETE FROM admin_notifications WHERE company_id=? AND is_deleted=1")
                ->execute([$auth['company_id']]);
        } elseif (!empty($d['ids']) && is_array($d['ids'])) {
            $del = $pdo->prepare("DELETE FROM admin_notifications WHERE company_id=? AND id=? AND is_deleted=1");
            foreach ($d['ids'] as $rawId) {
                $id = (int)$rawId;
                if ($id > 0) {
                    $del->execute([$auth['company_id'], $id]);
                }
            }
        } else {
            respond(400, 'Provide all: true or ids: []');
        }
        respond(200, [], 'Archived notifications deleted permanently');

    case 'GET:admin/company-news':
        $auth = requireRole('admin');
        $stmt = db()->prepare('SELECT id, message, is_active, sort_order, created_at FROM company_news_ticker WHERE company_id=? ORDER BY sort_order DESC, id DESC');
        $stmt->execute([$auth['company_id']]);
        respond(200, $stmt->fetchAll());

    case 'POST:admin/company-news':
        $auth = requireRole('admin');
        $d = input();
        $msg = trim((string)($d['message'] ?? ''));
        if ($msg === '') {
            respond(400, 'message required');
        }
        $maxLen = function_exists('mb_strlen') ? mb_strlen($msg) : strlen($msg);
        if ($maxLen > 8000) {
            respond(400, 'message too long');
        }
        $sort = isset($d['sort_order']) ? (int)$d['sort_order'] : 0;
        $active = 1;
        if (array_key_exists('is_active', $d)) {
            $v = $d['is_active'];
            $active = ($v === 1 || $v === '1' || $v === true) ? 1 : 0;
        }
        db()->prepare('INSERT INTO company_news_ticker (company_id, message, is_active, sort_order) VALUES (?,?,?,?)')
            ->execute([$auth['company_id'], $msg, $active, $sort]);
        if ($active) {
            $excerpt = function_exists('mb_substr') ? mb_substr($msg, 0, 140) : substr($msg, 0, 140);
            employeeSendWebPushCompany(
                $auth['company_id'],
                'HR Pro — news · أخبار من الإدارة',
                $excerpt !== '' ? $excerpt : ' ',
                '/hr/login.html?hrpro_pwa=employee&open_tab=news'
            );
        }
        respond(201, ['id' => (int)db()->lastInsertId()], 'News item created');

    case 'PUT:admin/company-news/item':
        $auth = requireRole('admin');
        $id = (int)($parts[2] ?? 0);
        if ($id < 1) {
            respond(400, 'Invalid id');
        }
        $d = input();
        $chk = db()->prepare('SELECT id FROM company_news_ticker WHERE id=? AND company_id=?');
        $chk->execute([$id, $auth['company_id']]);
        if (!$chk->fetch()) {
            respond(404, 'Not found');
        }
        $fields = [];
        $vals = [];
        if (array_key_exists('message', $d)) {
            $m = trim((string)$d['message']);
            if ($m === '') {
                respond(400, 'message empty');
            }
            $ml = function_exists('mb_strlen') ? mb_strlen($m) : strlen($m);
            if ($ml > 8000) {
                respond(400, 'message too long');
            }
            $fields[] = 'message=?';
            $vals[] = $m;
        }
        if (array_key_exists('is_active', $d)) {
            $v = $d['is_active'];
            $fields[] = 'is_active=?';
            $vals[] = ($v === 1 || $v === '1' || $v === true) ? 1 : 0;
        }
        if (array_key_exists('sort_order', $d)) {
            $fields[] = 'sort_order=?';
            $vals[] = (int)$d['sort_order'];
        }
        if (!$fields) {
            respond(400, 'Nothing to update');
        }
        $vals[] = $id;
        $vals[] = $auth['company_id'];
        db()->prepare('UPDATE company_news_ticker SET ' . implode(',', $fields) . ' WHERE id=? AND company_id=?')->execute($vals);
        respond(200, [], 'Updated');

    case 'DELETE:admin/company-news/item':
        $auth = requireRole('admin');
        $id = (int)($parts[2] ?? 0);
        if ($id < 1) {
            respond(400, 'Invalid id');
        }
        db()->prepare('DELETE FROM company_news_ticker WHERE id=? AND company_id=?')->execute([$id, $auth['company_id']]);
        respond(200, [], 'Deleted');

    case 'GET:company-news':
        $auth = requireRole('employee');
        $stmt = db()->prepare('SELECT id, message, sort_order, created_at FROM company_news_ticker WHERE company_id=? AND is_active=1 ORDER BY sort_order DESC, id DESC LIMIT 500');
        $stmt->execute([$auth['company_id']]);
        respond(200, $stmt->fetchAll());

    case 'GET:employee/contacts':
        $auth = requireRole('employee');
        $cid = $auth['company_id'] ?? null;
        $eid = (int) ($auth['id'] ?? 0);
        if (!$cid) {
            respond(403, 'Forbidden');
        }
        // Active employees only (stopped accounts never appear to employees)
        $stmt = db()->prepare("
            SELECT
                e.id,
                e.name,
                e.phone,
                e.email,
                e.job_title,
                e.photo_url,
                e.is_active,
                d.department_name,
                b.name AS branch_name
            FROM employees e
            LEFT JOIN departments d ON e.department_id = d.id
            LEFT JOIN branches b ON e.branch_id = b.id
            WHERE e.company_id = ?
              AND e.is_active = 1
              AND e.id <> ?
            ORDER BY e.name ASC
            LIMIT 5000
        ");
        $stmt->execute([$cid, $eid]);
        respond(200, $stmt->fetchAll());

    case 'GET:employee/push-vapid-public':
        requireRole('employee');
        $v = hrproGetEffectiveVapidConfig();
        $keysOk = hrproVapidKeysPresent();
        respond(200, [
            'publicKey' => $keysOk ? $v['public'] : null,
            'webpush_library_loaded' => hrproWebPushVendorAvailable(),
            'vapid_source' => $keysOk ? $v['source'] : null,
        ]);

    case 'POST:employee/push-subscribe':
        $auth = requireRole('employee');
        $d = input();
        $sub = $d['subscription'] ?? null;
        if ($sub === null) {
            respond(400, 'subscription required');
        }
        $arr = is_array($sub) ? $sub : json_decode((string)$sub, true);
        if (!$arr || empty($arr['endpoint'])) {
            respond(400, 'Invalid subscription');
        }
        $hash = hash('sha256', $arr['endpoint']);
        $json = json_encode($arr, JSON_UNESCAPED_UNICODE);
        db()->prepare('REPLACE INTO employee_push_subscriptions (company_id, employee_id, endpoint_hash, subscription_json) VALUES (?,?,?,?)')
            ->execute([$auth['company_id'], $auth['id'], $hash, $json]);
        respond(200, [], 'Push subscription saved');

    case 'POST:employee/push-unsubscribe':
        $auth = requireRole('employee');
        $d = input();
        $endpoint = $d['endpoint'] ?? '';
        if ($endpoint === '') {
            respond(400, 'endpoint required');
        }
        $h = hash('sha256', $endpoint);
        db()->prepare('DELETE FROM employee_push_subscriptions WHERE company_id=? AND employee_id=? AND endpoint_hash=?')
            ->execute([$auth['company_id'], $auth['id'], $h]);
        respond(200, [], 'Unsubscribed');

    case 'POST:admin/settings/logo':
        $auth = requireRole('admin');
        $d = input();
        if (empty($d['logo'])) respond(400, 'Logo is required');
        $logoPath = saveBase64Image($d['logo'], 'logo_' . $auth['company_id'], $auth['company_id']);
        if (!$logoPath) respond(500, 'Failed to save logo');
        db()->prepare("UPDATE companies SET company_logo = ? WHERE company_id = ?")
            ->execute([$logoPath, $auth['company_id']]);
        respond(200, ['logo_url' => $logoPath], 'Logo updated successfully');

    case 'POST:admin/change-password':
        $auth = requireRole('admin');
        $d = input();
        if (empty($d['old_password']) || empty($d['new_password'])) respond(400, 'Both old and new passwords are required');
        if (strlen($d['new_password']) < 6) respond(400, 'New password must be at least 6 characters');
        
        $comp = db()->prepare("SELECT admin_password FROM companies WHERE company_id=?");
        $comp->execute([$auth['company_id']]);
        $company = $comp->fetch();
        if (!$company) respond(404, 'Admin not found');
        
        if (!password_verify($d['old_password'], $company['admin_password'])) {
            respond(400, 'Old password is incorrect');
        }
        
        $hash = password_hash($d['new_password'], PASSWORD_BCRYPT, ['cost'=>12]);
        db()->prepare("UPDATE companies SET admin_password=? WHERE company_id=?")->execute([$hash, $auth['company_id']]);
        auditLog('admin', $auth['company_id'], 'CHANGE_PASSWORD', [], $auth['company_id']);
        respond(200, [], 'Admin password changed successfully');

// ═══════════════════════════════════════════════════════════════════════════════
// CLINIC LOCATION
// ═══════════════════════════════════════════════════════════════════════════════
    case 'GET:location':
        $auth = requireRole('admin','employee');
        $cid  = $auth['company_id'];
        $row  = db()->prepare("SELECT * FROM clinic_locations WHERE company_id=?");
        $row->execute([$cid]);
        respond(200, $row->fetch() ?: null);

    case 'POST:location':
    case 'PUT:location':
        $auth = requireRole('admin');
        $d = input();
        foreach (['latitude','longitude'] as $f) {
            if (!isset($d[$f])) respond(400, "Field '$f' is required");
        }
        $exists = db()->prepare("SELECT id FROM clinic_locations WHERE company_id=?");
        $exists->execute([$auth['company_id']]);
        if ($exists->fetch()) {
            db()->prepare("UPDATE clinic_locations SET latitude=?,longitude=?,location_name=?,allowed_radius_meters=? WHERE company_id=?")
                ->execute([$d['latitude'],$d['longitude'],$d['location_name']??'Main Office',$d['allowed_radius_meters']??100,$auth['company_id']]);
        } else {
            db()->prepare("INSERT INTO clinic_locations (company_id,latitude,longitude,location_name,allowed_radius_meters) VALUES (?,?,?,?,?)")
                ->execute([$auth['company_id'],$d['latitude'],$d['longitude'],$d['location_name']??'Main Office',$d['allowed_radius_meters']??100]);
        }
        respond(200, [], 'Location saved');

// ═══════════════════════════════════════════════════════════════════════════════
// QR CODE GENERATION (Admin generates, Employee scans)
// ═══════════════════════════════════════════════════════════════════════════════
    case 'POST:qr/generate':
        $auth = requireRole('admin');
        $cid  = $auth['company_id'];
        // Invalidate old tokens
        db()->prepare("UPDATE qr_tokens SET used=1 WHERE company_id=? AND expires_at < NOW()")->execute([$cid]);
        $token   = bin2hex(random_bytes(32));
        $expires = date('Y-m-d H:i:s', time() + QR_TOKEN_LIFETIME);
        db()->prepare("INSERT INTO qr_tokens (company_id,token,expires_at) VALUES (?,?,?)")->execute([$cid, $token, $expires]);
        respond(200, ['token'=>$token,'expires_at'=>$expires,'lifetime_seconds'=>QR_TOKEN_LIFETIME]);

    case 'POST:qr/validate':
        // Called before attendance to validate token
        $auth  = requireRole('employee');
        $d     = input();
        $token = $d['token'] ?? '';
        $stmt  = db()->prepare("SELECT * FROM qr_tokens WHERE token=? AND company_id=? AND used=0 AND expires_at > NOW()");
        $stmt->execute([$token, $auth['company_id']]);
        $qr = $stmt->fetch();
        if (!$qr) respond(400, 'QR code expired or invalid. Please refresh.');
        respond(200, ['valid'=>true]);

// ═══════════════════════════════════════════════════════════════════════════════
// ATTENDANCE
// ═══════════════════════════════════════════════════════════════════════════════
    case 'POST:attendance/direct':
        $auth     = requireRole('employee');
        $d        = input();
        $deviceId = $_SERVER['HTTP_X_DEVICE_ID'] ?? $d['device_id'] ?? null;

        // Device check
        $emp2 = db()->prepare("SELECT * FROM employees WHERE id=? AND company_id=?");
        $emp2->execute([$auth['id'], $auth['company_id']]);
        $employee2 = $emp2->fetch();
        if (!$employee2) respond(404, 'Employee not found');

        // Smart device check (fingerprint-aware auto-update)
        $devCheck2 = smartDeviceCheck($employee2, $deviceId, 'attendance');
        if (!$devCheck2['ok']) respond(403, $devCheck2['message']);

        // Geolocation
        $lat2      = (float)($d['latitude'] ?? 0);
        $lng2      = (float)($d['longitude'] ?? 0);
        $distance2 = null;
        $locStatus2 = 'ok';
        $finalNotes2 = $d['notes'] ?? '';
        $matchedBranchName2 = null;
        $gpsAccuracy2 = (float)($d['gps_accuracy'] ?? 0);

        if ($employee2['work_nature'] === 'external') {
            $locStatus2 = 'ok';
            $distance2 = 0;
            $matchedBranch2 = findMatchedBranch($auth['company_id'], $lat2, $lng2, $gpsAccuracy2, false);
            if ($matchedBranch2) $matchedBranchName2 = $matchedBranch2['name'];
        } else {
            // Strict Geofencing for Indoor (Internal)
            if (!$lat2 || !$lng2 || ($lat2 == 0 && $lng2 == 0)) {
                respond(403, 'عذراً، يجب تفعيل خدمة الموقع (GPS) للبصمة كحضور داخلي');
            }
            $matchedBranch2 = findAttendanceBranchForInternal(
                $auth['company_id'],
                $lat2,
                $lng2,
                $gpsAccuracy2,
                !empty($employee2['branch_id']) ? (int) $employee2['branch_id'] : null
            );
            if (!$matchedBranch2) {
                $homeBranchName2 = getBranchNameById($auth['company_id'], !empty($employee2['branch_id']) ? (int)$employee2['branch_id'] : null) ?? 'غير محدد';
                $nearest2 = findNearestBranch($auth['company_id'], $lat2, $lng2);
                $nearestMsg2 = $nearest2
                    ? " | أقرب فرع لك الآن: {$nearest2['name']} (يبعد " . round(((float)$nearest2['distance_meters']) / 1000, 2) . " كم)"
                    : '';
                respond(403, "عذراً، تم رفض البصمة لأنك مسجل على فرع {$homeBranchName2} وأنت الآن خارج نطاق جميع فروع الشركة{$nearestMsg2}");
            }

            $distance2 = $matchedBranch2['distance_meters'];
            $locStatus2 = 'ok';
            $matchedBranchName2 = $matchedBranch2['name'];
            
            // If in a branch but not the assigned one, add an automatic note
            if (!empty($employee2['branch_id']) && (int)$employee2['branch_id'] !== (int)$matchedBranch2['id']) {
                $branchNote = "(قام بالبصمة من فرع: " . $matchedBranch2['name'] . ")";
                $finalNotes2 = $finalNotes2 ? "$finalNotes2 $branchNote" : $branchNote;
                $homeBranchName2 = getBranchNameById($auth['company_id'], (int)$employee2['branch_id']);
                $typeForAlert2 = $d['type'] ?? 'check_in';
                notifyAdminBranchMismatch($auth['company_id'], $employee2['name'] ?? 'موظف', $homeBranchName2, $matchedBranch2['name'], $typeForAlert2);
            }
        }

        // Selfie
        $selfiePath2 = null;
        $boundAccountPhoto2 = null;
        if (!empty($d['selfie'])) {
            $selfiePath2 = saveSelfieBase64($d['selfie'], 'emp_' . $auth['id'], $auth['company_id']);
            // Bind first attendance selfie as account photo when none, cleared by admin, or legacy hidden path.
            if ($selfiePath2 && employeeNeedsAccountPhotoFromSelfie($employee2['photo_url'] ?? null)) {
                db()->prepare('UPDATE employees SET photo_url=? WHERE id=? AND company_id=?')->execute([$selfiePath2, $auth['id'], $auth['company_id']]);
                $boundAccountPhoto2 = $selfiePath2;
            }
        }

        // Type
        $type2 = $d['type'] ?? 'check_in';
        if (!in_array($type2, ['check_in','check_out'])) respond(400, 'Invalid attendance type');

        // Duplicate Check (2 minutes)
        $dup2 = db()->prepare("SELECT id FROM attendance WHERE employee_id=? AND type=? AND timestamp > DATE_SUB(NOW(), INTERVAL 2 MINUTE)");
        $dup2->execute([$auth['id'], $type2]);
        if ($dup2->fetch()) respond(429, 'جاري المعالجة.. يرجى الانتظار');

        $loggedNature2 = ($employee2['work_nature'] === 'external') ? 'external' : null;
        $visitDetails2 = $d['visit_details'] ?? null;

        db()->prepare("INSERT INTO attendance (company_id,employee_id,type,latitude,longitude,distance_meters,location_status,selfie_path,device_id,qr_token,ip_address,logged_work_nature,visit_details,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)")
            ->execute([$auth['company_id'],$auth['id'],$type2,$lat2,$lng2,$distance2,$locStatus2,$selfiePath2,$deviceId,'DIRECT',$_SERVER['REMOTE_ADDR']??'', $loggedNature2, $visitDetails2, $finalNotes2]);

        $payload2 = ['type'=>$type2,'distance_meters'=>round($distance2??0,1),'location_status'=>$locStatus2,'branch_name'=>$matchedBranchName2];
        if ($boundAccountPhoto2) {
            $payload2['photo_url'] = $boundAccountPhoto2;
        }
        respond(201, $payload2, 'Attendance recorded');

    case 'POST:attendance':
        $auth     = requireRole('employee');
        $d        = input();
        $deviceId = $_SERVER['HTTP_X_DEVICE_ID'] ?? $d['device_id'] ?? null;

        // Validate QR
        $qrToken = $d['qr_token'] ?? '';
        $stmt    = db()->prepare("SELECT * FROM qr_tokens WHERE token=? AND company_id=? AND used=0 AND expires_at > NOW()");
        $stmt->execute([$qrToken, $auth['company_id']]);
        $qr = $stmt->fetch();
        if (!$qr) respond(400, 'Invalid or expired QR code');

        // Mark QR as used
        db()->prepare("UPDATE qr_tokens SET used=1 WHERE id=?")->execute([$qr['id']]);

        // Device check
        $emp = db()->prepare("SELECT * FROM employees WHERE id=? AND company_id=?");
        $emp->execute([$auth['id'], $auth['company_id']]);
        $employee = $emp->fetch();
        if (!$employee) respond(404, 'Employee not found');

        // Smart device check (fingerprint-aware auto-update)
        $devCheckQr = smartDeviceCheck($employee, $deviceId, 'attendance');
        if (!$devCheckQr['ok']) respond(403, $devCheckQr['message']);


        // Geolocation
        $lat      = (float)($d['latitude'] ?? 0);
        $lng      = (float)($d['longitude'] ?? 0);
        $distance = null;
        $locStatus = 'ok';
        $finalNotes = $d['notes'] ?? '';
        $matchedBranchName = null;
        $gpsAccuracy = (float)($d['gps_accuracy'] ?? 0);

        if ($employee['work_nature'] === 'external') {
            $locStatus = 'ok';
            $distance = 0;
            $matchedBranch = findMatchedBranch($auth['company_id'], $lat, $lng, $gpsAccuracy, false);
            if ($matchedBranch) $matchedBranchName = $matchedBranch['name'];
        } else {
            // Strict Geofencing for Indoor (Internal)
            if (!$lat || !$lng || ($lat == 0 && $lng == 0)) {
                respond(403, 'عذراً، يجب تفعيل خدمة الموقع (GPS) للبصمة كحضور داخلي');
            }
            $matchedBranch = findAttendanceBranchForInternal(
                $auth['company_id'],
                $lat,
                $lng,
                $gpsAccuracy,
                !empty($employee['branch_id']) ? (int) $employee['branch_id'] : null
            );
            if (!$matchedBranch) {
                $homeBranchName = getBranchNameById($auth['company_id'], !empty($employee['branch_id']) ? (int)$employee['branch_id'] : null) ?? 'غير محدد';
                $nearest = findNearestBranch($auth['company_id'], $lat, $lng);
                $nearestMsg = $nearest
                    ? " | أقرب فرع لك الآن: {$nearest['name']} (يبعد " . round(((float)$nearest['distance_meters']) / 1000, 2) . " كم)"
                    : '';
                respond(403, "عذراً، تم رفض البصمة لأنك مسجل على فرع {$homeBranchName} وأنت الآن خارج نطاق جميع فروع الشركة{$nearestMsg}");
            }

            $distance = $matchedBranch['distance_meters'];
            $locStatus = 'ok';
            $matchedBranchName = $matchedBranch['name'];
            
            // If in a branch but not the assigned one, add an automatic note
            if (!empty($employee['branch_id']) && (int)$employee['branch_id'] !== (int)$matchedBranch['id']) {
                $branchNote = "(قام بالبصمة من فرع: " . $matchedBranch['name'] . ")";
                $finalNotes = $finalNotes ? "$finalNotes $branchNote" : $branchNote;
                $homeBranchName = getBranchNameById($auth['company_id'], (int)$employee['branch_id']);
                $typeForAlert = $d['type'] ?? 'check_in';
                notifyAdminBranchMismatch($auth['company_id'], $employee['name'] ?? 'موظف', $homeBranchName, $matchedBranch['name'], $typeForAlert);
            }
        }

        // Selfie
        $selfiePath = null;
        $boundAccountPhoto = null;
        if (!empty($d['selfie'])) {
            $selfiePath = saveSelfieBase64($d['selfie'], 'emp_' . $auth['id'], $auth['company_id']);
            if ($selfiePath && employeeNeedsAccountPhotoFromSelfie($employee['photo_url'] ?? null)) {
                db()->prepare('UPDATE employees SET photo_url=? WHERE id=? AND company_id=?')->execute([$selfiePath, $auth['id'], $auth['company_id']]);
                $boundAccountPhoto = $selfiePath;
            }
        }

        // Determine type
        $type = $d['type'] ?? 'check_in';
        if (!in_array($type, ['check_in','check_out'])) respond(400, 'Invalid attendance type');

        // Duplicate Check (2 minutes)
        $dup = db()->prepare("SELECT id FROM attendance WHERE employee_id=? AND type=? AND timestamp > DATE_SUB(NOW(), INTERVAL 2 MINUTE)");
        $dup->execute([$auth['id'], $type]);
        if ($dup->fetch()) respond(429, 'جاري المعالجة.. يرجى الانتظار');

        $loggedNature = ($employee['work_nature'] === 'external') ? 'external' : null;
        $visitDetails = $d['visit_details'] ?? null;

        db()->prepare("INSERT INTO attendance (company_id,employee_id,type,latitude,longitude,distance_meters,location_status,selfie_path,device_id,qr_token,ip_address,logged_work_nature,visit_details,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)")
            ->execute([$auth['company_id'],$auth['id'],$type,$lat,$lng,$distance,$locStatus,$selfiePath,$deviceId,$qrToken,$_SERVER['REMOTE_ADDR']??'', $loggedNature, $visitDetails, $finalNotes]);

        $payloadAtt = ['type'=>$type,'distance_meters'=>round($distance??0,1),'location_status'=>$locStatus,'branch_name'=>$matchedBranchName];
        if ($boundAccountPhoto) {
            $payloadAtt['photo_url'] = $boundAccountPhoto;
        }
        respond(201, $payloadAtt, 'Attendance recorded');

    case 'POST:admin/attendance/manual':
        $auth = requireRole('admin');
        $d = input();
        if (empty($d['employee_id']) || empty($d['type']) || empty($d['date']) || empty($d['time'])) {
            respond(400, 'Missing required fields');
        }
        $empId = $d['employee_id'];
        $type = $d['type'];
        if (!in_array($type, ['check_in', 'check_out'])) respond(400, 'Invalid type');
        
        $emp = db()->prepare("SELECT id FROM employees WHERE id=? AND company_id=?");
        $emp->execute([$empId, $auth['company_id']]);
        if (!$emp->fetch()) respond(404, 'Employee not found');
        
        // Manual Duplicate Check
        $dupManual = db()->prepare("SELECT id FROM attendance WHERE employee_id=? AND type=? AND DATE(timestamp) = ?");
        $dupManual->execute([$empId, $type, $d['date']]);
        if ($dupManual->fetch()) respond(409, 'هذا الموظف مسجل بالفعل لهذا التوقيت');
        
        $timestamp = $d['date'] . ' ' . $d['time'] . ':00';
        
        $selfieQuery = db()->prepare("SELECT selfie_path FROM attendance WHERE employee_id=? AND selfie_path IS NOT NULL ORDER BY timestamp DESC LIMIT 1");
        $selfieQuery->execute([$empId]);
        $lastSelfie = $selfieQuery->fetchColumn() ?: null;
        
        db()->prepare("INSERT INTO attendance (company_id, employee_id, type, timestamp, location_status, distance_meters, device_id, ip_address, selfie_path) VALUES (?, ?, ?, ?, 'ok', 0, 'manual', ?, ?)")
            ->execute([$auth['company_id'], $empId, $type, $timestamp, $_SERVER['REMOTE_ADDR'] ?? '', $lastSelfie]);
            
        respond(201, null, 'Manual attendance added successfully');

    case 'GET:attendance':
        $auth = requireRole('admin');
        $fromDef = date('Y-m-d', strtotime('-30 days'));
        $toDef = date('Y-m-d');
        $from = hrproRequireYmd($input['from'] ?? $_GET['from'] ?? null, $fromDef);
        $to = hrproRequireYmd($input['to'] ?? $_GET['to'] ?? null, $toDef);
        if ($from > $to) {
            $tmp = $from;
            $from = $to;
            $to = $tmp;
        }
        $eidRaw = $input['employee_id'] ?? $_GET['employee_id'] ?? null;
        $didRaw = $input['department_id'] ?? $_GET['department_id'] ?? null;
        $eidFilter = null;
        if (is_scalar($eidRaw) && $eidRaw !== '') {
            $es = trim((string) $eidRaw);
            if ($es !== '' && ctype_digit($es)) {
                $iv = (int) $es;
                if ($iv > 0) {
                    $eidFilter = $iv;
                }
            }
        }
        $didFilter = null;
        if (is_scalar($didRaw) && $didRaw !== '') {
            $ds = trim((string) $didRaw);
            if ($ds !== '' && ctype_digit($ds)) {
                $dv = (int) $ds;
                if ($dv > 0) {
                    $didFilter = $dv;
                }
            }
        }
        $sql  = "SELECT a.*, e.name, e.employee_code, e.job_title, e.branch_id, e.department_id, e.work_nature, d.department_name, b.name as branch_name 
                 FROM attendance a 
                 JOIN employees e ON a.employee_id=e.id 
                 LEFT JOIN departments d ON e.department_id=d.id 
                 LEFT JOIN branches b ON e.branch_id=b.id
                 WHERE a.company_id=? AND DATE(a.timestamp) BETWEEN ? AND ?";
        $params = [$auth['company_id'], $from, $to];
        if ($eidFilter !== null) {
            $sql .= ' AND a.employee_id=?';
            $params[] = $eidFilter;
        }
        if ($didFilter !== null) {
            $sql .= ' AND e.department_id=?';
            $params[] = $didFilter;
        }
        $sql .= " ORDER BY a.timestamp DESC";
        $stmt = db()->prepare($sql);
        $stmt->execute($params);
        respond(200, $stmt->fetchAll());

    case 'GET:attendance/summary':
        $auth = requireRole('admin','employee');
        $cid  = $auth['company_id'];
        if ($auth['role'] === 'employee') {
            $stmt = db()->prepare("SELECT * FROM attendance WHERE employee_id=? AND company_id=? ORDER BY timestamp DESC LIMIT 30");
            $stmt->execute([$auth['id'], $auth['company_id']]);
        } else {
            $stmt = db()->prepare("SELECT e.name, d.department_name, b.name as branch_name, DATE_FORMAT(a.timestamp, '%H:%i') AS checkin_time, a.distance_meters, a.latitude, a.longitude 
                                   FROM attendance a 
                                   JOIN employees e ON a.employee_id=e.id 
                                   LEFT JOIN departments d ON e.department_id=d.id 
                                   LEFT JOIN branches b ON e.branch_id=b.id
                                   WHERE a.company_id=? AND DATE(a.timestamp) = CURDATE() AND a.type = 'check_in' ORDER BY a.timestamp DESC");
            $stmt->execute([$cid]);
        }
        if ($auth['role'] === 'employee') {
            $logs = $stmt->fetchAll();
            $stmt_wn = db()->prepare("SELECT work_nature FROM employees WHERE id=?");
            $stmt_wn->execute([$auth['id']]);
            $wn = $stmt_wn->fetchColumn();
            respond(200, ['logs' => $logs, 'work_nature' => $wn]);
        } else {
            respond(200, $stmt->fetchAll());
        }

    case 'GET:attendance/my':
        $auth = requireRole('employee');
        $from = hrproRequireYmd($input['from'] ?? $_GET['from'] ?? null, date('Y-m-01'));
        $to = hrproRequireYmd($input['to'] ?? $_GET['to'] ?? null, date('Y-m-d'));
        if ($from > $to) {
            $tmp = $from;
            $from = $to;
            $to = $tmp;
        }
        $stmt = db()->prepare('SELECT * FROM attendance WHERE employee_id=? AND company_id=? AND DATE(timestamp) BETWEEN ? AND ? ORDER BY timestamp ASC');
        $stmt->execute([(int) $auth['id'], $auth['company_id'], $from, $to]);
        respond(200, $stmt->fetchAll());

    case 'GET:team/manager-meta':
        $auth = requireRole('employee');
        $cid = $auth['company_id'];
        $sid = (int)$auth['id'];

        $st = db()->prepare("SELECT
                t.employee_id,
                e.name,
                e.employee_code,
                e.job_title,
                d.department_name,
                b.name AS branch_name
            FROM employee_team_members t
            JOIN employees e ON e.id = t.employee_id AND e.company_id = t.company_id
            LEFT JOIN departments d ON d.id = e.department_id
            LEFT JOIN branches b ON b.id = e.branch_id
            WHERE t.company_id=? AND t.supervisor_id=?
            ORDER BY e.name");
        $st->execute([$cid, $sid]);
        $rows = $st->fetchAll();
        $ids = array_map('intval', array_column($rows, 'employee_id'));
        $members = array_map(function ($r) {
            return [
                'id' => (int)($r['employee_id'] ?? 0),
                'name' => (string)($r['name'] ?? ''),
                'employee_code' => (string)($r['employee_code'] ?? ''),
                'job_title' => (string)($r['job_title'] ?? ''),
                'department_name' => (string)($r['department_name'] ?? ''),
                'branch_name' => (string)($r['branch_name'] ?? ''),
            ];
        }, $rows);

        respond(200, [
            'has_team' => count($ids) > 0,
            'team_member_ids' => $ids,
            'team_members' => $members,
        ]);

    case 'GET:team/attendance':
        $auth = requireRole('employee');
        $cid = $auth['company_id'];
        $sid = (int)$auth['id'];

        $from = hrproRequireYmd($input['from'] ?? $_GET['from'] ?? null, date('Y-m-d', strtotime('-30 days')));
        $to = hrproRequireYmd($input['to'] ?? $_GET['to'] ?? null, date('Y-m-d'));
        if ($from > $to) {
            $tmp = $from;
            $from = $to;
            $to = $tmp;
        }
        $fromTs = $from . ' 00:00:00';
        $toExclusive = date('Y-m-d', strtotime($to . ' +1 day')) . ' 00:00:00';

        $stIds = db()->prepare("SELECT employee_id FROM employee_team_members WHERE company_id=? AND supervisor_id=? ORDER BY employee_id");
        $stIds->execute([$cid, $sid]);
        $ids = array_map('intval', array_column($stIds->fetchAll(), 'employee_id'));

        if (!count($ids)) {
            respond(200, ['logs' => [], 'team_member_ids' => []]);
        }

        $placeholders = implode(',', array_fill(0, count($ids), '?'));
        $sql = "SELECT a.*, e.name, e.employee_code, e.job_title, e.work_nature,
                       d.department_name, b.name as branch_name
                FROM attendance a
                JOIN employees e ON a.employee_id=e.id
                LEFT JOIN departments d ON e.department_id=d.id
                LEFT JOIN branches b ON e.branch_id=b.id
                WHERE a.company_id=? AND a.employee_id IN ($placeholders)
                  AND a.timestamp >= ? AND a.timestamp < ?
                ORDER BY a.timestamp DESC";

        $params = array_merge([$cid], $ids, [$fromTs, $toExclusive]);
        $stmt = db()->prepare($sql);
        $stmt->execute($params);
        respond(200, [
            'logs' => $stmt->fetchAll(),
            'team_member_ids' => $ids
        ]);

    case 'PUT:team/note':
        $auth = requireRole('employee');
        $cid = $auth['company_id'];
        $sid = (int)$auth['id'];
        $attendanceId = isset($parts[2]) ? (int)$parts[2] : 0;
        if ($attendanceId <= 0) respond(400, 'Attendance ID required');
        $d = input();
        $note = trim((string)($d['note'] ?? ''));

        $st = db()->prepare("SELECT a.id, a.employee_id
                             FROM attendance a
                             JOIN employee_team_members t
                               ON t.company_id = a.company_id
                              AND t.employee_id = a.employee_id
                             WHERE a.id=? AND a.company_id=? AND t.supervisor_id=?
                             LIMIT 1");
        $st->execute([$attendanceId, $cid, $sid]);
        $row = $st->fetch();
        if (!$row) respond(403, 'Forbidden');

        db()->prepare("UPDATE attendance SET visit_details=? WHERE id=? AND company_id=?")
            ->execute([$note, $attendanceId, $cid]);
        respond(200, [], 'Note updated');

    case 'DELETE:attendance':
        $auth = requireRole('admin', 'employee');
        $id   = $parts[1] ?? null;
        if (!$id) respond(400, 'Record ID required');

        $stmt = db()->prepare("SELECT * FROM attendance WHERE id=? AND company_id=?");
        $stmt->execute([$id, $auth['company_id']]);
        $row = $stmt->fetch();
        if (!$row) respond(404, 'Record not found');

        if ($auth['role'] === 'employee') {
            // Employee can only delete their own records within 60 seconds
            if ($row['employee_id'] != $auth['id']) respond(403, 'Unauthorized');
            $ageSeconds = time() - strtotime($row['timestamp']);
            if ($ageSeconds > 60) respond(403, 'Deletion time window (60s) expired');
        } else if ($auth['role'] === 'admin') {
            // Admin can delete any record within the current month
            $recordMonth = date('Y-m', strtotime($row['timestamp']));
            $currentMonth = date('Y-m');
            if ($recordMonth !== $currentMonth) respond(403, 'Can only delete records from the current month');
        } else {
            respond(403, 'Forbidden');
        }

        db()->prepare("DELETE FROM attendance WHERE id=?")->execute([$id]);
        $actorId = ($auth['role'] === 'admin')
            ? (string) ($auth['company_id'] ?? '')
            : (string) ($auth['id'] ?? '');
        auditLog($auth['role'], $actorId, 'ATTENDANCE_DELETE', ['id'=>$id, 'emp_id'=>$row['employee_id']], $auth['company_id']);
        respond(200, [], 'Record deleted successfully');

    case 'PUT:attendance/note':
        $auth = requireRole('admin');
        $id   = $parts[2] ?? null; // attendance/note/{id}
        if (!$id) respond(400, 'Record ID required');
        $d = input();
        
        db()->prepare("UPDATE attendance SET visit_details=? WHERE id=? AND company_id=?")
           ->execute([$d['note'] ?? '', $id, $auth['company_id']]);
        
        respond(200, [], 'Note updated');

    case 'PUT:attendance':
        $auth = requireRole('admin');
        $id = isset($parts[1]) ? (int) $parts[1] : 0;
        if ($id <= 0) {
            respond(400, 'Record ID required');
        }
        $d = input();
        $stmt = db()->prepare('SELECT * FROM attendance WHERE id = ? AND company_id = ?');
        $stmt->execute([$id, $auth['company_id']]);
        $row = $stmt->fetch();
        if (!$row) {
            respond(404, 'Record not found');
        }

        $type = $row['type'];
        if (isset($d['type']) && in_array($d['type'], ['check_in', 'check_out'], true)) {
            $type = $d['type'];
        }

        $timestampStr = null;
        if (!empty($d['timestamp'])) {
            $rawTs = trim(str_replace('T', ' ', (string) $d['timestamp']));
            if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$/', $rawTs)) {
                $timestampStr = strlen($rawTs) === 16 ? $rawTs . ':00' : $rawTs;
            } else {
                respond(400, 'Invalid timestamp');
            }
        } elseif (!empty($d['date']) && isset($d['time']) && $d['time'] !== '') {
            $timestampStr = $d['date'] . ' ' . $d['time'];
            if (strlen($timestampStr) === 16) {
                $timestampStr .= ':00';
            }
        }
        if ($timestampStr === null) {
            $timestampStr = (string) $row['timestamp'];
        }

        $dup = db()->prepare('SELECT id FROM attendance WHERE employee_id = ? AND type = ? AND DATE(timestamp) = DATE(?) AND id <> ?');
        $dup->execute([(int) $row['employee_id'], $type, $timestampStr, $id]);
        if ($dup->fetch()) {
            respond(409, 'هذا الموظف مسجل بالفعل بنفس نوع البصمة في هذا التاريخ');
        }

        $lat = $row['latitude'];
        $lng = $row['longitude'];
        if (array_key_exists('latitude', $d)) {
            $lv = $d['latitude'];
            $lat = ($lv === '' || $lv === null) ? null : (float) $lv;
        }
        if (array_key_exists('longitude', $d)) {
            $lg = $d['longitude'];
            $lng = ($lg === '' || $lg === null) ? null : (float) $lg;
        }

        $dist = $row['distance_meters'];
        if (array_key_exists('distance_meters', $d)) {
            $dm = $d['distance_meters'];
            if ($dm === '' || $dm === null) {
                $dist = null;
            } else {
                $dist = is_numeric($dm) ? round((float) $dm, 2) : null;
            }
        }

        $locStatus = $row['location_status'] ?? 'ok';
        if (!empty($d['location_status']) && in_array($d['location_status'], ['ok', 'alert'], true)) {
            $locStatus = $d['location_status'];
        }

        $notes = $row['notes'] ?? '';
        if (array_key_exists('notes', $d)) {
            $notes = $d['notes'] === null ? '' : (string) $d['notes'];
        }
        $visitDetails = $row['visit_details'] ?? '';
        if (array_key_exists('visit_details', $d)) {
            $visitDetails = $d['visit_details'] === null ? '' : (string) $d['visit_details'];
        }

        db()->prepare('UPDATE attendance SET type = ?, timestamp = ?, latitude = ?, longitude = ?, distance_meters = ?, location_status = ?, notes = ?, visit_details = ? WHERE id = ? AND company_id = ?')
            ->execute([$type, $timestampStr, $lat, $lng, $dist, $locStatus, $notes, $visitDetails, $id, $auth['company_id']]);

        $actorId = ($auth['role'] === 'admin')
            ? (string) ($auth['company_id'] ?? '')
            : (string) ($auth['id'] ?? '');
        auditLog($auth['role'], $actorId, 'ATTENDANCE_UPDATE', [
            'id' => $id,
            'employee_id' => (int) $row['employee_id'],
        ], $auth['company_id']);
        respond(200, [], 'Record updated successfully');

    case 'PUT:attendance/employee-note':
        $auth = requireRole('employee');
        $id   = $parts[2] ?? null; // attendance/employee-note/{id}
        if (!$id) respond(400, 'Record ID required');
        $d = input();
        if (!isset($d['note'])) respond(400, 'Note content required');

        // Verify ownership and date (must be today)
        $stmt = db()->prepare("SELECT timestamp FROM attendance WHERE id=? AND employee_id=? AND company_id=?");
        $stmt->execute([$id, $auth['id'], $auth['company_id']]);
        $row = $stmt->fetch();
        if (!$row) respond(404, 'Record not found or unauthorized');

        $recordDate = date('Y-m-d', strtotime($row['timestamp']));
        if ($recordDate !== date('Y-m-d')) {
            respond(403, 'لا يمكن تعديل الملاحظات إلا في نفس اليوم');
        }

        db()->prepare("UPDATE attendance SET visit_details=? WHERE id=?")
           ->execute([$d['note'], $id]);

        auditLog('employee', $auth['id'], 'ATTENDANCE_NOTE_UPDATE', ['id'=>$id], $auth['company_id']);
        respond(200, [], 'Note updated successfully');

// ═══════════════════════════════════════════════════════════════════════════════
// LEAVE REQUESTS
// ═══════════════════════════════════════════════════════════════════════════════
    case 'POST:leave':
        $auth = requireRole('admin', 'employee');
        $d    = input();
        $reqKind = strtolower(trim((string) ($d['request_kind'] ?? '')));
        if ($reqKind === 'permission') {
            foreach (['start_date', 'end_date', 'reason'] as $f) {
                if (empty($d[$f])) {
                    respond(400, "Field '$f' is required");
                }
            }
            if ((string) $d['start_date'] !== (string) $d['end_date']) {
                respond(400, 'Permission requests must use the same start and end date');
            }
            $pf = trim((string) ($d['permission_from'] ?? ''));
            $pt = trim((string) ($d['permission_to'] ?? ''));
            if (!preg_match('/^\d{2}:\d{2}$/', $pf) || !preg_match('/^\d{2}:\d{2}$/', $pt)) {
                respond(400, 'permission_from and permission_to are required as HH:MM');
            }
            $t1 = strtotime($d['start_date'] . ' ' . $pf . ':00');
            $t2 = strtotime($d['start_date'] . ' ' . $pt . ':00');
            if ($t1 === false || $t2 === false || $t2 <= $t1) {
                respond(400, 'permission_to must be after permission_from');
            }
            $d['leave_type'] = 'permission';
        } else {
            foreach (['start_date', 'end_date', 'reason', 'leave_type'] as $f) {
                if (empty($d[$f])) {
                    respond(400, "Field '$f' is required");
                }
            }
            if (!in_array($d['leave_type'], ['annual', 'casual', 'sick'], true)) {
                respond(400, 'leave_type must be annual, casual, or sick');
            }
        }
        $cid = $auth['company_id'];
        if ($auth['role'] === 'admin') {
            $targetEmpId = (int) ($d['employee_id'] ?? 0);
            if ($targetEmpId <= 0) {
                respond(400, 'employee_id is required when submitting leave as admin');
            }
            $chk = db()->prepare('SELECT id, name FROM employees WHERE id=? AND company_id=? AND is_active=1');
            $chk->execute([$targetEmpId, $cid]);
            $empRow = $chk->fetch();
            if (!$empRow) {
                respond(404, 'Employee not found');
            }
        } else {
            // Employees always submit for themselves (ignore any employee_id in body).
            $targetEmpId = (int) $auth['id'];
        }

        $permFrom = ($reqKind === 'permission') ? trim((string) ($d['permission_from'] ?? '')) : null;
        $permTo = ($reqKind === 'permission') ? trim((string) ($d['permission_to'] ?? '')) : null;

        $permissionKindVal = null;
        if ($reqKind === 'permission') {
            $pkRaw = strtolower(trim((string) ($d['permission_kind'] ?? '')));
            if ($pkRaw === 'mission') {
                $permissionKindVal = 'mission';
            } elseif ($pkRaw === '' || $pkRaw === 'standard') {
                $permissionKindVal = null;
            } else {
                respond(400, 'permission_kind must be standard or mission');
            }
            $wnStmt = db()->prepare('SELECT work_nature FROM employees WHERE id = ? AND company_id = ? LIMIT 1');
            $wnStmt->execute([$targetEmpId, $cid]);
            $wnRow = $wnStmt->fetch();
            $empWorkNature = (string) ($wnRow['work_nature'] ?? 'internal');
            if ($permissionKindVal === 'mission' && $empWorkNature !== 'internal') {
                respond(400, 'Mission requests are only allowed for internal employees');
            }
        }

        // Guard against accidental double-click submissions within the same second.
        $dupStmt = db()->prepare(
            "SELECT id
             FROM leave_requests
             WHERE company_id = ?
               AND employee_id = ?
               AND start_date = ?
               AND end_date = ?
               AND leave_type = ?
               AND reason = ?
               AND (permission_from <=> ?)
               AND (permission_to <=> ?)
               AND (permission_kind <=> ?)
               AND created_at >= (NOW() - INTERVAL 1 SECOND)
             ORDER BY id DESC
             LIMIT 1"
        );
        $dupStmt->execute([
            $cid,
            $targetEmpId,
            $d['start_date'],
            $d['end_date'],
            $d['leave_type'],
            (string) ($d['reason'] ?? ''),
            $permFrom,
            $permTo,
            $permissionKindVal,
        ]);
        if ($dupStmt->fetch()) {
            respond(409, 'Duplicate leave request detected. Please wait a moment before retrying.');
        }

        db()->prepare('INSERT INTO leave_requests (company_id,employee_id,start_date,end_date,reason,leave_type,permission_from,permission_to,permission_kind) VALUES (?,?,?,?,?,?,?,?,?)')
            ->execute([$cid, $targetEmpId, $d['start_date'], $d['end_date'], $d['reason'], $d['leave_type'], $permFrom, $permTo, $permissionKindVal]);

        // ---------------------------------------------------------------------
        // إشعارات البريد الديناميكية (Multi-tenant Email Notifications)
        // يتم الإرسال بناءً على الإعدادات الخاصة بكل شركة (company_id)
        // ---------------------------------------------------------------------
        $stmt = db()->prepare("SELECT c.notification_email, c.name AS company_name, e.name AS employee_name, e.employee_code FROM companies c JOIN employees e ON c.company_id = e.company_id WHERE c.company_id = ? AND e.id = ?");
        $stmt->execute([$cid, $targetEmpId]);
        $info = $stmt->fetch();

        $empName = $info['employee_name'] ?? 'Employee';
        $leaveSummary = $empName . "\n" . $d['start_date'] . ' — ' . $d['end_date'];
        if ($reqKind === 'permission' && $permFrom && $permTo) {
            $leaveSummary .= "\n" . $permFrom . '–' . $permTo;
        }
        $notifTitle = $reqKind === 'permission' ? 'New permission request · طلب إذن جديد' : 'New leave request · طلب إجازة جديد';
        adminNotifyInsert($cid, 'leave_request', $notifTitle, $leaveSummary, '/hr/admin.html?hrpro_pwa=admin&open_tab=leave');

        if ($info && !empty($info['notification_email']) && filter_var($info['notification_email'], FILTER_VALIDATE_EMAIL)) {
            $to = $info['notification_email'];
            [$subject, $message] = buildLeaveRequestNotifyEmail(
                (string) ($info['company_name'] ?? ''),
                (string) ($info['employee_name'] ?? ''),
                (string) ($info['employee_code'] ?? ''),
                $reqKind,
                (string) $d['start_date'],
                (string) $d['end_date'],
                (string) $d['reason'],
                $permFrom,
                $permTo,
                (string) $d['leave_type']
            );
            sendEmail($to, $subject, $message, (string) ($info['company_name'] ?? 'HR System'));
        }

        respond(201, null, 'Leave request submitted');

    case 'GET:leave':
        $auth = requireRole('admin','employee');
        if ($auth['role'] === 'admin') {
            $stmt = db()->prepare("SELECT lr.*, e.name, e.employee_code, e.phone FROM leave_requests lr JOIN employees e ON lr.employee_id=e.id WHERE lr.company_id=? ORDER BY lr.created_at DESC");
            $stmt->execute([$auth['company_id']]);
        } else {
            $stmt = db()->prepare("SELECT * FROM leave_requests WHERE company_id=? AND employee_id=? ORDER BY created_at DESC");
            $stmt->execute([$auth['company_id'], $auth['id']]);
        }
        $rows = $stmt->fetchAll();
        foreach ($rows as &$lrRow) {
            $pf = trim((string) ($lrRow['permission_from'] ?? ''));
            $pt = trim((string) ($lrRow['permission_to'] ?? ''));
            if ($pf !== '' && $pt !== '') {
                $lrRow['leave_type'] = 'permission';
            }
        }
        unset($lrRow);
        respond(200, $rows);

    case 'GET:leave/balance':
        $auth = requireRole('admin');
        ensureLeavePolicyPermissionLimitColumn(db());
        $eid  = $parts[2] ?? null; // leave/balance/{id}
        if (!$eid) respond(400, 'Employee ID required');
        $in = input();

        $stmtEmp = db()->prepare('SELECT id, policy_id FROM employees WHERE id = ? AND company_id = ?');
        $stmtEmp->execute([(int) $eid, $auth['company_id']]);
        $empRow = $stmtEmp->fetch();
        if (!$empRow) {
            respond(404, 'Employee not found');
        }

        $policy = null;
        $policyId = (int) ($empRow['policy_id'] ?? 0);
        if ($policyId > 0) {
            $stmt = db()->prepare('SELECT lp.* FROM leave_policies lp WHERE lp.id = ? AND lp.company_id = ?');
            $stmt->execute([$policyId, $auth['company_id']]);
            $policy = $stmt->fetch();
        }
        $policyMissing = !$policy;
        if ($policyMissing) {
            // No policy linked: return usable payload so admin can still review requests.
            // Entitlements default to 0; weekend Sat–Sun only for net-day estimate (assign a policy for accuracy).
            $policy = [
                'policy_name' => null,
                'weekend_days' => '["Saturday","Sunday"]',
                'annual_leave_days' => 0,
                'casual_leave_days' => 0,
                'sick_leave_days' => 0,
                'deduct_full_day' => 1.0,
                'deduct_half_day' => 0.5,
                'deduct_quarter_day' => 0.25,
                'permission_monthly_limit' => null,
            ];
        }

        $year = (int) ($in['year'] ?? $_GET['year'] ?? date('Y'));
        if ($year < 1990 || $year > 2120) {
            $year = (int) date('Y');
        }
        $yearStart = sprintf('%04d-01-01', $year);
        $yearEnd = sprintf('%04d-12-31', $year);
        $pdo = db();
        $approved = leaveApprovedUsedInRange($pdo, (int) $eid, $auth['company_id'], $yearStart, $yearEnd);
        $manual = leaveManualUsedInRange($pdo, (int) $eid, $auth['company_id'], $yearStart, $yearEnd);

        $balances = [];
        foreach (['annual', 'casual', 'sick'] as $k) {
            $tot = (int) ($policy[$k . '_leave_days'] ?? 0);
            $uA = round($approved[$k], 2);
            $uM = round($manual[$k], 2);
            $u = round($uA + $uM, 2);
            $balances[$k] = [
                'total' => $tot,
                'used_approved' => $uA,
                'used_manual' => $uM,
                'used' => $u,
                'rem' => round($tot - $u, 2),
            ];
        }

        $permRefRaw = trim((string) ($in['permission_date'] ?? $_GET['permission_date'] ?? ''));
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $permRefRaw)) {
            $permRefRaw = date('Y-m-d');
        }
        $permMonth = (int) substr($permRefRaw, 5, 2);
        $permYear = (int) substr($permRefRaw, 0, 4);
        if ($permMonth < 1 || $permMonth > 12 || $permYear < 1990 || $permYear > 2120) {
            $permRefRaw = date('Y-m-d');
            $permMonth = (int) substr($permRefRaw, 5, 2);
            $permYear = (int) substr($permRefRaw, 0, 4);
        }
        $permMonthStart = sprintf('%04d-%02d-01', $permYear, $permMonth);
        $permMonthEnd = date('Y-m-t', strtotime($permMonthStart));
        // Monthly permission quota counts standard permissions only — missions are normal work and do not consume the allowance.
        $permUsedStmt = $pdo->prepare("SELECT COUNT(*) FROM leave_requests WHERE company_id = ? AND employee_id = ? AND status = 'approved' AND leave_type = 'permission' AND start_date <= ? AND end_date >= ? AND (permission_kind IS NULL OR LOWER(TRIM(permission_kind)) <> 'mission')");
        $permUsedStmt->execute([$auth['company_id'], (int) $eid, $permMonthEnd, $permMonthStart]);
        $permUsed = (int) ($permUsedStmt->fetchColumn() ?: 0);
        $permLimitRaw = $policy['permission_monthly_limit'] ?? null;
        $permLimit = ($permLimitRaw === null || $permLimitRaw === '') ? null : max(0, (int) $permLimitRaw);
        $permRemaining = $permLimit === null ? null : max(0, $permLimit - $permUsed);

        respond(200, [
            'year' => $year,
            'balances' => $balances,
            'policy_missing' => $policyMissing,
            'permission_limit' => $permLimit,
            'permission_used_month' => $permUsed,
            'permission_remaining_month' => $permRemaining,
            'permission_ref_month' => sprintf('%04d-%02d', $permYear, $permMonth),
            'policy'   => [
                'name' => $policy['policy_name'] ?? null,
                'weekend_days' => json_decode($policy['weekend_days'] ?? '[]', true),
                'deduct_options' => policyDeductionFractionOptions($policy),
                'permission_monthly_limit' => $permLimit,
            ]
        ]);

    case 'PUT:leave':
        $auth = requireRole('admin');
        $lid  = $parts[1] ?? null;
        if (!$lid) respond(400, 'Leave request ID required');
        $d = input();
        
        // Fetch current record to check dates and company
        $stmt = db()->prepare("SELECT * FROM leave_requests WHERE id=? AND company_id=?");
        $stmt->execute([$lid, $auth['company_id']]);
        $lr = $stmt->fetch();
        if (!$lr) respond(404, 'Leave request not found');

        // 3-month rule applies when changing dates / type / reason — not when only approving/rejecting (status + note).
        $editingCore = isset($d['start_date']) || isset($d['end_date']) || isset($d['leave_type']) || isset($d['reason']);
        if ($editingCore) {
            $startDate = $lr['start_date'];
            $firstDayOfAllowedMonth = date('Y-m-01', strtotime('-2 months'));
            if ($startDate < $firstDayOfAllowedMonth) {
                respond(403, 'عفواً، لا يمكن تعديل الطلبات التي مر عليها أكثر من 3 أشهر');
            }
        }

        // Full Edit Support
        $leaveType = $d['leave_type'] ?? $lr['leave_type'];
        $start = $d['start_date'] ?? $lr['start_date'];
        $end = $d['end_date'] ?? $lr['end_date'];
        $status = $d['status'] ?? $lr['status'];
        $reason = $d['reason'] ?? ($lr['reason'] ?? '');
        $adminNote = $d['admin_note'] ?? ($lr['admin_note'] ?? null);
        $permFrom = array_key_exists('permission_from', $d) ? trim((string) ($d['permission_from'] ?? '')) : trim((string) ($lr['permission_from'] ?? ''));
        $permTo = array_key_exists('permission_to', $d) ? trim((string) ($d['permission_to'] ?? '')) : trim((string) ($lr['permission_to'] ?? ''));

        $permKindDb = null;
        if ($leaveType === 'permission') {
            if (!preg_match('/^\d{2}:\d{2}$/', $permFrom) || !preg_match('/^\d{2}:\d{2}$/', $permTo)) {
                respond(400, 'permission_from and permission_to are required as HH:MM');
            }
            if ((string) $start !== (string) $end) {
                respond(400, 'Permission requests must use the same start and end date');
            }
            $t1 = strtotime($start . ' ' . $permFrom . ':00');
            $t2 = strtotime($start . ' ' . $permTo . ':00');
            if ($t1 === false || $t2 === false || $t2 <= $t1) {
                respond(400, 'permission_to must be after permission_from');
            }
            if (array_key_exists('permission_kind', $d)) {
                $pkRaw = strtolower(trim((string) ($d['permission_kind'] ?? '')));
                if ($pkRaw === 'mission') {
                    $permKindDb = 'mission';
                } elseif ($pkRaw === '' || $pkRaw === 'standard') {
                    $permKindDb = null;
                } else {
                    respond(400, 'permission_kind must be standard or mission');
                }
            } else {
                $oldPk = $lr['permission_kind'] ?? null;
                $permKindDb = (is_string($oldPk) && strtolower($oldPk) === 'mission') ? 'mission' : null;
            }
            $wnPut = db()->prepare('SELECT work_nature FROM employees WHERE id = ? AND company_id = ? LIMIT 1');
            $wnPut->execute([(int) $lr['employee_id'], $auth['company_id']]);
            $wnPutRow = $wnPut->fetch();
            $empWnPut = (string) ($wnPutRow['work_nature'] ?? 'internal');
            if ($permKindDb === 'mission' && $empWnPut !== 'internal') {
                respond(400, 'Mission requests are only allowed for internal employees');
            }
        } else {
            $permFrom = '';
            $permTo = '';
        }
        
        try {
            db()->prepare('UPDATE leave_requests SET leave_type=?, start_date=?, end_date=?, status=?, reason=?, admin_note=?, permission_from=?, permission_to=?, permission_kind=?, reviewed_at=NOW() WHERE id=?')
                ->execute([$leaveType, $start, $end, $status, $reason, $adminNote, ($permFrom !== '' ? $permFrom : null), ($permTo !== '' ? $permTo : null), ($leaveType === 'permission' ? $permKindDb : null), $lid]);
        } catch (PDOException $e) {
            error_log('PUT leave ' . $lid . ': ' . $e->getMessage());
            respond(400, 'تعذر تحديث الطلب — تحقق من قاعدة البيانات (عمود leave_type يجب أن يدعم قيمة permission). / Could not update leave request (leave_type column must allow permission).');
        }
            
        // Company admin JWT has no numeric user id — use company_id like other admin auditLog calls.
        auditLog('admin', (string) $auth['company_id'], 'LEAVE_EDIT', ['id' => $lid, 'emp_id' => $lr['employee_id']], $auth['company_id']);
        respond(200, [], 'Leave request updated successfully');

    case 'DELETE:leave':
        $auth = requireRole('admin');
        $lid = $parts[1] ?? null;
        if (!$lid) respond(400, 'Leave request ID required');

        $stmt = db()->prepare("SELECT id, employee_id, status FROM leave_requests WHERE id=? AND company_id=?");
        $stmt->execute([$lid, $auth['company_id']]);
        $lr = $stmt->fetch();
        if (!$lr) respond(404, 'Leave request not found');
        if (($lr['status'] ?? '') !== 'pending') {
            respond(400, 'Only pending leave requests can be deleted');
        }

        db()->prepare('DELETE FROM leave_requests WHERE id=? AND company_id=?')->execute([$lid, $auth['company_id']]);
        auditLog('admin', (string) $auth['company_id'], 'LEAVE_DELETE', ['id' => $lid, 'emp_id' => $lr['employee_id']], $auth['company_id']);
        respond(200, ['id' => (int) $lid], 'Leave request deleted successfully');

// ═══════════════════════════════════════════════════════════════════════════════
// REPORTS – CSV Export (UTF-8 BOM for Arabic support)
// ═══════════════════════════════════════════════════════════════════════════════
    case 'GET:reports/export':
        $auth = requireRole('admin');
        $from = trim((string) ($_GET['from'] ?? ''));
        $to   = trim((string) ($_GET['to'] ?? ''));
        if ($from === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) {
            $from = date('Y-m-01');
        }
        if ($to === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
            $to = date('Y-m-d');
        }
        if ($from > $to) {
            [$from, $to] = [$to, $from];
        }
        $didRaw = $_GET['department_id'] ?? null;
        $did = null;
        if ($didRaw !== null && $didRaw !== '' && strtolower((string) $didRaw) !== 'all') {
            $did = ctype_digit((string) $didRaw) ? (int) $didRaw : null;
        }
        $sql  = "SELECT a.id, a.type, a.timestamp,
                        a.selfie_path, a.device_id, a.qr_token, a.ip_address, a.notes, a.visit_details,
                        a.logged_work_nature, a.lateness_discount, a.created_at AS record_created_at,
                        e.name AS emp_name, e.employee_code, e.job_title, e.work_nature,
                        d.department_name, b.name AS emp_branch_name, bb.name AS att_branch_name
                 FROM attendance a
                 JOIN employees e ON a.employee_id=e.id
                 LEFT JOIN departments d ON e.department_id=d.id
                 LEFT JOIN branches b ON e.branch_id=b.id
                 LEFT JOIN branches bb ON a.branch_id=bb.id AND bb.company_id=a.company_id
                 WHERE a.company_id=? AND DATE(a.timestamp) BETWEEN ? AND ?";
        $params = [$auth['company_id'], $from, $to];
        if ($did !== null && $did > 0) {
            $sql .= " AND e.department_id=?";
            $params[] = $did;
        }
        $sql .= " ORDER BY a.timestamp DESC";
        $stmt = db()->prepare($sql);
        $stmt->execute($params);
        $rows = $stmt->fetchAll();

        $utf16 = hrproCsvCanEmitUtf16Le();
        header('Content-Type: text/csv; charset=' . ($utf16 ? 'UTF-16LE' : 'UTF-8'));
        header('Content-Disposition: attachment; filename="attendance_' . date('Ymd') . '.csv"');
        $out = fopen('php://output', 'wb');
        if ($utf16) {
            fwrite($out, "\xFF\xFE");
            $sepLine = hrproUtf8ToUtf16Le("sep=;\r\n");
            if ($sepLine !== null) {
                fwrite($out, $sepLine);
            }
        } else {
            fwrite($out, "\xEF\xBB\xBF");
            fwrite($out, "sep=;\r\n");
        }
        hrproCsvWriteRowExcel($out, [
            'رقم السجل',
            'اسم الموظف',
            'كود الموظف',
            'المسمى الوظيفي',
            'طبيعة العمل',
            'فرع الموظف',
            'القسم',
            'فرع التسجيل',
            'نوع الحركة',
            'التاريخ والوقت',
            'التقط صورة',
            'ملاحظات',
            'تفاصيل الزيارة',
            'طبيعة العمل المسجّلة',
            'خصم التأخير',
            'معرّف الجهاز',
            'عنوان IP',
            'طريقة التحقق',
            'وقت التسجيل في النظام',
        ], $utf16);
        foreach ($rows as $r) {
            $wn = ($r['work_nature'] ?? '') === 'external' ? 'خارجي' : 'داخلي';
            $qt = (string) ($r['qr_token'] ?? '');
            if (($r['device_id'] ?? '') === 'manual') {
                $verify = 'تسجيل يدوي';
            } elseif ($qt === 'DIRECT') {
                $verify = 'مباشر';
            } elseif ($qt !== '') {
                $verify = 'رمز QR';
            } else {
                $verify = '—';
            }
            hrproCsvWriteRowExcel($out, [
                (string) ($r['id'] ?? ''),
                $r['emp_name'] ?? '',
                $r['employee_code'] ?? '',
                $r['job_title'] ?? '',
                $wn,
                $r['emp_branch_name'] ?? 'غير محدد',
                $r['department_name'] ?? 'غير محدد',
                $r['att_branch_name'] ?? '',
                ($r['type'] ?? '') === 'check_in' ? 'دخول' : 'خروج',
                (string) ($r['timestamp'] ?? ''),
                hrproCsvSelfiePresent($r['selfie_path'] ?? null),
                (string) ($r['notes'] ?? ''),
                (string) ($r['visit_details'] ?? ''),
                (string) ($r['logged_work_nature'] ?? ''),
                $r['lateness_discount'] !== null && $r['lateness_discount'] !== '' ? (string) $r['lateness_discount'] : '',
                (string) ($r['device_id'] ?? ''),
                (string) ($r['ip_address'] ?? ''),
                $verify,
                (string) ($r['record_created_at'] ?? ''),
            ], $utf16);
        }
        fclose($out);
        exit;

// ═══════════════════════════════════════════════════════════════════════════════
// DASHBOARD STATS
// ═══════════════════════════════════════════════════════════════════════════════
    case 'GET:dashboard/stats':
        $auth = requireRole('admin');
        $cid  = $auth['company_id'];
        $in = input();
        $dayRaw = trim((string) ($in['date'] ?? ''));
        if ($dayRaw === '') {
            $dayRaw = date('Y-m-d');
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dayRaw)) {
            respond(400, 'Invalid date');
        }
        $today = date('Y-m-d');
        if ($dayRaw > $today) {
            $dayRaw = $today;
        }
        $d = DateTimeImmutable::createFromFormat('Y-m-d', $dayRaw);
        if (!$d || $d->format('Y-m-d') !== $dayRaw) {
            respond(400, 'Invalid date');
        }
        $min = (new DateTimeImmutable('today'))->modify('-400 days');
        if ($d < $min) {
            $dayRaw = $min->format('Y-m-d');
        }

        $data = [];
        $q = function($sql, $params) { $s = db()->prepare($sql); $s->execute($params); return $s->fetchColumn(); };
        $cid_arr = [$cid];
        $data['total_employees']   = $q("SELECT COUNT(*) FROM employees WHERE company_id=? AND is_active=1", $cid_arr);
        $data['checkins_today']    = $q("SELECT COUNT(DISTINCT employee_id) FROM attendance WHERE company_id=? AND type='check_in' AND DATE(timestamp)=?", array_merge($cid_arr, [$dayRaw]));
        $data['alerts_today']      = $q("SELECT COUNT(*) FROM attendance WHERE company_id=? AND location_status='alert' AND DATE(timestamp)=?", array_merge($cid_arr, [$dayRaw]));
        $data['pending_leaves']    = $q("SELECT COUNT(*) FROM leave_requests WHERE company_id=? AND status='pending'", $cid_arr);
        $data['stats_date']        = $dayRaw;
        respond(200, $data);

    case 'GET:admin/branches-activity':
        $auth = requireRole('admin');
        $cid = $auth['company_id'];
        $in = input();
        $dayRaw = trim((string) ($in['date'] ?? ''));
        if ($dayRaw === '') {
            $dayRaw = date('Y-m-d');
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dayRaw)) {
            respond(400, 'Invalid date');
        }
        $today = date('Y-m-d');
        if ($dayRaw > $today) {
            $dayRaw = $today;
        }
        $d = DateTimeImmutable::createFromFormat('Y-m-d', $dayRaw);
        if (!$d || $d->format('Y-m-d') !== $dayRaw) {
            respond(400, 'Invalid date');
        }
        $min = (new DateTimeImmutable('today'))->modify('-400 days');
        if ($d < $min) {
            $dayRaw = $min->format('Y-m-d');
        }

        // Get branches
        $stmtB = db()->prepare("SELECT id, name FROM branches WHERE company_id = ? ORDER BY name");
        $stmtB->execute([$cid]);
        $branches = $stmtB->fetchAll();

        // Get employees with activity on the selected day (Attendance or Approved Leave)
        $stmtE = db()->prepare("
            SELECT 
                e.id, e.name, e.job_title, e.branch_id, e.photo_url,
                a.timestamp as checkin_time,
                lr.reason as leave_reason,
                lr.status as leave_status
            FROM employees e
            LEFT JOIN (
                SELECT employee_id, MIN(timestamp) as timestamp 
                FROM attendance 
                WHERE company_id = ? AND DATE(timestamp) = ? AND type = 'check_in' 
                GROUP BY employee_id
            ) a ON a.employee_id = e.id
            LEFT JOIN (
                SELECT employee_id, reason, status 
                FROM leave_requests 
                WHERE company_id = ? AND status = 'approved' AND leave_type IN ('annual','casual','sick') AND ? BETWEEN start_date AND end_date
            ) lr ON lr.employee_id = e.id
            WHERE e.company_id = ? AND e.is_active = 1
            ORDER BY e.name
        ");
        $stmtE->execute([$cid, $dayRaw, $cid, $dayRaw, $cid]);
        $allEmployees = $stmtE->fetchAll();

        $employeesByBranch = [];
        foreach ($allEmployees as $emp) {
            $bid = $emp['branch_id'] ?: 0;
            if (!isset($employeesByBranch[$bid])) {
                $employeesByBranch[$bid] = [];
            }
            $emp['account_photo_url'] = employeeAccountPhotoUrl($emp['photo_url'] ?? null);
            $employeesByBranch[$bid][] = $emp;
        }

        $result = [];
        foreach ($branches as $b) {
            $bid = $b['id'];
            $branchEmps = $employeesByBranch[$bid] ?? [];
            $attendanceCount = 0;
            foreach ($branchEmps as $emp) {
                if ($emp['checkin_time']) {
                    $attendanceCount++;
                }
            }
            $result[] = [
                'id' => $bid,
                'name' => $b['name'],
                'attendance_count' => $attendanceCount,
                'is_active' => $attendanceCount > 0,
                'employees' => $branchEmps
            ];
        }

        $unassigned = null;
        if (!empty($employeesByBranch[0])) {
            $list = $employeesByBranch[0];
            $attendanceCount = 0;
            foreach ($list as $emp) {
                if ($emp['checkin_time']) {
                    $attendanceCount++;
                }
            }
            $unassigned = [
                'count' => count($list),
                'employees' => $list,
                'attendance_count' => $attendanceCount,
            ];
        }

        respond(200, [
            'branches' => $result,
            'unassigned' => $unassigned,
            'activity_date' => $dayRaw,
        ]);

    case 'GET:admin/contacts':
        $auth = requireRole('admin');
        $cid = $auth['company_id'] ?? null;
        if (!$cid) {
            respond(403, 'Forbidden');
        }
        $stmt = db()->prepare("
            SELECT
                e.id,
                e.name,
                e.phone,
                e.email,
                e.job_title,
                e.photo_url,
                e.is_active,
                d.department_name,
                b.name AS branch_name
            FROM employees e
            LEFT JOIN departments d ON e.department_id = d.id AND d.company_id = e.company_id
            LEFT JOIN branches b ON e.branch_id = b.id AND b.company_id = e.company_id
            WHERE e.company_id = ?
              AND e.is_active = 1
            ORDER BY e.name ASC
            LIMIT 5000
        ");
        $stmt->execute([$cid]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        foreach ($rows as &$row) {
            $row['account_photo_url'] = employeeAccountPhotoUrl($row['photo_url'] ?? null);
        }
        unset($row);
        respond(200, $rows);

    case 'GET:admin/branch-monitor-detail':
        $auth = requireRole('admin');
        $cid = $auth['company_id'];
        $in = input();
        $branchId = (int) ($in['branch_id'] ?? 0);
        if ($branchId <= 0) {
            respond(400, 'branch_id is required');
        }
        $dayRaw = trim((string) ($in['date'] ?? ''));
        if ($dayRaw === '') {
            $dayRaw = date('Y-m-d');
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dayRaw)) {
            respond(400, 'Invalid date');
        }

        $pdo = db();
        $bStmt = $pdo->prepare('SELECT id, name, latitude, longitude, radius, photo_url FROM branches WHERE id=? AND company_id=?');
        $bStmt->execute([$branchId, $cid]);
        $branch = $bStmt->fetch();
        if (!$branch) {
            respond(404, 'Branch not found');
        }

        $empStmt = $pdo->prepare("
            SELECT e.id, e.name, e.employee_code, e.job_title, e.photo_url, e.department_id,
                   COALESCE(d.department_name, '') AS department_name,
                   COALESCE(d.is_active, 1) AS dept_is_active
            FROM employees e
            LEFT JOIN departments d ON d.id = e.department_id AND d.company_id = e.company_id
            WHERE e.company_id = ? AND e.branch_id = ? AND e.is_active = 1
            ORDER BY e.name ASC
        ");
        $empStmt->execute([$cid, $branchId]);
        $emps = $empStmt->fetchAll();

        $empIds = array_map(static function ($row) {
            return (int) ($row['id'] ?? 0);
        }, $emps);
        $reqByEmp = branchMonitorRequestsForDay($pdo, (string) $cid, $dayRaw, $empIds);

        $attStmt = $pdo->prepare("
            SELECT employee_id,
                MIN(CASE WHEN type = 'check_in' THEN timestamp END) AS check_in,
                MAX(CASE WHEN type = 'check_out' THEN timestamp END) AS check_out
            FROM attendance
            WHERE company_id = ? AND DATE(timestamp) = ?
            GROUP BY employee_id
        ");
        $attStmt->execute([$cid, $dayRaw]);
        $attByEmp = [];
        foreach ($attStmt->fetchAll() as $ar) {
            $attByEmp[(int) $ar['employee_id']] = $ar;
        }

        $employeesOut = [];
        foreach ($emps as $e) {
            $aid = (int) $e['id'];
            $ci = $attByEmp[$aid]['check_in'] ?? null;
            $co = $attByEmp[$aid]['check_out'] ?? null;
            $employeesOut[] = [
                'id' => $aid,
                'name' => $e['name'],
                'employee_code' => $e['employee_code'],
                'job_title' => $e['job_title'],
                'photo_url' => $e['photo_url'],
                'account_photo_url' => employeeAccountPhotoUrl($e['photo_url'] ?? null),
                'check_in' => $ci,
                'check_out' => $co,
                'requests_today' => $reqByEmp[$aid] ?? [],
            ];
        }

        respond(200, [
            'branch' => $branch,
            'date' => $dayRaw,
            'employees' => $employeesOut,
            'departments' => [],
        ]);

    case 'GET:super/stats':
        requireRole('super_admin');
        $pdo = db();
        $scalar = static function (string $sql, array $params = []) use ($pdo) {
            $st = $pdo->prepare($sql);
            $st->execute($params);

            return (int) $st->fetchColumn();
        };
        $totalCompanies = $scalar('SELECT COUNT(*) FROM companies');
        $activeCompanies = $scalar("SELECT COUNT(*) FROM companies WHERE subscription_status='active'");
        $trialCompanies = $scalar("SELECT COUNT(*) FROM companies WHERE subscription_status='trial'");
        $suspendedCompanies = $scalar("SELECT COUNT(*) FROM companies WHERE subscription_status='suspended'");
        $totalEmployees = $scalar('SELECT COUNT(*) FROM employees');
        $activeEmployees = $scalar('SELECT COUNT(*) FROM employees WHERE is_active=1');
        $inactiveEmployees = $scalar('SELECT COUNT(*) FROM employees WHERE is_active=0');
        $internalEmployees = $scalar("SELECT COUNT(*) FROM employees WHERE work_nature='internal'");
        $externalEmployees = $scalar("SELECT COUNT(*) FROM employees WHERE work_nature='external'");
        $checkinsToday = $scalar("SELECT COUNT(*) FROM attendance WHERE type='check_in' AND DATE(timestamp)=CURDATE()");
        $checkoutsToday = $scalar("SELECT COUNT(*) FROM attendance WHERE type='check_out' AND DATE(timestamp)=CURDATE()");
        $attRows7d = $scalar('SELECT COUNT(*) FROM attendance WHERE timestamp >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)');
        $leavePendingAll = $scalar("SELECT COUNT(*) FROM leave_requests WHERE status='pending'");
        $leaveApprovedAll = $scalar("SELECT COUNT(*) FROM leave_requests WHERE status='approved'");
        $leaveRejectedAll = $scalar("SELECT COUNT(*) FROM leave_requests WHERE status='rejected'");
        $unreadMessages = $scalar('SELECT COUNT(*) FROM admin_messages WHERE is_read=0');

        $sqlCompanies = "
            SELECT
                c.company_id,
                c.name,
                c.admin_username,
                c.subscription_status,
                c.max_employees,
                c.timezone,
                c.created_at,
                (SELECT COUNT(*) FROM employees e WHERE e.company_id = c.company_id) AS employees_total,
                (SELECT COUNT(*) FROM employees e WHERE e.company_id = c.company_id AND e.is_active = 1) AS employees_active,
                (SELECT COUNT(*) FROM employees e WHERE e.company_id = c.company_id AND e.is_active = 0) AS employees_inactive,
                (SELECT COUNT(*) FROM employees e WHERE e.company_id = c.company_id AND e.work_nature = 'internal') AS employees_internal,
                (SELECT COUNT(*) FROM employees e WHERE e.company_id = c.company_id AND e.work_nature = 'external') AS employees_external,
                (SELECT COUNT(*) FROM employees e WHERE e.company_id = c.company_id AND e.photo_url IS NOT NULL AND e.photo_url <> '') AS employees_with_photo,
                (SELECT COUNT(*) FROM employees e WHERE e.company_id = c.company_id AND e.device_id IS NOT NULL AND e.device_id <> '') AS employees_device_bound,
                (SELECT COUNT(*) FROM departments d WHERE d.company_id = c.company_id) AS departments_count,
                (SELECT COUNT(*) FROM branches b WHERE b.company_id = c.company_id) AS branches_count,
                (SELECT COUNT(*) FROM leave_policies lp WHERE lp.company_id = c.company_id) AS leave_policies_count,
                (SELECT COUNT(*) FROM attendance a WHERE a.company_id = c.company_id) AS attendance_records_total,
                (SELECT COUNT(*) FROM attendance a WHERE a.company_id = c.company_id AND a.type = 'check_in' AND DATE(a.timestamp) = CURDATE()) AS checkins_today,
                (SELECT COUNT(*) FROM attendance a WHERE a.company_id = c.company_id AND a.type = 'check_out' AND DATE(a.timestamp) = CURDATE()) AS checkouts_today,
                (SELECT COUNT(*) FROM attendance a WHERE a.company_id = c.company_id AND a.timestamp >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)) AS attendance_rows_last_7d,
                (SELECT COUNT(*) FROM attendance a WHERE a.company_id = c.company_id AND a.timestamp >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)) AS attendance_rows_last_30d,
                (SELECT COUNT(*) FROM attendance a WHERE a.company_id = c.company_id AND a.location_status = 'alert' AND a.timestamp >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)) AS location_alerts_last_30d,
                (SELECT COUNT(*) FROM leave_requests lr WHERE lr.company_id = c.company_id AND lr.status = 'pending') AS leave_pending,
                (SELECT COUNT(*) FROM leave_requests lr WHERE lr.company_id = c.company_id AND lr.status = 'approved') AS leave_approved,
                (SELECT COUNT(*) FROM leave_requests lr WHERE lr.company_id = c.company_id AND lr.status = 'rejected') AS leave_rejected,
                (SELECT COUNT(*) FROM leave_requests lr WHERE lr.company_id = c.company_id AND lr.created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)) AS leave_requests_last_30d,
                (SELECT COUNT(*) FROM admin_messages m WHERE m.company_id = c.company_id) AS admin_messages_total,
                (SELECT COUNT(*) FROM admin_messages m WHERE m.company_id = c.company_id AND m.is_read = 0) AS admin_messages_unread,
                (SELECT COUNT(*) FROM qr_tokens q WHERE q.company_id = c.company_id AND q.used = 0 AND q.expires_at > NOW()) AS qr_tokens_active,
                (SELECT COUNT(*) FROM audit_log al WHERE al.company_id = c.company_id AND al.created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)) AS audit_events_last_30d
            FROM companies c
            ORDER BY c.name ASC
        ";
        $byCompany = $pdo->query($sqlCompanies)->fetchAll(PDO::FETCH_ASSOC);
        foreach ($byCompany as &$row) {
            $cid = (string) ($row['company_id'] ?? '');
            $row['leave_manual_deductions'] = 0;
            if ($cid !== '') {
                try {
                    $stL = $pdo->prepare('SELECT COUNT(*) FROM leave_manual_deductions WHERE company_id = ?');
                    $stL->execute([$cid]);
                    $row['leave_manual_deductions'] = (int) $stL->fetchColumn();
                } catch (Throwable $e) {
                    /* table missing in very old DB */
                }
            }
            try {
                $stN = $pdo->prepare('SELECT COUNT(*) FROM admin_notifications WHERE company_id = ?');
                $stN->execute([$cid]);
                $row['admin_notifications_total'] = (int) $stN->fetchColumn();
                $stN2 = $pdo->prepare('SELECT COUNT(*) FROM admin_notifications WHERE company_id = ? AND is_read = 0');
                $stN2->execute([$cid]);
                $row['admin_notifications_unread'] = (int) $stN2->fetchColumn();
            } catch (Throwable $e) {
                $row['admin_notifications_total'] = 0;
                $row['admin_notifications_unread'] = 0;
            }
            try {
                $stNews = $pdo->prepare('SELECT COUNT(*) FROM company_news_ticker WHERE company_id = ? AND is_active = 1');
                $stNews->execute([$cid]);
                $row['company_news_active'] = (int) $stNews->fetchColumn();
            } catch (Throwable $e) {
                $row['company_news_active'] = 0;
            }
        }
        unset($row);

        respond(200, [
            'total_companies' => $totalCompanies,
            'active_companies' => $activeCompanies,
            'trial_companies' => $trialCompanies,
            'suspended_companies' => $suspendedCompanies,
            'total_employees' => $totalEmployees,
            'active_employees' => $activeEmployees,
            'inactive_employees' => $inactiveEmployees,
            'internal_employees' => $internalEmployees,
            'external_employees' => $externalEmployees,
            'checkins_today' => $checkinsToday,
            'checkouts_today' => $checkoutsToday,
            'attendance_rows_last_7d' => $attRows7d,
            'leave_pending_total' => $leavePendingAll,
            'leave_approved_total' => $leaveApprovedAll,
            'leave_rejected_total' => $leaveRejectedAll,
            'admin_messages_unread_total' => $unreadMessages,
            'by_company' => $byCompany,
        ]);

    case 'GET:super/trial-requests':
        try {
            requireRole('super_admin');
            $rows = db()->query('SELECT id, company_name, phone, email, ip_address, user_agent, created_at FROM trial_requests ORDER BY created_at DESC LIMIT 500')->fetchAll(PDO::FETCH_ASSOC);
            respond(200, ['rows' => $rows]);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'GET:super/login-insights':
        requireRole('super_admin');
        $d = input();
        $companyId = isset($d['company_id']) ? trim((string) $d['company_id']) : '';
        $companyId = $companyId !== '' ? $companyId : null;
        $pdo = db();
        if ($companyId !== null) {
            $chk = $pdo->prepare('SELECT 1 FROM companies WHERE company_id = ? LIMIT 1');
            $chk->execute([$companyId]);
            if (!$chk->fetchColumn()) {
                respond(404, 'Company not found');
            }
        }
        $limit = 500;
        $paramsA = [];
        $sqlA = 'SELECT al.id, al.company_id, UNIX_TIMESTAMP(al.created_at) AS created_at_unix, al.ip_address, c.name AS company_name, c.admin_username
            FROM audit_log al
            LEFT JOIN companies c ON c.company_id = al.company_id
            WHERE al.actor_type = \'admin\' AND al.action = \'LOGIN\'';
        if ($companyId !== null) {
            $sqlA .= ' AND al.company_id = ?';
            $paramsA[] = $companyId;
        }
        $sqlA .= ' ORDER BY al.created_at DESC LIMIT ' . $limit;
        $stA = $pdo->prepare($sqlA);
        $stA->execute($paramsA);
        $adminRows = $stA->fetchAll(PDO::FETCH_ASSOC);

        $paramsE = [];
        $sqlE = 'SELECT al.id, al.company_id, UNIX_TIMESTAMP(al.created_at) AS created_at_unix, al.ip_address, al.actor_id AS employee_id, al.details,
            e.name AS employee_name, e.username AS employee_username, e.employee_code,
            c.name AS company_name
            FROM audit_log al
            LEFT JOIN employees e ON e.company_id = al.company_id AND e.id = CAST(al.actor_id AS UNSIGNED)
            LEFT JOIN companies c ON c.company_id = al.company_id
            WHERE al.actor_type = \'employee\' AND al.action = \'LOGIN\'';
        if ($companyId !== null) {
            $sqlE .= ' AND al.company_id = ?';
            $paramsE[] = $companyId;
        }
        $sqlE .= ' ORDER BY al.created_at DESC LIMIT ' . $limit;
        $stE = $pdo->prepare($sqlE);
        $stE->execute($paramsE);
        $empRows = $stE->fetchAll(PDO::FETCH_ASSOC);
        foreach ($empRows as &$er) {
            if (isset($er['details']) && is_string($er['details'])) {
                $decoded = json_decode($er['details'], true);
                $er['details'] = is_array($decoded) ? $decoded : null;
            }
        }
        unset($er);

        foreach ($adminRows as &$ar) {
            $ts = isset($ar['created_at_unix']) ? (int) $ar['created_at_unix'] : 0;
            $ar['created_at'] = hrproUnixTimestampToCairo($ts);
            unset($ar['created_at_unix']);
        }
        unset($ar);
        foreach ($empRows as &$er) {
            $ts = isset($er['created_at_unix']) ? (int) $er['created_at_unix'] : 0;
            $er['created_at'] = hrproUnixTimestampToCairo($ts);
            unset($er['created_at_unix']);
        }
        unset($er);

        respond(200, [
            'company_id' => $companyId,
            'admin_logins' => $adminRows,
            'employee_logins' => $empRows,
        ]);

    case 'POST:super/purge-login-logs':
        try {
            $auth = requireRole('super_admin');
            $d = input();
            $companyId = isset($d['company_id']) ? trim((string) $d['company_id']) : '';
            $companyId = $companyId !== '' ? $companyId : null;
            $pdo = db();
            if ($companyId !== null) {
                $chk = $pdo->prepare('SELECT 1 FROM companies WHERE company_id = ? LIMIT 1');
                $chk->execute([$companyId]);
                if (!$chk->fetchColumn()) {
                    respond(404, 'Company not found');
                }
            }
            if ($companyId === null) {
                $del = $pdo->prepare("DELETE FROM audit_log WHERE action = 'LOGIN' AND actor_type IN ('admin','employee')");
                $del->execute();
            } else {
                $del = $pdo->prepare("DELETE FROM audit_log WHERE action = 'LOGIN' AND actor_type IN ('admin','employee') AND company_id = ?");
                $del->execute([$companyId]);
            }
            $deleted = $del->rowCount();
            auditLog('super_admin', (string) $auth['id'], 'PURGE_LOGIN_LOGS', [
                'scope' => $companyId === null ? 'all_companies' : 'company',
                'company_id' => $companyId,
                'deleted_rows' => $deleted,
            ], null);
            respond(200, ['deleted' => $deleted], $companyId === null ? 'تم مسح سجلات دخول المدراء والموظفين لكل الشركات.' : 'تم مسح سجلات الدخول لهذه الشركة.');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:super/change-password':
        try {
            $auth = requireRole('super_admin');
            $d = input();
            $old = (string) ($d['old_password'] ?? '');
            $new = (string) ($d['new_password'] ?? '');
            if ($old === '' || $new === '') {
                respond(400, 'Old and new passwords are required');
            }
            $err = validateStrongPassword($new);
            if ($err !== null) {
                respond(400, $err);
            }
            $stmt = db()->prepare('SELECT id, password FROM super_admins WHERE id = ?');
            $stmt->execute([(int) $auth['id']]);
            $row = $stmt->fetch();
            if (!$row || !password_verify($old, (string) $row['password'])) {
                respond(400, 'Current password is incorrect');
            }
            $hash = password_hash($new, PASSWORD_BCRYPT, ['cost' => 12]);
            db()->prepare('UPDATE super_admins SET password = ? WHERE id = ?')->execute([$hash, (int) $auth['id']]);
            auditLog('super_admin', (string) $auth['id'], 'SUPER_CHANGE_PASSWORD', [], null);
            respond(200, [], 'Password updated successfully');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:super/notification-test':
        try {
            requireRole('super_admin');
            $d = input();
            $cid = trim((string) ($d['company_id'] ?? ''));
            if ($cid === '') {
                respond(400, 'company_id is required');
            }
            $st = db()->prepare('SELECT name, notification_email FROM companies WHERE company_id=?');
            $st->execute([$cid]);
            $co = $st->fetch();
            if (!$co) {
                respond(404, 'Company not found');
            }
            $to = trim((string) ($co['notification_email'] ?? ''));
            if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
                respond(400, 'No valid notification_email configured for this company. Set it in company settings first.');
            }
            [$subject, $body] = buildLeaveRequestNotifyEmail(
                (string) ($co['name'] ?? ''),
                'موظف تجريبي (اختبار)',
                'TEST-0000',
                'leave',
                date('Y-m-d'),
                date('Y-m-d'),
                "هذه رسالة اختبار من Super Admin للتأكد من وصول بريد تنبيهات الإجازات.\nلم يُنشأ أي طلب إجازة فعلي.",
                null,
                null,
                'annual'
            );
            $subject = '[اختبار / Test] ' . $subject;
            $ok = sendEmail($to, $subject, $body, (string) ($co['name'] ?? 'HR System'));
            if ($ok) {
                respond(200, [], 'Test email sent to notification inbox.');
            }
            respond(500, 'Mail transport returned failure (check SMTP / server logs).');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'GET:super/smtp-settings':
        try {
            requireRole('super_admin');
            $cfg = hrproGetEffectiveSmtpConfig();
            $row = null;
            try {
                $st = db()->prepare('SELECT smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password_blob, from_email, from_name, reply_to_email FROM system_smtp_settings WHERE id = 1');
                $st->execute();
                $row = $st->fetch(PDO::FETCH_ASSOC);
            } catch (Throwable $e) {
                $row = null;
            }
            $pwBlob = is_array($row) ? trim((string) ($row['smtp_password_blob'] ?? '')) : '';
            respond(200, [
                'smtp_host' => is_array($row) ? trim((string) ($row['smtp_host'] ?? '')) : '',
                'smtp_port' => is_array($row) ? (int) ($row['smtp_port'] ?? 587) : 587,
                'smtp_encryption' => is_array($row) ? trim((string) ($row['smtp_encryption'] ?? 'tls')) : 'tls',
                'smtp_username' => is_array($row) ? trim((string) ($row['smtp_username'] ?? '')) : '',
                'from_email' => is_array($row) ? trim((string) ($row['from_email'] ?? '')) : '',
                'from_name' => is_array($row) ? trim((string) ($row['from_name'] ?? 'HR Pro')) : 'HR Pro',
                'reply_to_email' => is_array($row) ? trim((string) ($row['reply_to_email'] ?? '')) : '',
                'password_set' => $pwBlob !== '',
                'effective_source' => $cfg['source'],
            ]);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'PUT:super/smtp-settings':
        try {
            $auth = requireRole('super_admin');
            $d = input();
            $host = trim((string) ($d['smtp_host'] ?? ''));
            $port = (int) ($d['smtp_port'] ?? 587);
            $enc = strtolower(trim((string) ($d['smtp_encryption'] ?? 'tls')));
            $user = trim((string) ($d['smtp_username'] ?? ''));
            $passIn = array_key_exists('smtp_password', $d) ? (string) $d['smtp_password'] : null;
            $from = trim((string) ($d['from_email'] ?? ''));
            $fromName = trim((string) ($d['from_name'] ?? 'HR Pro')) ?: 'HR Pro';
            $replyTo = trim((string) ($d['reply_to_email'] ?? ''));
            if ($host === '') {
                respond(400, 'smtp_host is required');
            }
            if ($port < 1 || $port > 65535) {
                respond(400, 'smtp_port must be between 1 and 65535');
            }
            if (!in_array($enc, ['tls', 'ssl', 'none'], true)) {
                respond(400, 'smtp_encryption must be tls, ssl, or none');
            }
            if ($from === '' || !filter_var($from, FILTER_VALIDATE_EMAIL)) {
                respond(400, 'from_email must be a valid email address');
            }
            if ($replyTo !== '' && !filter_var($replyTo, FILTER_VALIDATE_EMAIL)) {
                respond(400, 'reply_to_email must be empty or a valid email');
            }
            $pdo = db();
            $cur = $pdo->prepare('SELECT smtp_password_blob FROM system_smtp_settings WHERE id = 1');
            $cur->execute();
            $existing = $cur->fetch(PDO::FETCH_ASSOC);
            $existingPw = is_array($existing) ? (string) ($existing['smtp_password_blob'] ?? '') : '';
            $newPwBlob = $existingPw;
            if ($passIn !== null) {
                $passIn = trim($passIn);
                if ($passIn !== '') {
                    $newPwBlob = hrproEncryptSmtpPassword($passIn);
                }
            }
            $replySql = $replyTo === '' ? null : $replyTo;
            $up = $pdo->prepare('UPDATE system_smtp_settings SET smtp_host=?, smtp_port=?, smtp_encryption=?, smtp_username=?, smtp_password_blob=?, from_email=?, from_name=?, reply_to_email=? WHERE id=1');
            $up->execute([$host, $port, $enc, $user, $newPwBlob, $from, $fromName, $replySql]);
            auditLog('super_admin', (string) $auth['id'], 'SMTP_SETTINGS_UPDATE', ['host' => $host, 'port' => $port, 'encryption' => $enc], null);
            respond(200, [], 'SMTP settings saved. Outbound mail uses this account for all companies; each company still receives leave alerts at its own notification email.');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:super/smtp-test':
        try {
            requireRole('super_admin');
            $d = input();
            $to = trim((string) ($d['to'] ?? ''));
            if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
                respond(400, 'Valid "to" email is required');
            }
            $cfg = hrproGetEffectiveSmtpConfig();
            if ($cfg['host'] === '') {
                respond(400, 'SMTP is not configured (missing host). Save settings in Master or define SMTP_* in config.');
            }
            $subj = '[HR Pro] SMTP test message';
            $body = "This is a test email from HR Pro Master.\n\nEffective transport: {$cfg['source']}\nHost: {$cfg['host']}:{$cfg['port']} ({$cfg['encryption']})\nFrom: {$cfg['from_email']}\n\nIf this landed in spam, align SPF/DKIM/DMARC for your sending domain and use a consistent From address.";
            $result = sendEmailDebug($to, $subj, $body, 'HR Pro');
            $status = $result['ok'] ? 'sent' : 'failed';
            $errDetail = $result['ok'] ? null : $result['error'];
            try {
                $ins = db()->prepare(
                    'INSERT INTO smtp_test_log (recipient, status, error_detail, smtp_host, smtp_port, smtp_encryption)
                     VALUES (?, ?, ?, ?, ?, ?)'
                );
                $ins->execute([$to, $status, $errDetail, $cfg['host'], (int) $cfg['port'], $cfg['encryption']]);
            } catch (Throwable $le) { /* log insert failure is non-fatal */ }
            if ($result['ok']) {
                respond(200, [], 'Test message sent.');
            }
            $msg = 'Mail transport returned failure';
            if ($result['error'] !== '') {
                $msg .= ': ' . $result['error'];
            }
            respond(500, $msg);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'GET:super/smtp-test-log':
        try {
            requireRole('super_admin');
            $rows = [];
            try {
                $st = db()->query(
                    'SELECT id, tested_at, recipient, status, error_detail, smtp_host, smtp_port, smtp_encryption
                     FROM smtp_test_log ORDER BY id DESC LIMIT 20'
                );
                $rows = $st->fetchAll(PDO::FETCH_ASSOC);
            } catch (Throwable $e) {
                $rows = [];
            }
            respond(200, $rows);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'GET:super/webpush-settings':
        try {
            requireRole('super_admin');
            $row = null;
            try {
                $st = db()->prepare('SELECT vapid_public_key, vapid_private_blob, vapid_subject FROM system_webpush_settings WHERE id = 1');
                $st->execute();
                $row = $st->fetch(PDO::FETCH_ASSOC);
            } catch (Throwable $e) {
                $row = null;
            }
            $privBlob = is_array($row) ? trim((string) ($row['vapid_private_blob'] ?? '')) : '';
            $cfg = hrproGetEffectiveVapidConfig();
            respond(200, [
                'vapid_public_key' => is_array($row) ? trim((string) ($row['vapid_public_key'] ?? '')) : '',
                'vapid_subject' => is_array($row) ? trim((string) ($row['vapid_subject'] ?? '')) : '',
                'private_key_set' => $privBlob !== '',
                'composer_installed' => hrproWebPushVendorAvailable(),
                'keys_effective' => hrproVapidKeysPresent(),
                'effective_source' => hrproVapidKeysPresent() ? $cfg['source'] : null,
            ]);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'PUT:super/webpush-settings':
        try {
            $auth = requireRole('super_admin');
            $d = input();
            $pub = trim((string) ($d['vapid_public_key'] ?? ''));
            $subj = trim((string) ($d['vapid_subject'] ?? ''));
            $privIn = array_key_exists('vapid_private_key', $d) ? (string) $d['vapid_private_key'] : null;
            if ($pub === '') {
                respond(400, 'vapid_public_key is required');
            }
            if (strlen($pub) < 40) {
                respond(400, 'vapid_public_key looks too short (paste the full key from web-push generate-vapid-keys)');
            }
            if ($subj === '') {
                $subj = 'mailto:admin@localhost';
            }
            if (stripos($subj, 'mailto:') !== 0 && stripos($subj, 'https://') !== 0) {
                respond(400, 'vapid_subject must start with mailto: or https:// (Web Push standard)');
            }
            if (strlen($subj) > 250) {
                respond(400, 'vapid_subject is too long');
            }
            $pdo = db();
            $cur = $pdo->prepare('SELECT vapid_private_blob FROM system_webpush_settings WHERE id = 1');
            $cur->execute();
            $existing = $cur->fetch(PDO::FETCH_ASSOC);
            $existingBlob = is_array($existing) ? (string) ($existing['vapid_private_blob'] ?? '') : '';
            $newBlob = $existingBlob;
            if ($privIn !== null) {
                $privIn = trim($privIn);
                if ($privIn !== '') {
                    if (strlen($privIn) < 20) {
                        respond(400, 'vapid_private_key looks too short');
                    }
                    $newBlob = hrproEncryptSmtpPassword($privIn);
                }
            }
            if (trim($pub) !== '' && $newBlob === '') {
                respond(400, 'vapid_private_key is required on first setup (or paste private key when changing public key)');
            }
            $up = $pdo->prepare('UPDATE system_webpush_settings SET vapid_public_key=?, vapid_private_blob=?, vapid_subject=? WHERE id=1');
            $up->execute([$pub, $newBlob, $subj]);
            auditLog('super_admin', (string) $auth['id'], 'WEBPUSH_SETTINGS_UPDATE', ['vapid_source' => 'database'], null);
            respond(200, [], 'Web Push VAPID keys saved. Install PHP dependencies on the server (composer install in /hr) so notifications can be delivered.');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:super/seed-demo':
        try {
            requireRole('super_admin');
            $pdo = db();
            $chk = $pdo->prepare("SELECT company_id FROM companies WHERE admin_username = 'demo' LIMIT 1");
            $chk->execute();
            $existing = $chk->fetch();
            if ($existing) {
                respond(200, ['company_id' => $existing['company_id'], 'already_exists' => true], 'Demo company already present');
            }
            $cid = generateUUID();
            $demoHash = password_hash('demo', PASSWORD_BCRYPT, ['cost' => 12]);
            $pdo->beginTransaction();
            $pdo->prepare('INSERT INTO companies (company_id, name, admin_username, admin_password, subscription_status, max_employees, language, timezone) VALUES (?,?,?,?,?,?,?,?)')
                ->execute([$cid, 'demo', 'demo', $demoHash, 'active', 80, 'ar', 'Africa/Cairo']);
            $pdo->prepare('INSERT INTO departments (company_id, department_name) VALUES (?,?)')->execute([$cid, 'Sales']);
            $deptSales = (int) $pdo->lastInsertId();
            $pdo->prepare('INSERT INTO departments (company_id, department_name) VALUES (?,?)')->execute([$cid, 'Engineering']);
            $deptEng = (int) $pdo->lastInsertId();
            $pdo->prepare('INSERT INTO branches (company_id, name, latitude, longitude, radius) VALUES (?,?,?,?,?)')
                ->execute([$cid, 'Main Office', 30.0444, 31.2357, 200]);
            $branchId = (int) $pdo->lastInsertId();
            $pdo->prepare('INSERT INTO leave_policies (company_id, policy_name, weekend_days, annual_leave_days, casual_leave_days, sick_leave_days, start_time, end_time, deduct_full_day, deduct_half_day, deduct_quarter_day, salary_deduct_enabled, salary_deduct_allow_with_balance) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)')
                ->execute([$cid, 'Default', '["5","6"]', 21, 7, 15, '09:00:00', '17:00:00', 1.0, 0.5, 0.25, 1, 0]);
            $policyId = (int) $pdo->lastInsertId();
            $empPass = password_hash('demo', PASSWORD_BCRYPT, ['cost' => 12]);
            $employees = [
                ['DEMO001', 'Ahmed Hassan', 'ahmed.demo', 1, 'internal', $deptSales, 'HR Specialist', '01001001001'],
                ['DEMO002', 'Sara Ali', 'sara.demo', 1, 'internal', $deptEng, 'Software Engineer', '01001001002'],
                ['DEMO003', 'Omar Khaled', 'omar.demo', 1, 'external', $deptSales, 'Sales Executive', '01001001003'],
                ['DEMO004', 'Layla Mahmoud', 'layla.demo', 0, 'internal', $deptEng, 'QA Engineer', '01001001004'],
                ['DEMO005', 'Youssef Nabil', 'youssef.demo', 1, 'internal', $deptSales, 'Account Manager', '01001001005'],
                ['DEMO006', 'Nour El-Din', 'nour.demo', 1, 'external', null, 'Field Technician', '01001001006'],
                ['DEMO007', 'Mariam Fathy', 'mariam.demo', 0, 'internal', $deptEng, 'Product Designer', '01001001007'],
                ['DEMO008', 'Karim Adel', 'karim.demo', 1, 'internal', $deptEng, 'DevOps Engineer', '01001001008'],
                ['DEMO009', 'Hana Sameh', 'hana.demo', 1, 'internal', null, 'Intern', '01001001009'],
                ['DEMO010', 'Tarek Ibrahim', 'tarek.demo', 1, 'external', $deptSales, 'Business Analyst', '01001001010'],
            ];
            $ins = $pdo->prepare('INSERT INTO employees (company_id, department_id, branch_id, policy_id, work_nature, employee_code, name, national_id, phone, email, job_title, salary, hire_date, birth_date, address, username, password, photo_url, must_change_password, is_active) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,?)');
            $empIds = [];
            foreach ($employees as $em) {
                [$code, $name, $user, $active, $wn, $deptId, $job, $phone] = $em;
                $email = $user . '@demo.local';
                $ins->execute([$cid, $deptId ?: null, $branchId, $policyId, $wn, $code, $name, null, $phone, $email, $job, 5000 + random_int(0, 4000), date('Y-m-d', strtotime('-' . random_int(3, 36) . ' months')), null, null, $user, $empPass, null, $active]);
                $empIds[] = (int) $pdo->lastInsertId();
            }
            $today = date('Y-m-d');
            $y = date('Y-m-d', strtotime('-1 day'));
            $att = $pdo->prepare("INSERT INTO attendance (company_id, employee_id, type, timestamp, latitude, longitude, distance_meters, location_status, device_id) VALUES (?,?,?,?,?,?,?,?,?)");
            foreach ([$empIds[0], $empIds[1], $empIds[2], $empIds[4]] as $eid) {
                $att->execute([$cid, $eid, 'check_in', $y . ' 09:05:00', 30.0444, 31.2357, 12, 'ok', 'SEED-DEVICE']);
                $att->execute([$cid, $eid, 'check_out', $y . ' 17:10:00', 30.0445, 31.2358, 20, 'ok', 'SEED-DEVICE']);
            }
            $att->execute([$cid, $empIds[0], 'check_in', $today . ' 09:02:00', 30.0444, 31.2357, 8, 'ok', 'SEED-DEVICE']);
            $lr = $pdo->prepare('INSERT INTO leave_requests (company_id, employee_id, start_date, end_date, reason, leave_type, status, reviewed_at, permission_from, permission_to, permission_kind) VALUES (?,?,?,?,?,?,?,?,?,?,?)');
            $lr->execute([$cid, $empIds[3], $today, $today, 'Medical', 'sick', 'approved', date('Y-m-d H:i:s'), null, null, null]);
            $lr->execute([$cid, $empIds[5], date('Y-m-d', strtotime('+3 days')), date('Y-m-d', strtotime('+5 days')), 'Family', 'annual', 'pending', null, null, null, null]);
            $lr->execute([$cid, $empIds[7], date('Y-m-d', strtotime('-10 days')), date('Y-m-d', strtotime('-8 days')), 'Personal', 'casual', 'rejected', date('Y-m-d H:i:s'), null, null, null]);
            try {
                $pdo->prepare('INSERT INTO leave_manual_deductions (company_id, employee_id, deduction_date, leave_type, days, reason) VALUES (?,?,?,?,?,?)')
                    ->execute([$cid, $empIds[1], $y, 'casual', 0.5, 'Seed: half day']);
            } catch (Throwable $e) { /* ignore */ }
            $pdo->commit();
            auditLog('super_admin', '0', 'SEED_DEMO_COMPANY', ['company_id' => $cid], null);
            respond(201, ['company_id' => $cid], 'Demo company created with 10 employees (admin & employees password: demo)');
        } catch (Exception $e) {
            try {
                if (isset($pdo) && $pdo->inTransaction()) {
                    $pdo->rollBack();
                }
            } catch (Throwable $t) { /* ignore */
            }
            respond(500, 'Seed failed: ' . $e->getMessage());
        }

    case 'GET:super/backup':
        requireRole('super_admin');
        
        $pdo = db();
        $tables = [];
        $stmt = $pdo->query("SHOW TABLES");
        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
            $tables[] = $row[0];
        }
        
        $output = "-- HR Pro Database Backup\n";
        $output .= "-- Generated on: " . date('Y-m-d H:i:s') . "\n\n";
        $output .= "SET NAMES utf8mb4;\n";
        $output .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
        
        foreach ($tables as $table) {
            // Structure
            $stmt = $pdo->query("SHOW CREATE TABLE `$table` ");
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            $output .= "\n-- Structure for table: $table\n";
            $output .= "DROP TABLE IF EXISTS `$table`;\n";
            $output .= $row['Create Table'] . ";\n\n";
            
            // Data
            $stmt = $pdo->query("SELECT * FROM `$table` ");
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
            
            if (count($rows) > 0) {
                $output .= "-- Data for table: $table\n";
                foreach ($rows as $r) {
                    $keys = array_keys($r);
                    $values = array_values($r);
                    
                    $escaped_values = array_map(function($v) use ($pdo) {
                        if ($v === null) return 'NULL';
                        return $pdo->quote($v);
                    }, $values);
                    
                    $output .= "INSERT INTO `$table` (`" . implode("`, `", $keys) . "`) VALUES (" . implode(", ", $escaped_values) . ");\n";
                }
            }
            $output .= "\n";
        }
        
        $output .= "SET FOREIGN_KEY_CHECKS = 1;\n";
        
        $filename = "HR_Backup_" . date('Y_m_d') . ".sql";
        header('Content-Type: application/sql; charset=UTF-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        echo $output;
        exit;

// ═══════════════════════════════════════════════════════════════════════════════
// MESSAGES (ADMIN TO EMPLOYEE)
// ═══════════════════════════════════════════════════════════════════════════════
    case 'POST:messages':
        $auth = requireRole('admin');
        $d = input();
        if (empty($d['employee_id']) || empty($d['message'])) respond(400, 'Missing fields');
        db()->prepare("INSERT INTO admin_messages (company_id, employee_id, message) VALUES (?, ?, ?)")
            ->execute([$auth['company_id'], $d['employee_id'], $d['message']]);
        $preview = mb_substr((string) $d['message'], 0, 140);
        employeeSendWebPushToEmployee(
            $auth['company_id'],
            (int) $d['employee_id'],
            'HR Pro',
            $preview !== '' ? $preview : 'New message from admin',
            '/hr/login.html?hrpro_pwa=employee&open_tab=messages'
        );
        respond(200, [], 'Message sent successfully');

    case 'GET:messages':
        $auth = requireRole('employee');
        $pdo = db();
        $hasDel = adminMessagesEnsureDeletedColumn($pdo);
        if ($hasDel) {
            $stmt = $pdo->prepare('SELECT id, message, is_read, is_deleted, created_at FROM admin_messages WHERE employee_id=? AND company_id=? ORDER BY created_at DESC');
        } else {
            $stmt = $pdo->prepare('SELECT id, message, is_read, created_at FROM admin_messages WHERE employee_id=? AND company_id=? ORDER BY created_at DESC');
        }
        $stmt->execute([$auth['id'], $auth['company_id']]);
        $rows = $stmt->fetchAll();
        if (!$hasDel) {
            foreach ($rows as &$row) {
                $row['is_deleted'] = 0;
            }
            unset($row);
        }
        respond(200, $rows);

    case 'PUT:messages/read':
        $auth = requireRole('employee');
        $mid = $parts[1] ?? null; // messages/{id}/read
        if (!$mid) respond(400, 'Message ID required');
        $pdo = db();
        $hasDel = adminMessagesEnsureDeletedColumn($pdo);
        if ($hasDel) {
            $pdo->prepare('UPDATE admin_messages SET is_read=1 WHERE id=? AND employee_id=? AND company_id=? AND is_deleted=0')
                ->execute([$mid, $auth['id'], $auth['company_id']]);
        } else {
            $pdo->prepare('UPDATE admin_messages SET is_read=1 WHERE id=? AND employee_id=? AND company_id=?')
                ->execute([$mid, $auth['id'], $auth['company_id']]);
        }
        respond(200, [], 'Message marked as read');

    case 'PUT:messages/mark-read':
        $auth = requireRole('employee');
        $pdo = db();
        $hasDel = adminMessagesEnsureDeletedColumn($pdo);
        $d = input();
        $cid = $auth['company_id'];
        $eid = (int) $auth['id'];
        if (!empty($d['all'])) {
            if ($hasDel) {
                $pdo->prepare('UPDATE admin_messages SET is_read=1 WHERE employee_id=? AND company_id=? AND is_deleted=0 AND is_read=0')
                    ->execute([$eid, $cid]);
            } else {
                $pdo->prepare('UPDATE admin_messages SET is_read=1 WHERE employee_id=? AND company_id=? AND is_read=0')
                    ->execute([$eid, $cid]);
            }
        } elseif (!empty($d['ids']) && is_array($d['ids'])) {
            $upd = $hasDel
                ? $pdo->prepare('UPDATE admin_messages SET is_read=1 WHERE id=? AND employee_id=? AND company_id=? AND is_deleted=0')
                : $pdo->prepare('UPDATE admin_messages SET is_read=1 WHERE id=? AND employee_id=? AND company_id=?');
            foreach ($d['ids'] as $rawId) {
                $id = (int) $rawId;
                if ($id > 0) {
                    $upd->execute([$id, $eid, $cid]);
                }
            }
        } else {
            respond(400, 'Provide all: true or ids: []');
        }
        respond(200, [], 'Messages updated');

    case 'PUT:messages/archive':
        $auth = requireRole('employee');
        $pdo = db();
        $hasDel = adminMessagesEnsureDeletedColumn($pdo);
        if (!$hasDel) {
            respond(503, 'تعذّر الأرشفة: يُرجى تحديث قاعدة البيانات (عمود is_deleted في admin_messages). / Archive unavailable: add is_deleted to admin_messages.');
        }
        $d = input();
        $cid = $auth['company_id'];
        $eid = (int) $auth['id'];
        if (!empty($d['all'])) {
            $pdo->prepare('UPDATE admin_messages SET is_deleted=1 WHERE employee_id=? AND company_id=? AND is_deleted=0')
                ->execute([$eid, $cid]);
        } elseif (!empty($d['ids']) && is_array($d['ids'])) {
            $upd = $pdo->prepare('UPDATE admin_messages SET is_deleted=1 WHERE id=? AND employee_id=? AND company_id=? AND is_deleted=0');
            foreach ($d['ids'] as $rawId) {
                $id = (int) $rawId;
                if ($id > 0) {
                    $upd->execute([$id, $eid, $cid]);
                }
            }
        } else {
            respond(400, 'Provide all: true or ids: []');
        }
        respond(200, [], 'Messages archived');

    case 'DELETE:messages/purge':
    case 'PUT:messages/purge':
        $auth = requireRole('employee');
        $pdo = db();
        $hasDel = adminMessagesEnsureDeletedColumn($pdo);
        if (!$hasDel) {
            respond(503, 'تعذّر الحذف النهائي: يُرجى تحديث قاعدة البيانات (عمود is_deleted في admin_messages). / Purge unavailable: add is_deleted to admin_messages.');
        }
        $d = input();
        $cid = $auth['company_id'];
        $eid = (int) $auth['id'];
        if (!empty($d['all'])) {
            $pdo->prepare('DELETE FROM admin_messages WHERE employee_id=? AND company_id=? AND is_deleted=1')
                ->execute([$eid, $cid]);
        } elseif (!empty($d['ids']) && is_array($d['ids'])) {
            $del = $pdo->prepare('DELETE FROM admin_messages WHERE id=? AND employee_id=? AND company_id=? AND is_deleted=1');
            foreach ($d['ids'] as $rawId) {
                $id = (int) $rawId;
                if ($id > 0) {
                    $del->execute([$id, $eid, $cid]);
                }
            }
        } else {
            respond(400, 'Provide all: true or ids: []');
        }
        respond(200, [], 'Archived messages deleted permanently');

// ═══════════════════════════════════════════════════════════════════════════════
// USER PREFERENCES
// ═══════════════════════════════════════════════════════════════════════════════
    case 'POST:user/language':
        $auth = getAuth(); // allows any authenticated user
        $d = input();
        $lang = in_array($d['language'] ?? '', ['ar','en']) ? $d['language'] : 'ar';
        
        if ($auth['role'] === 'employee') {
            db()->prepare("UPDATE employees SET language=? WHERE id=?")->execute([$lang, $auth['id']]);
        } elseif ($auth['role'] === 'admin') {
            db()->prepare("UPDATE companies SET language=? WHERE company_id=?")->execute([$lang, $auth['company_id']]);
        } elseif ($auth['role'] === 'super_admin') {
            db()->prepare("UPDATE super_admins SET language=? WHERE id=?")->execute([$lang, $auth['id']]);
        }
        respond(200, [], 'Language updated successfully');

    case 'POST:reports/final-link-token':
        $auth = requireRole('admin');
        $d = input();
        $eid  = (int)($d['employee_id'] ?? 0);
        $from = trim((string)($d['from'] ?? ''));
        $to   = trim((string)($d['to'] ?? ''));
        $hide = trim((string)($d['hide'] ?? 'all'));
        if ($eid <= 0) respond(400, 'Employee ID required');
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $from) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
            respond(400, 'Invalid date range');
        }
        $employee = hrproFetchEmployeeForFinalReport($eid, $auth['company_id']);
        if (!$employee) respond(404, 'Employee not found');
        if (!in_array($hide, ['all', 'hide_weekend', 'hide_official', 'hide_both'], true)) {
            $hide = 'all';
        }
        $ttl = 600; // 10 minutes
        $token = generateToken([
            'scope' => 'final_report_link',
            'company_id' => (string)$auth['company_id'],
            'employee_id' => $eid,
            'from' => $from,
            'to' => $to,
            'hide' => $hide,
            'link_exp' => time() + $ttl
        ]);
        respond(200, ['token' => $token, 'expires_in' => $ttl]);

    case 'POST:reports/final-link-resolve':
        $auth = requireRole('admin');
        $d = input();
        $token = (string)($d['token'] ?? '');
        if ($token === '') respond(400, 'Token required');
        $payload = verifyToken($token);
        if (!$payload) respond(401, 'Invalid or expired link token');
        if (($payload['scope'] ?? '') !== 'final_report_link') respond(403, 'Invalid token scope');
        if ((string)($payload['company_id'] ?? '') !== (string)($auth['company_id'] ?? '')) respond(403, 'Forbidden');
        if ((int)($payload['link_exp'] ?? 0) < time()) respond(401, 'Link token expired');
        respond(200, [
            'employee_id' => (int)($payload['employee_id'] ?? 0),
            'from' => (string)($payload['from'] ?? ''),
            'to' => (string)($payload['to'] ?? ''),
            'hide' => (string)($payload['hide'] ?? 'all')
        ]);

    case 'GET:reports/final':
        try {
            $auth = requireRole('admin');
            $eid  = $input['employee_id'] ?? $_GET['employee_id'] ?? null;
            $from = $input['from'] ?? $_GET['from'] ?? date('Y-m-01');
            $to   = $input['to']   ?? $_GET['to']   ?? date('Y-m-t');

            if (!$eid) respond(400, 'Employee ID required');

            // 1. Get Employee, Policy, Department, Branch
            $employee = hrproFetchEmployeeForFinalReport((int) $eid, $auth['company_id']);
            if (!$employee) respond(404, 'Employee not found');

            $policyRowForOpts = [
                'deduct_full_day' => $employee['deduct_full_day'] ?? null,
                'deduct_half_day' => $employee['deduct_half_day'] ?? null,
                'deduct_quarter_day' => $employee['deduct_quarter_day'] ?? null,
            ];
            $deductOptions = policyDeductionFractionOptions($policyRowForOpts);

            $policy = [
                'name' => $employee['policy_name'] ?? 'Default',
                'weekend_days' => json_decode($employee['weekend_days'] ?? '["Friday"]', true),
                'annual' => (int)($employee['annual_leave_days'] ?? 0),
                'casual' => (int)($employee['casual_leave_days'] ?? 0),
                'sick' => (int)($employee['sick_leave_days'] ?? 0),
                'start_time' => $employee['start_time'] ?? '09:00:00',
                'end_time' => $employee['end_time'] ?? '17:00:00',
                'deduct_full_day' => policyDeductionDecimal($employee['deduct_full_day'] ?? null, 1.0),
                'deduct_half_day' => policyDeductionDecimal($employee['deduct_half_day'] ?? null, 0.5),
                'deduct_quarter_day' => policyDeductionDecimal($employee['deduct_quarter_day'] ?? null, 0.25),
                'deduct_options' => $deductOptions,
                'salary_deduct_enabled' => (int) ($employee['salary_deduct_enabled'] ?? 1),
                'salary_deduct_allow_with_balance' => (int) ($employee['salary_deduct_allow_with_balance'] ?? 0),
            ];
            $policy = array_merge($policy, hrproFinalReportPolicyGraceFromEmployee($employee));

            // 2. Leave balances for the calendar year of the report period (renewal per year)
            $balanceYear = (int) substr((string) $from, 0, 4);
            if ($balanceYear < 1990 || $balanceYear > 2120) {
                $balanceYear = (int) date('Y');
            }
            $yearStart = sprintf('%04d-01-01', $balanceYear);
            $yearEnd = sprintf('%04d-12-31', $balanceYear);
            $pdo = db();

            // Collect all deduction fraction options across every policy in this company
            $stmtAllPol = $pdo->prepare(
                "SELECT deduct_full_day, deduct_half_day, deduct_quarter_day FROM leave_policies WHERE company_id = ?"
            );
            $stmtAllPol->execute([$auth['company_id']]);
            $allFractionsSet = [];
            while ($pr = $stmtAllPol->fetch(PDO::FETCH_ASSOC)) {
                foreach (policyDeductionFractionOptions($pr) as $f) {
                    $allFractionsSet[(string) round($f, 2)] = round($f, 2);
                }
            }
            // Always guarantee the three standard fractions
            foreach ([0.25, 0.5, 1.0] as $sf) {
                $allFractionsSet[(string) $sf] = $sf;
            }
            $allPolicyFractions = array_values($allFractionsSet);
            sort($allPolicyFractions);

            $usedApproved = leaveApprovedUsedInRange($pdo, (int) $eid, $auth['company_id'], $yearStart, $yearEnd);
            $usedManual = leaveManualUsedInRange($pdo, (int) $eid, $auth['company_id'], $yearStart, $yearEnd);

            $balancesOut = [];
            foreach (['annual', 'casual', 'sick'] as $lt) {
                $tot = (float) $policy[$lt];
                $uA = round($usedApproved[$lt], 2);
                $uM = round($usedManual[$lt], 2);
                $u = round($uA + $uM, 2);
                $balancesOut[$lt] = [
                    'total' => (int) round($tot),
                    'used_approved' => $uA,
                    'used_manual' => $uM,
                    'used' => $u,
                    'rem' => round($tot - $u, 2),
                ];
            }
            $policy['_balance_exhausted'] = finalReportAllLeaveRemDepleted($balancesOut);

            // Per-day manual deductions (report window)
            $stmtM = $pdo->prepare("SELECT id, deduction_date, leave_type, days, reason FROM leave_manual_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date BETWEEN ? AND ? ORDER BY id ASC");
            $stmtM->execute([(int) $eid, $auth['company_id'], $from, $to]);
            $manualByDate = [];
            while ($md = $stmtM->fetch()) {
                $ds = $md['deduction_date'];
                if (!isset($manualByDate[$ds])) {
                    $manualByDate[$ds] = [];
                }
                $manualByDate[$ds][] = [
                    'id' => (int) $md['id'],
                    'leave_type' => $md['leave_type'],
                    'days' => (float) $md['days'],
                    'reason' => $md['reason'],
                ];
            }

            // 3–4. Daily rows (attendance / leave / weekend) + non-binding issue suggestions
            $report = buildFinalReportDayRowsForEmployee($pdo, $employee, $policy, $auth['company_id'], $from, $to, $manualByDate);

            $settlementPolicy = leaveSettlementPolicyFromEmployeeRow($employee);
            $totalsProposedSettlement = ['annual' => 0.0, 'casual' => 0.0, 'sick' => 0.0];
            foreach ($report as $i => $rrow) {
                $sys = suggestLeaveSettlementDay($rrow, $settlementPolicy);
                $report[$i]['system_settlement'] = $sys;
                $report[$i]['manager_edit'] = null;
                if ($sys !== null) {
                    $report[$i]['manager_edit'] = [
                        'leave_type' => $sys['leave_type'],
                        'days' => $sys['days'],
                        'reason' => '',
                    ];
                    $lt = (string) $sys['leave_type'];
                    if (isset($totalsProposedSettlement[$lt])) {
                        $totalsProposedSettlement[$lt] = round($totalsProposedSettlement[$lt] + (float) $sys['days'], 2);
                    }
                }
            }
            $stmtFpSet = $pdo->prepare('SELECT MAX(timestamp) AS mx FROM attendance WHERE employee_id = ? AND company_id = ? AND DATE(timestamp) BETWEEN ? AND ?');
            $stmtFpSet->execute([(int) $eid, $auth['company_id'], $from, $to]);
            $fpSet = $stmtFpSet->fetch();
            $settlementFingerprint = md5((string) ($fpSet['mx'] ?? '') . '|' . (int) $eid . '|' . $from . '|' . $to);

            $totals = [
                'work_hours_sum' => 0.0,
                'late_days' => 0,
                'early_out_days' => 0,
                'location_alert_days' => 0,
                'incomplete_days' => 0,
            ];
            foreach ($report as $row) {
                if (($row['status'] ?? '') !== 'present') {
                    continue;
                }
                if (!empty($row['work_hours'])) {
                    $totals['work_hours_sum'] += (float)$row['work_hours'];
                }
                $ci = $row['check_in'] ?? null;
                $co = $row['check_out'] ?? null;
                $schIn = substr((string)($row['sch_in'] ?? '09:00:00'), 0, 5);
                $schOut = substr((string)($row['sch_out'] ?? '17:00:00'), 0, 5);
                if ($ci) {
                    $checkInTime = substr($ci, 11, 5);
                    if ($checkInTime > $schIn) {
                        $totals['late_days']++;
                    }
                }
                if ($co) {
                    $checkOutTime = substr($co, 11, 5);
                    if ($checkOutTime < $schOut) {
                        $totals['early_out_days']++;
                    }
                }
                if ($ci && !$co) {
                    $totals['incomplete_days']++;
                }
                if (($row['in_location_status'] ?? '') === 'alert' || ($row['out_location_status'] ?? '') === 'alert') {
                    $totals['location_alert_days']++;
                }
            }
            $totals['work_hours_sum'] = round($totals['work_hours_sum'], 2);
            $totals['salary_deduction_days'] = 0.0;
            foreach ($report as $rrow) {
                foreach ($rrow['salary_deductions'] ?? [] as $sx) {
                    $totals['salary_deduction_days'] += (float) ($sx['days'] ?? 0);
                }
            }
            $totals['salary_deduction_days'] = round($totals['salary_deduction_days'], 2);

            $employeeProfile = [
                'name' => $employee['name'],
                'employee_code' => $employee['employee_code'],
                'department' => $employee['department_name'] ?? '',
                'branch' => $employee['branch_name'] ?? '',
                'job_title' => $employee['job_title'] ?? '',
                'work_nature' => $employee['work_nature'] ?? 'internal',
                'phone' => $employee['phone'] ?? '',
                'email' => $employee['email'] ?? '',
                'policy_name' => $policy['name'],
            ];

            respond(200, [
                'report' => $report,
                'policy' => $policy,
                'employee_profile' => $employeeProfile,
                'totals' => $totals,
                'balance_year' => $balanceYear,
                'balances' => $balancesOut,
                'all_policy_fractions' => $allPolicyFractions,
                'suggestion_codes' => finalReportSuggestionCodes(),
                'settlement_policy' => $settlementPolicy,
                'totals_proposed_settlement' => $totalsProposedSettlement,
                'settlement_fingerprint' => $settlementFingerprint,
                'policy_missing' => empty($employee['policy_id']) || empty($employee['policy_name']),
            ]);
        } catch (Exception $e) {
            respond(500, 'Error generating report: ' . $e->getMessage());
        }

    case 'GET:reports/leave-settlement-preview':
        try {
            $auth = requireRole('admin');
            $eid = (int) ($input['employee_id'] ?? $_GET['employee_id'] ?? 0);
            $from = $input['from'] ?? $_GET['from'] ?? date('Y-m-01');
            $to = $input['to'] ?? $_GET['to'] ?? date('Y-m-t');
            if ($eid <= 0) {
                respond(400, 'Employee ID required');
            }
            $data = hrproLeaveSettlementPreviewData($auth, $eid, $from, $to);
            if (isset($data['error'])) {
                respond((int) ($data['http'] ?? 404), $data['error']);
            }
            respond(200, $data);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:reports/leave-settlement-apply':
        try {
            $auth = requireRole('admin');
            $d = input();
            $empId = (int) ($d['employee_id'] ?? 0);
            $from = trim((string) ($d['from'] ?? ''));
            $to = trim((string) ($d['to'] ?? ''));
            $rowsIn = $d['rows'] ?? [];
            $clearDatesIn = $d['clear_dates'] ?? [];
            if ($empId <= 0 || $from === '' || $to === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $from) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
                respond(400, 'employee_id, from, to (Y-m-d) required');
            }
            if (!is_array($rowsIn)) {
                respond(400, 'rows must be an array');
            }
            if (!is_array($clearDatesIn)) {
                $clearDatesIn = [];
            }

            $employee = hrproFetchEmployeeForFinalReport($empId, $auth['company_id']);
            if (!$employee) {
                respond(404, 'Employee not found');
            }

            $policyRowForOpts = [
                'deduct_full_day' => $employee['deduct_full_day'] ?? null,
                'deduct_half_day' => $employee['deduct_half_day'] ?? null,
                'deduct_quarter_day' => $employee['deduct_quarter_day'] ?? null,
            ];
            $allowedFracs = policyDeductionFractionOptions($policyRowForOpts);

            $pdo = db();
            $balanceYear = (int) substr($from, 0, 4);
            if ($balanceYear < 1990 || $balanceYear > 2120) {
                $balanceYear = (int) date('Y');
            }
            $yearStart = sprintf('%04d-01-01', $balanceYear);
            $yearEnd = sprintf('%04d-12-31', $balanceYear);
            $usedApproved = leaveApprovedUsedInRange($pdo, $empId, $auth['company_id'], $yearStart, $yearEnd);
            $usedManual = leaveManualUsedInRange($pdo, $empId, $auth['company_id'], $yearStart, $yearEnd);

            $policy = [
                'annual' => (int) ($employee['annual_leave_days'] ?? 0),
                'casual' => (int) ($employee['casual_leave_days'] ?? 0),
                'sick' => (int) ($employee['sick_leave_days'] ?? 0),
            ];

            $sumNew = ['annual' => 0.0, 'casual' => 0.0, 'sick' => 0.0];
            $normalized = [];
            foreach ($rowsIn as $r) {
                if (!is_array($r)) {
                    continue;
                }
                $dd = trim((string) ($r['deduction_date'] ?? ''));
                $lt = trim((string) ($r['leave_type'] ?? ''));
                $days = isset($r['days']) ? (float) $r['days'] : -1.0;
                $reason = isset($r['reason']) ? trim((string) $r['reason']) : '';
                if ($dd === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dd)) {
                    respond(400, 'Invalid deduction_date in rows');
                }
                if ($dd < $from || $dd > $to) {
                    respond(400, 'deduction_date outside from/to window');
                }
                if (!in_array($lt, ['annual', 'casual', 'sick'], true)) {
                    respond(400, 'leave_type must be annual, casual, or sick');
                }
                if ($days <= 0) {
                    continue;
                }
                $ok = false;
                foreach ($allowedFracs as $a) {
                    if (abs($days - (float) $a) < 0.001) {
                        $ok = true;
                        break;
                    }
                }
                if (!$ok) {
                    respond(400, 'days must match policy deduction options: ' . implode(', ', $allowedFracs));
                }
                if (strlen($reason) > 500) {
                    $reason = substr($reason, 0, 500);
                }
                $sumNew[$lt] = round($sumNew[$lt] + $days, 2);
                $normalized[] = ['deduction_date' => $dd, 'leave_type' => $lt, 'days' => round($days, 2), 'reason' => $reason !== '' ? $reason : null];
            }

            $clearDates = [];
            foreach ($clearDatesIn as $cd) {
                $ds = trim((string) $cd);
                if ($ds === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $ds)) {
                    continue;
                }
                if ($ds < $from || $ds > $to) {
                    respond(400, 'clear_dates must fall within from/to');
                }
                $clearDates[] = $ds;
            }
            $clearDates = array_values(array_unique($clearDates));

            $datesFromRows = array_values(array_unique(array_map(static function ($x) {
                return $x['deduction_date'];
            }, $normalized)));
            $allDeletionDates = array_values(array_unique(array_merge($datesFromRows, $clearDates)));

            if (count($normalized) === 0 && count($clearDates) === 0) {
                respond(400, 'No deduction rows or clear dates to apply');
            }

            $sumOldByDate = ['annual' => 0.0, 'casual' => 0.0, 'sick' => 0.0];
            if ($allDeletionDates !== []) {
                $ph = implode(',', array_fill(0, count($allDeletionDates), '?'));
                $stOld = $pdo->prepare("SELECT leave_type, SUM(days) AS s FROM leave_manual_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date IN ($ph) GROUP BY leave_type");
                $stOld->execute(array_merge([$empId, $auth['company_id']], $allDeletionDates));
                while ($o = $stOld->fetch()) {
                    $lt = (string) ($o['leave_type'] ?? '');
                    if (isset($sumOldByDate[$lt])) {
                        $sumOldByDate[$lt] = round((float) ($o['s'] ?? 0), 2);
                    }
                }
            }

            foreach (['annual', 'casual', 'sick'] as $lt) {
                $tot = (float) $policy[$lt];
                $uA = round($usedApproved[$lt], 2);
                $uM = round($usedManual[$lt], 2);
                $oldPart = $sumOldByDate[$lt] ?? 0.0;
                $newS = $sumNew[$lt] ?? 0.0;
                $adjUsed = round($uM - $oldPart + $newS, 2);
                $remAfter = round($tot - $uA - $adjUsed, 2);
                if ($remAfter < -0.01) {
                    respond(400, 'Insufficient leave balance for ' . $lt . ' (would remain ' . $remAfter . ')');
                }
            }

            $pdo->beginTransaction();
            try {
                if ($allDeletionDates !== []) {
                    $phd = implode(',', array_fill(0, count($allDeletionDates), '?'));
                    $del = $pdo->prepare("DELETE FROM leave_manual_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date IN ($phd)");
                    $del->execute(array_merge([$empId, $auth['company_id']], $allDeletionDates));
                }
                $ins = $pdo->prepare('INSERT INTO leave_manual_deductions (company_id, employee_id, deduction_date, leave_type, days, reason) VALUES (?,?,?,?,?,?)');
                foreach ($normalized as $nr) {
                    $ins->execute([$auth['company_id'], $empId, $nr['deduction_date'], $nr['leave_type'], $nr['days'], $nr['reason']]);
                }
                $pdo->commit();
            } catch (Throwable $e) {
                $pdo->rollBack();
                throw $e;
            }

            auditLog('admin', (string) $auth['company_id'], 'LEAVE_SETTLEMENT_APPLY', [
                'employee_id' => $empId,
                'from' => $from,
                'to' => $to,
                'rows' => count($normalized),
                'cleared_dates' => count($clearDates),
            ], $auth['company_id']);
            respond(200, ['inserted' => count($normalized), 'cleared_dates' => count($clearDates)], 'Leave settlement saved');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'GET:reports/leave-settlement-bulk-preview':
        try {
            $auth = requireRole('admin');
            $from = trim((string) ($input['from'] ?? $_GET['from'] ?? date('Y-m-01')));
            $to = trim((string) ($input['to'] ?? $_GET['to'] ?? date('Y-m-t')));
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $from) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
                respond(400, 'from and to must be Y-m-d');
            }
            $deptRaw = $input['department_id'] ?? $_GET['department_id'] ?? null;
            $departmentId = null;
            if ($deptRaw !== null && $deptRaw !== '' && ctype_digit((string) $deptRaw)) {
                $departmentId = (int) $deptRaw;
            }
            $pdo = db();
            $rows = hrproBulkSettlementCollectRows($pdo, $auth['company_id'], $from, $to, $departmentId);
            $byEmp = [];
            foreach ($rows as $r) {
                $eid = (int) ($r['employee_id'] ?? 0);
                if (!isset($byEmp[$eid])) {
                    $byEmp[$eid] = [
                        'employee_id' => $eid,
                        'employee_name' => $r['employee_name'] ?? '',
                        'employee_code' => $r['employee_code'] ?? '',
                        'day_count' => 0,
                    ];
                }
                $byEmp[$eid]['day_count']++;
            }
            $sample = array_slice($rows, 0, 40);

            respond(200, [
                'from' => $from,
                'to' => $to,
                'department_id' => $departmentId,
                'total_rows' => count($rows),
                'employees_affected' => count($byEmp),
                'by_employee' => array_values($byEmp),
                'sample_rows' => $sample,
            ]);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:reports/leave-settlement-bulk-apply':
        try {
            $auth = requireRole('admin');
            $d = input();
            $from = trim((string) ($d['from'] ?? ''));
            $to = trim((string) ($d['to'] ?? ''));
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $from) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
                respond(400, 'from and to must be Y-m-d');
            }
            $deptRaw = $d['department_id'] ?? null;
            $departmentId = null;
            if ($deptRaw !== null && $deptRaw !== '' && ctype_digit((string) $deptRaw)) {
                $departmentId = (int) $deptRaw;
            }
            $pdo = db();
            $rows = hrproBulkSettlementCollectRows($pdo, $auth['company_id'], $from, $to, $departmentId);
            if ($rows === []) {
                respond(200, ['inserted' => 0], 'No automatic settlement rows to apply.');
            }
            $grouped = [];
            foreach ($rows as $r) {
                $eid = (int) $r['employee_id'];
                if (!isset($grouped[$eid])) {
                    $grouped[$eid] = [];
                }
                $grouped[$eid][] = $r;
            }
            $err = hrproBulkSettlementValidateBalances($pdo, $auth['company_id'], $grouped);
            if ($err !== null) {
                respond(400, $err);
            }
            $reasonBulk = 'bulk_settlement';
            $ins = $pdo->prepare('INSERT INTO leave_manual_deductions (company_id, employee_id, deduction_date, leave_type, days, reason) VALUES (?,?,?,?,?,?)');
            $pdo->beginTransaction();
            try {
                foreach ($rows as $r) {
                    $ins->execute([
                        $auth['company_id'],
                        (int) $r['employee_id'],
                        $r['deduction_date'],
                        $r['leave_type'],
                        $r['days'],
                        $reasonBulk,
                    ]);
                }
                $pdo->commit();
            } catch (Throwable $e) {
                $pdo->rollBack();
                throw $e;
            }
            auditLog('admin', (string) $auth['company_id'], 'LEAVE_SETTLEMENT_BULK_APPLY', [
                'from' => $from,
                'to' => $to,
                'department_id' => $departmentId,
                'inserted' => count($rows),
            ], $auth['company_id']);
            respond(200, ['inserted' => count($rows)], 'Bulk settlement applied.');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'GET:reports/final-issues':
        try {
            $auth = requireRole('admin');
            $from = $input['from'] ?? $_GET['from'] ?? date('Y-m-01');
            $to = $input['to'] ?? $_GET['to'] ?? date('Y-m-t');
            $issueSingle = trim((string) ($input['issue'] ?? $_GET['issue'] ?? ''));
            $issuesCsv = trim((string) ($input['issues'] ?? $_GET['issues'] ?? ''));
            $deptId = $input['department_id'] ?? $_GET['department_id'] ?? null;

            $allowed = finalReportSuggestionCodes();
            $filterSet = [];
            if ($issueSingle !== '' && in_array($issueSingle, $allowed, true)) {
                $filterSet[] = $issueSingle;
            }
            if ($issuesCsv !== '') {
                foreach (explode(',', $issuesCsv) as $c) {
                    $c = trim($c);
                    if ($c !== '' && in_array($c, $allowed, true)) {
                        $filterSet[] = $c;
                    }
                }
            }
            $filterSet = array_values(array_unique($filterSet));

            $pdo = db();
            $sql = "SELECT e.*, lp.policy_name, lp.weekend_days, lp.annual_leave_days, lp.casual_leave_days, lp.sick_leave_days,
                           lp.deduct_full_day, lp.deduct_half_day, lp.deduct_quarter_day,
                           lp.start_time, lp.end_time,
                           lp.salary_deduct_enabled, lp.salary_deduct_allow_with_balance,
                           d.department_name, b.name AS branch_name
                    FROM employees e
                    LEFT JOIN leave_policies lp ON e.policy_id = lp.id
                    LEFT JOIN departments d ON e.department_id = d.id
                    LEFT JOIN branches b ON e.branch_id = b.id
                    WHERE e.company_id = ? AND e.is_active = 1";
            $params = [$auth['company_id']];
            if ($deptId !== null && $deptId !== '' && ctype_digit((string) $deptId)) {
                $sql .= ' AND e.department_id = ?';
                $params[] = (int) $deptId;
            }
            $sql .= ' ORDER BY e.name ASC';
            $stmtEmp = $pdo->prepare($sql);
            $stmtEmp->execute($params);
            $employees = $stmtEmp->fetchAll();

            $stmtM = $pdo->prepare('SELECT id, deduction_date, leave_type, days, reason FROM leave_manual_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date BETWEEN ? AND ? ORDER BY id ASC');

            $rows = [];
            foreach ($employees as $employee) {
                $eid = (int) $employee['id'];
                $policyRowForOpts = [
                    'deduct_full_day' => $employee['deduct_full_day'] ?? null,
                    'deduct_half_day' => $employee['deduct_half_day'] ?? null,
                    'deduct_quarter_day' => $employee['deduct_quarter_day'] ?? null,
                ];
                $deductOptions = policyDeductionFractionOptions($policyRowForOpts);
                $policy = [
                    'name' => $employee['policy_name'] ?? 'Default',
                    'weekend_days' => json_decode($employee['weekend_days'] ?? '["Friday"]', true),
                    'annual' => (int) ($employee['annual_leave_days'] ?? 0),
                    'casual' => (int) ($employee['casual_leave_days'] ?? 0),
                    'sick' => (int) ($employee['sick_leave_days'] ?? 0),
                    'start_time' => $employee['start_time'] ?? '09:00:00',
                    'end_time' => $employee['end_time'] ?? '17:00:00',
                    'deduct_full_day' => policyDeductionDecimal($employee['deduct_full_day'] ?? null, 1.0),
                    'deduct_half_day' => policyDeductionDecimal($employee['deduct_half_day'] ?? null, 0.5),
                    'deduct_quarter_day' => policyDeductionDecimal($employee['deduct_quarter_day'] ?? null, 0.25),
                    'deduct_options' => $deductOptions,
                    'salary_deduct_enabled' => (int) ($employee['salary_deduct_enabled'] ?? 1),
                    'salary_deduct_allow_with_balance' => (int) ($employee['salary_deduct_allow_with_balance'] ?? 0),
                ];
                $policy = array_merge($policy, hrproFinalReportPolicyGraceFromEmployee($employee));

                $balanceYearFi = (int) substr((string) $from, 0, 4);
                if ($balanceYearFi < 1990 || $balanceYearFi > 2120) {
                    $balanceYearFi = (int) date('Y');
                }
                $ysFi = sprintf('%04d-01-01', $balanceYearFi);
                $yeFi = sprintf('%04d-12-31', $balanceYearFi);
                $usedAFi = leaveApprovedUsedInRange($pdo, $eid, $auth['company_id'], $ysFi, $yeFi);
                $usedMFi = leaveManualUsedInRange($pdo, $eid, $auth['company_id'], $ysFi, $yeFi);
                $balancesFi = [];
                foreach (['annual', 'casual', 'sick'] as $lt) {
                    $tot = (float) $policy[$lt];
                    $uA = round($usedAFi[$lt], 2);
                    $uM = round($usedMFi[$lt], 2);
                    $u = round($uA + $uM, 2);
                    $balancesFi[$lt] = ['total' => (int) round($tot), 'rem' => round($tot - $u, 2)];
                }
                $policy['_balance_exhausted'] = finalReportAllLeaveRemDepleted($balancesFi);

                $stmtM->execute([$eid, $auth['company_id'], $from, $to]);
                $manualByDate = [];
                while ($md = $stmtM->fetch()) {
                    $ds = $md['deduction_date'];
                    if (!isset($manualByDate[$ds])) {
                        $manualByDate[$ds] = [];
                    }
                    $manualByDate[$ds][] = [
                        'id' => (int) $md['id'],
                        'leave_type' => $md['leave_type'],
                        'days' => (float) $md['days'],
                        'reason' => $md['reason'],
                    ];
                }

                $report = buildFinalReportDayRowsForEmployee($pdo, $employee, $policy, $auth['company_id'], $from, $to, $manualByDate);
                foreach ($report as $row) {
                    if (empty($row['has_issues'])) {
                        continue;
                    }
                    if ($filterSet !== []) {
                        $hit = array_intersect($row['suggestions'] ?? [], $filterSet);
                        if (count($hit) === 0) {
                            continue;
                        }
                    }
                    $rows[] = [
                        'employee_id' => $eid,
                        'name' => $employee['name'],
                        'employee_code' => $employee['employee_code'],
                        'department' => $employee['department_name'] ?? '',
                        'date' => $row['date'],
                        'status' => $row['status'],
                        'suggestions' => $row['suggestions'] ?? [],
                        'can_manual_deduct' => $row['can_manual_deduct'] ?? false,
                        'check_in' => $row['check_in'] ?? null,
                        'check_out' => $row['check_out'] ?? null,
                    ];
                }
            }

            usort($rows, static function ($a, $b) {
                $ac = strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
                if ($ac !== 0) {
                    return $ac;
                }

                return strcmp((string) ($a['date'] ?? ''), (string) ($b['date'] ?? ''));
            });

            respond(200, [
                'from' => $from,
                'to' => $to,
                'suggestion_codes' => $allowed,
                'filter' => $filterSet,
                'rows' => $rows,
            ]);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:reports/leave-deduct':
        try {
            $auth = requireRole('admin');
            $d = input();
            $empId = (int) ($d['employee_id'] ?? 0);
            $dedDate = $d['deduction_date'] ?? '';
            $leaveType = $d['leave_type'] ?? '';
            $days = isset($d['days']) ? (float) $d['days'] : -1;
            $reason = isset($d['reason']) ? trim((string) $d['reason']) : '';
            if ($empId <= 0 || $dedDate === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dedDate)) {
                respond(400, 'employee_id and deduction_date (Y-m-d) required');
            }
            if (!in_array($leaveType, ['annual', 'casual', 'sick'], true)) {
                respond(400, 'leave_type must be annual, casual, or sick');
            }
            $stmtE = db()->prepare("SELECT e.id, lp.deduct_full_day, lp.deduct_half_day, lp.deduct_quarter_day FROM employees e LEFT JOIN leave_policies lp ON e.policy_id = lp.id WHERE e.id = ? AND e.company_id = ?");
            $stmtE->execute([$empId, $auth['company_id']]);
            $emp = $stmtE->fetch();
            if (!$emp) {
                respond(404, 'Employee not found');
            }
            $allowed = policyDeductionFractionOptions($emp);
            $ok = false;
            foreach ($allowed as $a) {
                if (abs($days - (float) $a) < 0.001) {
                    $ok = true;
                    break;
                }
            }
            if (!$ok) {
                respond(400, 'days must match policy deduction options: ' . implode(', ', $allowed));
            }
            if (strlen($reason) > 500) {
                $reason = substr($reason, 0, 500);
            }
            db()->prepare("INSERT INTO leave_manual_deductions (company_id, employee_id, deduction_date, leave_type, days, reason) VALUES (?,?,?,?,?,?)")
                ->execute([$auth['company_id'], $empId, $dedDate, $leaveType, round($days, 2), $reason !== '' ? $reason : null]);
            $newId = (int) db()->lastInsertId();
            auditLog('admin', $auth['company_id'], 'LEAVE_MANUAL_DEDUCT', ['id' => $newId, 'employee_id' => $empId, 'date' => $dedDate, 'leave_type' => $leaveType, 'days' => $days], $auth['company_id']);
            respond(201, ['id' => $newId], 'Deduction recorded');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:reports/leave-deduct-late-one':
        try {
            $auth = requireRole('admin');
            $d = input();
            $empId = (int) ($d['employee_id'] ?? 0);
            $dedDate = trim((string) ($d['deduction_date'] ?? ''));
            if ($empId <= 0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dedDate)) {
                respond(400, 'employee_id and deduction_date (Y-m-d) required');
            }
            $pdo = db();
            $built = hrproBuildFinalReportWindowRows($pdo, $auth['company_id'], $empId, $dedDate, $dedDate);
            if (!$built) {
                respond(404, 'Employee not found');
            }
            $row = null;
            foreach ($built['report'] as $r0) {
                if ((string) ($r0['date'] ?? '') === $dedDate) {
                    $row = $r0;
                    break;
                }
            }
            if (!$row) {
                respond(400, 'Could not resolve report row for this date');
            }
            $pv = $row['late_leave_deduct_preview'] ?? null;
            if (!$pv || !is_array($pv)) {
                respond(400, 'No late leave deduction applies for this day under the policy');
            }
            if (!empty($row['manual_deductions'])) {
                respond(409, 'This day already has a manual leave deduction; remove or adjust it first');
            }
            $okIns = hrproInsertLeaveManualDeductFromLatePreview($pdo, $auth['company_id'], $empId, $dedDate, $pv, $built['employee']);
            if (!$okIns) {
                respond(400, 'Deduction days do not match policy options for this employee');
            }
            respond(201, ['deduction_date' => $dedDate], 'Late leave deduction recorded');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:reports/leave-deduct-late-bulk':
        try {
            $auth = requireRole('admin');
            $d = input();
            $empId = (int) ($d['employee_id'] ?? 0);
            $from = trim((string) ($d['from'] ?? ''));
            $to = trim((string) ($d['to'] ?? ''));
            if ($empId <= 0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $from) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
                respond(400, 'employee_id, from and to (Y-m-d) required');
            }
            $pdo = db();
            $built = hrproBuildFinalReportWindowRows($pdo, $auth['company_id'], $empId, $from, $to);
            if (!$built) {
                respond(404, 'Employee not found');
            }
            $applied = 0;
            $skippedHasManual = 0;
            $skippedNoPreview = 0;
            foreach ($built['report'] as $row) {
                $ds = (string) ($row['date'] ?? '');
                if ($ds === '') {
                    continue;
                }
                $pv = $row['late_leave_deduct_preview'] ?? null;
                if (!$pv || !is_array($pv)) {
                    $skippedNoPreview++;

                    continue;
                }
                if (!empty($row['manual_deductions'])) {
                    $skippedHasManual++;

                    continue;
                }
                if (hrproInsertLeaveManualDeductFromLatePreview($pdo, $auth['company_id'], $empId, $ds, $pv, $built['employee'])) {
                    $applied++;
                }
            }
            respond(200, [
                'applied' => $applied,
                'skipped_no_preview' => $skippedNoPreview,
                'skipped_has_manual' => $skippedHasManual,
            ], 'Late leave deductions processed');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'DELETE:reports/leave-deduct/item':
        try {
            $auth = requireRole('admin');
            $did = isset($parts[2]) ? (int) $parts[2] : 0;
            if ($did <= 0) {
                respond(400, 'Deduction id required');
            }
            $stmt = db()->prepare("SELECT id, employee_id FROM leave_manual_deductions WHERE id = ? AND company_id = ?");
            $stmt->execute([$did, $auth['company_id']]);
            $row = $stmt->fetch();
            if (!$row) {
                respond(404, 'Deduction not found');
            }
            db()->prepare("DELETE FROM leave_manual_deductions WHERE id = ? AND company_id = ?")->execute([$did, $auth['company_id']]);
            auditLog('admin', $auth['company_id'], 'LEAVE_MANUAL_DEDUCT_DELETE', ['id' => $did, 'employee_id' => (int) $row['employee_id']], $auth['company_id']);
            respond(200, [], 'Deduction removed');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:reports/salary-deduct':
        try {
            $auth = requireRole('admin');
            $d = input();
            $empId = (int) ($d['employee_id'] ?? 0);
            $dedDate = trim((string) ($d['deduction_date'] ?? ''));
            $days = isset($d['days']) ? (float) $d['days'] : -1;
            $reason = isset($d['reason']) ? trim((string) $d['reason']) : '';
            $force = !empty($d['force_despite_balance']) && ($d['force_despite_balance'] === true || $d['force_despite_balance'] === 1 || $d['force_despite_balance'] === '1');
            if ($empId <= 0 || $dedDate === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dedDate)) {
                respond(400, 'employee_id and deduction_date (Y-m-d) required');
            }
            $stmtE = db()->prepare('SELECT e.id, e.company_id, lp.deduct_full_day, lp.deduct_half_day, lp.deduct_quarter_day, lp.salary_deduct_enabled, lp.salary_deduct_allow_with_balance, lp.weekend_days, lp.annual_leave_days, lp.casual_leave_days, lp.sick_leave_days FROM employees e LEFT JOIN leave_policies lp ON e.policy_id = lp.id WHERE e.id = ? AND e.company_id = ?');
            $stmtE->execute([$empId, $auth['company_id']]);
            $emp = $stmtE->fetch();
            if (!$emp) {
                respond(404, 'Employee not found');
            }
            if ((int) ($emp['salary_deduct_enabled'] ?? 1) !== 1) {
                respond(403, 'Salary deduction marking is disabled for this employee\'s policy');
            }
            $allowed = policyDeductionFractionOptions($emp);
            $ok = false;
            foreach ($allowed as $a) {
                if (abs($days - (float) $a) < 0.001) {
                    $ok = true;
                    break;
                }
            }
            if (!$ok) {
                respond(400, 'days must match policy deduction options: ' . implode(', ', $allowed));
            }
            $allowWith = (int) ($emp['salary_deduct_allow_with_balance'] ?? 0) === 1;
            $balanceYear = (int) substr($dedDate, 0, 4);
            if ($balanceYear < 1990 || $balanceYear > 2120) {
                $balanceYear = (int) date('Y');
            }
            $ys = sprintf('%04d-01-01', $balanceYear);
            $ye = sprintf('%04d-12-31', $balanceYear);
            $pdo = db();
            $usedA = leaveApprovedUsedInRange($pdo, $empId, $auth['company_id'], $ys, $ye);
            $usedM = leaveManualUsedInRange($pdo, $empId, $auth['company_id'], $ys, $ye);
            $balancesQuick = [];
            foreach (['annual' => 'annual_leave_days', 'casual' => 'casual_leave_days', 'sick' => 'sick_leave_days'] as $lt => $col) {
                $tot = (float) ($emp[$col] ?? 0);
                $uA = round($usedA[$lt], 2);
                $uM = round($usedM[$lt], 2);
                $u = round($uA + $uM, 2);
                $balancesQuick[$lt] = ['total' => $tot, 'rem' => round($tot - $u, 2)];
            }
            $exhausted = finalReportAllLeaveRemDepleted($balancesQuick);
            if (!$allowWith && !$exhausted) {
                respond(403, 'Employee still has remaining leave balance. Enable “allow salary mark with balance” on the policy, or mark only after balances are used.');
            }
            if ($force && !$allowWith) {
                respond(400, 'force_despite_balance requires the policy option “allow with balance”');
            }
            if (!$exhausted && $allowWith && !$force) {
                respond(400, 'When the employee still has leave balance, check “apply despite remaining balance” or wait until balances are exhausted.');
            }
            $attCnt = $pdo->prepare('SELECT COUNT(*) FROM attendance WHERE employee_id = ? AND company_id = ? AND DATE(timestamp) = ?');
            $attCnt->execute([$empId, $auth['company_id'], $dedDate]);
            if ((int) $attCnt->fetchColumn() > 0) {
                respond(400, 'This day has attendance; salary deduction applies to absent workdays only');
            }
            $lrCnt = $pdo->prepare("SELECT COUNT(*) FROM leave_requests WHERE employee_id = ? AND company_id = ? AND status = 'approved' AND leave_type IN ('annual','casual','sick') AND start_date <= ? AND end_date >= ?");
            $lrCnt->execute([$empId, $auth['company_id'], $dedDate, $dedDate]);
            if ((int) $lrCnt->fetchColumn() > 0) {
                respond(400, 'This day is covered by approved leave');
            }
            $weekendDays = json_decode((string) ($emp['weekend_days'] ?? '["Friday"]'), true);
            if (!is_array($weekendDays)) {
                $weekendDays = ['Friday'];
            }
            $dayNameEn = (new DateTime($dedDate))->format('l');
            if (in_array($dayNameEn, $weekendDays, true)) {
                respond(400, 'Cannot mark salary deduction on a scheduled weekend day');
            }
            $holCnt = $pdo->prepare('SELECT COUNT(*) FROM company_official_holidays WHERE company_id = ? AND holiday_date = ?');
            $holCnt->execute([$auth['company_id'], $dedDate]);
            if ((int) $holCnt->fetchColumn() > 0) {
                respond(400, 'This day is an official company holiday');
            }
            $dup = $pdo->prepare('SELECT id FROM employee_salary_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date = ?');
            $dup->execute([$empId, $auth['company_id'], $dedDate]);
            if ($dup->fetch()) {
                respond(409, 'This day already has a salary deduction mark');
            }
            if (strlen($reason) > 500) {
                $reason = substr($reason, 0, 500);
            }
            $forceDb = ($force && $allowWith && !$exhausted) ? 1 : 0;
            $pdo->prepare('INSERT INTO employee_salary_deductions (company_id, employee_id, deduction_date, days, reason, force_despite_balance) VALUES (?,?,?,?,?,?)')
                ->execute([$auth['company_id'], $empId, $dedDate, round($days, 2), $reason !== '' ? $reason : null, $forceDb]);
            $newId = (int) $pdo->lastInsertId();
            auditLog('admin', $auth['company_id'], 'SALARY_DEDUCT_MARK', ['id' => $newId, 'employee_id' => $empId, 'date' => $dedDate, 'days' => $days, 'force' => $forceDb], $auth['company_id']);
            respond(201, ['id' => $newId], 'Salary deduction mark recorded');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'DELETE:reports/salary-deduct/item':
        try {
            $auth = requireRole('admin');
            $sid = isset($parts[2]) ? (int) $parts[2] : 0;
            if ($sid <= 0) {
                respond(400, 'Salary deduction id required');
            }
            $stmt = db()->prepare('SELECT id, employee_id FROM employee_salary_deductions WHERE id = ? AND company_id = ?');
            $stmt->execute([$sid, $auth['company_id']]);
            $row = $stmt->fetch();
            if (!$row) {
                respond(404, 'Record not found');
            }
            db()->prepare('DELETE FROM employee_salary_deductions WHERE id = ? AND company_id = ?')->execute([$sid, $auth['company_id']]);
            auditLog('admin', $auth['company_id'], 'SALARY_DEDUCT_DELETE', ['id' => $sid, 'employee_id' => (int) $row['employee_id']], $auth['company_id']);
            respond(200, [], 'Removed');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:reports/final-execute-single':
        try {
            $auth = requireRole('admin');
            $d = input();
            $empId = (int) ($d['employee_id'] ?? 0);
            $dedDate = trim((string) ($d['date'] ?? ''));

            // Manager's explicit deduction_type: 'salary' | 'leave' (null = system default)
            $deductionType = in_array($d['deduction_type'] ?? '', ['salary', 'leave'], true)
                ? (string) $d['deduction_type']
                : null;

            // Manager-specified leave_type (overrides system suggestion)
            $reqLeaveType = in_array($d['leave_type'] ?? '', ['annual', 'casual', 'sick'], true)
                ? (string) $d['leave_type']
                : null;

            // Manager-specified days fraction (overrides system suggestion)
            $reqDays = isset($d['days']) && is_numeric($d['days']) && (float) $d['days'] > 0
                ? (float) $d['days']
                : null;

            if ($empId <= 0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dedDate)) {
                respond(400, 'employee_id and date (Y-m-d) required');
            }
            $pdo = db();
            $built = hrproBuildFinalReportWindowRows($pdo, $auth['company_id'], $empId, $dedDate, $dedDate);
            if (!$built) {
                respond(404, 'Employee not found');
            }
            $row = null;
            foreach ($built['report'] as $r0) {
                if ((string) ($r0['date'] ?? '') === $dedDate) {
                    $row = $r0;
                    break;
                }
            }
            if (!$row) {
                respond(400, 'Could not resolve report row for this date');
            }
            $status = (string) ($row['status'] ?? '');
            if (in_array($status, ['weekend', 'official_holiday', 'leave'], true)) {
                respond(400, 'No penalty applicable for this day (weekend / holiday / approved leave)');
            }
            if (!empty($row['manual_deductions']) || !empty($row['salary_deductions'])) {
                respond(409, 'A deduction already exists for this day');
            }
            $employee = $built['employee'];
            $policy   = $built['policy'];

            // Validate manager-supplied days against policy fractions
            $allowedFractions = policyDeductionFractionOptions($employee);
            if ($reqDays !== null) {
                $fracOk = false;
                foreach ($allowedFractions as $af) {
                    if (abs($reqDays - (float) $af) < 0.001) {
                        $fracOk = true;
                        break;
                    }
                }
                if (!$fracOk) {
                    respond(400, 'days must match policy deduction fractions: ' . implode(', ', $allowedFractions));
                }
            }

            // ── SALARY DEDUCTION (any status except skipped) ──────────────────
            if ($deductionType === 'salary') {
                $pv      = $row['late_leave_deduct_preview'] ?? null;
                $lateMin = ($pv && is_array($pv)) ? (int) ($pv['late_minutes'] ?? 0) : 0;
                $tier    = ($pv && is_array($pv)) ? (string) ($pv['tier'] ?? '') : '';
                // Days: manager override → late preview → full day from policy
                $days = $reqDays ?? (($pv && is_array($pv)) ? round((float) ($pv['days'] ?? 1.0), 2) : round((float) ($policy['deduct_full_day'] ?? 1.0), 2));
                $reason = $lateMin > 0
                    ? sprintf('تأخير %d دقيقة — مستوى السياسة: %s — قرار ختامي', $lateMin, $tier)
                    : sprintf('خصم راتب بقرار المدير — قرار ختامي (%s)', $dedDate);
                $pdo->prepare('INSERT INTO employee_salary_deductions (company_id, employee_id, deduction_date, days, reason, force_despite_balance) VALUES (?,?,?,?,?,?)')
                    ->execute([$auth['company_id'], $empId, $dedDate, round($days, 2), $reason, 0]);
                $newId = (int) $pdo->lastInsertId();
                auditLog('admin', $auth['company_id'], 'SALARY_DEDUCT_MARK', ['id' => $newId, 'employee_id' => $empId, 'date' => $dedDate, 'days' => $days, 'source' => 'final_decision'], $auth['company_id']);
                respond(201, ['type' => 'salary', 'date' => $dedDate], 'Salary deduction recorded');
            }

            // ── LEAVE DEDUCTION (any status except skipped) ───────────────────
            if ($deductionType === 'leave') {
                $pv  = $row['late_leave_deduct_preview'] ?? null;
                $sp  = leaveSettlementPolicyFromEmployeeRow($employee);
                $sys = suggestLeaveSettlementDay($row, $sp);

                // Leave type: manager override → system suggestion → policy default
                $lt = $reqLeaveType
                    ?? (($sys && isset($sys['leave_type'])) ? (string) $sys['leave_type'] : null)
                    ?? (($pv && isset($pv['leave_type'])) ? (string) $pv['leave_type'] : null)
                    ?? (string) ($sp['auto_deduct_leave_type'] ?? 'annual');
                if (!in_array($lt, ['annual', 'casual', 'sick'], true)) {
                    $lt = 'annual';
                }

                // Days: manager override → system suggestion → late preview → full day
                $days = $reqDays
                    ?? (($sys && isset($sys['days'])) ? (float) $sys['days'] : null)
                    ?? (($pv && isset($pv['days'])) ? round((float) $pv['days'], 2) : null)
                    ?? round((float) ($policy['deduct_full_day'] ?? 1.0), 2);

                $reason = $status === 'absent'
                    ? sprintf('غياب بلا حضور — خصم %s — قرار ختامي (%s)', $lt, $dedDate)
                    : sprintf('خصم إجازة %s بقرار المدير — قرار ختامي (%s)', $lt, $dedDate);

                $pdo->prepare('INSERT INTO leave_manual_deductions (company_id, employee_id, deduction_date, leave_type, days, reason) VALUES (?,?,?,?,?,?)')
                    ->execute([$auth['company_id'], $empId, $dedDate, $lt, round((float) $days, 2), $reason]);
                $newId = (int) $pdo->lastInsertId();
                auditLog('admin', $auth['company_id'], 'LEAVE_MANUAL_DEDUCT', ['id' => $newId, 'employee_id' => $empId, 'date' => $dedDate, 'leave_type' => $lt, 'days' => $days], $auth['company_id']);
                respond(201, ['type' => 'leave', 'leave_type' => $lt, 'date' => $dedDate], 'Leave deduction recorded');
            }

            // ── SYSTEM DEFAULT (no explicit deduction_type) ───────────────────
            $pv = $row['late_leave_deduct_preview'] ?? null;
            if ($pv && is_array($pv)) {
                // Default for late = salary
                $days = round((float) ($pv['days'] ?? 1.0), 2);
                $lateMin = (int) ($pv['late_minutes'] ?? 0);
                $tier = (string) ($pv['tier'] ?? '');
                $reason = sprintf('تأخير %d دقيقة — مستوى السياسة: %s', $lateMin, $tier);
                $pdo->prepare('INSERT INTO employee_salary_deductions (company_id, employee_id, deduction_date, days, reason, force_despite_balance) VALUES (?,?,?,?,?,?)')
                    ->execute([$auth['company_id'], $empId, $dedDate, $days, $reason, 0]);
                $newId = (int) $pdo->lastInsertId();
                auditLog('admin', $auth['company_id'], 'SALARY_DEDUCT_MARK', ['id' => $newId, 'employee_id' => $empId, 'date' => $dedDate, 'days' => $days, 'source' => 'final_decision_late_default'], $auth['company_id']);
                respond(201, ['type' => 'salary', 'date' => $dedDate], 'Late salary deduction recorded (default)');
            }
            if ($status === 'absent') {
                // Default for absent = leave
                $sp  = leaveSettlementPolicyFromEmployeeRow($employee);
                $sys = suggestLeaveSettlementDay($row, $sp);
                if ($sys) {
                    $lt   = (string) ($sys['leave_type'] ?? 'annual');
                    $days = (float) ($sys['days'] ?? 1.0);
                    if (!in_array($lt, ['annual', 'casual', 'sick'], true)) $lt = 'annual';
                    $reason = sprintf('غياب بلا حضور — قرار ختامي (%s)', $dedDate);
                    $pdo->prepare('INSERT INTO leave_manual_deductions (company_id, employee_id, deduction_date, leave_type, days, reason) VALUES (?,?,?,?,?,?)')
                        ->execute([$auth['company_id'], $empId, $dedDate, $lt, round($days, 2), $reason]);
                    $newId = (int) $pdo->lastInsertId();
                    auditLog('admin', $auth['company_id'], 'LEAVE_MANUAL_DEDUCT', ['id' => $newId, 'employee_id' => $empId, 'date' => $dedDate, 'leave_type' => $lt, 'days' => $days], $auth['company_id']);
                    respond(201, ['type' => 'leave', 'date' => $dedDate], 'Absence leave deduction recorded (default)');
                }
            }

            respond(400, 'No applicable suggestion for this day. Specify deduction_type to apply manually.');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'POST:reports/final-execute-all':
        try {
            $auth = requireRole('admin');
            $d = input();
            $empId = (int) ($d['employee_id'] ?? 0);
            $from  = trim((string) ($d['from'] ?? ''));
            $to    = trim((string) ($d['to'] ?? ''));
            if ($empId <= 0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $from) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
                respond(400, 'employee_id, from and to (Y-m-d) required');
            }
            if ($from > $to) {
                respond(400, 'from must be <= to');
            }
            $pdo = db();
            $built = hrproBuildFinalReportWindowRows($pdo, $auth['company_id'], $empId, $from, $to);
            if (!$built) {
                respond(404, 'Employee not found');
            }
            $employee = $built['employee'];
            $policy   = $built['policy'];
            $sp       = leaveSettlementPolicyFromEmployeeRow($employee);

            $applied             = 0;
            $skippedExisting     = 0;
            $skippedNoSuggestion = 0;

            foreach ($built['report'] as $row) {
                $ds     = (string) ($row['date'] ?? '');
                $status = (string) ($row['status'] ?? '');
                if ($ds === '') {
                    continue;
                }
                if (in_array($status, ['weekend', 'official_holiday', 'leave'], true)) {
                    $skippedNoSuggestion++;
                    continue;
                }
                if (!empty($row['manual_deductions']) || !empty($row['salary_deductions'])) {
                    $skippedExisting++;
                    continue;
                }

                // ── LATE DAY — default: salary deduction ──────────────────────
                $pv = $row['late_leave_deduct_preview'] ?? null;
                if ($pv && is_array($pv)) {
                    $days = round((float) ($pv['days'] ?? 1.0), 2);
                    $lateMin = (int) ($pv['late_minutes'] ?? 0);
                    $tier = (string) ($pv['tier'] ?? '');
                    $reason = sprintf('تأخير %d دقيقة — مستوى السياسة: %s', $lateMin, $tier);
                    $pdo->prepare('INSERT INTO employee_salary_deductions (company_id, employee_id, deduction_date, days, reason, force_despite_balance) VALUES (?,?,?,?,?,?)')
                        ->execute([$auth['company_id'], $empId, $ds, $days, $reason, 0]);
                    $newId = (int) $pdo->lastInsertId();
                    auditLog('admin', $auth['company_id'], 'SALARY_DEDUCT_MARK', ['id' => $newId, 'employee_id' => $empId, 'date' => $ds, 'days' => $days, 'source' => 'final_decision_late_bulk'], $auth['company_id']);
                    $applied++;
                    continue;
                }

                // ── ABSENT DAY — default: leave deduction ─────────────────────
                if ($status === 'absent') {
                    $sys = suggestLeaveSettlementDay($row, $sp);
                    if ($sys) {
                        $lt   = (string) ($sys['leave_type'] ?? '');
                        $days = (float) ($sys['days'] ?? 0);
                        if (in_array($lt, ['annual', 'casual', 'sick'], true)) {
                            $reason = sprintf('غياب بلا حضور — قرار ختامي (%s)', $ds);
                            $pdo->prepare('INSERT INTO leave_manual_deductions (company_id, employee_id, deduction_date, leave_type, days, reason) VALUES (?,?,?,?,?,?)')
                                ->execute([$auth['company_id'], $empId, $ds, $lt, round($days, 2), $reason]);
                            $newId = (int) $pdo->lastInsertId();
                            auditLog('admin', $auth['company_id'], 'LEAVE_MANUAL_DEDUCT', ['id' => $newId, 'employee_id' => $empId, 'date' => $ds, 'leave_type' => $lt, 'days' => $days], $auth['company_id']);
                            $applied++;
                            continue;
                        }
                    }
                }
                $skippedNoSuggestion++;
            }

            respond(200, [
                'applied'              => $applied,
                'skipped_existing'     => $skippedExisting,
                'skipped_no_suggestion' => $skippedNoSuggestion,
            ], 'Final decision executed');
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'GET:reports/employee-deductions-summary':
        try {
            $auth = requireRole('admin');
            $eid = (int) ($input['employee_id'] ?? $_GET['employee_id'] ?? 0);
            $from = trim((string) ($input['from'] ?? $_GET['from'] ?? ''));
            $to = trim((string) ($input['to'] ?? $_GET['to'] ?? ''));
            if ($eid <= 0) {
                respond(400, 'employee_id required');
            }
            if ($from === '' || $to === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $from) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
                respond(400, 'from and to (Y-m-d) required');
            }
            if ($from > $to) {
                respond(400, 'from must be <= to');
            }
            $pdo = db();
            $cid = $auth['company_id'];
            $stE = $pdo->prepare('SELECT e.id, e.name, e.employee_code, e.job_title, e.work_nature, d.department_name, b.name AS branch_name
                FROM employees e
                LEFT JOIN departments d ON e.department_id = d.id
                LEFT JOIN branches b ON e.branch_id = b.id
                WHERE e.id = ? AND e.company_id = ?');
            $stE->execute([$eid, $cid]);
            $emp = $stE->fetch(PDO::FETCH_ASSOC);
            if (!$emp) {
                respond(404, 'Employee not found');
            }

            $stL = $pdo->prepare('SELECT id, deduction_date, leave_type, days, reason FROM leave_manual_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date BETWEEN ? AND ? ORDER BY deduction_date ASC, id ASC');
            $stL->execute([$eid, $cid, $from, $to]);
            $leaveRows = [];
            $totLeave = ['annual' => 0.0, 'casual' => 0.0, 'sick' => 0.0];
            while ($r = $stL->fetch(PDO::FETCH_ASSOC)) {
                $lt = (string) ($r['leave_type'] ?? '');
                $d = round((float) ($r['days'] ?? 0), 2);
                $leaveRows[] = [
                    'id' => (int) $r['id'],
                    'kind' => 'leave_balance',
                    'deduction_date' => (string) $r['deduction_date'],
                    'leave_type' => $lt,
                    'days' => $d,
                    'reason' => $r['reason'],
                ];
                if (isset($totLeave[$lt])) {
                    $totLeave[$lt] = round($totLeave[$lt] + $d, 2);
                }
            }

            $stS = $pdo->prepare('SELECT id, deduction_date, days, reason, force_despite_balance FROM employee_salary_deductions WHERE employee_id = ? AND company_id = ? AND deduction_date BETWEEN ? AND ? ORDER BY deduction_date ASC, id ASC');
            $stS->execute([$eid, $cid, $from, $to]);
            $salaryRows = [];
            $totSalary = 0.0;
            while ($r = $stS->fetch(PDO::FETCH_ASSOC)) {
                $d = round((float) ($r['days'] ?? 0), 2);
                $salaryRows[] = [
                    'id' => (int) $r['id'],
                    'kind' => 'salary',
                    'deduction_date' => (string) $r['deduction_date'],
                    'days' => $d,
                    'reason' => $r['reason'],
                    'force_despite_balance' => (int) ($r['force_despite_balance'] ?? 0) === 1,
                ];
                $totSalary = round($totSalary + $d, 2);
            }

            $rows = array_merge(
                array_map(static function ($x) {
                    return $x;
                }, $leaveRows),
                array_map(static function ($x) {
                    return $x;
                }, $salaryRows)
            );
            usort($rows, static function ($a, $b) {
                $c = strcmp((string) ($a['deduction_date'] ?? ''), (string) ($b['deduction_date'] ?? ''));
                if ($c !== 0) {
                    return $c;
                }
                $order = ['leave_balance' => 0, 'salary' => 1];
                $oa = $order[$a['kind'] ?? ''] ?? 9;
                $ob = $order[$b['kind'] ?? ''] ?? 9;

                return $oa <=> $ob;
            });

            respond(200, [
                'from' => $from,
                'to' => $to,
                'employee' => [
                    'id' => (int) $emp['id'],
                    'name' => $emp['name'],
                    'employee_code' => $emp['employee_code'],
                    'job_title' => $emp['job_title'] ?? '',
                    'work_nature' => $emp['work_nature'] ?? 'internal',
                    'department' => $emp['department_name'] ?? '',
                    'branch' => $emp['branch_name'] ?? '',
                ],
                'leave_manual' => $leaveRows,
                'salary_marks' => $salaryRows,
                'rows_merged' => $rows,
                'totals' => [
                    'leave_by_type' => $totLeave,
                    'leave_days_sum' => round($totLeave['annual'] + $totLeave['casual'] + $totLeave['sick'], 2),
                    'salary_days_sum' => $totSalary,
                ],
            ]);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'GET:reports/leave-overview':
        try {
            $auth = requireRole('admin');
            $year = (int) ($input['year'] ?? $_GET['year'] ?? date('Y'));
            if ($year < 1990 || $year > 2120) {
                $year = (int) date('Y');
            }
            $yStart = sprintf('%04d-01-01', $year);
            $yEnd = sprintf('%04d-12-31', $year);
            $cid = $auth['company_id'];
            $pdo = db();

            $branchRaw = trim((string) ($input['branch_id'] ?? $_GET['branch_id'] ?? 'all'));
            $deptRaw = trim((string) ($input['department_id'] ?? $_GET['department_id'] ?? 'all'));
            $policyRaw = trim((string) ($input['policy_id'] ?? $_GET['policy_id'] ?? 'all'));
            $natureRaw = trim((string) ($input['work_nature'] ?? $_GET['work_nature'] ?? 'all'));
            $statusRaw = trim((string) ($input['status'] ?? $_GET['status'] ?? 'active'));
            $policyPresence = trim((string) ($input['policy_presence'] ?? $_GET['policy_presence'] ?? 'all'));
            $search = trim((string) ($input['search'] ?? $_GET['search'] ?? ''));
            $sortRaw = trim((string) ($input['sort'] ?? $_GET['sort'] ?? 'name_asc'));

            $where = ['e.company_id = ?'];
            $params = [$cid];

            if ($branchRaw !== '' && $branchRaw !== 'all') {
                if ($branchRaw === 'none') {
                    $where[] = '(e.branch_id IS NULL OR e.branch_id = 0)';
                } else {
                    $bid = (int) $branchRaw;
                    if ($bid > 0) {
                        $where[] = 'e.branch_id = ?';
                        $params[] = $bid;
                    }
                }
            }
            if ($deptRaw !== '' && $deptRaw !== 'all') {
                if ($deptRaw === 'none') {
                    $where[] = '(e.department_id IS NULL OR e.department_id = 0)';
                } else {
                    $did = (int) $deptRaw;
                    if ($did > 0) {
                        $where[] = 'e.department_id = ?';
                        $params[] = $did;
                    }
                }
            }
            if ($policyRaw !== '' && $policyRaw !== 'all') {
                if ($policyRaw === 'none') {
                    $where[] = '(e.policy_id IS NULL OR e.policy_id = 0)';
                } else {
                    $pid = (int) $policyRaw;
                    if ($pid > 0) {
                        $where[] = 'e.policy_id = ?';
                        $params[] = $pid;
                    }
                }
            }
            if ($natureRaw === 'internal' || $natureRaw === 'external') {
                $where[] = 'e.work_nature = ?';
                $params[] = $natureRaw;
            }
            if ($statusRaw === 'active') {
                $where[] = 'e.is_active = 1';
            } elseif ($statusRaw === 'inactive') {
                $where[] = 'e.is_active = 0';
            }
            if ($policyPresence === 'with') {
                $where[] = 'e.policy_id IS NOT NULL AND e.policy_id <> 0';
            } elseif ($policyPresence === 'without') {
                $where[] = '(e.policy_id IS NULL OR e.policy_id = 0)';
            }
            if ($search !== '') {
                $where[] = '(e.name LIKE ? OR e.employee_code LIKE ? OR e.phone LIKE ? OR e.email LIKE ?)';
                $q = '%' . $search . '%';
                array_push($params, $q, $q, $q, $q);
            }

            $sql = 'SELECT e.id, e.name, e.employee_code, e.is_active, e.work_nature,
                lp.weekend_days, lp.annual_leave_days, lp.casual_leave_days, lp.sick_leave_days,
                lp.policy_name AS policy_name,
                d.department_name AS department_name,
                b.name AS branch_name
                FROM employees e
                LEFT JOIN leave_policies lp ON e.policy_id = lp.id
                LEFT JOIN departments d ON e.department_id = d.id
                LEFT JOIN branches b ON e.branch_id = b.id
                WHERE ' . implode(' AND ', $where) . '
                ORDER BY e.name ASC';
            $stmt = $pdo->prepare($sql);
            $stmt->execute($params);
            $employees = $stmt->fetchAll();

            $stmtA = $pdo->prepare("SELECT DISTINCT employee_id, DATE(timestamp) AS d FROM attendance WHERE company_id = ? AND DATE(timestamp) BETWEEN ? AND ?");
            $stmtA->execute([$cid, $yStart, $yEnd]);
            $present = [];
            while ($r = $stmtA->fetch()) {
                $eid = (int) $r['employee_id'];
                if (!isset($present[$eid])) {
                    $present[$eid] = [];
                }
                $present[$eid][$r['d']] = true;
            }

            $stmtL = $pdo->prepare("SELECT employee_id, start_date, end_date FROM leave_requests WHERE company_id = ? AND status = 'approved' AND leave_type IN ('annual','casual','sick') AND start_date <= ? AND end_date >= ?");
            $stmtL->execute([$cid, $yEnd, $yStart]);
            $leavesByEmp = [];
            while ($r = $stmtL->fetch()) {
                $eid = (int) $r['employee_id'];
                if (!isset($leavesByEmp[$eid])) {
                    $leavesByEmp[$eid] = [];
                }
                $leavesByEmp[$eid][] = ['start' => $r['start_date'], 'end' => $r['end_date']];
            }

            $rows = [];
            foreach ($employees as $emp) {
                $eid = (int) $emp['id'];
                $weekendDays = json_decode($emp['weekend_days'] ?? '["Friday","Saturday"]', true);
                if (!is_array($weekendDays)) {
                    $weekendDays = ['Friday', 'Saturday'];
                }

                $approved = leaveApprovedUsedInRange($pdo, $eid, $cid, $yStart, $yEnd);
                $manual = leaveManualUsedInRange($pdo, $eid, $cid, $yStart, $yEnd);

                $pres = $present[$eid] ?? [];
                $lrng = $leavesByEmp[$eid] ?? [];

                $absent = 0;
                $cur = new DateTime($yStart);
                $endY = new DateTime($yEnd);
                $endY->modify('+1 day');
                while ($cur < $endY) {
                    $dStr = $cur->format('Y-m-d');
                    $dayNameEn = $cur->format('l');
                    if (in_array($dayNameEn, $weekendDays, true)) {
                        $cur->modify('+1 day');
                        continue;
                    }
                    if (!empty($pres[$dStr])) {
                        $cur->modify('+1 day');
                        continue;
                    }
                    $onLeave = false;
                    foreach ($lrng as $seg) {
                        if ($dStr >= $seg['start'] && $dStr <= $seg['end']) {
                            $onLeave = true;
                            break;
                        }
                    }
                    if ($onLeave) {
                        $cur->modify('+1 day');
                        continue;
                    }
                    $absent++;
                    $cur->modify('+1 day');
                }

                $annualT = (int) ($emp['annual_leave_days'] ?? 0);
                $casualT = (int) ($emp['casual_leave_days'] ?? 0);
                $sickT = (int) ($emp['sick_leave_days'] ?? 0);

                $rows[] = [
                    'employee_id' => $eid,
                    'name' => $emp['name'],
                    'employee_code' => $emp['employee_code'],
                    'department_name' => $emp['department_name'] ?? '',
                    'branch_name' => $emp['branch_name'] ?? '',
                    'policy_name' => $emp['policy_name'] ?? '',
                    'is_active' => (int) ($emp['is_active'] ?? 0),
                    'work_nature' => $emp['work_nature'] ?? 'internal',
                    'absent_workdays' => $absent,
                    'annual' => [
                        'total' => $annualT,
                        'used_approved' => round($approved['annual'], 2),
                        'used_manual' => round($manual['annual'], 2),
                        'rem' => round($annualT - $approved['annual'] - $manual['annual'], 2),
                    ],
                    'casual' => [
                        'total' => $casualT,
                        'used_approved' => round($approved['casual'], 2),
                        'used_manual' => round($manual['casual'], 2),
                        'rem' => round($casualT - $approved['casual'] - $manual['casual'], 2),
                    ],
                    'sick' => [
                        'total' => $sickT,
                        'used_approved' => round($approved['sick'], 2),
                        'used_manual' => round($manual['sick'], 2),
                        'rem' => round($sickT - $approved['sick'] - $manual['sick'], 2),
                    ],
                    'manual_days_total' => round($manual['annual'] + $manual['casual'] + $manual['sick'], 2),
                ];
            }

            $sortFns = [
                'name_asc' => static function (array $a, array $b): int {
                    return strcmp((string) $a['name'], (string) $b['name']);
                },
                'name_desc' => static function (array $a, array $b): int {
                    return strcmp((string) $b['name'], (string) $a['name']);
                },
                'code_asc' => static function (array $a, array $b): int {
                    return strcmp((string) ($a['employee_code'] ?? ''), (string) ($b['employee_code'] ?? ''));
                },
                'absent_desc' => static function (array $a, array $b): int {
                    $c = ($b['absent_workdays'] <=> $a['absent_workdays']);

                    return $c !== 0 ? $c : strcmp((string) $a['name'], (string) $b['name']);
                },
                'annual_rem_asc' => static function (array $a, array $b): int {
                    $c = (($a['annual']['rem'] ?? 0) <=> ($b['annual']['rem'] ?? 0));

                    return $c !== 0 ? $c : strcmp((string) $a['name'], (string) $b['name']);
                },
                'annual_rem_desc' => static function (array $a, array $b): int {
                    $c = (($b['annual']['rem'] ?? 0) <=> ($a['annual']['rem'] ?? 0));

                    return $c !== 0 ? $c : strcmp((string) $a['name'], (string) $b['name']);
                },
                'manual_desc' => static function (array $a, array $b): int {
                    $c = (($b['manual_days_total'] ?? 0) <=> ($a['manual_days_total'] ?? 0));

                    return $c !== 0 ? $c : strcmp((string) $a['name'], (string) $b['name']);
                },
            ];
            $sortFn = $sortFns[$sortRaw] ?? $sortFns['name_asc'];
            usort($rows, $sortFn);

            respond(200, [
                'year' => $year,
                'employees' => $rows,
                'filters' => [
                    'branch_id' => $branchRaw,
                    'department_id' => $deptRaw,
                    'policy_id' => $policyRaw,
                    'work_nature' => $natureRaw,
                    'status' => $statusRaw,
                    'policy_presence' => $policyPresence,
                    'search' => $search,
                    'sort' => $sortRaw,
                ],
            ]);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    case 'GET:reports/leave-ledger':
        try {
            $auth = requireRole('admin');
            $cid = $auth['company_id'];
            $in = input();
            $empId = (int) ($in['employee_id'] ?? $_GET['employee_id'] ?? 0);
            if ($empId <= 0) {
                respond(400, 'employee_id is required');
            }
            $lt = strtolower(trim((string) ($in['leave_type'] ?? $_GET['leave_type'] ?? 'annual')));
            if (!in_array($lt, ['annual', 'casual', 'sick'], true)) {
                $lt = 'annual';
            }
            $fromYear = (int) ($in['from_year'] ?? $_GET['from_year'] ?? date('Y'));
            $toYear = (int) ($in['to_year'] ?? $_GET['to_year'] ?? $fromYear);
            if ($fromYear > $toYear) {
                $tmp = $fromYear;
                $fromYear = $toYear;
                $toYear = $tmp;
            }
            if ($fromYear < 1990) {
                $fromYear = 1990;
            }
            if ($toYear > 2120) {
                $toYear = 2120;
            }
            if ($toYear - $fromYear > 25) {
                respond(400, 'Year range too large (max 26 years)');
            }

            $pdo = db();
            $stE = $pdo->prepare('SELECT e.id, e.name, e.employee_code, e.company_id,
                lp.weekend_days, lp.annual_leave_days, lp.casual_leave_days, lp.sick_leave_days, lp.policy_name
                FROM employees e
                LEFT JOIN leave_policies lp ON e.policy_id = lp.id
                WHERE e.id = ? AND e.company_id = ?');
            $stE->execute([$empId, $cid]);
            $emp = $stE->fetch();
            if (!$emp) {
                respond(404, 'Employee not found');
            }

            $weekendDays = json_decode($emp['weekend_days'] ?? '["Friday","Saturday"]', true);
            if (!is_array($weekendDays)) {
                $weekendDays = ['Friday', 'Saturday'];
            }

            $entKey = $lt === 'annual' ? 'annual_leave_days' : ($lt === 'casual' ? 'casual_leave_days' : 'sick_leave_days');
            $entitlement = (float) ($emp[$entKey] ?? 0);

            $rangeStart = sprintf('%04d-01-01', $fromYear);
            $rangeEnd = sprintf('%04d-12-31', $toYear);

            $events = [];
            for ($y = $fromYear; $y <= $toYear; $y++) {
                if ($entitlement > 0.001) {
                    $accrualDate = leaveLedgerFirstWorkdayOfYear($y, $weekendDays);
                    $events[] = [
                        'sort_date' => $accrualDate,
                        'sort_key' => 0,
                        'date' => $accrualDate,
                        'kind' => 'accrual',
                        'delta' => round($entitlement, 4),
                        'detail' => 'year_accrual',
                        'ref_id' => null,
                    ];
                }
            }

            $stL = $pdo->prepare("SELECT id, start_date, end_date, leave_type, reason FROM leave_requests
                WHERE employee_id = ? AND company_id = ? AND status = 'approved' AND leave_type = ?
                AND start_date <= ? AND end_date >= ?
                ORDER BY start_date ASC, id ASC");
            $stL->execute([$empId, $cid, $lt, $rangeEnd, $rangeStart]);
            while ($lr = $stL->fetch()) {
                $days = leaveLedgerWorkdaysBetween((string) $lr['start_date'], (string) $lr['end_date'], $weekendDays);
                foreach ($days as $dStr) {
                    if ($dStr < $rangeStart || $dStr > $rangeEnd) {
                        continue;
                    }
                    $events[] = [
                        'sort_date' => $dStr,
                        'sort_key' => 1,
                        'date' => $dStr,
                        'kind' => 'leave_day',
                        'delta' => -1.0,
                        'detail' => 'approved_leave',
                        'ref_id' => (int) $lr['id'],
                        'note' => (string) ($lr['reason'] ?? ''),
                    ];
                }
            }

            $stM = $pdo->prepare('SELECT id, deduction_date, days, reason FROM leave_manual_deductions
                WHERE employee_id = ? AND company_id = ? AND leave_type = ?
                AND deduction_date BETWEEN ? AND ?
                ORDER BY deduction_date ASC, id ASC');
            $stM->execute([$empId, $cid, $lt, $rangeStart, $rangeEnd]);
            while ($md = $stM->fetch()) {
                $daysM = (float) ($md['days'] ?? 0);
                if ($daysM <= 0) {
                    continue;
                }
                $events[] = [
                    'sort_date' => (string) $md['deduction_date'],
                    'sort_key' => 2,
                    'date' => (string) $md['deduction_date'],
                    'kind' => 'manual',
                    'delta' => -1 * round($daysM, 4),
                    'detail' => 'manual_deduction',
                    'ref_id' => (int) $md['id'],
                    'note' => (string) ($md['reason'] ?? ''),
                ];
            }

            usort($events, static function (array $a, array $b): int {
                $c = strcmp($a['sort_date'], $b['sort_date']);
                if ($c !== 0) {
                    return $c;
                }

                return ($a['sort_key'] <=> $b['sort_key']);
            });

            $balance = 0.0;
            $outRows = [];
            foreach ($events as $ev) {
                $balance += $ev['delta'];
                $outRows[] = [
                    'date' => $ev['date'],
                    'kind' => $ev['kind'],
                    'delta' => $ev['delta'],
                    'balance' => round($balance, 4),
                    'detail' => $ev['detail'],
                    'ref_id' => $ev['ref_id'] ?? null,
                    'note' => $ev['note'] ?? '',
                ];
            }

            respond(200, [
                'employee_id' => $empId,
                'name' => $emp['name'],
                'employee_code' => $emp['employee_code'] ?? '',
                'policy_name' => $emp['policy_name'] ?? '',
                'leave_type' => $lt,
                'from_year' => $fromYear,
                'to_year' => $toYear,
                'events' => $outRows,
            ]);
        } catch (Exception $e) {
            respond(500, 'Error: ' . $e->getMessage());
        }

    default:
        respond(404, 'Endpoint not found');
}
} catch (Throwable $e) {
    error_log('api.php: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
    respond(500, 'Server error. Please try again.');
}
