<?php
declare(strict_types=1);

require_once __DIR__ . '/storage.php';

function chatStoreDirectory(): string
{
    return dirname((string) config('CHAT_HISTORY_FILE')) . '/chat_histories';
}

function chatStoreIsSplit(): bool
{
    $marker = chatStoreDirectory() . '/.ready.json';
    clearstatcache(true, $marker);
    return is_file($marker);
}

function chatStoreGate(int $operation, callable $callback)
{
    // Shared for normal operations; exclusive only for migration and clearing all chats.
    return withJsonFileLock((string) config('CHAT_HISTORY_FILE') . '.store', $operation, $callback);
}

function chatStoreFilename(string $key): string
{
    if (preg_match('/^(dialog|chat):((?:chat)?[0-9]+)$/D', $key, $match)) {
        return $match[1] . '_' . $match[2] . '.json';
    }
    return 'key_' . hash('sha256', $key) . '.json';
}

function chatStoreFile(string $key): string
{
    return chatStoreDirectory() . '/' . chatStoreFilename($key);
}

function chatStoreReadRecordUnlocked(string $file, ?string $key = null): array
{
    clearstatcache(true, $file);
    if (!is_file($file)) return ['chats' => []];
    if (!is_readable($file) || filesize($file) === 0) throw new RuntimeException('Unreadable chat record: ' . $file);
    $record = readJsonFileUnlocked($file);
    if (!isset($record['chats']) || !is_array($record['chats']) || count($record['chats']) > 1) {
        throw new RuntimeException('Invalid chat record: ' . $file);
    }
    foreach ($record['chats'] as $storedKey => $chat) {
        if (!is_array($chat) || ($key !== null && (string) $storedKey !== $key)
            || basename($file) !== chatStoreFilename((string) $storedKey)) {
            throw new RuntimeException('Mismatched chat record: ' . $file);
        }
    }
    return $record;
}

function chatStoreRead(string $key): array
{
    return chatStoreGate(LOCK_SH, static function () use ($key): array {
        if (chatStoreIsSplit()) {
            $file = chatStoreFile($key);
            $record = withJsonFileLock($file, LOCK_SH, static fn (): array => chatStoreReadRecordUnlocked($file, $key));
            return (array) ($record['chats'][$key] ?? []);
        }
        $history = readJsonFile((string) config('CHAT_HISTORY_FILE'), ['chats' => []]);
        if (isset($history['chats'][$key])) return (array) $history['chats'][$key];
        $snapshot = readJsonFile(dirname((string) config('CHAT_HISTORY_FILE')) . '/followup_states/' . sha1($key) . '.json');
        return (array) ($snapshot['chat'] ?? []);
    });
}

function chatStoreUpdate(string $key, callable $mutator): array
{
    return chatStoreGate(LOCK_SH, static function () use ($key, $mutator): array {
        if (!chatStoreIsSplit()) return updateJsonFile((string) config('CHAT_HISTORY_FILE'), ['chats' => []], $mutator);
        $file = chatStoreFile($key);
        return withJsonFileLock($file, LOCK_EX, static function () use ($file, $key, $mutator): array {
            $updated = $mutator(chatStoreReadRecordUnlocked($file, $key));
            if (!is_array($updated) || !isset($updated['chats']) || !is_array($updated['chats'])
                || array_diff(array_keys($updated['chats']), [$key]) !== []) {
                throw new RuntimeException('Chat mutator may only change its own chat');
            }
            writeJsonFileUnlocked($file, $updated);
            return $updated;
        });
    });
}

function chatStoreIterate(bool $includeFallback = false): Generator
{
    // Do not hold a shared layout lock across yield: callers may update the current chat.
    if (chatStoreIsSplit()) {
        foreach (new DirectoryIterator(chatStoreDirectory()) as $entry) {
            if (!$entry->isFile() || $entry->getExtension() !== 'json' || $entry->getFilename()[0] === '.') continue;
            $path = $entry->getPathname();
            try {
                $record = withJsonFileLock($path, LOCK_SH, static fn (): array => chatStoreReadRecordUnlocked($path));
            } catch (Throwable $error) {
                // One damaged record must not starve the rest of the queue.
                if (function_exists('logMessage')) logMessage('error', 'Chat record unreadable; continuing iteration', ['file' => basename($path), 'error' => $error->getMessage()]);
                continue;
            }
            foreach ((array) ($record['chats'] ?? []) as $key => $chat) {
                if (!is_array($chat) || chatStoreFilename((string) $key) !== $entry->getFilename()) {
                    throw new RuntimeException('Invalid chat record: ' . $entry->getFilename());
                }
                yield (string) $key => $chat;
            }
        }
        return;
    }
    $history = readJsonFile((string) config('CHAT_HISTORY_FILE'), ['chats' => []]);
    $seen = [];
    foreach ((array) ($history['chats'] ?? []) as $key => $chat) {
        if (!is_array($chat)) continue;
        $seen[(string) $key] = true;
        yield (string) $key => $chat;
    }
    unset($history);
    if (!$includeFallback) return;
    foreach (glob(dirname((string) config('CHAT_HISTORY_FILE')) . '/followup_states/*.json') ?: [] as $file) {
        $snapshot = readJsonFile($file);
        $key = (string) ($snapshot['chat_key'] ?? '');
        if ($key !== '' && !isset($seen[$key]) && is_array($snapshot['chat'] ?? null)) yield $key => $snapshot['chat'];
    }
}

function chatStoreClear(?string $key): void
{
    chatStoreGate(LOCK_EX, static function () use ($key): void {
        if (chatStoreIsSplit()) {
            // Empty records prevent old snapshots from resurrecting a cleared conversation.
            $files = $key !== null ? [chatStoreFile($key)] : (glob(chatStoreDirectory() . '/*.json') ?: []);
            foreach ($files as $file) {
                if (basename($file)[0] !== '.') writeJsonFile($file, ['chats' => []]);
            }
            return;
        }
        $file = (string) config('CHAT_HISTORY_FILE');
        updateJsonFile($file, ['chats' => []], static function (array $history) use ($key): array {
            if ($key === null) return ['chats' => []];
            unset($history['chats'][$key]);
            return $history;
        });
        $directory = dirname($file) . '/followup_states';
        $files = $key !== null ? [$directory . '/' . sha1($key) . '.json'] : (glob($directory . '/*.json') ?: []);
        foreach ($files as $path) if (is_file($path) && !unlink($path)) throw new RuntimeException('Cannot clear snapshot: ' . $path);
    });
}

function migrateChatStore(): array
{
    return chatStoreGate(LOCK_EX, static function (): array {
        if (chatStoreIsSplit()) return ['ok' => true, 'already_migrated' => true];
        $legacy = (string) config('CHAT_HISTORY_FILE');
        return withJsonFileLock($legacy, LOCK_EX, static function () use ($legacy): array {
            $directory = chatStoreDirectory();
            if (is_dir($directory)) throw new RuntimeException('Target directory exists without migration marker. Inspect it before retrying.');
            $staging = $directory . '.migrating-' . gmdate('Ymd-His') . '-' . bin2hex(random_bytes(4));
            ensureJsonDirectory($staging);
            $history = readJsonFileUnlocked($legacy, ['chats' => []]);
            if (!isset($history['chats']) || !is_array($history['chats'])) throw new RuntimeException('Invalid legacy history structure');
            $count = 0; $fallback = 0; $seen = [];
            $store = static function (string $key, array $chat) use ($staging, &$seen, &$count): void {
                $path = $staging . '/' . chatStoreFilename($key);
                $record = ['chats' => [$key => $chat]];
                writeJsonFile($path, $record);
                if (readJsonFile($path) !== $record) throw new RuntimeException('Migration verification failed: ' . $key);
                $seen[$key] = true;
                $count++;
            };
            foreach ($history['chats'] as $key => $chat) {
                if (!is_array($chat)) throw new RuntimeException('Invalid legacy chat: ' . $key);
                $store((string) $key, $chat);
            }
            unset($history);
            foreach (glob(dirname($legacy) . '/followup_states/*.json') ?: [] as $file) {
                $snapshot = readJsonFile($file);
                $key = (string) ($snapshot['chat_key'] ?? '');
                if ($key === '' || !is_array($snapshot['chat'] ?? null)) throw new RuntimeException('Invalid fallback snapshot: ' . $file);
                if (!isset($seen[$key])) { $store($key, $snapshot['chat']); $fallback++; }
            }
            $legacyHash = is_file($legacy) ? hash_file('sha256', $legacy) : null;
            writeJsonFile($staging . '/.ready.json', ['version' => 1, 'migrated_at' => date('c'), 'chats' => $count, 'fallback_chats' => $fallback, 'legacy_sha256' => $legacyHash]);
            if (!rename($staging, $directory)) throw new RuntimeException('Cannot activate split history');
            // The original history and snapshots remain unchanged as a migration backup.
            return ['ok' => true, 'chats' => $count, 'fallback_chats' => $fallback, 'directory' => $directory, 'legacy_sha256' => $legacyHash];
        });
    });
}
