<?php

declare(strict_types=1);

/**
 * APO-193 Card 5 — Service-layer for token-patch operator actions.
 *
 * Centralises the business logic for site-level token override management so
 * BOTH the existing admin form-POST path AND the new /api/v1/... action path
 * use the same code. Adding a patch, deleting one, clearing all, or pruning
 * orphans all funnel through this service.
 *
 * Cross-reference: full-parity-mcp-2026-05-10.md §2 (read action conventions
 * for the read companions in Card 9) and APO-186 §7 (service-layer extraction
 * canonical pattern).
 */
final class MyronTokenPatchActionService
{
    public function __construct(
        private readonly MyronTokenPatchRepository $repository,
    ) {
    }

    /**
     * Save a token override (patch) for a CSS variable name.
     *
     * @return array{tokenName: string, patchValue: string}
     */
    public function savePatch(
        string $tokenName,
        string $patchValue,
        string $patchNote,
        MyronActionCallerContext $caller
    ): array {
        $tokenName = trim($tokenName);
        if ($tokenName === '') {
            throw new InvalidArgumentException('tokenName is required.');
        }
        $this->repository->savePatch(
            $tokenName,
            $patchValue,
            $patchNote,
            $this->callerUserId($caller)
        );
        return [
            'tokenName'  => $tokenName,
            'patchValue' => $patchValue,
        ];
    }

    /**
     * Clear the override for a specific token.
     *
     * @return array{tokenName: string, cleared: bool}
     */
    public function clearPatch(string $tokenName, MyronActionCallerContext $caller): array
    {
        $tokenName = trim($tokenName);
        if ($tokenName === '') {
            throw new InvalidArgumentException('tokenName is required.');
        }
        $this->repository->deletePatch($tokenName);
        return ['tokenName' => $tokenName, 'cleared' => true];
    }

    /**
     * Clear ALL token patches.
     *
     * @return array{cleared: int}
     */
    public function clearAllPatches(MyronActionCallerContext $caller): array
    {
        $previousCount = count($this->repository->listPatchesByToken());
        $this->repository->clearAll();
        return ['cleared' => $previousCount];
    }

    /**
     * Clear orphan patches — patches whose token is no longer declared by any
     * registry surface. Non-orphan patches are preserved.
     *
     * @return array{cleared: int, preserved: int}
     */
    public function clearOrphanPatches(MyronActionCallerContext $caller): array
    {
        if (!function_exists('myronTokenReferenceValidTokenNames')) {
            throw new MyronActionPreconditionError(
                'Token reference service is not available.'
            );
        }
        $valid = (array) myronTokenReferenceValidTokenNames();
        $cleared   = (int) $this->repository->clearOrphanPatches($valid);
        $preserved = count($this->repository->listPatchesByToken());
        return ['cleared' => $cleared, 'preserved' => $preserved];
    }

    private function callerUserId(MyronActionCallerContext $caller): string
    {
        return $caller->callerType === 'human-session' ? $caller->callerId : '';
    }
}
