<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Tools;

use Closure;
use Plugin\tnt_mcp\src\Mcp\Contracts\Tool;
use Plugin\tnt_mcp\src\Mcp\Log\LogLevel;
use Plugin\tnt_mcp\src\Mcp\Log\LogQuery;
use Plugin\tnt_mcp\src\Mcp\Log\LogReader;
use Plugin\tnt_mcp\src\Mcp\Log\LogSource;
use Plugin\tnt_mcp\src\Mcp\Mode\EndpointClass;

/**
 * READ tool: the shop's four log sources, aggregated first and enumerated second.
 *
 * The header — `totalMatching`, `byLevel`, `byChannel`, `topMessages` — is the reason this is a
 * tool and not a documented `run_sql_read` query. A raw `SELECT * FROM tjtllog` answers "here are
 * 50 lines"; this answers "one plugin timed out 1200 times since 14:00", which is the question the
 * caller actually had. The level normalisation across two mutually inverted scales and the
 * secret-scrubbing are the other two things a hand-written SELECT would get wrong.
 *
 * @see \Plugin\tnt_mcp\src\Mcp\Log\LogLevel  for the inverted-scale problem
 * @see \Plugin\tnt_mcp\src\Mcp\Log\LogReader for the queries
 */
final class ReadLogs implements Tool
{
    /**
     * @param Closure(): LogReader $readerFactory must not touch the DB or the filesystem until run()
     */
    public function __construct(private readonly Closure $readerFactory)
    {
    }

    public function name(): string
    {
        return 'read_logs';
    }

    public function description(): string
    {
        // Hard limit: 140 characters, enforced by a test. Tool descriptions are prompt, and this
        // server ships more than a dozen of them.
        return 'Read shop logs (system, settings audit, payment, admin auth file) with level, '
            . 'time and text filters plus aggregated counts.';
    }

    /** @return array<string,mixed> */
    public function inputSchema(): array
    {
        return [
            'type'       => 'object',
            'properties' => [
                'source'        => [
                    'type'        => 'string',
                    'enum'        => LogSource::names(),
                    'description' => 'system = tjtllog (shop + plugin errors); settings = the '
                        . 'settings-change audit; payment = tzahlungslog; auth_file = '
                        . 'jtllogs/auth.log (file logging only).',
                ],
                'level'         => [
                    'type'        => 'string',
                    'enum'        => LogLevel::ORDER,
                    'description' => 'Minimum severity. Default "' . LogQuery::DEFAULT_LEVEL
                        . '". The settings source has no levels.',
                ],
                'since'         => [
                    'type'        => 'string',
                    'description' => 'ISO-8601, or a span "90m"/"24h"/"7d"/"2w". Default "'
                        . LogQuery::DEFAULT_SINCE . '".',
                ],
                'until'         => [
                    'type'        => 'string',
                    'description' => 'ISO-8601. Default now.',
                ],
                'search'        => [
                    'type'        => 'string',
                    'description' => 'Literal substring of the message ("%" and "_" are not '
                        . 'wildcards); for settings, of the setting name.',
                ],
                'channel'       => [
                    'type'        => 'string',
                    'description' => 'Exact match on the channel column (system = cKey, payment = '
                        . 'cModulId, settings = cEinstellungenName). See byChannel for its values.',
                ],
                'transactionId' => [
                    'type'        => 'string',
                    'description' => 'Payment only: transaction id. Returns that transaction\'s '
                        . 'whole log.',
                ],
                'limit'         => [
                    'type'        => 'integer',
                    'minimum'     => 1,
                    'maximum'     => LogQuery::MAX_LIMIT,
                    'description' => 'Entries per page. Default ' . LogQuery::DEFAULT_LIMIT
                        . '. The aggregation header covers the whole window.',
                ],
                'cursor'        => [
                    'type'        => 'string',
                    'description' => 'The nextCursor of a previous call, verbatim.',
                ],
            ],
            'required'             => ['source'],
            'additionalProperties' => false,
        ];
    }

    public function isWrite(): bool
    {
        return false;
    }

    public function scope(): string
    {
        return 'logs:read';
    }

    public function endpointClass(): EndpointClass
    {
        return EndpointClass::Reversible;
    }

    /**
     * @param array<string,mixed> $args
     * @return array<string,mixed>
     */
    public function run(array $args): array
    {
        $query = LogQuery::fromArgs($args);
        $page  = ($this->readerFactory)()->read($query);

        $result = [
            'source' => $query->source->value,
            'window' => [
                'since' => $query->since->format(\DATE_ATOM),
                'until' => $query->until->format(\DATE_ATOM),
            ],
        ];
        if (!$page->available) {
            // A source the shop has switched off is a fact about the shop, not a failure of the
            // call — the caller needs to route around it, not retry it.
            $result['available'] = false;
            $result['reason']    = $page->reason;
            $result['notes']     = \array_merge($query->notes, $page->notes);

            return $result;
        }

        $result['totalMatching'] = $page->totalMatching;
        if ($page->byLevel !== null) {
            $result['byLevel'] = $page->byLevel;
        }
        if ($page->byChannel !== null) {
            $result['byChannel'] = $page->byChannel;
        }
        if ($page->byAdmin !== null) {
            $result['byAdmin'] = $page->byAdmin;
        }
        $result['topMessages'] = $page->topMessages;
        $result['entries']     = $page->entries;
        if ($page->nextCursor !== null) {
            $result['nextCursor'] = $page->nextCursor;
        }
        $result['notes'] = \array_merge($query->notes, $page->notes);

        return $result;
    }
}
