<?php

declare(strict_types=1);

/**
 * DentalCare v0.5.9.8.43 security hardening helpers.
 * Runtime protections are intentionally fail-open when the new security tables
 * are not installed yet, so Superuser can still sign in and apply the update.
 */

function dental_security_table(): string
{
    return table('security_login_attempts');
}

function dental_security_client_ip(): string
{
    $candidates = [
        (string) ($_SERVER['HTTP_CF_CONNECTING_IP'] ?? ''),
        (string) ($_SERVER['HTTP_X_REAL_IP'] ?? ''),
        (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
    ];
    foreach ($candidates as $candidate) {
        $candidate = trim(explode(',', $candidate)[0]);
        if ($candidate !== '' && strlen($candidate) <= 64) return $candidate;
    }
    return 'unknown';
}

function dental_security_user_agent(): string
{
    return substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 500);
}

function dental_security_email_hash(string $email): string
{
    return hash('sha256', strtolower(trim($email)));
}

function dental_security_session_hash(): string
{
    $id = session_id();
    return $id !== '' ? hash('sha256', $id) : '';
}

function dental_security_schema_ready(?PDO $pdo = null): bool
{
    try {
        $pdo ??= db();
        $checks = [
            [table('security_login_attempts'), ['email_hash','ip_address','success','failure_reason','created_at']],
            [table('user_sessions'), ['user_id','session_id_hash','last_seen_at','revoked_at','revoke_reason']],
            [table('security_events'), ['event_type','severity','metadata_json','created_at']],
            [table('privileged_action_confirmations'), ['user_id','action_key','confirmed_at','expires_at']],
        ];
        foreach ($checks as [$table, $columns]) {
            if (!schema_table_exists((string) $table)) return false;
            foreach ($columns as $column) {
                if (!schema_column_exists((string) $table, (string) $column)) return false;
            }
        }
        return true;
    } catch (Throwable) {
        return false;
    }
}

function dental_security_setting_int(string $key, int $default, int $min, int $max): int
{
    try {
        $value = (int) setting_get('security', $key, (string) $default);
        return max($min, min($max, $value));
    } catch (Throwable) {
        return $default;
    }
}

function dental_security_enabled(string $key, bool $default = true): bool
{
    try {
        $value = (string) setting_get('security', $key, $default ? '1' : '0');
        return in_array(strtolower($value), ['1','true','yes','on'], true);
    } catch (Throwable) {
        return $default;
    }
}

function dental_security_record_event(string $eventType, string $severity = 'info', array $metadata = [], ?int $userId = null, string $entityType = '', int|string|null $entityId = null): void
{
    if (app_demo_read_only()) return;
    if (!dental_security_schema_ready()) return;
    try {
        $severity = in_array($severity, ['info','warning','critical'], true) ? $severity : 'info';
        $stmt = db()->prepare(
            'INSERT INTO `' . table('security_events') . '` (user_id,event_type,severity,entity_type,entity_id,metadata_json,ip_address,user_agent,created_at) VALUES (?,?,?,?,?,?,?, ?,NOW())'
        );
        $stmt->execute([
            $userId ?: (int) ($_SESSION['user']['id'] ?? 0) ?: null,
            substr($eventType, 0, 80),
            $severity,
            $entityType !== '' ? substr($entityType, 0, 80) : null,
            $entityId !== null ? (string) $entityId : null,
            $metadata ? json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
            dental_security_client_ip(),
            dental_security_user_agent(),
        ]);
    } catch (Throwable $exception) {
        error_log('DentalCare security event logging failed: ' . $exception->getMessage());
    }
}

function dental_security_login_throttle_status(string $email): array
{
    $email = strtolower(trim($email));
    if ($email === '' || !dental_security_enabled('login_throttle_enabled', true) || !dental_security_schema_ready()) {
        return ['blocked' => false, 'remaining_seconds' => 0, 'failed_count' => 0];
    }
    try {
        $maxFailures = dental_security_setting_int('login_max_failures', 5, 3, 20);
        $windowMinutes = dental_security_setting_int('login_failure_window_minutes', 15, 5, 120);
        $lockoutMinutes = dental_security_setting_int('login_lockout_minutes', 15, 5, 240);
        $emailHash = dental_security_email_hash($email);
        $ip = dental_security_client_ip();
        $stmt = db()->prepare(
            'SELECT COUNT(*) AS failed_count, MAX(created_at) AS last_failed_at FROM `' . table('security_login_attempts') . '` '
            . 'WHERE success=0 AND created_at >= DATE_SUB(NOW(), INTERVAL ? MINUTE) AND (email_hash=? OR ip_address=?)'
        );
        $stmt->execute([$windowMinutes, $emailHash, $ip]);
        $row = $stmt->fetch() ?: [];
        $failedCount = (int) ($row['failed_count'] ?? 0);
        $lastFailed = strtotime((string) ($row['last_failed_at'] ?? '')) ?: 0;
        $remaining = $lastFailed > 0 ? max(0, ($lastFailed + ($lockoutMinutes * 60)) - time()) : 0;
        return [
            'blocked' => $failedCount >= $maxFailures && $remaining > 0,
            'remaining_seconds' => $remaining,
            'failed_count' => $failedCount,
        ];
    } catch (Throwable) {
        return ['blocked' => false, 'remaining_seconds' => 0, 'failed_count' => 0];
    }
}

function dental_security_record_login_attempt(string $email, bool $success, ?int $userId = null, string $failureReason = ''): void
{
    if (app_demo_read_only() || !dental_security_schema_ready()) return;
    try {
        $email = strtolower(trim($email));
        $stmt = db()->prepare(
            'INSERT INTO `' . table('security_login_attempts') . '` (user_id,email,email_hash,ip_address,user_agent,success,failure_reason,created_at) VALUES (?,?,?,?,?,?,?,NOW())'
        );
        $stmt->execute([
            $userId,
            $email !== '' ? $email : null,
            $email !== '' ? dental_security_email_hash($email) : null,
            dental_security_client_ip(),
            dental_security_user_agent(),
            $success ? 1 : 0,
            $success ? null : substr($failureReason !== '' ? $failureReason : 'invalid_credentials', 0, 120),
        ]);
        dental_security_record_event($success ? 'login_success' : 'login_failure', $success ? 'info' : 'warning', ['email' => $email !== '' ? $email : null, 'reason' => $failureReason], $userId);
    } catch (Throwable $exception) {
        error_log('DentalCare login attempt logging failed: ' . $exception->getMessage());
    }
}

function dental_security_track_current_session(array $user): void
{
    if (app_demo_read_only() || !dental_security_enabled('track_staff_sessions', true) || !dental_security_schema_ready()) return;
    $hash = dental_security_session_hash();
    if ($hash === '' || empty($user['id'])) return;
    try {
        $minutes = max(5, min(720, (int) setting_get('security', 'session_timeout_minutes', '30')));
        $stmt = db()->prepare(
            'INSERT INTO `' . table('user_sessions') . '` (user_id,session_id_hash,ip_address,user_agent,created_at,last_seen_at,expires_at) VALUES (?,?,?,?,NOW(),NOW(),DATE_ADD(NOW(), INTERVAL ? MINUTE)) '
            . 'ON DUPLICATE KEY UPDATE user_id=VALUES(user_id), ip_address=VALUES(ip_address), user_agent=VALUES(user_agent), last_seen_at=NOW(), expires_at=VALUES(expires_at), logged_out_at=NULL'
        );
        $stmt->execute([(int) $user['id'], $hash, dental_security_client_ip(), dental_security_user_agent(), $minutes]);
    } catch (Throwable $exception) {
        error_log('DentalCare session tracking failed: ' . $exception->getMessage());
    }
}

function dental_security_touch_current_session(): void
{
    if (!dental_security_enabled('track_staff_sessions', true) || !dental_security_schema_ready()) return;
    $hash = dental_security_session_hash();
    if ($hash === '') return;
    $lastTouch = (int) ($_SESSION['security_last_touch'] ?? 0);
    if (time() - $lastTouch < 60) return;
    $_SESSION['security_last_touch'] = time();
    try {
        $minutes = max(5, min(720, (int)setting_get('security', 'session_timeout_minutes', '30')));
        db()->prepare('UPDATE `' . table('user_sessions') . '` SET last_seen_at=NOW(),expires_at=DATE_ADD(NOW(),INTERVAL ? MINUTE) WHERE session_id_hash=? AND logged_out_at IS NULL AND revoked_at IS NULL')
            ->execute([$minutes,$hash]);
    } catch (Throwable) {
        // Session tracking must not interrupt clinical work.
    }
}

function dental_security_session_is_revoked(): bool
{
    if (!dental_security_enabled('track_staff_sessions', true) || !dental_security_schema_ready()) return false;
    $hash = dental_security_session_hash();
    if ($hash === '') return false;
    try {
        $stmt = db()->prepare('SELECT CASE WHEN revoked_at IS NOT NULL OR logged_out_at IS NOT NULL OR (expires_at IS NOT NULL AND expires_at<NOW()) THEN 1 ELSE 0 END FROM `' . table('user_sessions') . '` WHERE session_id_hash=? LIMIT 1');
        $stmt->execute([$hash]);
        return (int)$stmt->fetchColumn() === 1;
    } catch (Throwable) {
        return false;
    }
}

function dental_security_end_current_session(string $reason = 'logout'): void
{
    if (!dental_security_schema_ready()) return;
    $hash = dental_security_session_hash();
    if ($hash === '') return;
    try {
        db()->prepare('UPDATE `' . table('user_sessions') . '` SET logged_out_at=NOW(), revoke_reason=COALESCE(revoke_reason,?) WHERE session_id_hash=?')
            ->execute([$reason, $hash]);
    } catch (Throwable) {
        // Ignore during logout.
    }
}

function dental_security_revoke_user_sessions(int $userId, string $reason = 'revoked_by_admin', bool $exceptCurrent = true): int
{
    if ($userId <= 0 || !dental_security_schema_ready()) return 0;
    try {
        $sql = 'UPDATE `' . table('user_sessions') . '` SET revoked_at=NOW(), revoke_reason=? WHERE user_id=? AND revoked_at IS NULL AND logged_out_at IS NULL';
        $args = [$reason, $userId];
        if ($exceptCurrent) {
            $hash = dental_security_session_hash();
            if ($hash !== '') {
                $sql .= ' AND session_id_hash<>?';
                $args[] = $hash;
            }
        }
        $stmt = db()->prepare($sql);
        $stmt->execute($args);
        return $stmt->rowCount();
    } catch (Throwable) {
        return 0;
    }
}

function dental_security_privileged_role(string $role): bool
{
    return in_array($role, ['super_admin','clinic_admin','dentist','cashier','staff'], true);
}

function dental_security_fresh_confirmation_valid(string $actionKey): bool
{
    if (!current_user() || !dental_security_schema_ready()) return false;
    try {
        $stmt = db()->prepare('SELECT COUNT(*) FROM `' . table('privileged_action_confirmations') . '` WHERE user_id=? AND action_key=? AND expires_at > NOW()');
        $stmt->execute([(int) current_user()['id'], substr($actionKey, 0, 120)]);
        return (int) $stmt->fetchColumn() > 0;
    } catch (Throwable) {
        return false;
    }
}

function dental_security_confirm_privileged_action(string $actionKey, string $password, string $reason = ''): void
{
    $user = current_user();
    if (!$user) throw new RuntimeException('Sign in again before confirming this action.');
    if (!dental_security_schema_ready()) throw new RuntimeException('Security confirmation is not ready. Ask the Superuser to apply pending updates.');
    $stmt = db()->prepare('SELECT password_hash FROM `' . table('users') . '` WHERE id=? LIMIT 1');
    $stmt->execute([(int) $user['id']]);
    $hash = (string) ($stmt->fetchColumn() ?: '');
    if ($hash === '' || !password_verify($password, $hash)) {
        dental_security_record_event('privileged_confirmation_failed', 'warning', ['action' => $actionKey], (int) $user['id']);
        throw new RuntimeException('Password confirmation failed.');
    }
    $minutes = dental_security_setting_int('privileged_action_confirmation_minutes', 10, 1, 60);
    db()->prepare(
        'INSERT INTO `' . table('privileged_action_confirmations') . '` (user_id,action_key,reason,confirmed_at,expires_at,ip_address,user_agent) VALUES (?,?,?,NOW(),DATE_ADD(NOW(), INTERVAL ? MINUTE),?,?)'
    )->execute([(int) $user['id'], substr($actionKey, 0, 120), $reason !== '' ? substr($reason, 0, 500) : null, $minutes, dental_security_client_ip(), dental_security_user_agent()]);
    dental_security_record_event('privileged_confirmation_success', 'info', ['action' => $actionKey, 'minutes' => $minutes], (int) $user['id']);
}

function dental_security_readiness_summary(PDO $pdo): array
{
    $items = [
        'Login throttling table' => dental_update_table_exists($pdo, table('security_login_attempts')),
        'Session/device tracking table' => dental_update_table_exists($pdo, table('user_sessions')),
        'Security event log table' => dental_update_table_exists($pdo, table('security_events')),
        'Privileged confirmation table' => dental_update_table_exists($pdo, table('privileged_action_confirmations')),
        'Login throttle setting' => setting_get('security','login_throttle_enabled',null) !== null,
        'Session tracking setting' => setting_get('security','track_staff_sessions',null) !== null,
    ];
    $missing = [];
    foreach ($items as $label => $ok) if (!$ok) $missing[] = $label;
    return ['ready' => !$missing, 'missing' => $missing, 'items' => $items];
}

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