"""Read-only GGPK container walker (GGPK / PDIR / FILE / FREE)."""
from __future__ import annotations

import struct
from dataclasses import dataclass
from pathlib import Path
from typing import BinaryIO, Iterator


@dataclass(frozen=True)
class GgpkEntry:
    name: str
    tag: str
    offset: int
    record_length: int
    sha256: bytes
    data_offset: int = 0
    data_size: int = 0
    child_count: int = 0
    path: str = ""


class GgpkFs:
    def __init__(self, ggpk_path: str | Path):
        self.path = Path(ggpk_path)
        self._fh: BinaryIO | None = None
        self.root_offset: int = 0

    def open(self) -> None:
        if not self.path.is_file():
            raise FileNotFoundError(f"Content.ggpk not found: {self.path}")
        self._fh = open(self.path, "rb")
        self._read_root()

    def close(self) -> None:
        if self._fh is not None:
            self._fh.close()
            self._fh = None

    def __enter__(self) -> "GgpkFs":
        self.open()
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        self.close()

    @property
    def file(self) -> BinaryIO:
        if self._fh is None:
            raise RuntimeError("GGPK is not open")
        return self._fh

    def _read_root(self) -> None:
        fh = self.file
        fh.seek(0)
        length, tag = struct.unpack("<I4s", fh.read(8))
        if tag != b"GGPK":
            raise ValueError(f"Invalid GGPK magic: {tag!r}")
        n_children = struct.unpack("<I", fh.read(4))[0]
        offsets = [struct.unpack("<Q", fh.read(8))[0] for _ in range(n_children)]
        if not offsets:
            raise ValueError("GGPK root has no children")
        self.root_offset = offsets[0]

    def read_entry(self, offset: int) -> GgpkEntry:
        fh = self.file
        fh.seek(offset)
        length, tag = struct.unpack("<I4s", fh.read(8))
        tag_s = tag.decode("ascii", "replace")
        if tag == b"FREE":
            return GgpkEntry(name="", tag="FREE", offset=offset, record_length=length, sha256=b"")
        if tag == b"PDIR":
            name_len = struct.unpack("<I", fh.read(4))[0]
            child_count = struct.unpack("<I", fh.read(4))[0]
            sha = fh.read(32)
            name = fh.read(name_len * 2).decode("utf-16le", "replace").rstrip("\x00")
            return GgpkEntry(
                name=name,
                tag="PDIR",
                offset=offset,
                record_length=length,
                sha256=sha,
                child_count=child_count,
            )
        if tag == b"FILE":
            name_len = struct.unpack("<I", fh.read(4))[0]
            sha = fh.read(32)
            name = fh.read(name_len * 2).decode("utf-16le", "replace").rstrip("\x00")
            header_len = 8 + 4 + 32 + name_len * 2
            data_size = length - header_len
            return GgpkEntry(
                name=name,
                tag="FILE",
                offset=offset,
                record_length=length,
                sha256=sha,
                data_offset=fh.tell(),
                data_size=data_size,
            )
        raise ValueError(f"Unknown GGPK tag {tag!r} at {hex(offset)}")

    def iter_dir_children(self, pdir_offset: int) -> Iterator[GgpkEntry]:
        fh = self.file
        fh.seek(pdir_offset)
        length, tag = struct.unpack("<I4s", fh.read(8))
        if tag != b"PDIR":
            return
        name_len = struct.unpack("<I", fh.read(4))[0]
        child_count = struct.unpack("<I", fh.read(4))[0]
        fh.read(32)
        fh.read(name_len * 2)
        child_ptrs = [struct.unpack("<IQ", fh.read(12)) for _ in range(child_count)]
        for _name_hash, child_off in child_ptrs:
            yield self.read_entry(child_off)

    def walk(
        self,
        dir_offset: int | None = None,
        prefix: str = "",
        max_depth: int | None = None,
        depth: int = 0,
    ) -> Iterator[GgpkEntry]:
        start = self.root_offset if dir_offset is None else dir_offset
        for child in self.iter_dir_children(start):
            path = f"{prefix}/{child.name}" if prefix else child.name
            yield GgpkEntry(
                name=child.name,
                tag=child.tag,
                offset=child.offset,
                record_length=child.record_length,
                sha256=child.sha256,
                data_offset=child.data_offset,
                data_size=child.data_size,
                child_count=child.child_count,
                path=path,
            )
            if child.tag == "PDIR" and (max_depth is None or depth < max_depth):
                yield from self.walk(child.offset, path, max_depth, depth + 1)

    def find(self, relative_path: str, max_depth: int | None = 4) -> GgpkEntry | None:
        want = relative_path.replace("\\", "/").strip("/")
        for ent in self.walk(max_depth=max_depth):
            if ent.path.replace("\\", "/") == want:
                return ent
        return None

    def read_payload(self, entry: GgpkEntry) -> bytes:
        if entry.tag != "FILE":
            raise ValueError(f"{entry.path} is not a FILE")
        self.file.seek(entry.data_offset)
        return self.file.read(entry.data_size)

    def read_payload_prefix(self, entry: GgpkEntry, nbytes: int) -> bytes:
        if entry.tag != "FILE":
            raise ValueError(f"{entry.path} is not a FILE")
        self.file.seek(entry.data_offset)
        return self.file.read(min(nbytes, entry.data_size))
