<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Tools;

use Closure;
use Plugin\tnt_mcp\src\Mcp\Backend\AdminIdentity;
use Plugin\tnt_mcp\src\Mcp\Backend\LegacyTokenException;
use Plugin\tnt_mcp\src\Mcp\Backend\UnknownAdminException;
use Plugin\tnt_mcp\src\Mcp\Contracts\Tool;
use Plugin\tnt_mcp\src\Mcp\Contracts\TokenAwareTool;
use Plugin\tnt_mcp\src\Mcp\Mode\EndpointClass;
use Plugin\tnt_mcp\src\Mcp\Settings\SettingsPermissions;
use Plugin\tnt_mcp\src\Mcp\Settings\SettingsRegistry;
use Plugin\tnt_mcp\src\Mcp\Settings\SettingsWriter;
use RuntimeException;

/**
 * WRITE tool: set one shop setting by its key, by replaying the shop's own admin-form save
 * ({@see SettingsWriter}). Destructive by classification (not because it typically IS destructive
 * — because writes are irreversible-by-default until proven otherwise) so {@see
 * \Plugin\tnt_mcp\src\Mcp\Mode\ModeGuard} denies it whenever the plugin runs in limited mode.
 *
 * Token-aware for the same reason `invoke_admin_action` is (decision Q2): the acting admin's rights
 * decide, not the MCP client's. This tool used to be identity-free, which made it the one write path
 * in the plugin that skipped the permission model entirely — scope plus full mode were the only
 * gates, so any admin who could authorise a connector could reconfigure the shop through it
 * regardless of their own backend rights. {@see SettingsPermissions} names the right each setting
 * needs, mirroring the shop's own settings controller.
 */
final class SetSetting implements Tool, TokenAwareTool
{
    /**
     * @param Closure(): SettingsWriter        $writerFactory   built lazily — see {@see
     *                                                          \Plugin\tnt_mcp\src\Mcp\Tools\GetSetting}
     *                                                          for why.
     * @param Closure(string): AdminIdentity   $identityResolver token audit id => the acting admin
     */
    public function __construct(
        private readonly SettingsRegistry $registry,
        private readonly Closure $writerFactory,
        private readonly Closure $identityResolver,
    ) {
    }

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

    public function description(): string
    {
        return 'Set one shop setting by key (see list_settings), exactly as an admin would save '
            . 'it. Full mode only.';
    }

    /** @return array<string,mixed> */
    public function inputSchema(): array
    {
        return [
            'type'       => 'object',
            'properties' => [
                'key'   => ['type' => 'string'],
                'value' => ['type' => ['string', 'number', 'boolean']],
            ],
            'required'             => ['key', 'value'],
            'additionalProperties' => false,
        ];
    }

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

    public function scope(): string
    {
        return 'settings:write';
    }

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

    /**
     * @param array<string,mixed> $args
     * @return array<string,mixed>
     */
    public function run(array $args): array
    {
        // Required by {@see TokenAwareTool}: never guess an administrator on a write path.
        throw new RuntimeException(
            'set_setting: no acting administrator — this tool must be called with an OAuth token '
            . 'that records the admin who approved it.'
        );
    }

    /**
     * @param array<string,mixed> $args
     * @return array<string,mixed>
     */
    public function runForToken(array $args, string $tokenId): array
    {
        $key = $args['key'] ?? null;
        if (!\is_string($key) || $key === '') {
            throw new RuntimeException('set_setting: "key" is required');
        }
        if (!\array_key_exists('value', $args)) {
            throw new RuntimeException('set_setting: "value" is required');
        }
        if (!$this->registry->has($key)) {
            throw new RuntimeException('set_setting: unknown setting "' . $key . '"');
        }

        $row = $this->registry->get($key);
        if (!$row->isWritable()) {
            throw new RuntimeException('set_setting: setting "' . $key . '" is not writable (mode=' . $row->mode . ')');
        }

        $permission = SettingsPermissions::forRow($row);
        try {
            $identity = ($this->identityResolver)($tokenId);
        } catch (LegacyTokenException | UnknownAdminException $e) {
            return [
                'ok'      => false,
                'refused' => 'no-acting-admin',
                'error'   => $e->getMessage(),
                'key'     => $key,
                'notes'   => ['Nothing was written. This plugin never substitutes a default administrator.'],
            ];
        }
        if (!$identity->has($permission)) {
            return [
                'ok'         => false,
                'refused'    => 'permission-denied',
                'error'      => 'The administrator this token acts as (' . $identity->login
                    . ') lacks the backend permission "' . $permission . '".',
                'key'        => $key,
                'permission' => $permission,
                'notes'      => ['Nothing was written. Grant the right in the backend, or use a token '
                    . 'authorised by an administrator who holds it.'],
            ];
        }

        $writer = ($this->writerFactory)();

        return $writer->write($row, $args['value']);
    }
}
