from __future__ import annotations

import re
from datetime import datetime
from pathlib import Path
from typing import Any, Callable


DEFAULT_CATEGORY_ID = "uncategorized"
SETTINGS_CATEGORIES_KEY = "liveryCategories"
SETTINGS_ASSIGNMENTS_KEY = "liveryCategoryAssignments"
SETTINGS_FAVORITES_KEY = "liveryFavoriteSkinKeys"
SETTINGS_SCOPES_KEY = "liveryCategoryScopes"
SETTINGS_SORT_MODE_KEY = "liveryCategorySortMode"

SCOPE_ADVANCED = "advanced"
SCOPE_OFFICIAL = "official"
SCOPE_ALL = "all"
KNOWN_SCOPES = {SCOPE_ADVANCED, SCOPE_OFFICIAL, SCOPE_ALL}
SCOPED_STORAGE_SCOPES = {SCOPE_ADVANCED, SCOPE_OFFICIAL}
SORT_CREATED = "created"
SORT_ALPHABETICAL = "alphabetical"
SORT_CUSTOM = "custom"
KNOWN_SORT_MODES = {SORT_CREATED, SORT_ALPHABETICAL, SORT_CUSTOM}


def _clean_text(value: Any, fallback: str = "") -> str:
    text = str(value or "").strip()
    return text or fallback


def _int_value(value: Any, fallback: int = 0) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return fallback


def _normalize_scope(value: Any, default: str = SCOPE_ALL) -> str:
    raw = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
    aliases = {
        "advanced_extracted": SCOPE_ADVANCED,
        "advanced_extracted_content": SCOPE_ADVANCED,
        "legacy": SCOPE_ADVANCED,
        "liverylab": SCOPE_ADVANCED,
        "official_external": SCOPE_OFFICIAL,
        "official_external_livery": SCOPE_OFFICIAL,
        "external_livery": SCOPE_OFFICIAL,
    }
    if raw in KNOWN_SCOPES:
        return raw
    if raw in aliases:
        return aliases[raw]
    return default if default in KNOWN_SCOPES else SCOPE_ALL


def _normalize_sort_mode(value: Any, fallback: str = SORT_CREATED) -> str:
    raw = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
    aliases = {
        "az": SORT_ALPHABETICAL,
        "a_z": SORT_ALPHABETICAL,
        "alpha": SORT_ALPHABETICAL,
        "name": SORT_ALPHABETICAL,
        "manual": SORT_CUSTOM,
        "order": SORT_CUSTOM,
        "created_order": SORT_CREATED,
    }
    mode = aliases.get(raw, raw)
    return mode if mode in KNOWN_SORT_MODES else fallback


def _skin_key_set(skin_keys: list[str] | tuple[str, ...] | set[str] | None = None) -> set[str]:
    return {str(key or "").strip().lower() for key in (skin_keys or []) if str(key or "").strip()}


def _normalize_category(raw: dict[str, Any], index: int) -> dict[str, Any]:
    category_id = _clean_text(raw.get("id"), f"cat_{index + 1:03d}")
    created_order = _int_value(raw.get("createdOrder", raw.get("created_order", raw.get("sortOrder", raw.get("order")))), (index + 1) * 10)
    return {
        "id": category_id,
        "name": _clean_text(raw.get("name"), f"Category {index + 1}"),
        "collapsed": bool(raw.get("collapsed", False)),
        "createdOrder": created_order,
        "sortOrder": _int_value(raw.get("sortOrder", raw.get("order")), created_order),
    }


def _category_sort_key(item: dict[str, Any], sort_mode: str) -> tuple[Any, ...]:
    if sort_mode == SORT_ALPHABETICAL:
        return (str(item.get("name") or "").lower(), str(item.get("id") or ""))
    if sort_mode == SORT_CREATED:
        return (_int_value(item.get("createdOrder"), _int_value(item.get("sortOrder"), 0)), str(item.get("name") or "").lower(), str(item.get("id") or ""))
    return (_int_value(item.get("sortOrder"), 0), str(item.get("name") or "").lower(), str(item.get("id") or ""))


def _normalize_categories(raw: Any, sort_mode: str = SORT_CUSTOM) -> list[dict[str, Any]]:
    categories: list[dict[str, Any]] = []
    seen: set[str] = set()
    rows = raw if isinstance(raw, list) else []
    for index, row in enumerate(rows):
        if not isinstance(row, dict):
            continue
        category = _normalize_category(row, index)
        category_id = category["id"]
        if not category_id or category_id == DEFAULT_CATEGORY_ID or category_id in seen:
            continue
        seen.add(category_id)
        categories.append(category)
    categories.sort(key=lambda item: _category_sort_key(item, sort_mode))
    for index, category in enumerate(categories):
        category["sortOrder"] = _int_value(category.get("sortOrder"), (index + 1) * 10)
        category["createdOrder"] = _int_value(category.get("createdOrder"), (index + 1) * 10)
    return categories


def _normalize_assignments(raw: Any, categories: list[dict[str, Any]], skin_keys: set[str] | None = None) -> dict[str, str]:
    if not isinstance(raw, dict):
        return {}
    category_ids = {str(category.get("id") or "") for category in categories}
    assignments: dict[str, str] = {}
    for raw_skin_key, raw_category_id in raw.items():
        skin_key = str(raw_skin_key or "").strip()
        skin_key_lookup = skin_key.lower()
        category_id = str(raw_category_id or "").strip()
        if not skin_key or not category_id or category_id == DEFAULT_CATEGORY_ID:
            continue
        if category_id not in category_ids:
            continue
        if skin_keys is not None and skin_key_lookup not in skin_keys:
            continue
        assignments[skin_key] = category_id
    return assignments


def _normalize_favorites(raw: Any, skin_keys: set[str] | None = None) -> list[str]:
    if not isinstance(raw, list):
        return []
    favorites: list[str] = []
    seen: set[str] = set()
    for value in raw:
        skin_key = str(value or "").strip()
        skin_key_lookup = skin_key.lower()
        if not skin_key or skin_key_lookup in seen:
            continue
        if skin_keys is not None and skin_key_lookup not in skin_keys:
            continue
        seen.add(skin_key_lookup)
        favorites.append(skin_key)
    return favorites


def _next_category_id(categories: list[dict[str, Any]], name: str = "category") -> str:
    base = re.sub(r"[^a-z0-9_]+", "_", _clean_text(name, "category").lower()).strip("_") or "category"
    base = f"cat_{base}" if not base.startswith("cat_") else base
    existing = {str(category.get("id") or "") for category in categories}
    category_id = base
    counter = 2
    while category_id in existing or category_id == DEFAULT_CATEGORY_ID:
        category_id = f"{base}_{counter}"
        counter += 1
    return category_id


def _save_settings(settings: dict[str, Any], save_app_settings: Callable[[dict[str, Any]], Path]) -> Path:
    settings["liveryCategoriesUpdatedAt"] = datetime.now().isoformat(timespec="seconds")
    return save_app_settings(settings)


def _legacy_scope_payload(settings: dict[str, Any], scope: str, skin_keys: set[str] | None) -> dict[str, Any]:
    legacy_categories = _normalize_categories(settings.get(SETTINGS_CATEGORIES_KEY), SORT_CREATED)
    legacy_assignments = settings.get(SETTINGS_ASSIGNMENTS_KEY) if isinstance(settings.get(SETTINGS_ASSIGNMENTS_KEY), dict) else {}
    assignments = _normalize_assignments(legacy_assignments, legacy_categories, skin_keys)
    assigned_category_ids = set(assignments.values())
    global_assigned_category_ids = {str(value or "").strip() for value in legacy_assignments.values() if str(value or "").strip()}
    if scope == SCOPE_OFFICIAL:
        categories = [row for row in legacy_categories if str(row.get("id") or "") in assigned_category_ids]
    elif scope == SCOPE_ADVANCED:
        categories = [
            row for row in legacy_categories
            if str(row.get("id") or "") in assigned_category_ids or str(row.get("id") or "") not in global_assigned_category_ids
        ]
    else:
        categories = legacy_categories
    sort_mode = _normalize_sort_mode(settings.get(SETTINGS_SORT_MODE_KEY), SORT_CREATED)
    return {
        "categories": categories,
        "assignments": assignments,
        "favorites": _normalize_favorites(settings.get(SETTINGS_FAVORITES_KEY), skin_keys),
        "sortMode": sort_mode,
    }


def _scope_payload(settings: dict[str, Any], scope: str, skin_keys: set[str] | None) -> dict[str, Any]:
    clean_scope = _normalize_scope(scope)
    if clean_scope not in SCOPED_STORAGE_SCOPES:
        return _legacy_scope_payload(settings, clean_scope, skin_keys)
    scoped = settings.get(SETTINGS_SCOPES_KEY)
    bucket = scoped.get(clean_scope) if isinstance(scoped, dict) and isinstance(scoped.get(clean_scope), dict) else None
    if not bucket:
        return _legacy_scope_payload(settings, clean_scope, skin_keys)
    sort_mode = _normalize_sort_mode(bucket.get("sortMode"), SORT_CREATED)
    categories = _normalize_categories(bucket.get("categories"), sort_mode)
    return {
        "categories": categories,
        "assignments": _normalize_assignments(bucket.get("assignments"), categories, skin_keys),
        "favorites": _normalize_favorites(bucket.get("favorites"), skin_keys),
        "sortMode": sort_mode,
    }


def _store_scope_payload(
    settings: dict[str, Any],
    scope: str,
    categories: list[dict[str, Any]],
    assignments: dict[str, str],
    favorites: list[str],
    sort_mode: str,
    skin_keys: set[str] | None = None,
) -> None:
    clean_scope = _normalize_scope(scope, SCOPE_ADVANCED)
    if clean_scope not in SCOPED_STORAGE_SCOPES:
        clean_scope = SCOPE_ADVANCED
    scoped = settings.get(SETTINGS_SCOPES_KEY)
    if not isinstance(scoped, dict):
        scoped = {}
    normalized_sort_mode = _normalize_sort_mode(sort_mode, SORT_CREATED)
    normalized_categories = _normalize_categories(categories, SORT_CUSTOM)
    scoped[clean_scope] = {
        "categories": normalized_categories,
        "assignments": _normalize_assignments(assignments, normalized_categories, skin_keys),
        "favorites": _normalize_favorites(favorites, skin_keys),
        "sortMode": normalized_sort_mode,
    }
    settings[SETTINGS_SCOPES_KEY] = scoped


def load_state(
    settings: dict[str, Any],
    skin_keys: list[str] | tuple[str, ...] | set[str] | None = None,
    scope: str = SCOPE_ALL,
) -> dict[str, Any]:
    valid_skin_keys = _skin_key_set(skin_keys) if skin_keys is not None else None
    clean_scope = _normalize_scope(scope)
    payload = _scope_payload(settings, clean_scope, valid_skin_keys)
    sort_mode = _normalize_sort_mode(payload.get("sortMode"), SORT_CREATED)
    categories = _normalize_categories(payload.get("categories"), sort_mode)
    assignments = _normalize_assignments(payload.get("assignments"), categories, valid_skin_keys)
    favorites = _normalize_favorites(payload.get("favorites"), valid_skin_keys)
    return {
        "favoritesCategory": {
            "id": "favorites",
            "name": "Favorites",
            "collapsed": False,
            "sortOrder": -10,
        },
        "defaultCategory": {
            "id": DEFAULT_CATEGORY_ID,
            "name": "Uncategorized",
            "collapsed": False,
            "sortOrder": 0,
        },
        "categories": categories,
        "assignments": assignments,
        "favorites": favorites,
        "scope": clean_scope,
        "sortMode": sort_mode,
    }


def save_state(
    settings: dict[str, Any],
    save_app_settings: Callable[[dict[str, Any]], Path],
    categories: list[dict[str, Any]],
    assignments: dict[str, str],
    skin_keys: list[str] | tuple[str, ...] | set[str] | None = None,
    scope: str = SCOPE_ALL,
    sort_mode: str = SORT_CREATED,
) -> dict[str, Any]:
    clean_scope = _normalize_scope(scope, SCOPE_ADVANCED)
    normalized_sort_mode = _normalize_sort_mode(sort_mode, SORT_CREATED)
    normalized_categories = _normalize_categories(categories, SORT_CUSTOM)
    valid_skin_keys = _skin_key_set(skin_keys) if skin_keys is not None else None
    normalized_assignments = _normalize_assignments(assignments, normalized_categories, valid_skin_keys)
    current = load_state(settings, skin_keys, clean_scope)
    _store_scope_payload(
        settings,
        clean_scope,
        normalized_categories,
        normalized_assignments,
        current.get("favorites") if isinstance(current.get("favorites"), list) else [],
        normalized_sort_mode,
        valid_skin_keys,
    )
    path = _save_settings(settings, save_app_settings)
    state = load_state(settings, skin_keys, clean_scope)
    state["settingsPath"] = str(path)
    return state


def create_category(
    settings: dict[str, Any],
    save_app_settings: Callable[[dict[str, Any]], Path],
    name: str,
    skin_keys: list[str] | tuple[str, ...] | set[str] | None = None,
    scope: str = SCOPE_ALL,
) -> dict[str, Any]:
    clean_name = _clean_text(name)
    if not clean_name:
        raise ValueError("Category name is required")
    state = load_state(settings, skin_keys, scope)
    categories = state["categories"]
    next_created_order = max([_int_value(row.get("createdOrder"), 0) for row in categories] or [0]) + 10
    next_sort_order = max([_int_value(row.get("sortOrder"), 0) for row in categories] or [0]) + 10
    category = {
        "id": _next_category_id(categories, clean_name),
        "name": clean_name,
        "collapsed": False,
        "createdOrder": next_created_order,
        "sortOrder": next_sort_order,
    }
    categories.append(category)
    saved = save_state(settings, save_app_settings, categories, state["assignments"], skin_keys, scope, state.get("sortMode", SORT_CREATED))
    saved["category"] = next((row for row in saved["categories"] if row.get("id") == category["id"]), category)
    return saved


def update_category(
    settings: dict[str, Any],
    save_app_settings: Callable[[dict[str, Any]], Path],
    category_id: str,
    patch: dict[str, Any],
    skin_keys: list[str] | tuple[str, ...] | set[str] | None = None,
    scope: str = SCOPE_ALL,
) -> dict[str, Any]:
    clean_id = _clean_text(category_id)
    if not clean_id or clean_id == DEFAULT_CATEGORY_ID:
        raise ValueError("Custom category id is required")
    state = load_state(settings, skin_keys, scope)
    category = next((row for row in state["categories"] if row.get("id") == clean_id), None)
    if not category:
        raise FileNotFoundError(f"Livery category not found: {clean_id}")
    if "name" in patch:
        clean_name = _clean_text(patch.get("name"))
        if not clean_name:
            raise ValueError("Category name is required")
        category["name"] = clean_name
    if "collapsed" in patch:
        category["collapsed"] = bool(patch.get("collapsed"))
    move = str(patch.get("move") or "").strip().lower()
    if move in {"up", "down"}:
        categories = _normalize_categories(state["categories"], SORT_CUSTOM)
        index = next((idx for idx, row in enumerate(categories) if row.get("id") == clean_id), -1)
        target = index - 1 if move == "up" else index + 1
        if 0 <= index < len(categories) and 0 <= target < len(categories):
            categories[index], categories[target] = categories[target], categories[index]
            for order_index, row in enumerate(categories):
                row["sortOrder"] = (order_index + 1) * 10
            state["categories"] = categories
            state["sortMode"] = SORT_CUSTOM
    saved = save_state(settings, save_app_settings, state["categories"], state["assignments"], skin_keys, scope, state.get("sortMode", SORT_CREATED))
    saved["category"] = next((row for row in saved["categories"] if row.get("id") == clean_id), category)
    return saved


def update_settings(
    settings: dict[str, Any],
    save_app_settings: Callable[[dict[str, Any]], Path],
    patch: dict[str, Any],
    skin_keys: list[str] | tuple[str, ...] | set[str] | None = None,
    scope: str = SCOPE_ALL,
) -> dict[str, Any]:
    state = load_state(settings, skin_keys, scope)
    sort_mode = state.get("sortMode", SORT_CREATED)
    if "sortMode" in patch:
        sort_mode = _normalize_sort_mode(patch.get("sortMode"), sort_mode)
    return save_state(settings, save_app_settings, state["categories"], state["assignments"], skin_keys, scope, sort_mode)


def delete_category(
    settings: dict[str, Any],
    save_app_settings: Callable[[dict[str, Any]], Path],
    category_id: str,
    skin_keys: list[str] | tuple[str, ...] | set[str] | None = None,
    scope: str = SCOPE_ALL,
) -> dict[str, Any]:
    clean_id = _clean_text(category_id)
    if not clean_id or clean_id == DEFAULT_CATEGORY_ID:
        raise ValueError("Custom category id is required")
    state = load_state(settings, skin_keys, scope)
    categories = [row for row in state["categories"] if row.get("id") != clean_id]
    if len(categories) == len(state["categories"]):
        raise FileNotFoundError(f"Livery category not found: {clean_id}")
    moved = sum(1 for value in state["assignments"].values() if value == clean_id)
    assignments = {key: value for key, value in state["assignments"].items() if value != clean_id}
    saved = save_state(settings, save_app_settings, categories, assignments, skin_keys, scope, state.get("sortMode", SORT_CREATED))
    saved["deletedCategoryId"] = clean_id
    saved["movedSkins"] = moved
    return saved


def assign_skin(
    settings: dict[str, Any],
    save_app_settings: Callable[[dict[str, Any]], Path],
    skin_key: str,
    category_id: str,
    skin_keys: list[str] | tuple[str, ...] | set[str] | None = None,
    scope: str = SCOPE_ALL,
) -> dict[str, Any]:
    clean_skin_key = _clean_text(skin_key)
    if not clean_skin_key:
        raise ValueError("Skin key is required")
    valid_skin_keys = _skin_key_set(skin_keys) if skin_keys is not None else None
    if valid_skin_keys is not None and clean_skin_key.lower() not in valid_skin_keys:
        raise FileNotFoundError(f"My Skin not found: {clean_skin_key}")
    clean_category_id = _clean_text(category_id, DEFAULT_CATEGORY_ID)
    state = load_state(settings, skin_keys, scope)
    if clean_category_id != DEFAULT_CATEGORY_ID and not any(row.get("id") == clean_category_id for row in state["categories"]):
        raise FileNotFoundError(f"Livery category not found: {clean_category_id}")
    assignments = dict(state["assignments"])
    if clean_category_id == DEFAULT_CATEGORY_ID:
        assignments.pop(clean_skin_key, None)
    else:
        assignments[clean_skin_key] = clean_category_id
    saved = save_state(settings, save_app_settings, state["categories"], assignments, skin_keys, scope, state.get("sortMode", SORT_CREATED))
    saved["skinKey"] = clean_skin_key
    saved["categoryId"] = clean_category_id
    return saved


def set_favorite(
    settings: dict[str, Any],
    save_app_settings: Callable[[dict[str, Any]], Path],
    skin_key: str,
    favorite: bool,
    skin_keys: list[str] | tuple[str, ...] | set[str] | None = None,
    scope: str = SCOPE_ALL,
) -> dict[str, Any]:
    clean_skin_key = _clean_text(skin_key)
    if not clean_skin_key:
        raise ValueError("Skin key is required")
    valid_skin_keys = _skin_key_set(skin_keys) if skin_keys is not None else None
    if valid_skin_keys is not None and clean_skin_key.lower() not in valid_skin_keys:
        raise FileNotFoundError(f"My Skin not found: {clean_skin_key}")
    state = load_state(settings, skin_keys, scope)
    favorites = _normalize_favorites(state.get("favorites"), valid_skin_keys)
    favorites_by_key = {item.lower(): item for item in favorites}
    if favorite:
        favorites_by_key[clean_skin_key.lower()] = clean_skin_key
    else:
        favorites_by_key.pop(clean_skin_key.lower(), None)
    state["favorites"] = sorted(favorites_by_key.values(), key=lambda value: value.lower())
    _store_scope_payload(
        settings,
        scope,
        state["categories"],
        state["assignments"],
        state["favorites"],
        state.get("sortMode", SORT_CREATED),
        valid_skin_keys,
    )
    path = _save_settings(settings, save_app_settings)
    state = load_state(settings, skin_keys, scope)
    state["settingsPath"] = str(path)
    state["skinKey"] = clean_skin_key
    state["favorite"] = bool(favorite)
    return state
