<?php

declare(strict_types=1);

const DISTRIBUTION_ROOT = __DIR__ . '/..';
const RELEASES_FILE = DISTRIBUTION_ROOT . '/versions/releases.json';
const LATEST_FILE = DISTRIBUTION_ROOT . '/versions/latest.json';

header('X-Content-Type-Options: nosniff');

$path = trim((string) ($_GET['path'] ?? ''), '/');
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';

try {
    if ($method === 'GET' && $path === 'version/latest') {
        jsonResponse(latestReleasePayload());
    }

    if (str_starts_with($path, 'releases/')) {
        requireReleaseToken();
        dispatchReleaseApi($method, substr($path, strlen('releases/')));
    }

    jsonResponse(['ok' => false, 'message' => 'Endpoint not found.'], 404);
} catch (Throwable $error) {
    writeLog('error', $error->getMessage());
    jsonResponse(['ok' => false, 'message' => $error->getMessage()], 500);
}

function dispatchReleaseApi(string $method, string $action): void
{
    if ($method === 'GET' && $action === 'list') {
        jsonResponse(['ok' => true, 'releases' => releases()]);
    }

    if ($method === 'GET' && $action === 'check') {
        $version = normalizeVersion((string) ($_GET['version'] ?? ''));
        $release = releaseByVersion($version);
        jsonResponse([
            'ok' => true,
            'exists' => $release !== null,
            'release' => $release,
        ]);
    }

    if ($method === 'POST' && $action === 'upload') {
        handleUpload();
    }

    if ($method === 'POST' && $action === 'publish') {
        handlePublish();
    }

    if ($method === 'POST' && $action === 'package') {
        handlePackage();
    }

    if ($method === 'POST' && $action === 'latest') {
        handleSetLatest();
    }

    jsonResponse(['ok' => false, 'message' => 'Release endpoint not found.'], 404);
}

function handleUpload(): void
{
    if (!isset($_FILES['release'])) {
        jsonResponse(['ok' => false, 'message' => 'Upload field "release" is required.'], 400);
    }

    $file = $_FILES['release'];
    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        jsonResponse(['ok' => false, 'message' => 'Release upload failed.'], 400);
    }

    $version = normalizeVersion((string) ($_POST['version'] ?? ''));
    $extension = strtolower(pathinfo((string) ($file['name'] ?? ''), PATHINFO_EXTENSION));
    if (!in_array($extension, ['zip', 'tgz', 'gz'], true)) {
        jsonResponse(['ok' => false, 'message' => 'Only zip, tgz, and gz release uploads are allowed.'], 400);
    }

    $uploadId = $version . '-' . date('YmdHis') . '-' . bin2hex(random_bytes(4));
    $target = DISTRIBUTION_ROOT . '/temp/' . $uploadId . '.' . $extension;
    if (!move_uploaded_file((string) $file['tmp_name'], $target)) {
        jsonResponse(['ok' => false, 'message' => 'Could not store uploaded release.'], 500);
    }

    writeJson(DISTRIBUTION_ROOT . '/temp/' . $uploadId . '.json', [
        'upload_id' => $uploadId,
        'version' => $version,
        'file' => basename($target),
        'original_name' => (string) ($file['name'] ?? ''),
        'uploaded_at' => gmdate('c'),
    ]);

    writeLog('upload', 'Stored release upload ' . $uploadId);
    jsonResponse(['ok' => true, 'upload_id' => $uploadId, 'version' => $version]);
}

function handlePublish(): void
{
    $input = jsonInput();
    $version = normalizeVersion((string) ($input['version'] ?? ''));
    $releaseDate = (string) ($input['release_date'] ?? gmdate('Y-m-d'));
    $changelog = $input['changelog'] ?? [];
    $markLatest = (bool) ($input['latest'] ?? false);
    $uploadId = (string) ($input['upload_id'] ?? '');

    if ($uploadId !== '') {
        publishUploadedZip($uploadId, $version);
    }

    $releaseDir = DISTRIBUTION_ROOT . '/releases/' . $version;
    if (!is_dir($releaseDir)) {
        jsonResponse(['ok' => false, 'message' => 'Release folder is missing: releases/' . $version], 400);
    }

    mirrorReleaseToCdn($version, $markLatest);
    $packagePath = generateDistributionPackage($version);

    $release = [
        'version' => $version,
        'release_date' => $releaseDate,
        'cdn_base' => publicUrl('cdn/' . $version),
        'download_url' => publicUrl('packages/' . basename($packagePath)),
        'package' => basename($packagePath),
        'status' => $markLatest ? 'latest' : 'published',
        'changelog' => array_values((array) $changelog),
        'published_at' => gmdate('c'),
    ];

    $releases = releases();
    $releases = array_values(array_filter($releases, static fn (array $item): bool => ($item['version'] ?? '') !== $version));
    $releases[] = $release;
    usort($releases, static fn (array $a, array $b): int => version_compare((string) $b['version'], (string) $a['version']));
    writeJson(RELEASES_FILE, $releases);

    if ($markLatest) {
        $latest = $release;
        $latest['cdn_base'] = publicUrl('cdn/latest');
        writeJson(LATEST_FILE, $latest);
    }

    writeLog('publish', 'Published release ' . $version . ($markLatest ? ' as latest' : ''));
    jsonResponse(['ok' => true, 'release' => $release, 'latest' => $markLatest, 'package_url' => $release['download_url']]);
}

function handlePackage(): void
{
    $input = jsonInput();
    $version = normalizeVersion((string) ($input['version'] ?? ''));
    $package = generateDistributionPackage($version);

    $releases = releases();
    foreach ($releases as &$release) {
        if (($release['version'] ?? '') === $version) {
            $release['download_url'] = publicUrl('packages/' . basename($package));
            $release['package'] = basename($package);
            $release['packaged_at'] = gmdate('c');
        }
    }
    unset($release);
    writeJson(RELEASES_FILE, $releases);

    jsonResponse(['ok' => true, 'version' => $version, 'package' => basename($package), 'download_url' => publicUrl('packages/' . basename($package))]);
}

function handleSetLatest(): void
{
    $input = jsonInput();
    $version = normalizeVersion((string) ($input['version'] ?? ''));
    $release = releaseByVersion($version);
    if ($release === null) {
        jsonResponse(['ok' => false, 'message' => 'Release is not registered.'], 404);
    }

    mirrorReleaseToCdn($version, true);
    generateDistributionPackage($version);

    $release['cdn_base'] = publicUrl('cdn/latest');
    $release['download_url'] = releaseDownloadUrl($version);
    $release['status'] = 'latest';
    writeJson(LATEST_FILE, $release);

    $releases = releases();
    foreach ($releases as &$item) {
        if (($item['version'] ?? '') === $version) {
            $item['status'] = 'latest';
            $item['download_url'] = $release['download_url'];
        } elseif (($item['status'] ?? '') === 'latest') {
            $item['status'] = 'published';
        }
    }
    unset($item);
    writeJson(RELEASES_FILE, $releases);

    jsonResponse(['ok' => true, 'latest' => $release]);
}

function publishUploadedZip(string $uploadId, string $version): void
{
    if (!preg_match('/^[a-zA-Z0-9._-]+$/', $uploadId)) {
        jsonResponse(['ok' => false, 'message' => 'Invalid upload id.'], 400);
    }

    $metaPath = DISTRIBUTION_ROOT . '/temp/' . $uploadId . '.json';
    if (!is_file($metaPath)) {
        jsonResponse(['ok' => false, 'message' => 'Upload metadata not found.'], 400);
    }

    $meta = readJson($metaPath, []);
    $file = DISTRIBUTION_ROOT . '/temp/' . basename((string) ($meta['file'] ?? ''));
    if (!is_file($file)) {
        jsonResponse(['ok' => false, 'message' => 'Uploaded release file not found.'], 400);
    }

    if (strtolower(pathinfo($file, PATHINFO_EXTENSION)) !== 'zip') {
        jsonResponse(['ok' => false, 'message' => 'Automatic publish currently extracts zip uploads only.'], 400);
    }

    if (!class_exists(ZipArchive::class)) {
        jsonResponse(['ok' => false, 'message' => 'ZipArchive is not available on this PHP installation.'], 500);
    }

    $target = DISTRIBUTION_ROOT . '/releases/' . $version;
    ensureDirectory($target);

    $zip = new ZipArchive();
    if ($zip->open($file) !== true) {
        jsonResponse(['ok' => false, 'message' => 'Could not open uploaded zip.'], 400);
    }
    validateReleaseZip($zip, $version);
    $zip->extractTo($target);
    $zip->close();
}

function validateReleaseZip(ZipArchive $zip, string $version): void
{
    $allowedRoots = [
        '.htaccess',
        'admin.php',
        'api/',
        'assets/',
        'cdn/',
        'css/',
        'js/',
        'logs/',
        'packages/',
        'plugins/',
        'releases/',
        'temp/',
        'tools/',
        'vendor/',
        'versions/',
        'blockwriteai.min.css',
        'blockwriteai.min.js',
        'blockwriteai.protected.min.js',
        'blockwriteai.bundle.protected.min.js',
        'blockwriteai-logo.svg',
        'blockwriteai-logo-dark.svg',
        'blockwriteai-favicon.svg',
    ];

    for ($index = 0; $index < $zip->numFiles; $index++) {
        $name = (string) $zip->getNameIndex($index);
        $normalized = str_replace('\\', '/', $name);
        $normalized = ltrim($normalized, '/');
        if ($normalized === '' || str_contains($normalized, '../') || str_starts_with($normalized, '../') || preg_match('#^[A-Za-z]:/#', $normalized)) {
            jsonResponse(['ok' => false, 'message' => 'Release zip contains an unsafe path.'], 400);
        }
        if (str_ends_with($normalized, '.map')) {
            jsonResponse(['ok' => false, 'message' => 'Release zip must not contain source maps.'], 400);
        }
        if (preg_match('#(^|/)(blockwriteai(?:-[a-z-]+)?|.*plugins/.*)\.js$#', $normalized) && !str_ends_with($normalized, '.min.js')) {
            jsonResponse(['ok' => false, 'message' => 'Release zip must not contain unminified JavaScript.'], 400);
        }
        if (preg_match('#(^|/)blockwriteai\.css$#', $normalized)) {
            jsonResponse(['ok' => false, 'message' => 'Release zip must not contain unminified CSS.'], 400);
        }
        if (!zipPathIsAllowed($normalized, $allowedRoots, $version)) {
            jsonResponse(['ok' => false, 'message' => 'Release zip contains an unexpected file: ' . $normalized], 400);
        }
    }
}

function zipPathIsAllowed(string $path, array $allowedRoots, string $version): bool
{
    foreach ($allowedRoots as $root) {
        if ($path === $root || str_starts_with($path, $root)) {
            return true;
        }
    }

    return str_starts_with($path, 'cdn/' . $version . '/') || str_starts_with($path, 'releases/' . $version . '/');
}

function mirrorReleaseToCdn(string $version, bool $markLatest): void
{
    mirrorDirectory(DISTRIBUTION_ROOT . '/releases/' . $version, DISTRIBUTION_ROOT . '/cdn/' . $version);

    if ($markLatest) {
        mirrorDirectory(DISTRIBUTION_ROOT . '/releases/' . $version, DISTRIBUTION_ROOT . '/cdn/latest');
        mirrorDirectory(DISTRIBUTION_ROOT . '/releases/' . $version . '/vendor', DISTRIBUTION_ROOT . '/cdn/vendor');
    }
}

function mirrorDirectory(string $source, string $target): void
{
    if (!is_dir($source)) {
        return;
    }

    ensureDirectory($target);
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($iterator as $item) {
        $relative = substr($item->getPathname(), strlen($source) + 1);
        $destination = $target . '/' . $relative;
        if ($item->isDir()) {
            ensureDirectory($destination);
            continue;
        }
        ensureDirectory(dirname($destination));
        copy($item->getPathname(), $destination);
    }
}

function latestReleasePayload(): array
{
    $latest = readJson(LATEST_FILE, []);
    if ($latest === []) {
        $all = releases();
        $latest = $all[0] ?? [
            'version' => '',
            'release_date' => '',
            'cdn_base' => publicUrl('cdn/latest'),
            'download_url' => '',
        ];
    }

    return [
        'version' => (string) ($latest['version'] ?? ''),
        'release_date' => (string) ($latest['release_date'] ?? ''),
        'cdn_base' => absoluteDistributionUrl((string) ($latest['cdn_base'] ?? 'cdn/latest')),
        'download_url' => absoluteDistributionUrl((string) ($latest['download_url'] ?? '')),
    ];
}

function releases(): array
{
    $releases = readJson(RELEASES_FILE, []);
    return is_array($releases) ? $releases : [];
}

function releaseByVersion(string $version): ?array
{
    foreach (releases() as $release) {
        if (($release['version'] ?? '') === $version) {
            return $release;
        }
    }

    return null;
}

function releaseDownloadUrl(string $version): string
{
    $candidates = glob(DISTRIBUTION_ROOT . '/packages/blockwriteai-' . $version . '.zip') ?: [];
    if ($candidates === []) {
        $candidates = glob(DISTRIBUTION_ROOT . '/packages/*' . $version . '*') ?: [];
    }
    if ($candidates === []) {
        return '';
    }

    return publicUrl('packages/' . basename($candidates[0]));
}

function generateDistributionPackage(string $version): string
{
    if (!class_exists(ZipArchive::class)) {
        jsonResponse(['ok' => false, 'message' => 'ZipArchive is not available on this PHP installation.'], 500);
    }

    $release = DISTRIBUTION_ROOT . '/releases/' . $version;
    if (!is_dir($release)) {
        jsonResponse(['ok' => false, 'message' => 'Release folder is missing: releases/' . $version], 400);
    }

    $package = DISTRIBUTION_ROOT . '/packages/blockwriteai-' . $version . '.zip';
    if (is_file($package)) {
        unlink($package);
    }

    $zip = new ZipArchive();
    if ($zip->open($package, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        jsonResponse(['ok' => false, 'message' => 'Could not create release ZIP.'], 500);
    }

    addZipFile($zip, DISTRIBUTION_ROOT . '/.htaccess', '.htaccess');
    addZipFile($zip, DISTRIBUTION_ROOT . '/admin.php', 'admin.php');
    addZipDirectory($zip, DISTRIBUTION_ROOT . '/api', 'api', ['config.local.php']);
    addZipDirectory($zip, DISTRIBUTION_ROOT . '/tools', 'tools');
    addZipDirectory($zip, DISTRIBUTION_ROOT . '/versions', 'versions');
    addZipDirectory($zip, DISTRIBUTION_ROOT . '/cdn/' . $version, 'cdn/' . $version);
    addZipDirectory($zip, DISTRIBUTION_ROOT . '/cdn/latest', 'cdn/latest');
    addZipDirectory($zip, DISTRIBUTION_ROOT . '/cdn/vendor', 'cdn/vendor');
    addZipDirectory($zip, $release . '/vendor', 'vendor');
    addZipDirectory($zip, $release . '/plugins', 'plugins');
    addZipDirectory($zip, $release . '/assets', 'assets');
    addZipDirectory($zip, $release, 'releases/' . $version, ['vendor', 'plugins', 'assets']);
    $zip->addEmptyDir('logs');
    $zip->addEmptyDir('temp');
    $zip->addEmptyDir('packages');
    $zip->close();

    writeLog('package', 'Generated package ' . basename($package));
    return $package;
}

function addZipDirectory(ZipArchive $zip, string $source, string $target, array $skip = []): void
{
    if (!is_dir($source)) {
        return;
    }

    $skip = array_flip($skip);
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($iterator as $item) {
        $name = $item->getFilename();
        if (isset($skip[$name]) || str_ends_with($name, '.map')) {
            continue;
        }

        $relative = str_replace('\\', '/', substr($item->getPathname(), strlen($source) + 1));
        foreach (explode('/', $relative) as $segment) {
            if (isset($skip[$segment])) {
                continue 2;
            }
        }

        $zipPath = trim($target . '/' . $relative, '/');
        if ($item->isDir()) {
            $zip->addEmptyDir($zipPath);
        } else {
            $zip->addFile($item->getPathname(), $zipPath);
        }
    }
}

function addZipFile(ZipArchive $zip, string $source, string $target): void
{
    if (is_file($source) && !str_ends_with($source, '.map')) {
        $zip->addFile($source, $target);
    }
}

function requireReleaseToken(): void
{
    $expected = distributionConfig()['api_token'] ?? '';
    if (!is_string($expected) || $expected === '') {
        jsonResponse(['ok' => false, 'message' => 'Release API token is not configured.'], 503);
    }

    $authorization = (string) ($_SERVER['HTTP_AUTHORIZATION'] ?? '');
    $provided = (string) ($_SERVER['HTTP_X_DISTRIBUTION_TOKEN'] ?? '');
    if (preg_match('/^Bearer\s+(.+)$/i', $authorization, $matches)) {
        $provided = trim($matches[1]);
    }

    if (!hash_equals($expected, $provided)) {
        jsonResponse(['ok' => false, 'message' => 'Unauthorized.'], 401);
    }
}

function distributionConfig(): array
{
    $config = [
        'api_token' => getenv('DISTRIBUTION_API_TOKEN') ?: '',
        'public_base_url' => getenv('DISTRIBUTION_PUBLIC_BASE_URL') ?: '',
    ];

    $local = __DIR__ . '/config.local.php';
    if (is_file($local)) {
        $loaded = require $local;
        if (is_array($loaded)) {
            $config = array_merge($config, $loaded);
        }
    }

    return $config;
}

function publicUrl(string $path): string
{
    $base = trim((string) (distributionConfig()['public_base_url'] ?? ''));
    if ($base === '') {
        $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $host = (string) ($_SERVER['HTTP_HOST'] ?? 'localhost');
        $scriptDir = str_replace('\\', '/', dirname((string) ($_SERVER['SCRIPT_NAME'] ?? '/distribution-server/api/index.php')));
        $basePath = preg_replace('#/api$#', '', $scriptDir);
        $base = $scheme . '://' . $host . rtrim($basePath, '/');
    }

    return rtrim($base, '/') . '/' . ltrim($path, '/');
}

function absoluteDistributionUrl(string $pathOrUrl): string
{
    if ($pathOrUrl === '' || preg_match('#^https?://#i', $pathOrUrl)) {
        return $pathOrUrl;
    }

    return publicUrl($pathOrUrl);
}

function normalizeVersion(string $version): string
{
    $version = trim($version);
    if (!preg_match('/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/', $version)) {
        jsonResponse(['ok' => false, 'message' => 'A valid semantic version is required.'], 400);
    }

    return $version;
}

function jsonInput(): array
{
    $raw = file_get_contents('php://input') ?: '';
    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        jsonResponse(['ok' => false, 'message' => 'JSON request body is required.'], 400);
    }

    return $decoded;
}

function readJson(string $path, array $default): array
{
    if (!is_file($path)) {
        return $default;
    }

    $decoded = json_decode((string) file_get_contents($path), true);
    return is_array($decoded) ? $decoded : $default;
}

function writeJson(string $path, array $data): void
{
    ensureDirectory(dirname($path));
    file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL, LOCK_EX);
}

function ensureDirectory(string $path): void
{
    if (!is_dir($path)) {
        mkdir($path, 0775, true);
    }
}

function writeLog(string $type, string $message): void
{
    $line = '[' . gmdate('c') . '] ' . $type . ' ' . $message . PHP_EOL;
    file_put_contents(DISTRIBUTION_ROOT . '/logs/distribution.log', $line, FILE_APPEND | LOCK_EX);
}

function jsonResponse(array $payload, int $status = 200): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL;
    exit;
}
