from __future__ import annotations

import json
import re
import shutil
import tempfile
from datetime import datetime
from pathlib import Path
from typing import Any, Callable

from liverylab_core.game_file_writer import GameFileWriter
from liverylab_core.proto import ProtoField, build_fields, encode_varint, parse_fields, patch_strings_recursive

NUMBERPLATES_DIRNAME = "Numberplates"
NUMBERPLATE_MANIFEST_FILENAME = "manifest.json"
NUMBERPLATE_KIND_DIRS = {
    "background": "backgrounds",
    "font": "fonts",
}


def _norm_slashes(value: str) -> str:
    return value.replace("/", "\\")


def _content_rel_from_disk(root: Path, path: Path) -> str | None:
    for base in [root, root.parent]:
        try:
            rel = path.resolve().relative_to(base.resolve())
            parts = list(rel.parts)
            if parts and parts[0].lower() == "content":
                return _norm_slashes(str(Path(*parts)))
            if base.name.lower() == "content":
                return _norm_slashes(str(Path("content") / rel))
        except Exception:
            pass
    return None


def list_numberplate_templates(root: str | Path | None = None) -> dict[str, list[str]]:
    root_path = Path(root) if root else None
    if root_path is None:
        return {"fonts": [], "backgrounds": []}
    base = root_path / "content" / "cars" / "common_assets" / "numberplates"
    fonts_dir = base / "fonts"
    backgrounds_dir = base / "backgrounds"

    def _collect(folder: Path) -> list[str]:
        if not folder.exists():
            return []
        values: list[str] = []
        for path in sorted(folder.rglob("*.texture")):
            rel = _content_rel_from_disk(root_path, path)
            if rel:
                values.append(_norm_slashes(rel))
        return values

    return {
        "fonts": _collect(fonts_dir),
        "backgrounds": _collect(backgrounds_dir),
    }


def _asset_kind_dir(library_root: Path, kind: str) -> Path:
    return library_root / NUMBERPLATES_DIRNAME / kind


def _clean_asset_id(name: str) -> str:
    clean = re.sub(r"[^A-Za-z0-9_\\-]+", "_", str(name or "").strip()).strip("_-")
    if not clean:
        raise ValueError("Asset name is required")
    return clean


def _resolve_numberplate_template(game_root: Path, template: str | Path, asset_type: str) -> Path:
    folder = NUMBERPLATE_KIND_DIRS.get(asset_type)
    if not folder:
        raise ValueError(f"Unsupported numberplate asset type: {asset_type}")
    base = game_root / "content" / "cars" / "common_assets" / "numberplates" / folder
    template_text = str(template or "").strip()
    if not template_text:
        raise ValueError("Base template is required")
    texture = Path(template_text)
    if not texture.is_absolute():
        texture = game_root / template_text
    texture = texture.expanduser().resolve()
    if texture.suffix.lower() != ".texture":
        raise ValueError("Base template must be a .texture file")
    if not texture.exists():
        raise FileNotFoundError(f"Base template not found: {texture}")
    try:
        texture.relative_to(base.resolve())
    except ValueError as exc:
        raise ValueError(f"Base template must be inside common_assets\\numberplates\\{folder}") from exc
    return texture


def _custom_asset_game_texture(game_root: Path, folder: str, asset_type: str, asset_id: str, manifest: dict[str, Any]) -> Path:
    target_base = game_root / "content" / "cars" / "common_assets" / "numberplates" / folder
    target_dir = (target_base / asset_id).resolve()
    try:
        target_dir.relative_to(target_base.resolve())
    except ValueError as exc:
        raise ValueError("Invalid custom numberplate target path") from exc

    rel = str(manifest.get("gameTextureRel") or "").strip()
    if rel:
        candidate = Path(rel)
        if not candidate.is_absolute():
            candidate = game_root / rel
        candidate = candidate.expanduser().resolve()
        try:
            candidate.relative_to(target_dir)
            if candidate.suffix.lower() == ".texture":
                return candidate
        except ValueError:
            pass

    base_texture = _resolve_numberplate_template(game_root, str(manifest.get("baseTemplate") or ""), asset_type)
    return target_dir / base_texture.name


def _inspect_png(path: Path) -> dict[str, Any]:
    try:
        from PIL import Image
    except Exception:
        return {}
    with Image.open(path) as image:
        return {
            "width": int(image.width),
            "height": int(image.height),
            "mode": str(image.mode),
        }


def _pack_varints(values: list[int]) -> bytes:
    out = bytearray()
    for value in values:
        out.extend(encode_varint(int(value)))
    return bytes(out)


def _mip_tile_layout(width: int, height: int, mips: int, tile_w: int, tile_h: int) -> tuple[list[int], list[int]]:
    offsets: list[int] = []
    counts: list[int] = []
    cursor = 0
    cur_w = max(1, int(width))
    cur_h = max(1, int(height))
    for _mip in range(max(1, int(mips))):
        tiles_x = max(1, (cur_w + int(tile_w) - 1) // int(tile_w))
        tiles_y = max(1, (cur_h + int(tile_h) - 1) // int(tile_h))
        count = tiles_x * tiles_y
        offsets.append(cursor)
        counts.append(count)
        cursor += count
        cur_w = max(1, cur_w // 2)
        cur_h = max(1, cur_h // 2)
    return offsets, counts


def _set_or_append_varint_field(fields: list[ProtoField], number: int, value: int) -> list[ProtoField]:
    changed = False
    out: list[ProtoField] = []
    for field in fields:
        if field.number == number and field.wire == 0:
            out.append(ProtoField(number, 0, int(value)))
            changed = True
        else:
            out.append(field)
    if not changed:
        out.append(ProtoField(number, 0, int(value)))
    return out


def _set_or_append_bytes_field(fields: list[ProtoField], number: int, value: bytes) -> list[ProtoField]:
    changed = False
    out: list[ProtoField] = []
    for field in fields:
        if field.number == number and field.wire == 2:
            out.append(ProtoField(number, 2, bytes(value)))
            changed = True
        else:
            out.append(field)
    if not changed:
        out.append(ProtoField(number, 2, bytes(value)))
    return out


def _patch_texture_descriptor_size(data: bytes, desc: dict[str, Any], width: int, height: int, mips: int) -> bytes:
    fields = parse_fields(data)
    tile_w = int(desc.get("tile_w") or 256)
    tile_h = int(desc.get("tile_h") or 256)
    offsets, counts = _mip_tile_layout(width, height, mips, tile_w, tile_h)
    out: list[ProtoField] = []
    layout_written = False
    for field in fields:
        if field.wire == 0 and field.number == 1:
            out.append(ProtoField(1, 0, int(width)))
        elif field.wire == 0 and field.number == 2:
            out.append(ProtoField(2, 0, int(height)))
        elif field.wire == 0 and field.number == 3:
            out.append(ProtoField(3, 0, int(mips)))
        elif field.wire == 2 and field.number == 12:
            layout_fields = parse_fields(bytes(field.value))
            layout_fields = _set_or_append_varint_field(layout_fields, 1, tile_w)
            layout_fields = _set_or_append_varint_field(layout_fields, 2, tile_h)
            layout_fields = _set_or_append_bytes_field(layout_fields, 4, _pack_varints(offsets))
            layout_fields = _set_or_append_bytes_field(layout_fields, 5, _pack_varints(counts))
            out.append(ProtoField(12, 2, build_fields(layout_fields)))
            layout_written = True
        else:
            out.append(field)
    out = _set_or_append_varint_field(out, 1, int(width))
    out = _set_or_append_varint_field(out, 2, int(height))
    out = _set_or_append_varint_field(out, 3, int(mips))
    if not layout_written:
        out.append(ProtoField(12, 2, build_fields([
            ProtoField(1, 0, tile_w),
            ProtoField(2, 0, tile_h),
            ProtoField(4, 2, _pack_varints(offsets)),
            ProtoField(5, 2, _pack_varints(counts)),
        ])))
    return build_fields(out)


def _full_mip_count(width: int, height: int) -> int:
    levels = 1
    cur_w = max(1, int(width))
    cur_h = max(1, int(height))
    while cur_w > 1 or cur_h > 1:
        cur_w = max(1, cur_w // 2)
        cur_h = max(1, cur_h // 2)
        levels += 1
    return levels


def _normalize_numberplate_source_png(source_png: Path, target_png: Path, asset_type: str) -> dict[str, Any]:
    png_info = _inspect_png(source_png)
    if asset_type == "font":
        if png_info and (png_info.get("width"), png_info.get("height")) != (1024, 512):
            raise ValueError("Numberplate font PNG must be 1024x512 pixels")
        shutil.copy2(source_png, target_png)
        return {
            **png_info,
            "normalized": False,
            "requiredSize": {"width": 1024, "height": 512},
        }
    if asset_type == "background":
        if not png_info:
            raise ValueError("Numberplate background PNG could not be inspected")
        width = int(png_info.get("width") or 0)
        height = int(png_info.get("height") or 0)
        if width <= 0 or height <= 0 or width != height:
            raise ValueError(f"Numberplate background PNG must be square. Selected image is {width}x{height}.")
        try:
            from PIL import Image
        except Exception as exc:
            raise RuntimeError(f"Pillow is required to normalize numberplate background PNGs: {exc}") from exc
        with Image.open(source_png) as image:
            resample = getattr(getattr(Image, "Resampling", Image), "LANCZOS", Image.BICUBIC)
            rgba = image.convert("RGBA")
            if rgba.size != (512, 512):
                rgba = rgba.resize((512, 512), resample)
            rgba.save(target_png, format="PNG")
        return {
            **_inspect_png(target_png),
            "normalized": True,
            "source": png_info,
            "requiredShape": "square",
            "targetSize": {"width": 512, "height": 512},
        }
    raise ValueError(f"Unsupported numberplate asset type: {asset_type}")


def _read_asset_manifest(asset_dir: Path, kind: str) -> dict[str, Any] | None:
    manifest_path = asset_dir / NUMBERPLATE_MANIFEST_FILENAME
    if not manifest_path.exists():
        return None
    try:
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    except Exception:
        return {
            "id": asset_dir.name,
            "name": asset_dir.name,
            "type": kind[:-1] if kind.endswith("s") else kind,
            "manifestPath": str(manifest_path),
            "assetDir": str(asset_dir),
            "status": "invalid",
            "error": "Manifest could not be read.",
        }
    if not isinstance(manifest, dict):
        return None
    asset_id = str(manifest.get("id") or asset_dir.name).strip() or asset_dir.name
    asset_type = str(manifest.get("type") or (kind[:-1] if kind.endswith("s") else kind)).strip()
    return {
        **manifest,
        "id": asset_id,
        "name": str(manifest.get("name") or asset_id),
        "type": asset_type,
        "manifestPath": str(manifest_path),
        "assetDir": str(asset_dir),
        "sourcePngPath": str(asset_dir / str(manifest.get("sourcePng") or "source.png")),
        "status": str(manifest.get("status") or "local"),
    }


def _write_asset_manifest(asset_dir: Path, manifest: dict[str, Any]) -> Path:
    manifest_path = asset_dir / NUMBERPLATE_MANIFEST_FILENAME
    manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
    return manifest_path


def list_custom_numberplate_assets(library_root: str | Path) -> dict[str, Any]:
    root = Path(library_root).expanduser().resolve()
    numberplate_root = root / NUMBERPLATES_DIRNAME
    backgrounds_dir = _asset_kind_dir(root, "backgrounds")
    fonts_dir = _asset_kind_dir(root, "fonts")
    backgrounds_dir.mkdir(parents=True, exist_ok=True)
    fonts_dir.mkdir(parents=True, exist_ok=True)

    def _collect(kind: str) -> list[dict[str, Any]]:
        folder = _asset_kind_dir(root, kind)
        rows: list[dict[str, Any]] = []
        for asset_dir in sorted((path for path in folder.iterdir() if path.is_dir()), key=lambda path: path.name.lower()):
            manifest = _read_asset_manifest(asset_dir, kind)
            if manifest:
                rows.append(manifest)
        return sorted(rows, key=lambda row: str(row.get("name") or row.get("id") or "").lower())

    return {
        "libraryRoot": str(root),
        "numberplateRoot": str(numberplate_root),
        "backgrounds": _collect("backgrounds"),
        "fonts": _collect("fonts"),
    }


def create_custom_numberplate_asset(
    library_root: str | Path,
    game_root: str | Path,
    asset_type: str,
    base_template: str | Path,
    name: str,
    png: str | Path,
    overwrite: bool = False,
) -> dict[str, Any]:
    asset_type = str(asset_type or "").strip().lower()
    folder = NUMBERPLATE_KIND_DIRS.get(asset_type)
    if not folder:
        raise ValueError(f"Unsupported numberplate asset type: {asset_type}")
    root = Path(library_root).expanduser().resolve()
    game_root_path = Path(game_root).expanduser().resolve()
    base_texture = _resolve_numberplate_template(game_root_path, base_template, asset_type)
    source_png = Path(str(png or "").strip())
    if not source_png.is_absolute():
        source_png = game_root_path / source_png
    source_png = source_png.expanduser().resolve()
    if source_png.suffix.lower() != ".png":
        raise ValueError("Source texture must be a PNG file")
    if not source_png.exists():
        raise FileNotFoundError(f"PNG file not found: {source_png}")
    asset_id = _clean_asset_id(name)
    target_dir = (_asset_kind_dir(root, folder) / asset_id).resolve()
    try:
        asset_root = _asset_kind_dir(root, folder).resolve()
        target_dir.relative_to(asset_root)
    except ValueError as exc:
        raise ValueError("Invalid numberplate asset name") from exc
    if target_dir.exists() and not overwrite:
        existing_manifest = _read_asset_manifest(target_dir, folder)
        if existing_manifest and str(existing_manifest.get("status") or "").lower() != "invalid":
            raise FileExistsError(f"Custom numberplate {asset_type} already exists: {asset_id}")
        trash_root = asset_root / "_trash"
        trash_root.mkdir(parents=True, exist_ok=True)
        stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
        shutil.move(str(target_dir), str(trash_root / f"{target_dir.name}_{stamp}"))

    target_dir.mkdir(parents=True, exist_ok=True)
    target_png = target_dir / "source.png"
    png_info = _normalize_numberplate_source_png(source_png, target_png, asset_type)

    base_rel = _content_rel_from_disk(game_root_path, base_texture) or str(base_texture)
    created_at = datetime.now().isoformat(timespec="seconds")
    manifest = {
        "formatVersion": 1,
        "type": asset_type,
        "id": asset_id,
        "name": asset_id,
        "status": "local",
        "active": False,
        "createdAt": created_at,
        "updatedAt": created_at,
        "baseTemplate": _norm_slashes(base_rel),
        "sourcePng": target_png.name,
        "png": png_info,
    }
    manifest_path = _write_asset_manifest(target_dir, manifest)
    return {
        **manifest,
        "assetDir": str(target_dir),
        "manifestPath": str(manifest_path),
        "sourcePngPath": str(target_png),
    }


def replace_custom_numberplate_asset_png(
    library_root: str | Path,
    game_root: str | Path,
    asset_type: str,
    asset_id: str,
    png: str | Path,
) -> dict[str, Any]:
    asset_type = str(asset_type or "").strip().lower()
    folder = NUMBERPLATE_KIND_DIRS.get(asset_type)
    if not folder:
        raise ValueError(f"Unsupported numberplate asset type: {asset_type}")
    clean_id = _clean_asset_id(asset_id)
    root = Path(library_root).expanduser().resolve()
    game_root_path = Path(game_root).expanduser().resolve()
    asset_dir = (_asset_kind_dir(root, folder) / clean_id).resolve()
    manifest = _read_asset_manifest(asset_dir, folder)
    if not manifest or manifest.get("status") == "invalid":
        raise FileNotFoundError(f"Custom numberplate asset not found or invalid: {clean_id}")
    if str(manifest.get("type") or "").strip().lower() != asset_type:
        raise ValueError(f"Custom numberplate asset type mismatch: {manifest.get('type')} != {asset_type}")

    source_png = Path(str(png or "").strip())
    if not source_png.is_absolute():
        source_png = game_root_path / source_png
    source_png = source_png.expanduser().resolve()
    if source_png.suffix.lower() != ".png":
        raise ValueError("Replacement texture must be a PNG file")
    if not source_png.exists():
        raise FileNotFoundError(f"PNG file not found: {source_png}")
    target_png = asset_dir / str(manifest.get("sourcePng") or "source.png")
    target_png.parent.mkdir(parents=True, exist_ok=True)
    backup_path = ""
    if target_png.exists():
        backup_dir = asset_dir / "_source_backups"
        backup_dir.mkdir(parents=True, exist_ok=True)
        stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
        backup = backup_dir / f"{target_png.stem}.{stamp}{target_png.suffix}"
        shutil.copy2(target_png, backup)
        backup_path = str(backup)
    png_info = _normalize_numberplate_source_png(source_png, target_png, asset_type)

    now = datetime.now().isoformat(timespec="seconds")
    manifest.update({
        "png": png_info,
        "updatedAt": now,
        "sourceUpdatedAt": now,
        "needsRefresh": bool(manifest.get("active")),
        "replacement": {
            "source": str(source_png),
            "backup": backup_path,
            "replacedAt": now,
        },
    })
    manifest_path = _write_asset_manifest(asset_dir, manifest)
    return {
        **manifest,
        "assetDir": str(asset_dir),
        "manifestPath": str(manifest_path),
        "sourcePngPath": str(target_png),
    }


def activate_custom_numberplate_asset(
    library_root: str | Path,
    game_root: str | Path,
    asset_type: str,
    asset_id: str,
    *,
    parse_texture_descriptor: Callable[[str | Path], dict[str, Any]],
    choose_auto_format: Callable[[str | Path, dict[str, Any], int], tuple[str, str]],
    convert_image_to_dds_with_texconv: Callable[..., Path],
    texturemips_bytes_from_dds: Callable[[str | Path, str | Path], bytes],
) -> dict[str, Any]:
    asset_type = str(asset_type or "").strip().lower()
    folder = NUMBERPLATE_KIND_DIRS.get(asset_type)
    if not folder:
        raise ValueError(f"Unsupported numberplate asset type: {asset_type}")
    clean_id = _clean_asset_id(asset_id)
    root = Path(library_root).expanduser().resolve()
    game_root_path = Path(game_root).expanduser().resolve()
    asset_dir = (_asset_kind_dir(root, folder) / clean_id).resolve()
    manifest = _read_asset_manifest(asset_dir, folder)
    if not manifest or manifest.get("status") == "invalid":
        raise FileNotFoundError(f"Custom numberplate asset not found or invalid: {clean_id}")
    if str(manifest.get("type") or "").strip().lower() != asset_type:
        raise ValueError(f"Custom numberplate asset type mismatch: {manifest.get('type')} != {asset_type}")

    source_png = asset_dir / str(manifest.get("sourcePng") or "source.png")
    if not source_png.exists():
        raise FileNotFoundError(f"Custom numberplate source PNG not found: {source_png}")
    base_texture = _resolve_numberplate_template(game_root_path, str(manifest.get("baseTemplate") or ""), asset_type)
    base_mips = base_texture.with_suffix(".texturemips")
    if not base_mips.exists():
        raise FileNotFoundError(f"Base template .texturemips not found: {base_mips}")

    target_base = game_root_path / "content" / "cars" / "common_assets" / "numberplates" / folder
    target_texture = _custom_asset_game_texture(game_root_path, folder, asset_type, clean_id, {
        **manifest,
        "baseTemplate": str(manifest.get("baseTemplate") or ""),
    })
    target_mips_path = target_texture.with_suffix(".texturemips")

    base_data = base_texture.read_bytes()
    replacements = {
        base_texture.parent.name: clean_id,
        base_texture.parent.name.lower(): clean_id.lower(),
        base_texture.parent.name.upper(): clean_id.upper(),
    }
    patched_texture = patch_strings_recursive(base_data, replacements)
    desc = parse_texture_descriptor(base_texture)
    format_name, auto_note = choose_auto_format(base_texture, desc, base_mips.stat().st_size)
    target_width = int(desc["width"])
    target_height = int(desc["height"])
    target_mips = int(desc["mips"])
    if asset_type == "background":
        target_width = 512
        target_height = 512
        target_mips = _full_mip_count(target_width, target_height)
        patched_texture = _patch_texture_descriptor_size(patched_texture, desc, target_width, target_height, target_mips)
    with tempfile.TemporaryDirectory(prefix="liverylab_numberplate_") as tmp:
        dds_path = Path(tmp) / f"{clean_id}.dds"
        target_size = (target_width, target_height) if asset_type == "font" else None
        convert_image_to_dds_with_texconv(source_png, dds_path, format_name, None, target_mips, target_size)
        descriptor_texture = Path(tmp) / base_texture.name
        descriptor_texture.write_bytes(patched_texture)
        mips_data = texturemips_bytes_from_dds(descriptor_texture, dds_path)

    writer = GameFileWriter(game_root_path, "activate custom numberplate asset", [target_base])
    texture_write = writer.write_bytes(target_texture, patched_texture, label=f"{asset_type} texture descriptor", backup=True)
    mips_write = writer.write_bytes(target_mips_path, mips_data, label=f"{asset_type} texturemips", backup=True)

    now = datetime.now().isoformat(timespec="seconds")
    game_texture_rel = _content_rel_from_disk(game_root_path, target_texture) or str(target_texture)
    game_mips_rel = _content_rel_from_disk(game_root_path, target_mips_path) or str(target_mips_path)
    manifest.update({
        "status": "active",
        "active": True,
        "updatedAt": now,
        "activatedAt": now,
        "needsRefresh": False,
        "gameTextureRel": _norm_slashes(game_texture_rel),
        "gameMipsRel": _norm_slashes(game_mips_rel),
        "activation": {
            "format": format_name,
            "note": auto_note,
            "targetSize": {"width": target_width, "height": target_height},
            "mips": target_mips,
            "preservedSourceSize": False,
            "texture": texture_write.to_dict(),
            "texturemips": mips_write.to_dict(),
        },
    })
    manifest_path = _write_asset_manifest(asset_dir, manifest)
    return {
        **manifest,
        "assetDir": str(asset_dir),
        "manifestPath": str(manifest_path),
        "sourcePngPath": str(source_png),
        "writer": writer.to_dict(),
    }


def deactivate_custom_numberplate_asset(
    library_root: str | Path,
    game_root: str | Path,
    asset_type: str,
    asset_id: str,
) -> dict[str, Any]:
    asset_type = str(asset_type or "").strip().lower()
    folder = NUMBERPLATE_KIND_DIRS.get(asset_type)
    if not folder:
        raise ValueError(f"Unsupported numberplate asset type: {asset_type}")
    clean_id = _clean_asset_id(asset_id)
    root = Path(library_root).expanduser().resolve()
    game_root_path = Path(game_root).expanduser().resolve()
    asset_dir = (_asset_kind_dir(root, folder) / clean_id).resolve()
    manifest = _read_asset_manifest(asset_dir, folder)
    if not manifest or manifest.get("status") == "invalid":
        raise FileNotFoundError(f"Custom numberplate asset not found or invalid: {clean_id}")
    if str(manifest.get("type") or "").strip().lower() != asset_type:
        raise ValueError(f"Custom numberplate asset type mismatch: {manifest.get('type')} != {asset_type}")

    target_base = game_root_path / "content" / "cars" / "common_assets" / "numberplates" / folder
    target_texture = _custom_asset_game_texture(game_root_path, folder, asset_type, clean_id, manifest)
    target_mips = target_texture.with_suffix(".texturemips")
    writer = GameFileWriter(game_root_path, "deactivate custom numberplate asset", [target_base])
    texture_delete = writer.delete_file(target_texture, label=f"{asset_type} texture descriptor", backup=True)
    mips_delete = writer.delete_file(target_mips, label=f"{asset_type} texturemips", backup=True)

    now = datetime.now().isoformat(timespec="seconds")
    manifest.update({
        "status": "local",
        "active": False,
        "updatedAt": now,
        "deactivatedAt": now,
        "deactivation": {
            "texture": texture_delete.to_dict(),
            "texturemips": mips_delete.to_dict(),
        },
    })
    manifest_path = _write_asset_manifest(asset_dir, manifest)
    source_png = asset_dir / str(manifest.get("sourcePng") or "source.png")
    return {
        **manifest,
        "assetDir": str(asset_dir),
        "manifestPath": str(manifest_path),
        "sourcePngPath": str(source_png),
        "writer": writer.to_dict(),
    }


def create_numberplate_background(root: str | Path, source_background: str | Path, new_name: str, texturemips: str | Path | None = None, overwrite: bool = False) -> dict[str, Any]:
    root_path = Path(root) if root else None
    if root_path is None:
        raise ValueError("Root path is required")
    root_path = root_path.expanduser().resolve()
    base = root_path / "content" / "cars" / "common_assets" / "numberplates" / "backgrounds"
    if not base.exists():
        raise FileNotFoundError(f"Numberplate backgrounds folder not found: {base}")
    writer = GameFileWriter(root_path, "create numberplate background", [base])

    source_text = str(source_background or "").strip()
    if not source_text:
        raise ValueError("Source background is required")
    source_texture = Path(source_text)
    if not source_texture.is_absolute():
        source_texture = root_path / source_text
    source_texture = source_texture.expanduser().resolve()
    if source_texture.suffix.lower() != ".texture":
        raise ValueError("Source background must be a .texture file")
    if not source_texture.exists():
        raise FileNotFoundError(f"Source background not found: {source_texture}")
    try:
        source_texture.relative_to(base.resolve())
    except ValueError as exc:
        raise ValueError("Source background must be inside common_assets\\numberplates\\backgrounds") from exc

    clean_name = re.sub(r"[^A-Za-z0-9_\\-]+", "_", str(new_name or "").strip()).strip("_-")
    if not clean_name:
        raise ValueError("New background name is required")
    target_dir = (base / clean_name).resolve()
    try:
        target_dir.relative_to(base.resolve())
    except ValueError as exc:
        raise ValueError("Invalid background name") from exc
    if target_dir.exists() and not overwrite:
        raise FileExistsError(f"Numberplate background already exists: {target_dir}")

    copied: list[str] = []
    target_texture = target_dir / source_texture.name
    data = source_texture.read_bytes()
    old_group = source_texture.parent.name
    replacements = {
        old_group: clean_name,
        old_group.lower(): clean_name.lower(),
        old_group.upper(): clean_name.upper(),
    }
    patched = patch_strings_recursive(data, replacements)
    writer.write_bytes(target_texture, patched, label="numberplate background texture")
    copied.append(str(target_texture))

    source_mips = source_texture.with_suffix(".texturemips")
    target_mips = target_texture.with_suffix(".texturemips")
    custom_mips_text = str(texturemips or "").strip()
    if custom_mips_text:
        custom_mips = Path(custom_mips_text)
        if not custom_mips.is_absolute():
            custom_mips = root_path / custom_mips
        custom_mips = custom_mips.expanduser().resolve()
        if custom_mips.suffix.lower() != ".texturemips":
            raise ValueError("Custom mips file must be a .texturemips file")
        if not custom_mips.exists():
            raise FileNotFoundError(f"Custom mips file not found: {custom_mips}")
        writer.copy_file(custom_mips, target_mips, label="numberplate background mips")
        copied.append(str(target_mips))
    elif source_mips.exists():
        writer.copy_file(source_mips, target_mips, label="numberplate background mips")
        copied.append(str(target_mips))

    source_dds = source_texture.with_suffix(".dds")
    if source_dds.exists():
        writer.copy_file(source_dds, target_texture.with_suffix(".dds"), label="numberplate background dds")
        copied.append(str(target_texture.with_suffix(".dds")))

    rel = _content_rel_from_disk(root_path, target_texture)
    return {
        "name": clean_name,
        "folder": str(target_dir),
        "texture": str(target_texture),
        "textureRel": _norm_slashes(rel or ""),
        "mips": str(target_mips) if target_mips.exists() else "",
        "copied": copied,
        "patchedTextureDescriptor": patched != data,
        "writer": writer.to_dict(),
    }
