<?php
declare(strict_types=1);

function csrf_token(string $scope = 'default'): string
{
    $scope = preg_replace('/[^a-z0-9_-]+/i', '', trim($scope));
    $scope = $scope !== '' ? strtolower($scope) : 'default';

    if (!isset($_SESSION['csrf_tokens']) || !is_array($_SESSION['csrf_tokens'])) {
        $_SESSION['csrf_tokens'] = array();
    }

    $token = (string) ($_SESSION['csrf_tokens'][$scope] ?? '');
    if (strlen($token) >= 32) {
        return $token;
    }

    try {
        $token = bin2hex(random_bytes(32));
    } catch (Throwable $exception) {
        $token = hash('sha256', $scope . '|' . uniqid('', true) . '|' . microtime(true));
    }

    $_SESSION['csrf_tokens'][$scope] = $token;

    return $token;
}

function csrf_input(string $scope = 'default'): string
{
    return '<input type="hidden" name="csrf_token" value="' . e(csrf_token($scope)) . '">';
}

function is_valid_csrf_token(?string $token, string $scope = 'default'): bool
{
    $expected = csrf_token($scope);
    $token = trim((string) $token);

    return $token !== '' && hash_equals($expected, $token);
}

function ensure_post_csrf_or_redirect(string $scope, string $redirectPath, array $oldInput = array()): void
{
    if (is_valid_csrf_token((string) ($_POST['csrf_token'] ?? ''), $scope)) {
        return;
    }

    redirect_with_flash(
        $redirectPath,
        'error',
        localized_flash_message(
            'เซสชันหมดอายุ กรุณาลองใหม่อีกครั้ง',
            'Your session expired. Please try again.'
        ),
        $oldInput
    );
}

function submission_token_scope(string $scope): string
{
    $scope = preg_replace('/[^a-z0-9_-]+/i', '_', trim($scope));
    $scope = trim((string) $scope, '_');

    return $scope !== '' ? strtolower($scope) : 'default';
}

function submission_token(string $scope = 'default'): string
{
    $scope = submission_token_scope($scope);

    if (!isset($_SESSION['submission_tokens']) || !is_array($_SESSION['submission_tokens'])) {
        $_SESSION['submission_tokens'] = array();
    }

    $token = (string) ($_SESSION['submission_tokens'][$scope] ?? '');
    if (strlen($token) >= 32) {
        return $token;
    }

    try {
        $token = bin2hex(random_bytes(32));
    } catch (Throwable $exception) {
        $token = hash('sha256', $scope . '|' . uniqid('', true) . '|' . microtime(true));
    }

    $_SESSION['submission_tokens'][$scope] = $token;

    return $token;
}

function submission_input(string $scope = 'default'): string
{
    return '<input type="hidden" name="submission_token" value="' . e(submission_token($scope)) . '">';
}

function consume_submission_token(?string $token, string $scope = 'default'): bool
{
    $scope = submission_token_scope($scope);
    $token = trim((string) $token);
    $expected = (string) ($_SESSION['submission_tokens'][$scope] ?? '');

    if ($token === '' || $expected === '' || !hash_equals($expected, $token)) {
        return false;
    }

    unset($_SESSION['submission_tokens'][$scope]);

    return true;
}

function ensure_submission_token_or_redirect(string $scope, string $redirectPath, array $oldInput = array()): void
{
    if (consume_submission_token((string) ($_POST['submission_token'] ?? ''), $scope)) {
        return;
    }

    redirect_with_flash(
        $redirectPath,
        'error',
        localized_flash_message(
            'รายการนี้ถูกส่งไปแล้วหรือแบบฟอร์มหมดอายุ กรุณารีเฟรชหน้าแล้วลองใหม่หากต้องการ',
            'This action was already submitted or the form expired. Refresh the page and try again if needed.'
        ),
        $oldInput
    );
}

function payment_slip_access_url(int $paymentId, bool $download = false): string
{
    if ($paymentId <= 0) {
        return '';
    }

    $query = http_build_query(array(
        'payment_id' => $paymentId,
        'download' => $download ? 1 : 0,
    ));

    return app_process_path('view_payment_slip.php') . '?' . $query;
}

function client_ip_address(): string
{
    $candidates = array(
        (string) ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''),
        (string) ($_SERVER['HTTP_CLIENT_IP'] ?? ''),
        (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
    );

    foreach ($candidates as $candidate) {
        $candidate = trim($candidate);
        if ($candidate === '') {
            continue;
        }

        if (str_contains($candidate, ',')) {
            $parts = array_filter(array_map('trim', explode(',', $candidate)));
            $candidate = (string) ($parts[0] ?? '');
        }

        if ($candidate !== '') {
            return substr($candidate, 0, 120);
        }
    }

    return 'unknown';
}

function login_throttle_storage_path(): string
{
    return dirname(__DIR__) . '/storage/settings/login_throttle.json';
}

function login_throttle_limits(): array
{
    return array(
        'max_failures' => 5,
        'lockout_seconds' => 900,
        'record_ttl_seconds' => 86400,
    );
}

function login_throttle_message(array $status): string|array
{
    $remainingSeconds = max(1, (int) ($status['remaining_seconds'] ?? 0));
    $remainingMinutes = max(1, (int) ceil($remainingSeconds / 60));

    if ($remainingMinutes === 1) {
        return localized_flash_message(
            'พยายามเข้าสู่ระบบมากเกินไป กรุณาลองใหม่อีกครั้งในอีกประมาณ 1 นาที',
            'Too many login attempts. Try again in about 1 minute.'
        );
    }

    return localized_flash_message(
        'พยายามเข้าสู่ระบบมากเกินไป กรุณาลองใหม่อีกครั้งในอีกประมาณ {minutes} นาที',
        'Too many login attempts. Try again in about {minutes} minutes.',
        array('minutes' => $remainingMinutes)
    );
}

function login_throttle_normalize_username(string $username): string
{
    return strtolower(trim($username));
}

function login_throttle_record_keys(string $username, string $ipAddress): array
{
    $normalizedUsername = login_throttle_normalize_username($username);
    $normalizedIpAddress = strtolower(trim($ipAddress));

    if ($normalizedUsername === '') {
        return array();
    }

    return array(
        'user:' . hash('sha256', $normalizedUsername),
        'user_ip:' . hash('sha256', $normalizedUsername . '|' . $normalizedIpAddress),
    );
}

function login_throttle_default_status(): array
{
    $limits = login_throttle_limits();

    return array(
        'is_locked' => false,
        'remaining_seconds' => 0,
        'failed_count' => 0,
        'remaining_attempts' => (int) $limits['max_failures'],
        'max_failures' => (int) $limits['max_failures'],
    );
}

function login_throttle_load_records($handle): array
{
    rewind($handle);
    $contents = stream_get_contents($handle);

    if (!is_string($contents) || trim($contents) === '') {
        return array();
    }

    $decoded = json_decode($contents, true);

    return is_array($decoded) ? $decoded : array();
}

function login_throttle_prune_records(array $records, int $now): array
{
    $ttlSeconds = (int) (login_throttle_limits()['record_ttl_seconds'] ?? 86400);

    foreach ($records as $key => $record) {
        if (!is_array($record)) {
            unset($records[$key]);
            continue;
        }

        $lockedUntil = (int) ($record['locked_until'] ?? 0);
        $lastAttemptAt = (int) ($record['last_attempt_at'] ?? 0);
        $failedCount = (int) ($record['failed_count'] ?? 0);

        if ($failedCount <= 0 && $lockedUntil <= 0) {
            unset($records[$key]);
            continue;
        }

        if ($lockedUntil > 0 && $lockedUntil > $now) {
            continue;
        }

        if ($lastAttemptAt > 0 && ($now - $lastAttemptAt) <= $ttlSeconds) {
            continue;
        }

        unset($records[$key]);
    }

    return $records;
}

function login_throttle_write_records($handle, array $records): void
{
    rewind($handle);
    ftruncate($handle, 0);
    $encoded = json_encode($records, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

    if (is_string($encoded) && $encoded !== '') {
        fwrite($handle, $encoded);
    }

    fflush($handle);
}

function login_throttle_calculate_status(array $records, array $keys, int $now): array
{
    $status = login_throttle_default_status();

    foreach ($keys as $key) {
        $record = $records[$key] ?? null;
        if (!is_array($record)) {
            continue;
        }

        $status['failed_count'] = max($status['failed_count'], (int) ($record['failed_count'] ?? 0));
        $remainingSeconds = max(0, (int) ($record['locked_until'] ?? 0) - $now);
        $status['remaining_seconds'] = max($status['remaining_seconds'], $remainingSeconds);
    }

    $status['is_locked'] = $status['remaining_seconds'] > 0;
    $status['remaining_attempts'] = max(0, (int) $status['max_failures'] - (int) $status['failed_count']);

    return $status;
}

function login_throttle_mutate(string $username, string $ipAddress, ?callable $mutator = null): array
{
    $keys = login_throttle_record_keys($username, $ipAddress);
    if (empty($keys)) {
        return login_throttle_default_status();
    }

    $path = login_throttle_storage_path();
    $directory = dirname($path);

    if (!is_dir($directory)) {
        @mkdir($directory, 0777, true);
    }

    $handle = @fopen($path, 'c+');
    if (!is_resource($handle)) {
        return login_throttle_default_status();
    }

    try {
        if (!flock($handle, LOCK_EX)) {
            fclose($handle);

            return login_throttle_default_status();
        }

        $now = time();
        $records = login_throttle_prune_records(login_throttle_load_records($handle), $now);

        if (is_callable($mutator)) {
            $records = (array) $mutator($records, $keys, $now);
        }

        $records = login_throttle_prune_records($records, $now);
        login_throttle_write_records($handle, $records);
        $status = login_throttle_calculate_status($records, $keys, $now);

        flock($handle, LOCK_UN);
        fclose($handle);

        return $status;
    } catch (Throwable $exception) {
        flock($handle, LOCK_UN);
        fclose($handle);

        return login_throttle_default_status();
    }
}

function login_throttle_status(string $username, ?string $ipAddress = null): array
{
    return login_throttle_mutate($username, (string) ($ipAddress ?? client_ip_address()));
}

function login_throttle_register_failure(string $username, ?string $ipAddress = null): array
{
    $limits = login_throttle_limits();
    $maxFailures = (int) ($limits['max_failures'] ?? 5);
    $lockoutSeconds = (int) ($limits['lockout_seconds'] ?? 900);

    return login_throttle_mutate($username, (string) ($ipAddress ?? client_ip_address()), static function (array $records, array $keys, int $now) use ($maxFailures, $lockoutSeconds): array {
        foreach ($keys as $key) {
            $record = $records[$key] ?? array();
            $failedCount = max(0, (int) ($record['failed_count'] ?? 0)) + 1;
            $lockedUntil = (int) ($record['locked_until'] ?? 0);

            if ($failedCount >= $maxFailures) {
                $lockedUntil = max($lockedUntil, $now + $lockoutSeconds);
            }

            $records[$key] = array(
                'failed_count' => $failedCount,
                'last_attempt_at' => $now,
                'locked_until' => $lockedUntil,
            );
        }

        return $records;
    });
}

function login_throttle_clear(string $username, ?string $ipAddress = null): void
{
    login_throttle_mutate($username, (string) ($ipAddress ?? client_ip_address()), static function (array $records, array $keys): array {
        foreach ($keys as $key) {
            unset($records[$key]);
        }

        return $records;
    });
}

function payment_slip_allowed_methods(): array
{
    return array('transfer', 'qr', 'card');
}

function payment_method_supports_slip(string $paymentMethod): bool
{
    return in_array(trim($paymentMethod), payment_slip_allowed_methods(), true);
}

function payment_slip_accept_attribute(): string
{
    return '.jpg,.jpeg,.png,.webp,.pdf,image/jpeg,image/png,image/webp,application/pdf';
}
