<?php
header("Content-Type: application/json");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, X-Auth-Token, X-Company, Authorization, X-Requested-With");


// Uncomment the block below only when debugging locally — never in production.
// ini_set('display_errors', 1);
// ini_set('display_startup_errors', 1);
// error_reporting(E_ALL);.


if (!isset($_SERVER['REQUEST_METHOD'])) $_SERVER['REQUEST_METHOD'] = 'CLI';

// Load environment config from .env file (must come before CORS so ALLOWED_ORIGINS is available)
$_env_path = __DIR__ . '/.env';
if (file_exists($_env_path)) {
    $_env_vars = parse_ini_file($_env_path, false, INI_SCANNER_RAW);
    if (is_array($_env_vars)) {
        foreach ($_env_vars as $_k => $_v) {
            if (!isset($_ENV[$_k])) $_ENV[$_k] = $_v;
        }
    }
    unset($_env_vars, $_k, $_v);
}
unset($_env_path);

// Structured logger — must be loaded before any logging calls
require_once __DIR__ . '/logger.php';
Logger::init(__DIR__ . '/logs');

// CORS — restrict to configured origins (set ALLOWED_ORIGINS=* in .env for dev)
$_allowed_origins_raw = $_ENV['ALLOWED_ORIGINS'] ?? '*';
$_request_origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($_allowed_origins_raw === '*') {
    header("Access-Control-Allow-Origin: *");
} else {
    $_allowed_list = array_map('trim', explode(',', $_allowed_origins_raw));
    if (in_array($_request_origin, $_allowed_list, true)) {
        header("Access-Control-Allow-Origin: " . $_request_origin);
        header("Vary: Origin");
    }
}
unset($_allowed_origins_raw, $_allowed_list);

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    exit;
}

// ── ping — must be handled before DB connections so it never fails ───────────
$_early_action = $_GET['action'] ?? (json_decode(file_get_contents('php://input'), true)['action'] ?? '');
if ($_early_action === 'ping') {
    header('Content-Type: application/json');
    echo '{"ok":true}';
    exit;
}
unset($_early_action);

// CSRF protection — require X-Requested-With header on all POST requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $xrw = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
    if ($xrw !== 'AfrihairApp') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Invalid request origin']);
        exit;
    }
}

// Client databases are resolved from the configured master registry DB.
// Credentials are loaded from .env — see .env.example for the template.
$db_host = $_ENV['DB_HOST'] ?? 'localhost';
$db_user = $_ENV['DB_USER'] ?? '';
$db_pass = $_ENV['DB_PASS'] ?? '';
$db_name_default = ""; // Do not default to any shared DB; require explicit client resolution

function safe_database_name(string $name): string {
    return trim(preg_replace('/[^a-z0-9_]/', '_', strtolower($name)), '_');
}

function safe_database_prefix(string $prefix): string {
    $safe_prefix = preg_replace('/[^a-z0-9_]/', '_', strtolower(trim($prefix)));
    return ltrim($safe_prefix, '_');
}

$db_name_prefix = safe_database_prefix((string)($_ENV['DB_NAME_PREFIX'] ?? ''));

function apply_database_prefix(string $name): string {
    global $db_name_prefix;
    $safe_name = safe_database_name($name);
    if ($db_name_prefix === '' || $safe_name === '' || strpos($safe_name, $db_name_prefix) === 0) {
        return $safe_name;
    }
    return substr($db_name_prefix . $safe_name, 0, 64);
}

// Parse JSON payload once so DB routing can use company/action hints.
$raw_input = json_decode(file_get_contents("php://input"), true);
if (!is_array($raw_input)) $raw_input = [];

// Allow CLI callers to pass --company or -c to specify target client slug.
// Use $argv presence + no HTTP_HOST instead of php_sapi_name() === 'cli' so this
// also fires under LiteSpeed PHP (lsphp) which reports sapi = 'litespeed'.
if (isset($argv) && is_array($argv) && !isset($_SERVER['HTTP_HOST'])) {
    foreach ($argv as $i => $arg) {
        if (strpos($arg, '--company=') === 0) {
            $raw_input['company'] = substr($arg, 10);
            break;
        }
        if ($arg === '--company' || $arg === '-c') {
            $val = $argv[$i + 1] ?? null;
            if ($val) { $raw_input['company'] = $val; }
            break;
        }
    }
}

// Keep PHP and MySQL session time in UTC so auth expiry comparisons are consistent.
date_default_timezone_set('UTC');

// Resolve a safe PHP CLI binary path for background exec calls (cPanel-safe).
function get_cli_php_binary(): string {
    $candidates = [];

    $env_php_cli = trim((string)($_ENV['PHP_CLI_PATH'] ?? getenv('PHP_CLI_PATH') ?: ''));
    if ($env_php_cli !== '') {
        $candidates[] = $env_php_cli;
    }

    $which_php = trim((string)@shell_exec('command -v php 2>/dev/null'));
    if ($which_php !== '') {
        $candidates[] = $which_php;
    }

    $candidates[] = '/usr/local/bin/php';
    $candidates[] = '/usr/bin/php';
    $candidates[] = '/opt/cpanel/ea-php83/root/usr/bin/php';
    $candidates[] = '/opt/cpanel/ea-php82/root/usr/bin/php';
    $candidates[] = '/opt/cpanel/ea-php81/root/usr/bin/php';
    $candidates[] = '/opt/cpanel/ea-php80/root/usr/bin/php';

    if (defined('PHP_BINARY') && is_string(PHP_BINARY) && PHP_BINARY !== '') {
        $candidates[] = PHP_BINARY;
    }

    $candidates[] = 'php';

    $seen = [];
    foreach ($candidates as $candidate) {
        if (!is_string($candidate) || $candidate === '') continue;
        if (isset($seen[$candidate])) continue;
        $seen[$candidate] = true;

        $lower = strtolower($candidate);
        if (strpos($lower, 'php-fpm') !== false || strpos($lower, 'php_cgi') !== false || strpos($lower, 'php-cgi') !== false) {
            continue;
        }

        if (strpos($candidate, '/') !== false && !@is_executable($candidate)) continue;

        $version = trim((string)@shell_exec(escapeshellarg($candidate) . ' -v 2>/dev/null'));
        if ($version !== '' && stripos($version, 'fpm') === false && stripos($version, 'fastcgi') === false) {
            return $candidate;
        }
    }

    return 'php';
}

// ── Master DB connection ──────────────────────────────────────────
// Read-only queries from the app use this. The master DB is provisioned
// once by running setup_master.php. Falls back gracefully if not yet
// provisioned (e.g. legacy install), so no hard exit here.
$master_db_name = apply_database_prefix($_ENV['MASTER_DB_NAME'] ?? 'pickpack_ship_master');
$master_db = new mysqli($db_host, $db_user, $db_pass, $master_db_name);
if ($master_db->connect_error) {
    // Non-fatal: master DB may not yet exist on legacy installs.
    // Actions that require it will detect $master_db->connect_error and bail cleanly.
    $master_db = null;
} else {
    $master_db->query("SET time_zone = '+00:00'");
    // Ensure rate-limiting table exists in master DB
    $master_db->query("
        CREATE TABLE IF NOT EXISTS `pp_login_attempts` (
            `id` bigint NOT NULL AUTO_INCREMENT,
            `ip_address` varchar(45) NOT NULL,
            `company_slug` varchar(50) NOT NULL DEFAULT '',
            `attempted_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (`id`),
            KEY `idx_ip_time` (`ip_address`, `attempted_at`),
            KEY `idx_cleanup` (`attempted_at`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    ");

    // Upgrade pp_clients for shared-device security mode on existing installs.
    $_mc_cols = [];
    $_mc_q = $master_db->query("SHOW COLUMNS FROM pp_clients");
    if ($_mc_q) while ($_mc_c = $_mc_q->fetch_assoc()) $_mc_cols[] = $_mc_c['Field'];
    if (!in_array('security_mode', $_mc_cols, true)) {
        $master_db->query("ALTER TABLE pp_clients ADD COLUMN `security_mode` enum('master_only','shared_honor','individual_pin') NOT NULL DEFAULT 'individual_pin' AFTER `secondary_password_hash`");
    }
    if (!in_array('allowed_couriers', $_mc_cols, true)) {
        $master_db->query("ALTER TABLE pp_clients ADD COLUMN `allowed_couriers` text DEFAULT NULL AFTER `security_mode`");
    }
    unset($_mc_cols, $_mc_q, $_mc_c);
}

// Resolve request action/company early so we can route to the correct client DB.
$_boot_action = $_GET['action'] ?? $raw_input['action'] ?? $_GET['cron'] ?? 'get_orders';
$_global_only_actions = ['login', 'verify_client', 'list_shipping_providers', 'list_cms_providers', 'ping'];
$_requires_client_db = !in_array($_boot_action, $_global_only_actions, true);

// ── ping — lightweight connectivity check (exit early, no DB needed) ────────
if ($_boot_action === 'ping') {
    header('Content-Type: application/json');
    echo json_encode(['ok' => true]);
    exit; //to force save
}

function get_request_company_slug(array $input = []): ?string {
    $headers = function_exists('getallheaders') ? getallheaders() : [];
    $company = null;
    foreach ($headers as $k => $v) {
        if (strtolower($k) === 'x-company') { $company = $v; break; }
    }
    if (!$company && isset($_SERVER['HTTP_X_COMPANY'])) $company = $_SERVER['HTTP_X_COMPANY'];
    if (!$company) $company = $_GET['company'] ?? $input['company'] ?? $input['company_slug'] ?? $input['business_slug'] ?? null;
    if (!$company) return null;
    $company = preg_replace('/[^a-z0-9\-_]/', '', strtolower(trim((string)$company)));
    return $company !== '' ? $company : null;
}

function generate_client_db_name(string $slug): string {
    $safe = preg_replace('/[^a-z0-9_]/', '_', strtolower($slug));
    $safe = trim($safe, '_');
    if ($safe === '') $safe = 'client';
    return apply_database_prefix('pickpack_ship_' . $safe);
}

function normalize_courier_slug_list($raw): array {
    if ($raw === null || $raw === false) return [];
    $values = is_array($raw) ? $raw : [$raw];
    $slugs = [];
    foreach ($values as $value) {
        foreach (preg_split('/[\s,;]+/', (string)$value) as $part) {
            $slug = preg_replace('/[^a-z0-9\-_]/', '', strtolower(trim($part)));
            if ($slug !== '') $slugs[$slug] = true;
        }
    }
    return array_keys($slugs);
}

function get_allowed_couriers_for_company($master_db, ?string $company_slug = null, ?string $client_db_name = null): array {
    if (!$master_db) return [];
    $row = null;
    if ($company_slug) {
        $stmt = $master_db->prepare("SELECT allowed_couriers FROM pp_clients WHERE slug = ? AND is_active = 1 LIMIT 1");
        if ($stmt) {
            $stmt->bind_param('s', $company_slug);
            $stmt->execute();
            $row = $stmt->get_result()->fetch_assoc();
        }
    }
    if (!$row && $client_db_name) {
        $stmt = $master_db->prepare("SELECT allowed_couriers FROM pp_clients WHERE client_db_name = ? AND is_active = 1 LIMIT 1");
        if ($stmt) {
            $stmt->bind_param('s', $client_db_name);
            $stmt->execute();
            $row = $stmt->get_result()->fetch_assoc();
        }
    }
    return normalize_courier_slug_list($row['allowed_couriers'] ?? '');
}

function courier_allowed_for_company($master_db, string $provider_slug, ?string $company_slug = null, ?string $client_db_name = null): bool {
    $allowed = get_allowed_couriers_for_company($master_db, $company_slug, $client_db_name);
    return empty($allowed) || in_array($provider_slug, $allowed, true);
}

$_boot_company_slug = get_request_company_slug($raw_input);
$_resolved_client_db = $db_name_default;

if ($master_db && $_boot_company_slug) {
    $mc = $master_db->prepare("SELECT id, client_db_name, is_active FROM pp_clients WHERE slug = ? LIMIT 1");
    $mc->bind_param('s', $_boot_company_slug);
    $mc->execute();
    $mrow = $mc->get_result()->fetch_assoc();

    if ($mrow && (int)$mrow['is_active'] === 1) {
        $_client_db_name = trim((string)($mrow['client_db_name'] ?? ''));

        // During setup, auto-assign and persist a dedicated DB name when missing.
        if ($_client_db_name === '' && $_boot_action === 'run_setup') {
            $_client_db_name = generate_client_db_name($_boot_company_slug);
            $mu = $master_db->prepare("UPDATE pp_clients SET client_db_name = ? WHERE id = ? AND (client_db_name IS NULL OR client_db_name = '')");
            $mu->bind_param('si', $_client_db_name, $mrow['id']);
            $mu->execute();
        }

        if ($_client_db_name !== '') $_resolved_client_db = $_client_db_name;
    }
}

// Ensure target client DB exists for setup requests.
if ($_boot_action === 'run_setup' && $_resolved_client_db !== '') {
    $_admin_db = new mysqli($db_host, $db_user, $db_pass);
    if (!$_admin_db->connect_error) {
        $_safe_db_name = preg_replace('/[^a-z0-9_]/', '_', strtolower($_resolved_client_db));
        if ($_safe_db_name !== '') {
            $_admin_db->query("CREATE DATABASE IF NOT EXISTS `" . $_safe_db_name . "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
            $_resolved_client_db = $_safe_db_name;
        }
    }
    unset($_admin_db);
}

$db = null;

// Safety: if we couldn't resolve a client DB name, fail early rather than
// silently writing into a shared/default database, but only for actions that
// actually require a client database.
if ($_requires_client_db) {
    if (empty($_resolved_client_db)) {
        echo json_encode(["error" => "No client DB resolved. Provide X-Company header, ?company parameter, or run setup with a company slug."]);
        exit;
    }

    $db = new mysqli($db_host, $db_user, $db_pass, $_resolved_client_db);
    if ($db->connect_error) {
        $_admin_db = new mysqli($db_host, $db_user, $db_pass);
        if (!$_admin_db->connect_error) {
            $_safe_boot_db = preg_replace('/[^a-z0-9_]/', '_', strtolower($_resolved_client_db));
            if ($_safe_boot_db !== '') {
                $_admin_db->query("CREATE DATABASE IF NOT EXISTS `" . $_safe_boot_db . "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
                $db = new mysqli($db_host, $db_user, $db_pass, $_safe_boot_db);
            }
        }
        if ($db->connect_error) {
            echo json_encode(["error" => "DB connection failed"]);
            exit;
        }
    }
    $db->query("SET time_zone = '+00:00'");

    $order_lock_ttl_seconds = max(60, (int)($_ENV['ORDER_LOCK_TTL_SECONDS'] ?? getenv('ORDER_LOCK_TTL_SECONDS') ?: 600));

    // ====================================================================
    // FIRST-RUN DB MIGRATION — ensures PickPack Ship client tables exist
    // ====================================================================
    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_order_locks` (
            `order_id` varchar(50) NOT NULL,
            `staff_name` varchar(100) DEFAULT NULL,
            `last_seen` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (`order_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_packie_cache` (
            `shopify_order_number` varchar(50) NOT NULL,
            `packie_id` int NOT NULL DEFAULT 0,
            `label_consignment_id` int NOT NULL DEFAULT 0,
            `label_pdf_url` varchar(500) DEFAULT NULL,
            `packie_status` varchar(20) DEFAULT 'PENDING',
            `last_checked` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            `original_shopify_address` text,
            `corrected_address` text,
            PRIMARY KEY (`shopify_order_number`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");
    // Add corrected_address column to existing installations (compatible with MySQL 5.x+).
    // Avoid information_schema dependency and swallow migration errors to prevent API bootstrap failure.
    try {
        $col_check = $db->query("SHOW COLUMNS FROM `pp_packie_cache` LIKE 'corrected_address'");
        if ($col_check && $col_check->num_rows === 0) {
            $db->query("ALTER TABLE `pp_packie_cache` ADD COLUMN `corrected_address` text");
        }
        $col_check = $db->query("SHOW COLUMNS FROM `pp_packie_cache` LIKE 'label_consignment_id'");
        if ($col_check && $col_check->num_rows === 0) {
            $db->query("ALTER TABLE `pp_packie_cache` ADD COLUMN `label_consignment_id` int NOT NULL DEFAULT 0");
        }
        $col_check = $db->query("SHOW COLUMNS FROM `pp_packie_cache` LIKE 'label_pdf_url'");
        if ($col_check && $col_check->num_rows === 0) {
            $db->query("ALTER TABLE `pp_packie_cache` ADD COLUMN `label_pdf_url` varchar(500) DEFAULT NULL");
        }
    } catch (Throwable $e) {
        Logger::warn('schema', 'pp_packie_cache corrected_address migration skipped', ['error' => $e->getMessage()]);
    }

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_packie_stocks` (
            `id` int NOT NULL AUTO_INCREMENT,
            `name` varchar(100) DEFAULT NULL,
            `serviceType` int DEFAULT NULL,
            `height` decimal(10,2) DEFAULT NULL,
            `width` decimal(10,2) DEFAULT NULL,
            `length` decimal(10,2) DEFAULT NULL,
            `weight` decimal(10,2) DEFAULT NULL,
            `cubicWeight` decimal(10,2) DEFAULT NULL,
            `quick_pick_enabled` tinyint(1) NOT NULL DEFAULT 0,
            `quick_pick_carrier` varchar(100) DEFAULT NULL,
            `quick_pick_display_name` varchar(100) DEFAULT NULL,
            `quick_pick_is_default` tinyint(1) NOT NULL DEFAULT 0,
            PRIMARY KEY (`id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");
    try {
        $packie_stock_columns = [
            'quick_pick_enabled' => "ALTER TABLE `pp_packie_stocks` ADD COLUMN `quick_pick_enabled` tinyint(1) NOT NULL DEFAULT 0",
            'quick_pick_carrier' => "ALTER TABLE `pp_packie_stocks` ADD COLUMN `quick_pick_carrier` varchar(100) DEFAULT NULL",
            'quick_pick_display_name' => "ALTER TABLE `pp_packie_stocks` ADD COLUMN `quick_pick_display_name` varchar(100) DEFAULT NULL",
            'quick_pick_is_default' => "ALTER TABLE `pp_packie_stocks` ADD COLUMN `quick_pick_is_default` tinyint(1) NOT NULL DEFAULT 0",
        ];
        foreach ($packie_stock_columns as $column => $ddl) {
            $col_check = $db->query("SHOW COLUMNS FROM `pp_packie_stocks` LIKE '" . $db->real_escape_string($column) . "'");
            if ($col_check && $col_check->num_rows === 0) {
                $db->query($ddl);
            }
        }
    } catch (Throwable $e) {
        Logger::warn('schema', 'pp_packie_stocks quick pick migration skipped', ['error' => $e->getMessage()]);
    }

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_package_presets` (
            `code` varchar(20) NOT NULL,
            `display_name` varchar(100) NOT NULL,
            `length` decimal(10,2) NOT NULL DEFAULT 0,
            `width` decimal(10,2) NOT NULL DEFAULT 0,
            `height` decimal(10,2) NOT NULL DEFAULT 0,
            `weight` decimal(10,2) NOT NULL DEFAULT 0,
            `sort_order` int NOT NULL DEFAULT 99,
            `is_enabled` tinyint(1) NOT NULL DEFAULT 1,
            `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (`code`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_provider_package_map` (
            `id` int NOT NULL AUTO_INCREMENT,
            `provider_slug` varchar(50) NOT NULL,
            `package_code` varchar(20) NOT NULL,
            `provider_stock_id` varchar(100) DEFAULT NULL,
            `provider_service_type` varchar(50) DEFAULT NULL,
            `provider_label` varchar(100) DEFAULT NULL,
            `length` decimal(10,2) DEFAULT NULL,
            `width` decimal(10,2) DEFAULT NULL,
            `height` decimal(10,2) DEFAULT NULL,
            `weight` decimal(10,2) DEFAULT NULL,
            `is_enabled` tinyint(1) NOT NULL DEFAULT 1,
            `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (`id`),
            UNIQUE KEY `uniq_provider_package` (`provider_slug`, `package_code`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_picked_items` (
            `id` int NOT NULL AUTO_INCREMENT,
            `order_id` varchar(50) NOT NULL,
            `sku` varchar(100) NOT NULL,
            `picked_qty` int NOT NULL DEFAULT '0',
            `last_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            `picked_by` varchar(50) DEFAULT NULL,
            PRIMARY KEY (`id`),
            UNIQUE KEY `unique_pick` (`order_id`, `sku`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_order_notes` (
            `order_id` varchar(50) NOT NULL,
            `note_text` text NOT NULL,
            `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (`order_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_staff_members` (
            `id` int NOT NULL AUTO_INCREMENT,
            `name` varchar(255) NOT NULL,
            `is_active` tinyint(1) NOT NULL DEFAULT 1,
            PRIMARY KEY (`id`),
            UNIQUE KEY `unique_name` (`name`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_settings` (
            `id`            int NOT NULL AUTO_INCREMENT,
            `company_id`    int NOT NULL DEFAULT 1,
            `category`      enum('shipping','ecommerce','general') NOT NULL,
            `provider_slug` varchar(50) NOT NULL,
            `key`           varchar(100) NOT NULL,
            `value`         text NOT NULL,
            `is_encrypted`  tinyint(1) NOT NULL DEFAULT 0,
            `updated_at`    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (`id`),
            UNIQUE KEY `uq_company_cat_provider_key` (`company_id`, `category`, `provider_slug`, `key`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    ");

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_companies` (
            `id` int NOT NULL AUTO_INCREMENT,
            `slug` varchar(50) NOT NULL,
            `display_name` varchar(100) NOT NULL,
            `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (`id`),
            UNIQUE KEY `uq_slug` (`slug`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    ");

    $db->query("
        CREATE TABLE IF NOT EXISTS `pp_auth_sessions` (
            `token` char(64) NOT NULL,
            `staff_id` int NOT NULL,
            `company_id` int NOT NULL,
            `device_name` varchar(100) DEFAULT NULL,
            `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
            `expires_at` timestamp NOT NULL,
            PRIMARY KEY (`token`),
            KEY `idx_staff` (`staff_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    ");

    // Keep V2 bootstrap non-destructive: do not drop legacy/shared tables.

    // Upgrade pp_auth_sessions: add columns that may be missing on older installations
    $_as_cols = [];
    $_as_q = $db->query("SHOW COLUMNS FROM pp_auth_sessions");
    if ($_as_q) while ($_c = $_as_q->fetch_assoc()) $_as_cols[] = $_c['Field'];
    if (!in_array('company_id',   $_as_cols)) $db->query("ALTER TABLE pp_auth_sessions ADD COLUMN `company_id` int NOT NULL DEFAULT 1 AFTER `staff_id`");
    if (!in_array('device_name',  $_as_cols)) $db->query("ALTER TABLE pp_auth_sessions ADD COLUMN `device_name` varchar(100) DEFAULT NULL");
    if (!in_array('expires_at',   $_as_cols)) $db->query("ALTER TABLE pp_auth_sessions ADD COLUMN `expires_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP");
    unset($_as_cols, $_as_q, $_c);

    // Upgrade pp_staff_members: add new columns if this is an existing installation
    $_sm_cols = [];
    $_sm_q = $db->query("SHOW COLUMNS FROM pp_staff_members");
    while ($_c = $_sm_q->fetch_assoc()) $_sm_cols[] = $_c['Field'];
    if (!in_array('company_id',    $_sm_cols)) $db->query("ALTER TABLE pp_staff_members ADD COLUMN `company_id` int NOT NULL DEFAULT 1 AFTER `id`");
    if (!in_array('username',      $_sm_cols)) $db->query("ALTER TABLE pp_staff_members ADD COLUMN `username` varchar(100) NULL");
    if (!in_array('password_hash', $_sm_cols)) $db->query("ALTER TABLE pp_staff_members ADD COLUMN `password_hash` varchar(255) NULL");
    if (!in_array('role',          $_sm_cols)) $db->query("ALTER TABLE pp_staff_members ADD COLUMN `role` enum('master','secondary') NOT NULL DEFAULT 'secondary'");
    if (!in_array('can_pick',      $_sm_cols)) $db->query("ALTER TABLE pp_staff_members ADD COLUMN `can_pick` tinyint(1) NOT NULL DEFAULT 1");
    if (!in_array('can_pack',      $_sm_cols)) $db->query("ALTER TABLE pp_staff_members ADD COLUMN `can_pack` tinyint(1) NOT NULL DEFAULT 1");
    if (!in_array('can_print',     $_sm_cols)) {
        $db->query("ALTER TABLE pp_staff_members ADD COLUMN `can_print` tinyint(1) NOT NULL DEFAULT 0");
        // Grant print access to all existing master accounts — they're admins and should always be able to print.
        $db->query("UPDATE pp_staff_members SET can_print = 1 WHERE role = 'master'");
    }
    if (!in_array('last_login',    $_sm_cols)) $db->query("ALTER TABLE pp_staff_members ADD COLUMN `last_login` timestamp NULL DEFAULT NULL");
    if (!in_array('pin_code',      $_sm_cols)) $db->query("ALTER TABLE pp_staff_members ADD COLUMN `pin_code` varchar(4) DEFAULT NULL AFTER `can_print`");
    unset($_sm_cols, $_sm_q, $_c);

    // Upgrade pp_settings: scope provider pp_settings by company_id on existing installations
        // Always ensure master accounts have print access — safe to run on every boot
        // since it only touches rows where can_print is already wrong (0 on a master).
        $db->query("UPDATE pp_staff_members SET can_print = 1 WHERE role = 'master' AND can_print = 0");

    $_st_cols = [];
    $_st_q = $db->query("SHOW COLUMNS FROM pp_settings");
    if ($_st_q) while ($_c = $_st_q->fetch_assoc()) $_st_cols[] = $_c['Field'];
    if (!in_array('company_id', $_st_cols)) {
        $db->query("ALTER TABLE pp_settings ADD COLUMN `company_id` int NOT NULL DEFAULT 1 AFTER `id`");
    }

    $_st_has_old_uq = false;
    $_st_has_new_uq = false;
    $_st_ix_q = $db->query("SHOW INDEX FROM pp_settings");
    if ($_st_ix_q) {
        while ($_ix = $_st_ix_q->fetch_assoc()) {
            if (($_ix['Key_name'] ?? '') === 'uq_cat_provider_key') $_st_has_old_uq = true;
            if (($_ix['Key_name'] ?? '') === 'uq_company_cat_provider_key') $_st_has_new_uq = true;
        }
    }
    if ($_st_has_old_uq) $db->query("ALTER TABLE pp_settings DROP INDEX `uq_cat_provider_key`");
    if (!$_st_has_new_uq) {
        $db->query("ALTER TABLE pp_settings ADD UNIQUE KEY `uq_company_cat_provider_key` (`company_id`, `category`, `provider_slug`, `key`)");
    }
    unset($_st_cols, $_st_q, $_st_ix_q, $_st_has_old_uq, $_st_has_new_uq, $_ix, $_c);
}

// ====================================================================
// PROVIDER MANAGER
// ====================================================================
// Reads ALL config from the `pp_settings` table in the resolved client DB.
// Keys/tokens are NEVER written to disk; bearer_token is stored in pp_settings.
// ====================================================================
class ProviderManager {
    private $db;
    private int $company_id;
    /** @var array<string,string> Flat key=>value map for active shipping provider */
    private array $ship   = [];
    /** @var array<string,string> Flat key=>value map for active ecommerce provider */
    private array $ecom   = [];
    private string $ship_slug = 'packie';
    private string $ecom_slug = '';

    public function getShipSlug(): string { return $this->ship_slug; }
    public function getEcomSlug(): string  { return $this->ecom_slug; }
    public function getShipConfig(): array { return array_merge($this->ship, ['_slug' => $this->ship_slug]); }

    public function __construct($db, int $company_id = 1) {
        $this->db         = $db;
        $this->company_id = $company_id;
        $this->load();
    }

    private function load(): void {
        // Read active provider slugs
        $stmt = $this->db->prepare("
            SELECT provider_slug, `key`, `value` FROM pp_settings
            WHERE company_id = ? AND category = 'general' AND provider_slug = 'app'
        ");
        $stmt->bind_param('i', $this->company_id);
        $stmt->execute();
        $res = $stmt->get_result();
        $general = [];
        if ($res) while ($r = $res->fetch_assoc()) $general[$r['key']] = $r['value'];
        if (!empty($general['active_shipping_provider'])) $this->ship_slug = $general['active_shipping_provider'];
        if (array_key_exists('active_ecom_provider', $general)) {
            $candidate = trim((string)$general['active_ecom_provider']);
            $this->ecom_slug = in_array(strtolower($candidate), ['', 'none', 'disabled', 'off'], true) ? '' : $candidate;
        }

        // Load all pp_settings rows for both active providers
        $stmt2 = $this->db->prepare("
            SELECT category, `key`, `value`, updated_at FROM pp_settings
            WHERE company_id = ?
              AND ((category = 'shipping'   AND provider_slug = ?)
                OR (category = 'ecommerce'  AND provider_slug = ?))
        ");
        $stmt2->bind_param('iss', $this->company_id, $this->ship_slug, $this->ecom_slug);
        $stmt2->execute();
        $res2 = $stmt2->get_result();
        if ($res2) while ($r = $res2->fetch_assoc()) {
            if ($r['category'] === 'shipping') {
                $this->ship[$r['key']] = $r['value'];
                if ($r['key'] === 'bearer_token') $this->ship['_bearer_token_updated_at'] = (string)($r['updated_at'] ?? '');
            }
            if ($r['category'] === 'ecommerce')  $this->ecom[$r['key']] = $r['value'];
        }
    }

    // ------------------------------------------------------------------
    // pp_settings write helper — used by run_setup and save_provider_config
    // ------------------------------------------------------------------
    public function saveSetting(string $category, string $provider_slug, string $key, string $value): void {
        $stmt = $this->db->prepare("
            INSERT INTO pp_settings (company_id, category, provider_slug, `key`, `value`)
            VALUES (?, ?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)
        ");
        $stmt->bind_param('issss', $this->company_id, $category, $provider_slug, $key, $value);
        $stmt->execute();
    }

    // ------------------------------------------------------------------
    // Ecommerce helpers
    // ------------------------------------------------------------------

    /** Expose raw ecommerce config so adapter transport functions can read credentials. */
    public function getEcomConfig(): array {
        return array_merge($this->ecom, ['_slug' => $this->ecom_slug]);
    }

    /**
     * Delegate to the ecommerce adapter's ecommerce_http_call() function.
     * Each adapter registers that function when included; returns null when no adapter is loaded.
     */
    public function ecomCall(string $method, string $endpoint, array $data = []): ?array {
        if (function_exists('ecommerce_http_call')) {
            return ecommerce_http_call($method, $endpoint, $data);
        }
        return null;
    }

    public function ecomGraphql(string $query, array $variables = []): ?array {
        if (function_exists('ecommerce_graphql_call')) {
            return ecommerce_graphql_call($query, $variables);
        }
        return null;
    }

    // ------------------------------------------------------------------
    // Shipping helpers
    // ------------------------------------------------------------------
    public function useShippingProvider(string $provider_slug): void {
        $provider_slug = strtolower(preg_replace('/[^a-z0-9_\-]/', '', trim($provider_slug)));
        if ($provider_slug === '' || $provider_slug === $this->ship_slug) return;

        $this->ship_slug = $provider_slug;
        $this->ship = [];

        $stmt = $this->db->prepare("
            SELECT `key`, `value`, updated_at
            FROM pp_settings
            WHERE company_id = ?
              AND category = 'shipping'
              AND provider_slug = ?
        ");
        $stmt->bind_param('is', $this->company_id, $this->ship_slug);
        $stmt->execute();
        $res = $stmt->get_result();
        if ($res) while ($r = $res->fetch_assoc()) {
            $this->ship[$r['key']] = $r['value'];
            if ($r['key'] === 'bearer_token') $this->ship['_bearer_token_updated_at'] = (string)($r['updated_at'] ?? '');
        }
    }

    private function shipBaseUrl(): string {
        if ($this->ship_slug === 'zappy') {
            $default = 'https://ship.zappy.nz';
        } elseif ($this->ship_slug === 'courierit1') {
            $default = 'https://courierit1.net.nz/api';
        } else {
            $default = 'https://www.packie.co.nz/api';
        }
        return rtrim($this->ship['base_url'] ?? $default, '/');
    }
    private function shipProvider(): string {
        return $this->ship_slug;
    }
    public function senderDetails(): array {
        return [
            'contactName'   => $this->ship['sender_contact_name'] ?? 'Manager',
            'company'       => $this->ship['sender_company']      ?? '',
            'streetAddress' => $this->ship['sender_street']       ?? '',
            'suburb'        => $this->ship['sender_suburb']       ?? '',
            'city'          => $this->ship['sender_city']         ?? '',
            'postCode'      => $this->ship['sender_postcode']     ?? '',
            'country'       => $this->ship['sender_country']      ?? 'NZ',
            'email'         => $this->ship['sender_email']        ?? '',
            'phone'         => $this->ship['sender_phone']        ?? '',
            'addressId'     => $this->ship['sender_address_id']   ?? '',
        ];
    }

    public function refreshShipToken(): bool {
        $provider = $this->shipProvider();
        $base     = $this->shipBaseUrl();

        if (in_array($provider, ['packie', 'courierit1'], true)) {
            $username = $this->ship['username'] ?? '';
            $password = $this->ship['api_key']  ?? '';
            if (!$username || !$password) return false;

            $ch = curl_init($base . '/users/login');
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['username' => $username, 'password' => $password, 'remember' => true, 'shopify' => null]));
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Accept: application/json']);
            $response  = curl_exec($ch);
            $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($http_code === 200 || $http_code === 201) {
                $data  = json_decode($response, true);
                $token = $data['result']['token'] ?? $data['token'] ?? $data['access_token'] ?? $data['accessToken'] ?? null;
                if (!$token && is_string($response) && preg_match('/eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/', $response, $m)) {
                    $token = $m[0];
                }
                if ($token) {
                    // Store token in DB — never in a file
                    $this->saveSetting('shipping', $provider, 'bearer_token', $token);
                    $this->ship['bearer_token'] = $token;
                    $this->ship['_bearer_token_updated_at'] = gmdate('Y-m-d H:i:s');
                    return true;
                }
            }
            return false;
        }
        // Other providers: extend here
        return false;
    }

    public function ensureShipTokenFresh(): bool {
        if (!in_array($this->shipProvider(), ['packie', 'courierit1'], true)) return true;

        $token = trim((string)($this->ship['bearer_token'] ?? ''));
        $updated_raw = trim((string)($this->ship['_bearer_token_updated_at'] ?? ''));
        $updated_ts = $updated_raw !== '' ? strtotime($updated_raw . ' UTC') : 0;
        $max_age_key = $this->shipProvider() === 'courierit1' ? 'COURIERIT1_TOKEN_REFRESH_SECONDS' : 'PACKIE_TOKEN_REFRESH_SECONDS';
        $max_age = (int)($_ENV[$max_age_key] ?? 72000); // 20 hours
        if ($max_age < 3600) $max_age = 3600;

        if ($token !== '' && preg_match('/^[^.]+\.([^.]+)\./', $token, $jwt_parts)) {
            $payload_json = base64_decode(strtr($jwt_parts[1], '-_', '+/'));
            $payload = json_decode((string)$payload_json, true);
            $exp = (int)($payload['exp'] ?? 0);
            if ($exp > 0 && $exp <= time() + 600) {
                $updated_ts = 0;
            }
        }

        $is_stale = $token === '' || !$updated_ts || (time() - $updated_ts) >= $max_age;
        if (!$is_stale) return true;

        Logger::info($this->shipProvider() . '_auth', 'refresh_needed', [
            'has_token' => $token !== '' ? '1' : '0',
            'updated_at' => $updated_raw,
            'max_age' => $max_age,
        ]);

        $ok = $this->refreshShipToken();
        Logger::info($this->shipProvider() . '_auth', $ok ? 'refresh_success' : 'refresh_failed');
        return $ok;
    }

    public function getShipToken(): ?string {
        // Read from DB (already loaded into $this->ship)
        $token = $this->ship['bearer_token'] ?? '';
        return $token !== '' ? $token : null;
    }

    public function getShipHeaders(): ?array {
        $this->ensureShipTokenFresh();
        $token = $this->getShipToken();
        if (!$token) return null;
        // All current providers use Bearer auth
        return ['Content-Type: application/json', 'Accept: application/json', 'Authorization: Bearer ' . $token];
    }

    public function shipUrl(string $path): string {
        return $this->shipBaseUrl() . '/' . ltrim($path, '/');
    }

  
public function detectShipStatus(string $street, string $suburb, string $city, string $postcode, string $country = 'NZ', string $debug_order = '', int $packie_order_id = 0): string {
    $debug     = ($debug_order === '17127');
    //$debug = true; // TESTING — remove or condition this in production
    // Local validation runs first — before any API call or token check.
    $street   = trim($street);
    $suburb   = trim($suburb);
    $city     = trim($city);
    $postcode = trim($postcode);
    $country  = (strlen($country) > 2) ? 'NZ' : strtoupper($country);

    if ($street === '' && $suburb !== '') { $street = $suburb; $suburb = ''; }
    if ($street === '' || $city === '') {
        if ($debug) Logger::debug('detectShipStatus', 'INVALID (local check)', ['street' => $street, 'suburb' => $suburb, 'city' => $city]);
        return 'INVALID';
    }

    $headers  = $this->getShipHeaders();
    if (!$headers) {
        if ($debug) Logger::debug('detectShipStatus', 'READY (no bearer token)', ['street' => $street, 'city' => $city]);
        return 'READY'; // token unavailable — local checks passed, can't do rural check
    }
    $provider = $this->shipProvider();

    if ($provider === 'packie') {
        // Use real stock dimensions from pp_packie_stocks so Packie can evaluate rates.
        // Fall back to a generic A4-pack size if the table is empty.
        // Default: NZ Post Courier Pack A4 dimensions — known to work with Packie calculate.
        // Prefer serviceType=4 stock from DB; fall back to these hardcoded safe values.
        $parcel = ['id' => '4', 'height' => '29.7', 'width' => '1', 'length' => '21', 'weight' => '1', 'quantity' => '1', 'serviceType' => 4];
        $stock_row = $this->db->query("SELECT id, height, width, length, weight, serviceType FROM pp_packie_stocks WHERE serviceType = 4 AND height > 0 AND weight > 0 ORDER BY id ASC LIMIT 1");
        if ($stock_row && $s = $stock_row->fetch_assoc()) {
            $parcel = [
                'id'          => (string)$s['id'],
                'height'      => (string)$s['height'],
                'width'       => (string)$s['width'],
                'length'      => (string)$s['length'],
                'weight'      => (string)$s['weight'],
                'quantity'    => '1',
                'serviceType' => (int)$s['serviceType'],
            ];
        }

        $payload = [
            'consignmentParcels'  => [$parcel],
            'hasDangerousGoods'   => false,
            'integrationType'     => 1,
            'isInternational'     => ($country !== 'NZ'),
            'receiver'            => ['suburb' => $suburb, 'city' => $city, 'country' => $country, 'streetAddress' => $street, 'postCode' => $postcode],
            'sender'              => $this->senderDetails(),
            'shippingDescription' => 'Fastest Delivery',
            'shippingMethod'      => 'Fastest Delivery',
            'signatureRequired'   => false,
        ];
        if ($packie_order_id > 0) {
            $payload['integratedOrderId'] = $packie_order_id;
        }

        if ($debug) Logger::debug('detectShipStatus', 'Packie calculate REQUEST', ['payload' => $payload]);

        $ch = curl_init($this->shipUrl('Consignments/calculate'));
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
        $raw_res   = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($debug) Logger::debug('detectShipStatus', 'Packie calculate RESPONSE', ['http_code' => $http_code, 'body' => $raw_res]);

        $res = json_decode($raw_res, true);

        // Curl/parse failure — be lenient, don't flag as invalid.
        if ($res === null || !isset($res['result']['rates'])) {
            if ($debug) Logger::debug('detectShipStatus', 'READY (null/no rates key — curl or parse failure)');
            return 'READY';
        }
        // Packie responded but returned no rates — address is not serviceable.
        if (empty($res['result']['rates'])) {
            if ($debug) Logger::debug('detectShipStatus', 'INVALID (Packie returned empty rates array)');
            return 'INVALID';
        }
        foreach ($res['result']['rates'] as $rate) {
            if (!empty($rate['rural_rate']) && (float)$rate['rural_rate'] > 0) {
                if ($debug) Logger::debug('detectShipStatus', 'RURAL (rural_rate found)', ['rural_rate' => $rate['rural_rate']]);
                return 'RURAL';
            }
            if (!empty($rate['is_rural']) && $rate['is_rural'] == true) {
                if ($debug) Logger::debug('detectShipStatus', 'RURAL (is_rural=true)');
                return 'RURAL';
            }
        }
        if ($debug) Logger::debug('detectShipStatus', 'READY (rates returned, no rural flags)');
        return 'READY';
    }
    return 'READY';
}
} // end class ProviderManager

function afri_seed_package_presets(mysqli $db): void {
    $defaults = [
        ['DL', 'Satchel DL', 23.00, 11.00, 2.00, 0.50, 1],
        ['A5', 'Satchel A5', 25.00, 20.00, 2.00, 1.00, 2],
        ['A4', 'Satchel A4', 33.00, 25.00, 2.00, 5.00, 3],
        ['A3', 'Satchel A3', 42.00, 30.00, 2.00, 5.00, 4],
        ['A2', 'Satchel A2', 59.00, 42.00, 2.00, 5.00, 5],
    ];

    $stmt = $db->prepare("
        INSERT INTO pp_package_presets (code, display_name, length, width, height, weight, sort_order, is_enabled)
        VALUES (?, ?, ?, ?, ?, ?, ?, 1)
        ON DUPLICATE KEY UPDATE
            display_name = IF(display_name = '', VALUES(display_name), display_name),
            sort_order = VALUES(sort_order),
            is_enabled = is_enabled
    ");
    if (!$stmt) return;

    foreach ($defaults as $preset) {
        [$code, $name, $length, $width, $height, $weight, $sort] = $preset;
        $stmt->bind_param('ssddddi', $code, $name, $length, $width, $height, $weight, $sort);
        $stmt->execute();
    }
}

function afri_package_code_from_name(string $name): string {
    if (preg_match('/\b(DL|A5|A4|A3|A2)\b/i', $name, $m)) {
        return strtoupper($m[1]);
    }
    return '';
}

function afri_packie_carrier_from_stock_name(string $name): string {
    $name = trim($name);
    if ($name === '') return '';
    if (preg_match('/^\s*([A-Za-z0-9 &+._-]{2,40})\s*(?:-|:|\/|\|)/', $name, $m)) {
        return trim($m[1]);
    }
    return '';
}

function afri_packie_default_quick_pick_name(string $name): string {
    $code = afri_package_code_from_name($name);
    if ($code !== '') return 'Satchel ' . $code;
    return trim($name) !== '' ? trim($name) : 'Packie stock';
}

function afri_packie_stock_group_key(array $row): string {
    $code = afri_package_code_from_name((string)($row['name'] ?? ''));
    if ($code !== '') return 'code:' . $code;
    return 'dims:' . implode('x', [
        (float)($row['length'] ?? 0),
        (float)($row['width'] ?? 0),
        (float)($row['height'] ?? 0),
        (float)($row['weight'] ?? 0),
    ]);
}

function afri_packie_carrier_label_from_stock(array $row): string {
    $saved = trim((string)($row['quick_pick_carrier'] ?? ''));
    if ($saved !== '') return $saved;
    $detected = afri_packie_carrier_from_stock_name((string)($row['name'] ?? ''));
    return $detected;
}

function afri_packie_stock_option_from_row(array $row): array {
    $name = (string)($row['name'] ?? '');
    $display_name = trim((string)($row['quick_pick_display_name'] ?? ''));
    $carrier = trim((string)($row['quick_pick_carrier'] ?? ''));
    if ($display_name === '') $display_name = afri_packie_default_quick_pick_name($name);

    return [
        'id' => (string)$row['id'],
        'label' => $name,
        'name' => $name,
        'display_name' => $display_name,
        'display_label' => $display_name !== '' ? $display_name : $name,
        'serviceType' => (int)$row['serviceType'],
        'length' => (float)$row['length'],
        'width' => (float)$row['width'],
        'height' => (float)$row['height'],
        'weight' => (float)$row['weight'],
        'quick_pick_enabled' => !empty($row['quick_pick_enabled']),
        'quick_pick_carrier' => $carrier,
        'quick_pick_is_default' => !empty($row['quick_pick_is_default']),
        'quick_pick_carrier_options' => [],
    ];
}

function afri_packie_attach_stock_carrier_options(array $options, array $global_carriers = []): array {
    $groups = [];
    foreach ($options as $option) {
        $groups[afri_packie_stock_group_key($option)][] = $option;
    }

    foreach ($options as &$option) {
        $carrier_options = [];
        foreach ($global_carriers as $carrier) {
            $carrier = trim((string)$carrier);
            if ($carrier !== '') $carrier_options[$carrier] = true;
        }
        foreach ($groups[afri_packie_stock_group_key($option)] ?? [$option] as $candidate) {
            $carrier = afri_packie_carrier_label_from_stock($candidate);
            if ($carrier !== '' && empty($global_carriers)) $carrier_options[$carrier] = true;
        }
        $saved = trim((string)($option['quick_pick_carrier'] ?? ''));
        if ($saved !== '') $carrier_options[$saved] = true;
        $option['quick_pick_carrier_options'] = array_keys($carrier_options);
    }
    unset($option);
    return $options;
}

function afri_packie_fetch_checkout_carriers(): array {
    global $pm;
    if (!$pm || !method_exists($pm, 'getShipHeaders') || !method_exists($pm, 'shipUrl')) return [];
    $headers = $pm->getShipHeaders();
    if (!$headers) return [];

    $ch = curl_init($pm->shipUrl('checkout-rates'));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge($headers, ['ngrok-skip-browser-warning: true']));
    curl_setopt($ch, CURLOPT_TIMEOUT, 15);
    $raw = curl_exec($ch);
    $err = curl_error($ch);
    $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $decoded = json_decode((string)$raw, true);
    if ($err !== '' || $http >= 400 || empty($decoded['success'])) {
        if (class_exists('Logger')) {
            Logger::warn('packie_checkout_rates', 'carrier_fetch_failed', ['http' => $http, 'error' => $err]);
        }
        return [];
    }

    $seen = [];
    foreach (($decoded['result']['services'] ?? []) as $service) {
        if (empty($service['enabled'])) continue;
        if (!empty($service['isInternational'])) continue;
        $courier = trim((string)($service['courier'] ?? ''));
        if ($courier !== '') $seen[$courier] = true;
    }
    return array_keys($seen);
}

function afri_sync_packie_package_map(mysqli $db): void {
    afri_seed_package_presets($db);

    $res = $db->query("SELECT id, name, serviceType, length, width, height, weight FROM pp_packie_stocks ORDER BY id ASC");
    if (!$res) return;

    $map_stmt = $db->prepare("
        INSERT INTO pp_provider_package_map
            (provider_slug, package_code, provider_stock_id, provider_service_type, provider_label, length, width, height, weight, is_enabled)
        VALUES
            ('packie', ?, ?, ?, ?, ?, ?, ?, ?, 1)
        ON DUPLICATE KEY UPDATE
            provider_stock_id = VALUES(provider_stock_id),
            provider_service_type = VALUES(provider_service_type),
            provider_label = VALUES(provider_label),
            length = VALUES(length),
            width = VALUES(width),
            height = VALUES(height),
            weight = VALUES(weight),
            is_enabled = 1
    ");
    $preset_stmt = $db->prepare("
        UPDATE pp_package_presets
        SET display_name = ?, length = ?, width = ?, height = ?, weight = ?
        WHERE code = ? AND is_enabled = 1
    ");
    if (!$map_stmt || !$preset_stmt) return;

    $seen = [];
    while ($stock = $res->fetch_assoc()) {
        $code = afri_package_code_from_name((string)($stock['name'] ?? ''));
        if ($code === '' || isset($seen[$code])) continue;
        $seen[$code] = true;

        $provider_stock_id = (string)($stock['id'] ?? '');
        $provider_service_type = (string)($stock['serviceType'] ?? '');
        $provider_label = (string)($stock['name'] ?? ('Satchel ' . $code));
        $length = (float)($stock['length'] ?? 0);
        $width = (float)($stock['width'] ?? 0);
        $height = (float)($stock['height'] ?? 0);
        $weight = (float)($stock['weight'] ?? 0);

        $map_stmt->bind_param(
            'ssssdddd',
            $code,
            $provider_stock_id,
            $provider_service_type,
            $provider_label,
            $length,
            $width,
            $height,
            $weight
        );
        $map_stmt->execute();

        $display_name = 'Satchel ' . $code;
        $preset_stmt->bind_param('sdddds', $display_name, $length, $width, $height, $weight, $code);
        $preset_stmt->execute();
    }
}

function afri_list_package_presets(mysqli $db): array {
    afri_seed_package_presets($db);
    afri_sync_packie_package_map($db);

    $sql = "
        SELECT
            p.code, p.display_name, p.length, p.width, p.height, p.weight, p.sort_order,
            m.provider_stock_id AS packie_stock_id,
            m.provider_service_type AS packie_service_type,
            m.provider_label AS packie_label
        FROM pp_package_presets p
        LEFT JOIN pp_provider_package_map m
            ON m.provider_slug = 'packie'
            AND m.package_code = p.code
            AND m.is_enabled = 1
        WHERE p.is_enabled = 1
        ORDER BY p.sort_order ASC, p.display_name ASC
    ";
    $res = $db->query($sql);
    $packages = [];
    if (!$res) return $packages;

    while ($row = $res->fetch_assoc()) {
        $packages[] = [
            'id' => (string)$row['code'],
            'code' => (string)$row['code'],
            'package_code' => (string)$row['code'],
            'name' => (string)$row['display_name'],
            'display_name' => (string)$row['display_name'],
            'length' => (float)$row['length'],
            'width' => (float)$row['width'],
            'height' => (float)$row['height'],
            'weight' => (float)$row['weight'],
            'sort_order' => (int)$row['sort_order'],
            'providerStockId' => $row['packie_stock_id'] !== null ? (int)$row['packie_stock_id'] : null,
            'packie_stock_id' => $row['packie_stock_id'] !== null ? (int)$row['packie_stock_id'] : null,
            'packie_service_type' => $row['packie_service_type'] !== null ? (int)$row['packie_service_type'] : null,
            'packie_label' => $row['packie_label'],
        ];
    }
    return $packages;
}

function afri_list_packie_quick_stocks(mysqli $db): array {
    if (function_exists('sync_packie_stocks')) {
        $has = $db->query("SELECT id FROM pp_packie_stocks LIMIT 1");
        if (!$has || $has->num_rows === 0) @sync_packie_stocks($db);
    }

    $res = $db->query("
        SELECT id, name, serviceType, length, width, height, weight, quick_pick_enabled, quick_pick_carrier, quick_pick_display_name, quick_pick_is_default
        FROM pp_packie_stocks
        WHERE quick_pick_enabled = 1
        ORDER BY quick_pick_is_default DESC,
                 COALESCE(NULLIF(quick_pick_carrier, ''), name) ASC,
                 COALESCE(NULLIF(quick_pick_display_name, ''), name) ASC,
                 id ASC
    ");
    $stocks = [];
    if ($res) {
        while ($row = $res->fetch_assoc()) {
            $option = afri_packie_stock_option_from_row($row);
            $code = afri_package_code_from_name($option['display_label'] ?: $option['label']);
            $stocks[] = [
                'id' => (string)$row['id'],
                'code' => $code,
                'package_code' => $code,
                'name' => $option['display_label'],
                'display_name' => $option['display_label'],
                'carrier' => $option['quick_pick_carrier'],
                'length' => $option['length'],
                'width' => $option['width'],
                'height' => $option['height'],
                'weight' => $option['weight'],
                'providerStockId' => (int)$row['id'],
                'packie_stock_id' => (int)$row['id'],
                'packie_service_type' => $option['serviceType'],
                'packie_label' => $option['label'],
                'quick_pick_enabled' => true,
                'quick_pick_is_default' => !empty($row['quick_pick_is_default']),
            ];
        }
    }

    return $stocks;
}

function afri_resolve_package_preset(mysqli $db, $package_ref): ?array {
    afri_seed_package_presets($db);
    $ref = strtoupper(trim((string)$package_ref));
    if ($ref === '') return null;

    if (ctype_digit($ref)) {
        $stmt = $db->prepare("SELECT name, length, width, height, weight FROM pp_packie_stocks WHERE id = ? LIMIT 1");
        $id = (int)$ref;
        $stmt->bind_param('i', $id);
        $stmt->execute();
        $stock = $stmt->get_result()->fetch_assoc();
        if (!$stock) return null;
        $code = afri_package_code_from_name((string)($stock['name'] ?? ''));
        return [
            'code' => $code ?: $ref,
            'name' => $stock['name'] ?? ('Package ' . $ref),
            'length' => (float)($stock['length'] ?? 0),
            'width' => (float)($stock['width'] ?? 0),
            'height' => (float)($stock['height'] ?? 0),
            'weight' => (float)($stock['weight'] ?? 0),
        ];
    }

    $code = preg_replace('/[^A-Z0-9]/', '', $ref);
    $stmt = $db->prepare("SELECT code, display_name, length, width, height, weight FROM pp_package_presets WHERE code = ? AND is_enabled = 1 LIMIT 1");
    $stmt->bind_param('s', $code);
    $stmt->execute();
    $preset = $stmt->get_result()->fetch_assoc();
    if (!$preset) return null;
    return [
        'code' => (string)$preset['code'],
        'name' => (string)$preset['display_name'],
        'length' => (float)$preset['length'],
        'width' => (float)$preset['width'],
        'height' => (float)$preset['height'],
        'weight' => (float)$preset['weight'],
    ];
}

function afri_resolve_packie_stock(mysqli $db, $stock_ref): ?array {
    if (function_exists('sync_packie_stocks')) {
        $has = $db->query("SELECT id FROM pp_packie_stocks LIMIT 1");
        if (!$has || $has->num_rows === 0) @sync_packie_stocks($db);
    }
    afri_sync_packie_package_map($db);

    $ref = trim((string)$stock_ref);
    if ($ref === '') return null;

    if (ctype_digit($ref)) {
        $id = (int)$ref;
        $stmt = $db->prepare("SELECT * FROM pp_packie_stocks WHERE id = ? LIMIT 1");
        $stmt->bind_param('i', $id);
        $stmt->execute();
        $stock = $stmt->get_result()->fetch_assoc();
        return $stock ?: null;
    }

    $code = strtoupper(preg_replace('/[^A-Z0-9]/', '', $ref));
    $stmt = $db->prepare("
        SELECT s.*
        FROM pp_provider_package_map m
        JOIN pp_packie_stocks s ON s.id = CAST(m.provider_stock_id AS UNSIGNED)
        WHERE m.provider_slug = 'packie'
          AND m.package_code = ?
          AND m.is_enabled = 1
        LIMIT 1
    ");
    $stmt->bind_param('s', $code);
    $stmt->execute();
    $stock = $stmt->get_result()->fetch_assoc();
    if ($stock) return $stock;

    $like = '%' . $code . '%';
    $stmt = $db->prepare("SELECT * FROM pp_packie_stocks WHERE name LIKE ? ORDER BY id ASC LIMIT 1");
    $stmt->bind_param('s', $like);
    $stmt->execute();
    $stock = $stmt->get_result()->fetch_assoc();
    return $stock ?: null;
}

function afri_provider_stock_options(mysqli $db, string $provider_slug): array {
    $provider_slug = strtolower(preg_replace('/[^a-z0-9_\-]/', '', $provider_slug));
    $hook = $provider_slug . '_provider_stock_options';
    if ($provider_slug !== 'packie' && function_exists($hook)) {
        $options = $hook();
        return is_array($options) ? $options : [];
    }

    if ($provider_slug === 'packie') {
        if (function_exists('sync_packie_stocks')) {
            $has = $db->query("SELECT id FROM pp_packie_stocks LIMIT 1");
            if (!$has || $has->num_rows === 0) @sync_packie_stocks($db);
        }
        afri_sync_packie_package_map($db);
        $res = $db->query("
            SELECT id, name, serviceType, length, width, height, weight,
                   quick_pick_enabled, quick_pick_carrier, quick_pick_display_name, quick_pick_is_default
            FROM pp_packie_stocks
            ORDER BY name ASC, id ASC
        ");
        $options = [];
        if ($res) {
            while ($row = $res->fetch_assoc()) {
                $options[] = afri_packie_stock_option_from_row($row);
            }
        }
        $carriers = afri_packie_fetch_checkout_carriers();
        return afri_packie_attach_stock_carrier_options($options, $carriers);
    }

    return [];
}

function afri_packie_carrier_options(mysqli $db): array {
    $packie_carriers = afri_packie_fetch_checkout_carriers();
    if (!empty($packie_carriers)) return $packie_carriers;

    $res = $db->query("SELECT name, quick_pick_carrier FROM pp_packie_stocks ORDER BY name ASC");
    $seen = [];
    if ($res) {
        while ($row = $res->fetch_assoc()) {
            $values = [
                trim((string)($row['quick_pick_carrier'] ?? '')),
                afri_packie_carrier_from_stock_name((string)($row['name'] ?? '')),
            ];
            foreach ($values as $value) {
                if ($value !== '') $seen[$value] = true;
            }
        }
    }
    return array_keys($seen);
}

function afri_get_package_mapping(mysqli $db, string $provider_slug): array {
    $provider_slug = strtolower(preg_replace('/[^a-z0-9_\-]/', '', $provider_slug));
    afri_seed_package_presets($db);
    if ($provider_slug === 'packie') afri_sync_packie_package_map($db);

    $stmt = $db->prepare("
        SELECT
            p.code, p.display_name, p.length AS preset_length, p.width AS preset_width,
            p.height AS preset_height, p.weight AS preset_weight, p.sort_order,
            m.provider_stock_id, m.provider_service_type, m.provider_label,
            m.length AS mapped_length, m.width AS mapped_width, m.height AS mapped_height, m.weight AS mapped_weight
        FROM pp_package_presets p
        LEFT JOIN pp_provider_package_map m
            ON m.provider_slug = ?
            AND m.package_code = p.code
            AND m.is_enabled = 1
        WHERE p.is_enabled = 1
        ORDER BY p.sort_order ASC, p.display_name ASC
    ");
    $stmt->bind_param('s', $provider_slug);
    $stmt->execute();
    $res = $stmt->get_result();

    $packages = [];
    while ($row = $res->fetch_assoc()) {
        $packages[] = [
            'code' => (string)$row['code'],
            'name' => (string)$row['display_name'],
            'provider_stock_id' => $row['provider_stock_id'] !== null ? (string)$row['provider_stock_id'] : '',
            'provider_service_type' => $row['provider_service_type'] !== null ? (string)$row['provider_service_type'] : '',
            'provider_label' => $row['provider_label'] !== null ? (string)$row['provider_label'] : '',
            'length' => (float)($row['mapped_length'] ?? $row['preset_length']),
            'width' => (float)($row['mapped_width'] ?? $row['preset_width']),
            'height' => (float)($row['mapped_height'] ?? $row['preset_height']),
            'weight' => (float)($row['mapped_weight'] ?? $row['preset_weight']),
            'sort_order' => (int)$row['sort_order'],
        ];
    }

    $response = [
        'success' => true,
        'provider_slug' => $provider_slug,
        'packages' => $packages,
        'stock_options' => afri_provider_stock_options($db, $provider_slug),
    ];
    if ($provider_slug === 'packie') {
        $response['carrier_options'] = afri_packie_carrier_options($db);
    }
    return $response;
}

function afri_save_packie_stock_settings(mysqli $db, array $stock_settings): int {
    if (empty($stock_settings)) return 0;
    $stmt = $db->prepare("
        UPDATE pp_packie_stocks
        SET quick_pick_enabled = ?,
            quick_pick_carrier = ?,
            quick_pick_display_name = ?,
            quick_pick_is_default = ?
        WHERE id = ?
    ");
    if (!$stmt) return 0;

    $saved = 0;
    foreach ($stock_settings as $stock) {
        $id = (int)($stock['id'] ?? 0);
        if ($id <= 0) continue;
        $enabled = !empty($stock['quick_pick_enabled']) ? 1 : 0;
        $carrier = trim((string)($stock['quick_pick_carrier'] ?? ''));
        $display_name = trim((string)($stock['quick_pick_display_name'] ?? $stock['display_name'] ?? $stock['label'] ?? $stock['name'] ?? ''));
        $is_default = (!empty($stock['quick_pick_is_default']) && $enabled) ? 1 : 0;
        if (strlen($carrier) > 100) $carrier = substr($carrier, 0, 100);
        if (strlen($display_name) > 100) $display_name = substr($display_name, 0, 100);
        $stmt->bind_param('issii', $enabled, $carrier, $display_name, $is_default, $id);
        $stmt->execute();
        if ($is_default) {
            $clear = $db->prepare("UPDATE pp_packie_stocks SET quick_pick_is_default = 0 WHERE id <> ?");
            if ($clear) {
                $clear->bind_param('i', $id);
                $clear->execute();
            }
        }
        $saved++;
    }
    return $saved;
}

function afri_save_package_mapping(mysqli $db, string $provider_slug, array $mappings, array $stock_settings = []): array {
    $provider_slug = strtolower(preg_replace('/[^a-z0-9_\-]/', '', $provider_slug));
    if ($provider_slug === '') return ['success' => false, 'error' => 'Invalid courier provider.'];

    afri_seed_package_presets($db);
    if ($provider_slug === 'packie') afri_sync_packie_package_map($db);

    $upsert = $db->prepare("
        INSERT INTO pp_provider_package_map
            (provider_slug, package_code, provider_stock_id, provider_service_type, provider_label, length, width, height, weight, is_enabled)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
        ON DUPLICATE KEY UPDATE
            provider_stock_id = VALUES(provider_stock_id),
            provider_service_type = VALUES(provider_service_type),
            provider_label = VALUES(provider_label),
            length = VALUES(length),
            width = VALUES(width),
            height = VALUES(height),
            weight = VALUES(weight),
            is_enabled = 1
    ");
    $preset_update = $db->prepare("
        UPDATE pp_package_presets
        SET length = ?, width = ?, height = ?, weight = ?
        WHERE code = ? AND is_enabled = 1
    ");
    if (!$upsert || !$preset_update) return ['success' => false, 'error' => 'Could not prepare package mapping save.'];

    $saved = 0;
    foreach ($mappings as $mapping) {
        $code = strtoupper(preg_replace('/[^A-Z0-9]/', '', (string)($mapping['package_code'] ?? $mapping['code'] ?? '')));
        if ($code === '') continue;

        $stock_id = trim((string)($mapping['provider_stock_id'] ?? ''));
        $service_type = trim((string)($mapping['provider_service_type'] ?? ''));
        $label = trim((string)($mapping['provider_label'] ?? ''));
        $length = (float)($mapping['length'] ?? 0);
        $width = (float)($mapping['width'] ?? 0);
        $height = (float)($mapping['height'] ?? 0);
        $weight = (float)($mapping['weight'] ?? 0);

        if ($provider_slug === 'packie' && $stock_id !== '') {
            $stock = afri_resolve_packie_stock($db, $stock_id);
            if ($stock) {
                $service_type = (string)($stock['serviceType'] ?? $service_type);
                $label = (string)($stock['name'] ?? $label);
                $length = (float)($stock['length'] ?? $length);
                $width = (float)($stock['width'] ?? $width);
                $height = (float)($stock['height'] ?? $height);
                $weight = (float)($stock['weight'] ?? $weight);
            }
        } elseif ($stock_id !== '') {
            $options = afri_provider_stock_options($db, $provider_slug);
            foreach ($options as $stock) {
                if ((string)($stock['id'] ?? '') !== $stock_id) continue;
                $service_type = (string)($stock['serviceType'] ?? $service_type);
                $label = (string)($stock['label'] ?? $stock['name'] ?? $label);
                $length = (float)($stock['length'] ?? $length);
                $width = (float)($stock['width'] ?? $width);
                $height = (float)($stock['height'] ?? $height);
                $weight = (float)($stock['weight'] ?? $weight);
                break;
            }
        }

        if ($stock_id === '' && $label === '' && $length <= 0 && $width <= 0 && $height <= 0 && $weight <= 0) continue;

        $upsert->bind_param('sssssdddd', $provider_slug, $code, $stock_id, $service_type, $label, $length, $width, $height, $weight);
        $upsert->execute();

        if ($length > 0 && $width > 0 && $height > 0 && $weight > 0) {
            $preset_update->bind_param('dddds', $length, $width, $height, $weight, $code);
            $preset_update->execute();
        }
        $saved++;
    }

    $stock_saved = $provider_slug === 'packie' ? afri_save_packie_stock_settings($db, $stock_settings) : 0;

    return [
        'success' => true,
        'saved' => $saved,
        'stock_saved' => $stock_saved,
        'mapping' => afri_get_package_mapping($db, $provider_slug),
    ];
}


// ====================================================================
// UNIVERSAL ROUTER
// ====================================================================
// Detect CLI mode by $argv presence + no HTTP_HOST rather than php_sapi_name()
// so the router works under LiteSpeed PHP (lsphp) which returns 'litespeed'.
if (isset($argv) && is_array($argv) && !isset($_SERVER['HTTP_HOST'])) {
    // Normalize CLI args: consume known --company/-c options first so the
    // remaining argv values map to action and positional args.
    $cli_args = $argv;
    array_shift($cli_args); // remove script name

    // Consume --company=slug or --company slug or -c slug
    foreach ($cli_args as $k => $v) {
        if (is_string($v) && strpos($v, '--company=') === 0) {
            $raw_input['company'] = substr($v, 10);
            unset($cli_args[$k]);
            continue;
        }
        if ($v === '--company' || $v === '-c') {
            $val = $cli_args[$k + 1] ?? null;
            if ($val) {
                $raw_input['company'] = $val;
                unset($cli_args[$k], $cli_args[$k + 1]);
            }
            break;
        }
    }
    $cli_args = array_values($cli_args);

    $action = $cli_args[0] ?? 'default';
    $input = [];
    $arg_oid = $cli_args[1] ?? null;
    $arg_staff = $cli_args[2] ?? 'Staff';
    $arg_stock = $cli_args[3] ?? 'none';
    $arg_rate = $cli_args[4] ?? 'none';
    $arg_w = $cli_args[5] ?? 0;
    $arg_l = $cli_args[6] ?? 0;
    $arg_h = $cli_args[7] ?? 0;
    $arg_weight = $cli_args[8] ?? 0;
    $arg_order_name = $cli_args[9] ?? '';
    $arg_rate_provider = $cli_args[10] ?? '';
    $arg_international_details = $cli_args[11] ?? '';
} else {
    $input = $raw_input;
    $action = $_GET['action'] ?? $input['action'] ?? $_GET['cron'] ?? 'get_orders';
}

$pm = null;
if ($_requires_client_db) {
    // ------------------------------------------------------------------
    // Resolve company_id for this request:
    //   - Authenticated requests: use the token's company_id
    //   - Cron / unauthenticated requests: fall back to company 1
    // ------------------------------------------------------------------
    $_req_company_id = 1;
    $_req_token = get_request_auth_token();
    if ($_req_token) {
        $_tok_stmt = $db->prepare("SELECT company_id FROM pp_auth_sessions WHERE token = ? AND expires_at > UTC_TIMESTAMP()");
        $_tok_stmt->bind_param('s', $_req_token);
        $_tok_stmt->execute();
        $_tok_row = $_tok_stmt->get_result()->fetch_assoc();
        if ($_tok_row) $_req_company_id = (int)$_tok_row['company_id'];
        unset($_tok_stmt, $_tok_row);
    }
    unset($_req_token);

    // Initialise the global ProviderManager — used by all wrapper functions below
    $pm = new ProviderManager($db, $_req_company_id);

    // ------------------------------------------------------------------
    // Ecommerce wrappers (available to both carrier and ecommerce adapters)
    // ------------------------------------------------------------------
    function ecom_call($method, $endpoint, $data = []) {
        global $pm;
        return $pm->ecomCall($method, $endpoint, $data);
    }
    function ecom_graphql($query, $variables = []) {
        global $pm;
        return $pm->ecomGraphql($query, $variables);
    }

    function afri_clean_packed_tags(string $tags): string {
        $clean = [];
        foreach (explode(',', $tags) as $tag) {
            $trimmed = trim($tag);
            if ($trimmed === '') continue;
            $lower = strtolower($trimmed);
            if ($lower === 'packed' || stripos($trimmed, 'Packed by') === 0) continue;
            $clean[] = $trimmed;
        }
        return implode(', ', array_values(array_unique($clean)));
    }

    function afri_extract_carrier_error($decoded, string $raw = '', string $curl_error = ''): string {
        if ($curl_error !== '') return $curl_error;
        if (is_array($decoded)) {
            foreach (['message', 'error', 'errormessage', 'errorMessage', 'Message'] as $key) {
                if (isset($decoded[$key]) && trim((string)$decoded[$key]) !== '') {
                    return trim((string)$decoded[$key]);
                }
            }
            foreach (['errors', 'errorMsgs', 'validationErrors'] as $key) {
                if (!empty($decoded[$key])) {
                    $value = $decoded[$key];
                    if (is_array($value)) {
                        $flat = [];
                        array_walk_recursive($value, function ($item) use (&$flat) {
                            $text = trim((string)$item);
                            if ($text !== '') $flat[] = $text;
                        });
                        if ($flat) return implode('; ', array_slice($flat, 0, 5));
                    }
                    $text = trim((string)$value);
                    if ($text !== '') return $text;
                }
            }
        }
        $raw = trim(strip_tags($raw));
        if ($raw !== '') return substr($raw, 0, 500);
        return 'Courier label creation failed.';
    }

    function afri_handle_label_failure(mysqli $db, string $order_id, string $order_name, string $shopify_num, string $tags, string $provider, string $message): array {
        $order_ref = $order_name !== '' ? $order_name : ($shopify_num !== '' ? '#' . $shopify_num : $order_id);
        $safe_message = trim(preg_replace('/\s+/', ' ', $message));
        if ($safe_message === '') $safe_message = 'Courier label creation failed.';
        $note = "Trouble printing - use desktop. {$provider} label failed for {$order_ref}: {$safe_message}";

        if ($order_id !== '') {
            $note_stmt = $db->prepare("INSERT INTO pp_order_notes (order_id, note_text) VALUES (?, ?) ON DUPLICATE KEY UPDATE note_text = VALUES(note_text)");
            if ($note_stmt) {
                $note_stmt->bind_param('ss', $order_id, $note);
                $note_stmt->execute();
            }
        }

        if ($shopify_num !== '') {
            $cache_stmt = $db->prepare("UPDATE pp_packie_cache SET packie_status = 'READY', label_consignment_id = 0, label_pdf_url = NULL WHERE shopify_order_number = ?");
            if ($cache_stmt) {
                $cache_stmt->bind_param('s', $shopify_num);
                $cache_stmt->execute();
            }
        }

        $remote_tags_updated = false;
        $remote_note_synced = false;
        if ($order_id !== '' && function_exists('ecommerce_update_order_tags')) {
            $remote_tags_updated = ecommerce_update_order_tags($order_id, afri_clean_packed_tags($tags));
        }
        if ($order_id !== '' && function_exists('ecommerce_sync_note')) {
            $sync = ecommerce_sync_note($order_id, $note);
            $remote_note_synced = !empty($sync['success']);
        }

        Logger::warn('label_failure', 'order_relisted', [
            'order_id' => $order_id,
            'order' => $order_ref,
            'provider' => $provider,
            'message' => $safe_message,
            'remote_tags_updated' => $remote_tags_updated ? '1' : '0',
            'remote_note_synced' => $remote_note_synced ? '1' : '0',
        ]);

        return [
            'success' => false,
            'error' => "{$order_ref} failed to create label: {$safe_message}",
            'note' => $note,
            'relisted' => true,
            'remote_tags_updated' => $remote_tags_updated,
            'remote_note_synced' => $remote_note_synced,
        ];
    }

    function afri_mark_order_packed_success(mysqli $db, string $order_id, string $shopify_num): void {
        if ($order_id !== '') {
            $s1 = $db->prepare("DELETE FROM pp_order_locks WHERE order_id = ?");
            if ($s1) {
                $s1->bind_param("s", $order_id);
                $s1->execute();
            }

            $s2 = $db->prepare("DELETE FROM pp_picked_items WHERE order_id = ?");
            if ($s2) {
                $s2->bind_param("s", $order_id);
                $s2->execute();
            }
        }

        if ($shopify_num !== '') {
            $s3 = $db->prepare("UPDATE pp_packie_cache SET packie_status = 'PRINTED' WHERE shopify_order_number = ?");
            if ($s3) {
                $s3->bind_param("s", $shopify_num);
                $s3->execute();
                Logger::info('mark_packed', 'cache_update', ['affected_rows' => $s3->affected_rows]);
            }
        }
    }

    function afri_parse_cli_json_result(array $lines): ?array {
        for ($i = count($lines) - 1; $i >= 0; $i--) {
            $line = trim((string)$lines[$i]);
            if ($line === '' || $line[0] !== '{') continue;
            $decoded = json_decode($line, true);
            if (is_array($decoded)) return $decoded;
        }
        return null;
    }

    // -- Load carrier adapter (defines carrier-specific helpers) --
    $_ship = $pm->getShipSlug();
    if (in_array(($action ?? ''), ['get_stock_quote', 'get_live_stocks', 'get_package_mapping', 'save_package_mapping', 'force_update_courier', 'print_label'], true)) {
        $_requested_ship = strtolower(preg_replace('/[^a-z0-9_\-]/', '', (string)($_GET['provider_slug'] ?? $input['provider_slug'] ?? '')));
        if ($_requested_ship === '' && ($action ?? '') === 'print_label' && isset($arg_rate_provider)) {
            $_requested_ship = strtolower(preg_replace('/[^a-z0-9_\-]/', '', (string)$arg_rate_provider));
        }
        if ($_requested_ship === '' && ($action ?? '') === 'print_label' && isset($arg_rate)) {
            $_decoded_rate = json_decode((string)base64_decode((string)$arg_rate, true), true);
            if (is_array($_decoded_rate) && !empty($_decoded_rate['provider'])) {
                $_requested_ship = strtolower(preg_replace('/[^a-z0-9_\-]/', '', (string)$_decoded_rate['provider']));
            }
            unset($_decoded_rate);
        }
        if ($_requested_ship !== '') {
            if (file_exists(__DIR__ . '/courier/' . $_requested_ship . '.php')) {
                $_ship = $_requested_ship;
            } elseif (($action ?? '') === 'get_stock_quote') {
                echo json_encode([
                    'success' => false,
                    'error' => ucfirst($_requested_ship) . ' live pricing is not implemented yet.',
                ]);
                exit;
            }
        }
        unset($_requested_ship);
    }
    if (($action ?? '') === 'print_label' && isset($_SERVER['HTTP_HOST'])) {
        $caller = validate_user_permission($db, 'can_print');
        if (!$caller) exit;
    }
    if (method_exists($pm, 'useShippingProvider')) $pm->useShippingProvider($_ship);
    if (file_exists(__DIR__ . '/courier/' . $_ship . '.php')) include __DIR__ . '/courier/' . $_ship . '.php';

    // -- Load ecommerce adapter --
    $_ecom = $pm->getEcomSlug();
    if (file_exists(__DIR__ . '/ecommerce/' . $_ecom . '.php')) include __DIR__ . '/ecommerce/' . $_ecom . '.php';
}

// ====================================================================
// AUTH HELPER
// ====================================================================
function get_request_auth_token(): ?string {
    $headers = function_exists('getallheaders') ? getallheaders() : [];
    $token = null;
    foreach ($headers as $k => $v) {
        if (strtolower($k) === 'x-auth-token') { $token = $v; break; }
    }
    if (!$token && isset($_SERVER['HTTP_X_AUTH_TOKEN'])) $token = $_SERVER['HTTP_X_AUTH_TOKEN'];
    if (!$token && isset($_SERVER['REDIRECT_HTTP_X_AUTH_TOKEN'])) $token = $_SERVER['REDIRECT_HTTP_X_AUTH_TOKEN'];
    if (!$token && isset($_SERVER['HTTP_AUTHORIZATION'])) {
        $auth = trim($_SERVER['HTTP_AUTHORIZATION']);
        if (stripos($auth, 'Bearer ') === 0) $token = trim(substr($auth, 7));
    }
    if (!$token) $token = $_GET['_token'] ?? null;
    if (!$token) return null;

    $token = preg_replace('/[^a-f0-9]/', '', strtolower(trim($token)));
    return strlen($token) === 64 ? $token : null;
}

function get_current_user_from_request($db) {
    $token = get_request_auth_token();
    if (!$token) return null;

    // Step 1: validate active session first.
    $s_stmt = $db->prepare("SELECT staff_id, company_id FROM pp_auth_sessions WHERE token = ? AND expires_at > UTC_TIMESTAMP() LIMIT 1");
    $s_stmt->bind_param("s", $token);
    $s_stmt->execute();
    $session = $s_stmt->get_result()->fetch_assoc();
    if (!$session) {
        Logger::info('auth.session_expired', 'Invalid or expired session token', ['token_prefix' => substr($token, 0, 8)]);
        return null;
    }

    $staff_id = (int)$session['staff_id'];
    $company_id = (int)$session['company_id'];

    // Step 2: resolve staff record. Use company_id from session for extra safety.
    $u_stmt = $db->prepare("SELECT id, name, username, role, can_pick, can_pack, can_print, company_id, is_active FROM pp_staff_members WHERE id = ? AND company_id = ? LIMIT 1");
    $u_stmt->bind_param("ii", $staff_id, $company_id);
    $u_stmt->execute();
    $user = $u_stmt->get_result()->fetch_assoc();

    // Return the real staff row regardless of is_active — active check belongs at login,
    // not here. A valid session token means the user is authenticated.
    if ($user) return $user;

    // Staff row genuinely missing (deleted after session was created) — minimal stub.
    return [
        'id' => $staff_id,
        'name' => 'Session User',
        'username' => '',
        'role' => 'secondary',
        'can_pick' => 0,
        'can_pack' => 0,
        'can_print' => 0,
        'company_id' => $company_id,
        'is_active' => 0,
    ];
}

// ====================================================================
// 2. APP API ENDPOINTS
// ====================================================================

// Client-side click logging from the React app. Stores a simple JSON
// blob to `client_clicks.log` for debugging UI interactions.
if ($action === 'client_click_log') {
    $user = get_current_user_from_request($db);
    if (!$user) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
    $entry = [
        'ts' => gmdate('c'),
        'company' => $_boot_company_slug ?? null,
        'user' => $user['name'] ?? null,
        'payload' => $input,
        'remote' => $_SERVER['REMOTE_ADDR'] ?? null,
    ];
    Logger::info('client_click', 'User click event', $entry);
    echo json_encode(['success' => true]);
    exit;
}

if ($action == 'get_orders') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
    if (function_exists('ecommerce_get_orders')) {
        echo json_encode(ecommerce_get_orders());
        exit;
    }
    if (function_exists('shipping_get_orders')) {
        echo json_encode(shipping_get_orders());
        exit;
    }
    echo json_encode([]);
    exit;
}

if ($action == 'get_sender_details') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }

    global $pm;
    $sender = $pm ? $pm->senderDetails() : [
        'contactName'   => 'Manager',
        'company'       => 'Afrihair Ltd',
        'streetAddress' => '247 Cuba Street',
        'city'          => 'Palmerston North',
        'postCode'      => '4410',
        'country'       => 'NZ',
    ];

    echo json_encode(['success' => true, 'sender' => $sender]);
    exit;
}

function afri_print_text($value): string {
    return trim((string)($value ?? ''));
}

function afri_short_order_datetime($value): string {
    $raw = trim((string)($value ?? ''));
    if ($raw === '') return '';
    $ts = strtotime($raw);
    if ($ts === false && preg_match('/^([A-Za-z]{3,})\s+(\d{1,2}),\s+(\d{4})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?/', $raw, $m)) {
        $ts = strtotime($m[2] . ' ' . $m[1] . ' ' . $m[3] . ' ' . $m[4] . ':' . $m[5] . ':' . ($m[6] ?? '00'));
    }
    if ($ts === false) return '';
    return date('H:i -j/n/y', $ts);
}

function afri_zpl_safe_text($value): string {
    return str_replace(['^', '~'], '', afri_print_text($value));
}

function afri_pdf_safe_text($value): string {
    return str_replace(["\\", "(", ")", "\r", "\n"], ["\\\\", "\\(", "\\)", " ", " "], afri_print_text($value));
}

function afri_print_order_from_input(array $input): array {
    $order = $input['order'] ?? [];
    if (!is_array($order)) $order = [];
    return [
        'id' => afri_print_text($order['id'] ?? $input['order_id'] ?? ''),
        'name' => afri_print_text($order['name'] ?? $input['order_name'] ?? $input['order_id'] ?? ''),
        'customer' => afri_print_text($order['customer'] ?? $order['name_display'] ?? 'Customer'),
        'address' => afri_print_text($order['original_address'] ?? $order['address'] ?? ''),
        'postcode' => afri_print_text($order['postcode'] ?? ''),
        'country' => afri_print_text($order['country'] ?? 'NZ'),
        'phone' => afri_print_text($order['phone'] ?? ''),
        'note' => afri_print_text($order['note'] ?? ''),
    ];
}

function validate_user_permission(mysqli $db, string $permission) {
    $allowed = ['can_pick', 'can_pack', 'can_print'];
    if (!in_array($permission, $allowed, true)) {
        http_response_code(400);
        echo json_encode(['success' => false, 'error' => 'Invalid permission check']);
        return null;
    }

    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Authentication required']);
        return null;
    }

    if (($caller['role'] ?? '') === 'master') {
        return $caller;
    }

    if (empty($caller[$permission])) {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Permission denied: ' . $permission . ' required']);
        return null;
    }

    return $caller;
}

function afri_build_thermal_zpl(array $order, array $sender, string $stock_id): string {
    $order_ref = $order['name'] !== '' ? $order['name'] : $order['id'];
    $barcode = preg_replace('/\D+/', '', $order_ref);
    if ($barcode === '') $barcode = preg_replace('/\D+/', '', $order['id']);
    if ($barcode === '') $barcode = '0000000000';

    $address = $order['address'];
    $address_parts = array_values(array_filter(array_map('trim', explode(',', $address))));
    $line1 = $address_parts[0] ?? $address;
    $line2 = trim(implode(' ', array_slice($address_parts, 1, 2)));

    return implode("\n", [
        '^XA',
        '^CI28',
        '^PW812',
        '^LL1218',
        '^LH0,0',
        '^FO0,0^GB812,205,3^FS',
        '^FO24,26^A0N,44,44^FDAFRIHAIR SHIPPING^FS',
        '^FO24,80^A0N,30,30^FDDirect Thermal Label^FS',
        '^FO0,205^GB812,150,3^FS',
        '^FO110,240^BY3,2,70^BCN,70,Y,N,N^FD' . afri_zpl_safe_text($barcode) . '^FS',
        '^FO0,355^GB812,390,3^FS',
        '^FO24,395^A0N,40,40^FD' . afri_zpl_safe_text($order['customer']) . '^FS',
        '^FO24,448^A0N,34,34^FD' . afri_zpl_safe_text($line1) . '^FS',
        '^FO24,495^A0N,34,34^FD' . afri_zpl_safe_text($line2) . '^FS',
        '^FO24,560^A0N,34,34^FD' . afri_zpl_safe_text($order['phone']) . '^FS',
        '^FO500,395^A0N,36,36^FDRef: ' . afri_zpl_safe_text($order_ref) . '^FS',
        '^FO500,435^A0N,36,36^FDStock: ' . afri_zpl_safe_text($stock_id) . '^FS',
        '^FO0,745^GB812,220,3^FS',
        '^FO24,785^A0N,36,36^FDSender:^FS',
        '^FO24,830^A0N,34,34^FD' . afri_zpl_safe_text($sender['company'] ?? 'Afrihair Ltd') . '^FS',
        '^FO24,872^A0N,34,34^FD' . afri_zpl_safe_text($sender['streetAddress'] ?? $sender['street'] ?? '247 Cuba Street') . '^FS',
        '^FO24,914^A0N,34,34^FD' . afri_zpl_safe_text(($sender['city'] ?? 'Palmerston North') . ' ' . ($sender['postCode'] ?? $sender['postcode'] ?? '4410')) . '^FS',
        '^FO0,965^GB812,130,3^FS',
        '^FO24,1020^A0N,46,46^FD' . afri_zpl_safe_text(date('d/m/Y')) . '^FS',
        '^XZ',
    ]);
}

function afri_build_simple_pdf(array $order, array $sender, string $stock_id): string {
    $lines = [
        'Afrihair Packing Slip',
        'Order: ' . ($order['name'] ?: $order['id']),
        'Customer: ' . $order['customer'],
        'Phone: ' . $order['phone'],
        'Address: ' . $order['address'],
        'Stock: ' . $stock_id,
        'Sender: ' . afri_print_text($sender['company'] ?? 'Afrihair Ltd'),
        'Generated: ' . gmdate('Y-m-d H:i:s') . ' UTC',
    ];

    $content = "BT\n/F1 16 Tf\n50 790 Td\n";
    foreach ($lines as $index => $line) {
        if ($index > 0) $content .= "0 -28 Td\n";
        $content .= '(' . afri_pdf_safe_text($line) . ") Tj\n";
    }
    $content .= "ET\n";

    $objects = [
        "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
        "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
        "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n",
        "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n",
        "5 0 obj\n<< /Length " . strlen($content) . " >>\nstream\n" . $content . "endstream\nendobj\n",
    ];

    $pdf = "%PDF-1.4\n";
    $offsets = [0];
    foreach ($objects as $object) {
        $offsets[] = strlen($pdf);
        $pdf .= $object;
    }

    $xref_offset = strlen($pdf);
    $pdf .= "xref\n0 " . (count($objects) + 1) . "\n";
    $pdf .= "0000000000 65535 f \n";
    for ($i = 1; $i <= count($objects); $i++) {
        $pdf .= sprintf("%010d 00000 n \n", $offsets[$i]);
    }
    $pdf .= "trailer\n<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\nstartxref\n{$xref_offset}\n%%EOF";
    return $pdf;
}

function afri_packie_test_label_pdf_url(): string {
    return 'https://www.packie.co.nz/api/consignments/pdf-labels-download/31367/parcels/false';
}

function afri_download_pdf_url(string $url): ?string {
    $url = trim($url);
    if ($url === '') return null;

    $parts = parse_url($url);
    $host = strtolower((string)($parts['host'] ?? ''));
    $scheme = strtolower((string)($parts['scheme'] ?? ''));
    $path = (string)($parts['path'] ?? '');
    if ($scheme !== 'https') return null;
    $is_packie_label = in_array($host, ['www.packie.co.nz', 'packie.co.nz'], true)
        && strpos($path, '/api/consignments/pdf-labels-download/') === 0;
    $is_zappy_label = $host === 'ship.zappy.nz'
        && preg_match('#^/Files/Downloads/(Fastway|NZPost)/[A-Za-z0-9_-]+\.pdf$#', $path);
    if (!$is_packie_label && !$is_zappy_label) return null;

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/pdf,*/*']);
    curl_setopt($ch, CURLOPT_TIMEOUT, 20);
    $body = curl_exec($ch);
    $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $ctype = strtolower((string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE));
    curl_close($ch);

    if ($http < 200 || $http >= 300 || !is_string($body) || $body === '') return null;
    if (strpos($ctype, 'pdf') === false && substr($body, 0, 5) !== '%PDF-') return null;
    return $body;
}

function afri_packie_live_label_lookup(string $shopify_num): array {
    global $pm, $db;
    $shopify_num = preg_replace('/\D+/', '', $shopify_num);
    if ($shopify_num === '' || !$pm || !method_exists($pm, 'getShipHeaders')) {
        return ['consignment_id' => 0, 'pdf_url' => '', 'source' => 'unavailable'];
    }

    $headers = $pm->getShipHeaders();
    if (!$headers) return ['consignment_id' => 0, 'pdf_url' => '', 'source' => 'missing_headers'];

    $today = date('Y-m-d');
    $from = date('Y-m-d', strtotime('-7 days'));
    $parcels_url = $pm->shipUrl('Consignments/parcels/1/25');
    $parcels_payload = json_encode([
        'search' => '',
        'from' => $from,
        'to' => $today,
        'status' => 'all',
    ]);

    $ch = curl_init($parcels_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $parcels_payload);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge($headers, ['Content-Type: application/json']));
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $raw = curl_exec($ch);
    $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err = curl_error($ch);
    curl_close($ch);

    $decoded = json_decode((string)$raw, true);
    $parcels = $decoded['result']['result'] ?? [];
    $matched = null;
    if (is_array($parcels)) {
        foreach ($parcels as $parcel) {
            $order_num = preg_replace('/\D+/', '', (string)($parcel['orderNumber'] ?? ''));
            if ($order_num === $shopify_num) {
                $matched = $parcel;
                break;
            }
        }
    }

    Logger::info('packie_label_lookup', 'parcels_query', [
        'order' => $shopify_num,
        'from' => $from,
        'to' => $today,
        'http_code' => $http,
        'curl_error' => $err !== '' ? $err : 'none',
        'parcels_count' => is_array($parcels) ? count($parcels) : 0,
        'matched' => $matched ? '1' : '0',
    ]);

    if ($matched) {
        $label_download_id = (int)($matched['id'] ?? 0);
        $packie_consignment_id = (int)($matched['consignmentId'] ?? 0);
        $pdf_url = $label_download_id > 0
            ? $pm->shipUrl('consignments/pdf-labels-download/' . $label_download_id . '/parcels/false')
            : '';

        Logger::info('packie_label_lookup', 'matched_parcel_label', [
            'order' => $shopify_num,
            'label_download_id' => $label_download_id,
            'packie_consignment_id' => $packie_consignment_id,
            'pdf_url' => $pdf_url,
        ]);

        if ($label_download_id > 0 && $pdf_url !== '') {
            if ($db && $shopify_num !== '') {
                $cache_stmt = $db->prepare(
                    "INSERT INTO pp_packie_cache (shopify_order_number, label_consignment_id, label_pdf_url, packie_status)
                     VALUES (?, ?, ?, 'PRINTED')
                     ON DUPLICATE KEY UPDATE
                         label_consignment_id = VALUES(label_consignment_id),
                         label_pdf_url = VALUES(label_pdf_url),
                         packie_status = 'PRINTED'"
                );
                $cache_stmt->bind_param('sis', $shopify_num, $label_download_id, $pdf_url);
                $cache_stmt->execute();
            }
            return [
                'consignment_id' => $label_download_id,
                'packie_consignment_id' => $packie_consignment_id,
                'pdf_url' => $pdf_url,
                'source' => 'consignments_parcels',
            ];
        }
    }

    $queries = [
        ['status' => 1, 'search' => $shopify_num, 'source' => 'status_1_search'],
        ['status' => 1, 'search' => '', 'source' => 'status_1_latest'],
        ['status' => 2, 'search' => $shopify_num, 'source' => 'status_2_search'],
        ['status' => 3, 'search' => $shopify_num, 'source' => 'status_3_search'],
        ['status' => 4, 'search' => $shopify_num, 'source' => 'status_4_search'],
        ['status' => 5, 'search' => $shopify_num, 'source' => 'status_5_search'],
        ['status' => 0, 'search' => $shopify_num, 'source' => 'status_0_search'],
        ['status' => 99, 'search' => $shopify_num, 'source' => 'status_99_search'],
    ];

    foreach ($queries as $q) {
        $url = $pm->shipUrl(
            'Orders/get-orders?status=' . (int)$q['status']
            . '&search=' . rawurlencode((string)$q['search'])
            . '&integrationId=0&from=&to=&page=1&perPage=25'
        );
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        $raw = curl_exec($ch);
        $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        curl_close($ch);

        $decoded = json_decode((string)$raw, true);
        $orders = $decoded['result']['orders'] ?? [];
        $matched = null;
        if (is_array($orders)) {
            foreach ($orders as $order) {
                $order_num = preg_replace('/\D+/', '', (string)($order['orderNumber'] ?? $order['shopify_order_number'] ?? ''));
                if ($order_num === $shopify_num) {
                    $matched = $order;
                    break;
                }
            }
        }

        Logger::info('packie_label_lookup', 'orders_query', [
            'order' => $shopify_num,
            'source' => $q['source'],
            'http_code' => $http,
            'curl_error' => $err !== '' ? $err : 'none',
            'orders_count' => is_array($orders) ? count($orders) : 0,
            'matched' => $matched ? '1' : '0',
        ]);

        if (!$matched) continue;

        $pdf_url = function_exists('packie_find_label_pdf_url_from_create_response')
            ? packie_find_label_pdf_url_from_create_response($matched)
            : '';
        $consignment_id = 0;
        if ($pdf_url !== '' && preg_match('~/pdf-labels-download/(\d+)/~i', $pdf_url, $m)) {
            $consignment_id = (int)$m[1];
        }

        Logger::info('packie_label_lookup', 'matched_order_label', [
            'order' => $shopify_num,
            'source' => $q['source'],
            'consignment_id' => $consignment_id,
            'pdf_url' => $pdf_url,
        ]);

        if ($pdf_url !== '') {
            if ($db && $shopify_num !== '') {
                $cache_stmt = $db->prepare(
                    "INSERT INTO pp_packie_cache (shopify_order_number, label_consignment_id, label_pdf_url, packie_status)
                     VALUES (?, ?, ?, 'PRINTED')
                     ON DUPLICATE KEY UPDATE
                         label_consignment_id = VALUES(label_consignment_id),
                         label_pdf_url = VALUES(label_pdf_url),
                         packie_status = 'PRINTED'"
                );
                $cache_stmt->bind_param('sis', $shopify_num, $consignment_id, $pdf_url);
                $cache_stmt->execute();
            }
            return ['consignment_id' => $consignment_id, 'pdf_url' => $pdf_url, 'source' => $q['source']];
        }
    }

    return ['consignment_id' => 0, 'pdf_url' => '', 'source' => 'not_found'];
}

if ($action == 'get_thermal_payload' || $action == 'get_laser_pdf_payload') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }

    global $pm;
    $sender = $pm ? $pm->senderDetails() : [
        'company' => 'Afrihair Ltd',
        'streetAddress' => '247 Cuba Street',
        'city' => 'Palmerston North',
        'postCode' => '4410',
        'country' => 'NZ',
    ];
    if (!is_array($sender)) $sender = [];

    $order = afri_print_order_from_input($input);
    $stock_id = afri_print_text($input['stock_id'] ?? 'none');

    if ($action == 'get_laser_pdf_payload') {
        $pdf_url = afri_print_text($input['pdf_url'] ?? '');
        if ($pdf_url === '' && !empty($input['is_test'])) $pdf_url = afri_packie_test_label_pdf_url();
        $pdf = $pdf_url !== '' ? afri_download_pdf_url($pdf_url) : null;
        if ($pdf === null) $pdf = afri_build_simple_pdf($order, $sender, $stock_id);
        echo json_encode([
            'success' => true,
            'encoding' => 'base64',
            'content_type' => 'application/pdf',
            'payload' => base64_encode($pdf),
        ]);
        exit;
    }

    $payload = afri_build_thermal_zpl($order, $sender, $stock_id);
    echo json_encode([
        'success' => true,
        'encoding' => 'utf8',
        'content_type' => 'application/x-zpl',
        'payload' => $payload,
    ]);
    exit;
}

if ($action == 'get_packie_label_payload' || $action == 'get_packie_label_url') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }

    $order_name = afri_print_text($input['order_name'] ?? '');
    $order_id = afri_print_text($input['order_id'] ?? '');
    $shopify_num = preg_replace('/\D+/', '', $order_name);
    if ($shopify_num === '') $shopify_num = preg_replace('/\D+/', '', $order_id);

    if ($shopify_num === '') {
        echo json_encode(['success' => false, 'available' => false, 'error' => 'order_name or order_id is required']);
        exit;
    }

    $stmt = $db->prepare("SELECT label_consignment_id, label_pdf_url FROM pp_packie_cache WHERE shopify_order_number = ? LIMIT 1");
    $stmt->bind_param('s', $shopify_num);
    $stmt->execute();
    $row = $stmt->get_result()->fetch_assoc();

    $consignment_id = (int)($row['label_consignment_id'] ?? 0);
    $pdf_url = afri_print_text($row['label_pdf_url'] ?? '');
    if ($pdf_url === '' && $consignment_id > 0) {
        global $pm;
        $pdf_url = $pm ? $pm->shipUrl('consignments/pdf-labels-download/' . $consignment_id . '/parcels/false') : '';
    }

    if ($pdf_url !== '' && afri_download_pdf_url($pdf_url) === null) {
        Logger::warn('packie_label_lookup', 'cached_pdf_url_invalid', [
            'order' => $shopify_num,
            'consignment_id' => $consignment_id,
            'pdf_url' => $pdf_url,
        ]);
        $pdf_url = '';
        $consignment_id = 0;
    }

    if ($pdf_url === '') {
        $live_label = afri_packie_live_label_lookup($shopify_num);
        $consignment_id = (int)($live_label['consignment_id'] ?? 0);
        $pdf_url = afri_print_text($live_label['pdf_url'] ?? '');
    }

    if ($pdf_url === '') {
        http_response_code(202);
        echo json_encode(['success' => false, 'available' => false, 'error' => 'Label is not ready yet']);
        exit;
    }

    if ($action == 'get_packie_label_url') {
        echo json_encode([
            'success' => true,
            'available' => true,
            'consignment_id' => $consignment_id,
            'pdf_url' => $pdf_url,
        ]);
        exit;
    }

    $pdf = afri_download_pdf_url($pdf_url);
    if ($pdf === null) {
        http_response_code(502);
        echo json_encode(['success' => false, 'available' => false, 'error' => 'Packie label PDF could not be downloaded']);
        exit;
    }

    echo json_encode([
        'success' => true,
        'available' => true,
        'encoding' => 'base64',
        'content_type' => 'application/pdf',
        'consignment_id' => $consignment_id,
        'pdf_url' => $pdf_url,
        'payload' => base64_encode($pdf),
    ]);
    exit;
}

if ($action == 'detect_ship_status') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }

    global $pm;
    if (!$pm) {
        echo json_encode(['success' => false, 'error' => 'Provider manager unavailable']);
        exit;
    }

    $street   = trim((string)($input['street'] ?? ''));
    $suburb   = trim((string)($input['suburb'] ?? ''));
    $city     = trim((string)($input['city'] ?? ''));
    $postcode = trim((string)($input['postcode'] ?? ''));
    $country  = trim((string)($input['country'] ?? 'NZ'));
    $order_name = trim((string)($input['order_name'] ?? ''));
    $shopify_num = str_replace('#', '', $order_name);

    $status = $pm->detectShipStatus($street, $suburb, $city, $postcode, $country, $shopify_num);

    if ($shopify_num !== '') {
        $stmt = $db->prepare("UPDATE pp_packie_cache SET packie_status = ? WHERE shopify_order_number = ?");
        $stmt->bind_param('ss', $status, $shopify_num);
        $stmt->execute();
    }

    echo json_encode(['success' => true, 'packie_status' => $status]);
    exit;
}

if ($action == 'add_note') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }

    $order_id  = trim((string)($input['order_id'] ?? ''));
    $note_text = trim(preg_replace('/[^\pL\pN\s\-\#\.\,\:\(\)\/]/u', '', (string)($input['note'] ?? '')));
    if ($order_id === '') {
        echo json_encode(['success' => false, 'error' => 'order_id is required']);
        exit;
    }

    // Always persist locally first.
    $stmt = $db->prepare("INSERT INTO pp_order_notes (order_id, note_text) VALUES (?, ?) ON DUPLICATE KEY UPDATE note_text = VALUES(note_text)");
    $stmt->bind_param('ss', $order_id, $note_text);
    $stmt->execute();

    $remote_synced = false;
    $remote_error = null;
    if (function_exists('ecommerce_sync_note')) {
        try {
            $sync = ecommerce_sync_note($order_id, $note_text);
            $remote_synced = !empty($sync['success']);
            if (!$remote_synced) $remote_error = $sync['error'] ?? 'Remote note sync failed';
        } catch (Throwable $e) {
            $remote_error = $e->getMessage();
        }
    }

    echo json_encode([
        'success' => true,
        'local_saved' => true,
        'remote_synced' => $remote_synced,
        'remote_error' => $remote_error,
    ]);
    exit;
}

if ($action == 'report_label_print_failure') {
    $caller = validate_user_permission($db, 'can_print');
    if (!$caller) exit;

    $order_id = trim((string)($input['order_id'] ?? ''));
    $order_name = trim((string)($input['order_name'] ?? ''));
    $provider = trim((string)($input['provider'] ?? 'Courier'));
    if ($provider === '') $provider = 'Courier';
    $message = trim((string)($input['message'] ?? 'Local print failed.'));
    $shopify_num = preg_replace('/\D+/', '', $order_name);

    $tags = '';
    if ($order_id !== '' && function_exists('ecommerce_get_order_meta')) {
        $meta = ecommerce_get_order_meta($order_id);
        if (is_array($meta)) {
            $tags = (string)($meta['tags'] ?? '');
            if ($shopify_num === '') $shopify_num = preg_replace('/\D+/', '', (string)($meta['name'] ?? ''));
        }
    }

    $failure = afri_handle_label_failure($db, $order_id, $order_name, $shopify_num, $tags, $provider, $message);
    echo json_encode($failure);
    exit;
}

if ($action == 'mark_packed') {
    $caller = validate_user_permission($db, 'can_pack');
    if (!$caller) exit;

    $is_print_attempt = (int)($input['ui_is_print_attempt'] ?? 0);
    if ($is_print_attempt && ($caller['role'] ?? '') !== 'master' && empty($caller['can_print'])) {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Permission denied: can_print required']);
        exit;
    }

    $order_id    = trim((string)($input['order_id'] ?? ''));
    $order_name  = trim((string)($input['order_name'] ?? ''));
    $shopify_num = str_replace('#', '', $order_name);
    $staff       = trim((string)($input['staff'] ?? '?'));
    $force_override = (int)($input['force_override'] ?? 0);

    $existing_lock_info = '';
    if ($force_override) {
        $lock_ttl = (int)($order_lock_ttl_seconds ?? 600);
        $db->query("DELETE FROM pp_order_locks WHERE last_seen < DATE_SUB(NOW(), INTERVAL " . $lock_ttl . " SECOND)");
        $lock_check_stmt = $db->prepare("
            SELECT staff_name
            FROM pp_order_locks
            WHERE order_id = ?
              AND last_seen >= DATE_SUB(NOW(), INTERVAL " . $lock_ttl . " SECOND)
        ");
        $lock_check_stmt->bind_param('s', $order_id);
        $lock_check_stmt->execute();
        $lock_row = $lock_check_stmt->get_result()->fetch_assoc();
        if ($lock_row) $existing_lock_info = ' (forced override from: ' . $lock_row['staff_name'] . ')';
    }

    Logger::info('mark_packed', 'start', ['order_id' => $order_id, 'order' => $shopify_num, 'staff' => $staff, 'stock' => ($input['stock_id'] ?? 'none'), 'rate' => ($input['rate_id'] ?? 'none'), 'company' => ($_boot_company_slug ?? ''), 'lock_info' => $existing_lock_info]);
    Logger::info('mark_packed', 'ui_debug', ['packie_status' => ($input['ui_packie_status'] ?? ''), 'is_pickup' => (int)($input['ui_is_pickup'] ?? 0), 'user_role' => ($input['ui_user_role'] ?? ''), 'user_can_print' => (int)($input['ui_user_can_print'] ?? 0), 'preferred_method' => ($input['ui_preferred_print_method'] ?? ''), 'is_print_attempt' => (int)($input['ui_is_print_attempt'] ?? 0)]);

    if (!$is_print_attempt) {
        afri_mark_order_packed_success($db, $order_id, $shopify_num);
    }

    $args = escapeshellarg($order_id) . ' '
        . escapeshellarg($input['staff']    ?? 'Staff') . ' '
        . escapeshellarg($input['stock_id'] ?? 'none')  . ' '
        . escapeshellarg($input['rate_id']  ?? 'none')  . ' '
        . escapeshellarg($input['w']        ?? 0)       . ' '
        . escapeshellarg($input['l']        ?? 0)       . ' '
        . escapeshellarg($input['h']        ?? 0)       . ' '
        . escapeshellarg($input['weight']   ?? 0)       . ' '
        . escapeshellarg($order_name)                   . ' '
        . escapeshellarg($input['rate_provider'] ?? '')  . ' '
        . escapeshellarg(base64_encode(json_encode($input['international_details'] ?? null)));

    $preferred_method = trim((string)($input['ui_preferred_print_method'] ?? ''));
    $local_driver = strtolower(trim((string)($input['ui_local_printer_driver'] ?? '')));
    $local_printer_type = strtoupper(trim((string)($input['ui_local_printer_type'] ?? '')));
    $local_print_background = (int)($input['ui_local_print_background'] ?? ($preferred_method === 'local' ? 1 : 0));
    $is_international_order = (int)($input['is_international'] ?? 0);
    $active_ship_slug = $pm && method_exists($pm, 'getShipSlug') ? strtolower((string)$pm->getShipSlug()) : 'packie';
    $active_ship_name = $active_ship_slug === 'courierit1' ? 'CourierIT1' : ($active_ship_slug === 'zappy' ? 'Zappy' : 'Packie');
    if ($is_print_attempt && $is_international_order) {
        $non_thermal_path = ($preferred_method === 'local')
            && ($local_driver === 'system' || ($local_driver === 'direct' && $local_printer_type === 'LASER'));
        if (!$non_thermal_path) {
            echo json_encode([
                'success' => false,
                'error' => 'International labels must be printed on a non-thermal printer. Set this device to LOCAL PRINTER with SYSTEM or DIRECT LASER.',
                'relisted' => false,
            ]);
            exit;
        }
    }
    Logger::info('mark_packed', 'print_decision', [
        'is_print_attempt' => $is_print_attempt,
        'stock_id' => ($input['stock_id'] ?? 'null'),
        'rate_id' => ($input['rate_id'] ?? 'null'),
        'packie_status' => ($input['ui_packie_status'] ?? 'null'),
        'is_pickup' => (int)($input['ui_is_pickup'] ?? 0),
        'can_print' => (int)($input['ui_user_can_print'] ?? 0),
        'preferred_method' => ($input['ui_preferred_print_method'] ?? 'null'),
        'local_driver' => $local_driver,
        'local_printer_type' => $local_printer_type,
        'local_print_background' => $local_print_background,
        'is_international' => $is_international_order,
    ]);

    $company_arg = escapeshellarg($_boot_company_slug ?? '');
    $php_bin = get_cli_php_binary();
    $api_script = __DIR__ . '/api.php';
    $php_bin_esc = escapeshellarg($php_bin);
    $api_script_esc = escapeshellarg($api_script);
    $cmd_line = $php_bin_esc . ' ' . $api_script_esc . " --company=$company_arg print_label $args";

    $should_return_local_pdf = $is_print_attempt
        && $preferred_method === 'local'
        && !$local_print_background
        && ($local_driver === 'system' || $local_driver === 'brother' || ($local_driver === 'direct' && $local_printer_type === 'LASER'));

    if ($should_return_local_pdf) {
        $sync_output = [];
        $sync_code = 0;
        Logger::info('mark_packed', 'local_pdf_print_sync_start', ['cmd_line' => $cmd_line]);
        exec($cmd_line . ' 2>&1', $sync_output, $sync_code);
        Logger::info('mark_packed', 'local_pdf_print_sync_result', ['exit_code' => $sync_code, 'output' => implode("\n", $sync_output)]);
        $print_result = afri_parse_cli_json_result($sync_output);
        if (is_array($print_result) && empty($print_result['success'])) {
            echo json_encode([
                'success' => false,
                'error' => $print_result['error'] ?? 'Courier label creation failed.',
                'note' => $print_result['note'] ?? null,
                'relisted' => !empty($print_result['relisted']),
            ]);
            exit;
        }

        $local_print_label = null;
        $local_print_error = '';
        if ($shopify_num !== '') {
            $label_row = null;
            for ($label_attempt = 1; $label_attempt <= 6; $label_attempt++) {
                $label_stmt = $db->prepare("SELECT label_consignment_id, label_pdf_url FROM pp_packie_cache WHERE shopify_order_number = ? LIMIT 1");
                $label_stmt->bind_param('s', $shopify_num);
                $label_stmt->execute();
                $candidate_row = $label_stmt->get_result()->fetch_assoc();
                $candidate_consignment_id = (int)($candidate_row['label_consignment_id'] ?? 0);
                $candidate_pdf_url = afri_print_text($candidate_row['label_pdf_url'] ?? '');
                if ($candidate_pdf_url !== '' || $candidate_consignment_id > 0) {
                    $label_row = $candidate_row;
                    break;
                }
                if ($label_attempt < 6) usleep(500000);
            }
            $label_consignment_id = (int)($label_row['label_consignment_id'] ?? 0);
            $label_pdf_url = afri_print_text($label_row['label_pdf_url'] ?? '');
            if ($label_pdf_url === '' && $label_consignment_id > 0 && $pm) {
                $label_pdf_url = $pm->shipUrl('consignments/pdf-labels-download/' . $label_consignment_id . '/parcels/false');
            }
            $label_pdf = $label_pdf_url !== '' ? afri_download_pdf_url($label_pdf_url) : null;
            if ($label_pdf === null && $active_ship_slug === 'packie') {
                $live_label = afri_packie_live_label_lookup($shopify_num);
                $live_pdf_url = afri_print_text($live_label['pdf_url'] ?? '');
                if ($live_pdf_url !== '') {
                    $label_pdf_url = $live_pdf_url;
                    $label_consignment_id = (int)($live_label['consignment_id'] ?? 0);
                    $label_pdf = afri_download_pdf_url($label_pdf_url);
                }
            }
            if ($label_pdf_url !== '') {
                if ($label_pdf !== null) {
                    $local_print_label = [
                        'encoding' => 'base64',
                        'content_type' => 'application/pdf',
                        'consignment_id' => $label_consignment_id,
                        'pdf_url' => $label_pdf_url,
                        'payload' => base64_encode($label_pdf),
                    ];
                } else {
                    Logger::warn('mark_packed', 'local_pdf_download_failed', ['url' => $label_pdf_url]);
                    $local_print_error = $active_ship_name . ' label PDF URL was found but could not be downloaded: ' . $label_pdf_url;
                }
            } else {
                Logger::warn('mark_packed', 'local_pdf_url_missing_after_sync', ['order' => $shopify_num]);
                $local_print_error = $active_ship_name . ' did not return a consignment label URL for order ' . $shopify_num . '. Check print_label/create_label_lookup logs.';
            }
        } else {
            $local_print_error = 'Could not resolve Shopify order number for label lookup.';
        }

        if (!$local_print_label) {
            $failure_tags = '';
            if (function_exists('ecommerce_get_order_meta')) {
                $failure_meta = ecommerce_get_order_meta($order_id);
                $failure_tags = (string)($failure_meta['tags'] ?? '');
            }
            $failure = afri_handle_label_failure($db, $order_id, $order_name, $shopify_num, $failure_tags, $active_ship_name, $local_print_error);
            echo json_encode($failure);
            exit;
        }

        afri_mark_order_packed_success($db, $order_id, $shopify_num);

        echo json_encode([
            'success' => true,
            'local_print_label' => $local_print_label,
            'local_print_error' => $local_print_error,
        ]);
        exit;
    }

    if ($is_print_attempt) {
        afri_mark_order_packed_success($db, $order_id, $shopify_num);
    }

    echo json_encode(['success' => true]);

    if (!$is_print_attempt) {
        Logger::info('mark_packed', 'pack_only skipping print_label');
        exit;
    }

    // Flush the success response to the app before starting any courier work so
    // the order detail screen can close immediately even if label creation is slow.
    ignore_user_abort(true);
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    } else {
        while (ob_get_level() > 0) {
            @ob_end_flush();
        }
        @flush();
    }

    $null_device = stripos(PHP_OS, 'WIN') === 0 ? 'NUL' : '/dev/null';
    // mark_packed now lives in api.php, so use __DIR__ (not dirname(__DIR__))
    // to target this same API file for the async print_label CLI call.
    Logger::info('mark_packed', 'php_bin', ['php_bin' => $php_bin]);
    if (strpos($php_bin, '/') !== false && !@is_executable($php_bin)) {
        Logger::warn('mark_packed', 'php_binary_not_executable', ['php_bin' => $php_bin]);
    }
    Logger::info('mark_packed', 'exec', ['cmd_line' => $cmd_line]);

    $spawn_output = [];
    $spawn_code = 0;
    $detach_cmd = 'nohup ' . $cmd_line . ' > ' . $null_device . ' 2>&1 < ' . $null_device . ' & echo $!';
    exec($detach_cmd, $spawn_output, $spawn_code);
    $spawn_pid = trim((string)($spawn_output[0] ?? ''));
    Logger::info('mark_packed', 'spawn_result', ['exit_code' => $spawn_code, 'pid' => $spawn_pid !== '' ? $spawn_pid : 'unknown']);

    if ($spawn_code !== 0 || $spawn_pid === '') {
        Logger::warn('mark_packed', 'sync_fallback attempting synchronous print_label');
        $sync_output = [];
        $sync_code = 0;
        exec($cmd_line . ' 2>&1', $sync_output, $sync_code);
        Logger::info('mark_packed', 'sync_result', ['exit_code' => $sync_code, 'output' => implode("\n", $sync_output)]);
    }
    exit;
}

if ($action == 'update_pick') {
    $caller = validate_user_permission($db, 'can_pick');
    if (!$caller) exit;
    $order_id = trim($input['order_id'] ?? '');
    $sku      = trim($input['sku'] ?? '');
    $qty      = (int)($input['qty'] ?? 0);
    $staff    = trim($input['staff'] ?? '');

    if ($order_id === '' || $sku === '') {
        echo json_encode(['success' => false, 'error' => 'order_id and sku are required']);
        exit;
    }

    $stmt = $db->prepare("
        INSERT INTO pp_picked_items (order_id, sku, picked_qty, picked_by)
        VALUES (?, ?, ?, ?)
        ON DUPLICATE KEY UPDATE picked_qty = ?, picked_by = ?
    ");
    $stmt->bind_param("ssisis", $order_id, $sku, $qty, $staff, $qty, $staff);
    $stmt->execute();

    echo json_encode(['success' => true]);
    exit;
}

if ($action == 'get_picks') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
    $res = $db->query("SELECT order_id, sku, picked_qty, picked_by FROM pp_picked_items");
    $picks = [];

    while ($row = $res->fetch_assoc()) {
        if (!isset($picks[$row['order_id']])) $picks[$row['order_id']] = [];
        $picks[$row['order_id']][$row['sku']] = [
            'qty' => (int)$row['picked_qty'],
            'by' => $row['picked_by']
        ];
    }

    echo json_encode($picks);
    exit;
}

if ($action == 'check_lock') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
    $order_id = trim($_GET['order_id'] ?? '');
    $staff    = trim($_GET['staff'] ?? '');
    if ($order_id === '' || $staff === '') {
        echo json_encode(['success' => false, 'error' => 'order_id and staff are required']);
        exit;
    }

    $lock_ttl = (int)($order_lock_ttl_seconds ?? 600);
    $db->query("DELETE FROM pp_order_locks WHERE last_seen < DATE_SUB(NOW(), INTERVAL " . $lock_ttl . " SECOND)");

    $stmt = $db->prepare("
        SELECT staff_name
        FROM pp_order_locks
        WHERE order_id = ?
          AND staff_name != ?
          AND last_seen >= DATE_SUB(NOW(), INTERVAL " . $lock_ttl . " SECOND)
    ");
    $stmt->bind_param("ss", $order_id, $staff);
    $stmt->execute();
    $res = $stmt->get_result()->fetch_assoc();

    if ($res) {
        echo json_encode(["locked_by" => $res['staff_name']]);
    } else {
        $stmt = $db->prepare("
            INSERT INTO pp_order_locks (order_id, staff_name)
            VALUES (?, ?)
            ON DUPLICATE KEY UPDATE staff_name = ?, last_seen = NOW()
        ");
        $stmt->bind_param("sss", $order_id, $staff, $staff);
        $stmt->execute();
        echo json_encode(["status" => "locked", "locked_by" => null]);
    }
    exit;
}

if ($action == 'release_lock') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
    $order_id = trim($_GET['order_id'] ?? '');
    $staff    = trim($_GET['staff'] ?? '');
    if ($order_id === '' || $staff === '') {
        echo json_encode(['success' => false, 'error' => 'order_id and staff are required']);
        exit;
    }

    $stmt = $db->prepare("DELETE FROM pp_order_locks WHERE order_id = ? AND staff_name = ?");
    $stmt->bind_param("ss", $order_id, $staff);
    $stmt->execute();

    echo json_encode(["status" => "released"]);
    exit;
}

// ====================================================================
// STAFF MANAGEMENT ENDPOINTS
// ====================================================================

if ($action == 'get_staff') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
    $cid = (int)$caller['company_id'];
    $stmt = $db->prepare("SELECT id, name, role, can_pick, can_pack, can_print, pin_code, is_active FROM pp_staff_members WHERE company_id = ? ORDER BY name ASC");
    $stmt->bind_param('i', $cid);
    $stmt->execute();
    $res = $stmt->get_result();
    $staff = [];
    while ($row = $res->fetch_assoc()) {
        $staff_row = [
            'id'        => (int)$row['id'],
            'name'      => $row['name'],
            'role'      => $row['role'],
            'can_pick'  => (bool)$row['can_pick'],
            'can_pack'  => (bool)$row['can_pack'],
            'can_print' => (bool)$row['can_print'],
            'has_pin'   => $row['pin_code'] !== null && $row['pin_code'] !== '',
            'is_active' => (bool)$row['is_active'],
        ];
        if (($caller['role'] ?? '') === 'master') {
            $staff_row['pin_code'] = $row['pin_code'];
        }
        $staff[] = $staff_row;
    }
    echo json_encode(['staff' => $staff]);
    exit;
}

if ($action == 'verify_pin_login') {
    $staff_id = (int)($input['staff_id'] ?? 0);
    $pin_code = trim((string)($input['pin_code'] ?? ''));
    $device_name = trim((string)($input['device_name'] ?? 'Warehouse App'));
    if ($device_name === '') $device_name = 'Warehouse App';

    if ($staff_id <= 0 || strlen($pin_code) !== 4 || !ctype_digit($pin_code)) {
        http_response_code(400);
        echo json_encode(['success' => false, 'error' => 'Invalid staff or PIN']);
        exit;
    }

    $stmt = $db->prepare("
        SELECT id, name, role, can_pick, can_pack, can_print, company_id
        FROM pp_staff_members
        WHERE id = ? AND pin_code = ? AND is_active = 1 AND role <> 'master'
        LIMIT 1
    ");
    $stmt->bind_param('is', $staff_id, $pin_code);
    $stmt->execute();
    $staff = $stmt->get_result()->fetch_assoc();

    if (!$staff) {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Incorrect PIN']);
        exit;
    }

    $token = bin2hex(random_bytes(32));
    $worker_days = (int)($_ENV['WORKER_SESSION_TTL_DAYS'] ?? ($_ENV['SESSION_REMEMBER_TTL_DAYS'] ?? 30));
    if ($worker_days < 1) $worker_days = 30;
    $expires_at = gmdate('Y-m-d H:i:s', time() + ($worker_days * 86400));
    $company_id = (int)$staff['company_id'];

    $session_stmt = $db->prepare("INSERT INTO pp_auth_sessions (token, staff_id, company_id, device_name, expires_at) VALUES (?, ?, ?, ?, ?)");
    if (!$session_stmt) {
        Logger::error('pin_login', 'session_prepare_failed', ['error' => $db->error]);
        echo json_encode(['success' => false, 'error' => 'Session creation failed']);
        exit;
    }
    $session_stmt->bind_param('siiss', $token, $staff_id, $company_id, $device_name, $expires_at);
    if (!$session_stmt->execute()) {
        Logger::error('pin_login', 'session_insert_failed', ['error' => $session_stmt->error]);
        echo json_encode(['success' => false, 'error' => 'Session creation failed']);
        exit;
    }

    $old_token = get_request_auth_token();
    if ($old_token && $old_token !== $token) {
        $del = $db->prepare("DELETE FROM pp_auth_sessions WHERE token = ?");
        $del->bind_param('s', $old_token);
        $del->execute();
    }

    $login_stmt = $db->prepare("UPDATE pp_staff_members SET last_login = UTC_TIMESTAMP() WHERE id = ? AND company_id = ?");
    $login_stmt->bind_param('ii', $staff_id, $company_id);
    $login_stmt->execute();

    Logger::info('pin_login', 'success', ['staff_id' => $staff_id, 'token_prefix' => substr($token, 0, 8)]);

    echo json_encode([
        'success' => true,
        'token' => $token,
        'expires_at' => $expires_at,
        'user' => [
            'id' => (int)$staff['id'],
            'name' => $staff['name'],
            'role' => $staff['role'],
            'can_pick' => (bool)$staff['can_pick'],
            'can_pack' => (bool)$staff['can_pack'],
            'can_print' => (bool)$staff['can_print'],
        ],
        'company' => $_boot_company_slug ?? null,
    ]);
    exit;
}

if ($action == 'switch_staff_honor') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
    if (!$master_db) {
        echo json_encode(['success' => false, 'error' => 'Master registry not available']);
        exit;
    }

    $mode_stmt = $master_db->prepare("SELECT security_mode FROM pp_clients WHERE client_db_name = ? LIMIT 1");
    $mode_stmt->bind_param('s', $_resolved_client_db);
    $mode_stmt->execute();
    $mode_row = $mode_stmt->get_result()->fetch_assoc();
    if (($mode_row['security_mode'] ?? 'individual_pin') !== 'shared_honor') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Shared honor mode is not enabled']);
        exit;
    }

    $staff_id = (int)($input['staff_id'] ?? 0);
    $company_id = (int)$caller['company_id'];
    $device_name = trim((string)($input['device_name'] ?? 'Warehouse App'));
    if ($device_name === '') $device_name = 'Warehouse App';

    $stmt = $db->prepare("
        SELECT id, name, role, can_pick, can_pack, can_print, company_id
        FROM pp_staff_members
        WHERE id = ? AND company_id = ? AND is_active = 1
        LIMIT 1
    ");
    $stmt->bind_param('ii', $staff_id, $company_id);
    $stmt->execute();
    $staff = $stmt->get_result()->fetch_assoc();

    if (!$staff) {
        echo json_encode(['success' => false, 'error' => 'Staff member not found']);
        exit;
    }

    $token = bin2hex(random_bytes(32));
    $worker_days = (int)($_ENV['WORKER_SESSION_TTL_DAYS'] ?? ($_ENV['SESSION_REMEMBER_TTL_DAYS'] ?? 30));
    if ($worker_days < 1) $worker_days = 30;
    $expires_at = gmdate('Y-m-d H:i:s', time() + ($worker_days * 86400));

    $session_stmt = $db->prepare("INSERT INTO pp_auth_sessions (token, staff_id, company_id, device_name, expires_at) VALUES (?, ?, ?, ?, ?)");
    $session_stmt->bind_param('siiss', $token, $staff_id, $company_id, $device_name, $expires_at);
    if (!$session_stmt->execute()) {
        Logger::error('honor_switch', 'session_insert_failed', ['error' => $session_stmt->error]);
        echo json_encode(['success' => false, 'error' => 'Session creation failed']);
        exit;
    }

    $old_token = get_request_auth_token();
    if ($old_token) {
        $del = $db->prepare("DELETE FROM pp_auth_sessions WHERE token = ?");
        $del->bind_param('s', $old_token);
        $del->execute();
    }

    $login_stmt = $db->prepare("UPDATE pp_staff_members SET last_login = UTC_TIMESTAMP() WHERE id = ? AND company_id = ?");
    $login_stmt->bind_param('ii', $staff_id, $company_id);
    $login_stmt->execute();

    Logger::info('honor_switch', 'success', ['staff_id' => $staff_id, 'token_prefix' => substr($token, 0, 8)]);

    echo json_encode([
        'success' => true,
        'token' => $token,
        'expires_at' => $expires_at,
        'user' => [
            'id' => (int)$staff['id'],
            'name' => $staff['name'],
            'role' => $staff['role'],
            'can_pick' => (bool)$staff['can_pick'],
            'can_pack' => (bool)$staff['can_pack'],
            'can_print' => (bool)$staff['can_print'],
        ],
        'company' => $_boot_company_slug ?? null,
    ]);
    exit;
}

if ($action == 'add_staff') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }
    $name = trim($input['name'] ?? '');
    if ($name === '') {
        echo json_encode(['success' => false, 'error' => 'Name is required']);
        exit;
    }
    $company_id = (int)$caller['company_id'];
    $stmt = $db->prepare("
        INSERT INTO pp_staff_members (company_id, name, is_active, role, can_pick, can_pack, can_print)
        VALUES (?, ?, 1, 'secondary', 1, 1, 0)
    ");
    $stmt->bind_param("is", $company_id, $name);
    if (!$stmt->execute()) {
        echo json_encode(['success' => false, 'error' => 'Could not add staff (name may already exist).']);
        exit;
    }
    echo json_encode(['success' => true, 'id' => (int)$db->insert_id]);
    exit;
}


if ($action == 'delete_staff') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }

    $id = (int)($input['id'] ?? 0);
    if ($id <= 0) {
        echo json_encode(['success' => false, 'error' => 'Invalid id']);
        exit;
    }

    $company_id = (int)$caller['company_id'];

    // Ensure we don't delete the last active master
    $roleStmt = $db->prepare("SELECT role FROM pp_staff_members WHERE id = ? AND company_id = ? LIMIT 1");
    $roleStmt->bind_param("ii", $id, $company_id);
    $roleStmt->execute();
    $roleRow = $roleStmt->get_result()->fetch_assoc();
    $roleStmt->close();

    if ($roleRow && ($roleRow['role'] === 'master')) {
        $cntStmt = $db->prepare("SELECT COUNT(*) AS masters FROM pp_staff_members WHERE company_id = ? AND role = 'master' AND is_active = 1");
        $cntStmt->bind_param("i", $company_id);
        $cntStmt->execute();
        $cnt = (int)$cntStmt->get_result()->fetch_assoc()['masters'];
        $cntStmt->close();

        if ($cnt <= 1) {
            echo json_encode(['success' => false, 'error' => 'Cannot delete the last master account']);
            exit;
        }
    }

    // Delete staff row
    $stmt = $db->prepare("DELETE FROM pp_staff_members WHERE id = ? AND company_id = ?");
    $stmt->bind_param("ii", $id, $company_id);
    if (!$stmt->execute()) {
        echo json_encode(['success' => false, 'error' => 'Delete failed: ' . $stmt->error]);
        exit;
    }
    if ($stmt->affected_rows === 0) {
        echo json_encode(['success' => false, 'error' => 'Staff member not found.']);
        exit;
    }
    $stmt->close();

    // Remove any active sessions for that staff (cleanup orphaned sessions)
    $sdel = $db->prepare("DELETE FROM pp_auth_sessions WHERE staff_id = ? AND company_id = ?");
    $sdel->bind_param("ii", $id, $company_id);
    $sdel->execute();
    $sdel->close();

    Logger::warn('staff.delete', 'Staff member deleted', ['staff_id' => $id, 'by' => $caller['name'] ?? 'unknown']);

    echo json_encode(['success' => true]);
    exit;
}

// ====================================================================
// AUTH + SETUP ENDPOINTS
// ====================================================================

if ($action == 'check_setup') {
    $slug = preg_replace('/[^a-z0-9\-_]/', '', strtolower(trim($_GET['company'] ?? '')));
    if ($slug === '') { echo json_encode(['exists' => false, 'error' => 'Company slug required']); exit; }

    if (!$master_db) {
        echo json_encode(['exists' => false, 'error' => 'Master registry not available.']);
        exit;
    }

    $stmt = $master_db->prepare("SELECT business_name, client_db_name, is_active FROM pp_clients WHERE slug = ? LIMIT 1");
    $stmt->bind_param("s", $slug);
    $stmt->execute();
    $row = $stmt->get_result()->fetch_assoc();

    $exists = $row && (int)$row['is_active'] === 1 && !empty($row['client_db_name']);
    echo json_encode(['exists' => (bool)$exists, 'display_name' => $row['business_name'] ?? null]);
    exit;
}

if ($action == 'run_setup') {
    $slug      = preg_replace('/[^a-z0-9\-_]/', '', strtolower(trim($input['company_slug']    ?? '')));
    $biz_slug  = preg_replace('/[^a-z0-9\-_]/', '', strtolower(trim($input['business_slug']   ?? $slug)));
    $display   = trim($input['company_name']   ?? '');
    $username  = trim($input['username']       ?? '');
    $password  = $input['password']            ?? '';
    $disp_name = trim($input['display_name']   ?? $username);
    $ship_prov = in_array($input['shipping_provider'] ?? '', ['packie', 'zappy']) ? $input['shipping_provider'] : 'packie';
    $ecom_prov = in_array($input['ecom_provider']     ?? '', ['none', 'shopify'], true) ? $input['ecom_provider'] : 'none';

    if (!$slug || !$display || !$username || strlen($password) < 8) {
        echo json_encode(['success' => false, 'error' => 'Missing required fields. Password must be at least 8 characters.']);
        exit;
    }

    if (!$master_db) {
        echo json_encode(['success' => false, 'error' => 'Master registry not available.']);
        exit;
    }

    if (!courier_allowed_for_company($master_db, $ship_prov, $biz_slug, null)) {
        echo json_encode(['success' => false, 'error' => ucfirst($ship_prov) . ' is not enabled for this company.']);
        exit;
    }

    $mstmt = $master_db->prepare("SELECT id, business_name, client_db_name, is_active, master_password_hash FROM pp_clients WHERE slug = ? LIMIT 1");
    $mstmt->bind_param("s", $biz_slug);
    $mstmt->execute();
    $mrow = $mstmt->get_result()->fetch_assoc();
    if (!$mrow || (int)$mrow['is_active'] !== 1) {
        echo json_encode(['success' => false, 'error' => 'Invalid business slug. Please verify your setup key again.']);
        exit;
    }

    $expected_db = trim((string)($mrow['client_db_name'] ?? ''));
    if ($expected_db === '') {
        $expected_db = generate_client_db_name($slug);
        $um = $master_db->prepare("UPDATE pp_clients SET client_db_name = ? WHERE id = ? AND (client_db_name IS NULL OR client_db_name = '')");
        $um->bind_param("si", $expected_db, $mrow['id']);
        $um->execute();
    }

    if ($expected_db !== $_resolved_client_db) {
        echo json_encode(['success' => false, 'error' => 'Client database routing mismatch. Please retry setup.']);
        exit;
    }

    if (!empty($mrow['master_password_hash'])) {
        echo json_encode(['success' => false, 'error' => 'Setup has already been completed for this client database.']);
        exit;
    }

    $db->begin_transaction();
    try {
        $company_id = 1;
        $pw_hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

        // Store master password hash in the master registry.
        $upd_pw = $master_db->prepare("UPDATE pp_clients SET master_password_hash = ? WHERE slug = ?");
        $upd_pw->bind_param("ss", $pw_hash, $biz_slug);
        if (!$upd_pw->execute() || $master_db->affected_rows < 1) throw new Exception("Failed to store master credentials");

        // Prefer explicit master first/last inputs when supplied by the client UI
        $mf = trim($input['master_first'] ?? '');
        $ml = trim($input['master_last_initial'] ?? '');

        if ($mf !== '' && $ml !== '') {
            $base_first = preg_replace('/[^\pL\pM\s\-]/u', '', $mf);
            $base_last_initial = strtoupper(substr($ml, 0, 1));
            $base_name = trim($base_first . ' ' . $base_last_initial);
        } else {
            // Derive: first token of display name + initial from last token if present, otherwise from username
            $parts = preg_split('/\s+/', $disp_name);
            $base_first = $parts[0] ?? $username;
            if (count($parts) > 1) {
                $base_last_initial = strtoupper(substr($parts[count($parts) - 1], 0, 1));
            } else {
                $base_last_initial = strtoupper(substr($username, 0, 1));
            }
            $base_name = trim($base_first . ' ' . $base_last_initial);
        }

        // Try to reserve a human-friendly unique name. Strategy:
        // 1) try BaseName (e.g. "Daniel S")
        // 2) try BaseName + ' (' + first2(username) + ')'
        // 3) append numeric suffixes until unique
        $chk_name_stmt = $db->prepare("SELECT id FROM pp_staff_members WHERE name = ?");
        $final_name = $base_name;

        $chk_name_stmt->bind_param("s", $final_name);
        $chk_name_stmt->execute();
        $r = $chk_name_stmt->get_result()->fetch_assoc();

        if ($r) {
            $short = preg_replace('/[^a-z0-9]/i', '', $username);
            $short = substr($short, 0, 2) ?: substr(md5(uniqid()), 0, 2);
            $candidate = $base_name . ' (' . $short . ')';
            $final_name = $candidate;
            $i = 1;
            while (true) {
                $chk_name_stmt->bind_param("s", $final_name);
                $chk_name_stmt->execute();
                $r2 = $chk_name_stmt->get_result()->fetch_assoc();
                if (!$r2) break;
                $i++;
                $final_name = $candidate . ' ' . $i;
                if ($i > 50) break; // safety
            }
        }
        $chk_name_stmt->close();

        $s2 = $db->prepare("INSERT INTO pp_staff_members (company_id, username, name, role, can_pick, can_pack, can_print, is_active) VALUES (?, ?, ?, 'master', 1, 1, 1, 1)");
        $s2->bind_param("iss", $company_id, $username, $final_name);
        if (!$s2->execute()) throw new Exception("Master account creation failed");

        $company_pm = new ProviderManager($db, $company_id);

        // Shipping provider config — written flat into pp_settings table
        $ship_base = 'https://www.packie.co.nz/api';
        if ($ship_prov === 'zappy') $ship_base = 'https://ship.zappy.nz';
        $ship_rows = [
            ['shipping', $ship_prov, 'base_url',             $ship_base],
            ['shipping', $ship_prov, 'username',             $input['shipping_username'] ?? ''],
            ['shipping', $ship_prov, 'api_key',              $input['shipping_api_key']  ?? ''],
            ['shipping', $ship_prov, 'bearer_token',         ''],
            ['shipping', $ship_prov, 'sender_contact_name',  $input['sender_name']       ?? ''],
            ['shipping', $ship_prov, 'sender_company',       $display],
            ['shipping', $ship_prov, 'sender_street',        $input['sender_street']     ?? ''],
            ['shipping', $ship_prov, 'sender_city',          $input['sender_city']       ?? ''],
            ['shipping', $ship_prov, 'sender_postcode',      $input['sender_postcode']   ?? ''],
            ['shipping', $ship_prov, 'sender_country',       'NZ'],
        ];
        foreach ($ship_rows as [$cat, $slug, $key, $val]) {
            $company_pm->saveSetting($cat, $slug, $key, $val);
        }

        // Ecommerce provider config is optional for shipping-only setups.
        if ($ecom_prov !== 'none') {
            $ecom_rows = [
                ['ecommerce', $ecom_prov, 'store_url',   trim($input['ecom_store_url'] ?? '')],
                ['ecommerce', $ecom_prov, 'api_key',     $input['ecom_api_key']        ?? ''],
                ['ecommerce', $ecom_prov, 'api_version', '2024-01'],
            ];
            foreach ($ecom_rows as [$cat, $slug, $key, $val]) {
                $company_pm->saveSetting($cat, $slug, $key, $val);
            }
        }

        // Record which providers are active
        $company_pm->saveSetting('general', 'app', 'active_shipping_provider', $ship_prov);
        $company_pm->saveSetting('general', 'app', 'active_ecom_provider',     $ecom_prov === 'none' ? '' : $ecom_prov);

        $db->commit();
        echo json_encode(['success' => true, 'message' => "Setup complete for {$display}"]);
    } catch (Exception $e) {
        $db->rollback();
        echo json_encode(['success' => false, 'error' => $e->getMessage()]);
    }
    exit;
}


if ($action == 'login') {
    $company_slug = preg_replace('/[^a-z0-9\-_]/', '', strtolower(trim($input['company'] ?? '')));
    $password     = $input['password'] ?? '';
    $role_req     = ($input['role'] ?? 'user') === 'admin' ? 'master' : 'secondary';
    $remember     = !empty($input['remember']);
    $_rem_days = (int)($_ENV['SESSION_REMEMBER_TTL_DAYS'] ?? 30);
    $_def_hours = (int)($_ENV['SESSION_DEFAULT_TTL_HOURS'] ?? 8);
    $_admin_minutes = (int)($_ENV['ADMIN_SESSION_TTL_MINUTES'] ?? 30);
    $_worker_days = (int)($_ENV['WORKER_SESSION_TTL_DAYS'] ?? $_rem_days);
    if ($_admin_minutes < 1) $_admin_minutes = 30;
    if ($_worker_days < 1) $_worker_days = 30;
    $session_ttl_seconds = $remember ? ($_rem_days * 86400) : ($_def_hours * 3600);

    Logger::info('login', 'attempt', ['company' => $company_slug, 'role_req' => $role_req, 'remote_ip' => ($_SERVER['REMOTE_ADDR'] ?? 'cli')]);

    if (!$company_slug || !$password) {
        Logger::warn('login', 'error_missing_fields', ['company' => $company_slug]);
        echo json_encode(['success' => false, 'error' => 'Company and password required']);
        exit;
    }
    if (!$master_db) {
        Logger::warn('login', 'error_master_db_missing', ['company' => $company_slug]);
        echo json_encode(['success' => false, 'error' => 'Master registry not available']);
        exit;
    }

    // Rate limiting — block after too many failed attempts from the same IP
    $_rl_ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    $_rl_max = (int)($_ENV['RATE_LIMIT_MAX_ATTEMPTS'] ?? 5);
    $_rl_window = (int)($_ENV['RATE_LIMIT_WINDOW_SECONDS'] ?? 300);

    $_rl_stmt = $master_db->prepare(
        "SELECT COUNT(*) AS cnt FROM pp_login_attempts WHERE ip_address = ? AND attempted_at > DATE_SUB(UTC_TIMESTAMP(), INTERVAL ? SECOND)"
    );
    $_rl_stmt->bind_param('si', $_rl_ip, $_rl_window);
    $_rl_stmt->execute();
    $_rl_count = (int)$_rl_stmt->get_result()->fetch_assoc()['cnt'];

    if ($_rl_count >= $_rl_max) {
        http_response_code(429);
        Logger::warn('login', 'rate_limited', ['ip' => $_rl_ip, 'attempts' => $_rl_count]);
        echo json_encode(['success' => false, 'error' => 'Too many login attempts. Please wait a few minutes.']);
        exit;
    }

    // Record this login attempt
    $_rl_ins = $master_db->prepare("INSERT INTO pp_login_attempts (ip_address, company_slug) VALUES (?, ?)");
    $_rl_ins->bind_param('ss', $_rl_ip, $company_slug);
    $_rl_ins->execute();

    // Opportunistic cleanup of old attempts (1% chance per request)
    if (mt_rand(1, 100) === 1) {
        $master_db->query("DELETE FROM pp_login_attempts WHERE attempted_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 DAY)");
    }

    // Resolve client DB and credentials from master registry
    $m_stmt = $master_db->prepare("SELECT client_db_name, is_active, master_password_hash, secondary_password_hash, security_mode FROM pp_clients WHERE slug = ? LIMIT 1");
    $m_stmt->bind_param("s", $company_slug);
    $m_stmt->execute();
    $m_row = $m_stmt->get_result()->fetch_assoc();
    if (!$m_row || (int)$m_row['is_active'] !== 1 || empty($m_row['client_db_name'])) {
        Logger::warn('login', 'error_client_not_resolved', ['slug' => $company_slug, 'm_row' => $m_row]);
        echo json_encode(['success' => false, 'error' => 'Invalid credentials']);
        exit;
    }

    // Connect to client DB (reuse existing routing logic earlier in file if available)
    $client_db_name = $m_row['client_db_name'];
    $client_db = new mysqli($db_host, $db_user, $db_pass, $client_db_name);
    if ($client_db->connect_error) {
        Logger::error('login', 'error_client_db_connect', ['db' => $client_db_name, 'error' => $client_db->connect_error]);
        echo json_encode(['success' => false, 'error' => 'Client DB unavailable']);
        exit;
    }
    $client_db->query("SET time_zone = '+00:00'");

    // Ensure pp_auth_sessions table and all required columns exist before inserting.
    // The main bootstrap migration only runs for non-login actions, so we must
    // apply it here too to prevent silent INSERT failures on pre-migration DBs.
    $client_db->query("
        CREATE TABLE IF NOT EXISTS `pp_auth_sessions` (
            `token` char(64) NOT NULL,
            `staff_id` int NOT NULL,
            `company_id` int NOT NULL,
            `device_name` varchar(100) DEFAULT NULL,
            `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
            `expires_at` timestamp NOT NULL,
            PRIMARY KEY (`token`),
            KEY `idx_staff` (`staff_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    ");
    $_lc_q    = $client_db->query("SHOW COLUMNS FROM pp_auth_sessions");
    $_lc_cols = [];
    if ($_lc_q) while ($_lc_c = $_lc_q->fetch_assoc()) $_lc_cols[] = $_lc_c['Field'];
    if (!in_array('company_id',  $_lc_cols)) $client_db->query("ALTER TABLE pp_auth_sessions ADD COLUMN `company_id` int NOT NULL DEFAULT 1 AFTER `staff_id`");
    if (!in_array('device_name', $_lc_cols)) $client_db->query("ALTER TABLE pp_auth_sessions ADD COLUMN `device_name` varchar(100) DEFAULT NULL");
    if (!in_array('expires_at',  $_lc_cols)) $client_db->query("ALTER TABLE pp_auth_sessions ADD COLUMN `expires_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP");
    unset($_lc_q, $_lc_cols, $_lc_c);

    $company_id = 1; // company is always 1 per-client-DB; no pp_companies lookup needed

    if ($role_req === 'master') {
        // Verify master password against hash stored in the master registry.
        if (empty($m_row['master_password_hash']) || !password_verify($password, $m_row['master_password_hash'])) {
            Logger::warn('login', 'master_password_mismatch', ['company' => $company_slug]);
            echo json_encode(['success' => false, 'error' => 'Invalid credentials']);
            exit;
        }
        // Load staff row from client DB for name and permissions
        $u_stmt = $client_db->prepare("SELECT id, name, role, can_pick, can_pack, can_print FROM pp_staff_members WHERE role = 'master' AND is_active = 1 LIMIT 1");
        $u_stmt->execute();
        $user = $u_stmt->get_result()->fetch_assoc();
        if (!$user) { Logger::warn('login', 'master_no_staff_row', ['company' => $company_slug]); echo json_encode(['success'=>false,'error'=>'Invalid credentials']); exit; }
        $token = bin2hex(random_bytes(32));
        $expires_at = gmdate('Y-m-d H:i:s', time() + ($_admin_minutes * 60));
        $t_stmt = $client_db->prepare("INSERT INTO pp_auth_sessions (token, staff_id, company_id, device_name, expires_at) VALUES (?, ?, ?, ?, ?)");
        if (!$t_stmt) {
            Logger::error('login', 'pp_auth_sessions prepare failed (master)', ['error' => $client_db->error]);
            echo json_encode(['success' => false, 'error' => 'Session creation failed. Please try again.']);
            exit;
        }
        $device_name = $input['device_name'] ?? 'device';
        $user_id = (int)($user['id'] ?? 0);
        $t_stmt->bind_param("siiss", $token, $user_id, $company_id, $device_name, $expires_at);
        if (!$t_stmt->execute()) {
            Logger::error('login', 'pp_auth_sessions INSERT failed (master)', ['error' => $t_stmt->error]);
            echo json_encode(['success' => false, 'error' => 'Session creation failed. Please try again.']);
            exit;
        }
        Logger::info('login', 'master_success', ['company' => $company_slug, 'user_id' => $user_id, 'token_prefix' => substr($token, 0, 8)]);
        echo json_encode(['success'=>true,'token'=>$token,'expires_at'=>$expires_at,'security_mode'=>($m_row['security_mode'] ?? 'individual_pin'),'user'=>['id'=>(int)$user['id'],'name'=>$user['name'],'role'=>$user['role'],'can_pick'=>(bool)$user['can_pick'],'can_pack'=>(bool)$user['can_pack'],'can_print'=>(bool)$user['can_print']]]);
        exit;
    } else {
        // Secondary (shared) password flow
        // Verify secondary password against hash stored in the master registry.
        if (empty($m_row['secondary_password_hash']) || !password_verify($password, $m_row['secondary_password_hash'])) {
            Logger::warn('login', 'secondary_password_mismatch', ['company' => $company_slug]);
            echo json_encode(['success' => false, 'error' => 'Invalid credentials']);
            exit;
        }

        // Find or create a secondary staff row to attach session to.
        $ss = $client_db->prepare("SELECT id, name, role, can_pick, can_pack, can_print FROM pp_staff_members WHERE role = 'secondary' AND is_active = 1 LIMIT 1");
        $ss->execute();
        $sec = $ss->get_result()->fetch_assoc();
        if (!$sec) {
            $ins = $client_db->prepare("INSERT INTO pp_staff_members (company_id, name, role, is_active) VALUES (?, 'Pack Station User', 'secondary', 1)");
            $ins->bind_param("i", $company_id);
            $ins->execute();
            $sec_id = (int)$client_db->insert_id;
            $sec = ['id'=>$sec_id,'name'=>'Pack Station User','role'=>'secondary','can_pick'=>1,'can_pack'=>1,'can_print'=>0];
        }

        $token = bin2hex(random_bytes(32));
        $worker_ttl_seconds = $remember ? ($_worker_days * 86400) : $session_ttl_seconds;
        $expires_at = gmdate('Y-m-d H:i:s', time() + $worker_ttl_seconds);
        $t_stmt = $client_db->prepare("INSERT INTO pp_auth_sessions (token, staff_id, company_id, device_name, expires_at) VALUES (?, ?, ?, ?, ?)");
        if (!$t_stmt) {
            Logger::error('login', 'pp_auth_sessions prepare failed (secondary)', ['error' => $client_db->error]);
            echo json_encode(['success' => false, 'error' => 'Session creation failed. Please try again.']);
            exit;
        }
        $device_name = $input['device_name'] ?? 'device';
        $sec_id = (int)($sec['id'] ?? 0);
        $t_stmt->bind_param("siiss", $token, $sec_id, $company_id, $device_name, $expires_at);
        if (!$t_stmt->execute()) {
            Logger::error('login', 'pp_auth_sessions INSERT failed (secondary)', ['error' => $t_stmt->error]);
            echo json_encode(['success' => false, 'error' => 'Session creation failed. Please try again.']);
            exit;
        }
        Logger::info('login', 'secondary_success', ['company' => $company_slug, 'sec_id' => $sec_id, 'token_prefix' => substr($token, 0, 8)]);
        echo json_encode(['success'=>true,'token'=>$token,'expires_at'=>$expires_at,'security_mode'=>($m_row['security_mode'] ?? 'individual_pin'),'user'=>[
            'id'=>(int)$sec['id'],
            'name'=>$sec['name'],
            'role'=>$sec['role'],
            'can_pick'=>(bool)($sec['can_pick'] ?? 0),
            'can_pack'=>(bool)($sec['can_pack'] ?? 0),
            'can_print'=>(bool)($sec['can_print'] ?? 0),
        ]]);
        exit;
    }
}

if ($action === 'set_secondary_password') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }

    $password = trim((string)($input['password'] ?? ''));
    if (strlen($password) < 4) {
        echo json_encode(['success' => false, 'error' => 'Secondary password must be at least 4 characters.']);
        exit;
    }

    $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
    $sp_stmt = $master_db->prepare("UPDATE pp_clients SET secondary_password_hash = ? WHERE client_db_name = ? LIMIT 1");
    $sp_stmt->bind_param("ss", $hash, $_resolved_client_db);
    $sp_stmt->execute();

    Logger::warn('auth.password_change', 'Secondary password changed', ['by' => $caller['name'] ?? 'unknown']);

    echo json_encode(['success' => true]);
    exit;
}

if (in_array($action, ['get_security_mode', 'get_client_security_mode'], true)) {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
    if (!$master_db) {
        echo json_encode(['success' => false, 'error' => 'Master registry not available']);
        exit;
    }

    $stmt = $master_db->prepare("SELECT security_mode FROM pp_clients WHERE client_db_name = ? LIMIT 1");
    $stmt->bind_param('s', $_resolved_client_db);
    $stmt->execute();
    $row = $stmt->get_result()->fetch_assoc();
    $security_mode = $row['security_mode'] ?? 'individual_pin';

    echo json_encode(['success' => true, 'security_mode' => $security_mode]);
    exit;
}

if (in_array($action, ['update_security_mode', 'set_security_mode', 'save_security_mode'], true)) {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }
    if (!$master_db) {
        echo json_encode(['success' => false, 'error' => 'Master registry not available']);
        exit;
    }

    $security_mode = trim((string)($input['security_mode'] ?? $input['mode'] ?? ''));
    if (!in_array($security_mode, ['master_only', 'shared_honor', 'individual_pin'], true)) {
        echo json_encode(['success' => false, 'error' => 'Invalid security mode']);
        exit;
    }

    $stmt = $master_db->prepare("UPDATE pp_clients SET security_mode = ? WHERE client_db_name = ? LIMIT 1");
    $stmt->bind_param('ss', $security_mode, $_resolved_client_db);
    if (!$stmt->execute()) {
        echo json_encode(['success' => false, 'error' => 'Could not update security mode']);
        exit;
    }

    Logger::warn('security.mode_change', 'Security mode updated', ['mode' => $security_mode, 'by' => $caller['name'] ?? 'unknown']);
    echo json_encode(['success' => true, 'security_mode' => $security_mode]);
    exit;
}

if ($action == 'logout') {
    $req_headers = function_exists('getallheaders') ? getallheaders() : [];
    $token = null;
    foreach ($req_headers as $k => $v) {
        if (strtolower($k) === 'x-auth-token') { $token = $v; break; }
    }
    if (!$token) $token = $input['token'] ?? null;
    if ($token) {
        $token = preg_replace('/[^a-f0-9]/', '', strtolower(trim($token)));
        $stmt = $db->prepare("DELETE FROM pp_auth_sessions WHERE token = ?");
        $stmt->bind_param("s", $token);
        $stmt->execute();
    }
    echo json_encode(['success' => true]);
    exit;
}

if ($action == 'create_staff') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }
    $company_id = (int)$caller['company_id'];
    $username   = trim($input['username']     ?? '');
    $disp_name  = trim($input['display_name'] ?? $username);
    $password   = $input['password']          ?? '';
    $role       = in_array($input['role'] ?? '', ['master', 'secondary']) ? $input['role'] : 'secondary';
    $can_pick   = isset($input['can_pick'])  ? (int)(bool)$input['can_pick']  : 1;
    $can_pack   = isset($input['can_pack'])  ? (int)(bool)$input['can_pack']  : 1;
    $can_print  = isset($input['can_print']) ? (int)(bool)$input['can_print'] : 0;

    if (!$username || strlen($password) < 8) {
        echo json_encode(['success' => false, 'error' => 'Username required. Password must be at least 8 characters.']);
        exit;
    }

    $chk = $db->prepare("SELECT id FROM pp_staff_members WHERE company_id = ? AND username = ?");
    $chk->bind_param("is", $company_id, $username);
    $chk->execute();
    if ($chk->get_result()->fetch_assoc()) {
        echo json_encode(['success' => false, 'error' => 'Username already taken']);
        exit;
    }

    $pw_hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
    $stmt = $db->prepare("INSERT INTO pp_staff_members (company_id, username, password_hash, name, role, can_pick, can_pack, can_print, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)");
    $stmt->bind_param("issssiii", $company_id, $username, $pw_hash, $disp_name, $role, $can_pick, $can_pack, $can_print);
    $stmt->execute();
    echo json_encode(['success' => true, 'id' => (int)$db->insert_id]);
    exit;
}

if ($action == 'list_staff_full') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }
    $company_id = (int)$caller['company_id'];
    $stmt = $db->prepare("SELECT id, username, name, role, can_pick, can_pack, can_print, pin_code, is_active, last_login FROM pp_staff_members WHERE company_id = ? ORDER BY name ASC");
    $stmt->bind_param("i", $company_id);
    $stmt->execute();
    $rows = $stmt->get_result();
    $staff = [];
    while ($row = $rows->fetch_assoc()) {
        $staff[] = [
            'id'         => (int)$row['id'],
            'username'   => $row['username'],
            'name'       => $row['name'],
            'role'       => $row['role'],
            'can_pick'   => (bool)$row['can_pick'],
            'can_pack'   => (bool)$row['can_pack'],
            'can_print'  => (bool)$row['can_print'],
            'pin_code'   => $row['pin_code'],
            'is_active'  => (bool)$row['is_active'],
            'last_login' => $row['last_login'],
        ];
    }
    echo json_encode(['success' => true, 'staff' => $staff]);
    exit;
}

if ($action == 'update_staff_perms') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }
    $company_id = (int)$caller['company_id'];
    $target_id  = (int)($input['id'] ?? 0);
    if ($target_id <= 0) { echo json_encode(['success' => false, 'error' => 'Invalid id']); exit; }

    $sets = []; $types = ''; $params = [];
    if (isset($input['can_pick']))  { $sets[] = '`can_pick` = ?';  $types .= 'i'; $params[] = (int)(bool)$input['can_pick']; }
    if (isset($input['can_pack']))  { $sets[] = '`can_pack` = ?';  $types .= 'i'; $params[] = (int)(bool)$input['can_pack']; }
    if (isset($input['can_print'])) { $sets[] = '`can_print` = ?'; $types .= 'i'; $params[] = (int)(bool)$input['can_print']; }
    if (array_key_exists('pin_code', $input)) {
        $pin_code = trim((string)$input['pin_code']);
        if ($pin_code === '') {
            $sets[] = '`pin_code` = NULL';
        } elseif (strlen($pin_code) === 4 && ctype_digit($pin_code)) {
            $sets[] = '`pin_code` = ?';
            $types .= 's';
            $params[] = $pin_code;
        } else {
            echo json_encode(['success' => false, 'error' => 'PIN must be exactly 4 digits.']);
            exit;
        }
    }
    if (isset($input['is_active'])) { $sets[] = '`is_active` = ?'; $types .= 'i'; $params[] = (int)(bool)$input['is_active']; }
    if (isset($input['role']) && in_array($input['role'], ['master', 'secondary'])) {
        $sets[] = '`role` = ?'; $types .= 's'; $params[] = $input['role'];
    }
    if (empty($sets)) { echo json_encode(['success' => false, 'error' => 'Nothing to update']); exit; }

    $params[] = $target_id;
    $params[] = $company_id;
    $types   .= 'ii';

    $stmt = $db->prepare("UPDATE pp_staff_members SET " . implode(', ', $sets) . " WHERE id = ? AND company_id = ?");
    $stmt->bind_param($types, ...$params);
    $stmt->execute();
    Logger::info('staff.permission_change', 'Staff permissions updated', ['staff_id' => $target_id, 'changes' => $input, 'by' => $caller['name'] ?? 'unknown']);
    echo json_encode(['success' => true]);
    exit;
}

if ($action == 'get_provider_config') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }
    $company_id = (int)$caller['company_id'];
    // api_key and bearer_token are intentionally excluded from this response
    $excluded_keys = ['api_key', 'api_secret', 'bearer_token', 'secondary_shared_password'];
    $stmt = $db->prepare("SELECT category, provider_slug, `key`, `value` FROM pp_settings WHERE company_id = ? ORDER BY category, provider_slug");
    $stmt->bind_param('i', $company_id);
    $stmt->execute();
    $res = $stmt->get_result();
    $grouped = [];
    while ($row = $res->fetch_assoc()) {
        $k = $row['category'] . '/' . $row['provider_slug'];
        if (in_array($row['key'], $excluded_keys, true)) {
            $grouped[$k]['__has_' . $row['key']] = true;
            continue;
        }
        $grouped[$k][$row['key']] = $row['value'];
    }
    echo json_encode(['success' => true, 'configs' => $grouped]);
    exit;
}

if ($action == 'force_update_courier') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }

    $provider_slug = strtolower(trim((string)($input['provider_slug'] ?? $pm->getShipSlug())));
    $active_slug = strtolower(trim((string)$pm->getShipSlug()));

    if ($provider_slug === '') $provider_slug = $active_slug;
    if ($provider_slug !== $active_slug) {
        Logger::warn('force_update_courier', 'provider_slug_differs_from_active', [
            'requested' => $provider_slug,
            'active' => $active_slug,
        ]);
    }

    if (in_array($provider_slug, ['zappy', 'courierit1'], true)) {
        $catalog_hook = $provider_slug . '_provider_stock_options_result';
        $display_provider = $provider_slug === 'courierit1' ? 'CourierIT1' : ucfirst($provider_slug);
        $catalog_result = function_exists($catalog_hook)
            ? $catalog_hook()
            : ['success' => false, 'error' => $display_provider . ' package sync function is not available.', 'options' => []];
        $options = is_array($catalog_result['options'] ?? null) ? $catalog_result['options'] : [];
        if (empty($catalog_result['success'])) {
            echo json_encode([
                'success' => false,
                'error' => $catalog_result['error'] ?? ($display_provider . ' package catalogue could not be loaded.'),
                'provider' => $provider_slug,
                'result' => ['stocks' => ['success' => false, 'count' => 0]],
            ]);
            exit;
        }
        echo json_encode([
            'success' => true,
            'provider' => $provider_slug,
            'db' => $_resolved_client_db,
            'result' => [
                'message' => ($catalog_result['error'] ?? '') ?: ($display_provider . ' package catalogue loaded'),
                'stocks' => ['success' => true, 'count' => count($options)],
            ],
        ]);
        exit;
    }

    if ($provider_slug !== 'packie') {
        echo json_encode(['success' => false, 'error' => 'Force update is not implemented for this courier yet.']);
        exit;
    }

    if (!function_exists('sync_packie_orders_and_stocks')) {
        echo json_encode(['success' => false, 'error' => 'Packie sync function not available']);
        exit;
    }

    // Execute sync in-process so writes stay on the currently routed client DB.
    $parsed = sync_packie_orders_and_stocks($db);
    if (!is_array($parsed) || empty($parsed['success'])) {
        echo json_encode(['success' => false, 'error' => $parsed['message'] ?? 'Force update failed', 'result' => $parsed]);
        exit;
    }

    echo json_encode(['success' => true, 'provider' => $provider_slug, 'db' => $_resolved_client_db, 'result' => $parsed]);
    exit;
}

if ($action == 'get_package_mapping') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }

    $provider_slug = strtolower(trim((string)($_GET['provider_slug'] ?? $input['provider_slug'] ?? $pm->getShipSlug())));
    echo json_encode(afri_get_package_mapping($db, $provider_slug));
    exit;
}

if ($action == 'save_package_mapping') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }

    $provider_slug = strtolower(trim((string)($input['provider_slug'] ?? $pm->getShipSlug())));
    $mappings = is_array($input['mappings'] ?? null) ? $input['mappings'] : [];
    $stock_settings = is_array($input['stock_settings'] ?? null) ? $input['stock_settings'] : [];
    echo json_encode(afri_save_package_mapping($db, $provider_slug, $mappings, $stock_settings));
    exit;
}

if ($action == 'save_provider_config') {
    $caller = get_current_user_from_request($db);
    if (!$caller || $caller['role'] !== 'master') {
        http_response_code(403);
        echo json_encode(['success' => false, 'error' => 'Master access required']);
        exit;
    }

    $category = strtolower(trim($input['category'] ?? ''));
    $provider_slug = strtolower(trim($input['provider_slug'] ?? ''));
    $fields = $input['fields'] ?? null;

    if (!in_array($category, ['shipping', 'ecommerce'], true) || !is_array($fields)) {
        echo json_encode(['success' => false, 'error' => 'Invalid payload']);
        exit;
    }

    $company_pm = new ProviderManager($db, (int)$caller['company_id']);

    if ($category === 'ecommerce' && in_array($provider_slug, ['none', 'disabled', 'off'], true)) {
        $company_pm->saveSetting('general', 'app', 'active_ecom_provider', '');
        echo json_encode(['success' => true, 'ecommerce_enabled' => false]);
        exit;
    }

    if ($provider_slug === '') {
        echo json_encode(['success' => false, 'error' => 'Invalid provider_slug']);
        exit;
    }

    if ($category === 'shipping' && $provider_slug === 'nzpost' && $company_pm->getEcomSlug() === '') {
        echo json_encode(['success' => false, 'error' => 'NZPost requires an ecommerce provider. Enable ecommerce first.']);
        exit;
    }
    if ($category === 'shipping' && !courier_allowed_for_company($master_db, $provider_slug, null, $_resolved_client_db)) {
        echo json_encode(['success' => false, 'error' => ucfirst($provider_slug) . ' is not enabled for this company.']);
        exit;
    }

    $saved_fields = [];
    foreach ($fields as $key => $value) {
        $safe_key = preg_replace('/[^a-z0-9_\-\.]/i', '', (string)$key);
        if ($safe_key === '') continue;
        if ($category === 'shipping' && in_array($provider_slug, ['zappy', 'courierit1'], true)) {
            $lk = strtolower($safe_key);
            if (
                in_array($lk, ['username', 'user_name', 'user', 'login', 'usernamefront', 'user_name_front'], true)
                || strpos($lk, 'username') !== false
                || strpos($lk, 'user_name') !== false
            ) {
                $safe_key = 'username';
            } elseif (
                in_array($lk, ['password', 'pass', 'api_key', 'apikey', 'api-secret', 'api_secret'], true)
                || strpos($lk, 'password') !== false
                || strpos($lk, 'pass') !== false
                || strpos($lk, 'apikey') !== false
                || strpos($lk, 'api_key') !== false
            ) {
                $safe_key = 'api_key';
            } elseif ($provider_slug === 'zappy' && (in_array($lk, ['clientid', 'client_id'], true) || strpos($lk, 'clientid') !== false || strpos($lk, 'client_id') !== false)) {
                $safe_key = 'client_id';
            }
        }
        if (is_array($value) || is_object($value)) {
            $value = json_encode($value);
        }
        $company_pm->saveSetting($category, $provider_slug, $safe_key, (string)($value ?? ''));
        $saved_fields[$safe_key] = true;
    }

    if ($category === 'shipping') {
        if ($provider_slug === 'zappy') {
            $zappy_base = trim((string)($fields['base_url'] ?? $fields['baseUrl'] ?? $fields['shipping_base_url'] ?? '')) ?: 'https://ship.zappy.nz';
            $company_pm->saveSetting('shipping', 'zappy', 'base_url', $zappy_base);
            $saved_fields['base_url'] = true;
            if (empty($saved_fields['client_id'])) {
                $company_pm->saveSetting('shipping', 'zappy', 'client_id', '2879');
                $saved_fields['client_id'] = true;
            }
        }
        if ($provider_slug === 'courierit1') {
            $courierit1_base = trim((string)($fields['base_url'] ?? $fields['baseUrl'] ?? $fields['shipping_base_url'] ?? '')) ?: 'https://courierit1.net.nz/api';
            $company_pm->saveSetting('shipping', 'courierit1', 'base_url', $courierit1_base);
            $saved_fields['base_url'] = true;
        }
        $company_pm->saveSetting('general', 'app', 'active_shipping_provider', $provider_slug);
    } else {
        $company_pm->saveSetting('general', 'app', 'active_ecom_provider', $provider_slug);
    }

    Logger::warn('config.save', 'Provider config saved', ['category' => $category, 'provider_slug' => $provider_slug, 'by' => $caller['name'] ?? 'unknown']);

    $response = ['success' => true];
    if ($category === 'shipping' && $provider_slug === 'zappy') {
        $response['zappy_saved'] = [
            'username' => !empty($saved_fields['username']),
            'password' => !empty($saved_fields['api_key']),
            'client_id' => !empty($saved_fields['client_id']),
        ];
    }
    if ($category === 'shipping' && $provider_slug === 'courierit1') {
        $response['courierit1_saved'] = [
            'username' => !empty($saved_fields['username']),
            'password' => !empty($saved_fields['api_key']),
            'base_url' => !empty($saved_fields['base_url']),
        ];
    }
    echo json_encode($response);
    exit;
}

// ====================================================================
// MASTER DB ACTIONS
// ====================================================================

/**
 * verify_client
 * POST { business_slug, client_key }
 * Checks the slug + key against the master registry.
 * Returns { valid, business_name, client_db_name } or error.
 * Used by the First Run onboarding wizard to gate setup.
 */
if ($action === 'verify_client') {
    if (!$master_db) {
        echo json_encode(['valid' => false, 'error' => 'Master registry not initialised. Run setup_master.php first.']);
        exit;
    }
    $slug = preg_replace('/[^a-z0-9\-_]/', '', strtolower(trim($input['business_slug'] ?? '')));
    $key  = trim($input['client_key'] ?? '');
    if (!$slug || !$key) {
        echo json_encode(['valid' => false, 'error' => 'Business slug and setup key are required.']);
        exit;
    }
    $stmt = $master_db->prepare("
        SELECT id, business_name, client_key_hash, client_db_name, is_active, setup_completed_at
        FROM pp_clients WHERE slug = ?
    ");
    $stmt->bind_param('s', $slug);
    $stmt->execute();
    $client = $stmt->get_result()->fetch_assoc();
    if (!$client || !$client['is_active']) {
        echo json_encode(['valid' => false, 'error' => 'Business not found. Contact your system administrator.']);
        exit;
    }
    if (!password_verify($key, $client['client_key_hash'])) {
        echo json_encode(['valid' => false, 'error' => 'Invalid setup key.']);
        exit;
    }
    // Mark setup_completed_at on first successful verification (idempotent)
    if (!$client['setup_completed_at']) {
        $upd = $master_db->prepare("UPDATE pp_clients SET setup_completed_at = NOW() WHERE id = ?");
        $upd->bind_param('i', $client['id']);
        $upd->execute();
    }
    echo json_encode([
        'valid'         => true,
        'business_name' => $client['business_name'],
        'client_db_name'=> $client['client_db_name'],
    ]);
    exit;
}

/**
 * list_shipping_providers
 * GET  (no auth required — public provider catalogue)
 * Returns all active shipping providers with their setup wizard field definitions.
 */
if ($action === 'list_shipping_providers') {
    if (!$master_db) {
        echo json_encode(['success' => false, 'error' => 'Master registry not available.']);
        exit;
    }
    $res = $master_db->query("
        SELECT slug, display_name, auth_type, required_fields, notes
        FROM pp_shipping_providers WHERE is_active = 1 ORDER BY display_name
    ");
    $providers = [];
    $seen_provider_slugs = [];
    while ($row = $res->fetch_assoc()) {
        $slug = (string)$row['slug'];
        if (isset($seen_provider_slugs[$slug])) continue;
        $seen_provider_slugs[$slug] = true;
        $providers[] = [
            'slug'             => $slug,
            'display_name'     => $row['display_name'],
            'auth_type'        => $row['auth_type'],
            'required_fields'  => json_decode($row['required_fields'], true),
            'notes'            => $row['notes'],
        ];
    }
    $has_zappy = false;
    $providers = array_values(array_filter($providers, function ($provider) {
        return (string)($provider['slug'] ?? '') !== 'courierit1';
    }));
    foreach ($providers as $p) {
        if (($p['slug'] ?? '') === 'zappy') { $has_zappy = true; }
    }
    if (!$has_zappy) {
        $providers[] = [
            'slug' => 'zappy',
            'display_name' => 'Zappy',
            'auth_type' => 'form_cookie',
            'required_fields' => [
                ['key' => 'base_url', 'label' => 'Zappy Base URL', 'type' => 'text', 'example' => 'https://ship.zappy.nz'],
                ['key' => 'username', 'label' => 'Zappy Username', 'type' => 'text', 'example' => ''],
                ['key' => 'api_key', 'label' => 'Zappy Password', 'type' => 'password', 'example' => ''],
                ['key' => 'client_id', 'label' => 'Zappy Client ID', 'type' => 'text', 'example' => '2879'],
                ['key' => 'sender_address_id', 'label' => 'Pickup Address ID', 'type' => 'text', 'example' => '133857'],
                ['key' => 'sender_suburb', 'label' => 'Pickup Suburb', 'type' => 'text', 'example' => 'Palmerston North'],
                ['key' => 'sender_email', 'label' => 'Pickup Email', 'type' => 'text', 'example' => 'sales@example.com'],
                ['key' => 'sender_phone', 'label' => 'Pickup Phone', 'type' => 'text', 'example' => ''],
            ],
            'notes' => 'Uses Zappy web session login for live quote lookup. Booking/label creation still needs the Zappy create-label request captured.',
        ];
    }

    $allowed_couriers = get_allowed_couriers_for_company($master_db, $_boot_company_slug, null);
    if (!empty($allowed_couriers)) {
        $providers = array_values(array_filter($providers, function ($provider) use ($allowed_couriers) {
            return in_array((string)($provider['slug'] ?? ''), $allowed_couriers, true);
        }));
    }

    echo json_encode(['success' => true, 'providers' => $providers]);
    exit;
}

/**
 * list_cms_providers
 * GET  (no auth required — public provider catalogue)
 * Returns all active CMS providers with their setup wizard field definitions.
 */
if ($action === 'list_cms_providers') {
    if (!$master_db) {
        echo json_encode(['success' => false, 'error' => 'Master registry not available.']);
        exit;
    }
    $res = $master_db->query("
        SELECT slug, display_name, api_base_pattern, required_fields, notes
        FROM pp_cms_providers WHERE is_active = 1 ORDER BY display_name
    ");
    $providers = [];
    while ($row = $res->fetch_assoc()) {
        if ((string)$row['slug'] === 'bigcommerce') continue;
        $providers[] = [
            'slug'            => $row['slug'],
            'display_name'    => $row['display_name'],
            'required_fields' => json_decode($row['required_fields'], true),
            'notes'           => $row['notes'],
        ];
    }
    echo json_encode(['success' => true, 'providers' => $providers]);
    exit;
}

// ── get_app_settings — fetch app settings from pp_settings ──────────────────
if ($action == 'get_app_settings') {
    $caller = get_current_user_from_request($db);
    if (!$caller) { http_response_code(401); echo json_encode(['success' => false, 'error' => 'Unauthorized']); exit; }
    $company_id = (int)$caller['company_id'];
    $stmt = $db->prepare("SELECT `key`, `value` FROM pp_settings WHERE company_id = ? AND category = 'general' AND provider_slug = 'app_settings'");
    $stmt->bind_param('i', $company_id);
    $stmt->execute();
    $res = $stmt->get_result();
    $settings = [];
    while ($row = $res->fetch_assoc()) {
        $settings[$row['key']] = $row['value'];
    }
    if ($master_db) {
        $sm_stmt = $master_db->prepare("SELECT security_mode FROM pp_clients WHERE client_db_name = ? LIMIT 1");
        $sm_stmt->bind_param('s', $_resolved_client_db);
        $sm_stmt->execute();
        $sm_row = $sm_stmt->get_result()->fetch_assoc();
        if ($sm_row) {
            $settings['security_mode'] = $sm_row['security_mode'] ?? 'individual_pin';
            $settings['securityMode'] = $sm_row['security_mode'] ?? 'individual_pin';
        }
    }
    echo json_encode(['success' => true, 'settings' => $settings]);
    exit;
}

// ── save_app_settings — persist app settings to pp_settings ─────────────────
if ($action == 'save_app_settings') {
    $caller = get_current_user_from_request($db);
    if (!$caller) { http_response_code(401); echo json_encode(['success' => false, 'error' => 'Unauthorized']); exit; }
    $settings = $input['settings'] ?? [];
    if (!is_array($settings)) { echo json_encode(['success' => false, 'error' => 'Invalid settings']); exit; }
    $company_id = (int)$caller['company_id'];
    foreach ($settings as $key => $value) {
        if ($key === 'security_mode' || $key === 'securityMode') {
            if (($caller['role'] ?? '') !== 'master') {
                http_response_code(403);
                echo json_encode(['success' => false, 'error' => 'Master access required']);
                exit;
            }
            $security_mode = trim((string)$value);
            if (!in_array($security_mode, ['master_only', 'shared_honor', 'individual_pin'], true)) {
                echo json_encode(['success' => false, 'error' => 'Invalid security mode']);
                exit;
            }
            if (!$master_db) {
                echo json_encode(['success' => false, 'error' => 'Master registry not available']);
                exit;
            }
            $sm_stmt = $master_db->prepare("UPDATE pp_clients SET security_mode = ? WHERE client_db_name = ? LIMIT 1");
            $sm_stmt->bind_param('ss', $security_mode, $_resolved_client_db);
            $sm_stmt->execute();
            continue;
        }
        $safe_key = preg_replace('/[^a-zA-Z0-9_]/', '', $key);
        if ($safe_key === '') continue;
        $pm->saveSetting('general', 'app_settings', $safe_key, (string)$value);
    }
    Logger::info('settings.save', 'App settings saved', ['by' => $caller['name'] ?? 'unknown', 'keys' => array_keys($settings)]);
    echo json_encode(['success' => true]);
    exit;
}

echo json_encode(["error" => "Unknown action"]);
?>
