<?php

declare(strict_types=1);

require_once __DIR__ . '/OperatorStateUpdateBoundary.php';

final class MyronUpdateManifest
{
    public const PACKAGE_TYPE = 'myroncms.update';
    public const PACKAGE_INSTALL_TYPE = 'myroncms.package.install';

    /**
     * @param array<string, mixed> $raw
     * @param list<array<string, string>> $operations
     * @param list<string> $sourceVersionConstraints
     * @param list<string> $targetRoots
     */
    private function __construct(
        private readonly array $raw,
        private readonly string $packageType,
        private readonly string $packageId,
        private readonly string $targetVersion,
        private readonly array $sourceVersionConstraints,
        private readonly array $operations,
        private readonly bool $runMigrations,
        private readonly bool $reseedRuntimeDefinitions,
        private readonly string $updateClass,
        private readonly string $requiredCapability,
        private readonly string $packageTier,
        private readonly array $releaseNotes,
        private readonly string $targetRoot,
        private readonly array $targetRoots,
        private readonly string $envelopeRef,
        private readonly array $definitionImpactPaths,
    ) {
    }

    /**
     * @param array<string, mixed> $manifest
     */
    public static function fromArray(array $manifest): self
    {
        $packageType = (string) ($manifest['packageType'] ?? '');
        if (!in_array($packageType, [self::PACKAGE_TYPE, self::PACKAGE_INSTALL_TYPE], true)) {
            throw new InvalidArgumentException('Update package manifest must declare packageType "' . self::PACKAGE_TYPE . '" or "' . self::PACKAGE_INSTALL_TYPE . '".');
        }

        if ((int) ($manifest['schemaVersion'] ?? 0) !== 1) {
            throw new InvalidArgumentException('Update package manifest must use schemaVersion 1.');
        }

        $packageId = trim((string) ($manifest['packageId'] ?? ''));
        if ($packageId === '') {
            throw new InvalidArgumentException('Update package manifest must declare packageId.');
        }

        $targetVersion = trim((string) ($manifest['targetVersion'] ?? $manifest['version'] ?? ''));
        if ($targetVersion === '' && $packageType === self::PACKAGE_TYPE) {
            throw new InvalidArgumentException('Update package manifest must declare targetVersion.');
        }

        $sourceConstraints = [];
        foreach ((array) ($manifest['sourceVersionConstraints'] ?? []) as $constraint) {
            $constraint = trim((string) $constraint);
            if ($constraint !== '') {
                $sourceConstraints[] = $constraint;
            }
        }

        $targetRoot = self::normalizeInstallRoot((string) ($manifest['targetRoot'] ?? ''));
        $targetRoots = self::normalizeInstallRoots($manifest['targetRoots'] ?? []);
        if ($targetRoots === [] && $targetRoot !== '') {
            $targetRoots = [$targetRoot];
        }
        if ($targetRoot === '' && $targetRoots !== []) {
            $targetRoot = $targetRoots[0];
        }
        $envelopeRef = self::normalizePath((string) ($manifest['envelopeRef'] ?? ''));
        if ($packageType === self::PACKAGE_INSTALL_TYPE) {
            if ($targetRoots === []) {
                throw new InvalidArgumentException('Package install manifest must declare targetRoot or targetRoots.');
            }
            if ($envelopeRef === '') {
                throw new InvalidArgumentException('Package install manifest must declare envelopeRef.');
            }
        }

        $operations = [];
        $files = $packageType === self::PACKAGE_INSTALL_TYPE ? ($manifest['operations'] ?? []) : ($manifest['files'] ?? []);
        if (!is_array($files)) {
            throw new InvalidArgumentException($packageType === self::PACKAGE_INSTALL_TYPE
                ? 'Package install manifest operations must be an array.'
                : 'Update package manifest files must be an array.');
        }

        foreach ($files as $file) {
            if (!is_array($file)) {
                throw new InvalidArgumentException('Every update file entry must be an object.');
            }

            $path = trim((string) ($file['path'] ?? ''));
            $operation = trim((string) ($file['operation'] ?? 'replace'));
            $payload = trim((string) ($file['payload'] ?? ('files/' . $path)));
            $sha256 = strtolower(trim((string) ($file['sha256'] ?? '')));
            if ($path === '' || !in_array($operation, ['add', 'replace', 'delete'], true)) {
                throw new InvalidArgumentException('Every update file entry must declare path and a valid operation.');
            }

            if ($operation !== 'delete' && ($payload === '' || !preg_match('/^[a-f0-9]{64}$/', $sha256))) {
                throw new InvalidArgumentException('Every add/replace update file entry must declare payload and sha256.');
            }

            if ($packageType === self::PACKAGE_INSTALL_TYPE && $operation !== 'add') {
                throw new InvalidArgumentException('Package install manifests may only declare add operations.');
            }

            $operations[] = [
                'path' => $path,
                'operation' => $operation,
                'payload' => $payload,
                'sha256' => $sha256,
            ];
        }

        if ($operations === []) {
            throw new InvalidArgumentException('Update package manifest must declare at least one file operation.');
        }

        $updateClass = self::normalizeUpdateClass((string) ($manifest['updateClass'] ?? $manifest['update_class'] ?? 'maintenance_patch'));
        $requiredCapability = trim((string) ($manifest['requiredCapability'] ?? $manifest['required_capability'] ?? self::defaultCapabilityForUpdateClass($updateClass)));
        if (!in_array($updateClass, ['security_patch', 'maintenance_patch'], true) && $requiredCapability === '') {
            throw new InvalidArgumentException('Feature update package manifests must declare requiredCapability.');
        }

        $packageTier = strtolower(trim((string) ($manifest['packageTier'] ?? $manifest['package_tier'] ?? MyronOperatorStateUpdateBoundary::packageTierForCapability($requiredCapability))));
        if (!in_array($packageTier, ['core', 'personal', 'pro', 'agency', 'enterprise', 'partner'], true)) {
            throw new InvalidArgumentException('Update package manifest declares an unknown packageTier.');
        }

        $releaseNotes = is_array($manifest['releaseNotes'] ?? null)
            ? $manifest['releaseNotes']
            : (is_array($manifest['release_notes'] ?? null) ? $manifest['release_notes'] : []);

        $definitionImpact = is_array($manifest['definitionImpact'] ?? null) ? $manifest['definitionImpact'] : [];
        $definitionImpactVersion = trim((string) ($definitionImpact['version'] ?? ''));
        $definitionImpactPaths = [];
        foreach ((array) ($definitionImpact['paths'] ?? []) as $path) {
            $normalized = self::normalizePath((string) $path);
            if ($normalized !== '') {
                $definitionImpactPaths[$normalized] = true;
            }
        }
        $definitionImpactPaths = array_keys($definitionImpactPaths);
        sort($definitionImpactPaths);
        $reseedRuntimeDefinitions = (bool) ($manifest['reseedRuntimeDefinitions'] ?? false);
        if ($reseedRuntimeDefinitions && $packageType === self::PACKAGE_TYPE) {
            if ($definitionImpactVersion === '' || $definitionImpactVersion !== $targetVersion) {
                throw new InvalidArgumentException('Definition reseed requires definitionImpact.version matching targetVersion.');
            }
            if ($definitionImpactPaths === []) {
                throw new InvalidArgumentException('Definition reseed requires one or more definitionImpact.paths.');
            }
            $operationPaths = array_fill_keys(array_column($operations, 'path'), true);
            foreach ($definitionImpactPaths as $path) {
                if (!str_starts_with($path, 'app/data/runtime-definitions/') || !isset($operationPaths[$path])) {
                    throw new InvalidArgumentException('Definition impact paths must be declared runtime-definition file operations.');
                }
            }
        }

        return new self(
            $manifest,
            $packageType,
            $packageId,
            $targetVersion,
            $sourceConstraints,
            $operations,
            (bool) ($manifest['runMigrations'] ?? false),
            $reseedRuntimeDefinitions,
            $updateClass,
            $requiredCapability,
            $packageTier,
            $releaseNotes,
            $targetRoot,
            $targetRoots,
            $envelopeRef,
            $definitionImpactPaths,
        );
    }

    /**
     * @return array<string, mixed>
     */
    public function raw(): array
    {
        return $this->raw;
    }

    public function packageId(): string
    {
        return $this->packageId;
    }

    public function packageType(): string
    {
        return $this->packageType;
    }

    public function isPackageInstall(): bool
    {
        return $this->packageType === self::PACKAGE_INSTALL_TYPE;
    }

    public function targetVersion(): string
    {
        return $this->targetVersion;
    }

    /**
     * @return list<string>
     */
    public function sourceVersionConstraints(): array
    {
        return $this->sourceVersionConstraints;
    }

    /**
     * @return list<array<string, string>>
     */
    public function operations(): array
    {
        return $this->operations;
    }

    public function shouldRunMigrations(): bool
    {
        return $this->runMigrations;
    }

    public function shouldReseedRuntimeDefinitions(): bool
    {
        return $this->reseedRuntimeDefinitions;
    }

    /**
     * @return list<string>
     */
    public function definitionImpactPaths(): array
    {
        return $this->definitionImpactPaths;
    }

    public function updateClass(): string
    {
        return $this->updateClass;
    }

    public function requiredCapability(): string
    {
        return $this->requiredCapability;
    }

    public function packageTier(): string
    {
        return $this->packageTier;
    }

    /**
     * @return array<string, mixed>
     */
    public function releaseNotes(): array
    {
        return $this->releaseNotes;
    }

    public function targetRoot(): string
    {
        return $this->targetRoot;
    }

    /**
     * @return list<string>
     */
    public function targetRoots(): array
    {
        return $this->targetRoots;
    }

    public function envelopeRef(): string
    {
        return $this->envelopeRef;
    }

    public function isPublicMaintenanceUpdate(): bool
    {
        return in_array($this->updateClass, ['security_patch', 'maintenance_patch'], true);
    }

    private static function normalizeUpdateClass(string $updateClass): string
    {
        $normalized = strtolower(trim($updateClass));
        $known = ['security_patch', 'maintenance_patch', 'feature_update', 'pro_feature_update', 'agency_enterprise_update'];
        if (!in_array($normalized, $known, true)) {
            throw new InvalidArgumentException('Update package manifest declares an unknown updateClass.');
        }

        return $normalized;
    }

    private static function defaultCapabilityForUpdateClass(string $updateClass): string
    {
        return match ($updateClass) {
            'security_patch' => 'updates.security',
            'maintenance_patch' => 'updates.maintenance',
            'feature_update' => 'updates.feature',
            'pro_feature_update' => 'updates.pro',
            'agency_enterprise_update' => 'updates.agency',
            default => '',
        };
    }

    private static function normalizeInstallRoot(string $path): string
    {
        $path = self::normalizePath($path);
        if ($path !== '' && !str_ends_with($path, '/')) {
            $path .= '/';
        }

        return $path;
    }

    /**
     * @return list<string>
     */
    private static function normalizeInstallRoots(mixed $raw): array
    {
        if ($raw === null || $raw === '') {
            return [];
        }
        if (!is_array($raw)) {
            throw new InvalidArgumentException('Package install manifest targetRoots must be a list.');
        }

        $roots = [];
        foreach ($raw as $root) {
            $normalized = self::normalizeInstallRoot((string) $root);
            if ($normalized !== '') {
                $roots[] = $normalized;
            }
        }

        $roots = array_values(array_unique($roots));
        sort($roots, SORT_STRING);
        return $roots;
    }

    private static function normalizePath(string $path): string
    {
        $normalized = str_replace('\\', '/', trim($path));
        while (str_starts_with($normalized, './')) {
            $normalized = substr($normalized, 2);
        }

        return $normalized;
    }
}
