from __future__ import annotations

import hashlib
import json
from typing import Any
import unicodedata

HashableParameters = dict[str, str | list[str]]


def _normalize_text(text: str) -> str:
    return unicodedata.normalize("NFC", text)


def _is_valid_parameters_value(value: Any) -> bool:
    if isinstance(value, str):
        return True

    if not isinstance(value, list):
        return False

    for item in value:
        if not isinstance(item, str):
            return False

    return True


def _as_utf8(text: str) -> bytes:
    return text.encode("utf-8")


def _canonicalize_string_parameters(parameters: HashableParameters) -> str:
    if not isinstance(parameters, dict):
        raise TypeError("Cannot canonicalize parameters objects that are not string maps of strings or arrays of "
                        "strings")

    parts: list[str] = []
    sorted_parameters = dict()
    for key, value in parameters.items():
        sorted_parameters[_normalize_text(key)] = value

    for key in sorted(sorted_parameters.keys(), key=_as_utf8):
        raw_value = sorted_parameters[key]
        if not _is_valid_parameters_value(raw_value):
            raise TypeError(f"Invalid parameters value for '{key}': {raw_value}")

        encoded_key = json.dumps(key, ensure_ascii=True, separators=(",", ":"))

        if isinstance(raw_value, str):
            encoded_value = json.dumps(_normalize_text(raw_value), ensure_ascii=True, separators=(",", ":"))
        else:
            normalized_entries = [_normalize_text(raw_value_entry) for raw_value_entry in raw_value]
            normalized_entries.sort(key=_as_utf8)
            encoded_value = "[" + ",".join(
                json.dumps(
                    raw_value_entry, ensure_ascii=True, separators=(",", ":")
                ) for raw_value_entry in normalized_entries
            ) + "]"

        parts.append(f"{encoded_key}:{encoded_value}")

    return "{" + ",".join(parts) + "}"


def hash_parameters(parameters: HashableParameters) -> str:
    canonical = _canonicalize_string_parameters(parameters)
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
