# -*- coding: utf-8 -*-
"""
HTTP Range probe: detekce jazyků audia/titulků z Webshare streamu bez ffprobe (Android / síť).

Jádro přesunuto z android_range_probe_test; výstup lze převést na stejný tvar jako parse_ffprobe_streams
pro subcaps_cache.upsert_result (range_probe_result_to_subcaps_meta).
"""

import re
import time

from resources.lib.utils.log_utils import get_logger

log = get_logger()


def _safe_url(url):
    if not url:
        return ""
    try:
        return str(url).split("?", 1)[0]
    except Exception:
        return "<invalid-url>"


def _preview_bytes(blob, limit=96):
    if not blob:
        return "", ""
    chunk = blob[:limit]
    hex_part = " ".join([f"{b:02x}" for b in chunk])
    ascii_part = "".join([chr(b) if 32 <= b <= 126 else "." for b in chunk])
    return hex_part, ascii_part


def _is_likely_matroska(blob):
    if not blob:
        return False
    if b"matroska" in blob.lower() or b"webm" in blob.lower():
        return True
    return blob.startswith(b"\x1a\x45\xdf\xa3")


def _is_likely_mp4(blob):
    if not blob or len(blob) < 8:
        return False
    if blob[4:8] == b"ftyp":
        return True
    if b"moov" in blob[:4096]:
        return True
    return False


def _is_mp4_mdat_first(blob):
    if not blob:
        return False
    mdat_pos = blob.find(b"mdat")
    moov_pos = blob.find(b"moov")
    if mdat_pos >= 0 and (moov_pos < 0 or mdat_pos < moov_pos):
        return True
    return False


def _parse_mp4_box_layout(blob):
    if not blob or len(blob) < 16:
        return 0, 0, 0
    try:
        import struct
        if blob[4:8] != b"ftyp":
            return 0, 0, 0
        ftyp_size = struct.unpack(">I", blob[0:4])[0]
        if ftyp_size < 8 or ftyp_size + 8 > len(blob):
            return 0, 0, 0
        mdat_start = ftyp_size
        if blob[mdat_start + 4:mdat_start + 8] != b"mdat":
            return 0, 0, 0
        mdat_size_raw = struct.unpack(">I", blob[mdat_start:mdat_start + 4])[0]
        if mdat_size_raw == 1:
            if mdat_start + 16 > len(blob):
                return 0, 0, 0
            mdat_size = struct.unpack(">Q", blob[mdat_start + 8:mdat_start + 16])[0]
        else:
            mdat_size = mdat_size_raw
        if mdat_size < 8:
            return 0, 0, 0
        moov_offset = ftyp_size + mdat_size
        return int(ftyp_size), int(mdat_size), int(moov_offset)
    except Exception:
        return 0, 0, 0


def _total_size_from_content_range(headers):
    cr = (headers or {}).get("Content-Range") or (headers or {}).get("content-range") or ""
    if "/" in cr:
        try:
            total = cr.rsplit("/", 1)[-1].strip()
            if total != "*":
                return int(total)
        except Exception:
            pass
    return 0


def _get_moov_size_from_data(data):
    if not data or len(data) < 8:
        return 0
    import struct
    scan_limit = min(64, len(data) - 7)
    for offset in range(scan_limit):
        if data[offset + 4:offset + 8] == b"moov":
            try:
                size_raw = struct.unpack(">I", data[offset:offset + 4])[0]
                if size_raw == 1 and offset + 16 <= len(data):
                    return int(struct.unpack(">Q", data[offset + 8:offset + 16])[0])
                elif size_raw > 8:
                    return int(size_raw)
            except Exception:
                pass
    return 0


def _read_ebml_vint_size(buf, pos):
    if not buf or pos < 0 or pos >= len(buf):
        return None, 0
    first = buf[pos]
    mask = 0x80
    length = 1
    while length <= 8 and (first & mask) == 0:
        mask >>= 1
        length += 1
    if length > 8:
        return None, 0
    if pos + length > len(buf):
        return None, 0
    value = first & (mask - 1)
    for i in range(1, length):
        value = (value << 8) | buf[pos + i]
    return int(value), int(length)


def _extract_matroska_langs(blob):
    if not blob:
        return []
    ids = (b"\x22\xb5\x9c", b"\x22\xb5\x9d")
    out = set()
    for elem_id in ids:
        start = 0
        while True:
            idx = blob.find(elem_id, start)
            if idx < 0:
                break
            size_pos = idx + len(elem_id)
            sz, sz_len = _read_ebml_vint_size(blob, size_pos)
            if sz is None or sz_len <= 0:
                start = idx + 1
                continue
            data_pos = size_pos + sz_len
            data_end = data_pos + sz
            if data_end > len(blob) or sz <= 0 or sz > 32:
                start = idx + 1
                continue
            raw = blob[data_pos:data_end]
            try:
                token = raw.decode("ascii", "ignore").strip().lower()
            except Exception:
                token = ""
            if token and re.match(r"^[a-z]{2,3}([_-][a-z]{2,8})?$", token):
                out.add(token.replace("_", "-"))
            start = idx + 1
    return sorted(out)


def _extract_langs_from_mkv_chunk(chunk):
    if not chunk:
        return []
    ids = (b"\x22\xb5\x9c", b"\x22\xb5\x9d")
    out = set()
    for elem_id in ids:
        start = 0
        while True:
            idx = chunk.find(elem_id, start)
            if idx < 0:
                break
            size_pos = idx + len(elem_id)
            sz, sz_len = _read_ebml_vint_size(chunk, size_pos)
            if sz is None or sz_len <= 0:
                start = idx + 1
                continue
            data_pos = size_pos + sz_len
            data_end = data_pos + sz
            if data_end > len(chunk) or sz <= 0 or sz > 32:
                start = idx + 1
                continue
            raw = chunk[data_pos:data_end]
            tok = raw.decode("ascii", "ignore").strip().lower().replace("_", "-")
            if tok and re.match(r"^[a-z]{2,3}([_-][a-z]{2,8})?$", tok):
                out.add(tok)
            start = idx + 1
    return sorted(out)


def _extract_mp4_langs(blob):
    if not blob:
        return []
    langs = set()
    pos = 0
    while True:
        idx = blob.find(b"mdhd", pos)
        if idx < 0:
            break
        try:
            vp = idx + 4
            if vp >= len(blob):
                break
            version = blob[vp]
            if version == 0:
                lang_offset = vp + 1 + 3 + 4 + 4 + 4 + 4
            elif version == 1:
                lang_offset = vp + 1 + 3 + 8 + 8 + 4 + 8
            else:
                pos = idx + 1
                continue
            if lang_offset + 2 <= len(blob):
                packed = (blob[lang_offset] << 8) | blob[lang_offset + 1]
                c1 = ((packed >> 10) & 0x1F) + 0x60
                c2 = ((packed >> 5) & 0x1F) + 0x60
                c3 = (packed & 0x1F) + 0x60
                raw_lang = chr(c1) + chr(c2) + chr(c3)
                if re.match(r"^[a-z]{3}$", raw_lang) and raw_lang != "und":
                    langs.add(raw_lang)
        except Exception:
            pass
        pos = idx + 1
    return sorted(langs)


def _extract_mp4_track_summary(blob):
    summary = {"audio": {}, "subtitle": {}, "video": {}, "other": {}}
    if not blob:
        return summary
    pos = 0
    while True:
        idx = blob.find(b"mdhd", pos)
        if idx < 0:
            break
        lang = None
        try:
            vp = idx + 4
            if vp >= len(blob):
                break
            version = blob[vp]
            if version == 0:
                lang_offset = vp + 1 + 3 + 4 + 4 + 4 + 4
            elif version == 1:
                lang_offset = vp + 1 + 3 + 8 + 8 + 4 + 8
            else:
                pos = idx + 1
                continue
            if lang_offset + 2 <= len(blob):
                packed = (blob[lang_offset] << 8) | blob[lang_offset + 1]
                c1 = ((packed >> 10) & 0x1F) + 0x60
                c2 = ((packed >> 5) & 0x1F) + 0x60
                c3 = (packed & 0x1F) + 0x60
                raw_lang = chr(c1) + chr(c2) + chr(c3)
                if re.match(r"^[a-z]{3}$", raw_lang):
                    lang = raw_lang
        except Exception:
            pos = idx + 1
            continue
        if lang is None:
            pos = idx + 1
            continue
        search_start = max(0, idx - 1024)
        search_end = min(len(blob), idx + 1024)
        chunk = blob[search_start:search_end]
        bucket = "other"
        hi = chunk.find(b"hdlr")
        if hi >= 0:
            try:
                ht_pos = hi + 4 + 4 + 4
                if ht_pos + 4 <= len(chunk):
                    ht = chunk[ht_pos:ht_pos + 4]
                    if ht == b"soun":
                        bucket = "audio"
                    elif ht == b"vide":
                        bucket = "video"
                    elif ht in (b"text", b"subt", b"sbtl", b"clcp", b"subp"):
                        bucket = "subtitle"
            except Exception:
                pass
        summary[bucket][lang] = summary[bucket].get(lang, 0) + 1
        pos = idx + 1
    return summary


def _extract_track_type_from_chunk(chunk):
    if not chunk:
        return None
    start = 0
    while True:
        idx = chunk.find(b"\x83", start)
        if idx < 0:
            return None
        sz, sz_len = _read_ebml_vint_size(chunk, idx + 1)
        if sz is None or sz_len <= 0:
            start = idx + 1
            continue
        val_pos = idx + 1 + sz_len
        val_end = val_pos + sz
        if val_end > len(chunk) or sz <= 0 or sz > 4:
            start = idx + 1
            continue
        val = 0
        for b in chunk[val_pos:val_end]:
            val = (val << 8) | b
        if val in (1, 2, 17, 18):
            return int(val)
        start = idx + 1


def _extract_mkv_track_summary(blob):
    summary = {"audio": {}, "subtitle": {}, "video": {}, "other": {}}
    if not blob:
        return summary
    pos = 0
    while True:
        idx = blob.find(b"\xae", pos)
        if idx < 0:
            break
        sz, sz_len = _read_ebml_vint_size(blob, idx + 1)
        if sz is None or sz_len <= 0:
            pos = idx + 1
            continue
        data_pos = idx + 1 + sz_len
        data_end = data_pos + sz
        if data_end > len(blob) or sz <= 0 or sz > 1024 * 1024:
            pos = idx + 1
            continue
        entry = blob[data_pos:data_end]
        track_type = _extract_track_type_from_chunk(entry)
        langs = _extract_langs_from_mkv_chunk(entry)
        if not langs:
            langs = ["und"]
        if track_type == 1:
            bucket = "video"
        elif track_type == 2:
            bucket = "audio"
        elif track_type == 17:
            bucket = "subtitle"
        else:
            bucket = "other"
        for lang in langs:
            summary[bucket][lang] = int(summary[bucket].get(lang, 0)) + 1
        pos = idx + 1
    return summary


def _merge_track_summaries(*items):
    out = {"audio": {}, "subtitle": {}, "video": {}, "other": {}}
    for s in items:
        if not isinstance(s, dict):
            continue
        for bucket in ("audio", "subtitle", "video", "other"):
            d = s.get(bucket) or {}
            for lang, cnt in d.items():
                out[bucket][lang] = int(out[bucket].get(lang, 0)) + int(cnt)
    return out


def is_cz_like_lang(lang):
    """Široká CZ-like detekce (vč. SK aliasů). Slouží **jen** k řazení /
    preferenci audio stop nad ne-CZ ne-SK obsahem (např. SK lepší než EN
    pro CZ uživatele). NIKDY se nesmí použít k nastavování plochého flagu
    `has_cz_audio` v subcaps_cache — k tomu slouží `is_strict_cz_lang`.
    """
    l = str(lang or "").lower()
    return l in ("cs", "cze", "ces", "sk", "slk", "slo") or l.startswith("cs-") or l.startswith("sk-")


def is_strict_cz_lang(lang):
    """Striktní CZ detekce — pouze češtinové ISO kódy (žádná SK varianta).

    Používat všude, kde se rozhoduje o tom, zda stream skutečně má **českou**
    audio stopu (`has_cz_audio`, badge `CZdab`, …). Slovenština je sice
    příbuzný jazyk a v UI ji řadíme nad EN, ale **CZdab není**.
    """
    l = str(lang or "").lower()
    return l in ("cs", "cz", "cze", "ces") or l.startswith("cs-") or l.startswith("cz-")


def _lang_count_items(d, prefer_cz=False):
    if not d:
        return []
    if not prefer_cz:
        return sorted(d.items(), key=lambda kv: (-int(kv[1]), kv[0]))
    return sorted(
        d.items(),
        key=lambda kv: (0 if is_cz_like_lang(kv[0]) else 1, -int(kv[1]), kv[0]),
    )


def _fetch_range(url, start=None, end=None, timeout=12, session=None):
    try:
        import requests
    except Exception as ex:
        log.error(f"requests import failed: {ex}")
        return {
            "ok": False, "status": 0, "headers": {}, "data": b"",
            "elapsed_ms": 0, "error": f"requests import failed: {ex}",
            "request_range": "", "final_url": "", "history": [],
        }
    client = session if session is not None else requests
    headers = {}
    if start is not None and end is not None:
        headers["Range"] = f"bytes={int(start)}-{int(end)}"
    elif start is None and end is not None:
        headers["Range"] = f"bytes=-{int(end)}"
    req_range = headers.get("Range", "<none>")
    t0 = time.time()
    log.info(f"RANGE REQUEST start url={_safe_url(url)} range={req_range} timeout={timeout}s")
    try:
        resp = client.get(url, headers=headers, timeout=timeout, stream=True, allow_redirects=True)
        data = resp.content or b""
        elapsed_ms = int((time.time() - t0) * 1000)
        final_url = getattr(resp, "url", "") or ""
        history = []
        try:
            for h in list(resp.history or []):
                history.append({"status": int(h.status_code), "url": _safe_url(getattr(h, "url", ""))})
        except Exception:
            pass
        log.info(
            f"RANGE RESPONSE status={resp.status_code} bytes={len(data)} elapsed_ms={elapsed_ms} "
            f"content_range={(resp.headers or {}).get('Content-Range', '')} final={_safe_url(final_url)}"
        )
        return {
            "ok": True, "status": int(resp.status_code), "headers": dict(resp.headers or {}),
            "data": data, "elapsed_ms": elapsed_ms, "error": "", "request_range": req_range,
            "final_url": final_url, "history": history,
        }
    except Exception as ex:
        elapsed_ms = int((time.time() - t0) * 1000)
        log.warning(f"range fetch failed range={req_range} elapsed_ms={elapsed_ms} err={ex}")
        return {
            "ok": False, "status": 0, "headers": {}, "data": b"",
            "elapsed_ms": elapsed_ms, "error": str(ex), "request_range": req_range,
            "final_url": "", "history": [],
        }


def _content_length_from_headers(headers):
    try:
        v = headers.get("Content-Length") or headers.get("content-length")
        return int(v) if v else 0
    except Exception:
        return 0


def _detect_tokens(blob):
    if not blob:
        return {
            "iso_lang_hits": [], "word_hits": [], "confidence": "none",
            "cz_audio_guess": False, "cz_subs_guess": False,
        }
    text = blob.decode("latin-1", "ignore").lower()
    mkv_langs = _extract_matroska_langs(blob)
    mp4_langs = _extract_mp4_langs(blob) if _is_likely_mp4(blob) else []
    all_container_langs = sorted(set(mkv_langs + mp4_langs))
    iso_patterns = [
        r"language[^a-z0-9]{0,8}(cs|ces|cze|sk|slk|slo)",
        r"lang[^a-z0-9]{0,8}(cs|ces|cze|sk|slk|slo)",
    ]
    iso_lang_hits = []
    for pat in iso_patterns:
        iso_lang_hits.extend(re.findall(pat, text, flags=re.IGNORECASE))
    word_hits = []
    for token in ("czech", "cesky", "češt", "slovak", "slovensky", "subtitle", "subtitles"):
        if token in text:
            word_hits.append(token)
    if all_container_langs or iso_lang_hits:
        confidence = "high"
    elif word_hits:
        confidence = "medium"
    else:
        confidence = "low"
    cz_signal = (
        bool(iso_lang_hits)
        or any(t in word_hits for t in ("czech", "cesky", "češt"))
        or any(x.startswith("cs") or x.startswith("sk") or x in ("cze", "ces", "slo", "slk") for x in all_container_langs)
    )
    return {
        "iso_lang_hits": sorted(set([str(x).lower() for x in iso_lang_hits])),
        "mkv_lang_hits": all_container_langs,
        "word_hits": sorted(set(word_hits)),
        "confidence": confidence,
        "cz_audio_guess": cz_signal,
        "cz_subs_guess": cz_signal,
    }


def _subs_langs_from_track_summary(track_summary):
    """Seznam jazyků titulků pro shodu s ffprobe větví (lowercase)."""
    subs = track_summary.get("subtitle") or {}
    out = []
    for lang in subs:
        l = str(lang).strip().lower()
        if l:
            out.append(l)
    return out


def _norm_lang_for_cz_subs(lang):
    l = (lang or "").lower()
    if l in ("cs", "cz", "cze", "ces"):
        return True
    if l.startswith("cs-"):
        return True
    return False


def range_probe_result_to_subcaps_meta(probe_result):
    """
    Převod výstupu run_http_range_probe na dict pro subcaps_cache.upsert_result
    (stejné klíče jako parse_ffprobe_streams).

    Prázdný výsledek (selhání head / žádná data) vrátí prázdné klíče jako parse_ffprobe_streams({}).
    """
    empty = {
        "cz_subs": False, "subs_langs": [], "has_cz_audio": False,
        "audio_tracks": [], "best_audio": None, "video": {},
    }
    if not probe_result or not probe_result.get("ok_head"):
        return empty

    ts = probe_result.get("track_summary") or {}
    audio_counts = ts.get("audio") or {}
    sub_counts = ts.get("subtitle") or {}

    has_tracks = bool(audio_counts or sub_counts)
    if not has_tracks and not probe_result.get("cz_guess"):
        return empty

    subs_langs = _subs_langs_from_track_summary(ts)
    cz_subs = any(_norm_lang_for_cz_subs(x) for x in subs_langs)

    audio_tracks = []
    for lang, _cnt in sorted(audio_counts.items(), key=lambda kv: (-int(kv[1]), kv[0])):
        audio_tracks.append({
            "lang": (lang or "und").lower(),
            "codec": "unknown",
            "channels": None,
            "layout": None,
            "title": "",
            "bit_rate": None,
            "atmos": False,
        })
    if not audio_tracks:
        audio_tracks.append({
            "lang": "und", "codec": "unknown", "channels": None, "layout": None,
            "title": "", "bit_rate": None, "atmos": False,
        })

    # POZOR: striktní CZ detekce — `is_cz_like_lang` zahrnuje i SK varianty,
    # což je správně pro řazení preferencí, ale NESMÍ se to propagovat do
    # plochého flagu `has_cz_audio` (jinak SK dub → falešné CZdab v UI).
    has_cz_audio = any(is_strict_cz_lang(t.get("lang")) for t in audio_tracks)

    def _rank(tr):
        l = (tr.get("lang") or "").lower()
        if l in ("cs", "cz", "cze", "ces"):
            return 100
        if is_cz_like_lang(l):
            return 95
        if l in ("en", "eng"):
            return 80
        return 10

    best_audio = None
    best_score = -1
    for tr in audio_tracks:
        sc = _rank(tr)
        if sc > best_score:
            best_score = sc
            best_audio = tr

    video = {
        "width": 0, "height": 0, "pix_fmt": None,
        "hdr": False, "dv": False, "resolution_tag": None,
        "probe_source": "http-range",
    }

    return {
        "cz_subs": cz_subs,
        "subs_langs": subs_langs,
        "has_cz_audio": has_cz_audio,
        "audio_tracks": audio_tracks,
        "best_audio": best_audio,
        "video": video,
    }


def run_http_range_probe(url, session=None, progress_callback=None):
    """
    Provede head/tail/(deep) HTTP Range sondu nad stream URL.

    progress_callback: volitelně callable(percent:int, message:str)

    Vrací dict s poli pro UI i pro range_probe_result_to_subcaps_meta.
    """
    def _prog(pct, msg):
        if progress_callback:
            try:
                progress_callback(int(pct), msg)
            except Exception:
                pass

    head = _fetch_range(url, 0, 262143, session=session)
    ok_head = head.get("ok", False)
    status_head = head.get("status", 0)
    headers_head = head.get("headers") or {}
    data_head = head.get("data") or b""
    head_len = len(data_head)
    content_len = _content_length_from_headers(headers_head)
    accept_ranges = (headers_head.get("Accept-Ranges") or headers_head.get("accept-ranges") or "").lower()

    det_head = _detect_tokens(data_head)
    is_mkv_head = _is_likely_matroska(data_head)
    is_mp4_head = not is_mkv_head and _is_likely_mp4(data_head)
    if is_mkv_head:
        track_head = _extract_mkv_track_summary(data_head)
    elif is_mp4_head:
        track_head = _extract_mp4_track_summary(data_head)
    else:
        track_head = {"audio": {}, "subtitle": {}, "video": {}, "other": {}}

    is_mp4_mdat_first = is_mp4_head and _is_mp4_mdat_first(data_head)
    ftyp_sz, mdat_sz, moov_offset = (0, 0, 0)
    total_file_size = _total_size_from_content_range(headers_head)
    if is_mp4_mdat_first:
        _ft, _md, moov_offset = _parse_mp4_box_layout(data_head)

    log.info(
        f"http_range_probe container: mkv={is_mkv_head} mp4={is_mp4_head} "
        f"mdat_first={is_mp4_mdat_first} moov_offset={moov_offset} total_size={total_file_size}"
    )

    has_head_tracks = bool((track_head.get("audio") or {}) or (track_head.get("subtitle") or {}))

    tail = {
        "ok": True, "status": 0, "headers": {}, "data": b"",
        "elapsed_ms": 0, "error": "skipped (head tracks found)",
        "request_range": "skipped", "final_url": "", "history": [],
    }
    data_tail = b""
    det_tail = {
        "iso_lang_hits": [], "mkv_lang_hits": [], "word_hits": [],
        "confidence": "none", "cz_audio_guess": False, "cz_subs_guess": False,
    }
    track_tail = {"audio": {}, "subtitle": {}, "video": {}, "other": {}}

    _prog(20, "Provadim HEAD/Range diagnostiku...")

    if not has_head_tracks:
        if is_mp4_mdat_first and moov_offset > 0:
            moov_fetch_end = moov_offset + 4 * 1024 * 1024
            if total_file_size > 0:
                moov_fetch_end = min(moov_fetch_end, total_file_size - 1)
            _prog(55, f"Stahuji moov box (offset={moov_offset})...")
            tail = _fetch_range(url, moov_offset, moov_fetch_end, session=session)
            data_tail = tail.get("data") or b""
            moov_total_size = _get_moov_size_from_data(data_tail)
            MAX_MOOV = 32 * 1024 * 1024
            if moov_total_size > 0 and len(data_tail) < moov_total_size <= MAX_MOOV:
                extra_start = moov_offset + len(data_tail)
                extra_end = moov_offset + moov_total_size - 1
                if total_file_size > 0:
                    extra_end = min(extra_end, total_file_size - 1)
                _prog(65, f"Doplnuji moov box ({moov_total_size // 1024} KB)...")
                extra_tail = _fetch_range(url, extra_start, extra_end, session=session)
                if extra_tail.get("ok") and extra_tail.get("data"):
                    data_tail = data_tail + extra_tail["data"]
        else:
            _prog(55, "Nacitam konec souboru (suffix range)...")
            tail = _fetch_range(url, None, 524288, session=session)
            data_tail = tail.get("data") or b""
        det_tail = _detect_tokens(data_tail)
        if _is_likely_matroska(data_tail):
            track_tail = _extract_mkv_track_summary(data_tail)
        elif is_mp4_head:
            track_tail = _extract_mp4_track_summary(data_tail)
    else:
        log.info("http_range_probe lazy-mode: tail/deep skipped, head already has audio/sub tracks")

    ok_tail = tail.get("ok", False)
    status_tail = int(tail.get("status", 0) or 0)
    tail_len = len(data_tail)

    _prog(80, "Vyhodnocuji signaly CZ jazyka...")

    deep = None
    data_deep = b""
    det_deep = {}
    track_deep = {"audio": {}, "subtitle": {}, "video": {}, "other": {}}

    need_deep = (
        not has_head_tracks
        and not is_mp4_mdat_first
        and not (
            det_head.get("iso_lang_hits")
            or det_head.get("mkv_lang_hits")
            or det_tail.get("iso_lang_hits")
            or det_tail.get("mkv_lang_hits")
        )
        and (is_mkv_head or is_mp4_head or bool(det_head.get("word_hits")))
    )
    if need_deep:
        container_hint = "MKV" if is_mkv_head else ("MP4" if is_mp4_head else "neznamy")
        _prog(88, f"{container_hint} detekovan, nacitam deep-head (2 MB)...")
        deep = _fetch_range(url, 0, 2097151, session=session)
        data_deep = deep.get("data") or b""
        det_deep = _detect_tokens(data_deep)
        if data_deep:
            if _is_likely_matroska(data_deep):
                track_deep = _extract_mkv_track_summary(data_deep)
            elif is_mp4_head:
                track_deep = _extract_mp4_track_summary(data_deep)

    track_summary = _merge_track_summaries(track_head, track_tail, track_deep)
    cz_audio_count = sum(int(v) for k, v in (track_summary.get("audio") or {}).items() if is_cz_like_lang(k))
    cz_sub_count = sum(int(v) for k, v in (track_summary.get("subtitle") or {}).items() if is_cz_like_lang(k))

    merged_iso = sorted(set((det_head.get("iso_lang_hits") or []) + (det_tail.get("iso_lang_hits") or [])))
    merged_mkv = sorted(set((det_head.get("mkv_lang_hits") or []) + (det_tail.get("mkv_lang_hits") or [])))
    merged_words = sorted(set((det_head.get("word_hits") or []) + (det_tail.get("word_hits") or [])))

    if det_deep:
        merged_iso = sorted(set(merged_iso + (det_deep.get("iso_lang_hits") or [])))
        merged_mkv = sorted(set(merged_mkv + (det_deep.get("mkv_lang_hits") or [])))
        merged_words = sorted(set(merged_words + (det_deep.get("word_hits") or [])))

    if merged_iso or merged_mkv:
        final_conf = "high"
    elif merged_words:
        final_conf = "medium"
    else:
        final_conf = "low"

    cz_guess = (
        bool(merged_iso)
        or any(t in merged_words for t in ("czech", "cesky", "češt"))
        or any(x.startswith("cs") or x.startswith("sk") or x in ("cze", "ces", "slo", "slk") for x in merged_mkv)
    )

    log.info(
        "http_range_probe summary "
        f"head_ok={ok_head} head_status={status_head} head_bytes={head_len} "
        f"tail_ok={ok_tail} tail_status={status_tail} tail_bytes={tail_len} deep_used={bool(deep)} "
        f"cz_audio_count={cz_audio_count} cz_sub_count={cz_sub_count} confidence={final_conf} cz_guess={cz_guess}"
    )

    return {
        "ok_head": ok_head,
        "head": head,
        "tail": tail,
        "deep": deep,
        "data_head": data_head,
        "data_tail": data_tail,
        "data_deep": data_deep,
        "status_head": status_head,
        "status_tail": status_tail,
        "head_len": head_len,
        "tail_len": tail_len,
        "content_len": content_len,
        "accept_ranges": accept_ranges,
        "total_file_size": total_file_size,
        "is_mkv_head": is_mkv_head,
        "is_mp4_head": is_mp4_head,
        "is_mp4_mdat_first": is_mp4_mdat_first,
        "moov_offset": moov_offset,
        "has_head_tracks": has_head_tracks,
        "track_summary": track_summary,
        "cz_audio_count": cz_audio_count,
        "cz_sub_count": cz_sub_count,
        "merged_iso": merged_iso,
        "merged_mkv": merged_mkv,
        "merged_words": merged_words,
        "final_conf": final_conf,
        "cz_guess": cz_guess,
    }


def format_probe_dialog_lines(name, probe_result):
    """Řádky pro textviewer (TEST Android CZ probe) — stejný obsah jako dříve."""
    if not probe_result:
        return [name or "Stream", "", "(žádná data)"]

    ok_head = probe_result.get("ok_head", False)
    head = probe_result.get("head") or {}
    tail = probe_result.get("tail") or {}
    deep = probe_result.get("deep")
    data_head = probe_result.get("data_head") or b""
    data_tail = probe_result.get("data_tail") or b""
    data_deep = probe_result.get("data_deep") or b""

    status_head = probe_result.get("status_head", 0)
    status_tail = probe_result.get("status_tail", 0)
    head_len = probe_result.get("head_len", 0)
    tail_len = probe_result.get("tail_len", 0)
    content_len = probe_result.get("content_len", 0)
    accept_ranges = probe_result.get("accept_ranges", "")
    total_file_size = probe_result.get("total_file_size", 0)
    is_mkv_head = probe_result.get("is_mkv_head", False)
    is_mp4_head = probe_result.get("is_mp4_head", False)
    is_mp4_mdat_first = probe_result.get("is_mp4_mdat_first", False)
    moov_offset = probe_result.get("moov_offset", 0)
    has_head_tracks = probe_result.get("has_head_tracks", False)
    track_summary = probe_result.get("track_summary") or {}
    cz_audio_count = probe_result.get("cz_audio_count", 0)
    cz_sub_count = probe_result.get("cz_sub_count", 0)
    merged_iso = probe_result.get("merged_iso") or []
    merged_mkv = probe_result.get("merged_mkv") or []
    merged_words = probe_result.get("merged_words") or []
    final_conf = probe_result.get("final_conf", "low")
    cz_guess = probe_result.get("cz_guess", False)

    _, h_ascii = _preview_bytes(data_head)
    _, t_ascii = _preview_bytes(data_tail)
    _, d_ascii = _preview_bytes(data_deep)

    if h_ascii:
        log.info(f"TEST probe head ascii preview: {h_ascii}")
    if t_ascii:
        log.info(f"TEST probe tail ascii preview: {t_ascii}")
    if d_ascii:
        log.info(f"TEST probe deep ascii preview: {d_ascii}")

    if not ok_head:
        log.warning(f"TEST probe head error: {head.get('error', '')}")
    if not has_head_tracks and not tail.get("ok", False):
        log.warning(f"TEST probe tail error: {tail.get('error', '')}")

    lines = []
    lines.append(name)
    lines.append("")

    lines.append("AUDIO STOPY (odhad z kontejneru):")
    audio_items = _lang_count_items(track_summary.get("audio") or {})
    if not audio_items:
        lines.append("  Zadna data o audio stopach.")
    else:
        for i, (lang, cnt) in enumerate(audio_items, 1):
            mark = " [CZ]" if is_cz_like_lang(lang) else ""
            lines.append(f"  {i}) {str(lang).upper()} | Stopy: {cnt}{mark}")
    lines.append("")

    lines.append("TITULKY (odhad z MKV):")
    sub_items = _lang_count_items(track_summary.get("subtitle") or {}, prefer_cz=True)
    if not sub_items:
        lines.append("  Zadna data o titulcich.")
    else:
        for i, (lang, cnt) in enumerate(sub_items, 1):
            mark = " [CZtit]" if is_cz_like_lang(lang) else ""
            lines.append(f"  {i}) {str(lang).upper()} | Stopy: {cnt}{mark}")
    lines.append("")

    lines.append("VIDEO:")
    container_label = "MKV/Matroska" if is_mkv_head else ("MP4/ISO BMFF" if is_mp4_head else "neznamy")
    if is_mp4_mdat_first:
        container_label += " [mdat-first, moov na konci]"
    lines.append(f"  Kontejner: {container_label}")
    lines.append(f"  Content-Length: {content_len or 'N/A'}")
    lines.append(f"  File size (Content-Range): {total_file_size or 'N/A'}")
    if is_mp4_mdat_first and moov_offset:
        lines.append(f"  moov offset: {moov_offset} ({moov_offset // (1024*1024)}.{(moov_offset % (1024*1024)) // 1024} MB)")
    lines.append("")

    lines.append("[SOUHRN TESTU]")
    lines.append(f"CZ audio count (odhad): {cz_audio_count}")
    lines.append(f"CZ subtitle count (odhad): {cz_sub_count}")
    lines.append(f"CZ odhad: {'ANO' if cz_guess else 'NE'}")
    lines.append(f"Confidence: {final_conf}")
    lines.append("")

    lines.append("[Detekce tokenu]")
    lines.append(f"ISO hity: {', '.join(merged_iso) if merged_iso else '-'}")
    lines.append(f"MKV lang hity: {', '.join(merged_mkv) if merged_mkv else '-'}")
    lines.append(f"Slovni hity: {', '.join(merged_words) if merged_words else '-'}")
    lines.append("")

    lines.append("[HTTP diagnostika]")
    lines.append(f"HEAD: {status_head} | bytes={head_len} | {head.get('elapsed_ms', 0)} ms | {head.get('request_range', '<none>')}")
    if has_head_tracks:
        lines.append("TAIL: SKIPPED (head uz obsahuje audio/sub stopy)")
    else:
        lines.append(f"TAIL: {status_tail} | bytes={tail_len} | {tail.get('elapsed_ms', 0)} ms | {tail.get('request_range', '<none>')}")
    lines.append(f"Accept-Ranges: {accept_ranges or 'N/A'}")
    lines.append(f"Deep-head used: {'ANO' if deep else 'NE'}")
    if deep:
        lines.append(f"DEEP: {deep.get('status', 0)} | bytes={len(data_deep)} | {deep.get('elapsed_ms', 0)} ms")
    if not ok_head:
        lines.append(f"HEAD error: {head.get('error') or '-'}")
    if not has_head_tracks and not tail.get("ok", False):
        lines.append(f"TAIL error: {tail.get('error') or '-'}")
    lines.append("")
    lines.append("Pozn.: Range fallback bez ffprobe (Android/Windows).")

    return lines
