from __future__ import annotations

import struct
from functools import lru_cache
from pathlib import Path
from typing import Any, Iterable

from liverylab_core import kspkg_extract


PACK_KEY = 0x9F9721A97D1135C1
PACK_FILE_TABLE_SIZE = 0x2000000
PACK_ENTRY_SIZE = 0x100
PACK_ENTRY_STRUCT = struct.Struct("<224sIHHQQQ")
PACK_TABLE_TRAILER_OFFSETS = (PACK_FILE_TABLE_SIZE, PACK_FILE_TABLE_SIZE * 2)
ARBITRARY_NAME_MIN_SIZE = 1024 * 1024 * 1024
CONTENT_CARS_PREFIX = "content\\cars"
IGNORED_NON_CONTENT_PACKAGE_NAMES = frozenset({"sdk.kspkg"})


def is_known_non_content_package(path: str | Path) -> bool:
    return Path(path).name.strip().lower() in IGNORED_NON_CONTENT_PACKAGE_NAMES


def _candidate_name_or_size(path: Path, size: int) -> bool:
    if is_known_non_content_package(path):
        return False
    return ".kspkg" in path.name.lower() or size >= ARBITRARY_NAME_MIN_SIZE


def _table_status(table: bytearray, package_size: int) -> dict[str, Any]:
    kspkg_extract.xor_in_place(table, PACK_KEY, 0)
    entry_count = 0
    content_cars_entry_count = 0
    for offset in range(0, len(table), PACK_ENTRY_SIZE):
        chunk = table[offset : offset + PACK_ENTRY_SIZE]
        if len(chunk) < PACK_ENTRY_SIZE:
            break
        file_name_buffer, _unknown, flags, file_name_length, path_hash, file_size, file_offset = PACK_ENTRY_STRUCT.unpack(chunk)
        if path_hash == 0:
            break
        if file_name_length <= 0 or file_name_length > len(file_name_buffer):
            raise ValueError("invalid package entry name length")
        name = file_name_buffer[: int(file_name_length)].decode("ascii", errors="strict")
        if not name or any(ord(char) < 32 for char in name):
            raise ValueError("invalid package entry name")
        is_directory = bool(flags & 0x0001)
        if not is_directory and (file_offset < 0 or file_size < 0 or file_offset + file_size > package_size):
            raise ValueError("invalid package entry offset")
        normalized = name.replace("/", "\\").strip("\\").lower()
        entry_count += 1
        if normalized == CONTENT_CARS_PREFIX or normalized.startswith(CONTENT_CARS_PREFIX + "\\"):
            content_cars_entry_count += 1
    if entry_count <= 0:
        raise ValueError("package table is empty")
    return {
        "entryCount": entry_count,
        "contentCarsEntryCount": content_cars_entry_count,
        "valid": content_cars_entry_count > 0,
    }


@lru_cache(maxsize=32)
def _inspect_cached(path_text: str, size: int, mtime_ns: int) -> dict[str, Any]:
    path = Path(path_text)
    errors: list[str] = []
    if size < PACK_FILE_TABLE_SIZE:
        return {"valid": False, "reason": "file is smaller than an AC EVO package table"}
    with path.open("rb") as stream:
        for trailer_offset in PACK_TABLE_TRAILER_OFFSETS:
            if size < trailer_offset:
                continue
            stream.seek(size - trailer_offset)
            table = bytearray(stream.read(PACK_FILE_TABLE_SIZE))
            if len(table) != PACK_FILE_TABLE_SIZE:
                errors.append(f"size-{trailer_offset:#x}: package table is truncated")
                continue
            try:
                status = _table_status(table, size)
            except (UnicodeDecodeError, ValueError, struct.error) as exc:
                errors.append(f"size-{trailer_offset:#x}: {exc}")
                continue
            if status.get("valid"):
                return {
                    **status,
                    "reason": "valid AC EVO content package",
                    "tableTrailerOffset": trailer_offset,
                }
            errors.append(f"size-{trailer_offset:#x}: package has no content/cars entries")
    return {
        "valid": False,
        "reason": "; ".join(errors) if errors else "no readable package table found",
    }


def inspect_content_package(path: str | Path) -> dict[str, Any]:
    candidate = Path(path).expanduser()
    try:
        stat = candidate.stat()
    except OSError as exc:
        return {
            "path": str(candidate),
            "name": candidate.name,
            "exists": False,
            "valid": False,
            "reason": str(exc),
        }
    if not candidate.is_file():
        return {
            "path": str(candidate),
            "name": candidate.name,
            "exists": True,
            "valid": False,
            "reason": "candidate is not a file",
        }
    try:
        status = _inspect_cached(str(candidate.resolve()), int(stat.st_size), int(stat.st_mtime_ns))
    except OSError as exc:
        status = {"valid": False, "reason": str(exc)}
    return {
        "path": str(candidate.resolve()),
        "name": candidate.name,
        "exists": True,
        "size": int(stat.st_size),
        "mtimeNs": int(stat.st_mtime_ns),
        **status,
    }


def discover_renamed_content_packages(
    root: str | Path,
    *,
    active_name: str = "content.kspkg",
    exclude_paths: Iterable[str | Path] = (),
) -> list[dict[str, Any]]:
    folder = Path(root).expanduser()
    if not folder.exists() or not folder.is_dir():
        return []
    excluded = {
        str(Path(value).expanduser().resolve()).lower()
        for value in exclude_paths
        if str(value).strip()
    }
    rows: list[dict[str, Any]] = []
    for path in sorted(folder.iterdir(), key=lambda item: item.name.lower()):
        if (
            not path.is_file()
            or path.name.lower() == active_name.lower()
            or str(path.resolve()).lower() in excluded
        ):
            continue
        try:
            size = int(path.stat().st_size)
        except OSError:
            continue
        if size < PACK_FILE_TABLE_SIZE or not _candidate_name_or_size(path, size):
            continue
        status = inspect_content_package(path)
        if status.get("valid"):
            rows.append(status)
    return rows
