<?php

declare(strict_types=1);

require_once dirname(__DIR__, 2) . '/system/bootstrap.php';

/**
 * @param array<string, mixed> $payload
 */
function myronApiFilesRespond(array $payload, int $status = 200): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    header('X-Content-Type-Options: nosniff');
    echo json_encode($payload, JSON_UNESCAPED_SLASHES);
    exit;
}

function myronApiFilesError(string $message, string $code, int $status = 400): never
{
    myronApiFilesRespond([
        'ok' => false,
        'error' => $message,
        'code' => $code,
    ], $status);
}

function myronApiFilesAuthorizationHeader(): string
{
    foreach (['HTTP_AUTHORIZATION', 'REDIRECT_HTTP_AUTHORIZATION', 'Authorization'] as $key) {
        $value = trim((string) ($_SERVER[$key] ?? ''));
        if ($value !== '') {
            return $value;
        }
    }

    if (function_exists('apache_request_headers')) {
        $headers = apache_request_headers();
        foreach ($headers as $key => $value) {
            if (strtolower((string) $key) === 'authorization') {
                return trim((string) $value);
            }
        }
    }

    return '';
}

function myronApiFilesRequireBearerToken(): void
{
    $configuredToken = trim((string) (getenv('MYRON_API_KEY') ?: ''));
    if ($configuredToken === '') {
        myronApiFilesError('API uploads are not configured.', 'api_key_not_configured', 503);
    }

    $header = myronApiFilesAuthorizationHeader();
    if ($header === '') {
        myronApiFilesError('Bearer token is required.', 'auth_required', 401);
    }

    if (preg_match('/^Bearer\s+(.+)$/i', $header, $matches) !== 1) {
        myronApiFilesError('Bearer token is required.', 'auth_scheme_invalid', 401);
    }

    $providedToken = trim((string) $matches[1]);
    if ($providedToken === '' || !hash_equals($configuredToken, $providedToken)) {
        myronApiFilesError('Bearer token is invalid.', 'auth_invalid', 401);
    }
}

function myronApiFilesContentType(): string
{
    return strtolower(trim(explode(';', (string) ($_SERVER['CONTENT_TYPE'] ?? ''), 2)[0]));
}

/**
 * @return array<string, mixed>
 */
function myronApiFilesJsonBody(): array
{
    $raw = file_get_contents('php://input');
    if (!is_string($raw) || trim($raw) === '') {
        myronApiFilesError('JSON upload payload is required.', 'invalid_json', 400);
    }

    try {
        $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException) {
        myronApiFilesError('JSON upload payload is invalid.', 'invalid_json', 400);
    }

    if (!is_array($decoded)) {
        myronApiFilesError('JSON upload payload must be an object.', 'invalid_json', 400);
    }

    return $decoded;
}

/**
 * @return array{upload: array<string, mixed>, tempPath: string|null, metadata: array<string, string>}
 */
function myronApiFilesUploadInput(): array
{
    if (isset($_FILES['file']) && is_array($_FILES['file'])) {
        return [
            'upload' => $_FILES['file'],
            'tempPath' => null,
            'metadata' => myronApiFilesMetadata($_POST),
        ];
    }

    if (myronApiFilesContentType() !== 'application/json') {
        myronApiFilesError('File upload payload is required.', 'missing_file', 400);
    }

    $body = myronApiFilesJsonBody();
    $data = trim((string) ($body['data'] ?? ''));
    $mime = trim((string) ($body['mime'] ?? ''));
    $filename = trim((string) ($body['filename'] ?? 'upload.bin'));
    if ($data === '' || $mime === '') {
        myronApiFilesError('Base64 data and MIME type are required.', 'invalid_base64_payload', 400);
    }

    if (preg_match('/^data:[^;]+;base64,(.+)$/', $data, $matches) === 1) {
        $data = (string) $matches[1];
    }

    $decoded = base64_decode($data, true);
    if (!is_string($decoded) || $decoded === '') {
        myronApiFilesError('Base64 data is invalid.', 'invalid_base64', 400);
    }

    $tempPath = tempnam(sys_get_temp_dir(), 'myron-api-upload-');
    if (!is_string($tempPath) || $tempPath === '') {
        myronApiFilesError('Upload temporary storage is unavailable.', 'storage_failed', 500);
    }

    if (file_put_contents($tempPath, $decoded) === false) {
        @unlink($tempPath);
        myronApiFilesError('Upload temporary storage is unavailable.', 'storage_failed', 500);
    }

    return [
        'upload' => [
            'error' => UPLOAD_ERR_OK,
            'tmp_name' => $tempPath,
            'name' => $filename === '' ? 'upload.bin' : $filename,
            'type' => $mime,
            'size' => strlen($decoded),
        ],
        'tempPath' => $tempPath,
        'metadata' => myronApiFilesMetadata($body),
    ];
}

/**
 * @param array<string, mixed> $input
 * @return array<string, string>
 */
function myronApiFilesMetadata(array $input): array
{
    $metadata = [];
    foreach (['alt_text', 'title', 'description', 'caption', 'asset_class'] as $key) {
        if (array_key_exists($key, $input)) {
            $metadata[$key] = trim((string) $input[$key]);
        }
    }

    return $metadata;
}

function myronApiFilesUploaderId(PDO $pdo): string
{
    $isUploadCapable = static function (array $user): bool {
        return ($user['is_active'] ?? false) === true
            && (myronUserCanAccessContent($user) || myronUserCanAccessAdministration($user));
    };

    $configuredUserId = trim((string) (getenv('MYRON_API_USER_ID') ?: ''));
    if ($configuredUserId !== '') {
        $configuredUser = myronAuthRepository()->findUserById($configuredUserId);
        if (is_array($configuredUser) && $isUploadCapable($configuredUser)) {
            return $configuredUserId;
        }

        myronApiFilesError('Configured API uploader is unavailable.', 'uploader_unavailable', 500);
    }

    foreach (myronAuthRepository()->listUsers() as $user) {
        if ($isUploadCapable($user)) {
            return (string) ($user['id'] ?? '');
        }
    }

    myronApiFilesError('No API upload user is available.', 'uploader_unavailable', 500);
}

function myronApiFilesErrorCode(Throwable $exception): string
{
    $message = strtolower($exception->getMessage());
    if (str_contains($message, 'not allowed') || str_contains($message, 'file type')) {
        return 'mime_rejected';
    }
    if (str_contains($message, 'size limit') || str_contains($message, '15 mb')) {
        return 'file_too_large';
    }
    if (str_contains($message, 'choose a file') || str_contains($message, 'payload is invalid') || str_contains($message, 'empty')) {
        return 'invalid_upload';
    }
    if (str_contains($message, 'is required for asset uploads')) {
        return 'invalid_upload';
    }
    if (str_contains($message, 'store uploaded file') || str_contains($message, 'storage')) {
        return 'storage_failed';
    }

    return 'server_error';
}

function myronApiFilesStatusForCode(string $code): int
{
    return match ($code) {
        'mime_rejected', 'invalid_upload' => 422,
        'file_too_large' => 413,
        'storage_failed' => 500,
        default => 500,
    };
}

if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
    header('Allow: POST');
    myronApiFilesError('POST is required.', 'method_not_allowed', 405);
}

myronApiFilesRequireBearerToken();

$tempPath = null;

try {
    $input = myronApiFilesUploadInput();
    $tempPath = $input['tempPath'];
    $metadata = $input['metadata'];

    $repository = myronFileRepository();
    $pdo = myronDatabase();
    $uploaderId = myronApiFilesUploaderId($pdo);

    $file = $repository->uploadFile($input['upload'], $uploaderId, $metadata, true);
    $fileId = (string) ($file['id'] ?? '');

    myronApiFilesRespond([
        'ok' => true,
        'file_id' => $fileId,
        'url' => (string) ($file['file_url'] ?? ''),
        'mime' => (string) ($file['mime_type'] ?? ''),
        'width' => isset($file['image_width']) ? (int) $file['image_width'] : null,
        'height' => isset($file['image_height']) ? (int) $file['image_height'] : null,
    ]);
} catch (Throwable $exception) {
    $code = myronApiFilesErrorCode($exception);
    if (is_string($tempPath) && $tempPath !== '' && is_file($tempPath)) {
        @unlink($tempPath);
        $tempPath = null;
    }
    myronApiFilesError($exception->getMessage(), $code, myronApiFilesStatusForCode($code));
} finally {
    if (is_string($tempPath) && $tempPath !== '' && is_file($tempPath)) {
        @unlink($tempPath);
    }
}
