<?php

declare(strict_types=1);

/**
 * APO-196 Cards 3 + 4 — Site Setup + Application Update action service.
 *
 * Wraps the install-bootstrap + application-update verbs. All update-mechanism
 * actions are operator-only (exposeToMcp:false). The save-site-setup verb is
 * complex (touches bundle activation + template/page replacement + content
 * cleanup); the service delegates to the existing onboarding/default-replacement
 * services so the admin and API paths share the same flow.
 *
 * 7 verbs covered:
 *   save-site-setup
 *   reset-site-baseline
 *   apply-application-update (uploaded package)
 *   apply-remote-application-update
 *   check-remote-application-update
 *   save-remote-update-manifest-url
 *   dismiss-remote-application-update
 */
final class MyronSiteSetupActionService
{
    public function __construct(
        private readonly MyronOnboardingStateRepository $onboarding,
    ) {
    }

    /**
     * Save the site-setup primary state. The admin route's complex flow
     * (template-replacement choices, page-sync mode, bootstrapping the
     * primary bundle, footer recommendation) stays in the route since it
     * uses many UI-form-specific helpers. The service-layer call is the
     * canonical state save — both paths funnel through onboarding->saveState.
     *
     * @param array<string, mixed> $stateInput
     * @return array{state: array<string, mixed>}
     */
    public function saveSiteSetup(array $stateInput, MyronActionCallerContext $caller): array
    {
        $userId = $caller->callerType === 'human-session' ? $caller->callerId : '';
        $state = (array) $this->onboarding->saveState($stateInput, $userId);
        return ['state' => $state];
    }

    /**
     * @param array<string, mixed> $stateInput
     */
    public function requiresDestructiveForSiteSetupSave(array $stateInput): bool
    {
        if (!array_key_exists('primary_bundle_id', $stateInput)) {
            return false;
        }
        $incoming = trim((string) ($stateInput['primary_bundle_id'] ?? ''));
        if ($incoming === '') {
            return false;
        }
        $current = $this->onboarding->getState();
        return $incoming !== trim((string) ($current['primary_bundle_id'] ?? ''));
    }

    /**
     * Reset the site to the baseline definition. Destructive; operator-only.
     *
     * @return array<string, mixed>
     */
    public function resetSiteBaseline(string $confirmationPhrase, MyronActionCallerContext $caller): array
    {
        if (!function_exists('myronSiteResetService')) {
            throw new MyronActionPreconditionError('Site reset service is unavailable.');
        }
        $resetService = myronSiteResetService();
        $resetService->assertConfirmed($confirmationPhrase);
        $userId = $caller->callerType === 'human-session' ? $caller->callerId : '';
        $result = (array) $resetService->resetToBaseline($userId);
        if (function_exists('myronRefreshSiteSettingsCache')) {
            myronRefreshSiteSettingsCache();
        }
        return $result;
    }

    /**
     * Apply an uploaded application update package.
     *
     * @param array<string, mixed> $uploadedPackage $_FILES-shaped
     * @return array<string, mixed>
     */
    public function applyApplicationUpdate(array $uploadedPackage, MyronActionCallerContext $caller): array
    {
        if (!function_exists('myronApplicationUpdateService')) {
            throw new MyronActionPreconditionError('Application update service is unavailable.');
        }
        $currentUser = $this->requireAdministrator($caller);
        return (array) myronApplicationUpdateService()->applyUploadedPackage($uploadedPackage, $currentUser);
    }

    /**
     * Apply the available remote application update.
     *
     * @return array<string, mixed>
     */
    public function applyRemoteApplicationUpdate(MyronActionCallerContext $caller): array
    {
        if (!function_exists('myronRemoteUpdateApplyService')) {
            throw new MyronActionPreconditionError('Remote update service is unavailable.');
        }
        $currentUser = $this->requireAdministrator($caller);
        return (array) myronRemoteUpdateApplyService()->applyAvailableUpdate($currentUser);
    }

    /**
     * Check the remote update manifest for available updates.
     *
     * @return array<string, mixed>
     */
    public function checkRemoteApplicationUpdate(MyronActionCallerContext $caller): array
    {
        if (!function_exists('myronRemoteUpdateCheckService')) {
            throw new MyronActionPreconditionError('Remote update check service is unavailable.');
        }
        $this->requireAdministrator($caller);
        return (array) myronRemoteUpdateCheckService()->checkNow(true);
    }

    /**
     * Save the remote update manifest URL.
     *
     * @return array{manifestUrl: string}
     */
    public function saveRemoteUpdateManifestUrl(string $manifestUrl, MyronActionCallerContext $caller): array
    {
        $this->requireAdministrator($caller);
        $manifestUrl = trim($manifestUrl);
        if (filter_var($manifestUrl, FILTER_VALIDATE_URL) === false) {
            throw new InvalidArgumentException('Manifest URL must be a valid URL.');
        }
        if (strtolower((string) parse_url($manifestUrl, PHP_URL_SCHEME)) !== 'https') {
            throw new InvalidArgumentException('Manifest URL must use HTTPS.');
        }
        if (!function_exists('myronRemoteUpdateSettingsRepository')) {
            throw new MyronActionPreconditionError('Remote update settings repository is unavailable.');
        }
        $repo = myronRemoteUpdateSettingsRepository();
        $settings = (array) $repo->get();
        $settings['manifest_url'] = $manifestUrl;
        $repo->save($settings);
        return ['manifestUrl' => $manifestUrl];
    }

    /**
     * Dismiss the available remote application update for a specific version.
     *
     * @return array{dismissedVersion: string}
     */
    public function dismissRemoteApplicationUpdate(string $version, MyronActionCallerContext $caller): array
    {
        $this->requireAdministrator($caller);
        $version = trim($version);
        if ($version === '') {
            throw new InvalidArgumentException('version is required.');
        }
        if (!function_exists('myronRemoteUpdateStateRepository')) {
            throw new MyronActionPreconditionError('Remote update state repository is unavailable.');
        }
        myronRemoteUpdateStateRepository()->dismissVersion($version);
        return ['dismissedVersion' => $version];
    }

    /**
     * Administrator gate. AI bearer callers cannot apply updates (the
     * exposeToMcp:false flag on these actions means MCP clients can't even
     * invoke them; this is a defense-in-depth check for direct API callers).
     *
     * @return array<string, mixed>
     */
    private function requireAdministrator(MyronActionCallerContext $caller): array
    {
        if ($caller->callerType === 'human-session' && function_exists('myronCurrentUser')) {
            $user = (array) myronCurrentUser();
            if (function_exists('myronUserHoldsScope') && myronUserHoldsScope($user, MyronApiScopes::ADMINISTRATION_DESTRUCTIVE)) {
                return $user;
            }
        }
        if ($caller->callerType === 'system') {
            return ['id' => $caller->callerId, 'role' => 'system'];
        }
        // AI bearer callers can reach this path only if exposeToMcp wasn't
        // enforced upstream — defense in depth.
        throw new MyronActionPreconditionError('Application update operations require destructive administration scope.');
    }
}
