from __future__ import annotations

import hashlib
import json
import re
import shutil
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from liverylab_core import online_league_activation, online_league_deployments, online_league_payload, package_security, package_zip, workflow_scopes


PROJECTS_DIRNAME = "LeagueProjects"
LIBRARY_DIRNAME = "LeagueLibrary"
PACKAGE_EXPORT_DIRNAME = "LeagueExport"
PROJECT_MANIFEST = "league_pack.json"
PROJECT_THUMBNAIL = "league_thumbnail.png"
EXPORT_MANIFEST = "league_manifest.json"
ENTRY_MANIFEST = "entry_manifest.json"
PACK_TYPE = "onlineLeaguePack"
EXPORT_TYPE = "liverylabOnlineLeaguePack"
PROJECT_FORMAT_VERSION = 2
EXPORT_FORMAT_VERSION = 2
MAX_IMPORT_ZIP_FILE_COUNT = 100000
MAX_IMPORT_ZIP_TOTAL_UNCOMPRESSED_BYTES = 12 * 1024 * 1024 * 1024
LEGACY_STORAGE_DIRNAMES = {
    "projects": "OnlineLeagueProjects",
    "library": "OnlineLeagueLibrary",
    "package_export": "OnlineLeaguePackageExport",
}


@dataclass(frozen=True)
class OnlineLeagueRoots:
    projects: Path
    library: Path
    package_export: Path
    temp: Path
    official_projects: Path | None = None


def _now_iso() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def _json_ready(value: Any) -> Any:
    if isinstance(value, Path):
        return str(value)
    if isinstance(value, dict):
        return {str(key): _json_ready(item) for key, item in value.items()}
    if isinstance(value, (list, tuple, set)):
        return [_json_ready(item) for item in value]
    return value


def _read_json(path: Path, fallback: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return fallback


def _write_json(path: Path, data: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(_json_ready(data), indent=2, ensure_ascii=False), encoding="utf-8")


def _validate_square_png(source: str | Path, label: str = "League pack thumbnail") -> Path:
    path = Path(source).expanduser()
    if path.suffix.lower() != ".png":
        raise ValueError(f"{label} must be a PNG file")
    if not path.is_file():
        raise FileNotFoundError(f"{label} PNG not found: {path}")
    try:
        from PIL import Image
    except Exception as exc:
        raise RuntimeError(f"Pillow is required for thumbnail validation: {exc}") from exc
    with Image.open(path) as image:
        width, height = image.size
    if width != height:
        raise ValueError(f"{label} PNG must be square. Selected image is {width}x{height}.")
    return path


def _official_thumbnail_index(roots: OnlineLeagueRoots) -> dict[str, Path]:
    official_root = Path(roots.official_projects).expanduser() if roots.official_projects else None
    if official_root is None or not official_root.is_dir():
        return {}
    resolved_root = official_root.resolve()
    matches: dict[str, list[Path]] = {}
    for external_json in sorted(official_root.glob("*/*/external_livery.json"), key=lambda path: path.as_posix().lower()):
        if external_json.is_symlink():
            continue
        external = _read_json(external_json, {})
        car_guid = str(external.get("car_guid") or "").strip().lower() if isinstance(external, dict) else ""
        if not online_league_payload.CAR_GUID_PATTERN.fullmatch(car_guid):
            continue
        thumbnail = external_json.parent / "livery_thumbnail.png"
        if not thumbnail.is_file() or thumbnail.is_symlink():
            continue
        try:
            thumbnail.resolve().relative_to(resolved_root)
            _validate_square_png(thumbnail, "Official livery thumbnail")
        except Exception:
            continue
        matches.setdefault(car_guid, []).append(thumbnail)
    return {car_guid: paths[0] for car_guid, paths in matches.items() if len(paths) == 1}


def _slugify(value: Any, fallback: str = "league_pack") -> str:
    text = str(value or "").strip().lower()
    text = text.replace("\u00e4", "ae").replace("\u00f6", "oe").replace("\u00fc", "ue").replace("\u00df", "ss")
    text = re.sub(r"[^a-z0-9_-]+", "_", text)
    text = re.sub(r"_+", "_", text).strip("_-")
    return text[:64] or fallback


def _short_hash(*parts: Any) -> str:
    digest = hashlib.sha1("|".join(str(part) for part in parts).encode("utf-8", errors="ignore")).hexdigest()
    return digest[:8]


def normalize_scope(value: Any, default: str = workflow_scopes.SCOPE_OFFICIAL) -> str:
    scope = workflow_scopes.normalize_scope(value, default=default)
    if scope in {workflow_scopes.SCOPE_ALL, workflow_scopes.SCOPE_OFFICIAL}:
        return workflow_scopes.SCOPE_OFFICIAL
    raise ValueError("Online League is available only in the Official workflow.")


def _safe_id(value: Any, label: str = "id") -> str:
    text = str(value or "").strip()
    if not text or Path(text).is_absolute() or ".." in Path(text).parts or "\\" in text or "/" in text:
        raise ValueError(f"Invalid Online League {label}: {value}")
    return _slugify(text, "item")


def _project_root(roots: OnlineLeagueRoots, scope: Any, project_id: str) -> Path:
    normalize_scope(scope)
    return roots.projects / _safe_id(project_id, "project id")


def _installed_root(roots: OnlineLeagueRoots, scope: Any, pack_id: str) -> Path:
    normalize_scope(scope)
    return roots.library / _safe_id(pack_id, "installed pack id")


def _move_or_merge(source: Path, target: Path) -> None:
    if not source.exists():
        return
    if not target.exists():
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.move(str(source), str(target))
        return
    if source.is_file() or target.is_file():
        raise RuntimeError(f"Online League storage migration conflict: {source} -> {target}")
    for child in sorted(source.iterdir(), key=lambda path: path.name.lower()):
        _move_or_merge(child, target / child.name)
    try:
        source.rmdir()
    except OSError:
        pass


def _scrub_legacy_project_paths(projects_root: Path) -> None:
    for manifest in projects_root.glob(f"*/{PROJECT_MANIFEST}"):
        raw = _read_json(manifest, {})
        if not isinstance(raw, dict):
            continue
        changed = False
        for key in ("projectRoot", "manifestPath", "thumbnailPath", "previewPngPath"):
            if key in raw:
                raw.pop(key, None)
                changed = True
        if changed:
            _write_json(manifest, raw)


def _migrate_storage_layout(roots: OnlineLeagueRoots) -> None:
    mappings = (
        (roots.projects, LEGACY_STORAGE_DIRNAMES["projects"]),
        (roots.library, LEGACY_STORAGE_DIRNAMES["library"]),
        (roots.package_export, LEGACY_STORAGE_DIRNAMES["package_export"]),
    )
    for target, legacy_name in mappings:
        legacy = target.parent / legacy_name
        if legacy.resolve(strict=False) != target.resolve(strict=False):
            _move_or_merge(legacy, target)
        target.mkdir(parents=True, exist_ok=True)
        _move_or_merge(target / "Official", target)
        advanced = target / "Advanced"
        if advanced.exists():
            try:
                advanced.rmdir()
            except OSError:
                pass
    _scrub_legacy_project_paths(roots.projects)


def _ensure_roots(roots: OnlineLeagueRoots) -> None:
    _migrate_storage_layout(roots)
    roots.temp.mkdir(parents=True, exist_ok=True)


def roots_info(roots: OnlineLeagueRoots, create: bool = False) -> dict[str, Any]:
    if create:
        _ensure_roots(roots)
    return {
        "onlineLeagueProjectsRoot": str(roots.projects),
        "onlineLeagueLibraryRoot": str(roots.library),
        "onlineLeaguePackageExportRoot": str(roots.package_export),
        "onlineLeagueTempRoot": str(roots.temp),
        "officialLiveryProjectsRoot": str(roots.official_projects or ""),
        "officialProjectsRoot": str(roots.projects),
        "officialLibraryRoot": str(roots.library),
        "officialPackageExportRoot": str(roots.package_export),
    }


def _status_summary(entries: list[dict[str, Any]]) -> dict[str, int]:
    summary = {"total": len(entries), "ready": 0, "warning": 0, "error": 0}
    for entry in entries:
        status = str(entry.get("status") or "warning").strip().lower()
        if status in {"ready", "warning", "error"}:
            summary[status] += 1
    return summary


def _coerce_entry(entry: dict[str, Any]) -> dict[str, Any]:
    data = dict(entry)
    for key in ("id", "displayName", "number", "driver", "team", "car", "notes", "carGuid", "externalFolderName"):
        data[key] = str(data.get(key) or "").strip()
    data["class"] = str(data.get("class") or data.get("carClass") or "").strip()
    data["status"] = str(data.get("status") or "warning").strip().lower()
    if data["status"] not in {"ready", "warning", "error"}:
        data["status"] = "warning"
    data["warnings"] = [str(item) for item in data.get("warnings") or [] if str(item).strip()]
    data["errors"] = [str(item) for item in data.get("errors") or [] if str(item).strip()]
    return data


def _coerce_project(data: Any, project_root: Path | None = None, fallback_scope: str = workflow_scopes.SCOPE_OFFICIAL) -> dict[str, Any]:
    raw = dict(data) if isinstance(data, dict) else {}
    scope = normalize_scope(raw.get("workflowScope") or raw.get("workflow_scope") or fallback_scope)
    entries = [_coerce_entry(row) for row in raw.get("entries") or [] if isinstance(row, dict)]
    project = {
        "formatVersion": PROJECT_FORMAT_VERSION,
        "type": PACK_TYPE,
        "id": str(raw.get("id") or (project_root.name if project_root else "")).strip(),
        "workflowScope": scope,
        "name": str(raw.get("name") or "Untitled League Pack").strip(),
        "packVersion": str(raw.get("packVersion") or raw.get("pack_version") or "1.0.0").strip(),
        "notes": str(raw.get("notes") or "").strip(),
        "createdAt": str(raw.get("createdAt") or _now_iso()),
        "updatedAt": str(raw.get("updatedAt") or _now_iso()),
        "entries": entries,
    }
    project["statusSummary"] = _status_summary(entries)
    if project_root is not None:
        project["projectRoot"] = str(project_root)
        project["manifestPath"] = str(project_root / PROJECT_MANIFEST)
        thumbnail_name = Path(str(raw.get("thumbnailPng") or raw.get("previewPng") or "")).name.strip()
        candidates = []
        if thumbnail_name:
            candidates.append(project_root / thumbnail_name)
        candidates.append(project_root / PROJECT_THUMBNAIL)
        thumbnail = next((path for path in candidates if path.is_file()), None)
        if thumbnail:
            project["thumbnailPng"] = thumbnail.name
            project["thumbnailPath"] = str(thumbnail)
            project["previewPng"] = thumbnail.name
            project["previewPngPath"] = str(thumbnail)
        else:
            project["thumbnailPng"] = thumbnail_name
            project["thumbnailPath"] = ""
            project["previewPng"] = thumbnail_name
            project["previewPngPath"] = ""
    return project


def _load_project_path(path: Path, fallback_scope: str | None = None) -> dict[str, Any] | None:
    manifest = path / PROJECT_MANIFEST
    if not manifest.exists():
        return None
    scope = fallback_scope or workflow_scopes.SCOPE_OFFICIAL
    project = _coerce_project(_read_json(manifest, {}), path, scope)
    return project if project.get("type") == PACK_TYPE else None


def save_project(roots: OnlineLeagueRoots, project: dict[str, Any]) -> dict[str, Any]:
    scope = normalize_scope(project.get("workflowScope"))
    project_id = _safe_id(project.get("id"), "project id")
    root = _project_root(roots, scope, project_id)
    root.mkdir(parents=True, exist_ok=True)
    data = _coerce_project({**project, "id": project_id, "workflowScope": scope}, root, scope)
    data["updatedAt"] = _now_iso()
    stored = dict(data)
    for key in ("projectRoot", "manifestPath", "thumbnailPath", "previewPngPath"):
        stored.pop(key, None)
    _write_json(root / PROJECT_MANIFEST, stored)
    return _coerce_project(stored, root, scope)


def create_project(roots: OnlineLeagueRoots, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    _ensure_roots(roots)
    data = payload if isinstance(payload, dict) else {}
    scope = normalize_scope(data.get("workflowScope") or data.get("scope"))
    name = str(data.get("name") or "New League Pack").strip()
    base_id = _slugify(data.get("id") or name, "league_pack")
    scope_root = roots.projects
    candidate = base_id
    index = 1
    while (scope_root / candidate / PROJECT_MANIFEST).exists():
        index += 1
        candidate = f"{base_id}_{index}"
    return save_project(roots, {
        "id": candidate,
        "workflowScope": scope,
        "name": name,
        "packVersion": str(data.get("packVersion") or "1.0.0").strip(),
        "notes": str(data.get("notes") or "").strip(),
        "createdAt": _now_iso(),
        "updatedAt": _now_iso(),
        "entries": [],
    })


def list_projects(roots: OnlineLeagueRoots, scope: Any = workflow_scopes.SCOPE_ALL) -> dict[str, Any]:
    _ensure_roots(roots)
    normalize_scope(scope)
    projects: list[dict[str, Any]] = []
    for folder in sorted(roots.projects.iterdir(), key=lambda path: path.name.lower()) if roots.projects.exists() else []:
        if folder.is_dir() and folder.name.lower() != "advanced":
            project = _load_project_path(folder, workflow_scopes.SCOPE_OFFICIAL)
            if project:
                projects.append(project)
    return {"projects": projects, "projectCount": len(projects), "roots": roots_info(roots)}


def load_project(roots: OnlineLeagueRoots, scope: Any, project_id: str) -> dict[str, Any]:
    project = _load_project_path(_project_root(roots, scope, project_id), normalize_scope(scope))
    if not project:
        raise FileNotFoundError(f"Online League project not found: {project_id}")
    return project


def update_project(roots: OnlineLeagueRoots, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    data = payload if isinstance(payload, dict) else {}
    scope = normalize_scope(data.get("workflowScope") or data.get("scope"))
    project = load_project(roots, scope, str(data.get("id") or data.get("projectId") or ""))
    for source_key, target_key in (("name", "name"), ("packVersion", "packVersion"), ("notes", "notes")):
        if source_key in data:
            project[target_key] = str(data.get(source_key) or "").strip()
    return save_project(roots, project)


def update_project_thumbnail(roots: OnlineLeagueRoots, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    data = payload if isinstance(payload, dict) else {}
    scope = normalize_scope(data.get("workflowScope") or data.get("scope"))
    project = load_project(roots, scope, str(data.get("id") or data.get("projectId") or ""))
    source = _validate_square_png(data.get("thumbnailPng") or data.get("previewPng") or data.get("path") or "", "League pack thumbnail")
    project_root = _project_root(roots, project.get("workflowScope"), str(project.get("id") or ""))
    target = project_root / PROJECT_THUMBNAIL
    target.parent.mkdir(parents=True, exist_ok=True)
    if source.resolve() != target.resolve():
        shutil.copy2(source, target)
    project["thumbnailPng"] = target.name
    project["previewPng"] = target.name
    return save_project(roots, project)


def _entry_root(project_root: Path, entry_id: str) -> Path:
    return project_root / "entries" / _safe_id(entry_id, "entry id")


def _backfill_entry_thumbnail(
    roots: OnlineLeagueRoots,
    project_root: Path,
    entry: dict[str, Any],
    official_thumbnails: dict[str, Path] | None = None,
) -> dict[str, Any]:
    data = dict(entry)
    entry_id = str(data.get("id") or "").strip()
    if not entry_id:
        return data
    preview_root = _entry_root(project_root, entry_id) / "preview"
    existing = next((path for path in sorted(preview_root.glob("thumbnail.*")) if path.is_file()), None)
    if existing is not None:
        data["thumbnailPath"] = str(existing)
        return data
    car_guid = str(data.get("carGuid") or "").strip().lower()
    thumbnail_index = official_thumbnails if official_thumbnails is not None else _official_thumbnail_index(roots)
    source = thumbnail_index.get(car_guid)
    if source is None:
        data["thumbnailPath"] = ""
        return data
    preview_root.mkdir(parents=True, exist_ok=True)
    target = preview_root / "thumbnail.png"
    package_zip.copy_file(source, target)
    data["thumbnailPath"] = str(target)
    return data


def _iter_files(root: Path):
    if root.is_file():
        yield root
        return
    for path in sorted(root.rglob("*"), key=lambda item: item.as_posix().lower()):
        if path.is_file():
            yield path


def _copy_tree(source: Path, target: Path) -> int:
    if target.exists():
        shutil.rmtree(target)
    target.mkdir(parents=True, exist_ok=True)
    copied = 0
    for path in _iter_files(source):
        if path.is_symlink():
            raise ValueError(f"Unsupported symlink in Online League import: {path}")
        suffix = path.suffix.lower()
        if suffix in package_security.DANGEROUS_EXTENSIONS:
            raise ValueError(f"Blocked dangerous file type in Online League import: {path.name}")
        if suffix in package_security.NESTED_ARCHIVE_EXTENSIONS:
            raise ValueError(f"Nested archives are not allowed in Online League import: {path.name}")
        if suffix and suffix not in package_security.ALLOWED_EXTENSIONS:
            raise ValueError(f"Unsupported file type in Online League import: {path.name}")
        package_zip.copy_file(path, target / path.relative_to(source))
        copied += 1
    return copied


def _path_size(path: Path) -> int:
    total = 0
    for file_path in _iter_files(path):
        try:
            total += file_path.stat().st_size
        except OSError:
            pass
    return total


def _find_first_json(source_root: Path, names: tuple[str, ...]) -> tuple[Path | None, dict[str, Any]]:
    wanted = {name.lower() for name in names}
    for path in _iter_files(source_root):
        if path.name.lower() in wanted:
            data = _read_json(path, {})
            if isinstance(data, dict):
                return path, data
    return None, {}


def _deep_find(data: Any, keys: tuple[str, ...]) -> str:
    wanted = {key.lower() for key in keys}
    if isinstance(data, dict):
        for key, value in data.items():
            if str(key).lower() in wanted and value not in (None, ""):
                return str(value).strip()
        for value in data.values():
            found = _deep_find(value, keys)
            if found:
                return found
    elif isinstance(data, list):
        for value in data:
            found = _deep_find(value, keys)
            if found:
                return found
    return ""


def _car_metadata_value(data: Any) -> str:
    raw = _deep_find(data, ("carModel", "car_model", "car", "vehicle", "vehicleId", "carContentPath", "car_content_path", "contentPath", "carData", "actorPath"))
    text = str(raw or "").strip().replace("/", "\\")
    if not text:
        return ""
    marker = "content\\cars\\"
    lower = text.lower()
    if marker in lower:
        tail = text[lower.index(marker) + len(marker):]
        return tail.split("\\", 1)[0].strip()
    return Path(text).stem if text.lower().endswith((".actor", ".cardesign", ".design")) else text


def _rel(source_root: Path, path: Path | None) -> str:
    if path is None:
        return ""
    try:
        return str(path.relative_to(source_root)).replace("/", "\\")
    except Exception:
        return str(path)


def _inspect_entry_source(source_root: Path, project_scope: str) -> dict[str, Any]:
    normalize_scope(project_scope)
    skin_path, skin_data = _find_first_json(source_root, ("liverylab_skin.json",))
    manifest_path, manifest_data = _find_first_json(source_root, ("manifest.json",))
    payload_root = online_league_payload.find_payload_root(source_root)
    payload = online_league_payload.inspect_payload(payload_root)
    external_path = Path(str(payload.get("externalLiveryJson") or ""))
    external_data = payload.get("externalLivery") if isinstance(payload.get("externalLivery"), dict) else {}
    merged = {"skin": skin_data, "external": external_data, "manifest": manifest_data}
    files = list(_iter_files(source_root))
    return {
        "fileCount": len(files),
        "bytes": _path_size(source_root),
        "detectedScope": workflow_scopes.SCOPE_OFFICIAL,
        "externalFolderName": str(payload.get("folderName") or payload_root.name),
        "carGuid": str(payload.get("carGuid") or ""),
        "detectedFiles": {
            "liverylabSkin": _rel(source_root, skin_path),
            "externalLivery": _rel(source_root, external_path),
            "manifest": _rel(source_root, manifest_path),
            "thumbnail": "",
        },
        "thumbnailPath": "",
        "payload": payload,
        "errors": list(payload.get("errors") or []),
        "warnings": list(payload.get("warnings") or []),
        "metadata": {
            "number": _deep_find(merged, ("startNumber", "start_number", "number", "raceNumber", "racingNumber")),
            "driver": _deep_find(merged, ("driver", "driverName", "author")),
            "team": _deep_find(merged, ("team", "teamName")),
            "car": _car_metadata_value(merged),
            "class": _deep_find(merged, ("class", "carClass", "category")),
            "displayName": _deep_find(merged, ("displayName", "display_name", "name", "skinId", "skin_id")),
        },
        "projectScope": project_scope,
    }


def _entry_preflight(entry: dict[str, Any], project_scope: str, project_root: Path) -> dict[str, Any]:
    normalize_scope(project_scope)
    data = dict(entry)
    source_root = _entry_root(project_root, str(data.get("id") or "")) / "source"
    warnings: list[str] = []
    errors: list[str] = []
    source_validation = data.get("sourceValidation") if isinstance(data.get("sourceValidation"), dict) else {}
    if not online_league_payload.is_verified_official_source(source_validation):
        errors.append(
            str(source_validation.get("error") or "This entry is not verified as an Official livery source. Re-add it from an Official package or direct ExternalLiveries folder.")
        )
    if not source_root.exists() or not source_root.is_dir():
        errors.append("Imported source folder is missing.")
        inspect = {"fileCount": 0, "bytes": 0, "detectedFiles": {}, "detectedScope": "", "thumbnailPath": "", "metadata": {}}
    else:
        try:
            inspect = _inspect_entry_source(source_root, project_scope)
            online_league_payload.assert_pack_payload_safe(source_root)
        except Exception as exc:
            inspect = {"fileCount": 0, "bytes": 0, "detectedFiles": {}, "detectedScope": workflow_scopes.SCOPE_OFFICIAL, "thumbnailPath": "", "metadata": {}}
            errors.append(str(exc))
        errors.extend(str(item) for item in inspect.get("errors") or [] if str(item).strip())
        warnings.extend(str(item) for item in inspect.get("warnings") or [] if str(item).strip())
        if int(inspect.get("fileCount") or 0) <= 0 and not errors:
            errors.append("Imported ExternalLiveries payload has no files.")
        if not str(inspect.get("carGuid") or "").strip() and not errors:
            errors.append("ExternalLiveries payload has no valid car_guid.")
    metadata = inspect.get("metadata") if isinstance(inspect.get("metadata"), dict) else {}
    for field, label in (("number", "Start number"), ("driver", "Driver name"), ("team", "Team name"), ("car", "Car")):
        if not str(data.get(field) or "").strip():
            value = str(metadata.get(field) or "").strip()
            if value:
                data[field] = value
            else:
                warnings.append(f"{label} is missing.")
    if not str(data.get("class") or "").strip():
        data["class"] = str(metadata.get("class") or "").strip()
    if not str(data.get("displayName") or "").strip():
        data["displayName"] = str(metadata.get("displayName") or "").strip()
    data["warnings"] = warnings
    data["errors"] = errors
    data["status"] = "error" if errors else ("warning" if warnings else "ready")
    data["inspection"] = inspect
    preview_root = _entry_root(project_root, str(data.get("id") or "")) / "preview"
    thumbnail = next((path for path in sorted(preview_root.glob("thumbnail.*")) if path.is_file()), None)
    data["thumbnailPath"] = str(thumbnail or "")
    data["carGuid"] = str(inspect.get("carGuid") or data.get("carGuid") or "").strip().lower()
    data["externalFolderName"] = str(data.get("externalFolderName") or inspect.get("externalFolderName") or "").strip()
    data["sourceRoot"] = str(source_root)
    return _coerce_entry(data)


def _ensure_entry_source_validation(roots: OnlineLeagueRoots, entry: dict[str, Any]) -> dict[str, Any]:
    data = dict(entry)
    current = data.get("sourceValidation") if isinstance(data.get("sourceValidation"), dict) else {}
    if online_league_payload.is_verified_official_source(current):
        return data
    source = Path(str(data.get("sourcePath") or "")).expanduser()
    source_kind = str(data.get("sourceKind") or "").strip().lower()
    if not source.exists():
        data["sourceValidation"] = {
            "formatVersion": online_league_payload.SOURCE_VALIDATION_VERSION,
            "verified": False,
            "sourceType": source_kind,
            "projectType": "",
            "error": "This entry has no verified Official source and its original import source is no longer available. Re-add the livery.",
        }
        return data
    try:
        if source_kind == "zip" or source.suffix.lower() == ".zip":
            with package_zip.package_root(
                source,
                roots.temp,
                allow_folder=False,
                required_manifest=None,
                max_file_count=MAX_IMPORT_ZIP_FILE_COUNT,
                max_total_uncompressed_bytes=MAX_IMPORT_ZIP_TOTAL_UNCOMPRESSED_BYTES,
            ) as package_root:
                validation = online_league_payload.validate_official_source(package_root, "zip")
        elif source_kind == "folder" or source.is_dir():
            validation = online_league_payload.validate_official_source(source, "folder")
        else:
            raise ValueError("The original source is not an Official livery ZIP or direct ExternalLiveries folder.")
        data["sourceValidation"] = validation
    except Exception as exc:
        data["sourceValidation"] = {
            "formatVersion": online_league_payload.SOURCE_VALIDATION_VERSION,
            "verified": False,
            "sourceType": source_kind,
            "projectType": "",
            "error": str(exc),
        }
    return data


def run_preflight(roots: OnlineLeagueRoots, scope: Any, project_id: str) -> dict[str, Any]:
    project = load_project(roots, scope, project_id)
    project_scope = normalize_scope(project.get("workflowScope"))
    project_root = _project_root(roots, project_scope, str(project.get("id") or ""))
    official_thumbnails = _official_thumbnail_index(roots)
    entries = []
    for entry in project.get("entries") or []:
        validated = _ensure_entry_source_validation(roots, entry)
        with_thumbnail = _backfill_entry_thumbnail(roots, project_root, validated, official_thumbnails)
        entries.append(_entry_preflight(with_thumbnail, project_scope, project_root))
    numbers: dict[str, int] = {}
    car_guids: dict[str, int] = {}
    folder_names: dict[str, int] = {}
    for entry in entries:
        number = str(entry.get("number") or "").strip().lower()
        if number:
            numbers[number] = numbers.get(number, 0) + 1
        car_guid = str(entry.get("carGuid") or "").strip().lower()
        if car_guid:
            car_guids[car_guid] = car_guids.get(car_guid, 0) + 1
        folder_name = str(entry.get("externalFolderName") or "").strip().lower()
        if folder_name:
            folder_names[folder_name] = folder_names.get(folder_name, 0) + 1
    for entry in entries:
        number = str(entry.get("number") or "").strip().lower()
        if number and numbers.get(number, 0) > 1:
            warnings = list(entry.get("warnings") or [])
            warnings.append(f"Duplicate start number in this league pack: {entry.get('number')}.")
            entry["warnings"] = warnings
            if entry.get("status") == "ready":
                entry["status"] = "warning"
        duplicate_errors = list(entry.get("errors") or [])
        car_guid = str(entry.get("carGuid") or "").strip().lower()
        folder_name = str(entry.get("externalFolderName") or "").strip().lower()
        if car_guid and car_guids.get(car_guid, 0) > 1:
            duplicate_errors.append(f"Duplicate car_guid in this league pack: {entry.get('carGuid')}.")
        if folder_name and folder_names.get(folder_name, 0) > 1:
            duplicate_errors.append(f"Duplicate ExternalLiveries folder name in this league pack: {entry.get('externalFolderName')}.")
        if duplicate_errors:
            entry["errors"] = duplicate_errors
            entry["status"] = "error"
    project["entries"] = entries
    return save_project(roots, project)


def _prepare_entry_source(
    roots: OnlineLeagueRoots,
    source: Path,
    project_scope: str,
    entry_root: Path,
) -> tuple[str, dict[str, Any], dict[str, Any], Path | None, dict[str, Any]]:
    source_target = entry_root / "source"
    preview_target = entry_root / "preview"

    def prepare(source_container: Path, source_kind: str) -> tuple[dict[str, Any], dict[str, Any], Path | None, dict[str, Any]]:
        validation = online_league_payload.validate_official_source(source_container, source_kind)
        raw = _inspect_entry_source(source_container, project_scope)
        clean = online_league_payload.sanitize_payload(source_container, source_target)
        preview_source = Path(str(clean.get("previewSource") or ""))
        if not preview_source.is_file():
            preview_source = _official_thumbnail_index(roots).get(str(clean.get("carGuid") or "").strip().lower(), Path())
        thumbnail = None
        if preview_source.is_file():
            preview_target.mkdir(parents=True, exist_ok=True)
            thumbnail = preview_target / f"thumbnail{preview_source.suffix.lower()}"
            package_zip.copy_file(preview_source, thumbnail)
        return raw, clean, thumbnail, validation

    try:
        if source.suffix.lower() == ".zip":
            with package_zip.package_root(
                source,
                roots.temp,
                allow_folder=False,
                required_manifest=None,
                max_file_count=MAX_IMPORT_ZIP_FILE_COUNT,
                max_total_uncompressed_bytes=MAX_IMPORT_ZIP_TOTAL_UNCOMPRESSED_BYTES,
            ) as package_root:
                raw, clean, thumbnail, validation = prepare(package_root, "zip")
            return "zip", raw, clean, thumbnail, validation
        if source.is_dir():
            raw, clean, thumbnail, validation = prepare(source, "folder")
            return "folder", raw, clean, thumbnail, validation
        raise ValueError("Online League import source must be a ZIP package or folder.")
    except Exception:
        shutil.rmtree(entry_root, ignore_errors=True)
        raise


def import_entry(roots: OnlineLeagueRoots, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    _ensure_roots(roots)
    data = payload if isinstance(payload, dict) else {}
    scope = normalize_scope(data.get("workflowScope") or data.get("scope"))
    project = load_project(roots, scope, str(data.get("projectId") or data.get("id") or ""))
    source = Path(str(data.get("sourcePath") or data.get("path") or "")).expanduser()
    if not source.exists():
        raise FileNotFoundError(f"Online League import source not found: {source}")
    project_scope = normalize_scope(project.get("workflowScope"))
    project_root = _project_root(roots, project_scope, str(project.get("id") or ""))
    existing_ids = {str(row.get("id") or "") for row in project.get("entries") or [] if isinstance(row, dict)}
    base_label = data.get("displayName") or data.get("driver") or source.stem or "entry"
    base_id = f"entry_{_slugify(base_label, 'entry')}"
    entry_id = base_id
    index = 1
    while entry_id in existing_ids or _entry_root(project_root, entry_id).exists():
        index += 1
        entry_id = f"{base_id}_{index}"
    entry_root = _entry_root(project_root, entry_id)
    source_target = entry_root / "source"
    source_kind, raw_inspection, sanitized, thumbnail, source_validation = _prepare_entry_source(roots, source, project_scope, entry_root)
    inspection = _inspect_entry_source(source_target, project_scope)
    metadata = raw_inspection.get("metadata") if isinstance(raw_inspection.get("metadata"), dict) else {}
    entry = {
        "id": entry_id,
        "sourceKind": source_kind,
        "sourcePath": str(source),
        "sourceRoot": str(source_target),
        "displayName": str(data.get("displayName") or metadata.get("displayName") or sanitized.get("folderName") or source.stem).strip(),
        "number": str(data.get("number") or metadata.get("number") or "").strip(),
        "driver": str(data.get("driver") or metadata.get("driver") or "").strip(),
        "team": str(data.get("team") or metadata.get("team") or "").strip(),
        "car": str(data.get("car") or metadata.get("car") or "").strip(),
        "class": str(data.get("class") or metadata.get("class") or "").strip(),
        "notes": str(data.get("notes") or "").strip(),
        "status": "warning",
        "warnings": [],
        "errors": [],
        "thumbnailPath": str(thumbnail or ""),
        "carGuid": str(sanitized.get("carGuid") or inspection.get("carGuid") or "").strip().lower(),
        "externalFolderName": str(sanitized.get("folderName") or inspection.get("externalFolderName") or "").strip(),
        "excludedFiles": list(sanitized.get("excludedFiles") or []),
        "sourceValidation": source_validation,
        "inspection": inspection,
        "createdAt": _now_iso(),
        "updatedAt": _now_iso(),
        "copiedFiles": int(sanitized.get("copiedCount") or 0),
    }
    _write_json(entry_root / ENTRY_MANIFEST, entry)
    project["entries"] = [row for row in project.get("entries") or [] if str(row.get("id") or "") != entry_id]
    project["entries"].append(entry)
    save_project(roots, project)
    return run_preflight(roots, project_scope, str(project.get("id") or ""))


def update_entry(roots: OnlineLeagueRoots, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    data = payload if isinstance(payload, dict) else {}
    scope = normalize_scope(data.get("workflowScope") or data.get("scope"))
    project = load_project(roots, scope, str(data.get("projectId") or ""))
    entry_id = _safe_id(data.get("entryId") or data.get("id"), "entry id")
    updated = False
    entries = []
    for entry in project.get("entries") or []:
        if str(entry.get("id") or "") == entry_id:
            row = dict(entry)
            for key in ("displayName", "number", "driver", "team", "car", "class", "notes"):
                if key in data:
                    row[key] = str(data.get(key) or "").strip()
            row["updatedAt"] = _now_iso()
            _write_json(_entry_root(_project_root(roots, scope, str(project.get("id") or "")), entry_id) / ENTRY_MANIFEST, row)
            entries.append(row)
            updated = True
        else:
            entries.append(entry)
    if not updated:
        raise FileNotFoundError(f"Online League entry not found: {entry_id}")
    project["entries"] = entries
    save_project(roots, project)
    return run_preflight(roots, scope, str(project.get("id") or ""))


def _readme_text(project: dict[str, Any], entries: list[dict[str, Any]]) -> str:
    normalize_scope(project.get("workflowScope"))
    lines = [
        f"{project.get('name') or 'Online League Pack'}",
        f"Version: {project.get('packVersion') or '-'}",
        "Workflow: Official ExternalLiveries",
        "",
        "This ZIP is a LiveryLab Online League pack.",
        "Import it from the Online League area. It is stored separately from My Liveries.",
        "Entries contain ExternalLiveries payloads only. No SavedCar is included or required.",
        "Importing this pack does not activate liveries or write files into AC EVO automatically.",
        "",
        "Entries:",
    ]
    for entry in entries:
        label = " / ".join(str(entry.get(key) or "-") for key in ("number", "driver", "team", "car"))
        lines.append(f"- {label} [{entry.get('status') or '-'}]")
    return "\n".join(lines) + "\n"


def _portable_export_entry(entry: dict[str, Any], entry_id: str, copied: int, thumbnail_relative: str = "") -> dict[str, Any]:
    export_entry = dict(entry)
    export_entry["sourcePath"] = ""
    export_entry["sourceRoot"] = f"entries/{entry_id}/source"
    export_entry["exportedFiles"] = copied
    export_entry["thumbnailPath"] = thumbnail_relative
    inspection = dict(export_entry.get("inspection") or {}) if isinstance(export_entry.get("inspection"), dict) else {}
    if inspection:
        inspection["thumbnailPath"] = export_entry.get("thumbnailPath") or ""
        payload = dict(inspection.get("payload") or {}) if isinstance(inspection.get("payload"), dict) else {}
        if payload:
            payload["payloadRoot"] = f"entries/{entry_id}/source"
            payload["externalLiveryJson"] = f"entries/{entry_id}/source/{online_league_payload.EXTERNAL_LIVERY_JSON}"
            payload.pop("sourcePayloadRoot", None)
            payload.pop("previewSource", None)
            inspection["payload"] = payload
        export_entry["inspection"] = inspection
    return export_entry


def build_league_pack(roots: OnlineLeagueRoots, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    data = payload if isinstance(payload, dict) else {}
    scope = normalize_scope(data.get("workflowScope") or data.get("scope"))
    project = run_preflight(roots, scope, str(data.get("projectId") or data.get("id") or ""))
    unverified = [
        entry for entry in project.get("entries") or []
        if not online_league_payload.is_verified_official_source(entry.get("sourceValidation"))
    ]
    if unverified:
        label = str(unverified[0].get("displayName") or unverified[0].get("externalFolderName") or unverified[0].get("id") or "Unknown livery")
        detail = str((unverified[0].get("sourceValidation") or {}).get("error") or "Official source verification failed.")
        raise ValueError(f"Official League pack build blocked by unverified livery '{label}': {detail}")
    include_warnings = bool(data.get("includeWarnings") or data.get("include_warnings"))
    selected_ids = {str(item) for item in data.get("entryIds") or [] if str(item).strip()}
    entries = []
    skipped = []
    for entry in project.get("entries") or []:
        if selected_ids and str(entry.get("id") or "") not in selected_ids:
            continue
        status = str(entry.get("status") or "")
        if status == "ready" or (status == "warning" and include_warnings):
            entries.append(entry)
        else:
            skipped.append(entry)
    if not entries:
        raise ValueError("No exportable Online League entries found. Ready entries are required unless warning export is enabled.")
    export_root = Path(str(data.get("targetDir") or "")).expanduser() if str(data.get("targetDir") or "").strip() else roots.package_export
    export_root.mkdir(parents=True, exist_ok=True)
    package_name = f"{_slugify(project.get('name'), 'league_pack')}_v{_slugify(project.get('packVersion'), '1_0_0')}_{_short_hash(project.get('id'), project.get('updatedAt'))}.liverylab-league"
    zip_path = export_root / f"{package_name}.zip"
    stage = roots.temp / f"online_league_export_{_short_hash(zip_path, _now_iso())}"
    if stage.exists():
        shutil.rmtree(stage)
    stage.mkdir(parents=True, exist_ok=True)
    project_root = _project_root(roots, scope, str(project.get("id") or ""))
    try:
        export_entries = []
        for entry in entries:
            entry_id = str(entry.get("id") or "")
            target_root = stage / "entries" / entry_id
            source_root = _entry_root(project_root, entry_id) / "source"
            online_league_payload.assert_pack_payload_safe(source_root)
            copied = _copy_tree(source_root, target_root / "source")
            preview_source_root = _entry_root(project_root, entry_id) / "preview"
            thumbnail_relative = ""
            preview_file = next((path for path in sorted(preview_source_root.glob("thumbnail.*")) if path.is_file()), None)
            if preview_file:
                preview_target = target_root / "preview" / preview_file.name
                package_zip.copy_file(preview_file, preview_target)
                thumbnail_relative = f"entries/{entry_id}/preview/{preview_file.name}"
            export_entry = _portable_export_entry(entry, entry_id, copied, thumbnail_relative)
            _write_json(target_root / ENTRY_MANIFEST, export_entry)
            export_entries.append(export_entry)
        manifest = {
            "formatVersion": EXPORT_FORMAT_VERSION,
            "type": EXPORT_TYPE,
            "packId": project.get("id"),
            "name": project.get("name"),
            "packVersion": project.get("packVersion"),
            "workflowScope": scope,
            "thumbnailPng": PROJECT_THUMBNAIL if (project_root / PROJECT_THUMBNAIL).is_file() else "",
            "createdAt": _now_iso(),
            "entryCount": len(export_entries),
            "skippedCount": len(skipped),
            "statusSummary": _status_summary(export_entries),
            "entries": export_entries,
        }
        if manifest["thumbnailPng"]:
            shutil.copy2(project_root / PROJECT_THUMBNAIL, stage / PROJECT_THUMBNAIL)
        _write_json(stage / EXPORT_MANIFEST, manifest)
        (stage / "readme.txt").write_text(_readme_text(project, export_entries), encoding="utf-8")
        package_zip.zip_directory(stage, zip_path)
    finally:
        shutil.rmtree(stage, ignore_errors=True)
    return {"zipPath": str(zip_path), "bundlePath": str(zip_path), "entryCount": len(entries), "skippedCount": len(skipped), "workflowScope": scope, "manifest": manifest}


def _validate_pack_entries(package_root: Path, entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
    entries_root = package_root / "entries"
    if not entries_root.is_dir():
        raise FileNotFoundError("Online League pack is missing its entries folder.")
    expected_ids: set[str] = set()
    validated: list[dict[str, Any]] = []
    for raw in entries:
        entry_id = _safe_id(raw.get("id"), "entry id")
        if entry_id in expected_ids:
            raise ValueError(f"Duplicate Online League entry id: {entry_id}")
        expected_ids.add(entry_id)
        entry_root = entries_root / entry_id
        package_security.validate_package_root_structure(
            entry_root,
            allowed_top_level={ENTRY_MANIFEST, "source", "preview"},
            required_files={ENTRY_MANIFEST, f"source/{online_league_payload.EXTERNAL_LIVERY_JSON}"},
            label=f"Online League entry {entry_id}",
        )
        payload = online_league_payload.assert_pack_payload_safe(entry_root / "source")
        manifest_entry = _read_json(entry_root / ENTRY_MANIFEST, {})
        if not isinstance(manifest_entry, dict) or str(manifest_entry.get("id") or "") != entry_id:
            raise ValueError(f"Online League entry manifest does not match entry id: {entry_id}")
        if not online_league_payload.is_verified_official_source(raw.get("sourceValidation")):
            raise ValueError(f"Online League entry is not verified as an Official livery source: {entry_id}")
        if not online_league_payload.is_verified_official_source(manifest_entry.get("sourceValidation")):
            raise ValueError(f"Online League entry manifest is not verified as an Official livery source: {entry_id}")
        expected_guid = str(raw.get("carGuid") or "").strip().lower()
        actual_guid = str(payload.get("carGuid") or "").strip().lower()
        if not expected_guid or expected_guid != actual_guid:
            raise ValueError(f"Online League entry car_guid does not match its payload: {entry_id}")
        external_folder_name = online_league_payload.validate_external_folder_name(raw.get("externalFolderName"))
        if str(manifest_entry.get("carGuid") or "").strip().lower() != actual_guid:
            raise ValueError(f"Online League entry manifest car_guid does not match its payload: {entry_id}")
        if online_league_payload.validate_external_folder_name(manifest_entry.get("externalFolderName")) != external_folder_name:
            raise ValueError(f"Online League entry folder name does not match its manifest: {entry_id}")
        preview_root = entry_root / "preview"
        for preview in preview_root.rglob("*") if preview_root.exists() else []:
            if preview.is_file() and preview.suffix.lower() not in {".png", ".jpg", ".jpeg"}:
                raise ValueError(f"Unsupported Online League preview file: {preview.relative_to(entry_root)}")
        validated.append({**raw, "carGuid": actual_guid, "externalFolderName": external_folder_name})
    actual_ids = {path.name for path in entries_root.iterdir() if path.is_dir()}
    if actual_ids != expected_ids:
        extra = sorted(actual_ids - expected_ids)
        missing = sorted(expected_ids - actual_ids)
        detail = extra[0] if extra else (missing[0] if missing else "unknown")
        raise ValueError(f"Online League pack entry folder mismatch: {detail}")
    car_guids = [str(entry.get("carGuid") or "").lower() for entry in validated]
    folder_names = [str(entry.get("externalFolderName") or "").lower() for entry in validated]
    if len(set(car_guids)) != len(car_guids):
        raise ValueError("Online League pack contains duplicate car_guid values.")
    if len(set(folder_names)) != len(folder_names):
        raise ValueError("Online League pack contains duplicate ExternalLiveries folder names.")
    return validated


def inspect_league_pack(roots: OnlineLeagueRoots, package_path: str | Path) -> dict[str, Any]:
    source = Path(package_path).expanduser()
    with package_zip.package_root(source, roots.temp, allow_folder=False, required_manifest=EXPORT_MANIFEST, max_file_count=MAX_IMPORT_ZIP_FILE_COUNT, max_total_uncompressed_bytes=MAX_IMPORT_ZIP_TOTAL_UNCOMPRESSED_BYTES) as package_root:
        package_security.validate_package_root_structure(package_root, allowed_top_level={EXPORT_MANIFEST, PROJECT_THUMBNAIL, "readme.txt", "entries"}, required_files={EXPORT_MANIFEST}, label="Online League pack")
        manifest = _read_json(package_root / EXPORT_MANIFEST, {})
        if not isinstance(manifest, dict) or manifest.get("type") != EXPORT_TYPE:
            raise ValueError("Selected ZIP is not a LiveryLab Online League pack.")
        if int(manifest.get("formatVersion") or 0) != EXPORT_FORMAT_VERSION:
            raise ValueError(f"Unsupported Online League pack version: {manifest.get('formatVersion')}")
        normalize_scope(manifest.get("workflowScope"))
        entries = [row for row in manifest.get("entries") or [] if isinstance(row, dict)]
        if int(manifest.get("entryCount") or 0) != len(entries) or not entries:
            raise ValueError("Online League pack entry count is invalid.")
        entries = _validate_pack_entries(package_root, entries)
        return {"packagePath": str(source), "manifest": manifest, "workflowScope": normalize_scope(manifest.get("workflowScope")), "name": str(manifest.get("name") or ""), "packVersion": str(manifest.get("packVersion") or ""), "thumbnailPng": str(manifest.get("thumbnailPng") or ""), "entryCount": len(entries), "statusSummary": _status_summary(entries), "entries": entries}


def import_league_pack(roots: OnlineLeagueRoots, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    _ensure_roots(roots)
    data = payload if isinstance(payload, dict) else {}
    package_path = Path(str(data.get("packagePath") or data.get("path") or "")).expanduser()
    info = inspect_league_pack(roots, package_path)
    scope = normalize_scope(info.get("workflowScope"))
    expected_scope = str(data.get("expectedWorkflowScope") or data.get("expected_scope") or "").strip()
    if expected_scope:
        clean_expected_scope = normalize_scope(expected_scope)
        if clean_expected_scope != scope:
            raise ValueError(f"Selected Online League pack is {scope}, but the active workflow is {clean_expected_scope}.")
    base_id = _slugify(f"{info.get('name')}_{info.get('packVersion')}", "league_pack")
    installed_id = base_id
    index = 1
    while _installed_root(roots, scope, installed_id).exists():
        index += 1
        installed_id = f"{base_id}_{index}"
    target = _installed_root(roots, scope, installed_id)
    try:
        with package_zip.package_root(package_path, roots.temp, allow_folder=False, required_manifest=EXPORT_MANIFEST, max_file_count=MAX_IMPORT_ZIP_FILE_COUNT, max_total_uncompressed_bytes=MAX_IMPORT_ZIP_TOTAL_UNCOMPRESSED_BYTES) as package_root:
            _copy_tree(package_root, target)
        installed_manifest = {**info.get("manifest", {}), "installedId": installed_id, "installedAt": _now_iso(), "installedRoot": str(target), "sourcePackagePath": str(package_path), "activationState": "local_only"}
        _write_json(target / "installed_pack.json", installed_manifest)
    except Exception:
        shutil.rmtree(target, ignore_errors=True)
        raise
    return {"installedId": installed_id, "installedRoot": str(target), "workflowScope": scope, "entryCount": info.get("entryCount", 0), "manifest": installed_manifest}


def _installed_entry_view(installed_root: Path, entry: dict[str, Any]) -> dict[str, Any]:
    view = dict(entry)
    inspection = dict(entry.get("inspection") or {}) if isinstance(entry.get("inspection"), dict) else {}
    raw_thumbnail = str(entry.get("thumbnailPath") or inspection.get("thumbnailPath") or "").strip()
    thumbnail_path = ""
    if raw_thumbnail:
        relative = Path(raw_thumbnail)
        if not relative.is_absolute():
            root = installed_root.resolve()
            candidate = (installed_root / relative).resolve()
            try:
                candidate.relative_to(root)
            except ValueError:
                pass
            else:
                if candidate.is_file():
                    thumbnail_path = str(candidate)
    view["thumbnailPath"] = thumbnail_path
    if inspection:
        inspection["thumbnailPath"] = thumbnail_path
        view["inspection"] = inspection
    return view


def list_installed_packs(roots: OnlineLeagueRoots, scope: Any = workflow_scopes.SCOPE_ALL) -> dict[str, Any]:
    _ensure_roots(roots)
    normalize_scope(scope)
    packs = []
    for folder in sorted(roots.library.iterdir(), key=lambda path: path.name.lower()) if roots.library.exists() else []:
        if not folder.is_dir() or folder.name.lower() == "advanced":
            continue
        manifest = _read_json(folder / "installed_pack.json", {})
        if (
            not isinstance(manifest, dict)
            or manifest.get("type") != EXPORT_TYPE
            or int(manifest.get("formatVersion") or 0) != EXPORT_FORMAT_VERSION
        ):
            continue
        entries = [_installed_entry_view(folder, row) for row in manifest.get("entries") or [] if isinstance(row, dict)]
        deployment = online_league_deployments.pack_reference_status(
            roots.library,
            str(manifest.get("installedId") or folder.name),
            entries,
        )
        thumbnail_path = folder / PROJECT_THUMBNAIL
        packs.append({
            "installedId": str(manifest.get("installedId") or folder.name),
            "name": str(manifest.get("name") or folder.name),
            "packVersion": str(manifest.get("packVersion") or ""),
            "workflowScope": normalize_scope(manifest.get("workflowScope") or workflow_scopes.SCOPE_OFFICIAL),
            "thumbnailPng": PROJECT_THUMBNAIL if thumbnail_path.is_file() else "",
            "thumbnailPath": str(thumbnail_path) if thumbnail_path.is_file() else "",
            "installedAt": str(manifest.get("installedAt") or ""),
            "installedRoot": str(folder),
            "activationState": str(deployment.get("state") or manifest.get("activationState") or "local_only"),
            "deploymentSummary": deployment,
            "entryCount": len(entries),
            "statusSummary": _status_summary(entries),
            "entries": entries,
        })
    return {"packs": packs, "packCount": len(packs)}


def preflight_installed_pack_activation(
    roots: OnlineLeagueRoots,
    installed_pack_id: str,
    external_liveries_root: str | Path,
) -> dict[str, Any]:
    _ensure_roots(roots)
    return online_league_activation.activation_preflight(roots.library, installed_pack_id, external_liveries_root)


def activate_installed_pack(
    roots: OnlineLeagueRoots,
    installed_pack_id: str,
    external_liveries_root: str | Path,
    *,
    confirm_token: str,
) -> dict[str, Any]:
    _ensure_roots(roots)
    return online_league_activation.activate_pack(
        roots.library,
        installed_pack_id,
        external_liveries_root,
        confirm_token=confirm_token,
    )


def preflight_installed_pack_deactivation(
    roots: OnlineLeagueRoots,
    installed_pack_id: str,
    external_liveries_root: str | Path,
) -> dict[str, Any]:
    _ensure_roots(roots)
    return online_league_activation.deactivation_preflight(roots.library, installed_pack_id, external_liveries_root)


def deactivate_installed_pack(
    roots: OnlineLeagueRoots,
    installed_pack_id: str,
    external_liveries_root: str | Path,
    *,
    confirm_token: str,
) -> dict[str, Any]:
    _ensure_roots(roots)
    return online_league_activation.deactivate_pack(
        roots.library,
        installed_pack_id,
        external_liveries_root,
        confirm_token=confirm_token,
    )


def preflight_installed_pack_deletion(
    roots: OnlineLeagueRoots,
    installed_pack_id: str,
    external_liveries_root: str | Path,
) -> dict[str, Any]:
    _ensure_roots(roots)
    return online_league_activation.deletion_preflight(roots.library, installed_pack_id, external_liveries_root)


def delete_installed_pack(
    roots: OnlineLeagueRoots,
    installed_pack_id: str,
    external_liveries_root: str | Path,
    *,
    confirm_token: str,
) -> dict[str, Any]:
    _ensure_roots(roots)
    return online_league_activation.delete_pack(
        roots.library,
        installed_pack_id,
        external_liveries_root,
        confirm_token=confirm_token,
    )


def overview(roots: OnlineLeagueRoots, scope: Any = workflow_scopes.SCOPE_ALL) -> dict[str, Any]:
    return {"roots": roots_info(roots, create=True), "projects": list_projects(roots, scope).get("projects", []), "installedPacks": list_installed_packs(roots, scope).get("packs", [])}
