<?php

declare(strict_types=1);

namespace App\Support\RestoreSafety;

use InvalidArgumentException;
use JsonException;

final readonly class RestoreScopeFingerprint
{
    public function __construct(
        public ?int $backupSetId,
        public string $scopeMode,
        /**
         * @var list<int>
         */
        public array $selectedItemIds,
        /**
         * @var array<string, string>
         */
        public array $groupMapping,
        public string $groupMappingFingerprint,
        public string $fingerprint,
    ) {
        if (! in_array($this->scopeMode, ['all', 'selected'], true)) {
            throw new InvalidArgumentException('Restore scope mode must be all or selected.');
        }
    }

    public static function fromInputs(
        mixed $backupSetId,
        mixed $scopeMode,
        mixed $selectedItemIds,
        mixed $groupMapping,
    ): self {
        $normalizedBackupSetId = is_numeric($backupSetId) ? max(1, (int) $backupSetId) : null;
        $normalizedScopeMode = $scopeMode === 'selected' ? 'selected' : 'all';
        $normalizedSelectedItemIds = self::normalizeItemIds(
            $normalizedScopeMode === 'selected' ? $selectedItemIds : []
        );
        $normalizedGroupMapping = self::normalizeGroupMapping($groupMapping);
        $groupMappingFingerprint = self::hashPayload($normalizedGroupMapping);

        return new self(
            backupSetId: $normalizedBackupSetId,
            scopeMode: $normalizedScopeMode,
            selectedItemIds: $normalizedSelectedItemIds,
            groupMapping: $normalizedGroupMapping,
            groupMappingFingerprint: $groupMappingFingerprint,
            fingerprint: self::hashPayload([
                'backup_set_id' => $normalizedBackupSetId,
                'scope_mode' => $normalizedScopeMode,
                'selected_item_ids' => $normalizedSelectedItemIds,
                'group_mapping_fingerprint' => $groupMappingFingerprint,
            ]),
        );
    }

    public static function fromArray(mixed $payload): ?self
    {
        if (! is_array($payload)) {
            return null;
        }

        if (
            ! array_key_exists('backup_set_id', $payload)
            && ! array_key_exists('scope_mode', $payload)
            && ! array_key_exists('fingerprint', $payload)
        ) {
            return null;
        }

        return self::fromInputs(
            $payload['backup_set_id'] ?? null,
            $payload['scope_mode'] ?? null,
            $payload['selected_item_ids'] ?? [],
            $payload['group_mapping'] ?? [],
        );
    }

    public function matches(?string $fingerprint): bool
    {
        return is_string($fingerprint)
            && $fingerprint !== ''
            && hash_equals($this->fingerprint, $fingerprint);
    }

    /**
     * @return array{
     *     backup_set_id: ?int,
     *     scope_mode: string,
     *     selected_item_ids: list<int>,
     *     group_mapping: array<string, string>,
     *     group_mapping_fingerprint: string,
     *     fingerprint: string
     * }
     */
    public function toArray(): array
    {
        return [
            'backup_set_id' => $this->backupSetId,
            'scope_mode' => $this->scopeMode,
            'selected_item_ids' => $this->selectedItemIds,
            'group_mapping' => $this->groupMapping,
            'group_mapping_fingerprint' => $this->groupMappingFingerprint,
            'fingerprint' => $this->fingerprint,
        ];
    }

    /**
     * @return list<int>
     */
    private static function normalizeItemIds(mixed $selectedItemIds): array
    {
        if (! is_array($selectedItemIds)) {
            return [];
        }

        $normalized = [];

        foreach ($selectedItemIds as $itemId) {
            if (is_int($itemId) && $itemId > 0) {
                $normalized[] = $itemId;

                continue;
            }

            if (is_string($itemId) && ctype_digit($itemId) && (int) $itemId > 0) {
                $normalized[] = (int) $itemId;
            }
        }

        $normalized = array_values(array_unique($normalized));
        sort($normalized);

        return $normalized;
    }

    /**
     * @return array<string, string>
     */
    private static function normalizeGroupMapping(mixed $groupMapping): array
    {
        if ($groupMapping instanceof \Illuminate\Contracts\Support\Arrayable) {
            $groupMapping = $groupMapping->toArray();
        }

        if ($groupMapping instanceof \stdClass) {
            $groupMapping = (array) $groupMapping;
        }

        if (! is_array($groupMapping)) {
            return [];
        }

        $normalized = [];

        foreach ($groupMapping as $sourceGroupId => $targetGroupId) {
            if (! is_string($sourceGroupId) || trim($sourceGroupId) === '') {
                continue;
            }

            if ($targetGroupId instanceof \BackedEnum) {
                $targetGroupId = $targetGroupId->value;
            }

            if (! is_string($targetGroupId)) {
                continue;
            }

            $targetGroupId = trim($targetGroupId);

            if ($targetGroupId === '') {
                continue;
            }

            $normalized[trim($sourceGroupId)] = strtoupper($targetGroupId) === 'SKIP'
                ? 'SKIP'
                : $targetGroupId;
        }

        ksort($normalized);

        return $normalized;
    }

    /**
     * @param  array<mixed>  $payload
     */
    private static function hashPayload(array $payload): string
    {
        try {
            return hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR));
        } catch (JsonException $exception) {
            throw new InvalidArgumentException('Restore scope payload could not be fingerprinted.', previous: $exception);
        }
    }
}
