<?php

declare(strict_types=1);

/**
 * File-backed MCP lifecycle session store.
 *
 * MCP Streamable HTTP requests are independent PHP requests on shared hosting,
 * so lifecycle state cannot live only inside the server singleton.
 */
final class MyronMcpSessionStore
{
    private const TTL_SECONDS = 3600;

    public function __construct(
        private readonly string $directory,
    ) {
    }

    public static function default(): self
    {
        return new self(dirname(__DIR__, 2) . '/data/mcp/sessions');
    }

    public function create(MyronActionCallerContext $caller): string
    {
        $this->ensureDirectory();
        $this->garbageCollect();

        $sessionId = 'mcp_' . bin2hex(random_bytes(16));
        $this->write($sessionId, [
            'id' => $sessionId,
            'callerType' => $caller->callerType,
            'callerId' => $caller->callerId,
            'createdAt' => time(),
            'expiresAt' => time() + self::TTL_SECONDS,
            'initialized' => false,
        ]);

        return $sessionId;
    }

    public function markInitialized(string $sessionId, MyronActionCallerContext $caller): void
    {
        $record = $this->requireValid($sessionId, $caller);
        $record['initialized'] = true;
        $record['expiresAt'] = time() + self::TTL_SECONDS;
        $this->write($sessionId, $record);
    }

    /**
     * @return array<string, mixed>
     */
    public function requireValid(string $sessionId, MyronActionCallerContext $caller): array
    {
        $sessionId = trim($sessionId);
        if (!$this->isSafeSessionId($sessionId)) {
            throw new RuntimeException('MCP session is missing or malformed.');
        }

        $path = $this->pathFor($sessionId);
        if (!is_file($path)) {
            throw new RuntimeException('MCP session was not found.');
        }

        $decoded = json_decode((string) file_get_contents($path), true);
        if (!is_array($decoded)) {
            @unlink($path);
            throw new RuntimeException('MCP session is unreadable.');
        }

        $expiresAt = (int) ($decoded['expiresAt'] ?? 0);
        if ($expiresAt < time()) {
            @unlink($path);
            throw new RuntimeException('MCP session expired.');
        }

        if (!hash_equals((string) ($decoded['callerType'] ?? ''), $caller->callerType)
            || !hash_equals((string) ($decoded['callerId'] ?? ''), $caller->callerId)
        ) {
            throw new RuntimeException('MCP session does not match the authorized caller.');
        }

        return $decoded;
    }

    private function write(string $sessionId, array $record): void
    {
        $this->ensureDirectory();
        $path = $this->pathFor($sessionId);
        $json = json_encode($record, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
        if ($json === false) {
            throw new RuntimeException('Failed to encode MCP session.');
        }
        if (file_put_contents($path, $json, LOCK_EX) === false) {
            throw new RuntimeException('Failed to write MCP session.');
        }
    }

    private function ensureDirectory(): void
    {
        if (is_dir($this->directory)) {
            return;
        }
        if (!mkdir($this->directory, 0775, true) && !is_dir($this->directory)) {
            throw new RuntimeException('Failed to create MCP session directory.');
        }
    }

    private function garbageCollect(): void
    {
        if (!is_dir($this->directory)) {
            return;
        }
        foreach (glob($this->directory . '/mcp_*.json') ?: [] as $path) {
            $decoded = json_decode((string) @file_get_contents($path), true);
            if (!is_array($decoded) || (int) ($decoded['expiresAt'] ?? 0) < time()) {
                @unlink($path);
            }
        }
    }

    private function pathFor(string $sessionId): string
    {
        return rtrim($this->directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $sessionId . '.json';
    }

    private function isSafeSessionId(string $sessionId): bool
    {
        return (bool) preg_match('/^mcp_[a-f0-9]{32}$/', $sessionId);
    }
}
