<?php

declare(strict_types=1);

require_once DENTAL_ROOT . '/app/production_readiness.php';

function dental_automated_validation_check(string $key, string $title, bool $ready, string $detail, string $metric = ''): array
{
    return [
        'key' => $key,
        'title' => $title,
        'status' => $ready ? 'pass' : 'blocked',
        'ready' => $ready,
        'detail' => $detail,
        'metric' => $metric,
    ];
}

function dental_automated_validation_source_contains(string $relative, array $patterns): bool
{
    $path = DENTAL_ROOT . '/' . ltrim($relative, '/');
    if (!is_file($path)) return false;
    $source = (string)file_get_contents($path);
    foreach ($patterns as $pattern) {
        if (!str_contains($source, (string)$pattern)) return false;
    }
    return true;
}

function dental_automated_validation_staff_separation(PDO $pdo): array
{
    if (!schema_table_exists(table('roles')) || !schema_table_exists(table('permissions')) || !schema_table_exists(table('role_permissions'))) {
        return ['ready' => false, 'detail' => 'Role and permission tables are incomplete.'];
    }

    $staffRole = $pdo->prepare('SELECT id FROM `' . table('roles') . '` WHERE code="staff" LIMIT 1');
    $staffRole->execute();
    $roleId = (int)($staffRole->fetchColumn() ?: 0);
    if ($roleId < 1) return ['ready' => false, 'detail' => 'General Staff role is missing.'];

    $allowedRequired = ['patients.manage', 'appointments.manage', 'queue.manage', 'communications.manage', 'billing.manage', 'cashier.session.manage', 'cashier.statement.print'];
    $forbidden = ['clinical.manage', 'clinical.finalize', 'inventory.manage', 'sterilization.manage', 'users.manage', 'roles.manage', 'settings.manage'];
    $stmt = $pdo->prepare('SELECT p.code,rp.allowed FROM `' . table('role_permissions') . '` rp JOIN `' . table('permissions') . '` p ON p.id=rp.permission_id WHERE rp.role_id=?');
    $stmt->execute([$roleId]);
    $map = [];
    foreach ($stmt->fetchAll() as $row) $map[(string)$row['code']] = (int)$row['allowed'] === 1;

    $missing = array_values(array_filter($allowedRequired, static fn(string $code): bool => empty($map[$code])));
    $excess = array_values(array_filter($forbidden, static fn(string $code): bool => !empty($map[$code])));
    $ready = $missing === [] && $excess === [];
    $detail = $ready
        ? 'General Staff has the combined Receptionist–Cashier lane and remains blocked from clinical, inventory, approval, user, and protected setup authority.'
        : 'General Staff permission boundary needs correction.';
    if ($missing) $detail .= ' Missing: ' . implode(', ', $missing) . '.';
    if ($excess) $detail .= ' Forbidden grants: ' . implode(', ', $excess) . '.';
    return ['ready' => $ready, 'detail' => $detail];
}

function dental_automated_validation_multi_role_isolation(PDO $pdo): array
{
    if (!schema_table_exists(table('roles')) || !schema_table_exists(table('permissions')) || !schema_table_exists(table('role_permissions'))) {
        return ['ready'=>false,'detail'=>'Role and permission tables are incomplete.'];
    }
    $forbidden = [
        'dental_assistant' => ['inventory.manage','inventory.catalog','billing.manage','cashier.session.manage'],
        'receptionist' => ['billing.manage','inventory.manage','clinical.manage','clinical.finalize'],
        'cashier' => ['patients.manage','appointments.manage','clinical.view','inventory.manage'],
        'inventory_officer' => ['patients.manage','appointments.manage','billing.manage','clinical.manage'],
        'staff' => ['inventory.manage','clinical.manage','clinical.finalize','settings.manage','staff.manage'],
    ];
    $violations=[];
    $stmt=$pdo->query('SELECT r.code role_code,p.code permission_code FROM `' . table('role_permissions') . '` rp JOIN `' . table('roles') . '` r ON r.id=rp.role_id JOIN `' . table('permissions') . '` p ON p.id=rp.permission_id WHERE rp.allowed=1');
    foreach($stmt->fetchAll() as $row){
        $role=(string)$row['role_code']; $permission=(string)$row['permission_code'];
        if(in_array($permission,$forbidden[$role]??[],true))$violations[]=$role.' → '.$permission;
    }

    $assignmentIssues=[];
    if(schema_table_exists(table('user_role_assignments'))){
        $expiryFilter=schema_column_exists(table('user_role_assignments'),'expires_at')?' AND (ura.expires_at IS NULL OR ura.expires_at>NOW())':'';
        $rows=$pdo->query('SELECT ura.user_id,ura.assignment_type,r.code FROM `' . table('user_role_assignments') . '` ura JOIN `' . table('roles') . '` r ON r.id=ura.role_id WHERE ura.status="active"'.$expiryFilter.' ORDER BY ura.user_id,FIELD(ura.assignment_type,"primary","secondary"),ura.id')->fetchAll();
        $byUser=[]; foreach($rows as $row)$byUser[(int)$row['user_id']][]=(string)$row['code'];
        $exclusive=['super_admin','cashier','staff'];
        $conflicts=[['dentist','dental_assistant'],['dentist','inventory_officer'],['dental_assistant','inventory_officer']];
        foreach($byUser as $uid=>$codes){
            $codes=array_values(array_unique($codes));
            if(count($codes)>1&&array_intersect($codes,$exclusive))$assignmentIssues[]='user #'.$uid.' has an exclusive role plus another active role';
            foreach($conflicts as [$a,$b])if(in_array($a,$codes,true)&&in_array($b,$codes,true))$assignmentIssues[]='user #'.$uid.' has incompatible '.$a.' + '.$b;
        }
    }

    $assignmentControlReady=schema_table_exists(table('user_role_assignments'))
        && schema_column_exists(table('user_role_assignments'),'expires_at')
        && schema_column_exists(table('user_role_assignments'),'last_switched_at')
        && schema_column_exists(table('users'),'role_context_version');
    $sourceReady=dental_automated_validation_source_contains('app/bootstrap.php',[
        'active_role_permission_hard_denied',
        'user_role_assignment_records',
        'active_role_switch_blockers',
        'role_context_version',
    ]) && dental_automated_validation_source_contains('modules/overview/role_switch.php',[
        "verify_csrf();",
        "active_role_switch_blockers",
        "Active role:",
    ]) && dental_automated_validation_source_contains('app/layout.php',[
        'task-role-switcher',
        'Return to Superuser',
    ]);
    $ready=$violations===[]&&$assignmentIssues===[]&&$sourceReady&&$assignmentControlReady;
    $detail=$ready
        ? 'Ordinary multi-role users are isolated to the selected assigned role. Superuser workspace selection filters navigation without surrendering owner authority. Dental Assistant supplier/inventory access and other high-risk cross-role grants remain blocked.'
        : 'Multi-role isolation or Superuser workspace authority requires correction.';
    if($violations)$detail.=' Forbidden permission grants: '.implode(', ',array_slice($violations,0,8)).'.';
    if($assignmentIssues)$detail.=' Assignment conflicts: '.implode(', ',array_slice($assignmentIssues,0,8)).'.';
    if(!$sourceReady)$detail.=' Active-role hard-deny or POST switch controls are incomplete.';
    if(!$assignmentControlReady)$detail.=' Assignment expiration, last-used tracking, or forced-primary reset schema is incomplete.';
    return ['ready'=>$ready,'detail'=>$detail];
}

function dental_automated_validation_snapshot(PDO $pdo): array
{
    $checks = [];
    $health = dental_production_health_snapshot($pdo);

    // Recovery freshness and actual outbound delivery are live-environment gates.
    // All other blocked health findings remain technical blockers.
    $liveOnly = ['backup_freshness', 'mail_configuration'];
    foreach ((array)($health['checks'] ?? []) as $healthCheck) {
        $key = (string)($healthCheck['key'] ?? '');
        if ($key === '' || in_array($key, $liveOnly, true)) continue;
        $status = (string)($healthCheck['status'] ?? 'blocked');
        $ready = $status !== 'blocked';
        $checks[] = dental_automated_validation_check(
            'health_' . $key,
            (string)($healthCheck['title'] ?? $key),
            $ready,
            (string)($healthCheck['detail'] ?? ''),
            (string)($healthCheck['metric'] ?? '')
        );
    }

    $rolePrerequisites = dental_production_role_prerequisites($pdo);
    foreach (dental_production_role_codes() as $code => $label) {
        $role = (array)($rolePrerequisites[$code] ?? ['ready' => false, 'detail' => 'Role prerequisites could not be verified.']);
        $checks[] = dental_automated_validation_check(
            'role_' . $code,
            $label . ' technical workflow',
            !empty($role['ready']),
            (string)($role['detail'] ?? ''),
            !empty($role['ready']) ? 'Routes and schema ready' : 'Blocked'
        );
    }

    $staffBoundary = dental_automated_validation_staff_separation($pdo);
    $checks[] = dental_automated_validation_check(
        'general_staff_separation',
        'General Staff financial separation',
        !empty($staffBoundary['ready']),
        (string)$staffBoundary['detail'],
        !empty($staffBoundary['ready']) ? 'Controlled hybrid role' : 'Review permissions'
    );

    $multiRole = dental_automated_validation_multi_role_isolation($pdo);
    $checks[] = dental_automated_validation_check(
        'multi_role_isolation',
        'Active-role permission isolation',
        !empty($multiRole['ready']),
        (string)$multiRole['detail'],
        !empty($multiRole['ready']) ? 'One role at a time' : 'Blocked'
    );

    $patientLedgerReady = function_exists('patient_visit_ledger_page')
        || dental_automated_validation_source_contains('app/patient_visit_ledger.php', [
            'finalized',
            'payment_item_allocations',
            'appointment_services',
        ]);
    $patientLedgerPrivacy = dental_automated_validation_source_contains('app/patient_visit_ledger.php', [
        'status IN ("finalized","addendum")',
    ]);
    $checks[] = dental_automated_validation_check(
        'patient_ledger_completeness',
        'Patient lifetime visit and transaction ledger',
        $patientLedgerReady && $patientLedgerPrivacy,
        $patientLedgerReady && $patientLedgerPrivacy
            ? 'The patient ledger includes historical appointments, finalized care, itemized billing, allocations, and returns while excluding draft encounters.'
            : 'The patient ledger source does not prove both lifetime transaction coverage and finalized-only clinical disclosure.',
        $patientLedgerReady && $patientLedgerPrivacy ? 'Finalized-only' : 'Blocked'
    );

    $adaptiveReady = function_exists('dental_adaptive_encounter_schema_ready')
        ? dental_adaptive_encounter_schema_ready()
        : (schema_column_exists(table('clinical_encounters'), 'care_path') && schema_column_exists(table('clinical_encounters'), 'vitals_skip_reason'));
    $checks[] = dental_automated_validation_check(
        'adaptive_encounter_safety',
        'Adaptive encounter safety rules',
        $adaptiveReady && dental_automated_validation_source_contains('app/adaptive_encounter.php', ['focused_dental', 'emergency', 'vitals_skip_reason']),
        'Focused and follow-up visits may skip nonessential sections, while emergency, medical-alert, medication, extraction, surgery, sedation, and other higher-risk care remain protected.',
        $adaptiveReady ? 'Installed' : 'Blocked'
    );

    $backupTable = table('backup_logs');
    $backupStatusReady = false;
    $verificationColumnsReady = false;
    if (schema_table_exists($backupTable)) {
        $stmt = $pdo->prepare('SELECT COLUMN_TYPE FROM information_schema.columns WHERE table_schema=? AND table_name=? AND column_name="status" LIMIT 1');
        $stmt->execute([(string)app_config('database.name'), $backupTable]);
        $columnType = strtolower((string)$stmt->fetchColumn());
        $backupStatusReady = $columnType !== '';
        $verificationColumnsReady = schema_column_exists($backupTable, 'verified_at')
            && schema_column_exists($backupTable, 'verification_evidence_json')
            && schema_column_exists($backupTable, 'verification_file_count')
            && schema_column_exists($backupTable, 'verification_format_version');
    }
    $backupCodeSafe = dental_automated_validation_source_contains('app/Services/SystemBackupService.php', [
        '$sets = [\'verified_at=NOW()\'];',
        'verifyZipBackup',
        'checksum mismatch',
    ]);
    $checks[] = dental_automated_validation_check(
        'backup_verification_integrity',
        'Deep backup verification and status compatibility',
        $backupStatusReady && $verificationColumnsReady && $backupCodeSafe,
        $backupStatusReady && $verificationColumnsReady && $backupCodeSafe
            ? 'Backup verification validates archive contents and records verified_at without writing an invalid status value.'
            : 'Backup verification schema or code is incomplete. Apply v0.8.0.5 before relying on recovery evidence.',
        $backupStatusReady && $verificationColumnsReady && $backupCodeSafe ? 'Deep verification ready' : 'Blocked'
    );

    $updateRunnerReady = schema_table_exists(table('system_update_runs'))
        && schema_table_exists(table('system_migration_ledger'))
        && schema_column_exists(table('system_migration_ledger'), 'attempt_count')
        && schema_column_exists(table('system_migration_ledger'), 'execution_ms')
        && is_file(DENTAL_ROOT . '/app/update_checkpoint.php')
        && dental_automated_validation_source_contains('app/update_checkpoint.php', ['dental_migration_ledger_mark_running', 'checksum mismatch', 'Retry failed migration'])
        && dental_automated_validation_source_contains('modules/administration/system_setup.php', ['0.9.0.0_hardening_foundation', 'setup_apply_v0900', '0.9.0.1_migration_recovery_hardening_tracker', 'setup_apply_v0901']);
    $checks[] = dental_automated_validation_check(
        'interrupted_update_recovery',
        'Interrupted-update recovery',
        $updateRunnerReady,
        $updateRunnerReady
            ? 'Updates run one release step per request, persist checkpoints, use a database lock, record a permanent migration ledger, and do not replay historical releases because a later health diagnostic changed.'
            : 'The resumable update runner or its persistent run table is missing.',
        $updateRunnerReady ? 'Resumable' : 'Blocked'
    );

    $storageModelReady = is_file(DENTAL_ROOT . '/app/storage_health.php')
        && dental_automated_validation_source_contains('app/storage_health.php', [
            'hosting_quota',
            'shared_filesystem',
            'dental_storage_write_probe',
            'Capacity is unknown—not low',
        ])
        && function_exists('dental_storage_health_snapshot');
    $checks[] = dental_automated_validation_check(
        'quota_aware_storage_health',
        'Quota-aware storage health',
        $storageModelReady,
        $storageModelReady
            ? 'DentalCare separates cPanel account quota from shared-filesystem capacity, performs a protected write probe, and treats unavailable quota as unknown rather than low.'
            : 'The quota-aware storage model is missing or incomplete.',
        $storageModelReady ? 'False low-disk warning prevented' : 'Blocked'
    );

    require_once DENTAL_ROOT . '/app/hardening_foundation.php';
    $hardening = dental_hardening_run($pdo);
    $checks[] = dental_automated_validation_check(
        'hardening_foundation',
        'Automated testing, SAST, and CI foundation',
        !empty($hardening['ready']),
        !empty($hardening['ready'])
            ? 'The repository contains executable test files, PHPStan and Semgrep configurations, a CI workflow, deterministic release checks, and no blocked internal source-policy finding. Review findings remain visible in Hardening Center.'
            : (int)($hardening['counts']['blocked'] ?? 0) . ' hardening foundation check(s) are blocked. Open Hardening Center for exact evidence.',
        (int)($hardening['scan']['test_file_count'] ?? 0) . ' test files'
    );

    $packageReady = false;
    try {
        require_once DENTAL_ROOT . '/app/Services/InstallerPackageService.php';
        $packageReady = InstallerPackageService::releaseSafetyReady();
    } catch (Throwable) {
        $packageReady = false;
    }
    $checks[] = dental_automated_validation_check(
        'clean_package',
        'Clean installer package controls',
        $packageReady,
        $packageReady
            ? 'The installer allowlist, protected-path exclusions, manifest, and checksum controls are active.'
            : 'Installer release-safety controls did not pass.',
        $packageReady ? 'Protected' : 'Blocked'
    );

    $blocked = array_values(array_filter($checks, static fn(array $check): bool => empty($check['ready'])));
    $evidence = [
        'version' => app_release_version(),
        'installed' => installed_application_version(),
        'checks' => array_map(static fn(array $check): array => [
            'key' => $check['key'],
            'status' => $check['status'],
            'metric' => $check['metric'],
        ], $checks),
    ];

    return [
        'ready' => $blocked === [],
        'checks' => $checks,
        'blocked' => $blocked,
        'counts' => ['pass' => count($checks) - count($blocked), 'blocked' => count($blocked)],
        'fingerprint' => hash('sha256', json_encode($evidence, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: ''),
        'generated_at' => date('c'),
    ];
}

function dental_automated_validation_record(PDO $pdo): array
{
    $snapshot = dental_automated_validation_snapshot($pdo);
    $user = current_user() ?: [];
    $record = [
        'evidence_id' => bin2hex(random_bytes(12)),
        'version' => app_release_version(),
        'installed_version' => installed_application_version(),
        'run_at' => date('c'),
        'run_by' => (string)($user['name'] ?? $user['email'] ?? 'System'),
        'run_by_user_id' => (int)($user['id'] ?? 0),
        'ready' => !empty($snapshot['ready']),
        'fingerprint' => (string)$snapshot['fingerprint'],
        'blocked' => array_map(static fn(array $row): array => [
            'key' => (string)$row['key'],
            'title' => (string)$row['title'],
            'detail' => (string)$row['detail'],
        ], (array)$snapshot['blocked']),
    ];
    setting_set('production_acceptance', 'automated_validation_evidence', json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    return ['snapshot' => $snapshot, 'record' => $record];
}

function dental_automated_validation_evidence(): array
{
    $raw = (string)setting_get('production_acceptance', 'automated_validation_evidence', '');
    $decoded = $raw !== '' ? json_decode($raw, true) : null;
    return is_array($decoded) ? $decoded : [];
}
