<?php

namespace App\Services\Drift;

class DriftHasher
{
    /**
     * @param  array<int, string>  $volatileKeys
     */
    public function hashNormalized(mixed $value, array $volatileKeys = [
        '@odata.context',
        '@odata.etag',
        'createdDateTime',
        'lastModifiedDateTime',
        'modifiedDateTime',
        'createdAt',
        'updatedAt',
    ]): string
    {
        $normalized = $this->normalizeValue($value, $volatileKeys);

        return hash('sha256', json_encode($normalized, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
    }

    public function fingerprint(
        int $tenantId,
        string $scopeKey,
        string $subjectType,
        string $subjectExternalId,
        string $changeType,
        string $baselineHash,
        string $currentHash,
    ): string {
        $parts = [
            (string) $tenantId,
            $this->normalize($scopeKey),
            $this->normalize($subjectType),
            $this->normalize($subjectExternalId),
            $this->normalize($changeType),
            $this->normalize($baselineHash),
            $this->normalize($currentHash),
        ];

        return hash('sha256', implode('|', $parts));
    }

    private function normalize(string $value): string
    {
        return trim(mb_strtolower($value));
    }

    /**
     * @param  array<int, string>  $volatileKeys
     */
    private function normalizeValue(mixed $value, array $volatileKeys): mixed
    {
        if (is_array($value)) {
            if ($this->isList($value)) {
                $items = array_map(fn ($item) => $this->normalizeValue($item, $volatileKeys), $value);

                usort($items, function ($a, $b): int {
                    return strcmp(
                        json_encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                        json_encode($b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
                    );
                });

                return $items;
            }

            $result = [];

            foreach ($value as $key => $item) {
                if (is_string($key) && in_array($key, $volatileKeys, true)) {
                    continue;
                }

                $result[$key] = $this->normalizeValue($item, $volatileKeys);
            }

            ksort($result);

            return $result;
        }

        if (is_string($value)) {
            return trim($value);
        }

        return $value;
    }

    private function isList(array $value): bool
    {
        if ($value === []) {
            return true;
        }

        return array_keys($value) === range(0, count($value) - 1);
    }
}
