import json
import typing as t
from hashlib import sha256
from pathlib import Path

from trezorlib import cosi, device, models
from trezorlib._internal import translations
from trezorlib.debuglink import TrezorClientDebugLink as Client

from . import common

HERE = Path(__file__).resolve().parent
ROOT = HERE.parent

TRANSLATIONS = ROOT / "core" / "translations"
FONTS_DIR = TRANSLATIONS / "fonts"
ORDER_FILE = TRANSLATIONS / "order.json"

LANGUAGES = [file.stem for file in TRANSLATIONS.glob("??.json")]


def prepare_blob(
    lang_or_def: translations.JsonDef | Path | str,
    model: models.TrezorModel,
    version: translations.VersionTuple | tuple[int, int, int] | None = None,
) -> translations.TranslationsBlob:
    order = translations.order_from_json(json.loads(ORDER_FILE.read_text()))
    if isinstance(lang_or_def, str):
        lang_or_def = get_lang_json(lang_or_def)
    if isinstance(lang_or_def, Path):
        lang_or_def = t.cast(translations.JsonDef, json.loads(lang_or_def.read_text()))

    # generate raw blob
    if version is None:
        version = translations.version_from_json(lang_or_def["header"]["version"])
    elif len(version) == 3:
        # version coming from client object does not have build item
        version = *version, 0
    return translations.blob_from_defs(lang_or_def, order, model, version, FONTS_DIR)


def sign_blob(blob: translations.TranslationsBlob) -> bytes:
    # build 0-item Merkle proof
    digest = sha256(b"\x00" + blob.header_bytes).digest()
    signature = cosi.sign_with_privkeys(digest, common.PRIVATE_KEYS_DEV)
    blob.proof = translations.Proof(
        merkle_proof=[],
        sigmask=0b111,
        signature=signature,
    )
    return blob.build()


def build_and_sign_blob(
    lang_or_def: translations.JsonDef | Path | str,
    client: Client,
) -> bytes:
    blob = prepare_blob(lang_or_def, client.model, client.version)
    return sign_blob(blob)


def set_language(client: Client, lang: str):
    if lang.startswith("en"):
        language_data = b""
    else:
        language_data = build_and_sign_blob(lang, client)
    with client:
        device.change_language(client, language_data)  # type: ignore


def get_lang_json(lang: str) -> translations.JsonDef:
    assert lang in LANGUAGES
    lang_json = json.loads((TRANSLATIONS / f"{lang}.json").read_text())
    if (fonts_safe3 := lang_json.get("fonts", {}).get("##Safe3")) is not None:
        lang_json["fonts"]["T2B1"] = fonts_safe3
        lang_json["fonts"]["T3B1"] = fonts_safe3
    return lang_json


def _get_all_language_data() -> list[dict[str, str]]:
    return [_get_language_data(language) for language in LANGUAGES]


def _get_language_data(lang: str) -> dict[str, str]:
    return get_lang_json(lang)["translations"]


all_language_data = _get_all_language_data()


def _resolve_path_to_texts(
    path: str, template: t.Iterable[t.Any] = (), lower: bool = True
) -> list[str]:
    texts: list[str] = []
    lookups = path.split(".")
    for language_data in all_language_data:
        language_data_missing = False
        data: dict[str, t.Any] | str = language_data
        for lookup in lookups:
            assert isinstance(data, dict), f"{lookup} is not a dict"
            if lookup not in data:
                language_data_missing = True
                break
            data = data[lookup]
        if language_data_missing:
            continue
        assert isinstance(data, str), f"{path} is not a string"
        if template:
            data = data.format(*template)
        texts.append(data)

    if lower:
        texts = [t.lower() for t in texts]
    texts = [t.replace("\xa0", " ").strip() for t in texts]
    return texts


def assert_equals(text: str, path: str, template: t.Iterable[t.Any] = ()) -> None:
    # TODO: we can directly pass in the current device language
    texts = _resolve_path_to_texts(path, template)
    assert text.lower() in texts, f"{text} not found in {texts}"


def assert_equals_multiple(
    text: str, paths: list[str], template: t.Iterable[t.Any] = ()
) -> None:
    texts: list[str] = []
    for path in paths:
        texts += _resolve_path_to_texts(path, template)
    assert text.lower() in texts, f"{text} not found in {texts}"


def assert_in(text: str, path: str, template: t.Iterable[t.Any] = ()) -> None:
    texts = _resolve_path_to_texts(path, template)
    for tt in texts:
        if tt in text.lower():
            return
    assert False, f"{text} not found in {texts}"


def assert_in_multiple(
    text: str, paths: list[str], template: t.Iterable[t.Any] = ()
) -> None:
    texts: list[str] = []
    for path in paths:
        texts += _resolve_path_to_texts(path, template)
    for tt in texts:
        if tt in text.lower():
            return
    assert False, f"{text} not found in {texts}"


def assert_startswith(text: str, path: str, template: t.Iterable[t.Any] = ()) -> None:
    texts = _resolve_path_to_texts(path, template)
    for tt in texts:
        if text.lower().startswith(tt):
            return
    assert False, f"{text} not found in {texts}"


def assert_template(text: str, template_path: str) -> None:
    templates = _resolve_path_to_texts(template_path)
    for tt in templates:
        # Checking at least the first part
        first_part = tt.split("{")[0]
        if text.lower().startswith(first_part):
            return
    assert False, f"{text} not found in {templates}"


def translate(
    path: str, template: t.Iterable[t.Any] = (), lower: bool = False
) -> list[str]:
    # Do not converting to lowercase, we want the exact value
    return _resolve_path_to_texts(path, template, lower=lower)