<?php

if (PHP_SAPI !== 'cli') {
    http_response_code(403);
    exit("CLI only\n");
}

$options = getopt('', array(
    'app-root::',
    'env::',
    'stage::',
    'strict',
    'no-db',
    'fix-dirs',
    'json::',
    'help',
));

if (isset($options['help'])) {
    echo "ICYLAB Billing production preflight\n";
    echo "Usage:\n";
    echo "  php scripts/billing_production_preflight.php [options]\n\n";
    echo "Options:\n";
    echo "  --app-root=/path       Application root.\n";
    echo "  --env=production       Target FACTO environment. Only production is accepted.\n";
    echo "  --stage=readiness      readiness (default) or pilot.\n";
    echo "  --strict               Treat warnings as blocking failures.\n";
    echo "  --no-db                Skip database checks.\n";
    echo "  --fix-dirs             Create missing production storage directories.\n";
    echo "  --json=/path/file.json Write a machine-readable report.\n";
    exit(0);
}

$appRoot = isset($options['app-root']) && $options['app-root'] !== false
    ? (string) $options['app-root']
    : dirname(__DIR__);
$appRoot = realpath($appRoot) ?: rtrim($appRoot, DIRECTORY_SEPARATOR);
$environment = strtolower(trim(isset($options['env']) && $options['env'] !== false ? (string) $options['env'] : 'production'));
$stage = strtolower(trim(isset($options['stage']) && $options['stage'] !== false ? (string) $options['stage'] : 'readiness'));
$strict = isset($options['strict']);
$skipDb = isset($options['no-db']);
$fixDirs = isset($options['fix-dirs']);
$jsonPath = isset($options['json']) && $options['json'] !== false ? (string) $options['json'] : '';

if ($environment !== 'production') {
    fwrite(STDERR, "ERROR: this preflight only accepts --env=production.\n");
    exit(1);
}
if (!in_array($stage, array('readiness', 'pilot'), true)) {
    fwrite(STDERR, "ERROR: --stage must be readiness or pilot.\n");
    exit(1);
}
if (!is_dir($appRoot)) {
    fwrite(STDERR, "ERROR: application root does not exist.\n");
    exit(1);
}

$configPath = $appRoot . '/core/config.php';
if (!is_file($configPath)) {
    fwrite(STDERR, "ERROR: core/config.php was not found.\n");
    exit(1);
}

require_once $configPath;
if (is_file($appRoot . '/core/billing_runtime.php')) {
    require_once $appRoot . '/core/billing_runtime.php';
}

$report = array(
    'tool' => 'ICYLAB Billing production preflight',
    'version' => '7.1.0',
    'generated_at_utc' => gmdate('c'),
    'environment' => $environment,
    'stage' => $stage,
    'strict' => $strict,
    'app_root' => $appRoot,
    'checks' => array(),
    'summary' => array(),
);

function preflight_add(&$report, $level, $code, $message, array $context = array())
{
    $level = strtoupper((string) $level);
    $allowed = array('PASS', 'INFO', 'WARN', 'FAIL');
    if (!in_array($level, $allowed, true)) {
        $level = 'INFO';
    }

    $report['checks'][] = array(
        'level' => $level,
        'code' => (string) $code,
        'message' => (string) $message,
        'context' => $context,
    );
}

function preflight_mask_mode($path)
{
    $perms = @fileperms($path);
    if ($perms === false) {
        return null;
    }
    return $perms & 0777;
}

function preflight_explicit_value($name)
{
    if (defined($name)) {
        return trim((string) constant($name));
    }
    $value = getenv($name);
    return $value === false ? '' : trim((string) $value);
}

function preflight_table_exists(PDO $pdo, $table)
{
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?');
    $stmt->execute(array($table));
    return (int) $stmt->fetchColumn() > 0;
}

function preflight_columns(PDO $pdo, $table)
{
    $stmt = $pdo->prepare('SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?');
    $stmt->execute(array($table));
    $columns = array();
    foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $column) {
        $columns[] = (string) $column;
    }
    return $columns;
}

function preflight_index_exists(PDO $pdo, $table, $indexName, array $columns)
{
    $stmt = $pdo->prepare('SELECT INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ? ORDER BY SEQ_IN_INDEX');
    $stmt->execute(array($table, $indexName));
    $found = array();
    foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
        $found[] = (string) $row['COLUMN_NAME'];
    }
    return $found === array_values($columns);
}

function preflight_scalar(PDO $pdo, $sql, array $params = array())
{
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt->fetchColumn();
}

function preflight_write_json($path, array $report)
{
    if ($path === '') {
        return true;
    }
    $dir = dirname($path);
    if (!is_dir($dir) && !@mkdir($dir, 0750, true)) {
        return false;
    }
    $json = json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    return $json !== false && file_put_contents($path, $json . PHP_EOL, LOCK_EX) !== false;
}

// Runtime and PHP checks.
if (version_compare(PHP_VERSION, '7.4.0', '>=')) {
    preflight_add($report, 'PASS', 'PHP_VERSION', 'PHP version is supported.', array('version' => PHP_VERSION));
} else {
    preflight_add($report, 'FAIL', 'PHP_VERSION', 'PHP 7.4 or newer is required.', array('version' => PHP_VERSION));
}

$requiredExtensions = array('PDO', 'pdo_mysql', 'curl', 'json', 'openssl', 'zip');
foreach ($requiredExtensions as $extension) {
    if (extension_loaded($extension)) {
        preflight_add($report, 'PASS', 'PHP_EXT_' . strtoupper($extension), 'Required PHP extension is loaded.', array('extension' => $extension));
    } else {
        preflight_add($report, 'FAIL', 'PHP_EXT_' . strtoupper($extension), 'Required PHP extension is missing.', array('extension' => $extension));
    }
}

$currentAppEnvironment = defined('APP_ENV') ? APP_ENV : 'undefined';
if ($stage === 'pilot') {
    if ($currentAppEnvironment === 'production') {
        preflight_add($report, 'PASS', 'APP_ENV', 'APP_ENV is production for the pilot gate.');
    } else {
        preflight_add($report, 'FAIL', 'APP_ENV', 'APP_ENV must be production for the pilot gate.', array('app_env' => $currentAppEnvironment));
    }
} elseif (in_array($currentAppEnvironment, array('staging', 'production'), true)) {
    preflight_add($report, 'PASS', 'APP_ENV', 'APP_ENV is valid for the readiness stage.', array('app_env' => $currentAppEnvironment));
} else {
    preflight_add($report, 'FAIL', 'APP_ENV', 'APP_ENV must be staging or production for readiness.', array('app_env' => $currentAppEnvironment));
}
if (defined('APP_DEBUG') && APP_DEBUG) {
    preflight_add($report, 'FAIL', 'APP_DEBUG', 'APP_DEBUG must be disabled for production.');
} else {
    preflight_add($report, 'PASS', 'APP_DEBUG', 'Debug mode is disabled.');
}

// Secret file and source-leak checks.
$privateConfigPath = getenv('ICYLAB_PRIVATE_CONFIG_PATH');
if ($privateConfigPath === false || trim((string) $privateConfigPath) === '') {
    $privateConfigPath = $appRoot . '/core/private_config.php';
}
$privateConfigPath = (string) $privateConfigPath;

if (is_file($privateConfigPath)) {
    preflight_add($report, 'PASS', 'PRIVATE_CONFIG_EXISTS', 'Private configuration file exists.', array('outside_app_root' => strpos(realpath($privateConfigPath) ?: $privateConfigPath, $appRoot) !== 0));
    $mode = preflight_mask_mode($privateConfigPath);
    if ($mode === null) {
        preflight_add($report, 'WARN', 'PRIVATE_CONFIG_MODE', 'Unable to inspect private configuration permissions.');
    } elseif (($mode & 0007) !== 0 || ($mode & 0020) !== 0) {
        preflight_add($report, 'FAIL', 'PRIVATE_CONFIG_MODE', 'Private configuration permissions are too broad.', array('mode' => sprintf('%04o', $mode)));
    } elseif ($mode === 0600 || $mode === 0640 || $mode === 0400 || $mode === 0440) {
        preflight_add($report, 'PASS', 'PRIVATE_CONFIG_MODE', 'Private configuration permissions are restrictive.', array('mode' => sprintf('%04o', $mode)));
    } else {
        preflight_add($report, 'WARN', 'PRIVATE_CONFIG_MODE', 'Review private configuration permissions; 0600 is recommended.', array('mode' => sprintf('%04o', $mode)));
    }
} else {
    preflight_add($report, 'FAIL', 'PRIVATE_CONFIG_EXISTS', 'Private configuration file was not found.');
}

$defaultPrivateConfigPath = $appRoot . '/core/private_config.php';
$resolvedPrivateConfigPath = realpath($privateConfigPath) ?: $privateConfigPath;
$resolvedDefaultPrivateConfigPath = realpath($defaultPrivateConfigPath) ?: $defaultPrivateConfigPath;
if ($resolvedPrivateConfigPath !== $resolvedDefaultPrivateConfigPath && is_file($defaultPrivateConfigPath)) {
    preflight_add($report, 'FAIL', 'PRIVATE_CONFIG_STALE_COPY', 'An external private configuration is active, but a second core/private_config.php copy remains in the application root.');
} else {
    preflight_add($report, 'PASS', 'PRIVATE_CONFIG_STALE_COPY', 'No stale duplicate private configuration was detected.');
}

$coreHtaccess = $appRoot . '/core/.htaccess';
if (is_file($coreHtaccess)) {
    $contents = (string) file_get_contents($coreHtaccess);
    if (stripos($contents, 'Require all denied') !== false || stripos($contents, 'Deny from all') !== false) {
        preflight_add($report, 'PASS', 'PRIVATE_CONFIG_WEB_DENY', 'core/.htaccess contains a direct-access deny rule.');
    } else {
        preflight_add($report, 'WARN', 'PRIVATE_CONFIG_WEB_DENY', 'core/.htaccess does not contain a recognizable deny rule.');
    }
} else {
    preflight_add($report, 'WARN', 'PRIVATE_CONFIG_WEB_DENY', 'core/.htaccess was not found. Verify equivalent web-server protection.');
}

$unexpectedSensitive = array();
$scanRoots = array($appRoot, $appRoot . '/core');
foreach ($scanRoots as $scanRoot) {
    if (!is_dir($scanRoot)) {
        continue;
    }
    $iterator = new DirectoryIterator($scanRoot);
    foreach ($iterator as $item) {
        if ($item->isDot() || !$item->isFile()) {
            continue;
        }
        $name = $item->getFilename();
        $path = $item->getPathname();
        if ($path === $privateConfigPath || $name === 'private_config.example.php') {
            continue;
        }
        if (preg_match('/(?:\.sql|\.dump|\.bak|\.old|\.zip|\.tar|\.tar\.gz|\.tgz|~)$/i', $name)
            || preg_match('/private_config.*(?:copy|backup|old|bak)/i', $name)) {
            $unexpectedSensitive[] = str_replace($appRoot . '/', '', $path);
        }
    }
}
if (empty($unexpectedSensitive)) {
    preflight_add($report, 'PASS', 'SOURCE_ARTIFACTS', 'No obvious dumps, archives, or private-config backups were found in the application root.');
} else {
    preflight_add($report, 'FAIL', 'SOURCE_ARTIFACTS', 'Potentially sensitive artifacts exist under the application root.', array('files' => $unexpectedSensitive));
}

// Explicit production FACTO configuration. Legacy fallback is not accepted.
$explicitFactoMap = array(
    'base_url' => 'ICYLAB_FACTO_PRODUCTION_API_BASE_URL',
    'account_id' => 'ICYLAB_FACTO_PRODUCTION_ACCOUNT_ID',
    'client_id' => 'ICYLAB_FACTO_PRODUCTION_CLIENT_ID',
    'client_secret' => 'ICYLAB_FACTO_PRODUCTION_CLIENT_SECRET',
    'username' => 'ICYLAB_FACTO_PRODUCTION_USERNAME',
    'password' => 'ICYLAB_FACTO_PRODUCTION_PASSWORD',
);
$missingExplicit = array();
$placeholderExplicit = array();
foreach ($explicitFactoMap as $label => $name) {
    $value = preflight_explicit_value($name);
    if ($value === '') {
        $missingExplicit[] = $label;
    } elseif (preg_match('/(?:REPLACE_|CHANGEME|YOUR_|EXAMPLE)/i', $value)) {
        $placeholderExplicit[] = $label;
    }
}
if (empty($missingExplicit) && empty($placeholderExplicit)) {
    preflight_add($report, 'PASS', 'FACTO_EXPLICIT_PRODUCTION_CONFIG', 'Explicit production FACTO configuration is complete.');
} else {
    preflight_add($report, 'FAIL', 'FACTO_EXPLICIT_PRODUCTION_CONFIG', 'Explicit production FACTO configuration is incomplete or still contains placeholders. Legacy ICYLAB_FACTO_* fallback is not accepted.', array('missing_fields' => $missingExplicit, 'placeholder_fields' => $placeholderExplicit));
}

$productionBaseUrl = preflight_explicit_value('ICYLAB_FACTO_PRODUCTION_API_BASE_URL');
$productionHost = $productionBaseUrl !== '' ? parse_url($productionBaseUrl, PHP_URL_HOST) : '';
$productionHost = is_string($productionHost) ? strtolower($productionHost) : '';
if ($productionBaseUrl !== '' && stripos($productionBaseUrl, 'https://') === 0
    && strpos($productionHost, 'sandbox') === false
    && strpos($productionHost, 'test') === false
    && strpos($productionHost, 'dev') === false) {
    preflight_add($report, 'PASS', 'FACTO_PRODUCTION_URL', 'Production FACTO URL uses HTTPS and does not look like a non-production host.', array('host' => $productionHost));
} elseif ($productionBaseUrl !== '') {
    preflight_add($report, 'FAIL', 'FACTO_PRODUCTION_URL', 'Production FACTO URL failed the HTTPS/hostname safety check.', array('host' => $productionHost));
}

// Feature gates. Readiness must remain closed. Pilot has stricter canary requirements.
$readEnabled = defined('ICYLAB_BILLING_PRODUCTION_READ_ENABLED') && ICYLAB_BILLING_PRODUCTION_READ_ENABLED;
$financeEnabled = defined('ICYLAB_BILLING_PRODUCTION_FINANCE_ENABLED') && ICYLAB_BILLING_PRODUCTION_FINANCE_ENABLED;
$issueEnabled = defined('ICYLAB_BILLING_PRODUCTION_ISSUE_ENABLED') && ICYLAB_BILLING_PRODUCTION_ISSUE_ENABLED;
$supportedProductionDocumentKeys = array(
    'invoice_electronic','invoice_exempt_electronic','invoice_export_electronic',
    'credit_note_electronic','debit_note_electronic',
    'credit_note_export_electronic','debit_note_export_electronic',
    'dispatch_guide_electronic'
);
if ($stage === 'readiness') {
    if ($readEnabled && $financeEnabled && !$issueEnabled) {
        preflight_add($report, 'PASS', 'BILLING_GATES', 'Production read and reception/finance are enabled; issuance remains closed.', array('read_enabled' => $readEnabled));
    } else {
        preflight_add($report, 'FAIL', 'BILLING_GATES', 'Readiness requires READ=true, FINANCE=true and ISSUE=false.', array('read_enabled' => $readEnabled, 'finance_enabled' => $financeEnabled, 'issue_enabled' => $issueEnabled));
    }
} else {
    $allowedProductionDocumentKeys = function_exists('billing_runtime_csv_values')
        ? billing_runtime_csv_values(defined('ICYLAB_BILLING_PRODUCTION_ALLOWED_DOCUMENT_KEYS') ? ICYLAB_BILLING_PRODUCTION_ALLOWED_DOCUMENT_KEYS : '')
        : array();
    $unexpectedAllowedKeys = array_values(array_diff($allowedProductionDocumentKeys, $supportedProductionDocumentKeys));
    if ($readEnabled && $financeEnabled && $issueEnabled && !empty($allowedProductionDocumentKeys) && empty($unexpectedAllowedKeys)) {
        preflight_add($report, 'PASS', 'BILLING_GATES', 'Production gates and an explicit supported DTE allowlist are enabled.', array('allowed_keys'=>$allowedProductionDocumentKeys));
    } else {
        preflight_add($report, 'FAIL', 'BILLING_GATES', 'Production requires all gates and a non-empty allowlist containing only supported DTE types.', array('allowed_keys'=>$allowedProductionDocumentKeys,'unexpected_keys'=>$unexpectedAllowedKeys));
    }

    $canaryMode = defined('ICYLAB_BILLING_PRODUCTION_CANARY_MODE')
        ? (bool)ICYLAB_BILLING_PRODUCTION_CANARY_MODE
        : true;
    $allowedUserIds = function_exists('billing_runtime_csv_values')
        ? array_values(array_filter(array_map('intval', billing_runtime_csv_values(defined('ICYLAB_BILLING_PRODUCTION_ALLOWED_USER_IDS') ? ICYLAB_BILLING_PRODUCTION_ALLOWED_USER_IDS : ''))))
        : array();
    $allowedCustomerIds = function_exists('billing_runtime_csv_values')
        ? array_values(array_filter(array_map('intval', billing_runtime_csv_values(defined('ICYLAB_BILLING_PRODUCTION_ALLOWED_CUSTOMER_IDS') ? ICYLAB_BILLING_PRODUCTION_ALLOWED_CUSTOMER_IDS : ''))))
        : array();
    $pilotEndsAt = defined('ICYLAB_BILLING_PRODUCTION_PILOT_ENDS_AT') ? trim((string)ICYLAB_BILLING_PRODUCTION_PILOT_ENDS_AT) : '';
    $pilotTimestamp = $pilotEndsAt !== '' ? strtotime($pilotEndsAt) : false;
    $maxPilotAmount = defined('ICYLAB_BILLING_PRODUCTION_MAX_TOTAL_AMOUNT') ? (float)ICYLAB_BILLING_PRODUCTION_MAX_TOTAL_AMOUNT : 0.0;
    $confirmationText = defined('ICYLAB_BILLING_PRODUCTION_CONFIRMATION_TEXT') ? trim((string)ICYLAB_BILLING_PRODUCTION_CONFIRMATION_TEXT) : '';
    $canaryReady = $canaryMode
        && !empty($allowedUserIds)
        && !empty($allowedCustomerIds)
        && $pilotTimestamp !== false
        && $pilotTimestamp > time()
        && $maxPilotAmount > 0
        && strlen($confirmationText) >= 12;
    preflight_add(
        $report,
        $canaryReady ? 'PASS' : 'FAIL',
        'PRODUCTION_CANARY_LIMITS',
        $canaryReady
            ? 'The production pilot is restricted by user, customer, amount, confirmation, and expiry.'
            : 'Pilot mode requires non-empty user/customer allowlists, a future expiry, a positive maximum amount, and an explicit confirmation phrase.',
        array(
            'canary_mode' => $canaryMode,
            'allowed_user_count' => count($allowedUserIds),
            'allowed_customer_count' => count($allowedCustomerIds),
            'pilot_expiry_valid' => $pilotTimestamp !== false && $pilotTimestamp > time(),
            'max_amount_configured' => $maxPilotAmount > 0,
            'confirmation_configured' => strlen($confirmationText) >= 12
        )
    );
}

$draftPreviewEnabled = defined('ICYLAB_FACTO_PRODUCTION_DRAFT_PREVIEW_ENABLED') && ICYLAB_FACTO_PRODUCTION_DRAFT_PREVIEW_ENABLED;
preflight_add(
    $report,
    'PASS',
    'FACTO_DRAFT_PREVIEW_CONTRACT',
    $draftPreviewEnabled
        ? 'Production draft preview is explicitly enabled for this FACTO account.'
        : 'Production draft preview remains blocked; the pilot can issue only through the validated final issuance path.'
);

$csrfFiles = array(
    $appRoot . '/core/csrf.php',
    $appRoot . '/core/session.php',
    $appRoot . '/assets/js/csrf.js',
    $appRoot . '/modules/billing/confirm_issue_ajax.php',
    $appRoot . '/modules/billing/receive_decide_ajax.php'
);
$csrfMissing = array();
foreach ($csrfFiles as $csrfFile) {
    $contents = is_file($csrfFile) ? (string)file_get_contents($csrfFile) : '';
    if ($contents === '' || ($csrfFile !== $appRoot . '/assets/js/csrf.js' && strpos($contents, 'csrf') === false)) {
        $csrfMissing[] = str_replace($appRoot . '/', '', $csrfFile);
    }
}
preflight_add($report, empty($csrfMissing) ? 'PASS' : 'FAIL', 'CSRF_PROTECTION', empty($csrfMissing) ? 'CSRF generation, transport, and server validation are installed.' : 'CSRF protection is incomplete.', array('files'=>$csrfMissing));

// La v7 usa un bloqueo central dinámico; no deben sobrevivir bloqueos sandbox dispersos.
if ($stage === 'readiness') {
    $gatePatterns = array(
        'services/billing/BillingRuntimeGate.php' => array('ICYLAB_BILLING_PRODUCTION_ISSUE_ENABLED', 'billing_assert_issue_confirmation', 'billing_assert_issue_payload_policy'),
        'services/billing/BillingManualDocumentService.php' => array('billing_assert_runtime_operation', 'billing_assert_issue_confirmation'),
        'services/billing/BillingDispatchGuideService.php' => array('billing_assert_runtime_operation', 'billing_assert_issue_confirmation'),
        'services/billing/BillingUnifiedIssuerService.php' => array('billing_assert_runtime_operation'),
    );
    $missingGates = array();
    foreach ($gatePatterns as $relative => $patterns) {
        $path = $appRoot . '/' . $relative;
        $contents = is_file($path) ? (string) file_get_contents($path) : '';
        $ok = $contents !== '';
        foreach ($patterns as $pattern) {
            if (stripos($contents, $pattern) === false) {
                $ok = false;
                break;
            }
        }
        if (!$ok) {
            $missingGates[] = $relative;
        }
    }
    if (empty($missingGates)) {
        preflight_add($report, 'PASS', 'ISSUANCE_RUNTIME_GATES', 'All production issuance paths use the central runtime gate and reinforced confirmation.');
    } else {
        preflight_add($report, 'FAIL', 'ISSUANCE_RUNTIME_GATES', 'A production issuance path is missing central gate controls.', array('files' => $missingGates));
    }
}

// Storage checks.
$storageBase = $appRoot . '/storage/billing';
$productionStorageTypes = array('pdf', 'xml', 'stamp', 'pdf_transfer_copy', 'preview_pdf', 'preview_xml', 'issued_pdf', 'issued_xml');
$storagePaths = array($appRoot . '/storage/logs', $appRoot . '/storage/cache', $appRoot . '/storage/temp', $storageBase);
foreach ($productionStorageTypes as $type) {
    $storagePaths[] = $storageBase . '/production/' . $type;
}

$missingStorage = array();
$unwritableStorage = array();
foreach ($storagePaths as $path) {
    if (!is_dir($path)) {
        if ($fixDirs) {
            if (@mkdir($path, 0750, true) || is_dir($path)) {
                @chmod($path, 0750);
                $denyFile = $path . '/.htaccess';
                if (!is_file($denyFile)) {
                    @file_put_contents($denyFile, "Require all denied\nDeny from all\n");
                }
            }
        }
    }
    if (!is_dir($path)) {
        $missingStorage[] = str_replace($appRoot . '/', '', $path);
    } elseif (!is_writable($path)) {
        $unwritableStorage[] = str_replace($appRoot . '/', '', $path);
    }
}
if (empty($missingStorage)) {
    preflight_add($report, 'PASS', 'STORAGE_DIRECTORIES', 'Required runtime and production billing directories exist.');
} else {
    preflight_add($report, 'WARN', 'STORAGE_DIRECTORIES', 'Required storage directories are missing. Re-run with --fix-dirs after validating ownership.', array('paths' => $missingStorage));
}
if (empty($unwritableStorage)) {
    preflight_add($report, 'PASS', 'STORAGE_WRITABLE', 'Existing required storage directories are writable by the CLI user.');
} else {
    preflight_add($report, 'FAIL', 'STORAGE_WRITABLE', 'Required storage directories are not writable.', array('paths' => $unwritableStorage));
}

// Database checks.
if ($skipDb) {
    preflight_add($report, 'INFO', 'DATABASE_SKIPPED', 'Database checks were skipped by --no-db.');
} else {
    try {
        require_once $appRoot . '/core/database.php';
        $pdo = db();
        $pdo->query('SELECT 1');
        preflight_add($report, 'PASS', 'DATABASE_CONNECTION', 'Database connection succeeded.');

        $requiredSchema = array(
            'billing_document_types' => array('document_key', 'direction', 'enabled_sandbox', 'enabled_production', 'is_active'),
            'billing_issue_logs' => array('environment', 'mode', 'status', 'idempotency_key', 'facto_document_id'),
            'billing_collections' => array('environment', 'direction', 'source_table', 'facto_document_id', 'inbox_document_id', 'customer_id', 'finance_center_id', 'total_amount', 'paid_amount', 'balance_amount', 'collection_status'),
            'billing_payments' => array('collection_id', 'environment', 'direction', 'amount', 'payment_date'),
            'billing_payment_allocations' => array('payment_batch_id', 'payment_id', 'collection_id', 'amount_applied'),
            'billing_manual_operations' => array('environment', 'status', 'idempotency_key'),
            'billing_export_operations' => array('environment', 'status', 'idempotency_key'),
            'billing_tax_operations' => array('environment', 'status', 'idempotency_key'),
            'billing_complementary_operations' => array('environment', 'status', 'idempotency_key'),
            'billing_dispatch_guides' => array('environment', 'status', 'idempotency_key'),
            'billing_bulk_action_logs' => array('environment', 'scope', 'action_key', 'created_by', 'created_at'),
            'billing_reconciliation_issues' => array('severity', 'status', 'direction'),
            'facto_tokens' => array('environment', 'access_token', 'expires_at'),
            'facto_documents' => array('environment', 'document_id', 'crm_customer_id', 'finance_center_id', 'payment_status'),
            'facto_inbox_documents' => array('environment', 'inbox_document_id', 'supplier_customer_id', 'supplier_resolution_status', 'finance_center_id', 'payment_status'),
            'crm_customers' => array('counterparty_type', 'customer_tax_id', 'default_income_center_id', 'default_cost_center_id', 'is_supplier', 'supplier_default_cost_center_id'),
            'finance_centers' => array('center_code', 'center_type', 'is_default', 'is_active'),
            'permissions' => array('permission_key', 'module'),
            'role_permissions' => array('role', 'permission_key'),
            'users' => array('role', 'status', 'email'),
            'facto_sync_logs' => array('status', 'synced_at'),
        );

        $missingTables = array();
        $missingColumns = array();
        foreach ($requiredSchema as $table => $requiredColumns) {
            if (!preflight_table_exists($pdo, $table)) {
                $missingTables[] = $table;
                continue;
            }
            $existingColumns = preflight_columns($pdo, $table);
            foreach ($requiredColumns as $column) {
                if (!in_array($column, $existingColumns, true)) {
                    $missingColumns[] = $table . '.' . $column;
                }
            }
        }
        if (empty($missingTables) && empty($missingColumns)) {
            preflight_add($report, 'PASS', 'DATABASE_SCHEMA', 'Required billing, FACTO, CRM, and finance schema is present.');
        } else {
            preflight_add($report, 'FAIL', 'DATABASE_SCHEMA', 'Required schema objects are missing.', array('missing_tables' => $missingTables, 'missing_columns' => $missingColumns));
        }

        $requiredIndexes = array(
            array('facto_tokens', 'uq_facto_tokens_environment', array('environment')),
            array('facto_inbox_documents', 'uq_inbox_document_env', array('environment', 'inbox_document_id')),
            array('billing_collections', 'uq_collection_document_direction', array('environment', 'direction', 'facto_document_id')),
            array('billing_issue_logs', 'uq_billing_issue_idempotency', array('idempotency_key')),
            array('crm_customers', 'unique_customer_tax_id', array('customer_tax_id')),
            array('finance_centers', 'uq_finance_center_code', array('center_code')),
        );
        $missingIndexes = array();
        foreach ($requiredIndexes as $definition) {
            list($table, $indexName, $columns) = $definition;
            if (!preflight_table_exists($pdo, $table) || !preflight_index_exists($pdo, $table, $indexName, $columns)) {
                $missingIndexes[] = $table . '.' . $indexName;
            }
        }
        if (empty($missingIndexes)) {
            preflight_add($report, 'PASS', 'DATABASE_INDEXES', 'Required uniqueness and lookup indexes are present.');
        } else {
            preflight_add($report, 'FAIL', 'DATABASE_INDEXES', 'Required indexes are missing or have unexpected columns.', array('indexes' => $missingIndexes));
        }

        if (empty($missingTables) && empty($missingColumns)) {
            $productionIssuedEnabled = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_document_types WHERE direction='issued' AND is_active=1 AND enabled_production=1");
            $requestedIssuedEnabled = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_document_types WHERE direction='issued' AND is_active=1 AND enabled_production=1 AND document_key IN ('invoice_electronic','invoice_exempt_electronic','invoice_export_electronic','credit_note_electronic','debit_note_electronic','credit_note_export_electronic','debit_note_export_electronic','dispatch_guide_electronic')");
            if ($stage === 'readiness') {
                if ($productionIssuedEnabled === 8 && $requestedIssuedEnabled === 8) {
                    preflight_add($report, 'PASS', 'DOCUMENT_TYPES_PRODUCTION', 'The supported issued document catalog is enabled; runtime gates remain authoritative.', array('count' => $productionIssuedEnabled));
                } else {
                    preflight_add($report, 'FAIL', 'DOCUMENT_TYPES_PRODUCTION', 'The v7 production document catalog migration is incomplete.', array('count' => $productionIssuedEnabled));
                }
            } else {
                if ($productionIssuedEnabled === 8 && $requestedIssuedEnabled === 8) {
                    preflight_add($report, 'PASS', 'DOCUMENT_TYPES_PRODUCTION', 'The supported production document catalog is enabled.', array('count' => $productionIssuedEnabled));
                } else {
                    preflight_add($report, 'FAIL', 'DOCUMENT_TYPES_PRODUCTION', 'The production document catalog is incomplete.', array('count' => $productionIssuedEnabled));
                }
            }

            $inboxStatusDrift = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM facto_inbox_documents WHERE environment='production' AND COALESCE(icylab_reception_status,'') <> CASE CAST(inbox_document_status AS CHAR) WHEN '1' THEN 'pending' WHEN '2' THEN 'approved' WHEN '3' THEN 'approved_with_observations' WHEN '4' THEN 'rejected' WHEN '5' THEN 'merchandise_received' ELSE COALESCE(icylab_reception_status,'') END");
            if ($inboxStatusDrift === 0) {
                preflight_add($report, 'PASS', 'INBOX_STATUS_RECONCILIATION', 'Local reception status matches FACTO for production inbox documents.');
            } else {
                preflight_add($report, 'FAIL', 'INBOX_STATUS_RECONCILIATION', 'Local reception status is not reconciled with FACTO.', array('count'=>$inboxStatusDrift));
            }

            $pendingOperations = array(
                'billing_issue_logs' => "SELECT COUNT(*) FROM billing_issue_logs WHERE environment='production' AND status IN ('sent','pending_reconcile')",
                'billing_manual_operations' => "SELECT COUNT(*) FROM billing_manual_operations WHERE environment='production' AND status IN ('issuing','pending_local_commit','remote_uncertain')",
                'billing_export_operations' => "SELECT COUNT(*) FROM billing_export_operations WHERE environment='production' AND status IN ('issuing','pending_local_commit','remote_uncertain')",
                'billing_tax_operations' => "SELECT COUNT(*) FROM billing_tax_operations WHERE environment='production' AND status IN ('issuing','pending_local_commit','remote_uncertain')",
                'billing_complementary_operations' => "SELECT COUNT(*) FROM billing_complementary_operations WHERE environment='production' AND status IN ('sent','pending_reconcile')",
                'billing_dispatch_guides' => "SELECT COUNT(*) FROM billing_dispatch_guides WHERE environment='production' AND status IN ('issuing','pending_local_commit','remote_uncertain')",
            );
            $pendingCounts = array();
            foreach ($pendingOperations as $table => $sql) {
                $count = (int) preflight_scalar($pdo, $sql);
                if ($count > 0) {
                    $pendingCounts[$table] = $count;
                }
            }
            if (empty($pendingCounts)) {
                preflight_add($report, 'PASS', 'PENDING_REMOTE_OPERATIONS', 'No unresolved production issuance operation is pending reconciliation.');
            } else {
                preflight_add($report, 'FAIL', 'PENDING_REMOTE_OPERATIONS', 'Production operations require reconciliation before rollout.', array('counts' => $pendingCounts));
            }

            $inconsistentBalances = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_collections WHERE environment='production' AND ABS(balance_amount - (total_amount - paid_amount)) > 0.01");
            $negativeBalances = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_collections WHERE environment='production' AND (total_amount < 0 OR paid_amount < 0 OR balance_amount < -0.01)");
            if ($inconsistentBalances === 0 && $negativeBalances === 0) {
                preflight_add($report, 'PASS', 'COLLECTION_BALANCES', 'Production collection balances are arithmetically consistent and non-negative.');
            } else {
                preflight_add($report, 'FAIL', 'COLLECTION_BALANCES', 'Production collection balances contain inconsistencies.', array('inconsistent' => $inconsistentBalances, 'negative' => $negativeBalances));
            }

            $duplicatePayables = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM (SELECT inbox_document_id FROM billing_collections WHERE environment='production' AND direction='payable' AND inbox_document_id IS NOT NULL AND inbox_document_id<>'' GROUP BY inbox_document_id HAVING COUNT(*)>1) d");
            if ($duplicatePayables === 0) {
                preflight_add($report, 'PASS', 'DUPLICATE_PAYABLES', 'No duplicate production payable collection was found by inbox document ID.');
            } else {
                preflight_add($report, 'FAIL', 'DUPLICATE_PAYABLES', 'Duplicate production payable collections exist.', array('groups' => $duplicatePayables));
            }

            $orphanPayments = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_payments p LEFT JOIN billing_collections c ON c.id=p.collection_id WHERE p.environment='production' AND p.collection_id IS NOT NULL AND c.id IS NULL");
            $paymentMismatches = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM (SELECT c.id, c.paid_amount, COALESCE(SUM(p.amount),0) AS payment_sum FROM billing_collections c LEFT JOIN billing_payments p ON p.collection_id=c.id AND p.environment=c.environment WHERE c.environment='production' GROUP BY c.id, c.paid_amount HAVING ABS(c.paid_amount-payment_sum)>0.01) x");
            if ($orphanPayments === 0 && $paymentMismatches === 0) {
                preflight_add($report, 'PASS', 'PAYMENT_RECONCILIATION', 'Production payments reconcile with collections and have no orphan references.');
            } else {
                preflight_add($report, 'FAIL', 'PAYMENT_RECONCILIATION', 'Production payment reconciliation failed.', array('orphan_payments' => $orphanPayments, 'amount_mismatches' => $paymentMismatches));
            }

            $invalidCenters = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_collections c LEFT JOIN finance_centers f ON f.id=c.finance_center_id WHERE c.environment='production' AND c.finance_center_id IS NOT NULL AND f.id IS NULL");
            $openWithoutCenter = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_collections WHERE environment='production' AND balance_amount>0.01 AND collection_status NOT IN ('pagado','anulado','rechazado') AND finance_center_id IS NULL");
            if ($invalidCenters === 0) {
                preflight_add($report, 'PASS', 'FINANCE_CENTER_REFERENCES', 'No invalid production finance-center references were found.');
            } else {
                preflight_add($report, 'FAIL', 'FINANCE_CENTER_REFERENCES', 'Invalid production finance-center references exist.', array('count' => $invalidCenters));
            }
            if ($openWithoutCenter === 0) {
                preflight_add($report, 'PASS', 'FINANCE_CENTER_COVERAGE', 'All open production balances have a finance center.');
            } else {
                preflight_add($report, 'WARN', 'FINANCE_CENTER_COVERAGE', 'Open production balances without a finance center exist.', array('count' => $openWithoutCenter));
            }

            $unresolvedSuppliers = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM facto_inbox_documents WHERE environment='production' AND supplier_resolution_status IN ('pending','missing_rut','error')");
            if ($unresolvedSuppliers === 0) {
                preflight_add($report, 'PASS', 'SUPPLIER_RESOLUTION', 'All production inbox suppliers are resolved.');
            } else {
                preflight_add($report, 'WARN', 'SUPPLIER_RESOLUTION', 'Production inbox documents still have unresolved suppliers.', array('count' => $unresolvedSuppliers));
            }

            $missingCounterparties = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_collections WHERE environment='production' AND balance_amount>0.01 AND collection_status NOT IN ('pagado','anulado','rechazado') AND customer_id IS NULL");
            if ($missingCounterparties === 0) {
                preflight_add($report, 'PASS', 'COUNTERPARTY_COVERAGE', 'All open production balances have a linked counterparty.');
            } else {
                preflight_add($report, 'FAIL', 'COUNTERPARTY_COVERAGE', 'Open production balances without a linked customer/supplier exist.', array('count' => $missingCounterparties));
            }

            $duplicateNormalizedTaxIds = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM (SELECT UPPER(REPLACE(REPLACE(REPLACE(TRIM(customer_tax_id),'.',''),'-',''),' ','')) AS normalized_tax_id FROM crm_customers WHERE deleted_at IS NULL AND customer_tax_id IS NOT NULL AND TRIM(customer_tax_id)<>'' GROUP BY normalized_tax_id HAVING COUNT(*)>1) x");
            if ($duplicateNormalizedTaxIds === 0) {
                preflight_add($report, 'PASS', 'COUNTERPARTY_TAX_ID_UNIQUENESS', 'No duplicate normalized active customer/supplier tax ID was found.');
            } else {
                preflight_add($report, 'FAIL', 'COUNTERPARTY_TAX_ID_UNIQUENESS', 'Duplicate normalized active customer/supplier tax IDs exist.', array('groups' => $duplicateNormalizedTaxIds));
            }

            $criticalReconciliation = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_reconciliation_issues WHERE status='open' AND severity='critical'");
            $warningReconciliation = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM billing_reconciliation_issues WHERE status='open' AND severity='warning'");
            if ($criticalReconciliation === 0) {
                preflight_add($report, 'PASS', 'RECONCILIATION_CRITICAL', 'No critical reconciliation issue is open.');
            } else {
                preflight_add($report, 'FAIL', 'RECONCILIATION_CRITICAL', 'Critical reconciliation issues remain open.', array('count' => $criticalReconciliation));
            }
            if ($warningReconciliation > 0) {
                preflight_add($report, 'WARN', 'RECONCILIATION_WARNINGS', 'Warning-level reconciliation issues remain open.', array('count' => $warningReconciliation));
            } else {
                preflight_add($report, 'PASS', 'RECONCILIATION_WARNINGS', 'No warning-level reconciliation issue is open.');
            }

            $tokenRows = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM facto_tokens WHERE environment='production'");
            $validTokenRows = (int) preflight_scalar($pdo, "SELECT COUNT(*) FROM facto_tokens WHERE environment='production' AND expires_at>NOW()");
            preflight_add($report, 'INFO', 'FACTO_TOKEN_STATE', 'Production FACTO token metadata was inspected without exposing token values.', array('rows' => $tokenRows, 'currently_valid_rows' => $validTokenRows));

            $productionPermissionRoles = (int) preflight_scalar($pdo, "SELECT COUNT(DISTINCT role) FROM role_permissions WHERE permission_key='billing.issue.production'");
            preflight_add($report, $productionPermissionRoles > 2 ? 'WARN' : 'INFO', 'PRODUCTION_PERMISSION_SCOPE', 'Roles with billing.issue.production were counted.', array('role_count' => $productionPermissionRoles));
        }
    } catch (Throwable $e) {
        preflight_add($report, 'FAIL', 'DATABASE_CONNECTION', 'Database checks could not be completed. Review credentials, connectivity, and schema.');
    }
}

// Summary and exit status.
$summary = array('PASS' => 0, 'INFO' => 0, 'WARN' => 0, 'FAIL' => 0);
foreach ($report['checks'] as $check) {
    $summary[$check['level']]++;
}
$report['summary'] = $summary;
$report['result'] = $summary['FAIL'] > 0 || ($strict && $summary['WARN'] > 0) ? 'FAIL' : ($summary['WARN'] > 0 ? 'WARN' : 'PASS');

$line = str_repeat('=', 76) . PHP_EOL;
echo $line;
echo "ICYLAB Billing production preflight v5\n";
echo "Environment: production | Stage: " . $stage . " | Strict: " . ($strict ? 'yes' : 'no') . "\n";
echo $line;
foreach ($report['checks'] as $check) {
    echo '[' . str_pad($check['level'], 4) . '] ' . str_pad($check['code'], 36) . ' ' . $check['message'] . PHP_EOL;
    if (!empty($check['context'])) {
        echo '       ' . json_encode($check['context'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
    }
}
echo $line;
echo 'Summary: PASS=' . $summary['PASS'] . ' INFO=' . $summary['INFO'] . ' WARN=' . $summary['WARN'] . ' FAIL=' . $summary['FAIL'] . PHP_EOL;
echo 'Result: ' . $report['result'] . PHP_EOL;

if ($jsonPath !== '') {
    if (preflight_write_json($jsonPath, $report)) {
        echo 'JSON report: ' . $jsonPath . PHP_EOL;
    } else {
        echo "WARNING: JSON report could not be written.\n";
        $report['result'] = 'FAIL';
        $summary['FAIL']++;
    }
}

if ($summary['FAIL'] > 0 || ($strict && $summary['WARN'] > 0)) {
    exit(1);
}
if ($summary['WARN'] > 0) {
    exit(2);
}
exit(0);
