<?php
/**
 * MJ Marketplace v3.0.4.2 Advanced Batch Installer Runner
 * - JSON-only AJAX endpoint.
 * - Runs SQL in small batches so browser/server does not hang.
 * - No v3.0.5 location/city importer.
 */
@ini_set('display_errors', '0');
@ini_set('log_errors', '1');
@set_time_limit(25);
if (session_status() !== PHP_SESSION_ACTIVE) { @session_start(); }

$root = realpath(__DIR__ . '/..');
$stateDir = __DIR__ . '/runtime';
$legacyStateFile = __DIR__ . '/.mj_install_state.json';
$logFile = __DIR__ . '/install-log.txt';
$configFile = $root . '/includes/config.php';
$lockFile = __DIR__ . '/installed.lock';
$MJ_INSTALL_JSON_SENT = false;
if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8'); }
@ob_start();
register_shutdown_function(function() {
    global $MJ_INSTALL_JSON_SENT, $logFile;
    if ($MJ_INSTALL_JSON_SENT) { return; }
    $err = error_get_last();
    if ($err && in_array($err['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) {
        while (ob_get_level() > 0) { @ob_end_clean(); }
        if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8', true, 500); }
        $msg = 'PHP fatal error: ' . $err['message'] . ' in ' . basename($err['file']) . ' line ' . $err['line'];
        @file_put_contents($logFile, '[' . date('Y-m-d H:i:s') . '] ERROR: ' . $msg . PHP_EOL, FILE_APPEND | LOCK_EX);
        echo json_encode(array('ok'=>false, 'error'=>$msg));
    }
});


function mj_random_install_id() {
    if (function_exists('random_bytes')) { return bin2hex(random_bytes(8)); }
    if (function_exists('openssl_random_pseudo_bytes')) { return bin2hex(openssl_random_pseudo_bytes(8)); }
    return substr(sha1(uniqid('', true) . mt_rand()), 0, 16);
}

function mj_json($data) {
    global $MJ_INSTALL_JSON_SENT;
    $MJ_INSTALL_JSON_SENT = true;
    while (ob_get_level() > 0) { @ob_end_clean(); }
    if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8'); }
    echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}
function mj_fail($message, $extra = []) {
    mj_log('ERROR: ' . $message);
    mj_json(array_merge(['ok'=>false, 'error'=>$message], $extra));
}
function mj_post($key, $default = '') {
    return isset($_POST[$key]) ? $_POST[$key] : $default;
}
function mj_human_bytes($bytes) {
    $units = ['B','KB','MB','GB'];
    $i = 0; $v = max(0, $bytes);
    while ($v >= 1024 && $i < count($units)-1) { $v /= 1024; $i++; }
    return round($v, 2) . ' ' . $units[$i];
}
function mj_log($line) {
    global $logFile;
    $prefix = '[' . date('Y-m-d H:i:s') . '] ';
    @file_put_contents($logFile, $prefix . $line . PHP_EOL, FILE_APPEND | LOCK_EX);
}
function mj_read_log($lines = 80) {
    global $logFile;
    if (!is_file($logFile)) { return []; }
    $content = @file($logFile, FILE_IGNORE_NEW_LINES);
    if (!$content) { return []; }
    return array_slice($content, -$lines);
}
function mj_backtick($name) { return '`' . str_replace('`', '``', $name) . '`'; }
function mj_detect_base_url() {
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['SERVER_PORT'] ?? '') == 443);
    $scheme = $https ? 'https' : 'http';
    $host = (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost');
    $script = str_replace('\\', '/', (string)((isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '/install/install-runner.php')));
    $basePath = preg_replace('#/install/install-runner\.php$#', '', $script);
    $basePath = rtrim((string)$basePath, '/');
    return $scheme . '://' . $host . ($basePath ? $basePath : '');
}
function mj_table_from_sql($sql) {
    $s = preg_replace('/\s+/', ' ', trim($sql));
    $patterns = [
        '/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([a-zA-Z0-9_]+)`?/i',
        '/^ALTER\s+TABLE\s+`?([a-zA-Z0-9_]+)`?/i',
        '/^INSERT\s+(?:IGNORE\s+)?INTO\s+`?([a-zA-Z0-9_]+)`?/i',
        '/^REPLACE\s+INTO\s+`?([a-zA-Z0-9_]+)`?/i',
        '/^UPDATE\s+`?([a-zA-Z0-9_]+)`?/i',
        '/^DELETE\s+FROM\s+`?([a-zA-Z0-9_]+)`?/i',
        '/^DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?`?([a-zA-Z0-9_]+)`?/i',
        '/^CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+`?([a-zA-Z0-9_]+)`?/i',
        '/^CREATE\s+(?:DEFINER=.*?\s+)?TRIGGER\s+`?([a-zA-Z0-9_]+)`?/i',
        '/^CREATE\s+(?:DEFINER=.*?\s+)?PROCEDURE\s+`?([a-zA-Z0-9_]+)`?/i',
    ];
    foreach ($patterns as $p) {
        if (preg_match($p, $s, $m)) { return $m[1]; }
    }
    if (preg_match('/^SET\s+/i', $s)) { return 'session/settings'; }
    return 'general';
}
function mj_preview_sql($sql) {
    $s = preg_replace('/\s+/', ' ', trim($sql));
    if (function_exists('mb_substr')) { return mb_substr((string)$s, 0, 220); }
    return substr((string)$s, 0, 220);
}
function mj_split_sql_semicolon_statements($sql) {
    $out = [];
    $buf = '';
    $quote = '';
    $len = strlen($sql);
    for ($i = 0; $i < $len; $i++) {
        $ch = $sql[$i];
        if ($quote !== '') {
            $buf .= $ch;
            if ($quote === '`') {
                if ($ch === '`') { $quote = ''; }
            } elseif ($ch === $quote) {
                // MySQL allows doubled quotes inside strings: '' or "".
                if ($i + 1 < $len && $sql[$i + 1] === $quote) {
                    $i++;
                    $buf .= $sql[$i];
                } else {
                    $quote = '';
                }
            } elseif ($ch === '\\' && $i + 1 < $len) {
                $i++;
                $buf .= $sql[$i];
            }
            continue;
        }
        if ($ch === "'" || $ch === '"' || $ch === '`') {
            $quote = $ch;
            $buf .= $ch;
            continue;
        }
        if ($ch === ';') {
            $stmt = trim($buf);
            if ($stmt !== '') { $out[] = $stmt; }
            $buf = '';
            continue;
        }
        $buf .= $ch;
    }
    $stmt = trim($buf);
    if ($stmt !== '') { $out[] = $stmt; }
    return $out;
}
function mj_parse_sql_file($file) {
    if (!is_file($file)) { throw new Exception('SQL file not found: ' . basename($file)); }
    $handle = fopen($file, 'rb');
    if (!$handle) { throw new Exception('Cannot open SQL file: ' . basename($file)); }
    $delimiter = ';';
    $buffer = '';
    $lineNo = 0;
    $statementStartLine = 1;
    $statements = [];
    while (($line = fgets($handle)) !== false) {
        $lineNo++;
        $trim = trim($line);
        if (preg_match('/^DELIMITER\s+(.+)$/i', $trim, $m)) {
            if (trim($buffer) !== '') {
                $sql = trim($buffer);
                if ($delimiter === ';') {
                    foreach (mj_split_sql_semicolon_statements($sql) as $part) {
                        $statements[] = ['sql'=>$part, 'line'=>$statementStartLine, 'table'=>mj_table_from_sql($part)];
                    }
                } else {
                    $statements[] = ['sql'=>$sql, 'line'=>$statementStartLine, 'table'=>mj_table_from_sql($sql)];
                }
                $buffer = '';
            }
            $delimiter = trim($m[1]);
            $statementStartLine = $lineNo + 1;
            continue;
        }
        if ($buffer === '' && ($trim === '' || strpos($trim, '--') === 0 || strpos($trim, '#') === 0)) {
            continue;
        }
        if ($buffer === '') { $statementStartLine = $lineNo; }
        $buffer .= $line;
        if ($delimiter === ';') {
            if (substr(rtrim($buffer), -1) === ';') {
                $sql = trim($buffer);
                foreach (mj_split_sql_semicolon_statements($sql) as $part) {
                    $statements[] = ['sql'=>$part, 'line'=>$statementStartLine, 'table'=>mj_table_from_sql($part)];
                }
                $buffer = '';
            }
        } else {
            $rtrim = rtrim($buffer);
            if ($delimiter !== '' && substr($rtrim, -strlen($delimiter)) === $delimiter) {
                $sql = trim(substr($rtrim, 0, -strlen($delimiter)));
                if ($sql !== '') { $statements[] = ['sql'=>$sql, 'line'=>$statementStartLine, 'table'=>mj_table_from_sql($sql)]; }
                $buffer = '';
            }
        }
    }
    fclose($handle);
    if (trim($buffer) !== '') {
        $sql = trim($buffer);
        if ($delimiter === ';') {
            foreach (mj_split_sql_semicolon_statements($sql) as $part) {
                $statements[] = ['sql'=>$part, 'line'=>$statementStartLine, 'table'=>mj_table_from_sql($part)];
            }
        } else {
            $statements[] = ['sql'=>$sql, 'line'=>$statementStartLine, 'table'=>mj_table_from_sql($sql)];
        }
    }
    return $statements;
}
function mj_is_variable_set_statement($sql) {
    $s = trim($sql);
    return (bool)(preg_match('/^SET\s+@[a-zA-Z0-9_]+\s*:?=/i', $s) || preg_match('/^SELECT\s+.+\s+INTO\s+@[a-zA-Z0-9_]+/is', $s));
}
function mj_prepare_from_variable($sql) {
    if (preg_match('/^PREPARE\s+[a-zA-Z0-9_]+\s+FROM\s+(@[a-zA-Z0-9_]+)/i', trim($sql), $m)) { return $m[1]; }
    return '';
}
function mj_dynamic_group_end_index($statements, $index) {
    $max = min(count($statements), $index + 20);
    $seenPrepare = false;
    if (mj_is_variable_set_statement($statements[$index]['sql'])) {
        for ($j = $index; $j < $max; $j++) {
            $sql = trim($statements[$j]['sql']);
            if (mj_prepare_from_variable($sql) !== '') { $seenPrepare = true; }
            if ($seenPrepare && preg_match('/^DEALLOCATE\s+PREPARE\s+/i', $sql)) { return $j; }
            if (!$seenPrepare && $j > $index && !mj_is_variable_set_statement($sql) && mj_prepare_from_variable($sql) === '') { return $index; }
        }
    }
    if (mj_prepare_from_variable($statements[$index]['sql']) !== '') {
        for ($j = $index; $j < $max; $j++) {
            if (preg_match('/^DEALLOCATE\s+PREPARE\s+/i', trim($statements[$j]['sql']))) { return $j; }
        }
    }
    return $index;
}
function mj_dynamic_prepare_prelude($pdo, $statements, $index) {
    $var = mj_prepare_from_variable($statements[$index]['sql']);
    if ($var === '') { return; }
    $start = max(0, $index - 10);
    $pre = [];
    for ($j = $index - 1; $j >= $start; $j--) {
        $sql = trim($statements[$j]['sql']);
        if (mj_is_variable_set_statement($sql)) { array_unshift($pre, $sql); continue; }
        break;
    }
    foreach ($pre as $sql) { if ($sql !== '') { mj_exec_sql($pdo, $sql); } }
    mj_exec_sql($pdo, 'SET ' . $var . ' := COALESCE(' . $var . ', \'SELECT 1\')');
}
function mj_install_id_from_request() {
    $id = (string)($_POST['install_id'] ?? $_GET['install_id'] ?? (isset($_SESSION['mj_install_id']) ? $_SESSION['mj_install_id'] : ''));
    $id = preg_replace('/[^a-zA-Z0-9_\-]/', '', $id);
    if ($id === '') { $id = 'default'; }
    $_SESSION['mj_install_id'] = $id;
    return $id;
}
function mj_state_path($installId = null) {
    global $stateDir;
    $installId = $installId ?: mj_install_id_from_request();
    $installId = preg_replace('/[^a-zA-Z0-9_\-]/', '', $installId) ?: 'default';
    return rtrim($stateDir, '/\\') . '/state_' . $installId . '.json';
}
function mj_ensure_state_dir() {
    global $stateDir;
    if (!is_dir($stateDir)) { @mkdir($stateDir, 0775, true); }
    if (is_dir($stateDir) && !is_writable($stateDir)) { @chmod($stateDir, 0775); }
}
function mj_state_read() {
    global $legacyStateFile;
    $installId = mj_install_id_from_request();
    $file = mj_state_path($installId);
    if (is_file($file)) {
        $json = @file_get_contents($file);
        $state = json_decode((string)$json, true);
        if (is_array($state)) { return $state; }
    }
    if ($installId === 'default' && is_file($legacyStateFile)) {
        $json = @file_get_contents($legacyStateFile);
        $state = json_decode((string)$json, true);
        if (is_array($state)) { return $state; }
    }
    if (!empty($_SESSION['mj_install_state']) && is_array($_SESSION['mj_install_state'])) {
        return $_SESSION['mj_install_state'];
    }
    return [];
}
function mj_state_write($state) {
    global $legacyStateFile;
    mj_ensure_state_dir();
    $installId = (string)((isset($state['install_id']) ? $state['install_id'] : mj_install_id_from_request()));
    $installId = preg_replace('/[^a-zA-Z0-9_\-]/', '', $installId) ?: 'default';
    $state['install_id'] = $installId;
    $_SESSION['mj_install_id'] = $installId;
    $_SESSION['mj_install_state'] = $state;
    $json = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    $file = mj_state_path($installId);
    $ok = false;
    if (is_dir(dirname($file)) && is_writable(dirname($file))) {
        $bytes = @file_put_contents($file, $json, LOCK_EX);
        if ($bytes !== false && $bytes > 10) { @chmod($file, 0600); $ok = true; }
    }
    // Backward-compatible default file, helpful on hosts that preserve only one file.
    if ($installId === 'default') { @file_put_contents($legacyStateFile, $json, LOCK_EX); @chmod($legacyStateFile, 0600); }
    // Session fallback is enough for same-browser AJAX requests. If sessions are not active, fail early.
    if (!$ok && session_status() !== PHP_SESSION_ACTIVE) {
        throw new Exception('Installer cannot save progress. Make /install/runtime writable or enable PHP sessions.');
    }
}
function mj_state_clear() {
    global $stateDir, $legacyStateFile, $logFile;
    $installId = mj_install_id_from_request();
    @unlink(mj_state_path($installId));
    @unlink($legacyStateFile);
    if (is_dir($stateDir)) {
        foreach (glob(rtrim($stateDir,'/\\') . '/state_*.json') ?: [] as $f) { @unlink($f); }
    }
    unset($_SESSION['mj_install_state']);
    unset($_SESSION['mj_install_id']);
    @unlink($logFile);
}
function mj_pdo_from_state($state, $withDb = true) {
    $host = (isset($state['db_host']) ? $state['db_host'] : 'localhost');
    $user = (isset($state['db_user']) ? $state['db_user'] : '');
    $pass = (isset($state['db_pass']) ? $state['db_pass'] : '');
    $db = (isset($state['db_name']) ? $state['db_name'] : '');
    $dsn = 'mysql:host=' . $host . ';charset=utf8mb4';
    if ($withDb && $db !== '') { $dsn = 'mysql:host=' . $host . ';dbname=' . $db . ';charset=utf8mb4'; }
    return new PDO($dsn, $user, $pass, mj_pdo_options());
}
function mj_test_connection($input) {
    $pdo = new PDO('mysql:host='.$input['db_host'].';charset=utf8mb4', $input['db_user'], $input['db_pass'], mj_pdo_options());
    return $pdo;
}
function mj_drop_all_tables($pdo, $db) {
    mj_exec_sql($pdo, 'USE ' . mj_backtick($db));
    $tables = mj_fetch_all_sql($pdo, 'SHOW FULL TABLES WHERE Table_type = "BASE TABLE"', PDO::FETCH_NUM);
    $views = mj_fetch_all_sql($pdo, 'SHOW FULL TABLES WHERE Table_type = "VIEW"', PDO::FETCH_NUM);
    $count = 0;
    mj_exec_sql($pdo, 'SET FOREIGN_KEY_CHECKS=0');
    foreach ($views as $row) { mj_exec_sql($pdo, 'DROP VIEW IF EXISTS ' . mj_backtick($row[0])); $count++; }
    foreach ($tables as $row) { mj_exec_sql($pdo, 'DROP TABLE IF EXISTS ' . mj_backtick($row[0])); $count++; }
    mj_exec_sql($pdo, 'SET FOREIGN_KEY_CHECKS=1');
    return $count;
}
function mj_require_state() {
    $state = mj_state_read();
    if (empty($state)) { mj_fail('Installer state not found. If you already clicked Start, your server could not save install progress. Click Start Install again; if it repeats, set /install/runtime permission to 775.'); }
    return $state;
}

function mj_pdo_options() {
    $opts = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    );
    // Prevent "Cannot execute queries while other unbuffered queries are active" after EXECUTE stmt.
    if (defined('PDO::MYSQL_ATTR_USE_BUFFERED_QUERY')) {
        $opts[PDO::MYSQL_ATTR_USE_BUFFERED_QUERY] = true;
    }
    return $opts;
}
function mj_close_statement($stmt) {
    if ($stmt instanceof PDOStatement) {
        try {
            if ($stmt->columnCount() > 0) { $stmt->fetchAll(PDO::FETCH_NUM); }
        } catch (Exception $ignore) {}
        while (true) {
            try {
                if (!$stmt->nextRowset()) { break; }
                if ($stmt->columnCount() > 0) { $stmt->fetchAll(PDO::FETCH_NUM); }
            } catch (Exception $ignore) { break; }
        }
        try { $stmt->closeCursor(); } catch (Exception $ignore) {}
    }
}
function mj_exec_sql($pdo, $sql) {
    $sql = trim((string)$sql);
    if ($sql === '') { return 0; }
    $stmt = null;
    try {
        $stmt = $pdo->query($sql);
        $rows = ($stmt instanceof PDOStatement) ? $stmt->rowCount() : 0;
        mj_close_statement($stmt);
        return $rows;
    } catch (Exception $e) {
        mj_close_statement($stmt);
        throw $e;
    }
}
function mj_fetch_all_sql($pdo, $sql, $mode = null) {
    $stmt = $pdo->query($sql);
    try {
        $rows = ($mode === null) ? $stmt->fetchAll() : $stmt->fetchAll($mode);
        mj_close_statement($stmt);
        return $rows;
    } catch (Exception $e) {
        mj_close_statement($stmt);
        throw $e;
    }
}
function mj_fetch_column_sql($pdo, $sql) {
    $stmt = $pdo->query($sql);
    try {
        $val = $stmt->fetchColumn();
        mj_close_statement($stmt);
        return $val;
    } catch (Exception $e) {
        mj_close_statement($stmt);
        throw $e;
    }
}
function mj_sample_data_rehydrate_vars($pdo) {
    // Sample data is installed in AJAX batches. MySQL @variables are connection-scoped,
    // so re-create all lookup variables at the start of every sample batch.
    $sets = array(
        "SET @demo_password_hash := '$2y$12$NGleMQJMvmHnNNEplea./e2sXmoIvcSxlotv3bjv4ecDJsMgh3vQG'",
        "SET @country_sg := (SELECT id FROM mj_countries WHERE iso2='SG' OR slug='singapore' ORDER BY id LIMIT 1)",
        "SET @country_my := (SELECT id FROM mj_countries WHERE iso2='MY' OR slug='malaysia' ORDER BY id LIMIT 1)",
        "SET @country_in := (SELECT id FROM mj_countries WHERE iso2='IN' OR slug='india' ORDER BY id LIMIT 1)",
        "SET @state_sg := (SELECT id FROM mj_states WHERE slug='singapore' AND country_id=@country_sg ORDER BY id LIMIT 1)",
        "SET @state_kl := (SELECT id FROM mj_states WHERE slug='kuala-lumpur' AND country_id=@country_my ORDER BY id LIMIT 1)",
        "SET @state_johor := (SELECT id FROM mj_states WHERE slug='johor' AND country_id=@country_my ORDER BY id LIMIT 1)",
        "SET @state_tn := (SELECT id FROM mj_states WHERE slug='tamil-nadu' AND country_id=@country_in ORDER BY id LIMIT 1)",
        "SET @city_sg := (SELECT id FROM mj_cities WHERE slug='singapore' AND country_id=@country_sg ORDER BY id LIMIT 1)",
        "SET @city_cck := (SELECT id FROM mj_cities WHERE slug='choa-chu-kang' AND country_id=@country_sg ORDER BY id LIMIT 1)",
        "SET @city_kl := (SELECT id FROM mj_cities WHERE slug='kuala-lumpur' AND country_id=@country_my ORDER BY id LIMIT 1)",
        "SET @city_jb := (SELECT id FROM mj_cities WHERE slug='johor-bahru' AND country_id=@country_my ORDER BY id LIMIT 1)",
        "SET @city_chennai := (SELECT id FROM mj_cities WHERE slug='chennai' AND country_id=@country_in ORDER BY id LIMIT 1)",
        "SET @admin_demo := (SELECT id FROM mj_admin_users WHERE email='admin.demo@mjmarketplace.test' ORDER BY id LIMIT 1)",
        "SET @seller_sg := (SELECT id FROM mj_users WHERE email='seller.sg@mjmarketplace.test' ORDER BY id LIMIT 1)",
        "SET @seller_my := (SELECT id FROM mj_users WHERE email='seller.my@mjmarketplace.test' ORDER BY id LIMIT 1)",
        "SET @seller_in := (SELECT id FROM mj_users WHERE email='seller.in@mjmarketplace.test' ORDER BY id LIMIT 1)",
        "SET @buyer_demo := (SELECT id FROM mj_users WHERE email='buyer.demo@mjmarketplace.test' ORDER BY id LIMIT 1)",
        "SET @owner_demo := (SELECT id FROM mj_users WHERE email='owner.demo@mjmarketplace.test' ORDER BY id LIMIT 1)",
        "SET @cat_electronics := (SELECT id FROM mj_categories WHERE type='marketplace' AND slug='electronics-it' ORDER BY id LIMIT 1)",
        "SET @cat_home := (SELECT id FROM mj_categories WHERE type='marketplace' AND slug='home-services' ORDER BY id LIMIT 1)",
        "SET @cat_data := (SELECT id FROM mj_categories WHERE type='marketplace' AND slug='data-recovery' ORDER BY id LIMIT 1)",
        "SET @cat_company_it := (SELECT id FROM mj_categories WHERE type='company' AND slug='it-services' ORDER BY id LIMIT 1)",
        "SET @cat_company_cleaning := (SELECT id FROM mj_categories WHERE type='company' AND slug='cleaning-services' ORDER BY id LIMIT 1)",
        "SET @cat_company_beauty := (SELECT id FROM mj_categories WHERE type='company' AND slug='beauty-wellness' ORDER BY id LIMIT 1)",
        "SET @company_data := (SELECT id FROM mj_companies WHERE slug='mj-data-recovery-singapore' ORDER BY id LIMIT 1)",
        "SET @company_clean := (SELECT id FROM mj_companies WHERE slug='kl-home-cleaning-pro' ORDER BY id LIMIT 1)",
        "SET @company_beauty := (SELECT id FROM mj_companies WHERE slug='chennai-beauty-clinic-demo' ORDER BY id LIMIT 1)",
        "SET @listing_data := (SELECT id FROM mj_listings WHERE slug IN ('hard-disk-data-recovery-service-singapore-demo','hard-disk-data-recovery-service-singapore') ORDER BY FIELD(slug,'hard-disk-data-recovery-service-singapore-demo','hard-disk-data-recovery-service-singapore'), id LIMIT 1)",
        "SET @listing_laptop := (SELECT id FROM mj_listings WHERE slug IN ('used-lenovo-thinkpad-business-laptop-demo','used-business-laptop-thinkpad-singapore') ORDER BY FIELD(slug,'used-lenovo-thinkpad-business-laptop-demo','used-business-laptop-thinkpad-singapore'), id LIMIT 1)",
        "SET @listing_clean := (SELECT id FROM mj_listings WHERE slug IN ('move-in-move-out-cleaning-kuala-lumpur-demo','move-in-move-out-cleaning-kuala-lumpur') ORDER BY FIELD(slug,'move-in-move-out-cleaning-kuala-lumpur-demo','move-in-move-out-cleaning-kuala-lumpur'), id LIMIT 1)",
        "SET @listing_beauty := (SELECT id FROM mj_listings WHERE slug IN ('skin-care-consultation-chennai-demo','skin-care-consultation-chennai') ORDER BY FIELD(slug,'skin-care-consultation-chennai-demo','skin-care-consultation-chennai'), id LIMIT 1)",
        "SET @deal_data := (SELECT id FROM mj_deals WHERE slug IN ('free-diagnosis-data-recovery-singapore-demo','free-diagnosis-data-recovery-singapore') ORDER BY FIELD(slug,'free-diagnosis-data-recovery-singapore-demo','free-diagnosis-data-recovery-singapore'), id LIMIT 1)",
        "SET @deal_clean := (SELECT id FROM mj_deals WHERE slug IN ('four-hours-home-cleaning-promo-kl-demo','4-hours-home-cleaning-promo-kl') ORDER BY FIELD(slug,'four-hours-home-cleaning-promo-kl-demo','4-hours-home-cleaning-promo-kl'), id LIMIT 1)",
        "SET @deal_beauty := (SELECT id FROM mj_deals WHERE slug IN ('skin-consultation-intro-offer-chennai-demo','aesthetic-consultation-demo-chennai') ORDER BY FIELD(slug,'skin-consultation-intro-offer-chennai-demo','aesthetic-consultation-demo-chennai'), id LIMIT 1)",
        "SET @lead_john := (SELECT id FROM seller_leads WHERE seller_user_id=@seller_sg AND customer_email='john.tan.demo@example.com' ORDER BY id LIMIT 1)",
        "SET @lead_ravi := (SELECT id FROM seller_leads WHERE seller_user_id=@seller_sg AND customer_email='ravi.demo@example.com' ORDER BY id LIMIT 1)",
        "SET @lead_nur := (SELECT id FROM seller_leads WHERE seller_user_id=@seller_sg AND customer_email='nur.demo@example.com' ORDER BY id LIMIT 1)",
        "SET @lead_lim := (SELECT id FROM seller_leads WHERE seller_user_id=@seller_my AND customer_email='lim.demo@example.com' ORDER BY id LIMIT 1)",
        "SET @chat_data := (SELECT id FROM mj_chats WHERE listing_id=@listing_data AND buyer_id=@buyer_demo AND seller_id=@seller_sg ORDER BY id LIMIT 1)"
    );
    foreach ($sets as $sql) {
        try { mj_exec_sql($pdo, $sql); } catch (Exception $ignore) {}
    }
}
function mj_final_checks($pdo) {
    $needed = ['mj_users','mj_admin_users','mj_categories','mj_listings','mj_companies','mj_deals','seller_leads','mj_seller_leads','mj_notifications','mj_settings','mj_schema_migrations'];
    $out = [];
    foreach ($needed as $t) {
        try {
            $cnt = (int)mj_fetch_column_sql($pdo, 'SELECT COUNT(*) FROM ' . mj_backtick($t));
            $out[] = ['table'=>$t, 'ok'=>true, 'count'=>$cnt];
        } catch (Exception $e) {
            $out[] = ['table'=>$t, 'ok'=>false, 'count'=>null, 'error'=>$e->getMessage()];
        }
    }
    return $out;
}
function mj_write_config($state) {
    global $root, $configFile;
    $includesDir = $root . '/includes';
    if (!is_dir($includesDir)) { @mkdir($includesDir, 0775, true); }
    if (!is_writable($includesDir)) { @chmod($includesDir, 0775); }
    $cfgArray = [
        'db_host'=>$state['db_host'],
        'db_name'=>$state['db_name'],
        'db_user'=>$state['db_user'],
        'db_pass'=>$state['db_pass'],
        'base_url'=>$state['base_url'],
        'timezone'=>'Asia/Singapore',
        'installed'=>true,
        'installed_at'=>date('Y-m-d H:i:s'),
        'installed_version'=>'3.0.4.2.16-install-sample-fk-safe',
    ];
    $cfg = "<?php\n// Auto-generated by MJ Marketplace advanced installer.\nreturn " . var_export($cfgArray, true) . ";\n";
    $bytes = @file_put_contents($configFile, $cfg, LOCK_EX);
    if ($bytes === false || $bytes < 20) {
        @file_put_contents(__DIR__.'/generated-config.php', $cfg, LOCK_EX);
        throw new Exception('Database installed, but installer could not write includes/config.php. Create it manually using install/generated-config.php content, or set /includes permission to 755/775 and finalize again.');
    }
    @chmod($configFile, 0644);
}
function mj_create_admin_and_settings($pdo, $state) {
    $hash = password_hash($state['admin_pass'], PASSWORD_DEFAULT);
    $stmt = $pdo->prepare('INSERT INTO mj_admin_users(name,email,password_hash,role,is_active,created_at) VALUES(?,?,?,?,1,NOW()) ON DUPLICATE KEY UPDATE name=VALUES(name), password_hash=VALUES(password_hash), role=VALUES(role), is_active=1');
    $stmt->execute([$state['admin_name'], $state['admin_email'], $hash, 'super_admin']);
    $settings = [
        ['site_installed','1'],
        ['base_url',$state['base_url']],
        ['installed_version','3.0.4.2.16-install-sample-fk-safe'],
        ['payment_scope_lock','customer_to_seller_disabled'],
    ];
    foreach ($settings as $s) {
        $pdo->prepare('INSERT INTO mj_settings(setting_key, setting_value, updated_at) VALUES(?,?,NOW()) ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value), updated_at=NOW()')->execute($s);
    }
}
function mj_requirements() {
    global $root, $configFile, $stateDir;
    $checks = [];
    $checks[] = ['name'=>'PHP version 8.0+', 'ok'=>version_compare(PHP_VERSION, '8.0.0', '>='), 'value'=>PHP_VERSION];
    foreach (['pdo','pdo_mysql','json','mbstring','openssl','fileinfo','curl'] as $ext) {
        $checks[] = ['name'=>'PHP extension: '.$ext, 'ok'=>extension_loaded($ext), 'value'=>extension_loaded($ext) ? 'Loaded' : 'Missing'];
    }
    $dirs = [
        'includes' => $root . '/includes',
        'uploads' => $root . '/uploads',
        'install' => __DIR__,
        'install/runtime' => $stateDir,
    ];
    foreach ($dirs as $name=>$path) {
        if (!is_dir($path)) { @mkdir($path, 0775, true); }
        $checks[] = ['name'=>'Writable folder: '.$name, 'ok'=>is_writable($path), 'value'=>$path];
    }
    $checks[] = ['name'=>'Config file', 'ok'=>!is_file($configFile) || is_writable($configFile), 'value'=>is_file($configFile) ? 'Exists' : 'Will create'];
    return $checks;
}
function mj_statement_should_ignore($message, $state) {
    if (empty($state['safe_continue'])) { return false; }
    $m = strtolower($message);
    $safe = [
        'duplicate column name',
        'already exists',
        'duplicate key name',
        'duplicate entry',
        'check that column/key exists',
        'can\'t drop',
    ];
    foreach ($safe as $needle) { if (strpos($m, strtolower($needle)) !== false) { return true; } }
    return false;
}

$action = (isset($_POST['action']) ? $_POST['action'] : (isset($_GET['action']) ? $_GET['action'] : ''));
try {
    if ($action === 'requirements') {
        mj_json(['ok'=>true, 'checks'=>mj_requirements(), 'log'=>mj_read_log()]);
    }

    if ($action === 'download_log') {
        header('Content-Type: text/plain; charset=utf-8');
        header('Content-Disposition: attachment; filename="mj-marketplace-install-log.txt"');
        echo is_file($logFile) ? file_get_contents($logFile) : 'No install log yet.';
        exit;
    }

    if ($action === 'reset_state') {
        mj_state_clear();
        mj_json(['ok'=>true, 'message'=>'Installer progress cleared.']);
    }

    if ($action === 'start') {
        mj_state_clear();
        $dbName = preg_replace('/[^a-zA-Z0-9_]/', '', trim((string)mj_post('db_name', 'mj_marketplace')));
        $base = rtrim(trim((string)mj_post('base_url', '')), '/');
        if ($base === '') { $base = mj_detect_base_url(); }
        $installId = mj_random_install_id();
        $_SESSION['mj_install_id'] = $installId;
        mj_ensure_state_dir();
        $state = [
            'install_id'=>$installId,
            'started_at'=>date('Y-m-d H:i:s'),
            'db_host'=>trim((string)mj_post('db_host', 'localhost')),
            'db_name'=>$dbName,
            'db_user'=>trim((string)mj_post('db_user', '')),
            'db_pass'=>(string)mj_post('db_pass', ''),
            'base_url'=>$base,
            'admin_name'=>trim((string)mj_post('admin_name', 'Admin')),
            'admin_email'=>trim((string)mj_post('admin_email', '')),
            'admin_pass'=>(string)mj_post('admin_pass', ''),
            'install_sample'=>!empty($_POST['sample_data']),
            'mode'=>(string)mj_post('mode', 'fresh'),
            'safe_continue'=>!empty($_POST['safe_continue']),
            'batch_size'=>max(5, min(100, (int)mj_post('batch_size', 10))),
            'phase'=>'core',
            'statement_index'=>0,
            'sample_statement_index'=>0,
            'done'=>false,
            'current_table'=>'starting',
            'completed_statements'=>0,
            'skipped_statements'=>0,
            'errors'=>[],
        ];
        if (!$dbName) { throw new Exception('Database name is required.'); }
        if ($state['db_user'] === '') { throw new Exception('Database user is required.'); }
        if (!$state['admin_email'] || !filter_var($state['admin_email'], FILTER_VALIDATE_EMAIL)) { throw new Exception('Valid admin email is required.'); }
        if (strlen($state['admin_pass']) < 8) { throw new Exception('Admin password must be at least 8 characters.'); }
        $pdo = mj_test_connection($state);
        mj_exec_sql($pdo, 'CREATE DATABASE IF NOT EXISTS ' . mj_backtick($dbName) . ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
        mj_exec_sql($pdo, 'USE ' . mj_backtick($dbName));
        if ($state['mode'] === 'reset') {
            $dropped = mj_drop_all_tables($pdo, $dbName);
            mj_log('Reset mode: dropped ' . $dropped . ' existing tables/views.');
        } elseif ($state['mode'] === 'fresh') {
            $exists = (int)mj_fetch_column_sql($pdo, 'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE()');
            if ($exists > 0) {
                throw new Exception('Database is not empty. Choose Repair existing install or Reset database and reinstall.');
            }
        }
        $sqlFile = __DIR__ . '/fresh_install_v3.0.4.2.4_all_in_one.sql';
        $sampleFile = __DIR__ . '/sample_data.sql';
        $coreStatements = mj_parse_sql_file($sqlFile);
        $sampleStatements = is_file($sampleFile) ? mj_parse_sql_file($sampleFile) : [];
        $state['core_total'] = count($coreStatements);
        $state['sample_total'] = count($sampleStatements);
        $state['sql_file'] = basename($sqlFile);
        $state['sample_file'] = basename($sampleFile);
        mj_state_write($state);
        mj_log('Started ' . $state['mode'] . ' install for DB ' . $dbName . '. Core statements: ' . $state['core_total'] . '. Sample enabled: ' . ($state['install_sample'] ? 'yes' : 'no') . '. Batch size: ' . $state['batch_size']);
        mj_json(['ok'=>true, 'message'=>'Install started.', 'install_id'=>$installId, 'state'=>$state, 'log'=>mj_read_log()]);
    }

    if ($action === 'status') {
        $state = mj_state_read();
        mj_json(['ok'=>true, 'state'=>$state, 'log'=>mj_read_log()]);
    }

    if ($action === 'batch') {
        $state = mj_require_state();
        if (!empty($state['done'])) { mj_json(['ok'=>true, 'done'=>true, 'state'=>$state, 'log'=>mj_read_log()]); }
        $pdo = mj_pdo_from_state($state, false);
        mj_exec_sql($pdo, 'CREATE DATABASE IF NOT EXISTS ' . mj_backtick($state['db_name']) . ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
        mj_exec_sql($pdo, 'USE ' . mj_backtick($state['db_name']));
        $file = ($state['phase'] === 'sample') ? (__DIR__ . '/sample_data.sql') : (__DIR__ . '/fresh_install_v3.0.4.2.4_all_in_one.sql');
        $statements = mj_parse_sql_file($file);
        $indexKey = ($state['phase'] === 'sample') ? 'sample_statement_index' : 'statement_index';
        $totalKey = ($state['phase'] === 'sample') ? 'sample_total' : 'core_total';
        $index = (int)((isset($state[$indexKey]) ? $state[$indexKey] : 0));
        $batchSize = (int)((isset($state['batch_size']) ? $state['batch_size'] : 50));
        $startTime = microtime(true);
        $executed = 0; $skipped = 0;
        if ($state['phase'] === 'sample') {
            mj_sample_data_rehydrate_vars($pdo);
        }
        while ($index < count($statements) && $executed < $batchSize && (microtime(true) - $startTime) < 3.0) {
            $groupEnd = mj_dynamic_group_end_index($statements, $index);
            $item = $statements[$index];
            $groupCount = max(1, $groupEnd - $index + 1);
            $state['current_table'] = $item['table'];
            $state['current_line'] = $item['line'];
            $state['last_sql_preview'] = mj_preview_sql($item['sql']);
            $groupOk = true;

            for ($gi = $index; $gi <= $groupEnd; $gi++) {
                $gitem = $statements[$gi];
                $sql = trim((string)$gitem['sql']);
                if ($sql === '') { continue; }
                $state['current_table'] = $gitem['table'];
                $state['current_line'] = $gitem['line'];
                $state['last_sql_preview'] = mj_preview_sql($sql);
                try {
                    if ($state['phase'] === 'sample' && strpos($sql, '@') !== false) {
                        mj_sample_data_rehydrate_vars($pdo);
                    }
                    if (mj_prepare_from_variable($sql) !== '') {
                        // If an older failed install resumes exactly at PREPARE, rebuild the variable on this DB connection.
                        mj_dynamic_prepare_prelude($pdo, $statements, $gi);
                    }
                    mj_exec_sql($pdo, $sql);
                } catch (Exception $e) {
                    $msg = 'SQL error in ' . basename($file) . ' near line ' . $gitem['line'] . ' table ' . $gitem['table'] . ': ' . $e->getMessage() . ' | SQL: ' . mj_preview_sql($sql);
                    if (mj_statement_should_ignore($e->getMessage(), $state)) {
                        $skipped++;
                        $state['skipped_statements'] = (int)((isset($state['skipped_statements']) ? $state['skipped_statements'] : 0)) + 1;
                        mj_log('Skipped safe schema warning: ' . $msg);
                        if (preg_match('/^EXECUTE\s+/i', $sql) || preg_match('/^PREPARE\s+/i', $sql)) {
                            try { mj_exec_sql($pdo, 'DEALLOCATE PREPARE stmt'); } catch (Exception $ignore) {}
                            try { mj_exec_sql($pdo, 'DEALLOCATE PREPARE mj_stmt'); } catch (Exception $ignore) {}
                            break;
                        }
                    } else {
                        $state['errors'][] = $msg;
                        mj_state_write($state);
                        mj_fail($msg, ['state'=>$state, 'log'=>mj_read_log()]);
                    }
                }
            }

            $executed += $groupCount;
            $state['completed_statements'] = (int)((isset($state['completed_statements']) ? $state['completed_statements'] : 0)) + $groupCount;
            if ($executed === $groupCount || $executed % 20 === 0) {
                $groupNote = ($groupCount > 1) ? (' dynamic group x' . $groupCount) : '';
                mj_log('Processing ' . $state['phase'] . ': ' . $item['table'] . ' line ' . $item['line'] . ' statement #' . ($index + 1) . '/' . count($statements) . $groupNote);
            }
            $index = $groupEnd + 1;
            $state[$indexKey] = $index;
        }

        if ($index >= count($statements)) {
            if ($state['phase'] === 'core' && !empty($state['install_sample'])) {
                $state['phase'] = 'sample';
                $state['sample_statement_index'] = 0;
                $state['current_table'] = 'sample data';
                mj_log('Core database completed. Starting sample data batch import.');
            } else {
                $state['phase'] = 'finalize';
                $state['current_table'] = 'final checks';
                mj_log('SQL import completed. Starting finalization.');
            }
        }
        mj_state_write($state);
        $total = (int)((isset($state[$totalKey]) ? $state[$totalKey] : count($statements)));
        mj_json(['ok'=>true, 'done'=>false, 'phase'=>$state['phase'], 'executed'=>$executed, 'skipped'=>$skipped, 'index'=>$index, 'total'=>$total, 'state'=>$state, 'log'=>mj_read_log()]);
    }

    if ($action === 'finalize') {
        $state = mj_require_state();
        $pdo = mj_pdo_from_state($state, true);
        mj_create_admin_and_settings($pdo, $state);
        mj_write_config($state);
        $checks = mj_final_checks($pdo);
        @file_put_contents($lockFile, 'Installed at '.date('Y-m-d H:i:s').' base_url='.$state['base_url'].' version=3.0.4.2.16'.PHP_EOL, LOCK_EX);
        $state['done'] = true;
        $state['phase'] = 'done';
        $state['current_table'] = 'complete';
        $state['completed_at'] = date('Y-m-d H:i:s');
        $state['final_checks'] = $checks;
        // Do not keep DB/admin passwords in persistent state after success.
        $state['db_pass'] = '';
        $state['admin_pass'] = '';
        mj_state_write($state);
        mj_log('Installation complete. Config created and installer locked.');
        mj_json(['ok'=>true, 'done'=>true, 'state'=>$state, 'checks'=>$checks, 'log'=>mj_read_log()]);
    }

    if ($action === 'table_counts') {
        $state = mj_require_state();
        $pdo = mj_pdo_from_state($state, true);
        mj_json(['ok'=>true, 'checks'=>mj_final_checks($pdo), 'log'=>mj_read_log()]);
    }

    mj_fail('Unknown installer action.');
} catch (Exception $e) {
    mj_fail($e->getMessage());
}
