<?php

declare(strict_types=1);

require_once __DIR__ . '/storage_health.php';
require_once __DIR__ . '/query_integrity.php';
require_once __DIR__ . '/data_integrity.php';

/**
 * Operational-health checks are deliberately diagnostic. Only failures that
 * prevent safe request/session/database writes are marked blocked. Optional
 * services, old backups, missing ZIP support, and pending review evidence are
 * warnings and never create an application-wide redirect gate.
 */

function dental_operational_health_table_ready(PDO $pdo): bool
{
    try {
        return schema_table_exists(table('operational_health_snapshots'));
    } catch (Throwable) {
        return false;
    }
}

function dental_operational_health_probe_directory(string $relativePath, string $label): array
{
    $path = DENTAL_ROOT . '/' . ltrim($relativePath, '/');
    if (!is_dir($path) && !@mkdir($path, 0750, true)) {
        return [
            'key' => 'directory_' . preg_replace('/[^a-z0-9]+/', '_', strtolower($label)),
            'label' => $label,
            'status' => 'blocked',
            'metric' => 'Directory unavailable',
            'detail' => $relativePath . ' could not be created.',
            'blocking' => true,
        ];
    }
    if (!is_writable($path)) {
        return [
            'key' => 'directory_' . preg_replace('/[^a-z0-9]+/', '_', strtolower($label)),
            'label' => $label,
            'status' => 'blocked',
            'metric' => 'Not writable',
            'detail' => $relativePath . ' is not writable by PHP.',
            'blocking' => true,
        ];
    }

    $probe = @tempnam($path, 'dc-health-');
    if (!is_string($probe) || $probe === '') {
        return [
            'key' => 'directory_' . preg_replace('/[^a-z0-9]+/', '_', strtolower($label)),
            'label' => $label,
            'status' => 'blocked',
            'metric' => 'Write failed',
            'detail' => 'PHP could not create a temporary file in ' . $relativePath . '.',
            'blocking' => true,
        ];
    }

    $payload = 'DentalCare operational-health write probe ' . date(DATE_ATOM);
    $written = @file_put_contents($probe, $payload, LOCK_EX);
    clearstatcache(true, $probe);
    $valid = $written === strlen($payload) && is_file($probe) && (int)(filesize($probe) ?: 0) === strlen($payload);
    @unlink($probe);

    return [
        'key' => 'directory_' . preg_replace('/[^a-z0-9]+/', '_', strtolower($label)),
        'label' => $label,
        'status' => $valid ? 'pass' : 'blocked',
        'metric' => $valid ? 'Writable' : 'Verification failed',
        'detail' => $valid
            ? $relativePath . ' accepted and removed a protected write probe.'
            : 'A file was created in ' . $relativePath . ' but could not be verified safely.',
        'blocking' => !$valid,
    ];
}

function dental_operational_health_latest_backup(PDO $pdo): array
{
    $table = table('backup_logs');
    try {
        if (!schema_table_exists($table)) {
            return [
                'key' => 'backup_readiness',
                'label' => 'Backup readiness',
                'status' => 'review',
                'metric' => 'Schema unavailable',
                'detail' => 'Apply the current database update before relying on managed backup evidence.',
                'blocking' => false,
                'link' => 'backup-restore',
            ];
        }

        $columns = ['id','file_name','backup_type','created_at'];
        foreach (['verified_at','checksum_sha256','file_size'] as $optional) {
            if (schema_column_exists($table, $optional)) $columns[] = $optional;
        }
        $row = $pdo->query('SELECT `' . implode('`,`', $columns) . '` FROM `' . $table . '` ORDER BY `created_at` DESC,`id` DESC LIMIT 1')->fetch(PDO::FETCH_ASSOC) ?: null;
        if (!$row) {
            return [
                'key' => 'backup_readiness',
                'label' => 'Backup readiness',
                'status' => 'review',
                'metric' => 'No managed backup',
                'detail' => 'Create and verify a complete backup before the next update or production release.',
                'blocking' => false,
                'link' => 'backup-restore',
            ];
        }

        $created = strtotime((string)($row['created_at'] ?? '')) ?: 0;
        $ageDays = $created > 0 ? (int)floor((time() - $created) / 86400) : 99999;
        $verified = trim((string)($row['verified_at'] ?? '')) !== ''
            && trim((string)($row['checksum_sha256'] ?? '')) !== '';
        $fileName = basename((string)($row['file_name'] ?? ''));
        $backupRoot = function_exists('backup_directory') ? backup_directory() : (DENTAL_ROOT . '/storage/backups');
        $fileExists = $fileName !== '' && is_file(rtrim($backupRoot, '/\\') . DIRECTORY_SEPARATOR . $fileName);
        $complete = strtolower((string)($row['backup_type'] ?? '')) === 'complete';
        $status = ($verified && $fileExists && $complete && $ageDays <= 7) ? 'pass' : 'review';
        $parts = [];
        $parts[] = $complete ? 'complete backup' : ((string)($row['backup_type'] ?? 'database') . ' backup');
        $parts[] = $verified ? 'checksum verified' : 'not verified';
        $parts[] = $fileExists ? 'file present' : 'file missing';
        $parts[] = $ageDays . ' day' . ($ageDays === 1 ? '' : 's') . ' old';

        return [
            'key' => 'backup_readiness',
            'label' => 'Backup readiness',
            'status' => $status,
            'metric' => $status === 'pass' ? 'Current and verified' : 'Review required',
            'detail' => implode(' · ', $parts) . '.',
            'blocking' => false,
            'link' => 'backup-restore',
        ];
    } catch (Throwable $exception) {
        return [
            'key' => 'backup_readiness',
            'label' => 'Backup readiness',
            'status' => 'review',
            'metric' => 'Check unavailable',
            'detail' => 'Backup evidence could not be read: ' . mb_substr($exception->getMessage(), 0, 180),
            'blocking' => false,
            'link' => 'backup-restore',
        ];
    }
}

function dental_operational_health_extension_checks(): array
{
    $required = [
        'pdo_mysql' => true,
        'openssl' => true,
        'mbstring' => true,
        'json' => true,
        'fileinfo' => true,
        'curl' => false,
        'zip' => false,
    ];
    $checks = [];
    foreach ($required as $extension => $blocking) {
        $loaded = extension_loaded($extension);
        $checks[] = [
            'key' => 'extension_' . $extension,
            'label' => 'PHP extension: ' . $extension,
            'status' => $loaded ? 'pass' : ($blocking ? 'blocked' : 'review'),
            'metric' => $loaded ? 'Loaded' : 'Missing',
            'detail' => $loaded
                ? 'The extension is available.'
                : ($blocking
                    ? 'This extension is required for safe application operation.'
                    : 'This optional extension is needed only for related integrations or ZIP backup packaging.'),
            'blocking' => !$loaded && $blocking,
        ];
    }
    return $checks;
}

function dental_operational_health_cleanup_inventory(): array
{
    $targets = [
        ['label'=>'Expired session candidates','path'=>DENTAL_ROOT . '/storage/runtime/sessions','older_than'=>2 * 86400,'pattern'=>'/^sess_/'],
        ['label'=>'Old runtime logs','path'=>DENTAL_ROOT . '/storage/runtime/logs','older_than'=>30 * 86400,'pattern'=>'/\.(?:log|jsonl)$/i'],
        ['label'=>'Old generated installers','path'=>DENTAL_ROOT . '/storage/generated-installers','older_than'=>30 * 86400,'pattern'=>'/\.(?:zip|json)$/i'],
    ];
    $rows = [];
    foreach ($targets as $target) {
        $count = 0;
        $bytes = 0;
        $deadline = microtime(true) + 0.25;
        if (is_dir($target['path'])) {
            try {
                foreach (new DirectoryIterator($target['path']) as $item) {
                    if (microtime(true) > $deadline) break;
                    if (!$item->isFile() || $item->isLink()) continue;
                    if (!preg_match($target['pattern'], $item->getFilename())) continue;
                    if ($item->getMTime() > time() - $target['older_than']) continue;
                    $count++;
                    $bytes += max(0, (int)$item->getSize());
                }
            } catch (Throwable) {
                // Inventory is informational and must never break the page.
            }
        }
        $rows[] = ['label'=>$target['label'],'count'=>$count,'bytes'=>$bytes];
    }
    return $rows;
}

function dental_operational_health_snapshot(PDO $pdo): array
{
    $checks = [];

    try {
        $dbReady = (int)$pdo->query('SELECT 1')->fetchColumn() === 1;
        $checks[] = [
            'key'=>'database_connection','label'=>'Database connection','status'=>$dbReady ? 'pass' : 'blocked',
            'metric'=>$dbReady ? 'Connected' : 'Failed','detail'=>$dbReady
                ? 'DentalCare completed a read query against the configured database.'
                : 'The database did not return a valid response.','blocking'=>!$dbReady,
        ];
    } catch (Throwable $exception) {
        $checks[] = [
            'key'=>'database_connection','label'=>'Database connection','status'=>'blocked','metric'=>'Connection failed',
            'detail'=>'Database check failed: ' . mb_substr($exception->getMessage(), 0, 180),'blocking'=>true,
        ];
    }

    $checks[] = dental_operational_health_probe_directory('storage/runtime/sessions', 'Session storage');
    $checks[] = dental_operational_health_probe_directory('storage/runtime', 'Runtime storage');
    $checks[] = dental_operational_health_probe_directory('storage/private', 'Protected patient files');
    $checks[] = dental_operational_health_probe_directory('storage/backups', 'Backup storage');

    $storage = dental_storage_health_snapshot();
    $checks[] = [
        'key'=>'storage_capacity','label'=>'Hosting storage capacity',
        'status'=>(string)($storage['status'] ?? 'review'),
        'metric'=>(string)($storage['metric'] ?? 'Unavailable'),
        'detail'=>(string)($storage['detail'] ?? 'Storage capacity could not be measured.'),
        'blocking'=>(string)($storage['status'] ?? '') === 'blocked',
        'link'=>'production-monitoring#storage-health',
    ];

    $checks[] = dental_operational_health_latest_backup($pdo);

    $release = app_release_version();
    $installed = installed_application_version();
    $pending = false;
    try {
        $pending = app_update_pending();
    } catch (Throwable) {
        $pending = version_compare($installed, $release, '<');
    }
    $checks[] = [
        'key'=>'release_alignment','label'=>'Application release alignment',
        'status'=>$pending ? 'review' : 'pass',
        'metric'=>$installed . ' installed / ' . $release . ' files',
        'detail'=>$pending
            ? 'A database update or release-state reconciliation is pending. This warning does not block normal staff pages.'
            : 'Application files and the exact current-release migration ledger are aligned.',
        'blocking'=>false,
        'link'=>'system-setup?tab=updates',
    ];

    foreach (dental_operational_health_extension_checks() as $check) $checks[] = $check;
    $checks[] = dental_query_integrity_health_check($pdo);
    $checks[] = dental_data_integrity_health_check($pdo);

    $blocked = count(array_filter($checks, static fn(array $row): bool => ($row['status'] ?? '') === 'blocked'));
    $review = count(array_filter($checks, static fn(array $row): bool => ($row['status'] ?? '') === 'review'));
    $pass = count(array_filter($checks, static fn(array $row): bool => ($row['status'] ?? '') === 'pass'));
    $overall = $blocked > 0 ? 'blocked' : ($review > 0 ? 'review' : 'pass');

    $canonical = [
        'release'=>$release,
        'installed'=>$installed,
        'overall'=>$overall,
        'checks'=>array_map(static function(array $row): array {
            return [
                'key'=>(string)($row['key'] ?? ''),
                'status'=>(string)($row['status'] ?? ''),
                'metric'=>(string)($row['metric'] ?? ''),
                'blocking'=>!empty($row['blocking']),
            ];
        }, $checks),
    ];
    $json = json_encode($canonical, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $fingerprint = is_string($json) ? hash('sha256', $json) : '';

    return [
        'release'=>$release,
        'installed'=>$installed,
        'overall'=>$overall,
        'counts'=>['pass'=>$pass,'review'=>$review,'blocked'=>$blocked,'total'=>count($checks)],
        'checks'=>$checks,
        'cleanup_inventory'=>dental_operational_health_cleanup_inventory(),
        'fingerprint'=>$fingerprint,
        'generated_at'=>date(DATE_ATOM),
    ];
}

function dental_operational_health_record_snapshot(PDO $pdo, array $snapshot): int
{
    if (!dental_operational_health_table_ready($pdo)) {
        throw new RuntimeException('Operational-health schema is not ready. Apply the current system update first.');
    }
    $encoded = json_encode($snapshot, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    if (!is_string($encoded) || $encoded === '') {
        throw new RuntimeException('The operational-health snapshot could not be encoded.');
    }
    $user = current_user() ?: [];
    $stmt = $pdo->prepare('INSERT INTO `' . table('operational_health_snapshots') . '` (`release_version`,`installed_version`,`overall_status`,`pass_count`,`review_count`,`blocked_count`,`fingerprint_sha256`,`snapshot_json`,`created_by_user_id`,`created_at`) VALUES (?,?,?,?,?,?,?,?,?,NOW())');
    $stmt->execute([
        (string)($snapshot['release'] ?? ''),
        (string)($snapshot['installed'] ?? ''),
        (string)($snapshot['overall'] ?? 'review'),
        (int)($snapshot['counts']['pass'] ?? 0),
        (int)($snapshot['counts']['review'] ?? 0),
        (int)($snapshot['counts']['blocked'] ?? 0),
        (string)($snapshot['fingerprint'] ?? ''),
        $encoded,
        (int)($user['id'] ?? 0) ?: null,
    ]);
    return (int)$pdo->lastInsertId();
}

function dental_operational_health_recent_snapshots(PDO $pdo, int $limit = 20): array
{
    if (!dental_operational_health_table_ready($pdo)) return [];
    $limit = max(1, min(100, $limit));
    try {
        return $pdo->query('SELECT s.*,u.name AS creator_name,u.email AS creator_email FROM `' . table('operational_health_snapshots') . '` s LEFT JOIN `' . table('users') . '` u ON u.id=s.created_by_user_id ORDER BY s.id DESC LIMIT ' . $limit)->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable) {
        return [];
    }
}
