<?php

declare(strict_types=1);

ini_set('memory_limit', '1024M');

/**
 * LLOS unified PHP server.
 *
 * Run with:
 *   php -S 127.0.0.1:5555 router.php
 */

$rootDir = __DIR__;
$iconsDir = $rootDir . '/expressions/icons-emojis';

require_once __DIR__ . '/api/routes_portals.php';

$ratingsFile = $iconsDir . '/shape_ratings.json';
$keywordsFile = $iconsDir . '/shape_rater_keywords.json';
$exprRatingsFile = $iconsDir . '/expressive_ratings.json';
$exprMetadataFile = $iconsDir . '/expressive_metadata.json';
$gerundRatingsFile = $iconsDir . '/gerund_ratings.json';
$shapesLibraryFile = $iconsDir . '/shapes_library.json';
$expressiveShapesFile = $iconsDir . '/expressive_shapes.json';

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($method === 'OPTIONS') {
    http_response_code(204);
    exit;
}

if (str_starts_with($uriPath, '/api/')) {
    handleApiRequest($uriPath, $method);
    exit;
}

serveStatic($uriPath);
exit;

function handleApiRequest(string $path, string $method): void
{
    global $ratingsFile, $keywordsFile, $exprRatingsFile, $exprMetadataFile, $gerundRatingsFile;

    if ($path === '/api/health' && $method === 'GET') {
        jsonResponse(['status' => 'ok']);
    }

    if (llos_handle_portal_api_request($path, $method)) {
        return;
    }

    if ($path === '/api/spellcheck' && $method === 'POST') {
        $payload = readJsonBody();
        if (!isset($payload['words']) || !is_array($payload['words'])) {
            jsonError('Missing "words" array', 400);
        }

        $results = [];
        foreach ($payload['words'] as $word) {
            $token = strtolower(trim((string) $word));
            if ($token === '') {
                continue;
            }

            if (isKnownWord($token)) {
                $results[] = ['word' => $token, 'ok' => true];
            } else {
                $results[] = [
                    'word' => $token,
                    'ok' => false,
                    'suggestions' => suggestWords($token),
                ];
            }
        }
        jsonResponse(['results' => $results]);
    }

    if ($path === '/api/ratings') {
        if ($method === 'GET') {
            jsonResponse(readJsonFileOrDefault($ratingsFile, ['ratings' => new stdClass(), 'updated' => null]));
        }
        if ($method === 'POST') {
            $payload = readJsonBody();
            if (!isset($payload['ratings']) || !is_array($payload['ratings'])) {
                jsonError('Missing "ratings" object', 400);
            }
            $data = [
                'ratings' => $payload['ratings'],
                'updated' => gmdate('c'),
            ];
            writeJsonAtomically($ratingsFile, $data);
            jsonResponse(['ok' => true, 'count' => count($payload['ratings'])]);
        }
    }

    if ($path === '/api/keywords') {
        if ($method === 'GET') {
            jsonResponse(readJsonFileOrDefault($keywordsFile, ['keywords' => new stdClass(), 'updated' => null]));
        }
        if ($method === 'POST') {
            $payload = readJsonBody();
            if (!isset($payload['keywords']) || !is_array($payload['keywords'])) {
                jsonError('Missing "keywords" object', 400);
            }
            $data = [
                'keywords' => $payload['keywords'],
                'updated' => gmdate('c'),
            ];
            writeJsonAtomically($keywordsFile, $data);
            jsonResponse(['ok' => true, 'count' => count($payload['keywords'])]);
        }
    }

    if ($path === '/api/svg' && $method === 'GET') {
        $name = trim((string) ($_GET['name'] ?? ''));
        if ($name === '') {
            jsonError('Missing "name" parameter', 400);
        }
        $shapeIndex = loadShapesIndex();
        $entry = $shapeIndex[strtolower($name)] ?? null;
        if ($entry === null) {
            jsonError('Shape not found: ' . $name, 404);
        }
        $variant = strtolower(trim((string) ($_GET['variant'] ?? '')));
        if ($variant === 'detailed' || $variant === 'd') {
            jsonResponse(['svg' => $entry['d'] ?? '']);
        }
        if ($variant === 'minimal' || $variant === 'm') {
            jsonResponse(['svg' => $entry['m'] ?? '']);
        }
        jsonResponse(['detailed' => $entry['d'] ?? '', 'minimal' => $entry['m'] ?? '']);
    }

    if ($path === '/api/svg/batch' && $method === 'POST') {
        $payload = readJsonBody();
        $names = $payload['names'] ?? null;
        if (!is_array($names) || count($names) > 120) {
            jsonError('"names" must be an array (max 120)', 400);
        }

        $shapeIndex = loadShapesIndex();
        $result = [];
        foreach ($names as $name) {
            $raw = (string) $name;
            $entry = $shapeIndex[strtolower(trim($raw))] ?? null;
            if ($entry !== null) {
                $result[$raw] = [
                    'detailed' => $entry['d'] ?? '',
                    'minimal' => $entry['m'] ?? '',
                ];
            }
        }
        jsonResponse($result);
    }

    if ($path === '/api/expressive/meta' && $method === 'GET') {
        $expressive = loadExpressive();
        $categories = [];
        foreach ($expressive['categories'] as $name => $count) {
            $categories[] = ['name' => $name, 'count' => $count];
        }
        usort($categories, static fn(array $a, array $b): int => strcmp((string) $a['name'], (string) $b['name']));
        jsonResponse([
            'total' => count($expressive['shapes']),
            'categories' => $categories,
            'words_count' => count($expressive['words']),
        ]);
    }

    if ($path === '/api/expressive/words' && $method === 'GET') {
        $expressive = loadExpressive();
        $q = strtolower(trim((string) ($_GET['q'] ?? '')));
        $category = trim((string) ($_GET['category'] ?? ''));
        $limit = min(max((int) ($_GET['limit'] ?? 50), 1), 200);

        $wordCounts = [];
        if ($category !== '') {
            foreach ($expressive['shapes'] as $shape) {
                if (($shape['category'] ?? '') === $category) {
                    $word = (string) ($shape['word'] ?? '');
                    $wordCounts[$word] = ($wordCounts[$word] ?? 0) + 1;
                }
            }
        } else {
            $wordCounts = $expressive['words'];
        }

        if ($q !== '') {
            $wordCounts = array_filter(
                $wordCounts,
                static fn($_count, $word): bool => str_contains(strtolower((string) $word), $q),
                ARRAY_FILTER_USE_BOTH
            );
        }

        ksort($wordCounts, SORT_NATURAL | SORT_FLAG_CASE);
        $words = [];
        $n = 0;
        foreach ($wordCounts as $word => $count) {
            if ($n++ >= $limit) {
                break;
            }
            $words[] = ['word' => $word, 'count' => $count];
        }

        jsonResponse(['words' => $words, 'total' => count($wordCounts)]);
    }

    if ($path === '/api/expressive/shapes' && $method === 'GET') {
        $expressive = loadExpressive();
        $ratingsPayload = readJsonFileOrDefault($exprRatingsFile, ['ratings' => []]);
        $ratings = is_array($ratingsPayload['ratings'] ?? null) ? $ratingsPayload['ratings'] : [];

        $page = max(1, (int) ($_GET['page'] ?? 1));
        $size = min(max((int) ($_GET['size'] ?? 20), 1), 100);
        $category = trim((string) ($_GET['category'] ?? ''));
        $word = trim((string) ($_GET['word'] ?? ''));
        $context = trim((string) ($_GET['context'] ?? ''));
        $search = strtolower(trim((string) ($_GET['search'] ?? '')));
        $sort = trim((string) ($_GET['sort'] ?? 'default'));
        $ratingFilter = trim((string) ($_GET['rating_filter'] ?? 'all'));

        $tagsStr = trim((string) ($_GET['tags'] ?? ''));
        $requiredTags = [];
        if ($tagsStr !== '') {
            foreach (explode(',', $tagsStr) as $tag) {
                $tag = trim($tag);
                if ($tag !== '') {
                    $requiredTags[$tag] = true;
                }
            }
        }

        $filtered = [];
        foreach ($expressive['shapes'] as $shape) {
            if ($category !== '' && (($shape['category'] ?? '') !== $category)) {
                continue;
            }
            if ($word !== '' && (($shape['word'] ?? '') !== $word)) {
                continue;
            }
            if ($context !== '' && (($shape['context'] ?? '') !== $context)) {
                continue;
            }

            if (!empty($requiredTags)) {
                $shapeTags = [];
                foreach (($shape['tags'] ?? []) as $tag) {
                    $shapeTags[(string) $tag] = true;
                }
                $motion = (string) ($shape['motion'] ?? '');
                $ok = true;
                foreach (array_keys($requiredTags) as $tag) {
                    if (!isset($shapeTags[$tag]) && $tag !== $motion) {
                        $ok = false;
                        break;
                    }
                }
                if (!$ok) {
                    continue;
                }
            }

            if ($search !== '') {
                $haystack = strtolower(implode(' ', [
                    (string) ($shape['name'] ?? ''),
                    (string) ($shape['word'] ?? ''),
                    (string) ($shape['context'] ?? ''),
                    (string) ($shape['category'] ?? ''),
                    implode(' ', (array) ($shape['keywords'] ?? [])),
                    implode(' ', (array) ($shape['tags'] ?? [])),
                ]));
                if (!str_contains($haystack, $search)) {
                    continue;
                }
            }

            if ($ratingFilter !== 'all') {
                $shapeName = (string) ($shape['name'] ?? '');
                $rating = $ratings[$shapeName] ?? null;
                if ($ratingFilter === 'unrated' && $rating !== null) {
                    continue;
                }
                if ($ratingFilter === 'rated' && ($rating === null || (int) $rating === 0)) {
                    continue;
                }
                if ($ratingFilter === 'rejected' && (int) $rating !== 0) {
                    continue;
                }
            }

            $filtered[] = $shape;
        }

        if ($sort === 'az') {
            usort($filtered, static fn(array $a, array $b): int => strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? '')));
        } elseif ($sort === 'za') {
            usort($filtered, static fn(array $a, array $b): int => strcasecmp((string) ($b['name'] ?? ''), (string) ($a['name'] ?? '')));
        } elseif ($sort === 'category') {
            usort($filtered, static function (array $a, array $b): int {
                $ca = strtolower((string) ($a['category'] ?? ''));
                $cb = strtolower((string) ($b['category'] ?? ''));
                if ($ca === $cb) {
                    return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
                }
                return strcmp($ca, $cb);
            });
        } elseif ($sort === 'rating') {
            usort($filtered, static function (array $a, array $b) use ($ratings): int {
                $ra = $ratings[(string) ($a['name'] ?? '')] ?? -1;
                $rb = $ratings[(string) ($b['name'] ?? '')] ?? -1;
                if ((int) $ra === (int) $rb) {
                    return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
                }
                return (int) $rb <=> (int) $ra;
            });
        }

        $total = count($filtered);
        $pages = max(1, (int) ceil($total / $size));
        $slice = array_slice($filtered, ($page - 1) * $size, $size);

        $shapesOut = array_map(static function (array $shape): array {
            return [
                'name' => (string) ($shape['name'] ?? ''),
                'word' => (string) ($shape['word'] ?? ''),
                'category' => (string) ($shape['category'] ?? ''),
                'context' => (string) ($shape['context'] ?? ''),
                'tags' => (array) ($shape['tags'] ?? []),
                'motion' => (string) ($shape['motion'] ?? 'static'),
                'keywords' => (array) ($shape['keywords'] ?? []),
            ];
        }, $slice);

        jsonResponse([
            'shapes' => $shapesOut,
            'total' => $total,
            'page' => $page,
            'pages' => $pages,
        ]);
    }

    if ($path === '/api/expressive/contexts' && $method === 'GET') {
        $expressive = loadExpressive();
        $category = trim((string) ($_GET['category'] ?? ''));
        $word = trim((string) ($_GET['word'] ?? ''));
        $limit = min(max((int) ($_GET['limit'] ?? 100), 1), 500);

        $contexts = [];
        $seen = [];
        foreach ($expressive['shapes'] as $shape) {
            if ($category !== '' && (($shape['category'] ?? '') !== $category)) {
                continue;
            }
            if ($word !== '' && (($shape['word'] ?? '') !== $word)) {
                continue;
            }
            $ctx = (string) ($shape['context'] ?? '');
            if ($ctx === '' || isset($seen[$ctx])) {
                continue;
            }
            $seen[$ctx] = true;
            $contexts[] = ['context' => $ctx, 'word' => (string) ($shape['word'] ?? '')];
        }

        $total = count($contexts);
        jsonResponse([
            'contexts' => array_slice($contexts, 0, $limit),
            'total' => $total,
            'capped' => $total > $limit,
        ]);
    }

    if ($path === '/api/expressive/svg/batch' && $method === 'POST') {
        $payload = readJsonBody();
        $names = $payload['names'] ?? null;
        if (!is_array($names) || count($names) > 120) {
            jsonError('"names" must be an array (max 120)', 400);
        }

        $expressive = loadExpressive();
        $result = [];
        foreach ($names as $name) {
            $shapeName = (string) $name;
            $shape = $expressive['map'][$shapeName] ?? null;
            if ($shape !== null && isset($shape['svg'])) {
                $result[$shapeName] = ['svg' => (string) $shape['svg']];
            }
        }
        jsonResponse($result);
    }

    if ($path === '/api/expressive/shape' && $method === 'GET') {
        $name = trim((string) ($_GET['name'] ?? ''));
        if ($name === '') {
            jsonError('Missing "name" parameter', 400);
        }
        $expressive = loadExpressive();
        $shape = $expressive['map'][$name] ?? null;
        if ($shape === null) {
            jsonError('Shape not found: ' . $name, 404);
        }
        jsonResponse([
            'name' => (string) ($shape['name'] ?? ''),
            'word' => (string) ($shape['word'] ?? ''),
            'category' => (string) ($shape['category'] ?? ''),
            'context' => (string) ($shape['context'] ?? ''),
            'tags' => (array) ($shape['tags'] ?? []),
            'motion' => (string) ($shape['motion'] ?? 'static'),
            'brief_text' => (string) ($shape['brief_text'] ?? ''),
            'svg' => (string) ($shape['svg'] ?? ''),
            'keywords' => (array) ($shape['keywords'] ?? []),
        ]);
    }

    if ($path === '/api/expressive/search' && $method === 'GET') {
        $expressive = loadExpressive();
        $q = strtolower(trim((string) ($_GET['q'] ?? '')));
        $limit = min(max((int) ($_GET['limit'] ?? 12), 1), 50);
        if (strlen($q) < 2) {
            jsonResponse(['results' => []]);
        }

        $results = [];
        $seen = [];
        $matchers = [
            static fn(array $s): bool => strtolower((string) ($s['name'] ?? '')) === $q,
            static fn(array $s): bool => str_starts_with(strtolower((string) ($s['name'] ?? '')), $q),
            static fn(array $s): bool => str_contains(strtolower((string) ($s['name'] ?? '')), $q)
                || str_contains(strtolower((string) ($s['word'] ?? '')), $q)
                || str_contains(strtolower((string) ($s['context'] ?? '')), $q)
                || str_contains(strtolower((string) ($s['category'] ?? '')), $q),
            static fn(array $s): bool => str_contains(strtolower(implode(' ', (array) ($s['keywords'] ?? []))), $q)
                || str_contains(strtolower(implode(' ', (array) ($s['tags'] ?? []))), $q),
        ];

        foreach ($matchers as $rank => $matcher) {
            foreach ($expressive['shapes'] as $shape) {
                $name = (string) ($shape['name'] ?? '');
                if (isset($seen[$name])) {
                    continue;
                }
                if ($matcher($shape)) {
                    $seen[$name] = true;
                    $results[] = [
                        'name' => $name,
                        'word' => (string) ($shape['word'] ?? ''),
                        'context' => (string) ($shape['context'] ?? ''),
                        'category' => (string) ($shape['category'] ?? ''),
                        'rank' => $rank,
                    ];
                    if (count($results) >= $limit) {
                        break 2;
                    }
                }
            }
        }

        jsonResponse(['results' => $results]);
    }

    if ($path === '/api/expressive/ratings') {
        if ($method === 'GET') {
            jsonResponse(readJsonFileOrDefault($exprRatingsFile, ['ratings' => new stdClass(), 'updated' => null]));
        }
        if ($method === 'POST') {
            $payload = readJsonBody();
            if (!isset($payload['ratings']) || !is_array($payload['ratings'])) {
                jsonError('Missing "ratings" object', 400);
            }
            $data = [
                'ratings' => $payload['ratings'],
                'updated' => gmdate('c'),
            ];
            writeJsonAtomically($exprRatingsFile, $data);
            jsonResponse(['ok' => true, 'count' => count($payload['ratings'])]);
        }
    }

    if ($path === '/api/expressive/metadata') {
        if ($method === 'GET') {
            jsonResponse(readJsonFileOrDefault($exprMetadataFile, ['metadata' => new stdClass(), 'updated' => null]));
        }
        if ($method === 'POST') {
            $payload = readJsonBody();
            if (!isset($payload['metadata']) || !is_array($payload['metadata'])) {
                jsonError('Missing "metadata" object', 400);
            }
            $data = [
                'metadata' => $payload['metadata'],
                'updated' => gmdate('c'),
            ];
            writeJsonAtomically($exprMetadataFile, $data);
            jsonResponse(['ok' => true, 'count' => count($payload['metadata'])]);
        }
    }

    if ($path === '/api/gerund/ratings') {
        if ($method === 'GET') {
            jsonResponse(readJsonFileOrDefault($gerundRatingsFile, []));
        }
        if ($method === 'POST') {
            $payload = readJsonBody();
            if (!is_array($payload)) {
                jsonError('Invalid ratings payload', 400);
            }
            writeJsonAtomically($gerundRatingsFile, $payload, true);
            jsonResponse(['ok' => true, 'count' => count($payload)]);
        }
    }

    jsonError('Not found', 404);
}

function serveStatic(string $path): void
{
    global $rootDir;

    $relative = ltrim(urldecode($path), '/');
    if ($relative === '') {
        $relative = 'index.php';
    }

    $fullPath = realpath($rootDir . '/' . $relative);

    if ($fullPath !== false && is_dir($fullPath)) {
        $index = $fullPath . '/index.php';
        if (is_file($index)) {
            outputFile($index);
        }
    }

    if ($fullPath !== false && is_file($fullPath) && str_starts_with($fullPath, realpath($rootDir))) {
        outputFile($fullPath);
    }

    $fallback = realpath($rootDir . '/' . $relative . '/index.php');
    if ($fallback !== false && is_file($fallback) && str_starts_with($fallback, realpath($rootDir))) {
        outputFile($fallback);
    }

    http_response_code(404);
    header('Content-Type: text/plain; charset=utf-8');
    echo 'Not Found';
}

function outputFile(string $path): void
{
    $mime = mime_content_type($path);
    if (!is_string($mime) || $mime === '') {
        $mime = 'application/octet-stream';
    }
    header('Content-Type: ' . $mime);
    readfile($path);
    exit;
}

function jsonResponse($payload, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($payload, JSON_UNESCAPED_UNICODE);
    exit;
}

function jsonError(string $message, int $status): void
{
    jsonResponse(['error' => $message], $status);
}

function readJsonBody(): array
{
    $raw = file_get_contents('php://input');
    if (!is_string($raw) || trim($raw) === '') {
        return [];
    }
    $data = json_decode($raw, true);
    if (!is_array($data)) {
        jsonError('Invalid JSON body', 400);
    }
    return $data;
}

function readJsonFileOrDefault(string $path, array $default): array
{
    if (!is_file($path)) {
        return $default;
    }
    $raw = file_get_contents($path);
    if (!is_string($raw) || $raw === '') {
        return $default;
    }
    $data = json_decode($raw, true);
    if (!is_array($data)) {
        return $default;
    }
    return $data;
}

function writeJsonAtomically(string $path, array $payload, bool $pretty = false): void
{
    $flags = JSON_UNESCAPED_UNICODE;
    if ($pretty) {
        $flags |= JSON_PRETTY_PRINT;
    }

    $json = json_encode($payload, $flags);
    if (!is_string($json)) {
        jsonError('Failed to serialize JSON', 500);
    }

    $tmpPath = $path . '.tmp';
    if (file_put_contents($tmpPath, $json) === false) {
        jsonError('Failed to write temp file', 500);
    }
    if (!rename($tmpPath, $path)) {
        @unlink($tmpPath);
        jsonError('Failed to write file', 500);
    }
}

function loadShapesIndex(): array
{
    global $shapesLibraryFile;

    static $cache = null;
    if (is_array($cache)) {
        return $cache;
    }

    $cache = [];
    $payload = readJsonFileOrDefault($shapesLibraryFile, ['shapes' => []]);
    foreach ((array) ($payload['shapes'] ?? []) as $shape) {
        if (!is_array($shape)) {
            continue;
        }
        $name = strtolower(trim((string) ($shape['name'] ?? '')));
        if ($name === '') {
            continue;
        }
        $cache[$name] = [
            'd' => (string) (($shape['detailed']['svg'] ?? '') ?: ''),
            'm' => (string) (($shape['minimal']['svg'] ?? '') ?: ''),
        ];
    }
    return $cache;
}

function loadExpressive(): array
{
    global $expressiveShapesFile;

    static $cache = null;
    if (is_array($cache)) {
        return $cache;
    }

    $payload = readJsonFileOrDefault($expressiveShapesFile, ['shapes' => []]);
    $shapes = [];

    if (isset($payload['shapes']) && is_array($payload['shapes'])) {
        $shapes = $payload['shapes'];
    } elseif (array_is_list($payload)) {
        $shapes = $payload;
    }

    $map = [];
    $words = [];
    $categories = [];
    foreach ($shapes as $shape) {
        if (!is_array($shape)) {
            continue;
        }
        $name = (string) ($shape['name'] ?? '');
        if ($name !== '') {
            $map[$name] = $shape;
        }

        $word = (string) ($shape['word'] ?? '');
        if ($word !== '') {
            $words[$word] = ($words[$word] ?? 0) + 1;
        }

        $category = (string) ($shape['category'] ?? '');
        if ($category !== '') {
            $categories[$category] = ($categories[$category] ?? 0) + 1;
        }
    }

    $cache = [
        'shapes' => $shapes,
        'map' => $map,
        'words' => $words,
        'categories' => $categories,
    ];

    return $cache;
}

function isKnownWord(string $token): bool
{
    static $dict = null;

    if (is_array($dict)) {
        return isset($dict[$token]);
    }

    $dict = [];

    if (function_exists('pspell_new') && function_exists('pspell_check')) {
        $link = @pspell_new('en');
        if ($link !== false && pspell_check($link, $token)) {
            return true;
        }
    }

    $expressive = loadExpressive();
    foreach ($expressive['shapes'] as $shape) {
        foreach ([(string) ($shape['word'] ?? ''), (string) ($shape['name'] ?? ''), (string) ($shape['category'] ?? ''), (string) ($shape['context'] ?? '')] as $text) {
            foreach (preg_split('/[^a-zA-Z]+/', strtolower($text)) as $piece) {
                if ($piece !== '') {
                    $dict[$piece] = true;
                }
            }
        }
        foreach ((array) ($shape['keywords'] ?? []) as $kw) {
            $w = strtolower(trim((string) $kw));
            if ($w !== '') {
                $dict[$w] = true;
            }
        }
    }

    return isset($dict[$token]);
}

function suggestWords(string $token): array
{
    $expressive = loadExpressive();
    $candidates = [];

    foreach ($expressive['words'] as $word => $_count) {
        $w = strtolower((string) $word);
        if ($w === '') {
            continue;
        }

        if (str_starts_with($w, substr($token, 0, 1))) {
            $distance = levenshtein($token, $w);
            if ($distance <= 2) {
                $candidates[$w] = $distance;
            }
        }
    }

    asort($candidates);
    return array_slice(array_keys($candidates), 0, 5);
}
