"""Parse Path of Exile .datc64 using poe-tool-dev/dat-schema column types."""
from __future__ import annotations

import json
import struct
from pathlib import Path
from typing import Any

BB_MARKER = b"\xBB" * 8
NULL_REF = 0xFEFEFEFEFEFEFEFE

_SCALAR_SIZE = {
    "bool": 1,
    "i16": 2,
    "u16": 2,
    "i32": 4,
    "u32": 4,
    "f32": 4,
    "string": 8,
    "enumrow": 4,
    "foreignrow": 16,
    "row": 16,
    "array": 16,
}


def load_schema(path: str | Path) -> dict[str, Any]:
    return json.loads(Path(path).read_text(encoding="utf-8"))


def poe2_table_schema(schema: dict[str, Any], table_name: str) -> dict[str, Any] | None:
    found: dict[str, Any] | None = None
    for table in schema.get("tables") or []:
        if table.get("name") != table_name:
            continue
        valid = table.get("validFor")
        if valid in (2, 3):
            return table
        if valid == 1:
            found = found or table
    return found


def cell_size(column: dict[str, Any]) -> int:
    if column.get("array"):
        return 16
    return _SCALAR_SIZE.get(str(column.get("type") or "i32"), 4)


def _ok_off(buf: bytes, pos: int, need: int) -> bool:
    return 0 <= pos <= len(buf) - need


def _read_utf16z(buf: bytes, offset: int) -> str:
    if not _ok_off(buf, offset, 2):
        return ""
    end = offset
    while end + 1 < len(buf):
        if buf[end] == 0 and buf[end + 1] == 0:
            break
        end += 2
        if end - offset > 1_000_000:
            break
    return buf[offset:end].decode("utf-16le", "replace")


def _read_scalar(kind: str, buf: bytes, pos: int, data_offset: int) -> Any:
    if kind == "bool":
        return buf[pos] != 0 if _ok_off(buf, pos, 1) else False
    if kind == "i16":
        return struct.unpack_from("<h", buf, pos)[0] if _ok_off(buf, pos, 2) else 0
    if kind == "u16":
        return struct.unpack_from("<H", buf, pos)[0] if _ok_off(buf, pos, 2) else 0
    if kind in ("i32", "enumrow"):
        return struct.unpack_from("<i", buf, pos)[0] if _ok_off(buf, pos, 4) else 0
    if kind == "u32":
        return struct.unpack_from("<I", buf, pos)[0] if _ok_off(buf, pos, 4) else 0
    if kind == "f32":
        return struct.unpack_from("<f", buf, pos)[0] if _ok_off(buf, pos, 4) else 0.0
    if kind == "string":
        if not _ok_off(buf, pos, 8):
            return ""
        rel = struct.unpack_from("<Q", buf, pos)[0]
        if rel >= NULL_REF or not _ok_off(buf, data_offset + rel, 0):
            return ""
        return _read_utf16z(buf, data_offset + rel)
    if kind in ("foreignrow", "row"):
        if not _ok_off(buf, pos, 8):
            return None
        idx = struct.unpack_from("<q", buf, pos)[0]
        if idx in (-1, NULL_REF) or idx > 10_000_000:
            return None
        return int(idx)
    return None


def parse_datc64(raw: bytes, table_schema: dict[str, Any]) -> list[dict[str, Any]]:
    if len(raw) < 12:
        return []
    row_count = struct.unpack_from("<I", raw, 0)[0]
    marker_at = raw.find(BB_MARKER, 4)
    if marker_at < 0:
        raise ValueError(f"{table_schema.get('name')}: missing 0xBB data marker")
    if row_count <= 0:
        return []
    row_span = marker_at - 4
    if row_span % row_count != 0:
        raise ValueError(
            f"{table_schema.get('name')}: rowSize does not divide ({row_span} / {row_count})"
        )
    row_size = row_span // row_count
    columns = list(table_schema.get("columns") or [])
    fitted: list[dict[str, Any]] = []
    used = 0
    for col in columns:
        width = cell_size(col)
        if used + width > row_size:
            break
        fitted.append(col)
        used += width
    if not fitted:
        raise ValueError(
            f"{table_schema.get('name')}: schema width {sum(cell_size(c) for c in columns)} > rowSize {row_size}"
        )
    columns = fitted
    data_offset = marker_at
    rows: list[dict[str, Any]] = []
    for i in range(row_count):
        base = 4 + i * row_size
        rec: dict[str, Any] = {"_row": i}
        col_off = 0
        unnamed = 0
        for col in columns:
            ctype = str(col.get("type") or "i32")
            cname = col.get("name") or f"col_{unnamed}"
            if not col.get("name"):
                unnamed += 1
            width = cell_size(col)
            cell = base + col_off
            if col.get("array") or ctype == "array":
                if not _ok_off(raw, cell, 16):
                    rec[cname] = []
                    col_off += width
                    continue
                count = struct.unpack_from("<Q", raw, cell)[0]
                rel = struct.unpack_from("<Q", raw, cell + 8)[0]
                elem_kind = "i32" if ctype == "array" else ctype
                elem_size = 16 if elem_kind in ("foreignrow", "row") else _SCALAR_SIZE.get(elem_kind, 4)
                if elem_kind == "string":
                    elem_size = 8
                values = []
                if rel < NULL_REF and _ok_off(raw, data_offset + rel, 0):
                    cap = min(int(count), 4096)
                    for n in range(cap):
                        ep = data_offset + rel + n * elem_size
                        if not _ok_off(raw, ep, 1 if elem_kind == "bool" else min(elem_size, 8)):
                            break
                        values.append(_read_scalar(elem_kind, raw, ep, data_offset))
                rec[cname] = values
            else:
                rec[cname] = _read_scalar(ctype, raw, cell, data_offset)
            col_off += width
        rows.append(rec)
    return rows
