<?php

declare(strict_types=1);

require_once __DIR__ . '/LicenseTier.php';
require_once __DIR__ . '/TierPolicy.php';
require_once __DIR__ . '/CapabilityCatalog.php';

final class MyronEntitlementSnapshot
{
    /**
     * @param array<string, bool> $capabilities
     * @param array<string, int|string|bool|null> $limits
     */
    public function __construct(
        public readonly string $plan,
        public readonly string $status,
        public readonly array $capabilities,
        public readonly array $limits,
        public readonly string $deploymentGroup,
        public readonly string $environment,
        public readonly ?string $expiresAt,
        public readonly ?string $renewsAt,
        public readonly ?string $graceEndsAt,
        public readonly ?string $lastValidatedAt,
        public readonly array $activeSkus = [],
        public readonly string $apiVersion = 'v1',
    ) {
    }

    public static function missing(): self
    {
        return new self(
            'core',
            MyronLicenseStatus::UNREGISTERED,
            self::defaultCapabilities('core'),
            self::defaultLimits('core'),
            '',
            'production',
            null,
            null,
            null,
            null,
            [],
            'v1'
        );
    }

    /**
     * @param array<string, mixed> $payload
     */
    public static function fromArray(array $payload): self
    {
        $plan = self::normalizePlan((string) ($payload['plan'] ?? 'core'));
        $status = MyronLicenseStatus::normalize((string) ($payload['status'] ?? MyronLicenseStatus::MISSING));
        $entitlements = is_array($payload['entitlements'] ?? null) ? $payload['entitlements'] : [];
        $capabilities = is_array($payload['capabilities'] ?? null)
            ? $payload['capabilities']
            : (is_array($entitlements['capabilities'] ?? null) ? $entitlements['capabilities'] : []);
        $limits = is_array($payload['limits'] ?? null)
            ? $payload['limits']
            : (is_array($entitlements['limits'] ?? null) ? $entitlements['limits'] : []);
        $activeSkus = is_array($payload['activeSkus'] ?? null)
            ? $payload['activeSkus']
            : (is_array($payload['active_skus'] ?? null) ? $payload['active_skus'] : []);
        $apiVersion = trim((string) ($payload['apiVersion'] ?? $payload['api_version'] ?? 'v1'));

        $normalizedSkus = self::normalizeActiveSkus($activeSkus);
        $normalizedCapabilities = self::applySiteTypeCatalogGrant(
            $plan,
            $status,
            self::normalizeCapabilities($plan, $capabilities),
            $normalizedSkus
        );

        return new self(
            $plan,
            $status,
            $normalizedCapabilities,
            self::normalizeLimits($plan, $limits),
            trim((string) ($payload['deploymentGroup'] ?? $payload['deployment_group'] ?? '')),
            MyronLicenseClient::normalizeEnvironment((string) ($payload['environment'] ?? 'production')),
            self::nullableTimestamp($payload['expiresAt'] ?? $payload['expires_at'] ?? null),
            self::nullableTimestamp($payload['renewsAt'] ?? $payload['renews_at'] ?? null),
            self::nullableTimestamp($payload['graceEndsAt'] ?? $payload['grace_ends_at'] ?? null),
            self::nullableTimestamp($payload['validatedAt'] ?? $payload['lastValidatedAt'] ?? $payload['last_validated_at'] ?? null),
            $normalizedSkus,
            $apiVersion !== '' ? substr($apiVersion, 0, 8) : 'v1'
        );
    }

    /**
     * The full non-core site-type catalog is granted whenever the license carries any
     * paid entitlement: a paid plan tier (Personal+), or any active non-legacy paid SKU
     * (module/bundle). This is the single rule that replaces per-type site-type tiering —
     * "buy any paid Module and every site type becomes selectable" — while module-dependent
     * FEATURES inside a site type continue to gate on their own capabilities.
     *
     * @param array<string, bool> $capabilities
     * @param list<array<string, mixed>> $activeSkus
     * @return array<string, bool>
     */
    private static function applySiteTypeCatalogGrant(string $plan, string $status, array $capabilities, array $activeSkus): array
    {
        // Mirrors MyronEntitlementPresentationPolicy::SITE_TYPE_CATALOG_CAPABILITY; kept as a
        // literal here so EntitlementSnapshot carries no dependency on the presentation layer.
        $capability = 'site_types.catalog';
        if (MyronCapabilityCatalog::isInvariantDisabled($capability)) {
            return $capabilities;
        }
        if (($capabilities[$capability] ?? false) === true) {
            return $capabilities;
        }
        if (!MyronLicenseStatus::allowsRuntime($status)) {
            return $capabilities;
        }

        $granted = MyronLicenseTier::isAtLeast($plan, MyronLicenseTier::PERSONAL);
        if (!$granted) {
            foreach ($activeSkus as $sku) {
                $id = trim((string) ($sku['id'] ?? ''));
                if ($id === '' || str_starts_with($id, 'legacy-')) {
                    continue;
                }
                $granted = true;
                break;
            }
        }

        if ($granted) {
            $capabilities[$capability] = true;
        }

        return $capabilities;
    }

    public static function fromJson(string $json): self
    {
        $decoded = json_decode($json, true);
        return is_array($decoded) ? self::fromArray($decoded) : self::missing();
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        return [
            'plan' => $this->plan,
            'status' => $this->status,
            'capabilities' => $this->capabilities,
            'limits' => $this->limits,
            'deploymentGroup' => $this->deploymentGroup,
            'environment' => $this->environment,
            'expiresAt' => $this->expiresAt,
            'renewsAt' => $this->renewsAt,
            'graceEndsAt' => $this->graceEndsAt,
            'lastValidatedAt' => $this->lastValidatedAt,
            'activeSkus' => $this->activeSkus,
            'apiVersion' => $this->apiVersion,
        ];
    }

    public function toJson(): string
    {
        return (string) json_encode($this->toArray(), JSON_UNESCAPED_SLASHES);
    }

    public static function normalizePlan(string $plan): string
    {
        return MyronLicenseTier::normalize($plan);
    }

    /**
     * @param array<string, mixed> $capabilities
     * @return array<string, bool>
     */
    private static function normalizeCapabilities(string $plan, array $capabilities): array
    {
        $normalized = self::defaultCapabilities($plan);
        foreach ($capabilities as $key => $value) {
            $name = trim((string) $key);
            if ($name === '') {
                continue;
            }
            $normalized[$name] = MyronCapabilityCatalog::isInvariantDisabled($name) ? false : (bool) $value;
        }

        return $normalized;
    }

    /**
     * @return array<string, bool>
     */
    private static function defaultCapabilities(string $plan): array
    {
        return MyronCapabilityCatalog::defaultCapabilities($plan);
    }

    /**
     * @param array<string, mixed> $limits
     * @return array<string, int|string|bool|null>
     */
    private static function normalizeLimits(string $plan, array $limits): array
    {
        $normalized = self::defaultLimits($plan);
        foreach ($limits as $key => $value) {
            $name = trim((string) $key);
            if ($name === '') {
                continue;
            }
            if (is_int($value) || is_string($value) || is_bool($value) || $value === null) {
                $normalized[$name] = $value;
            }
        }

        return $normalized;
    }

    /**
     * @return array<string, int|string|bool|null>
     */
    private static function defaultLimits(string $plan): array
    {
        return MyronTierPolicy::limitsFor($plan);
    }

    private static function nullableTimestamp(mixed $value): ?string
    {
        $timestamp = trim((string) $value);
        return $timestamp !== '' ? $timestamp : null;
    }

    /**
     * @param array<int|string, mixed> $activeSkus
     * @return list<array<string, mixed>>
     */
    private static function normalizeActiveSkus(array $activeSkus): array
    {
        $normalized = [];
        foreach ($activeSkus as $sku) {
            if (is_string($sku)) {
                $id = trim($sku);
                if ($id !== '') {
                    $normalized[] = ['id' => $id];
                }
                continue;
            }
            if (!is_array($sku)) {
                continue;
            }
            $id = trim((string) ($sku['id'] ?? ''));
            if ($id === '') {
                continue;
            }
            $sku['id'] = $id;
            $normalized[] = $sku;
        }

        return $normalized;
    }
}
