<?php
/**
 * DWSK Backend Helpers
 * ---------------------------------------------------------------
 * Shared utility functions used by submit_order.php, submit_application.php
 * and submit_contact.php.
 */

if (!defined('DWSK_APP')) {
    http_response_code(403);
    exit('Direct access not permitted.');
}

/**
 * Send a JSON response and terminate the script.
 */
function json_response(bool $success, string $message, array $extra = [], int $httpCode = 200): void
{
    http_response_code($httpCode);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(array_merge([
        'success' => $success,
        'message' => $message,
    ], $extra));
    exit;
}

/**
 * Trim, strip control characters, and cap length of plain-text input.
 * (XSS mitigation happens at output time via htmlspecialchars() in the email templates —
 * this just normalises/limits what we accept as input.)
 */
function clean_text(?string $value, int $maxLen = 5000): string
{
    $value = $value ?? '';
    $value = trim($value);
    $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value); // strip control chars
    if (function_exists('mb_substr')) {
        $value = mb_substr($value, 0, $maxLen);
    } else {
        $value = substr($value, 0, $maxLen);
    }
    return $value;
}

/**
 * Validate an email address.
 */
function is_valid_email(string $email): bool
{
    return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
}

/**
 * Escape a string for safe inclusion in HTML email bodies (XSS prevention).
 */
function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

/**
 * Resolve the visitor's IP address.
 * We intentionally trust only REMOTE_ADDR to avoid IP-spoofing via forged
 * X-Forwarded-For headers. If this app sits behind a load balancer/CDN,
 * update this to read the trusted proxy header instead.
 */
function client_ip(): string
{
    return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}

/**
 * Honeypot check. Real users never fill this hidden field; bots often do.
 * Returns true if the submission looks like spam.
 */
function is_spam_honeypot(): bool
{
    $value = $_POST[HONEYPOT_FIELD] ?? '';
    return trim((string) $value) !== '';
}

/**
 * Simple flat-file rate limiter, keyed by IP address.
 * Returns true if the request is ALLOWED, false if the caller should be blocked.
 */
function rate_limit_allow(string $ip): bool
{
    if (!is_dir(LOG_DIR)) {
        @mkdir(LOG_DIR, 0755, true);
    }

    $now = time();
    $data = [];

    $fp = @fopen(RATE_LIMIT_STORE, 'c+');
    if (!$fp) {
        // If we can't open the store, fail open (allow) rather than block legitimate users.
        return true;
    }

    flock($fp, LOCK_EX);
    $contents = stream_get_contents($fp);
    if ($contents) {
        $decoded = json_decode($contents, true);
        if (is_array($decoded)) {
            $data = $decoded;
        }
    }

    // Prune old entries for this IP and across all IPs to keep the file small.
    foreach ($data as $key => $timestamps) {
        $data[$key] = array_values(array_filter($timestamps, function ($t) use ($now) {
            return ($now - $t) < RATE_LIMIT_WINDOW_SECONDS;
        }));
        if (empty($data[$key])) {
            unset($data[$key]);
        }
    }

    $recent = $data[$ip] ?? [];
    $allowed = count($recent) < RATE_LIMIT_MAX_SUBMISSIONS;

    if ($allowed) {
        $recent[] = $now;
        $data[$ip] = $recent;
        ftruncate($fp, 0);
        rewind($fp);
        fwrite($fp, json_encode($data));
        fflush($fp);
    }

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

    return $allowed;
}

/**
 * Generate a unique, human-readable submission reference, e.g. DWSK-ORD-20260718-0001.
 * Uses a per-day, file-locked counter so numbers increment safely under concurrent requests.
 */
function generate_submission_id(string $prefix): string
{
    if (!is_dir(COUNTER_DIR)) {
        @mkdir(COUNTER_DIR, 0755, true);
    }

    $datePart = date('Ymd');
    $counterFile = COUNTER_DIR . $prefix . '_' . $datePart . '.count';

    $fp = fopen($counterFile, 'c+');
    flock($fp, LOCK_EX);
    $current = (int) fgets($fp);
    $next = $current + 1;
    rewind($fp);
    ftruncate($fp, 0);
    fwrite($fp, (string) $next);
    fflush($fp);
    flock($fp, LOCK_UN);
    fclose($fp);

    return sprintf('DWSK-%s-%s-%04d', $prefix, $datePart, $next);
}

/**
 * Verify the CSRF token submitted with the form against the one stored in the session.
 */
function verify_csrf_token(?string $submittedToken): bool
{
    if (session_status() !== PHP_SESSION_ACTIVE) {
        session_start();
    }
    if (empty($_SESSION['csrf_token']) || empty($submittedToken)) {
        return false;
    }
    return hash_equals($_SESSION['csrf_token'], $submittedToken);
}

/**
 * Validate and normalise the $_FILES structure for a given field name that
 * may contain zero, one, or many uploaded files (e.g. "documents[]").
 *
 * Returns ['ok' => bool, 'error' => string|null, 'files' => array of ['tmp_name','name','size']]
 */
function collect_uploaded_files(string $fieldName): array
{
    if (!isset($_FILES[$fieldName])) {
        return ['ok' => true, 'error' => null, 'files' => []];
    }

    $raw = $_FILES[$fieldName];
    $files = [];

    // Normalise single vs multiple file input structures
    if (is_array($raw['name'])) {
        $count = count($raw['name']);
        for ($i = 0; $i < $count; $i++) {
            if ($raw['error'][$i] === UPLOAD_ERR_NO_FILE) {
                continue; // empty slot, skip
            }
            $files[] = [
                'tmp_name' => $raw['tmp_name'][$i],
                'name'     => $raw['name'][$i],
                'size'     => $raw['size'][$i],
                'error'    => $raw['error'][$i],
            ];
        }
    } else {
        if ($raw['error'] !== UPLOAD_ERR_NO_FILE) {
            $files[] = [
                'tmp_name' => $raw['tmp_name'],
                'name'     => $raw['name'],
                'size'     => $raw['size'],
                'error'    => $raw['error'],
            ];
        }
    }

    if (count($files) > MAX_FILES_PER_SUBMISSION) {
        return ['ok' => false, 'error' => 'Too many files. Maximum ' . MAX_FILES_PER_SUBMISSION . ' files allowed.', 'files' => []];
    }

    foreach ($files as $file) {
        if ($file['error'] !== UPLOAD_ERR_OK) {
            return ['ok' => false, 'error' => 'Submission failed. Please try again later.', 'files' => []];
        }

        if ($file['size'] > MAX_FILE_SIZE_BYTES) {
            return ['ok' => false, 'error' => 'File exceeds 10 MB.', 'files' => []];
        }

        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if (!in_array($ext, ALLOWED_EXTENSIONS, true)) {
            return ['ok' => false, 'error' => 'Unsupported file type.', 'files' => []];
        }

        // Real MIME-type validation (don't trust the client-supplied Content-Type)
        if (!is_uploaded_file($file['tmp_name'])) {
            return ['ok' => false, 'error' => 'Submission failed. Please try again later.', 'files' => []];
        }
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        if (!in_array($mime, ALLOWED_MIME_TYPES, true)) {
            return ['ok' => false, 'error' => 'Unsupported file type.', 'files' => []];
        }
    }

    return ['ok' => true, 'error' => null, 'files' => $files];
}

/**
 * Move validated uploaded files into the destination directory using
 * randomly generated filenames (prevents overwrite, directory traversal,
 * and leaking of original filenames/paths).
 *
 * Returns array of ['stored_path' => ..., 'stored_name' => ..., 'original_name' => ...]
 */
function store_uploaded_files(array $validatedFiles, string $destDir): array
{
    if (!is_dir($destDir)) {
        @mkdir($destDir, 0755, true);
    }

    $stored = [];
    foreach ($validatedFiles as $file) {
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        $randomName = bin2hex(random_bytes(16)) . '.' . $ext;
        $destPath = rtrim($destDir, '/\\') . DIRECTORY_SEPARATOR . $randomName;

        // Ensure the resolved destination stays within the intended directory (defence in depth)
        $realDestDir = realpath($destDir);
        if ($realDestDir === false) {
            continue;
        }

        if (!move_uploaded_file($file['tmp_name'], $destPath)) {
            continue;
        }

        $stored[] = [
            'stored_path'   => $destPath,
            'stored_name'   => $randomName,
            'original_name' => basename($file['name']), // basename() guards against path traversal in the display name too
        ];
    }

    return $stored;
}

/**
 * Append a JSON line to a log file (simple, dependency-free audit trail —
 * no database or SQL involved, so no SQL-injection surface here).
 */
function log_submission(string $logFile, array $data): void
{
    if (!is_dir(LOG_DIR)) {
        @mkdir(LOG_DIR, 0755, true);
    }
    $line = json_encode(array_merge(['timestamp' => date('c')], $data)) . PHP_EOL;
    @file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
}

/**
 * Log an internal error for the site owner without exposing details to the client.
 */
function log_error(string $context, \Throwable $e): void
{
    log_submission(LOG_FILE_ERRORS, [
        'context' => $context,
        'error'   => $e->getMessage(),
        'ip'      => client_ip(),
    ]);
}

/**
 * Optional Google reCAPTCHA verification (only runs if enabled in config.php).
 */
function recaptcha_passes(?string $token): bool
{
    if (!RECAPTCHA_ENABLED) {
        return true; // feature disabled — skip check
    }
    if (empty($token)) {
        return false;
    }

    $response = @file_get_contents('https://www.google.com/recaptcha/api/siteverify?' . http_build_query([
        'secret'   => RECAPTCHA_SECRET_KEY,
        'response' => $token,
        'remoteip' => client_ip(),
    ]));

    if ($response === false) {
        // If Google's endpoint is unreachable, fail open rather than blocking real users.
        return true;
    }

    $result = json_decode($response, true);
    return !empty($result['success']);
}
