"""Parse decompressed Bundles2/_.index.bin (PoE 3.21.2+ / PoE2)."""
from __future__ import annotations

import struct
from dataclasses import dataclass

from .bundle import decompress_bundle
from .murmur import hash_bundle_path


@dataclass(frozen=True)
class BundleInfo:
    name: str
    uncompressed_size: int


@dataclass(frozen=True)
class LogicalFile:
    path: str
    hash: int
    bundle_index: int
    offset: int
    size: int
    bundle_name: str


def _read_u32(data: bytes, pos: int) -> tuple[int, int]:
    return struct.unpack_from("<I", data, pos)[0], pos + 4


def _read_u64(data: bytes, pos: int) -> tuple[int, int]:
    return struct.unpack_from("<Q", data, pos)[0], pos + 8


def generate_paths(section: bytes) -> list[str]:
    """Path-spec decoder (poedb Bundle schema, 3.21.2+)."""
    pos = 0
    n = len(section)
    bases: list[str] = []
    outputs: list[str] = []
    generating = True

    def read_u32() -> int:
        nonlocal pos
        if pos + 4 > n:
            raise ValueError("path spec truncated (u32)")
        v = struct.unpack_from("<I", section, pos)[0]
        pos += 4
        return v

    def read_str() -> str:
        nonlocal pos
        try:
            end = section.index(0, pos)
        except ValueError as exc:
            raise ValueError("path spec truncated (string)") from exc
        s = section[pos:end].decode("ascii", "replace")
        pos = end + 1
        return s

    while pos + 4 <= n:
        word = read_u32()
        if word == 0:
            generating = not generating
            if not generating:
                bases = []
            continue
        if pos >= n:
            break
        s = read_str()
        if generating:
            if 1 <= word <= len(bases):
                outputs.append(bases[word - 1] + s)
            else:
                outputs.append(s)
        else:
            if 1 <= word <= len(bases):
                bases.append(bases[word - 1] + s)
            else:
                bases.append(s)
    return outputs


def parse_index(uncompressed: bytes) -> tuple[list[BundleInfo], list[LogicalFile]]:
    pos = 0
    bundle_count, pos = _read_u32(uncompressed, pos)
    bundles: list[BundleInfo] = []
    for _ in range(bundle_count):
        name_len, pos = _read_u32(uncompressed, pos)
        name = uncompressed[pos : pos + name_len].decode("ascii", "replace")
        pos += name_len
        usize, pos = _read_u32(uncompressed, pos)
        bundles.append(BundleInfo(name=name, uncompressed_size=usize))

    file_count, pos = _read_u32(uncompressed, pos)
    raw_files: list[tuple[int, int, int, int]] = []
    for _ in range(file_count):
        h, pos = _read_u64(uncompressed, pos)
        bidx, pos = _read_u32(uncompressed, pos)
        off, pos = _read_u32(uncompressed, pos)
        size, pos = _read_u32(uncompressed, pos)
        raw_files.append((h, bidx, off, size))

    path_rep_count, pos = _read_u32(uncompressed, pos)
    reps: list[tuple[int, int, int, int]] = []
    for _ in range(path_rep_count):
        h, pos = _read_u64(uncompressed, pos)
        poff, pos = _read_u32(uncompressed, pos)
        psize, pos = _read_u32(uncompressed, pos)
        rec, pos = _read_u32(uncompressed, pos)
        reps.append((h, poff, psize, rec))

    nested = decompress_bundle(uncompressed[pos:])
    hash_to_path: dict[int, str] = {}
    for _h, poff, psize, _rec in reps:
        if psize == 0:
            continue
        for path in generate_paths(nested[poff : poff + psize]):
            lowered = path.replace("\\", "/").lower()
            hash_to_path[hash_bundle_path(lowered)] = lowered

    logical: list[LogicalFile] = []
    for h, bidx, off, size in raw_files:
        path = hash_to_path.get(h, f"hash:{h:016x}")
        bname = bundles[bidx].name if 0 <= bidx < len(bundles) else ""
        logical.append(
            LogicalFile(
                path=path,
                hash=h,
                bundle_index=bidx,
                offset=off,
                size=size,
                bundle_name=bname,
            )
        )
    return bundles, logical


def decompress_index_blob(compressed: bytes) -> tuple[list[BundleInfo], list[LogicalFile]]:
    return parse_index(decompress_bundle(compressed))
