from __future__ import annotations

import json
import re
import shutil
from copy import deepcopy
from datetime import datetime
from pathlib import Path
from threading import RLock
from typing import Any


DEFAULT_CONFIG: dict[str, Any] = {
    "version": 22,
    "project_name": "Новый интерфейс",
    "data_source": "golf",
    "canvas": {
        "width": 1440,
        "height": 900,
        "grid_size": 10,
        "snap_enabled": True,
        "snap_threshold": 8,
        "show_grid": True,
        "auto_bind_containers": True,
        "background": "#0c1421",
    },
    "tabs": [
        {"id": "main", "label": "Основное"},
        {"id": "data", "label": "Данные"},
    ],
    "components": [],
    "triggers": [],
    "shortcut_sequences": [],
    "prematch_groups": [],
    "prematch_buttons": [],
    "quick_panel_selectors": [],
    "player_selection_panels": [],
}


DEFAULT_HOCKEY_PLAYER_SELECTION_PANELS: list[dict[str, Any]] = [
    {"id": "compare_home", "label": "Сравнение · левая команда", "description": "Два игрока левой команды", "slots": 2, "rule": "home", "sync_selected_player": False, "collapsed_default": True, "sort_order": 0, "enabled": True},
    {"id": "compare_away", "label": "Сравнение · правая команда", "description": "Два игрока правой команды", "slots": 2, "rule": "away", "sync_selected_player": False, "collapsed_default": True, "sort_order": 10, "enabled": True},
    {"id": "compare_mixed", "label": "Сравнение · разные команды", "description": "По одному игроку каждой команды", "slots": 2, "rule": "different_teams", "sync_selected_player": False, "collapsed_default": True, "sort_order": 20, "enabled": True},
    {"id": "three_stars", "label": "3 звезды", "description": "Три игрока для финального титра", "slots": 3, "rule": "any", "sync_selected_player": False, "collapsed_default": True, "sort_order": 30, "enabled": True},
    {"id": "selected_player", "label": "Выбранный игрок", "description": "Один игрок для подписи / индивидуального титра", "slots": 1, "rule": "any", "sync_selected_player": True, "collapsed_default": True, "sort_order": 40, "enabled": True},
]


class UIBuilderManager:
    """JSON settings storage with atomic writes and rolling backups."""

    def __init__(self, settings_dir: Path, *, filename: str = "ui_builder.json") -> None:
        self.settings_dir = Path(settings_dir)
        self.settings_file = self.settings_dir / Path(filename).name
        self.backups_dir = self.settings_dir / "backups"
        self.backup_prefix = self.settings_file.stem
        self._lock = RLock()
        self.settings_dir.mkdir(parents=True, exist_ok=True)
        self.backups_dir.mkdir(parents=True, exist_ok=True)
        if not self.settings_file.exists():
            self.save(DEFAULT_CONFIG, create_backup=False)

    def load(self) -> dict[str, Any]:
        with self._lock:
            try:
                raw = json.loads(self.settings_file.read_text(encoding="utf-8"))
            except (OSError, json.JSONDecodeError, TypeError):
                return deepcopy(DEFAULT_CONFIG)
            had_legacy_shortcuts = self._has_legacy_hockey_component_shortcuts(raw)
            try:
                source_version = int(raw.get("version") or 0) if isinstance(raw, dict) else 0
            except (TypeError, ValueError):
                source_version = 0
            normalized = self._normalize(raw)
            if had_legacy_shortcuts or source_version < 22:
                # Build61: remove old hidden component-level Space/Ctrl+R bindings.
                # BUILD90: persist the one-time v21 -> v22 native-countdown migration
                # so old Text mirror timer steps stop producing per-second SetText traffic.
                self.save(normalized, create_backup=False)
            return normalized

    def save(self, config: dict[str, Any], *, create_backup: bool = True) -> dict[str, Any]:
        normalized = self._normalize(config)
        with self._lock:
            if create_backup and self.settings_file.exists():
                stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
                shutil.copy2(self.settings_file, self.backups_dir / f"{self.backup_prefix}_{stamp}.json")
                self._trim_backups(40)

            temp_file = self.settings_file.with_suffix(self.settings_file.suffix + ".tmp")
            temp_file.write_text(
                json.dumps(normalized, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
            temp_file.replace(self.settings_file)
        return normalized

    def import_config(self, payload: dict[str, Any]) -> dict[str, Any]:
        return self.save(payload, create_backup=True)

    def list_backups(self) -> list[dict[str, Any]]:
        result: list[dict[str, Any]] = []
        for path in sorted(self.backups_dir.glob(f"{self.backup_prefix}_*.json"), reverse=True):
            result.append(
                {
                    "name": path.name,
                    "modified": datetime.fromtimestamp(path.stat().st_mtime).isoformat(timespec="seconds"),
                    "size": path.stat().st_size,
                }
            )
        return result

    def restore_backup(self, name: str) -> dict[str, Any]:
        safe_name = Path(name).name
        path = self.backups_dir / safe_name
        if not path.exists() or path.parent.resolve() != self.backups_dir.resolve():
            raise FileNotFoundError(safe_name)
        payload = json.loads(path.read_text(encoding="utf-8"))
        return self.save(payload, create_backup=True)

    def _trim_backups(self, keep: int) -> None:
        backups = sorted(self.backups_dir.glob(f"{self.backup_prefix}_*.json"), reverse=True)
        for path in backups[keep:]:
            path.unlink(missing_ok=True)

    @staticmethod
    def _is_hockey_config(config: dict[str, Any]) -> bool:
        components = config.get("components") if isinstance(config, dict) else None
        if not isinstance(components, list):
            return False
        for component in components:
            if not isinstance(component, dict):
                continue
            component_type = str(component.get("type") or "")
            action_id = str(component.get("action_id") or "")
            if component_type.startswith("hockey_") or action_id.startswith("hockey_"):
                return True
        return False

    @staticmethod
    def _legacy_component_shortcut_combo(value: Any) -> bool:
        compact = re.sub(r"\s+", "", str(value or "")).lower()
        return compact in {"space", "spacebar", "ctrl+r", "control+r"}

    @classmethod
    def _has_legacy_hockey_component_shortcuts(cls, config: dict[str, Any]) -> bool:
        if not cls._is_hockey_config(config):
            return False
        components = config.get("components") if isinstance(config.get("components"), list) else []
        for component in components:
            if not isinstance(component, dict):
                continue
            shortcuts = component.get("shortcuts") if isinstance(component.get("shortcuts"), list) else []
            if any(isinstance(shortcut, dict) and cls._legacy_component_shortcut_combo(shortcut.get("combo")) for shortcut in shortcuts):
                return True
        return False

    @staticmethod
    def _normalize(config: dict[str, Any]) -> dict[str, Any]:
        result = deepcopy(DEFAULT_CONFIG)
        if not isinstance(config, dict):
            return result

        source_version_raw = config.get("version", 0)
        try:
            source_version = int(source_version_raw or 0)
        except (TypeError, ValueError):
            source_version = 0
        result["version"] = 22
        result["project_name"] = str(config.get("project_name") or result["project_name"])
        result["data_source"] = str(config.get("data_source") or result["data_source"])

        canvas = config.get("canvas") if isinstance(config.get("canvas"), dict) else {}
        result["canvas"].update(canvas)
        for key, low, high, fallback in (
            ("width", 640, 7680, 1440),
            ("height", 360, 4320, 900),
            ("grid_size", 1, 100, 10),
            ("snap_threshold", 1, 50, 8),
        ):
            try:
                value = int(result["canvas"].get(key, fallback))
            except (TypeError, ValueError):
                value = fallback
            result["canvas"][key] = max(low, min(high, value))
        result["canvas"]["snap_enabled"] = bool(result["canvas"].get("snap_enabled", True))
        result["canvas"]["show_grid"] = bool(result["canvas"].get("show_grid", True))
        result["canvas"]["auto_bind_containers"] = bool(result["canvas"].get("auto_bind_containers", True))
        result["canvas"]["background"] = str(result["canvas"].get("background") or "#0c1421")

        tabs = config.get("tabs") if isinstance(config.get("tabs"), list) else []
        normalized_tabs: list[dict[str, str]] = []
        seen_tabs: set[str] = set()
        for index, tab in enumerate(tabs):
            if not isinstance(tab, dict):
                continue
            tab_id = str(tab.get("id") or f"tab-{index + 1}").strip()
            if not tab_id or tab_id in seen_tabs or tab_id == "*":
                continue
            seen_tabs.add(tab_id)
            normalized_tabs.append({"id": tab_id, "label": str(tab.get("label") or tab_id)})
        result["tabs"] = normalized_tabs or deepcopy(DEFAULT_CONFIG["tabs"])

        components = config.get("components") if isinstance(config.get("components"), list) else []
        normalized_components: list[dict[str, Any]] = []
        for index, component in enumerate(components):
            if not isinstance(component, dict):
                continue
            item = deepcopy(component)
            item["id"] = str(item.get("id") or f"component-{index + 1}")
            item["type"] = str(item.get("type") or "text")
            item["title"] = str(item.get("title") or item["type"])
            for key, fallback in (("x", 20), ("y", 20), ("w", 240), ("h", 80), ("z", index + 1)):
                try:
                    item[key] = int(float(item.get(key, fallback)))
                except (TypeError, ValueError):
                    item[key] = fallback
            item["x"] = max(0, min(result["canvas"]["width"] - 20, item["x"]))
            item["y"] = max(0, min(result["canvas"]["height"] - 20, item["y"]))
            item["w"] = max(40, min(result["canvas"]["width"], item["w"]))
            item["h"] = max(28, min(result["canvas"]["height"], item["h"]))
            item["z"] = max(0, item["z"])
            component_tabs = item.get("tabs") if isinstance(item.get("tabs"), list) else ["*"]
            item["tabs"] = [str(tab) for tab in component_tabs if str(tab)] or ["*"]
            item["props"] = item.get("props") if isinstance(item.get("props"), dict) else {}
            item["style"] = item.get("style") if isinstance(item.get("style"), dict) else {}
            item["locked"] = bool(item.get("locked", False))
            item["hidden"] = bool(item.get("hidden", False))
            parent_id = item.get("parent_id")
            item["parent_id"] = str(parent_id) if parent_id else None
            action_id = str(item.get("action_id") or f"{item['type']}_{index + 1}").strip()
            action_id = re.sub(r"[^a-zA-Z0-9_.:-]+", "_", action_id).strip("_") or f"component_{index + 1}"
            item["action_id"] = action_id
            item["interaction_mode"] = str(item.get("interaction_mode") or "event_only")
            item["initial_state"] = bool(item.get("initial_state", False))
            raw_shortcuts = item.get("shortcuts") if isinstance(item.get("shortcuts"), list) else []
            if UIBuilderManager._is_hockey_config(config):
                raw_shortcuts = [
                    shortcut for shortcut in raw_shortcuts
                    if not (
                        isinstance(shortcut, dict)
                        and UIBuilderManager._legacy_component_shortcut_combo(shortcut.get("combo"))
                    )
                ]
            normalized_shortcuts: list[dict[str, Any]] = []
            for shortcut_index, shortcut in enumerate(raw_shortcuts):
                if not isinstance(shortcut, dict):
                    continue
                combo = str(shortcut.get("combo") or "").strip()
                normalized_shortcuts.append({
                    "id": str(shortcut.get("id") or f"shortcut-{shortcut_index + 1}"),
                    "enabled": bool(shortcut.get("enabled", True)),
                    "combo": combo,
                    "event": str(shortcut.get("event") or "click"),
                    "item_id": str(shortcut.get("item_id") or ""),
                    "value": shortcut.get("value", ""),
                    "prevent_default": bool(shortcut.get("prevent_default", True)),
                    "allow_in_inputs": bool(shortcut.get("allow_in_inputs", False)),
                    "global": bool(shortcut.get("global", False)),
                    "scope": "all" if shortcut.get("scope") == "all" else "runtime",
                })
            item["shortcuts"] = normalized_shortcuts
            normalized_components.append(item)

        valid_ids = {item["id"] for item in normalized_components}
        by_id = {item["id"]: item for item in normalized_components}
        for item in normalized_components:
            if item["parent_id"] not in valid_ids or item["parent_id"] == item["id"]:
                item["parent_id"] = None
                continue
            seen = {item["id"]}
            parent_id = item["parent_id"]
            while parent_id:
                if parent_id in seen:
                    item["parent_id"] = None
                    break
                seen.add(parent_id)
                parent_id = by_id.get(parent_id, {}).get("parent_id")

        used_action_ids: set[str] = set()
        for index, item in enumerate(normalized_components):
            base = item["action_id"]
            candidate = base
            suffix = 2
            while candidate in used_action_ids:
                candidate = f"{base}_{suffix}"
                suffix += 1
            item["action_id"] = candidate
            used_action_ids.add(candidate)

        triggers = config.get("triggers") if isinstance(config.get("triggers"), list) else []
        normalized_triggers: list[dict[str, Any]] = []
        for index, trigger in enumerate(triggers):
            if not isinstance(trigger, dict):
                continue
            action = trigger.get("action") if isinstance(trigger.get("action"), dict) else {}
            condition = trigger.get("condition") if isinstance(trigger.get("condition"), dict) else {}
            normalized_triggers.append({
                "id": str(trigger.get("id") or f"trigger-{index + 1}"),
                "name": str(trigger.get("name") or f"Триггер {index + 1}"),
                "enabled": bool(trigger.get("enabled", True)),
                "source_action_id": str(trigger.get("source_action_id") or ""),
                "event": str(trigger.get("event") or "click"),
                "item_id": str(trigger.get("item_id") or ""),
                "condition": {
                    "field": str(condition.get("field") or ""),
                    "operator": str(condition.get("operator") or "equals"),
                    "value": condition.get("value", ""),
                },
                "action": {
                    "type": str(action.get("type") or "show_message"),
                    "target_action_id": str(action.get("target_action_id") or ""),
                    "state_key": str(action.get("state_key") or "active"),
                    "value": action.get("value", "true"),
                    "function_name": str(action.get("function_name") or ""),
                    "event_name": str(action.get("event_name") or "ui-builder:custom"),
                    "message": str(action.get("message") or "Триггер выполнен"),
                    "tab_id": str(action.get("tab_id") or ""),
                    "url": str(action.get("url") or ""),
                    "method": str(action.get("method") or "POST").upper(),
                    "body": str(action.get("body") or ""),
                    "timer_command": str(action.get("timer_command") or "toggle"),
                    "timer_value": action.get("timer_value", ""),
                    "sequence_id": str(action.get("sequence_id") or ""),
                },
            })

        shortcut_sequences = config.get("shortcut_sequences") if isinstance(config.get("shortcut_sequences"), list) else []
        normalized_shortcut_sequences: list[dict[str, Any]] = []
        valid_conditions = {
            "always", "has_penalties", "no_penalties", "has_home_penalties",
            "has_away_penalties", "has_both_penalties", "no_home_penalties", "no_away_penalties",
            "home_delayed_penalty", "away_delayed_penalty", "any_delayed_penalty",
            "home_empty_net", "away_empty_net", "any_empty_net",
            "prematch_button_active", "prematch_button_inactive",
            "active_tab", "inactive_tab",
        }
        valid_step_types = {
            "timer_command", "hockey_penalties_command", "hockey_score_command", "vmix_command",
            "hockey_vmix_timers_start", "delay", "dispatch_event",
        }
        for sequence_index, sequence in enumerate(shortcut_sequences):
            if not isinstance(sequence, dict):
                continue
            raw_steps = sequence.get("steps") if isinstance(sequence.get("steps"), list) else []
            steps: list[dict[str, Any]] = []
            for step_index, step in enumerate(raw_steps):
                if not isinstance(step, dict):
                    continue
                step_type = str(step.get("type") or "vmix_command")
                if step_type not in valid_step_types:
                    step_type = "vmix_command"
                condition = str(step.get("condition") or "always")
                if condition not in valid_conditions:
                    condition = "always"
                try:
                    milliseconds = int(float(step.get("milliseconds") or 0))
                except (TypeError, ValueError):
                    milliseconds = 0
                milliseconds = max(0, min(10000, milliseconds))

                def _normalize_penalty_targets(raw_targets: Any, legacy_inputs: Any, legacy_names: Any) -> list[dict[str, str]]:
                    targets: list[dict[str, Any]] = []
                    if isinstance(raw_targets, list):
                        targets = [item for item in raw_targets if isinstance(item, dict)]
                    else:
                        inputs = [item.strip() for item in str(legacy_inputs or "").replace(";", ",").split(",") if item.strip()]
                        names = [item.strip() for item in str(legacy_names or "").replace(";", ",").split(",") if item.strip()]
                        targets = [{"input": input_ref, "selected_name": names[index] if index < len(names) else ""} for index, input_ref in enumerate(inputs)]
                    return [{
                        "id": str(item.get("id") or f"penalty-target-{index + 1}"),
                        "input": str(item.get("input") or ""),
                        "selected_name": str(item.get("selected_name") or item.get("selectedName") or ""),
                        "overlay": str(item.get("overlay") or "2") if str(item.get("overlay") or "2") in {"1", "2", "3", "4", "5", "6", "7", "8"} else "2",
                        "auto_hide_on_finish": bool(item.get("auto_hide_on_finish", True)),
                    } for index, item in enumerate(targets[:8])]

                def _normalize_finish_actions(raw_actions: Any) -> list[dict[str, Any]]:
                    if not isinstance(raw_actions, list):
                        return []
                    result_actions: list[dict[str, Any]] = []
                    for action_index, action in enumerate(raw_actions[:12]):
                        if not isinstance(action, dict):
                            continue
                        source = str(action.get("source") or "game")
                        if source not in {"game", "any_penalty", "home_penalty", "away_penalty"}:
                            source = "game"
                        overlay = str(action.get("overlay") or "1")
                        if overlay not in {"1", "2", "3", "4", "5", "6", "7", "8"}:
                            overlay = "1"
                        try:
                            duration_ms = int(float(action.get("duration_ms") or 3000))
                        except (TypeError, ValueError):
                            duration_ms = 3000
                        result_actions.append({
                            "id": str(action.get("id") or f"finish-action-{action_index + 1}"),
                            "enabled": bool(action.get("enabled", True)),
                            "source": source,
                            "input": str(action.get("input") or ""),
                            "overlay": overlay,
                            "duration_ms": max(100, min(120000, duration_ms)),
                            "only_when_side_clear": bool(action.get("only_when_side_clear", source in {"any_penalty", "home_penalty", "away_penalty"})),
                        })
                    return result_actions

                home_penalty_targets = _normalize_penalty_targets(
                    step.get("home_penalty_targets"), step.get("home_penalty_inputs"), step.get("home_penalty_selected_names")
                )
                away_penalty_targets = _normalize_penalty_targets(
                    step.get("away_penalty_targets"), step.get("away_penalty_inputs"), step.get("away_penalty_selected_names")
                )
                timer_finish_actions = _normalize_finish_actions(step.get("timer_finish_actions"))

                steps.append({
                    "id": str(step.get("id") or f"step-{step_index + 1}"),
                    "enabled": bool(step.get("enabled", True)),
                    "type": step_type,
                    "label": str(step.get("label") or ""),
                    "condition": condition,
                    "condition_value": str(step.get("condition_value") or "")[:96],
                    "target_action_id": str(step.get("target_action_id") or ""),
                    "timer_command": str(step.get("timer_command") or "start"),
                    "timer_value": step.get("timer_value", ""),
                    "penalty_command": str(step.get("penalty_command") or "start"),
                    "score_command": str(step.get("score_command") or "home_plus") if str(step.get("score_command") or "home_plus") in {"home_plus", "home_minus", "away_plus", "away_minus", "sync"} else "home_plus",
                    "function": str(step.get("function") or ""),
                    "input": str(step.get("input") or ""),
                    "value": step.get("value", ""),
                    "selected_name": str(step.get("selected_name") or ""),
                    "duration": str(step.get("duration") or ""),
                    "mix": str(step.get("mix") or ""),
                    "use_scoreboard_alternate": bool(step.get("use_scoreboard_alternate", False)),
                    "scoreboard_alternate_input": str(step.get("scoreboard_alternate_input") or ""),
                    "scoreboard_alternate_selected_name": str(step.get("scoreboard_alternate_selected_name") or ""),
                    "game_timer_action_id": str(step.get("game_timer_action_id") or "hockey_game_timer"),
                    "hockey_timer_command": str(step.get("hockey_timer_command") or "toggle") if str(step.get("hockey_timer_command") or "toggle") in {"toggle", "start", "pause", "resume"} else "toggle",
                    # BUILD90: configs created before v22 used Text mirror as the default,
                    # which pushed timer text every second. Migrate those hockey timer
                    # sync steps once to native vMix countdown transport. From v22 onward
                    # an explicitly selected legacy Text mirror remains available.
                    "game_vmix_mode": (
                        "countdown" if source_version < 22 and step_type == "hockey_vmix_timers_start"
                        else (str(step.get("game_vmix_mode") or "countdown") if str(step.get("game_vmix_mode") or "countdown") in {"countdown", "text"} else "countdown")
                    ),
                    "penalty_vmix_mode": (
                        "countdown" if source_version < 22 and step_type == "hockey_vmix_timers_start"
                        else (str(step.get("penalty_vmix_mode") or "countdown") if str(step.get("penalty_vmix_mode") or "countdown") in {"countdown", "text"} else "countdown")
                    ),
                    "penalty_display_mode": "all" if str(step.get("penalty_display_mode") or "soonest") == "all" else "soonest",
                    "game_vmix_input": str(step.get("game_vmix_input") or ""),
                    "game_vmix_selected_name": str(step.get("game_vmix_selected_name") or ""),
                    "home_penalty_inputs": str(step.get("home_penalty_inputs") or ""),
                    "away_penalty_inputs": str(step.get("away_penalty_inputs") or ""),
                    "home_penalty_selected_names": str(step.get("home_penalty_selected_names") or ""),
                    "away_penalty_selected_names": str(step.get("away_penalty_selected_names") or ""),
                    "home_penalty_targets": home_penalty_targets,
                    "away_penalty_targets": away_penalty_targets,
                    "timer_finish_actions": timer_finish_actions,
                    "start_web_game": bool(step.get("start_web_game", True)),
                    "start_web_penalties": bool(step.get("start_web_penalties", True)),
                    "sync_vmix_game": bool(step.get("sync_vmix_game", True)),
                    "sync_vmix_penalties": bool(step.get("sync_vmix_penalties", True)),
                    "milliseconds": milliseconds,
                    "event_name": str(step.get("event_name") or "ui-builder:shortcut-sequence"),
                })
            normalized_shortcut_sequences.append({
                "id": str(sequence.get("id") or f"sequence-{sequence_index + 1}"),
                "name": str(sequence.get("name") or f"Шорткат {sequence_index + 1}"),
                "description": str(sequence.get("description") or ""),
                "enabled": bool(sequence.get("enabled", True)),
                "combo": str(sequence.get("combo") or "").strip(),
                "prevent_default": bool(sequence.get("prevent_default", True)),
                "allow_in_inputs": bool(sequence.get("allow_in_inputs", False)),
                "toggle_all_overlays_on_repeat": bool(sequence.get("toggle_all_overlays_on_repeat", False)),
                "repeat_overlay_outs": [
                    value for value in dict.fromkeys(
                        int(item) for item in (sequence.get("repeat_overlay_outs") if isinstance(sequence.get("repeat_overlay_outs"), list) else [1, 2, 3])
                        if str(item).isdigit() and 1 <= int(item) <= 8
                    )
                ] or [1, 2, 3],
                "sync_hockey_team_states": bool(sequence.get("sync_hockey_team_states", sequence.get("toggle_all_overlays_on_repeat", False))),
                "is_scoreboard_sequence": bool(sequence.get("is_scoreboard_sequence", sequence.get("sync_hockey_team_states", sequence.get("toggle_all_overlays_on_repeat", False)))),
                "scope": "all" if sequence.get("scope") == "all" else "runtime",
                "steps": steps,
            })

        raw_prematch_groups = config.get("prematch_groups") if isinstance(config.get("prematch_groups"), list) else []
        normalized_prematch_groups: list[dict[str, Any]] = []
        seen_group_ids: set[str] = set()
        for group_index, group in enumerate(raw_prematch_groups[:24]):
            if not isinstance(group, dict):
                continue
            raw_id = re.sub(r"[^A-Za-z0-9_-]+", "_", str(group.get("id") or f"group_{group_index + 1}").strip())[:48].strip("_")
            group_id = raw_id or f"group_{group_index + 1}"
            suffix = 2
            base_id = group_id
            while group_id in seen_group_ids:
                group_id = f"{base_id}_{suffix}"[:48]
                suffix += 1
            seen_group_ids.add(group_id)
            normalized_prematch_groups.append({
                "id": group_id,
                "label": str(group.get("label") or f"Вкладка {group_index + 1}")[:80],
                "sort_order": int(group.get("sort_order") if str(group.get("sort_order", "")).lstrip("-").isdigit() else group_index * 10),
                "enabled": bool(group.get("enabled", True)),
            })

        raw_prematch_buttons = config.get("prematch_buttons") if isinstance(config.get("prematch_buttons"), list) else []
        normalized_prematch_buttons: list[dict[str, Any]] = []
        seen_prematch_ids: set[str] = set()
        for button_index, button in enumerate(raw_prematch_buttons[:64]):
            if not isinstance(button, dict):
                continue
            raw_id = re.sub(r"[^A-Za-z0-9_-]+", "_", str(button.get("id") or f"prematch_{button_index + 1}").strip())[:48].strip("_")
            button_id = raw_id or f"prematch_{button_index + 1}"
            suffix = 2
            base_id = button_id
            while button_id in seen_prematch_ids:
                button_id = f"{base_id}_{suffix}"[:48]
                suffix += 1
            seen_prematch_ids.add(button_id)
            mode = str(button.get("mode") or "action").lower()
            if mode not in {"action", "toggle"}:
                mode = "action"
            normalized_prematch_buttons.append({
                "id": button_id,
                "label": str(button.get("label") or f"Кнопка {button_index + 1}")[:80],
                "description": str(button.get("description") or "")[:300],
                "mode": mode,
                "sequence_id": str(button.get("sequence_id") or "")[:128],
                "group_id": str(button.get("group_id") or "")[:48] if str(button.get("group_id") or "") in seen_group_ids else "",
                "sort_order": int(button.get("sort_order") if str(button.get("sort_order", "")).lstrip("-").isdigit() else button_index * 10),
                "enabled": bool(button.get("enabled", True)),
            })

        raw_quick_panel_selectors = config.get("quick_panel_selectors") if isinstance(config.get("quick_panel_selectors"), list) else []
        normalized_quick_panel_selectors: list[dict[str, Any]] = []
        seen_selector_ids: set[str] = set()
        valid_button_ids = {item["id"] for item in normalized_prematch_buttons}
        valid_group_ids = {item["id"] for item in normalized_prematch_groups}
        for selector_index, selector in enumerate(raw_quick_panel_selectors[:64]):
            if not isinstance(selector, dict):
                continue
            raw_id = re.sub(r"[^A-Za-z0-9_-]+", "_", str(selector.get("id") or f"selector_{selector_index + 1}").strip())[:48].strip("_")
            selector_id = raw_id or f"selector_{selector_index + 1}"
            base_id = selector_id
            suffix = 2
            while selector_id in seen_selector_ids:
                selector_id = f"{base_id}_{suffix}"[:48]
                suffix += 1
            seen_selector_ids.add(selector_id)
            raw_options = selector.get("options") if isinstance(selector.get("options"), list) else []
            options: list[dict[str, str]] = []
            for option_index, option in enumerate(raw_options[:16]):
                if isinstance(option, dict):
                    value = str(option.get("value", option.get("id", option_index + 1)))[:64]
                    label = str(option.get("label", value))[:80]
                else:
                    value = str(option)[:64]
                    label = value[:80]
                if value:
                    options.append({"value": value, "label": label})
            if not options:
                options = [{"value": "1", "label": "1"}, {"value": "2", "label": "2"}, {"value": "3", "label": "3"}]
            default_value = str(selector.get("default_value") or options[0]["value"])[:64]
            if default_value not in {item["value"] for item in options}:
                default_value = options[0]["value"]
            group_id = str(selector.get("group_id") or "")[:48]
            button_id = str(selector.get("button_id") or "")[:48]
            normalized_quick_panel_selectors.append({
                "id": selector_id,
                "label": str(selector.get("label") or f"Переключатель {selector_index + 1}")[:80],
                "description": str(selector.get("description") or "")[:300],
                "group_id": group_id if group_id in valid_group_ids else "",
                "button_id": button_id if button_id in valid_button_ids else "",
                "style": str(selector.get("style") or "segments") if str(selector.get("style") or "segments") in {"segments", "select", "periods"} else "segments",
                "options": options,
                "default_value": default_value,
                "sort_order": int(selector.get("sort_order") if str(selector.get("sort_order", "")).lstrip("-").isdigit() else selector_index * 10),
                "enabled": bool(selector.get("enabled", True)),
            })

        raw_player_selection_panels = config.get("player_selection_panels") if isinstance(config.get("player_selection_panels"), list) else None
        if raw_player_selection_panels is None:
            raw_player_selection_panels = deepcopy(DEFAULT_HOCKEY_PLAYER_SELECTION_PANELS) if ("player_selection_panels" not in config and UIBuilderManager._is_hockey_config(config)) else []
        normalized_player_selection_panels: list[dict[str, Any]] = []
        seen_player_panel_ids: set[str] = set()
        allowed_player_panel_rules = {"any", "home", "away", "different_teams"}
        for panel_index, panel in enumerate(raw_player_selection_panels[:16]):
            if not isinstance(panel, dict):
                continue
            raw_id = re.sub(r"[^A-Za-z0-9_]+", "_", str(panel.get("id") or f"players_{panel_index + 1}").strip())[:48].strip("_")
            panel_id = raw_id or f"players_{panel_index + 1}"
            if not re.match(r"^[A-Za-z]", panel_id):
                panel_id = f"p_{panel_id}"[:48]
            base_id = panel_id
            suffix = 2
            while panel_id in seen_player_panel_ids:
                panel_id = f"{base_id}_{suffix}"[:48]
                suffix += 1
            seen_player_panel_ids.add(panel_id)
            try:
                slots = max(1, min(6, int(panel.get("slots") or 1)))
            except (TypeError, ValueError):
                slots = 1
            rule = str(panel.get("rule") or "any").strip().lower()
            if rule not in allowed_player_panel_rules:
                rule = "any"
            normalized_player_selection_panels.append({
                "id": panel_id,
                "label": str(panel.get("label") or f"Игроки {panel_index + 1}")[:80],
                "description": str(panel.get("description") or "")[:300],
                "slots": slots,
                "rule": rule,
                "sync_selected_player": bool(panel.get("sync_selected_player", False)),
                "collapsed_default": bool(panel.get("collapsed_default", True)),
                "sort_order": int(panel.get("sort_order") if str(panel.get("sort_order", "")).lstrip("-").isdigit() else panel_index * 10),
                "enabled": bool(panel.get("enabled", True)),
            })

        result["components"] = normalized_components
        result["triggers"] = normalized_triggers
        result["shortcut_sequences"] = normalized_shortcut_sequences
        result["prematch_groups"] = sorted(normalized_prematch_groups, key=lambda item: (item["sort_order"], item["label"]))
        result["prematch_buttons"] = sorted(normalized_prematch_buttons, key=lambda item: (item.get("group_id", ""), item["sort_order"], item["label"]))
        result["quick_panel_selectors"] = sorted(normalized_quick_panel_selectors, key=lambda item: (item.get("group_id", ""), item["sort_order"], item["label"]))
        result["player_selection_panels"] = sorted(normalized_player_selection_panels, key=lambda item: (item["sort_order"], item["label"]))
        return result
