<?php
/**
 * Read-only ERP Analyzer
 * Safe: no DB connection, no DROP/DELETE/ALTER, no command execution.
 * It only reads ZIP + SQL and creates a JSON report.
 */

ini_set('display_errors', '0');
error_reporting(E_ALL);
date_default_timezone_set('Asia/Dhaka');

const SECRET_KEY = 'jbderp_review_6f29c8a31b_20260613';
const ZIP_FILE = 'api-13-06-2026.zip';
const SQL_FILE = 'jewellersbd_13_06_2026_erp-5.sql';
const REPORT_FILE = 'erp_analysis_report_20260613.json';
const MAX_PHP_SCAN_BYTES = 1048576;
const MAX_ISSUES = 1000;

if (!function_exists('hash_equals')) {
    function hash_equals($known_string, $user_string) {
        if (!is_string($known_string) || !is_string($user_string)) return false;
        if (strlen($known_string) !== strlen($user_string)) return false;
        $res = 0;
        for ($i = 0; $i < strlen($known_string); $i++) {
            $res |= ord($known_string[$i]) ^ ord($user_string[$i]);
        }
        return $res === 0;
    }
}

$key = isset($_GET['key']) ? (string)$_GET['key'] : '';
if (!hash_equals(SECRET_KEY, $key)) {
    http_response_code(403);
    header('Content-Type: text/plain; charset=UTF-8');
    echo "Forbidden. Wrong key.";
    exit;
}

write_protection_htaccess();

$report = build_report();
$json = to_json($report);
@file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . REPORT_FILE, $json, LOCK_EX);

if (isset($_GET['download'])) {
    header('Content-Type: application/json; charset=UTF-8');
    header('Content-Disposition: attachment; filename="' . REPORT_FILE . '"');
    echo $json;
    exit;
}

render_html($report);

function write_protection_htaccess() {
    $path = __DIR__ . DIRECTORY_SEPARATOR . '.htaccess';
    if (file_exists($path)) return;

    $content = "# ERP Analyzer temporary protection\n";
    $content .= "Options -Indexes\n";
    $content .= "<FilesMatch \"\\.(zip|sql|json|bak|log)$\">\n";
    $content .= "  <IfModule mod_authz_core.c>\n";
    $content .= "    Require all denied\n";
    $content .= "  </IfModule>\n";
    $content .= "  <IfModule !mod_authz_core.c>\n";
    $content .= "    Deny from all\n";
    $content .= "  </IfModule>\n";
    $content .= "</FilesMatch>\n";

    @file_put_contents($path, $content, LOCK_EX);
}

function build_report() {
    $zipPath = __DIR__ . DIRECTORY_SEPARATOR . ZIP_FILE;
    $sqlPath = __DIR__ . DIRECTORY_SEPARATOR . SQL_FILE;

    $zipReport = analyze_zip($zipPath);
    $dirReport = null;

    if (isset($_GET['scan_dir']) && (string)$_GET['scan_dir'] === '1') {
        $dirReport = analyze_directory(__DIR__);
    }

    $sqlReport = analyze_sql($sqlPath);

    $report = array(
        'generated_at' => date('c'),
        'tool' => 'read_only_erp_analyzer_v1',
        'server' => array(
            'php_version' => PHP_VERSION,
            'timezone' => date_default_timezone_get()
        ),
        'source_files' => array(
            'zip' => array(
                'expected_name' => ZIP_FILE,
                'exists' => is_file($zipPath),
                'size_bytes' => is_file($zipPath) ? (int)filesize($zipPath) : 0
            ),
            'sql' => array(
                'expected_name' => SQL_FILE,
                'exists' => is_file($sqlPath),
                'size_bytes' => is_file($sqlPath) ? (int)filesize($sqlPath) : 0
            )
        ),
        'api_source_zip' => $zipReport,
        'api_source_directory_scan' => $dirReport,
        'database_sql' => $sqlReport
    );

    $report['recommendations'] = generate_recommendations($zipReport, $sqlReport, $dirReport);
    $report['module_update_sequence'] = generate_module_sequence($sqlReport);

    return $report;
}

function analyze_zip($zipPath) {
    $result = init_file_report('zip', basename($zipPath));
    $result['exists'] = is_file($zipPath);

    if (!$result['exists']) {
        $result['errors'][] = 'ZIP file not found in this folder.';
        return $result;
    }

    $result['archive_size_bytes'] = (int)filesize($zipPath);

    if (!class_exists('ZipArchive')) {
        $result['errors'][] = 'PHP ZipArchive extension is not enabled. Extract ZIP in this folder, then run URL with &scan_dir=1';
        return $result;
    }

    $zip = new ZipArchive();
    $open = $zip->open($zipPath);

    if ($open !== true) {
        $result['errors'][] = 'Could not open ZIP archive.';
        return $result;
    }

    $result['zip_entries'] = $zip->numFiles;

    for ($i = 0; $i < $zip->numFiles; $i++) {
        $stat = $zip->statIndex($i);
        if (!$stat || !isset($stat['name'])) continue;

        $name = str_replace('\\', '/', $stat['name']);
        if (substr($name, -1) === '/') continue;

        $size = isset($stat['size']) ? (int)$stat['size'] : 0;

        record_file($result, $name, $size, function() use ($zip, $i) {
            return $zip->getFromIndex($i);
        });
    }

    $zip->close();
    finalize_file_report($result);
    return $result;
}

function analyze_directory($dir) {
    $result = init_file_report('directory', basename($dir));
    $result['exists'] = is_dir($dir);

    if (!$result['exists']) {
        $result['errors'][] = 'Directory not found.';
        return $result;
    }

    try {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
        );

        foreach ($iterator as $file) {
            if (!$file->isFile()) continue;

            $path = $file->getPathname();
            $rel = str_replace('\\', '/', substr($path, strlen($dir) + 1));

            if (should_skip_scan_file($rel)) continue;

            $size = (int)$file->getSize();

            record_file($result, $rel, $size, function() use ($path, $size) {
                if ($size > MAX_PHP_SCAN_BYTES) return '';
                return @file_get_contents($path);
            });
        }
    } catch (Exception $e) {
        $result['errors'][] = 'Directory scan error: ' . $e->getMessage();
    }

    finalize_file_report($result);
    return $result;
}

function should_skip_scan_file($rel) {
    $rel = str_replace('\\', '/', $rel);
    $base = basename($rel);

    $skipBases = array(
        basename(__FILE__),
        REPORT_FILE,
        ZIP_FILE,
        SQL_FILE,
        '.htaccess'
    );

    if (in_array($base, $skipBases, true)) return true;
    if (preg_match('#(^|/)(\.git|node_modules)(/|$)#i', $rel)) return true;

    return false;
}

function init_file_report($mode, $source) {
    return array(
        'mode' => $mode,
        'source' => $source,
        'exists' => true,
        'total_files' => 0,
        'total_size_bytes' => 0,
        'php_files' => 0,
        'extensions' => array(),
        'top_directories' => array(),
        'api_endpoint_candidates' => array(),
        'issues' => array(),
        'issue_overflow_count' => 0,
        'issues_by_severity' => array(),
        'errors' => array(),
        'notes' => array()
    );
}

function record_file(&$result, $name, $size, $contentProvider = null) {
    $name = str_replace('\\', '/', $name);
    if ($name === '' || substr($name, -1) === '/') return;

    $result['total_files']++;
    $result['total_size_bytes'] += max(0, (int)$size);

    $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    if ($ext === '') $ext = '[no_ext]';

    inc_count($result['extensions'], $ext);
    inc_count($result['top_directories'], top_dir($name));

    if ($ext === 'php') {
        $result['php_files']++;

        if (looks_like_api_file($name)) {
            $result['api_endpoint_candidates'][] = $name;
        }

        if (is_callable($contentProvider) && $size <= MAX_PHP_SCAN_BYTES) {
            $content = call_user_func($contentProvider);
            if (is_string($content) && $content !== '') {
                scan_php_content($result, $name, $content);
            }
        } elseif ($size > MAX_PHP_SCAN_BYTES) {
            $result['notes'][] = 'Skipped large PHP scan: ' . $name;
        }
    }
}

function scan_php_content(&$result, $file, $content) {
    $patterns = array(
        array(
            'severity' => 'critical',
            'code' => 'command_execution_function',
            'regex' => '/\b(shell_exec|exec|system|passthru|proc_open|popen)\s*\(/i',
            'message' => 'Command execution function found. Audit this file carefully.'
        ),
        array(
            'severity' => 'critical',
            'code' => 'eval_function',
            'regex' => '/\beval\s*\(/i',
            'message' => 'eval() found. High backdoor/malware risk unless strongly justified.'
        ),
        array(
            'severity' => 'high',
            'code' => 'obfuscation_decode_function',
            'regex' => '/\b(base64_decode|gzinflate|str_rot13)\s*\(/i',
            'message' => 'Decode/obfuscation function found. Check whether it is legitimate.'
        ),
        array(
            'severity' => 'high',
            'code' => 'sql_query_near_variable',
            'regex' => '/\b(SELECT|INSERT|UPDATE|DELETE)\b[\s\S]{0,500}(\$_(GET|POST|REQUEST|COOKIE)|\.\s*\$[A-Za-z_][A-Za-z0-9_]*)/i',
            'message' => 'SQL query appears near request/variable concatenation. Check for SQL injection; prefer prepared statements.'
        ),
        array(
            'severity' => 'medium',
            'code' => 'raw_request_superglobal',
            'regex' => '/\$_(GET|POST|REQUEST|COOKIE)\b/',
            'message' => 'Raw request data used. Ensure validation and sanitization.'
        ),
        array(
            'severity' => 'medium',
            'code' => 'wildcard_cors',
            'regex' => '/Access-Control-Allow-Origin\s*:\s*\*/i',
            'message' => 'Wildcard CORS found. Restrict allowed origins for ERP API.'
        ),
        array(
            'severity' => 'medium',
            'code' => 'display_errors_enabled',
            'regex' => '/ini_set\s*\(\s*[\'"]display_errors[\'"]\s*,\s*[\'"]?(1|on|true)[\'"]?\s*\)/i',
            'message' => 'display_errors appears enabled. Turn off on live server.'
        ),
        array(
            'severity' => 'medium',
            'code' => 'weak_hash_function',
            'regex' => '/\b(md5|sha1)\s*\(/i',
            'message' => 'md5/sha1 found. If used for password/token, replace with password_hash/password_verify.'
        ),
        array(
            'severity' => 'medium',
            'code' => 'hardcoded_secret_like_value',
            'regex' => '/\b(password|passwd|db_pass|db_password|secret|api_key|token)\b\s*[:=]\s*[\'"][^\'"]{4,}[\'"]/i',
            'message' => 'Hardcoded secret-like value found. Move secrets to protected config outside public access.'
        ),
        array(
            'severity' => 'low',
            'code' => 'file_upload_handler',
            'regex' => '/\bmove_uploaded_file\s*\(/i',
            'message' => 'File upload handler found. Verify extension/MIME/size checks and store uploads safely.'
        )
    );

    foreach ($patterns as $p) {
        $m = array();
        $ok = preg_match($p['regex'], $content, $m, PREG_OFFSET_CAPTURE);
        if ($ok === 1) {
            $line = line_number_from_offset($content, $m[0][1]);
            add_issue($result, $p['severity'], $p['code'], $file, $line, $p['message']);
        }
    }

    if (preg_match('/\bsession_start\s*\(/i', $content) && !preg_match('/\bsession_regenerate_id\s*\(/i', $content)) {
        add_issue(
            $result,
            'low',
            'session_without_regenerate_hint',
            $file,
            0,
            'session_start found but session_regenerate_id not found in same file. Regenerate session ID after login.'
        );
    }
}

function add_issue(&$result, $severity, $code, $file, $line, $message) {
    if (count($result['issues']) >= MAX_ISSUES) {
        $result['issue_overflow_count']++;
        return;
    }

    $result['issues'][] = array(
        'severity' => $severity,
        'code' => $code,
        'file' => $file,
        'line' => (int)$line,
        'message' => $message
    );
}

function finalize_file_report(&$result) {
    arsort($result['extensions']);
    arsort($result['top_directories']);

    $result['api_endpoint_candidates'] = array_values(array_unique($result['api_endpoint_candidates']));
    sort($result['api_endpoint_candidates']);

    $result['issues_by_severity'] = severity_counts($result['issues']);
}

function analyze_sql($sqlPath) {
    $result = array(
        'exists' => is_file($sqlPath),
        'file' => basename($sqlPath),
        'size_bytes' => is_file($sqlPath) ? (int)filesize($sqlPath) : 0,
        'destructive_statement_counts' => array(),
        'table_count' => 0,
        'tables' => array(),
        'tables_by_module' => array(),
        'tables_without_primary_key' => array(),
        'tables_without_foreign_key' => array(),
        'audit_column_gaps' => array(),
        'engines' => array(),
        'insert_statement_counts' => array(),
        'errors' => array(),
        'notes' => array()
    );

    if (!$result['exists']) {
        $result['errors'][] = 'SQL file not found in this folder.';
        return $result;
    }

    $sql = @file_get_contents($sqlPath);
    if ($sql === false) {
        $result['errors'][] = 'Could not read SQL file.';
        return $result;
    }

    $result['destructive_statement_counts'] = array(
        'drop_database' => count_regex('/\bDROP\s+DATABASE\b/i', $sql),
        'drop_table' => count_regex('/\bDROP\s+TABLE\b/i', $sql),
        'truncate_table' => count_regex('/\bTRUNCATE\s+TABLE\b/i', $sql),
        'delete_from' => count_regex('/\bDELETE\s+FROM\b/i', $sql),
        'alter_table' => count_regex('/\bALTER\s+TABLE\b/i', $sql),
        'foreign_key_checks_off' => count_regex('/FOREIGN_KEY_CHECKS\s*=\s*0/i', $sql)
    );

    $tables = parse_create_tables($sql);
    $result['tables'] = $tables;
    $result['table_count'] = count($tables);

    foreach ($tables as $t) {
        $module = $t['module'];
        if (!isset($result['tables_by_module'][$module])) {
            $result['tables_by_module'][$module] = array();
        }
        $result['tables_by_module'][$module][] = $t['name'];

        if ($t['engine'] !== '') {
            inc_count($result['engines'], $t['engine']);
        }

        if (empty($t['primary_key'])) {
            $result['tables_without_primary_key'][] = $t['name'];
        }

        if (empty($t['foreign_keys'])) {
            $result['tables_without_foreign_key'][] = $t['name'];
        }

        $hasCreated = table_has_any_column($t, array('created_at', 'created_on', 'date_added', 'created_date'));
        $hasUpdated = table_has_any_column($t, array('updated_at', 'updated_on', 'modified_at', 'modified_date'));

        if (!$hasCreated || !$hasUpdated) {
            $result['audit_column_gaps'][] = array(
                'table' => $t['name'],
                'missing_created_column' => !$hasCreated,
                'missing_updated_column' => !$hasUpdated
            );
        }
    }

    foreach ($result['tables_by_module'] as $k => $v) {
        sort($result['tables_by_module'][$k]);
    }

    arsort($result['engines']);

    $result['insert_statement_counts'] = parse_insert_counts($sql);

    return $result;
}

function parse_create_tables($sql) {
    $tables = array();
    $pattern = '/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?:`[^`]+`|[A-Za-z0-9_]+)\.)?`?([A-Za-z0-9_]+)`?\s*\(/i';

    $matches = array();
    if (!preg_match_all($pattern, $sql, $matches, PREG_OFFSET_CAPTURE)) {
        return $tables;
    }

    for ($i = 0; $i < count($matches[0]); $i++) {
        $full = $matches[0][$i][0];
        $start = $matches[0][$i][1];
        $tableName = $matches[1][$i][0];
        $openPos = $start + strlen($full) - 1;

        $closePos = find_matching_parenthesis($sql, $openPos);
        if ($closePos === false) continue;

        $body = substr($sql, $openPos + 1, $closePos - $openPos - 1);
        $semi = strpos($sql, ';', $closePos);
        $suffix = ($semi === false)
            ? substr($sql, $closePos + 1, 1000)
            : substr($sql, $closePos + 1, $semi - $closePos - 1);

        $tables[] = parse_table_definition($tableName, $body, $suffix);
    }

    return $tables;
}

function parse_table_definition($tableName, $body, $suffix) {
    $info = array(
        'name' => $tableName,
        'module' => guess_module($tableName),
        'engine' => '',
        'charset' => '',
        'collation' => '',
        'columns' => array(),
        'column_count' => 0,
        'primary_key' => array(),
        'indexes_count' => 0,
        'unique_indexes_count' => 0,
        'foreign_keys' => array()
    );

    $m = array();

    if (preg_match('/ENGINE\s*=\s*([A-Za-z0-9_]+)/i', $suffix, $m)) {
        $info['engine'] = $m[1];
    }

    if (preg_match('/CHARSET\s*=\s*([A-Za-z0-9_]+)/i', $suffix, $m)) {
        $info['charset'] = $m[1];
    }

    if (preg_match('/COLLATE\s*=\s*([A-Za-z0-9_]+)/i', $suffix, $m)) {
        $info['collation'] = $m[1];
    }

    $defs = split_sql_definitions($body);

    foreach ($defs as $def) {
        $d = trim($def);
        if ($d === '') continue;

        if (preg_match('/FOREIGN\s+KEY\s*\((.*?)\)\s+REFERENCES\s+(?:(?:`[^`]+`|[A-Za-z0-9_]+)\.)?`?([A-Za-z0-9_]+)`?\s*\((.*?)\)/is', $d, $m)) {
            $info['foreign_keys'][] = array(
                'columns' => extract_names_from_list($m[1]),
                'references_table' => $m[2],
                'references_columns' => extract_names_from_list($m[3])
            );
            continue;
        }

        if (preg_match('/^PRIMARY\s+KEY\s*\((.*?)\)/is', $d, $m)) {
            $info['primary_key'] = extract_names_from_list($m[1]);
            continue;
        }

        if (preg_match('/^(UNIQUE\s+)?KEY\b/is', $d, $m)) {
            $info['indexes_count']++;
            if (isset($m[1]) && trim($m[1]) !== '') {
                $info['unique_indexes_count']++;
            }
            continue;
        }

        if (preg_match('/^`([^`]+)`\s+(.+)$/s', $d, $m)) {
            $colName = $m[1];
            $rest = trim($m[2]);

            $col = array(
                'name' => $colName,
                'type' => parse_column_type($rest),
                'nullable' => stripos($rest, 'NOT NULL') === false,
                'auto_increment' => stripos($rest, 'AUTO_INCREMENT') !== false,
                'has_default' => preg_match('/\bDEFAULT\b/i', $rest) === 1
            );

            $info['columns'][] = $col;

            if (preg_match('/\bPRIMARY\s+KEY\b/i', $rest)) {
                $info['primary_key'][] = $colName;
            }
        }
    }

    $info['primary_key'] = array_values(array_unique($info['primary_key']));
    $info['column_count'] = count($info['columns']);

    return $info;
}

function split_sql_definitions($body) {
    $defs = array();
    $buf = '';
    $depth = 0;
    $quote = null;
    $len = strlen($body);

    for ($i = 0; $i < $len; $i++) {
        $ch = $body[$i];

        if ($quote !== null) {
            $buf .= $ch;
            if ($ch === $quote) {
                if ($quote === '`' || $i === 0 || $body[$i - 1] !== '\\') {
                    $quote = null;
                }
            }
            continue;
        }

        if ($ch === "'" || $ch === '"' || $ch === '`') {
            $quote = $ch;
            $buf .= $ch;
            continue;
        }

        if ($ch === '(') $depth++;
        if ($ch === ')' && $depth > 0) $depth--;

        if ($ch === ',' && $depth === 0) {
            if (trim($buf) !== '') $defs[] = trim($buf);
            $buf = '';
            continue;
        }

        $buf .= $ch;
    }

    if (trim($buf) !== '') $defs[] = trim($buf);

    return $defs;
}

function find_matching_parenthesis($sql, $openPos) {
    $depth = 0;
    $quote = null;
    $len = strlen($sql);

    for ($i = $openPos; $i < $len; $i++) {
        $ch = $sql[$i];

        if ($quote !== null) {
            if ($ch === $quote) {
                if ($quote === '`' || $i === 0 || $sql[$i - 1] !== '\\') {
                    $quote = null;
                }
            }
            continue;
        }

        if ($ch === "'" || $ch === '"' || $ch === '`') {
            $quote = $ch;
            continue;
        }

        if ($ch === '(') {
            $depth++;
        } elseif ($ch === ')') {
            $depth--;
            if ($depth === 0) return $i;
        }
    }

    return false;
}

function parse_column_type($rest) {
    $markers = array(
        ' NOT NULL',
        ' NULL',
        ' DEFAULT',
        ' COLLATE',
        ' CHARACTER SET',
        ' AUTO_INCREMENT',
        ' COMMENT',
        ' PRIMARY KEY',
        ' UNIQUE',
        ' KEY',
        ' REFERENCES',
        ' ON UPDATE'
    );

    $upper = strtoupper($rest);
    $cut = strlen($rest);

    foreach ($markers as $marker) {
        $pos = strpos($upper, $marker);
        if ($pos !== false && $pos < $cut) {
            $cut = $pos;
        }
    }

    $type = trim(substr($rest, 0, $cut));
    return preg_replace('/\s+/', ' ', $type);
}

function parse_insert_counts($sql) {
    $counts = array();
    $m = array();

    if (preg_match_all('/INSERT\s+INTO\s+(?:(?:`[^`]+`|[A-Za-z0-9_]+)\.)?`?([A-Za-z0-9_]+)`?/i', $sql, $m)) {
        foreach ($m[1] as $table) {
            inc_count($counts, $table);
        }
    }

    arsort($counts);
    return $counts;
}

function extract_names_from_list($list) {
    $names = array();
    $m = array();

    if (preg_match_all('/`([^`]+)`/', $list, $m)) {
        return $m[1];
    }

    $list = preg_replace('/\([0-9]+\)/', '', $list);
    $parts = explode(',', $list);

    foreach ($parts as $p) {
        $p = trim($p, " \t\n\r\0\x0B`");
        if ($p !== '') $names[] = $p;
    }

    return $names;
}

function guess_module($table) {
    $t = strtolower($table);

    $rules = array(
        'auth_user_role' => array('user', 'admin', 'role', 'permission', 'staff', 'employee', 'login'),
        'customer_crm' => array('customer', 'client', 'party'),
        'supplier_purchase' => array('supplier', 'vendor', 'purchase', 'po', 'receive'),
        'sales_invoice' => array('sale', 'sales', 'invoice', 'bill', 'quotation', 'return'),
        'workshop_karigar' => array('karigar', 'artisan', 'worker', 'job', 'repair', 'order', 'workshop', 'production', 'making'),
        'accounts_finance' => array('account', 'ledger', 'payment', 'cash', 'bank', 'expense', 'income', 'voucher', 'transaction', 'balance'),
        'inventory_product' => array('stock', 'inventory', 'product', 'item', 'category', 'metal', 'gold', 'silver', 'purity', 'stone', 'diamond', 'warehouse', 'barcode'),
        'reports_audit_settings' => array('audit', 'log', 'setting', 'config', 'backup', 'migration', 'report')
    );

    foreach ($rules as $module => $words) {
        foreach ($words as $w) {
            if (strpos($t, $w) !== false) {
                return $module;
            }
        }
    }

    return 'other_uncategorized';
}

function generate_recommendations($zipReport, $sqlReport, $dirReport) {
    $rec = array();

    if (isset($sqlReport['destructive_statement_counts']['drop_table']) && $sqlReport['destructive_statement_counts']['drop_table'] > 0) {
        $rec[] = array(
            'priority' => 'P0',
            'area' => 'DB Import Safety',
            'message' => 'SQL dump contains DROP TABLE. Do NOT import into live DB directly. Use fresh DB copy or remove DROP lines first.'
        );
    }

    if (isset($sqlReport['destructive_statement_counts']['delete_from']) && $sqlReport['destructive_statement_counts']['delete_from'] > 0) {
        $rec[] = array(
            'priority' => 'P0',
            'area' => 'DB Import Safety',
            'message' => 'SQL dump contains DELETE FROM. Review before import.'
        );
    }

    $critical = severity_count_from_report($zipReport, 'critical');
    if (is_array($dirReport)) {
        $critical += severity_count_from_report($dirReport, 'critical');
    }

    if ($critical > 0) {
        $rec[] = array(
            'priority' => 'P0',
            'area' => 'API Security',
            'message' => 'Critical PHP security flags found. Fix/quarantine before any live upload.'
        );
    }

    if (isset($sqlReport['tables_without_primary_key']) && count($sqlReport['tables_without_primary_key']) > 0) {
        $rec[] = array(
            'priority' => 'P1',
            'area' => 'Database Integrity',
            'message' => 'Some tables have no primary key. Add safe PK/index plan after backup.'
        );
    }

    if (isset($sqlReport['tables_without_foreign_key']) && count($sqlReport['tables_without_foreign_key']) === (int)$sqlReport['table_count'] && (int)$sqlReport['table_count'] > 3) {
        $rec[] = array(
            'priority' => 'P1',
            'area' => 'Database Integrity',
            'message' => 'No foreign keys detected. Use application-level ledger validation or add FK gradually.'
        );
    }

    if (!sql_has_table_like($sqlReport, array('migration', 'schema_version'))) {
        $rec[] = array(
            'priority' => 'P1',
            'area' => 'Future Updates',
            'message' => 'No migration/version table detected. Add migrations table so future updates stay controlled.'
        );
    }

    $rec[] = array(
        'priority' => 'P2',
        'area' => 'Update Process',
        'message' => 'Follow rename backup pattern: old file/folder rename first, then upload new version.'
    );

    return $rec;
}

function generate_module_sequence($sqlReport) {
    $map = isset($sqlReport['tables_by_module']) ? $sqlReport['tables_by_module'] : array();

    $found = function($keys) use ($map) {
        $out = array();
        foreach ($keys as $k) {
            if (isset($map[$k])) {
                $out = array_merge($out, $map[$k]);
            }
        }
        $out = array_values(array_unique($out));
        sort($out);
        return $out;
    };

    return array(
        array(
            'serial' => 0,
            'module' => 'Safety Backup + Version Lock',
            'found_tables' => array(),
            'update_focus' => 'Full DB backup, API folder backup, no live overwrite.',
            'test' => 'Backup download + restore test on copy DB.'
        ),
        array(
            'serial' => 1,
            'module' => 'DB Import Safety',
            'found_tables' => array(),
            'update_focus' => 'Check DROP/DELETE/TRUNCATE before import.',
            'test' => 'Import only on fresh test DB first.'
        ),
        array(
            'serial' => 2,
            'module' => 'API Security Core',
            'found_tables' => array(),
            'update_focus' => 'Central DB connection, auth guard, JSON response, validation, error handling.',
            'test' => 'Invalid token, invalid input, PHP error hidden.'
        ),
        array(
            'serial' => 3,
            'module' => 'Auth / User / Role Permission',
            'found_tables' => $found(array('auth_user_role')),
            'update_focus' => 'Password hashing, session/token safety, role-based API access.',
            'test' => 'Admin/staff permission test.'
        ),
        array(
            'serial' => 4,
            'module' => 'Customer / Supplier / Karigar Master',
            'found_tables' => $found(array('customer_crm', 'supplier_purchase', 'workshop_karigar')),
            'update_focus' => 'Clean master data model before transaction modules.',
            'test' => 'Create/edit/search master records.'
        ),
        array(
            'serial' => 5,
            'module' => 'Product / Metal / Purity / Stone Model',
            'found_tables' => $found(array('inventory_product')),
            'update_focus' => 'Item coding, metal type, purity, stone/diamond fields, barcode if needed.',
            'test' => 'Product create + duplicate prevention.'
        ),
        array(
            'serial' => 6,
            'module' => 'Stock Ledger / Inventory Core',
            'found_tables' => $found(array('inventory_product')),
            'update_focus' => 'Stock in/out ledger, weight/piece tracking, no direct stock overwrite.',
            'test' => 'Purchase increases stock, sale decreases stock, return adjusts stock.'
        ),
        array(
            'serial' => 7,
            'module' => 'Purchase',
            'found_tables' => $found(array('supplier_purchase')),
            'update_focus' => 'Purchase header/items/payment/stock impact.',
            'test' => 'Purchase entry creates ledger and stock movement.'
        ),
        array(
            'serial' => 8,
            'module' => 'Sales / Invoice',
            'found_tables' => $found(array('sales_invoice')),
            'update_focus' => 'Invoice, sale items, discount, VAT/tax if any, payment, stock out.',
            'test' => 'Invoice print + stock out + due balance.'
        ),
        array(
            'serial' => 9,
            'module' => 'Karigar / Order / Repair / Making',
            'found_tables' => $found(array('workshop_karigar')),
            'update_focus' => 'Job issue/receive, making charge, wastage, repair status.',
            'test' => 'Karigar issue and receive updates ledger.'
        ),
        array(
            'serial' => 10,
            'module' => 'Accounts / Cash / Bank / Ledger',
            'found_tables' => $found(array('accounts_finance')),
            'update_focus' => 'Cashbook, bank, due, supplier/customer ledger.',
            'test' => 'Payment entry reflects correct balance.'
        ),
        array(
            'serial' => 11,
            'module' => 'Reports',
            'found_tables' => $found(array('reports_audit_settings')),
            'update_focus' => 'Daily sale, stock, due, profit, karigar, purchase reports.',
            'test' => 'Report totals match transaction tables.'
        ),
        array(
            'serial' => 12,
            'module' => 'Audit Log + Backup Automation',
            'found_tables' => $found(array('reports_audit_settings')),
            'update_focus' => 'Who changed what, backup log, safe restore points.',
            'test' => 'Update action creates audit log.'
        ),
        array(
            'serial' => 13,
            'module' => 'Performance / Index Cleanup',
            'found_tables' => array(),
            'update_focus' => 'Add missing indexes safely; no DROP.',
            'test' => 'Search/list APIs load fast.'
        )
    );
}

function sql_has_table_like($sqlReport, $words) {
    if (!isset($sqlReport['tables']) || !is_array($sqlReport['tables'])) return false;

    foreach ($sqlReport['tables'] as $t) {
        $name = strtolower($t['name']);
        foreach ($words as $w) {
            if (strpos($name, strtolower($w)) !== false) {
                return true;
            }
        }
    }

    return false;
}

function table_has_any_column($table, $names) {
    if (!isset($table['columns'])) return false;

    $set = array();
    foreach ($table['columns'] as $c) {
        $set[strtolower($c['name'])] = true;
    }

    foreach ($names as $n) {
        if (isset($set[strtolower($n)])) return true;
    }

    return false;
}

function count_regex($pattern, $text) {
    $n = preg_match_all($pattern, $text, $m);
    return $n === false ? 0 : (int)$n;
}

function inc_count(&$arr, $key) {
    if (!isset($arr[$key])) $arr[$key] = 0;
    $arr[$key]++;
}

function top_dir($name) {
    $parts = explode('/', $name);
    return count($parts) > 1 ? $parts[0] : '[root]';
}

function looks_like_api_file($name) {
    $l = '/' . strtolower($name);
    $base = strtolower(basename($name));

    if (strpos($l, '/api') !== false) return true;

    $keys = array(
        'login', 'logout', 'auth', 'customer', 'supplier', 'product',
        'stock', 'inventory', 'purchase', 'sale', 'invoice', 'payment',
        'report', 'user', 'role', 'karigar', 'order'
    );

    foreach ($keys as $k) {
        if (strpos($base, $k) !== false) return true;
    }

    return false;
}

function line_number_from_offset($text, $offset) {
    if ($offset <= 0) return 1;
    return substr_count(substr($text, 0, $offset), "\n") + 1;
}

function severity_counts($issues) {
    $counts = array(
        'critical' => 0,
        'high' => 0,
        'medium' => 0,
        'low' => 0,
        'info' => 0
    );

    foreach ($issues as $i) {
        $s = isset($i['severity']) ? $i['severity'] : 'info';
        if (!isset($counts[$s])) $counts[$s] = 0;
        $counts[$s]++;
    }

    return $counts;
}

function severity_count_from_report($report, $severity) {
    if (!is_array($report)) return 0;
    if (!isset($report['issues_by_severity'][$severity])) return 0;
    return (int)$report['issues_by_severity'][$severity];
}

function to_json($data) {
    $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
    if (defined('JSON_INVALID_UTF8_SUBSTITUTE')) {
        $flags |= JSON_INVALID_UTF8_SUBSTITUTE;
    }

    $json = json_encode($data, $flags);

    if ($json === false) {
        $json = json_encode(array(
            'json_error' => json_last_error_msg(),
            'message' => 'Report generated but JSON encoding failed.'
        ), JSON_PRETTY_PRINT);
    }

    return (string)$json;
}

function h($v) {
    return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function human_bytes($bytes) {
    $bytes = (float)$bytes;
    $units = array('B', 'KB', 'MB', 'GB');
    $i = 0;

    while ($bytes >= 1024 && $i < count($units) - 1) {
        $bytes /= 1024;
        $i++;
    }

    return round($bytes, 2) . ' ' . $units[$i];
}

function limit_list($arr, $max) {
    if (!is_array($arr)) return array();
    if (count($arr) <= $max) return $arr;

    $out = array_slice($arr, 0, $max);
    $out[] = '... +' . (count($arr) - $max) . ' more';
    return $out;
}

function render_html($report) {
    header('Content-Type: text/html; charset=UTF-8');

    $extra = (isset($_GET['scan_dir']) && (string)$_GET['scan_dir'] === '1') ? '&scan_dir=1' : '';
    $download = h($_SERVER['PHP_SELF'] . '?key=' . rawurlencode(SECRET_KEY) . $extra . '&download=1');

    $zip = $report['api_source_zip'];
    $sql = $report['database_sql'];

    $critical = severity_count_from_report($zip, 'critical');
    if (is_array($report['api_source_directory_scan'])) {
        $critical += severity_count_from_report($report['api_source_directory_scan'], 'critical');
    }

    echo '<!doctype html><html><head><meta charset="UTF-8">';
    echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
    echo '<title>ERP Analyzer Report</title>';
    echo '<style>
        body{font-family:Arial,sans-serif;background:#f6f7fb;margin:0;padding:16px;color:#222}
        .card{background:#fff;border-radius:12px;padding:16px;margin-bottom:14px;box-shadow:0 2px 10px rgba(0,0,0,.06)}
        h1,h2{margin-top:0}
        table{width:100%;border-collapse:collapse;font-size:14px}
        th,td{border:1px solid #ddd;padding:8px;text-align:left;vertical-align:top}
        th{background:#f0f2f7}
        .btn{display:inline-block;background:#0b7cff;color:#fff;padding:12px 16px;border-radius:10px;text-decoration:none;font-weight:bold}
        .warn{background:#fff3cd;border:1px solid #ffe08a}
        .bad{color:#b00020;font-weight:bold}
        .ok{color:#0a7a2f;font-weight:bold}
        code{background:#f0f0f0;padding:2px 5px;border-radius:4px}
    </style>';
    echo '</head><body>';

    echo '<div class="card">';
    echo '<h1>ERP Analyzer Report Ready ✅</h1>';
    echo '<p>JSON report saved as <code>' . h(REPORT_FILE) . '</code></p>';
    echo '<p><a class="btn" href="' . $download . '">Download JSON Report</a></p>';
    echo '</div>';

    echo '<div class="card">';
    echo '<h2>Quick Summary</h2>';
    echo '<table>';
    echo '<tr><th>Area</th><th>Result</th></tr>';
    echo '<tr><td>ZIP exists</td><td>' . ($zip['exists'] ? '<span class="ok">Yes</span>' : '<span class="bad">No</span>') . '</td></tr>';
    echo '<tr><td>API/PHP files found</td><td>' . h($zip['php_files']) . '</td></tr>';
    echo '<tr><td>SQL exists</td><td>' . ($sql['exists'] ? '<span class="ok">Yes</span>' : '<span class="bad">No</span>') . '</td></tr>';
    echo '<tr><td>DB tables found</td><td>' . h($sql['table_count']) . '</td></tr>';
    echo '<tr><td>Critical PHP flags</td><td>' . ($critical > 0 ? '<span class="bad">' . h($critical) . '</span>' : '<span class="ok">0</span>') . '</td></tr>';
    echo '<tr><td>DROP TABLE count in SQL</td><td>' . h(isset($sql['destructive_statement_counts']['drop_table']) ? $sql['destructive_statement_counts']['drop_table'] : 0) . '</td></tr>';
    echo '</table>';
    echo '</div>';

    if (!empty($zip['errors']) || !empty($sql['errors'])) {
        echo '<div class="card warn"><h2>Errors / Notes</h2><ul>';
        foreach ($zip['errors'] as $e) echo '<li>' . h($e) . '</li>';
        foreach ($sql['errors'] as $e) echo '<li>' . h($e) . '</li>';
        echo '</ul>';
        echo '<p>If ZipArchive error shows, extract the ZIP in this same folder and open URL with <code>&scan_dir=1</code>.</p>';
        echo '</div>';
    }

    echo '<div class="card">';
    echo '<h2>Recommended Module Update Sequence</h2>';
    echo '<table><tr><th>Serial</th><th>Module</th><th>Found Tables</th><th>Update Focus</th><th>Test</th></tr>';

    foreach ($report['module_update_sequence'] as $row) {
        $found = implode(', ', limit_list($row['found_tables'], 8));
        echo '<tr>';
        echo '<td>' . h($row['serial']) . '</td>';
        echo '<td>' . h($row['module']) . '</td>';
        echo '<td>' . h($found) . '</td>';
        echo '<td>' . h($row['update_focus']) . '</td>';
        echo '<td>' . h($row['test']) . '</td>';
        echo '</tr>';
    }

    echo '</table></div>';

    echo '<div class="card">';
    echo '<h2>Next Step</h2>';
    echo '<p>Download JSON report and send it back. Then I will give exact table-by-table + API-file-by-file update serial with safe SQL migration plan.</p>';
    echo '<p>After report download, rename this folder to <code>_erp_review_20260613_x7p_LOCKED</code>.</p>';
    echo '</div>';

    echo '</body></html>';
}
