#!/usr/bin/env php
<?php

declare(strict_types=1);

use RedMCP\McpDispatcher;
use RedMCP\McpDebugLogger;
use RedMCP\McpEnvironment;
use RedMCP\McpStdioServer;
use RedMCP\RedmineClient;

require __DIR__ . '/../vendor/autoload.php';

$env = McpEnvironment::load(__DIR__ . '/../.env');
$server = new McpStdioServer(
    new McpDispatcher(
        RedmineClient::fromCredentials($env['redmine_url'], $env['redmine_api_key']),
        new McpDebugLogger($env['mcp_debug_log'])
    )
);
$server->run();
exit(0);
__halt_compiler();

function main(): void
{
    $env = loadEnv(__DIR__ . '/../.env');
    $url = getenv('REDMINE_URL') ?: ($env['REDMINE_URL'] ?? 'http://192.168.50.170');
    $apiKey = getenv('REDMINE_API_KEY') ?: getenv('REDMNINE_API_KEY') ?: ($env['REDMINE_API_KEY'] ?? $env['REDMNINE_API_KEY'] ?? null);

    if (!is_string($apiKey) || trim($apiKey) === '') {
        fwrite(STDERR, "REDMINE_API_KEY is required in the environment or redMCP/.env\n");
        exit(1);
    }

    $server = new RedmcpStdioServer(RedmineClient::fromCredentials(rtrim($url, '/'), $apiKey));
    $server->run();
}

/**
 * @return array<string,string>
 */
function loadEnv(string $path): array
{
    if (!is_file($path)) {
        return [];
    }

    $values = [];
    foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
        $line = trim($line);
        if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
            continue;
        }
        [$key, $value] = explode('=', $line, 2);
        $values[trim($key)] = trim(trim($value), "\"'");
    }

    return $values;
}

final class RedmcpStdioServer
{
    private RedmineClient $redmine;

    public function __construct(RedmineClient $redmine)
    {
        $this->redmine = $redmine;
    }

    public function run(): void
    {
        while (($message = $this->readMessage(STDIN)) !== null) {
            $this->handleMessage($message);
        }
    }

    /**
     * @param resource $stream
     *
     * @return array<string,mixed>|null
     */
    private function readMessage($stream): ?array
    {
        $headers = [];
        while (($line = fgets($stream)) !== false) {
            $line = rtrim($line, "\r\n");
            if ($line === '') {
                break;
            }
            if (!str_contains($line, ':')) {
                $decoded = json_decode($line, true);
                return is_array($decoded) ? $decoded : null;
            }
            [$name, $value] = explode(':', $line, 2);
            $headers[strtolower(trim($name))] = trim($value);
        }

        if ($line === false && $headers === []) {
            return null;
        }

        $length = isset($headers['content-length']) ? (int) $headers['content-length'] : 0;
        if ($length <= 0) {
            return null;
        }

        $body = '';
        while (strlen($body) < $length && !feof($stream)) {
            $chunk = fread($stream, $length - strlen($body));
            if ($chunk === false || $chunk === '') {
                break;
            }
            $body .= $chunk;
        }

        $decoded = json_decode($body, true);
        return is_array($decoded) ? $decoded : null;
    }

    /**
     * @param array<string,mixed> $message
     */
    private function handleMessage(array $message): void
    {
        $id = $message['id'] ?? null;
        $method = (string) ($message['method'] ?? '');

        if ($id === null) {
            return;
        }

        try {
            $result = $this->dispatch($method, is_array($message['params'] ?? null) ? $message['params'] : []);
            $this->writeMessage(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result]);
        } catch (Throwable $exception) {
            $this->writeMessage([
                'jsonrpc' => '2.0',
                'id' => $id,
                'error' => [
                    'code' => -32000,
                    'message' => $exception->getMessage(),
                ],
            ]);
        }
    }

    /**
     * @param array<string,mixed> $params
     *
     * @return array<string,mixed>
     */
    private function dispatch(string $method, array $params): array
    {
        switch ($method) {
            case 'initialize':
                return [
                    'protocolVersion' => '2024-11-05',
                    'capabilities' => [
                        'tools' => ['listChanged' => false],
                    ],
                    'serverInfo' => [
                        'name' => 'redMCP',
                        'version' => '0.1.0',
                    ],
                ];
            case 'ping':
                return [];
            case 'tools/list':
                return ['tools' => $this->tools()];
            case 'tools/call':
                return $this->callTool($params);
            case 'resources/list':
                return ['resources' => []];
            case 'prompts/list':
                return ['prompts' => []];
            default:
                throw new RuntimeException('Unsupported MCP method: ' . $method);
        }
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function tools(): array
    {
        return [
            $this->tool('redmine_list_issues', 'List Redmine issues using native /issues.json filters.', [
                'filters' => ['type' => 'object', 'description' => 'Redmine issue list filters such as project_id, status_id, query_id, sort, offset, and limit.'],
            ]),
            $this->tool('redmine_search', 'Search Redmine using native /search.json.', [
                'query' => ['type' => 'string'],
                'params' => ['type' => 'object', 'description' => 'Redmine search params such as project_id, all_words, titles_only, offset, and limit.'],
            ], ['query']),
            $this->tool('redmine_search_issues', 'Search only issues using native /search.json.', [
                'query' => ['type' => 'string'],
                'params' => ['type' => 'object', 'description' => 'Additional Redmine search params.'],
            ], ['query']),
            $this->tool('redmine_get_issue', 'Fetch one Redmine issue.', [
                'issue_id' => ['type' => 'integer'],
                'include' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Issue includes such as journals, attachments, children, relations, changesets.'],
            ], ['issue_id']),
            $this->tool('redmine_issue_with_helpdesk', 'Fetch one issue plus Helpdesk ticket/message context when available.', [
                'issue_id' => ['type' => 'integer'],
                'message_limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 200],
                'include' => ['type' => 'array', 'items' => ['type' => 'string']],
            ], ['issue_id']),
            $this->tool('redmine_create_issue', 'Create a Redmine issue.', [
                'fields' => ['type' => 'object', 'description' => 'Issue fields including project_id and subject.'],
            ], ['fields']),
            $this->tool('redmine_update_issue', 'Update a Redmine issue. Helpdesk email is opt-in.', [
                'issue_id' => ['type' => 'integer'],
                'fields' => ['type' => 'object'],
                'options' => ['type' => 'object', 'description' => 'Pass send_helpdesk_email=true only for customer-visible Helpdesk replies.'],
            ], ['issue_id', 'fields']),
            $this->tool('redmine_delete_issue', 'Delete a Redmine issue.', [
                'issue_id' => ['type' => 'integer'],
            ], ['issue_id']),
            $this->tool('redmine_send_helpdesk_response', 'Send a customer-visible Helpdesk email response.', [
                'issue_id' => ['type' => 'integer'],
                'content' => ['type' => 'string'],
                'options' => ['type' => 'object', 'description' => 'Optional to_address, cc_address, bcc_address, and status_id.'],
            ], ['issue_id', 'content']),
        ];
    }

    /**
     * @param array<string,mixed> $properties
     * @param array<int,string> $required
     *
     * @return array<string,mixed>
     */
    private function tool(string $name, string $description, array $properties, array $required = []): array
    {
        return [
            'name' => $name,
            'description' => $description,
            'inputSchema' => [
                'type' => 'object',
                'properties' => $properties,
                'required' => $required,
                'additionalProperties' => false,
            ],
        ];
    }

    /**
     * @param array<string,mixed> $params
     *
     * @return array<string,mixed>
     */
    private function callTool(array $params): array
    {
        $name = (string) ($params['name'] ?? '');
        $arguments = is_array($params['arguments'] ?? null) ? $params['arguments'] : [];

        switch ($name) {
            case 'redmine_list_issues':
                $result = $this->redmine->filterIssues($this->objectArg($arguments, 'filters'));
                break;
            case 'redmine_search':
                $result = $this->redmine->search($this->stringArg($arguments, 'query'), $this->objectArg($arguments, 'params'));
                break;
            case 'redmine_search_issues':
                $result = $this->redmine->searchIssues($this->stringArg($arguments, 'query'), $this->objectArg($arguments, 'params'));
                break;
            case 'redmine_get_issue':
                $result = $this->redmine->issue($this->intArg($arguments, 'issue_id'), $this->stringListArg($arguments, 'include', ['journals', 'attachments']));
                break;
            case 'redmine_issue_with_helpdesk':
                $result = $this->redmine->issueWithHelpdesk(
                    $this->intArg($arguments, 'issue_id'),
                    $this->intArg($arguments, 'message_limit', 100),
                    $this->stringListArg($arguments, 'include', ['journals', 'attachments'])
                );
                break;
            case 'redmine_create_issue':
                $result = $this->redmine->createIssue($this->objectArg($arguments, 'fields'));
                break;
            case 'redmine_update_issue':
                $result = ['ok' => $this->redmine->updateIssue($this->intArg($arguments, 'issue_id'), $this->objectArg($arguments, 'fields'), $this->objectArg($arguments, 'options'))];
                break;
            case 'redmine_delete_issue':
                $result = ['ok' => $this->redmine->deleteIssue($this->intArg($arguments, 'issue_id'))];
                break;
            case 'redmine_send_helpdesk_response':
                $result = $this->redmine->sendHelpdeskIssueResponse($this->intArg($arguments, 'issue_id'), $this->stringArg($arguments, 'content'), $this->objectArg($arguments, 'options'));
                break;
            default:
                throw new RuntimeException('Unknown tool: ' . $name);
        }

        $encoded = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        if ($encoded === false) {
            throw new RuntimeException('Could not encode tool result.');
        }

        return [
            'content' => [
                [
                    'type' => 'text',
                    'text' => $encoded,
                ],
            ],
        ];
    }

    /**
     * @param array<string,mixed> $arguments
     *
     * @return array<string,mixed>
     */
    private function objectArg(array $arguments, string $key): array
    {
        return is_array($arguments[$key] ?? null) ? $arguments[$key] : [];
    }

    /**
     * @param array<string,mixed> $arguments
     */
    private function stringArg(array $arguments, string $key): string
    {
        $value = trim((string) ($arguments[$key] ?? ''));
        if ($value === '') {
            throw new RuntimeException($key . ' is required.');
        }

        return $value;
    }

    /**
     * @param array<string,mixed> $arguments
     */
    private function intArg(array $arguments, string $key, ?int $default = null): int
    {
        if (!isset($arguments[$key])) {
            if ($default !== null) {
                return $default;
            }
            throw new RuntimeException($key . ' is required.');
        }

        return (int) $arguments[$key];
    }

    /**
     * @param array<string,mixed> $arguments
     * @param array<int,string> $default
     *
     * @return array<int,string>
     */
    private function stringListArg(array $arguments, string $key, array $default): array
    {
        if (!isset($arguments[$key]) || !is_array($arguments[$key])) {
            return $default;
        }

        return array_values(array_filter(array_map('strval', $arguments[$key])));
    }

    /**
     * @param array<string,mixed> $message
     */
    private function writeMessage(array $message): void
    {
        $body = json_encode($message, JSON_UNESCAPED_SLASHES);
        if ($body === false) {
            return;
        }

        fwrite(STDOUT, 'Content-Length: ' . strlen($body) . "\r\n\r\n" . $body);
        fflush(STDOUT);
    }
}
