<?php

declare(strict_types=1);

/**
 * APO-186 Card 3 / APO-193 Card 4 — Footer design service-layer.
 *
 * Centralises the business logic for footer-design operator actions so that
 * BOTH the existing admin form-POST path AND the new /api/v1/... action path
 * use the same code. No drift possible because there's only one implementation.
 *
 * APO-186 Foundation extracted only setDefault() as the worked example.
 * APO-193 Pass A1 extracts the rest of the footer-domain verb vocabulary:
 *   - activate / deactivate
 *   - applyRecommendation / dismissRecommendation
 *   - applyDefaultsForZone
 *   - updateConfiguration
 *
 * The service throws domain exceptions on declared failures (precondition
 * violations, business-rule failures). Both callers catch and translate:
 * - Admin form path → flash-error redirect
 * - API action path → MyronActionResult::error() with appropriate code
 */
final class MyronFooterDesignActionService
{
    public function __construct(
        private readonly MyronFooterDesignRepository $footers,
        private readonly ?MyronDesignLibraryEntitlementPolicy $entitlements = null,
    ) {
    }

    /**
     * Activate a footer variant — makes it eligible for selection as default
     * and for use in template chrome assignments.
     *
     * @return array{variantId: string, isActivated: bool}
     */
    public function activate(string $variantId, MyronActionCallerContext $caller): array
    {
        $variantId = $this->normaliseVariantId($variantId);
        $this->assertVariantAvailable($variantId);
        $this->footers->saveActivation($variantId, true, $this->callerUserId($caller));
        return ['variantId' => $variantId, 'isActivated' => true];
    }

    /**
     * Deactivate a footer variant.
     *
     * @return array{variantId: string, isActivated: bool}
     * @throws MyronActionPreconditionError when deactivating the current global default
     */
    public function deactivate(string $variantId, MyronActionCallerContext $caller): array
    {
        $variantId = $this->normaliseVariantId($variantId);
        $this->assertVariantAvailable($variantId);
        $current   = (string) ($this->footers->getDefault()['variant_id'] ?? '');
        if ($current === $variantId) {
            throw new MyronActionPreconditionError(
                'Cannot deactivate the current global default footer. Set a different default first.'
            );
        }
        $this->footers->saveActivation($variantId, false, $this->callerUserId($caller));
        return ['variantId' => $variantId, 'isActivated' => false];
    }

    /**
     * Set the global default footer variant.
     *
     * @return array{variantId: string, previousDefault: string}
     * @throws InvalidArgumentException     when the variant is not in the valid set
     * @throws MyronActionPreconditionError when the variant is not activated
     */
    public function setDefault(string $variantId, MyronActionCallerContext $caller): array
    {
        $variantId = $this->normaliseVariantId($variantId);
        $this->assertVariantAvailable($variantId);
        $row = $this->footers->getByVariantId($variantId);
        if (!(bool) ($row['is_activated'] ?? false)) {
            throw new MyronActionPreconditionError(
                'Variant is not activated; activate before setting as default.'
            );
        }
        $previousDefault = (string) ($this->footers->getDefault()['variant_id'] ?? '');
        $this->footers->setDefault($variantId, $this->callerUserId($caller));
        return ['variantId' => $variantId, 'previousDefault' => $previousDefault];
    }

    /**
     * Apply a site-type-bundle footer recommendation (sets the recommended
     * variant as the new default for the install).
     *
     * @return array{bundleId: string, variantId: string}
     */
    public function applyRecommendation(string $bundleId, string $variantId, MyronActionCallerContext $caller): array
    {
        $bundleId  = trim($bundleId);
        $variantId = $this->normaliseVariantId($variantId);
        $this->assertVariantAvailable($variantId);
        if ($bundleId === '') {
            throw new InvalidArgumentException('bundleId is required.');
        }
        $this->footers->applyRecommendation($bundleId, $variantId, $this->callerUserId($caller));
        return ['bundleId' => $bundleId, 'variantId' => $variantId];
    }

    /**
     * Dismiss a footer recommendation without applying it.
     *
     * @return array{bundleId: string, variantId: string}
     */
    public function dismissRecommendation(string $bundleId, string $variantId, MyronActionCallerContext $caller): array
    {
        $bundleId  = trim($bundleId);
        $variantId = $this->normaliseVariantId($variantId);
        $this->assertVariantAvailable($variantId);
        if ($bundleId === '') {
            throw new InvalidArgumentException('bundleId is required.');
        }
        $this->footers->dismissRecommendation($bundleId, $variantId, $this->callerUserId($caller));
        return ['bundleId' => $bundleId, 'variantId' => $variantId];
    }

    /**
     * Apply per-zone default content for the active site type's bundle.
     *
     * @return array{variantId: string, zoneId: string, applied: bool}
     */
    public function applyDefaultsForZone(string $variantId, string $zoneId, MyronActionCallerContext $caller): array
    {
        $variantId = $this->normaliseVariantId($variantId);
        $this->assertVariantAvailable($variantId);
        $zoneId    = trim($zoneId);
        if ($zoneId === '') {
            throw new InvalidArgumentException('zoneId is required.');
        }
        if (!function_exists('myronFooterContentSeedService') || !function_exists('myronBundleActivationRepository')) {
            throw new MyronActionPreconditionError('Footer content seed service is unavailable.');
        }
        $primaryBundleId = (string) (myronBundleActivationRepository()->getPrimaryBundleId() ?? '');
        $applied = (bool) myronFooterContentSeedService()->applyDefaultsForZone(
            $primaryBundleId,
            $variantId,
            $zoneId,
            $this->footers,
            $this->callerUserId($caller)
        );
        return ['variantId' => $variantId, 'zoneId' => $zoneId, 'applied' => $applied];
    }

    /**
     * Update the configuration (zone visibility + content) for a footer variant.
     *
     * @param array<string, bool> $visibility
     * @param array<string, mixed> $contentByZone
     * @return array{variantId: string, zonesUpdated: int}
     */
    public function updateConfiguration(
        string $variantId,
        array $visibility,
        array $contentByZone,
        MyronActionCallerContext $caller
    ): array {
        $variantId = $this->normaliseVariantId($variantId);
        $this->assertVariantAvailable($variantId);
        $this->footers->saveConfiguration($variantId, $visibility, $contentByZone, $this->callerUserId($caller));
        return [
            'variantId'    => $variantId,
            'zonesUpdated' => count($contentByZone),
        ];
    }

    private function normaliseVariantId(string $variantId): string
    {
        $variantId = trim($variantId);
        if ($variantId === '') {
            throw new InvalidArgumentException('variantId is required.');
        }
        if (!in_array($variantId, $this->footers->validVariants(), true)) {
            throw new InvalidArgumentException('Unknown footer variant: ' . $variantId);
        }
        return $variantId;
    }

    private function assertVariantAvailable(string $variantId): void
    {
        if ($this->entitlements instanceof MyronDesignLibraryEntitlementPolicy) {
            $this->entitlements->assertFooterVariantAvailable($variantId);
        }
    }

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

/**
 * APO-186 Card 3 — declared precondition failure for action service-layer.
 *
 * Distinct from InvalidArgumentException so callers can choose: the API
 * dispatcher maps this to error code `precondition_failed` (HTTP 422); the
 * admin form path maps it to a flash-error redirect.
 */
final class MyronActionPreconditionError extends RuntimeException
{
}
