<?php

declare(strict_types=1);

namespace App\Support\Ui\DerivedState;

use Illuminate\Database\Eloquent\Model;
use JsonException;

final class DerivedStateKey
{
    public function __construct(
        public readonly DerivedStateFamily $family,
        public readonly string $recordClass,
        public readonly string $recordKey,
        public readonly string $variant,
        public readonly ?int $workspaceId = null,
        public readonly ?int $tenantId = null,
        public readonly ?string $contextHash = null,
    ) {
        if (trim($this->recordClass) === '') {
            throw new \InvalidArgumentException('Derived state keys require a non-empty record class.');
        }

        if (trim($this->recordKey) === '') {
            throw new \InvalidArgumentException('Derived state keys require a non-empty record key.');
        }

        if (trim($this->variant) === '') {
            throw new \InvalidArgumentException('Derived state keys require a non-empty variant.');
        }
    }

    /**
     * @param  array<string, mixed>|string|null  $context
     */
    public static function fromModel(
        DerivedStateFamily $family,
        Model $record,
        string $variant,
        array|string|null $context = null,
        ?int $workspaceId = null,
        ?int $tenantId = null,
    ): self {
        return new self(
            family: $family,
            recordClass: $record::class,
            recordKey: (string) $record->getKey(),
            variant: $variant,
            workspaceId: $workspaceId ?? self::normalizeScopeId($record->getAttribute('workspace_id')),
            tenantId: $tenantId ?? self::normalizeScopeId($record->getAttribute('managed_environment_id')),
            contextHash: self::hashContext($context),
        );
    }

    /**
     * @return array{
     *     family: string,
     *     record_class: string,
     *     record_key: string,
     *     variant: string,
     *     workspace_id: ?int,
     *     managed_environment_id: ?int,
     *     context_hash: ?string
     * }
     */
    public function toArray(): array
    {
        return [
            'family' => $this->family->value,
            'record_class' => $this->recordClass,
            'record_key' => $this->recordKey,
            'variant' => $this->variant,
            'workspace_id' => $this->workspaceId,
            'managed_environment_id' => $this->tenantId,
            'context_hash' => $this->contextHash,
        ];
    }

    public function fingerprint(): string
    {
        try {
            /** @var string $json */
            $json = json_encode($this->toArray(), JSON_THROW_ON_ERROR);
        } catch (JsonException $exception) {
            throw new \RuntimeException('Unable to encode derived state key fingerprint.', previous: $exception);
        }

        return $json;
    }

    public function matches(
        DerivedStateFamily $family,
        ?string $recordClass = null,
        string|int|null $recordKey = null,
        ?string $variant = null,
        ?int $workspaceId = null,
        ?int $tenantId = null,
    ): bool {
        if ($this->family !== $family) {
            return false;
        }

        if ($recordClass !== null && $this->recordClass !== $recordClass) {
            return false;
        }

        if ($recordKey !== null && $this->recordKey !== (string) $recordKey) {
            return false;
        }

        if ($variant !== null && $this->variant !== $variant) {
            return false;
        }

        if ($workspaceId !== null && $this->workspaceId !== $workspaceId) {
            return false;
        }

        if ($tenantId !== null && $this->tenantId !== $tenantId) {
            return false;
        }

        return true;
    }

    /**
     * @param  array<string, mixed>|string|null  $context
     */
    public static function hashContext(array|string|null $context): ?string
    {
        if ($context === null) {
            return null;
        }

        if (is_string($context)) {
            $context = trim($context);

            return $context === '' ? null : sha1($context);
        }

        if ($context === []) {
            return null;
        }

        $normalized = self::normalizeContext($context);

        try {
            /** @var string $json */
            $json = json_encode($normalized, JSON_THROW_ON_ERROR);
        } catch (JsonException $exception) {
            throw new \RuntimeException('Unable to encode derived state context.', previous: $exception);
        }

        return sha1($json);
    }

    /**
     * @param  array<string, mixed>  $context
     * @return array<string, mixed>
     */
    private static function normalizeContext(array $context): array
    {
        ksort($context);

        foreach ($context as $key => $value) {
            if (is_array($value)) {
                /** @var mixed $normalized */
                $normalized = array_is_list($value)
                    ? array_map(static fn (mixed $item): mixed => is_array($item) ? self::normalizeContext($item) : $item, $value)
                    : self::normalizeContext($value);

                $context[$key] = $normalized;
            }
        }

        return $context;
    }

    private static function normalizeScopeId(mixed $value): ?int
    {
        if (! is_numeric($value)) {
            return null;
        }

        $normalized = (int) $value;

        return $normalized > 0 ? $normalized : null;
    }
}
