<?php

declare(strict_types=1);

/**
 * APO-194 Card 6 — Definitions (Design Systems) action service.
 *
 * Wraps the JSONM DesignSystemRegistry. Per APO-205-006 (substrate correction),
 * the seven mutation verbs are `exposeToMcp:true` and scoped to `design.write` —
 * an MCP integration with the Design role can apply / save / import definitions
 * as the operator can via the admin UI.
 *
 * APO-205-011 (Fix B) — this service is now the CANONICAL apply path. Both the
 * MCP action layer and the admin POST route (`/admin/design/systems`) call into
 * it instead of the bare JSONM registry. The service writes back to
 * `onboarding_state.applied_design_system_id` after a successful registry apply
 * so reciprocal telemetry surfaces (administration.site-setup-state.get,
 * compatibility/configured) reflect the apply in the same request cycle.
 *
 * Cross-reference: architecture log §3 (substrate test) — scope is the gate.
 */
final class MyronDefinitionsActionService
{
    public function __construct(
        private readonly object $registry,
        private readonly ?MyronOnboardingStateRepository $onboarding = null,
    ) {
    }

    /**
     * @return array<string, mixed>
     */
    public function saveCurrent(string $name, ?string $requestedId, ?string $description, MyronActionCallerContext $caller): array
    {
        $this->requireMethod('saveCurrentAsNamed');
        // saveCurrent persists the current live state as a new stored row but
        // does NOT change the active definition; no onboarding_state write.
        return (array) $this->registry->saveCurrentAsNamed($name, $requestedId, $description);
    }

    /**
     * @return array<string, mixed>
     */
    public function applyStored(string $definitionId, MyronActionCallerContext $caller): array
    {
        $this->requireMethod('applyStored');
        $record = (array) $this->registry->applyStored($definitionId);
        $this->recordApplyTelemetry($record, $caller, /* archetypeThemeIdOnApply */ '');
        return $record;
    }

    /**
     * @return array<string, mixed>
     */
    public function applyDefinition(string $definitionId, MyronActionCallerContext $caller): array
    {
        $this->requireMethod('applySelectable');
        $record = (array) $this->registry->applySelectable($definitionId);
        // For packaged-archetype applies, the archetype id IS the theme id.
        $themeId = ((string) ($record['kind'] ?? '')) === 'archetype'
            ? (string) ($record['id'] ?? '')
            : '';
        $this->recordApplyTelemetry($record, $caller, $themeId);
        return $record;
    }

    /**
     * @return array<string, mixed>
     */
    public function resetBaseline(MyronActionCallerContext $caller): array
    {
        $this->requireMethod('applyBaseline');
        $record = (array) $this->registry->applyBaseline();
        $this->recordApplyTelemetry($record, $caller, /* archetypeThemeIdOnApply */ '');
        return $record;
    }

    /**
     * @return array<string, mixed>
     */
    public function removeStored(string $definitionId, MyronActionCallerContext $caller): array
    {
        $this->requireMethod('removeStored');
        $result = (array) $this->registry->removeStored($definitionId);
        // If removal triggered a baseline fallback, sync onboarding_state.
        if (($result['removedActive'] ?? false) === true) {
            $this->recordApplyTelemetryFromFallback($result, $caller);
        }
        return $result;
    }

    /**
     * @return array<string, mixed>
     */
    public function removeSystemArchetype(string $definitionId, MyronActionCallerContext $caller): array
    {
        $this->requireMethod('removePackagedArchetype');
        $result = (array) $this->registry->removePackagedArchetype($definitionId);
        if (($result['removedActive'] ?? false) === true) {
            $this->recordApplyTelemetryFromFallback($result, $caller);
        }
        return $result;
    }

    /**
     * @return array<string, mixed>
     */
    public function importDefinition(string $tmpPath, ?string $importName, bool $applyImmediately, MyronActionCallerContext $caller): array
    {
        $this->requireMethod('importDefinitionFile');
        $record = (array) $this->registry->importDefinitionFile($tmpPath, $importName, $applyImmediately);
        if ($applyImmediately) {
            $this->recordApplyTelemetry($record, $caller, /* archetypeThemeIdOnApply */ '');
        }
        return $record;
    }

    /**
     * APO-205-011 Fix B — write the most-recently-applied design system id to
     * onboarding_state so reciprocal-telemetry reads reflect the apply.
     *
     * @param array<string, mixed> $record  the registry apply result
     */
    private function recordApplyTelemetry(array $record, MyronActionCallerContext $caller, string $archetypeThemeId): void
    {
        if ($this->onboarding === null) {
            return; // legacy constructor path — no-op
        }
        $appliedId = trim((string) ($record['id'] ?? ''));
        if ($appliedId === '') {
            return;
        }
        $actor = trim($caller->callerId) !== '' ? $caller->callerId : 'system-apply';
        $this->onboarding->recordAppliedDesignSystem($appliedId, $archetypeThemeId, $actor);
    }

    /**
     * @param array<string, mixed> $result  registry remove result with `fallbackName` / `fallbackId`
     */
    private function recordApplyTelemetryFromFallback(array $result, MyronActionCallerContext $caller): void
    {
        if ($this->onboarding === null) {
            return;
        }
        $fallbackId = trim((string) ($result['fallbackId'] ?? ''));
        if ($fallbackId === '') {
            // The remove path may carry only the name; defensively read current.
            if (method_exists($this->registry, 'currentDefinition')) {
                try {
                    $cur = (array) $this->registry->currentDefinition();
                    $fallbackId = trim((string) ($cur['id'] ?? ''));
                } catch (Throwable) {
                    return;
                }
            }
        }
        if ($fallbackId === '') {
            return;
        }
        $actor = trim($caller->callerId) !== '' ? $caller->callerId : 'system-apply';
        $this->onboarding->recordAppliedDesignSystem($fallbackId, '', $actor);
    }

    private function requireMethod(string $method): void
    {
        if (!method_exists($this->registry, $method)) {
            throw new MyronActionPreconditionError('Design system registry does not support ' . $method . '.');
        }
    }
}
