from __future__ import annotations

import asyncio
from datetime import date
from time import perf_counter
from typing import Any, Callable

from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import text

from .auth_bridge import HockeyUser
from .service import HockeyDataService
from .vmix_portable import HockeyVmixPortable


class PublicSettingsPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    base_url: str | None = None
    auth_mode: str | None = None
    ui_language: str | None = None
    fallback_language: str | None = None
    store_ru: bool | None = None
    store_en: bool | None = None
    sync_interval_seconds: int | None = None
    request_timeout_seconds: int | None = None
    verify_ssl: bool | None = None
    timezone: str | None = None
    auto_sync_on_startup: bool | None = None
    games_endpoint_template: str | None = None
    games_auto_discovery: bool | None = None
    games_cache_seconds: int | None = None
    players_endpoint_template: str | None = None
    player_countries_endpoint_template: str | None = None
    referees_endpoint_template: str | None = None
    match_json_endpoint_template: str | None = None
    match_json_en_endpoint_template: str | None = None
    shots_endpoint_template: str | None = None
    match_directories_cache_seconds: int | None = None
    standings_endpoint_template: str | None = None
    standings_cache_seconds: int | None = None
    powerplay_endpoint_template: str | None = None
    rank_endpoint_template: str | None = None
    tournament_statistics_cache_seconds: int | None = None
    optional_resource_negative_cache_seconds: int | None = None

    timer_period_minutes: int | None = None
    timer_regular_overtime_minutes: int | None = None
    timer_playoff_overtime_minutes: int | None = None
    timer_reset_on_period_change: bool | None = None
    strength_regulation_skaters: int | None = None
    strength_regular_overtime_skaters: int | None = None
    strength_playoff_overtime_skaters: int | None = None
    strength_min_skaters: int | None = None
    strength_regulation_display: str | None = None
    strength_regular_overtime_display: str | None = None
    strength_playoff_overtime_display: str | None = None
    strength_powerplay_label: str | None = None
    strength_shorthanded_label: str | None = None
    strength_show_shorthanded: bool | None = None
    strength_state_labels: dict[str, Any] | None = None
    period_status_labels: dict[str, Any] | None = None
    scoreboard_team_states: dict[str, Any] | None = None
    score_external_home_key: str | None = None
    score_external_away_key: str | None = None


class CredentialsPayload(BaseModel):
    username: str | None = None
    password: str | None = Field(default=None, max_length=1024)
    clear_password: bool = False


class PreferencePayload(BaseModel):
    display_language: str = "ru"
    vmix_language: str = "ru"
    tournament_external_id: str = ""


class TriggerPreferencePayload(BaseModel):
    enabled: bool = True


class SessionPayload(BaseModel):
    tournament_external_id: str = Field(min_length=1, max_length=64)
    game_external_id: str = Field(default="", max_length=64)
    display_language: str = "ru"
    vmix_language: str = "ru"
    vmix_device_id: str = Field(default="", max_length=128)
    replace_existing: bool = True


class GamePeriodPayload(BaseModel):
    current_period: str = Field(min_length=1, max_length=16)
    language: str = Field(default="ru", pattern="^(ru|en)$")


class GameTimersPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    main_timer: dict[str, Any] = Field(default_factory=dict)
    penalty_board: dict[str, Any] = Field(default_factory=dict)
    language: str = Field(default="ru", pattern="^(ru|en)$")


class GameFlagsPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    flags: dict[str, bool] = Field(default_factory=dict)
    language: str = Field(default="ru", pattern="^(ru|en)$")


class GameValuesPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    values: dict[str, Any] = Field(default_factory=dict)
    language: str = Field(default="ru", pattern="^(ru|en)$")


class GameScorePayload(BaseModel):
    command: str = Field(pattern="^(home_plus|home_minus|away_plus|away_minus|sync)$")
    language: str = Field(default="ru", pattern="^(ru|en)$")


class GameLineAssignmentPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    player_external_id: str = Field(min_length=1, max_length=64)
    line_number: str = Field(default="", max_length=16)
    sort_order: int = Field(default=0, ge=0, le=999)


class GameLinesPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    side: str = Field(pattern="^(home|away)$")
    assignments: list[GameLineAssignmentPayload] = Field(default_factory=list)
    reset: bool = False
    language: str = Field(default="ru", pattern="^(ru|en)$")


class ShootoutSetupPayload(BaseModel):
    initial_attempts: int = Field(default=3)
    reset: bool = False
    language: str = Field(default="ru", pattern="^(ru|en)$")


class ShootoutAttemptPayload(BaseModel):
    side: str = Field(pattern="^(home|away)$")
    player_external_id: str = Field(min_length=1, max_length=64)
    scored: bool
    language: str = Field(default="ru", pattern="^(ru|en)$")


class TeamDirectoryPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    external_id: str = Field(default="", max_length=64)
    league_key: str = Field(default="", max_length=32)
    tournament_external_id: str = Field(default="", max_length=64)
    name_ru: str = Field(default="", max_length=500)
    name_en: str = Field(default="", max_length=500)
    short_name_ru: str = Field(default="", max_length=255)
    short_name_en: str = Field(default="", max_length=255)
    city_ru: str = Field(default="", max_length=255)
    city_en: str = Field(default="", max_length=255)
    color_hex: str = Field(default="", max_length=9)
    logo_url: str = Field(default="", max_length=2000)
    active: bool = True


class CountryDirectoryPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    external_id: str = Field(default="", max_length=64)
    iso2: str = Field(default="", max_length=2)
    iso3: str = Field(default="", max_length=3)
    name_ru: str = Field(default="", max_length=255)
    name_en: str = Field(default="", max_length=255)
    active: bool = True


class PlayerDirectoryPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    external_id: str = Field(default="", max_length=64)
    tournament_external_id: str = Field(default="", max_length=64)
    first_name_ru: str = Field(default="", max_length=255)
    first_name_en: str = Field(default="", max_length=255)
    last_name_ru: str = Field(default="", max_length=255)
    last_name_en: str = Field(default="", max_length=255)
    full_name_ru: str = Field(default="", max_length=500)
    full_name_en: str = Field(default="", max_length=500)
    position_ru: str = Field(default="", max_length=128)
    position_en: str = Field(default="", max_length=128)
    birth_date: str = Field(default="", max_length=32)
    height_cm: int = Field(default=0, ge=0, le=300)
    weight_kg: int = Field(default=0, ge=0, le=300)
    stick: str = Field(default="", max_length=32)
    country_external_id: str = Field(default="", max_length=64)
    country_code: str = Field(default="", max_length=8)
    photo_url: str = Field(default="", max_length=2000)
    club_external_id: str = Field(default="", max_length=64)
    team_entry_external_id: str = Field(default="", max_length=64)
    team_name_ru: str = Field(default="", max_length=255)
    team_name_en: str = Field(default="", max_length=255)
    jersey_number: str = Field(default="", max_length=16)
    active: bool = True


class RefereeDirectoryPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    external_id: str = Field(default="", max_length=64)
    first_name_ru: str = Field(default="", max_length=255)
    first_name_en: str = Field(default="", max_length=255)
    last_name_ru: str = Field(default="", max_length=255)
    last_name_en: str = Field(default="", max_length=255)
    full_name_ru: str = Field(default="", max_length=500)
    full_name_en: str = Field(default="", max_length=500)
    position_code: str = Field(default="", max_length=32)
    number: str = Field(default="", max_length=16)
    birth_date: str = Field(default="", max_length=32)
    country_ru: str = Field(default="", max_length=255)
    country_en: str = Field(default="", max_length=255)
    country_code: str = Field(default="", max_length=8)
    town_ru: str = Field(default="", max_length=255)
    town_en: str = Field(default="", max_length=255)
    active: bool = True


class CoachDirectoryPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    external_id: str = Field(default="", max_length=64)
    tournament_external_id: str = Field(default="", max_length=64)
    first_name_ru: str = Field(default="", max_length=255)
    first_name_en: str = Field(default="", max_length=255)
    last_name_ru: str = Field(default="", max_length=255)
    last_name_en: str = Field(default="", max_length=255)
    full_name_ru: str = Field(default="", max_length=500)
    full_name_en: str = Field(default="", max_length=500)
    birth_date: str = Field(default="", max_length=32)
    country_external_id: str = Field(default="", max_length=64)
    country_code: str = Field(default="", max_length=8)
    photo_url: str = Field(default="", max_length=2000)
    team_external_id: str = Field(default="", max_length=64)
    team_name_ru: str = Field(default="", max_length=255)
    team_name_en: str = Field(default="", max_length=255)
    role_ru: str = Field(default="Главный тренер", max_length=128)
    role_en: str = Field(default="Head coach", max_length=128)
    active: bool = True


class PenaltyDirectoryPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    code: str = Field(min_length=1, max_length=64)
    name_ru: str = Field(min_length=1, max_length=500)
    name_en: str = Field(min_length=1, max_length=500)
    default_preset: str = Field(default="2", max_length=32)
    team_penalty: bool = False
    sort_order: int | None = Field(default=None, ge=0, le=100000)
    active: bool = True


def create_hockey_router(
    service: HockeyDataService,
    *,
    auth_dependency: Callable[..., Any] | None = None,
    admin_dependency: Callable[..., Any] | None = None,
    agent_hub: Any | None = None,
) -> APIRouter:
    authenticated = [Depends(auth_dependency)] if auth_dependency else []
    router = APIRouter(
        prefix="/api/hockey",
        tags=["Hockey / Stat2TV"],
        dependencies=authenticated,
    )
    admin = [Depends(admin_dependency)] if admin_dependency else []

    @router.get("/me")
    async def current_user(
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        return {
            "id": user.id,
            "login": user.login,
            "display_name": user.display_name,
            "role": user.role,
            "is_admin": user.is_admin,
        }

    @router.get("/vmix-channel")
    async def current_vmix_channel(
        request: Request,
        language: str = Query("ru", pattern="^(ru|en|bilingual)$"),
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        channel = service.get_user_vmix_channel(user.id)
        return {
            "channel": channel,
            "language": language,
            "links": (
                service.vmix_links(
                    token=str(channel.get("channel_key") or channel.get("token")),
                    base_url=str(request.base_url).rstrip("/"),
                    language=language,
                )
                if channel is not None
                else {}
            ),
        }

    @router.get("/status")
    async def status() -> dict[str, Any]:
        return {
            "settings": service.settings.status(),
            "last_sync": service.last_sync(),
            "database": service.database.schema_status(),
        }

    @router.get("/settings")
    async def get_settings() -> dict[str, Any]:
        return service.settings.status()

    @router.get("/database/status")
    async def database_status() -> dict[str, Any]:
        return service.database.schema_status()

    @router.get("/database/statistics-status")
    async def database_statistics_status(
        tournament_id: str = Query("", max_length=64),
    ) -> dict[str, Any]:
        return service.normalized_storage_status(
            tournament_external_id=tournament_id
        )

    @router.post("/database/statistics-backfill", dependencies=admin)
    async def database_statistics_backfill() -> dict[str, Any]:
        try:
            return service.backfill_normalized_statistics()
        except Exception as error:
            raise HTTPException(status_code=500, detail=str(error)) from error

    @router.post("/database/repair", dependencies=admin)
    async def repair_database() -> dict[str, Any]:
        try:
            return service.database.ensure_game_schema()
        except Exception as error:
            raise HTTPException(status_code=500, detail=str(error)) from error

    @router.put("/settings", dependencies=admin)
    async def update_settings(payload: PublicSettingsPayload) -> dict[str, Any]:
        values = {
            key: value
            for key, value in payload.model_dump().items()
            if value is not None
        }
        return service.settings.update_public(values)

    @router.put("/credentials", dependencies=admin)
    async def update_credentials(payload: CredentialsPayload) -> dict[str, Any]:
        return service.settings.update_credentials(
            username=payload.username,
            password=payload.password,
            clear_password=payload.clear_password,
        )

    @router.post("/connection/test", dependencies=admin)
    async def test_connection() -> dict[str, Any]:
        try:
            return await service.test_connection()
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.get("/admin/connection-diagnostics", dependencies=admin)
    async def admin_connection_diagnostics(
        tournament_id: str = Query("", max_length=64),
        game_id: str = Query("", max_length=64),
        language: str = Query("ru", pattern="^(ru|en)$"),
        deep: bool = False,
    ) -> dict[str, Any]:
        """Admin-only health check for the dependencies used by match loading.

        The lightweight check never requires an Agent/vMix to open a match: the
        database is the only hard dependency for cached schedule selection, while
        Stat2TV is required only for remote enrichment/sync.  ``deep=true`` also
        exercises the selected match enrichment path and returns its warnings.
        """
        checks: dict[str, Any] = {}

        started = perf_counter()
        try:
            with service.database.engine.connect() as connection:
                connection.execute(text("SELECT 1"))
            checks["database"] = {
                "ok": True,
                "latency_ms": round((perf_counter() - started) * 1000, 1),
                "target": service.database.schema_status(),
            }
        except Exception as error:
            checks["database"] = {
                "ok": False,
                "latency_ms": round((perf_counter() - started) * 1000, 1),
                "error": str(error),
            }

        settings_status = service.settings.status()
        checks["settings"] = {
            "ok": bool(settings_status.get("base_url")),
            "base_url": settings_status.get("base_url", ""),
            "credentials_configured": bool(settings_status.get("credentials_configured")),
            "credentials_source": settings_status.get("credentials_source", "missing"),
            "username_masked": settings_status.get("username_masked", ""),
            "verify_ssl": bool(settings_status.get("verify_ssl")),
        }

        started = perf_counter()
        try:
            stat2tv = await asyncio.wait_for(service.test_connection(), timeout=12.0)
            checks["stat2tv"] = {
                "ok": True,
                "latency_ms": round((perf_counter() - started) * 1000, 1),
                "status_code": stat2tv.get("status_code"),
                "auth_mode": stat2tv.get("auth_mode"),
                "content_type": stat2tv.get("content_type"),
                "items_found": stat2tv.get("items_found", 0),
                "url": stat2tv.get("url", ""),
            }
        except Exception as error:
            checks["stat2tv"] = {
                "ok": False,
                "latency_ms": round((perf_counter() - started) * 1000, 1),
                "error": str(error),
            }

        if agent_hub is not None:
            try:
                devices_payload = await agent_hub.list_mapping_devices()
                devices = list(devices_payload.get("devices") or [])
                checks["agents"] = {
                    "ok": any(bool(item.get("online")) for item in devices),
                    "optional": True,
                    "online": sum(1 for item in devices if item.get("online")),
                    "vmix_connected": sum(1 for item in devices if item.get("online") and item.get("vmix_connected")),
                    "devices": [
                        {
                            "device_id": item.get("device_id", ""),
                            "name": item.get("name", ""),
                            "owner": item.get("owner", ""),
                            "online": bool(item.get("online")),
                            "vmix_connected": bool(item.get("vmix_connected")),
                            "vmix_version": item.get("vmix_version", ""),
                        }
                        for item in devices
                    ],
                }
            except Exception as error:
                checks["agents"] = {"ok": False, "optional": True, "error": str(error), "devices": []}
        else:
            checks["agents"] = {"ok": False, "optional": True, "error": "Agent hub недоступен", "devices": []}

        tournament_id = str(tournament_id or "").strip()
        game_id = str(game_id or "").strip()
        if tournament_id:
            tournament = service.tournament(tournament_id, language=language)
            endpoint = service.endpoint_diagnostics(tournament_external_id=tournament_id)
            checks["tournament"] = {
                "ok": tournament is not None,
                "tournament_id": tournament_id,
                "local_found": tournament is not None,
                "endpoint_diagnostics": endpoint,
            }

        if game_id:
            local_game = service.game(game_id, language=language)
            match_check: dict[str, Any] = {
                "ok": local_game is not None,
                "game_id": game_id,
                "tournament_id": tournament_id,
                "local_found": local_game is not None,
                "deep_requested": bool(deep),
            }
            if deep and tournament_id:
                started = perf_counter()
                try:
                    payload = await asyncio.wait_for(
                        service.sync_selected_game_details(
                            tournament_external_id=tournament_id,
                            game_external_id=game_id,
                            language=language,
                        ),
                        timeout=20.0,
                    )
                    details = payload.get("details") if isinstance(payload, dict) else None
                    match_check.update({
                        "ok": True,
                        "latency_ms": round((perf_counter() - started) * 1000, 1),
                        "degraded": bool(payload.get("degraded")) if isinstance(payload, dict) else False,
                        "details_sync_failed": bool(payload.get("details_sync_failed")) if isinstance(payload, dict) else False,
                        "warnings": list(payload.get("warnings") or []) if isinstance(payload, dict) else [],
                        "details_loaded": bool(details.get("loaded")) if isinstance(details, dict) else False,
                    })
                except Exception as error:
                    match_check.update({
                        "ok": False,
                        "latency_ms": round((perf_counter() - started) * 1000, 1),
                        "error": str(error),
                    })
            checks["match"] = match_check

        database_ok = bool(checks.get("database", {}).get("ok"))
        stat2tv_ok = bool(checks.get("stat2tv", {}).get("ok"))
        return {
            "ok": database_ok,
            "required_for_match_selection": {
                "ok": database_ok,
                "database": database_ok,
            },
            "required_for_remote_sync": {
                "ok": database_ok and stat2tv_ok,
                "database": database_ok,
                "stat2tv": stat2tv_ok,
            },
            "checks": checks,
        }

    @router.post("/sync/tournaments", dependencies=admin)
    async def sync_tournaments() -> dict[str, Any]:
        try:
            return await service.sync_tournaments()
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.post("/sync/tournaments/seed", dependencies=admin)
    async def import_seed() -> dict[str, Any]:
        if not service.seed_file.exists():
            raise HTTPException(status_code=404, detail="Seed XML не найден")
        return service.import_tournaments_file(service.seed_file, source="seed")

    @router.get("/directories/teams")
    async def directory_teams(
        language: str = Query("ru", pattern="^(ru|en)$"),
        tournament_external_id: str = Query("", max_length=64),
        league_key: str = Query("", max_length=32),
        include_inactive: bool = False,
    ) -> dict[str, Any]:
        return service.team_directory(
            language=language,
            tournament_external_id=tournament_external_id,
            league_key=league_key,
            include_inactive=include_inactive,
        )

    @router.post("/directories/teams/sync", dependencies=admin)
    async def sync_directory_teams(
        tournament_external_id: str = Query("", max_length=64),
    ) -> dict[str, Any]:
        return service.sync_team_directory(
            tournament_external_id=tournament_external_id,
        )

    @router.post("/directories/teams", dependencies=admin)
    async def create_directory_team(
        payload: TeamDirectoryPayload,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        try:
            return service.save_directory_team(
                payload.model_dump(),
                language=language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/directories/teams/{membership_id}", dependencies=admin)
    async def update_directory_team(
        membership_id: int,
        payload: TeamDirectoryPayload,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        try:
            return service.save_directory_team(
                payload.model_dump(),
                membership_id=membership_id,
                language=language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.get("/directories/countries")
    async def directory_countries(
        language: str = Query("ru", pattern="^(ru|en)$"),
        include_inactive: bool = False,
        search: str = Query("", max_length=200),
    ) -> dict[str, Any]:
        return service.country_directory(language=language, include_inactive=include_inactive, search=search)

    @router.post("/directories/countries/sync", dependencies=admin)
    async def sync_directory_countries() -> dict[str, Any]:
        try:
            return await service.sync_country_directory()
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.post("/directories/countries", dependencies=admin)
    async def create_directory_country(payload: CountryDirectoryPayload, language: str = Query("ru", pattern="^(ru|en)$")) -> dict[str, Any]:
        try:
            return service.save_country(payload.model_dump(), language=language)
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/directories/countries/{country_id}", dependencies=admin)
    async def update_directory_country(country_id: int, payload: CountryDirectoryPayload, language: str = Query("ru", pattern="^(ru|en)$")) -> dict[str, Any]:
        try:
            return service.save_country(payload.model_dump(), country_id=country_id, language=language)
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.get("/directories/players")
    async def directory_players(
        tournament_external_id: str = Query("", max_length=64),
        language: str = Query("ru", pattern="^(ru|en)$"),
        include_inactive: bool = False,
        search: str = Query("", max_length=200),
    ) -> dict[str, Any]:
        return service.player_directory(tournament_external_id=tournament_external_id, language=language, include_inactive=include_inactive, search=search)

    @router.post("/directories/players/sync", dependencies=admin)
    async def sync_directory_players(tournament_external_id: str = Query(..., min_length=1, max_length=64)) -> dict[str, Any]:
        try:
            return await service.sync_player_directory(tournament_external_id=tournament_external_id)
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.post("/directories/players", dependencies=admin)
    async def create_directory_player(payload: PlayerDirectoryPayload) -> dict[str, Any]:
        try:
            return service.save_player(payload.model_dump())
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/directories/players/{player_id}", dependencies=admin)
    async def update_directory_player(player_id: int, payload: PlayerDirectoryPayload) -> dict[str, Any]:
        try:
            return service.save_player(payload.model_dump(), player_id=player_id)
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.get("/directories/referees")
    async def directory_referees(
        tournament_external_id: str = Query("", max_length=64),
        language: str = Query("ru", pattern="^(ru|en)$"),
        include_inactive: bool = False,
        search: str = Query("", max_length=200),
    ) -> dict[str, Any]:
        return service.referee_directory(tournament_external_id=tournament_external_id, language=language, include_inactive=include_inactive, search=search)

    @router.post("/directories/referees/sync", dependencies=admin)
    async def sync_directory_referees(tournament_external_id: str = Query(..., min_length=1, max_length=64)) -> dict[str, Any]:
        try:
            return await service.sync_referee_directory(tournament_external_id=tournament_external_id)
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.post("/directories/referees", dependencies=admin)
    async def create_directory_referee(payload: RefereeDirectoryPayload) -> dict[str, Any]:
        try:
            return service.save_referee(payload.model_dump())
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/directories/referees/{referee_id}", dependencies=admin)
    async def update_directory_referee(referee_id: int, payload: RefereeDirectoryPayload) -> dict[str, Any]:
        try:
            return service.save_referee(payload.model_dump(), referee_id=referee_id)
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.get("/directories/coaches")
    async def directory_coaches(
        tournament_external_id: str = Query("", max_length=64),
        language: str = Query("ru", pattern="^(ru|en)$"),
        include_inactive: bool = False,
        search: str = Query("", max_length=200),
    ) -> dict[str, Any]:
        return service.coach_directory(tournament_external_id=tournament_external_id, language=language, include_inactive=include_inactive, search=search)

    @router.post("/directories/coaches/sync", dependencies=admin)
    async def sync_directory_coaches(tournament_external_id: str = Query(..., min_length=1, max_length=64)) -> dict[str, Any]:
        try:
            return await service.sync_coach_directory(tournament_external_id=tournament_external_id)
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.post("/directories/coaches", dependencies=admin)
    async def create_directory_coach(payload: CoachDirectoryPayload) -> dict[str, Any]:
        try:
            return service.save_coach(payload.model_dump())
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/directories/coaches/{coach_id}", dependencies=admin)
    async def update_directory_coach(coach_id: int, payload: CoachDirectoryPayload) -> dict[str, Any]:
        try:
            return service.save_coach(payload.model_dump(), coach_id=coach_id)
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.get("/directories/penalties")
    async def directory_penalties(
        language: str = Query("ru", pattern="^(ru|en)$"),
        include_inactive: bool = False,
    ) -> dict[str, Any]:
        return service.penalty_directory(
            language=language,
            include_inactive=include_inactive,
        )

    @router.post("/directories/penalties", dependencies=admin)
    async def create_directory_penalty(
        payload: PenaltyDirectoryPayload,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        try:
            return service.save_penalty_type(
                payload.model_dump(),
                language=language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/directories/penalties/{penalty_id}", dependencies=admin)
    async def update_directory_penalty(
        penalty_id: int,
        payload: PenaltyDirectoryPayload,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        try:
            return service.save_penalty_type(
                payload.model_dump(),
                penalty_id=penalty_id,
                language=language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.get("/navigation")
    async def navigation(
        language: str = Query("ru", pattern="^(ru|en)$"),
        on_date: date = Query(default_factory=date.today),
        search: str = Query("", max_length=200),
        league: str = Query("", max_length=32),
        season: str = Query("", max_length=32),
        stage: str = Query("", max_length=32),
        limit: int = Query(600, ge=1, le=2000),
    ) -> dict[str, Any]:
        return service.navigation(
            language=language,
            on_date=on_date,
            search=search,
            league=league,
            season=season,
            stage=stage,
            limit=limit,
        )


    @router.get("/games")
    async def games_by_date(
        on_date: date = Query(default_factory=date.today),
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        return service.games(
            on_date=on_date,
            language=language,
        )

    @router.get("/tournaments/{external_id}/games")
    async def tournament_games(
        external_id: str,
        on_date: date = Query(default_factory=date.today),
        language: str = Query("ru", pattern="^(ru|en)$"),
        all_dates: bool = False,
    ) -> dict[str, Any]:
        if service.tournament(external_id, language=language) is None:
            raise HTTPException(status_code=404, detail="Турнир не найден")
        return service.games(
            tournament_external_id=external_id,
            on_date=on_date,
            language=language,
            all_dates=all_dates,
        )

    @router.post("/tournaments/{external_id}/games/sync")
    async def sync_tournament_games(
        external_id: str,
        on_date: date = Query(default_factory=date.today),
        force_discovery: bool = False,
        lightweight: bool = False,
    ) -> dict[str, Any]:
        if service.tournament(external_id, language="ru") is None:
            raise HTTPException(status_code=404, detail="Турнир не найден")
        try:
            return await service.sync_games(
                tournament_external_id=external_id,
                on_date=on_date,
                force_discovery=force_discovery,
                lightweight=lightweight,
            )
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.get("/tournaments/{external_id}/standings")
    async def tournament_standings(
        external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
        game_id: str = Query("", max_length=64),
    ) -> dict[str, Any]:
        value = service.standings(
            external_id,
            language=language,
            game_external_id=game_id,
        )
        if value is None:
            raise HTTPException(status_code=404, detail="Турнирная таблица ещё не загружена")
        return value

    @router.post("/tournaments/{external_id}/standings/sync")
    async def sync_tournament_standings(
        external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
        game_id: str = Query("", max_length=64),
    ) -> dict[str, Any]:
        if service.tournament(external_id, language=language) is None:
            raise HTTPException(status_code=404, detail="Турнир не найден")
        try:
            return await service.sync_standings(
                external_id,
                language=language,
                game_external_id=game_id,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error


    @router.get("/tournaments/{external_id}/statistics")
    async def tournament_statistics(
        external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        if service.tournament(external_id, language=language) is None:
            raise HTTPException(status_code=404, detail="Турнир не найден")
        return service.tournament_statistics(external_id, language=language)

    @router.get("/tournaments/{external_id}/navigation-labels")
    async def tournament_navigation_labels(
        external_id: str,
        force: bool = False,
    ) -> dict[str, Any]:
        if service.tournament(external_id, language="ru") is None:
            raise HTTPException(status_code=404, detail="Турнир не найден")
        return await service.statistic_navigation_labels(external_id, force=force)

    @router.post("/tournaments/{external_id}/statistics/sync")
    async def sync_tournament_statistics(
        external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
        force: bool = False,
    ) -> dict[str, Any]:
        if service.tournament(external_id, language=language) is None:
            raise HTTPException(status_code=404, detail="Турнир не найден")
        try:
            return await service.sync_tournament_statistics(
                external_id, language=language, force=force
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.get(
        "/tournaments/{external_id}/games/diagnostics",
        dependencies=admin,
    )
    async def games_diagnostics(external_id: str) -> dict[str, Any]:
        return service.endpoint_diagnostics(
            tournament_external_id=external_id
        )

    @router.post("/games/{external_id}/live")
    async def refresh_selected_game(
        external_id: str,
        tournament_id: str = Query(..., min_length=1, max_length=64),
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        try:
            return await service.sync_live_game(
                tournament_external_id=tournament_id,
                game_external_id=external_id,
                language=language,
            )
        except ValueError as error:
            raise HTTPException(status_code=404, detail=str(error)) from error
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.post("/games/manual/{game_external_id}")
    async def sync_manual_game_global(
        game_external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        try:
            return await service.sync_game_by_id_global(
                game_external_id=game_external_id,
                language=language,
            )
        except ValueError as error:
            raise HTTPException(status_code=404, detail=str(error)) from error
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.post("/tournaments/{tournament_external_id}/games/manual/{game_external_id}")
    async def sync_manual_game(
        tournament_external_id: str,
        game_external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        if service.tournament(tournament_external_id, language=language) is None:
            raise HTTPException(status_code=404, detail="Турнир не найден")
        try:
            return await service.sync_game_by_id(
                tournament_external_id=tournament_external_id,
                game_external_id=game_external_id,
                language=language,
            )
        except ValueError as error:
            raise HTTPException(status_code=404, detail=str(error)) from error
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.post("/games/{external_id}/details/sync")
    async def sync_game_details(
        external_id: str,
        tournament_id: str = Query(..., min_length=1, max_length=64),
        language: str = Query("ru", pattern="^(ru|en)$"),
        opening: bool = False,
    ) -> dict[str, Any]:
        try:
            return await service.sync_selected_game_details(
                tournament_external_id=tournament_id,
                game_external_id=external_id,
                language=language,
                opening=opening,
            )
        except ValueError as error:
            raise HTTPException(status_code=404, detail=str(error)) from error
        except Exception as error:
            raise HTTPException(status_code=502, detail=str(error)) from error

    @router.get("/games/{external_id}/details")
    async def game_details(
        external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        value = service.game_details(external_id, language=language)
        if value is None:
            raise HTTPException(status_code=404, detail="Матч не найден")
        return value

    @router.get("/games/{external_id}/control")
    async def game_control(
        external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        value = service.game_control(external_id, language=language)
        if value is None:
            raise HTTPException(status_code=404, detail="Матч не найден")
        return value

    @router.put("/games/{external_id}/control/period")
    async def update_game_period(
        external_id: str,
        payload: GamePeriodPayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            return service.set_game_period(
                external_id,
                current_period=payload.current_period,
                updated_by=user.id,
                language=payload.language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/games/{external_id}/control/timers")
    async def update_game_timers(
        external_id: str,
        payload: GameTimersPayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            return service.save_game_timers(
                external_id,
                main_timer=payload.main_timer,
                penalty_board=payload.penalty_board,
                updated_by=user.id,
                language=payload.language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/games/{external_id}/control/flags")
    async def update_game_flags(
        external_id: str,
        payload: GameFlagsPayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            return service.set_game_flags(
                external_id,
                flags=payload.flags,
                updated_by=user.id,
                language=payload.language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/games/{external_id}/control/score")
    async def update_game_score(
        external_id: str,
        payload: GameScorePayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            return service.set_game_score(
                external_id,
                command=payload.command,
                updated_by=user.id,
                language=payload.language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/games/{external_id}/lines")
    async def update_game_lines(
        external_id: str,
        payload: GameLinesPayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            if payload.reset:
                return service.reset_game_lines(
                    external_id,
                    side=payload.side,
                    updated_by=user.id,
                    language=payload.language,
                )
            return service.save_game_lines(
                external_id,
                side=payload.side,
                assignments=[item.model_dump() for item in payload.assignments],
                updated_by=user.id,
                language=payload.language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/games/{external_id}/control/values")
    async def update_game_values(
        external_id: str,
        payload: GameValuesPayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            return service.set_game_values(
                external_id,
                values=payload.values,
                updated_by=user.id,
                language=payload.language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.put("/games/{external_id}/shootout/setup")
    async def setup_shootout(
        external_id: str,
        payload: ShootoutSetupPayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        if payload.initial_attempts not in {3, 5}:
            raise HTTPException(status_code=400, detail="Допустимо 3 или 5 буллитов")
        try:
            return service.configure_shootout(
                external_id,
                initial_attempts=payload.initial_attempts,
                reset=payload.reset,
                updated_by=user.id,
                language=payload.language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.post("/games/{external_id}/shootout/round")
    async def add_shootout_round(
        external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            return service.add_shootout_round(
                external_id,
                updated_by=user.id,
                language=language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.post("/games/{external_id}/shootout/attempts")
    async def add_shootout_attempt(
        external_id: str,
        payload: ShootoutAttemptPayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            return service.add_shootout_attempt(
                external_id,
                side=payload.side,
                player_external_id=payload.player_external_id,
                scored=payload.scored,
                created_by=user.id,
                language=payload.language,
            )
        except ValueError as error:
            raise HTTPException(status_code=400, detail=str(error)) from error

    @router.delete("/games/{external_id}/shootout/attempts/{attempt_id}")
    async def delete_shootout_attempt(
        external_id: str,
        attempt_id: int,
        language: str = Query("ru", pattern="^(ru|en)$"),
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            return service.delete_shootout_attempt(
                external_id,
                attempt_id,
                updated_by=user.id,
                language=language,
            )
        except ValueError as error:
            raise HTTPException(status_code=404, detail=str(error)) from error

    @router.get("/games/{external_id}")
    async def game(
        external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        value = service.game(external_id, language=language)
        if value is None:
            raise HTTPException(status_code=404, detail="Матч не найден")
        return value

    @router.get("/tournaments/{external_id}")
    async def tournament(
        external_id: str,
        language: str = Query("ru", pattern="^(ru|en)$"),
    ) -> dict[str, Any]:
        value = service.tournament(external_id, language=language)
        if value is None:
            raise HTTPException(status_code=404, detail="Турнир не найден")
        return value

    @router.get("/preferences")
    async def user_preference(
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        return service.user_preference(wfl_user_id=user.id)

    @router.put("/preferences")
    async def save_preference(
        payload: PreferencePayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        return service.save_preference(
            wfl_user_id=user.id,
            **payload.model_dump(),
        )

    @router.put("/preferences/toasts")
    async def save_toasts_preference(
        payload: TriggerPreferencePayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        return service.save_toasts_preference(
            wfl_user_id=user.id,
            enabled=payload.enabled,
        )

    @router.put("/preferences/events")
    async def save_events_preference(
        payload: TriggerPreferencePayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        return service.save_toasts_preference(
            wfl_user_id=user.id,
            enabled=payload.enabled,
        )

    @router.post("/sessions")
    async def open_session(
        payload: SessionPayload,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        try:
            session_data = payload.model_dump()
            vmix_device_id = str(session_data.pop("vmix_device_id", "") or "").strip()
            result = service.open_session(
                wfl_user_id=user.id,
                login_snapshot=user.login,
                display_name_snapshot=user.display_name,
                vmix_device_uuid=vmix_device_id,
                **session_data,
            )
            if agent_hub is not None and result.get("game_external_id"):
                try:
                    result["agent_assignment"] = await agent_hub.assign_match(
                        wfl_user_id=user.id,
                        game_external_id=str(result.get("game_external_id") or ""),
                        tournament_external_id=str(result.get("tournament_external_id") or ""),
                        device_id=vmix_device_id,
                        operator_session_token=str(result.get("token") or ""),
                        # BUILD111: session opening sends only match.assign. Mapping
                        # is applied after the selected match roster/details are in
                        # the database, otherwise vMix can receive stale match data.
                        wait_for_mapping=False,
                    )
                    assignment = result.get("agent_assignment") or {}
                    result["agent_status"] = (
                        "delivered" if assignment.get("delivered")
                        else ("offline" if assignment.get("device_id") else "unassigned")
                    )
                except Exception as agent_error:
                    # The match/session must remain usable even when a browser
                    # carries a stale Agent id from another WFL account.
                    result["agent_assignment"] = None
                    result["agent_warning"] = str(agent_error)[:300]
            return result
        except ValueError as error:
            raise HTTPException(status_code=409, detail=str(error)) from error

    @router.get("/sessions/{token}")
    async def get_session(
        token: str,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        value = service.get_session(token)
        if value is None or value["wfl_user_id"] != user.id:
            raise HTTPException(status_code=404, detail="Сессия не найдена")
        return value

    @router.delete("/sessions/{token}")
    async def close_session(
        token: str,
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        if not service.close_session(token, wfl_user_id=user.id):
            raise HTTPException(status_code=404, detail="Сессия не найдена")
        return {"ok": True}

    @router.get("/sessions/{token}/vmix-links")
    async def vmix_links(
        token: str,
        request: Request,
        language: str = Query("ru", pattern="^(ru|en|bilingual)$"),
        user: HockeyUser = Depends(auth_dependency),
    ) -> dict[str, Any]:
        operator_session = service.get_session(token)
        if operator_session is None or operator_session["wfl_user_id"] != user.id:
            raise HTTPException(status_code=404, detail="Сессия не найдена")
        return {
            "token": token,  # legacy field
            "channel_key": token,
            "channel_owner": operator_session.get("channel_owner", ""),
            "language": language,
            "links": service.vmix_links(
                token=token,
                base_url=str(request.base_url).rstrip("/"),
                language=language,
            ),
        }

    return router


def create_hockey_vmix_router(
    service: HockeyDataService,
    portable: HockeyVmixPortable | None = None,
) -> APIRouter:
    portable = portable or HockeyVmixPortable(service)
    router = APIRouter(tags=["Hockey vMix JSON"])

    def translate_error(error: Exception) -> HTTPException:
        if isinstance(error, KeyError):
            return HTTPException(status_code=404, detail=str(error).strip("'"))
        return HTTPException(status_code=400, detail=str(error))

    @router.get("/vmix/hockey/session/{token}/{language}/info.json", include_in_schema=False)
    @router.get("/vmix/hockey/channel/{token}/{language}/info.json")
    async def info(token: str, language: str) -> dict[str, Any]:
        try:
            return portable.info_payload(token, language)
        except (KeyError, ValueError) as error:
            raise translate_error(error) from error

    @router.get("/vmix/hockey/session/{token}/{language}/scoreboard.json", include_in_schema=False)
    @router.get("/vmix/hockey/channel/{token}/{language}/scoreboard.json")
    async def scoreboard(token: str, language: str) -> dict[str, Any]:
        try:
            return portable.scoreboard_payload(token, language)
        except (KeyError, ValueError) as error:
            raise translate_error(error) from error

    @router.get("/vmix/hockey/session/{token}/{language}/home-lineup.json", include_in_schema=False)
    @router.get("/vmix/hockey/channel/{token}/{language}/home-lineup.json")
    async def home_lineup(token: str, language: str) -> dict[str, Any]:
        try:
            return portable.line_up_payload(token, language, "home")
        except (KeyError, ValueError) as error:
            raise translate_error(error) from error

    @router.get("/vmix/hockey/session/{token}/{language}/away-lineup.json", include_in_schema=False)
    @router.get("/vmix/hockey/channel/{token}/{language}/away-lineup.json")
    async def away_lineup(token: str, language: str) -> dict[str, Any]:
        try:
            return portable.line_up_payload(token, language, "away")
        except (KeyError, ValueError) as error:
            raise translate_error(error) from error

    @router.get("/vmix/hockey/session/{token}/{language}/officials.json", include_in_schema=False)
    @router.get("/vmix/hockey/channel/{token}/{language}/officials.json")
    async def officials(token: str, language: str) -> dict[str, Any]:
        try:
            return portable.officials_payload(token, language)
        except (KeyError, ValueError) as error:
            raise translate_error(error) from error

    @router.get("/vmix/hockey/session/{token}/{language}/penalties.json", include_in_schema=False)
    @router.get("/vmix/hockey/channel/{token}/{language}/penalties.json")
    async def penalties(token: str, language: str) -> dict[str, Any]:
        try:
            return portable.penalties_payload(token, language)
        except (KeyError, ValueError) as error:
            raise translate_error(error) from error

    @router.get("/vmix/hockey/session/{token}/{language}/events.json", include_in_schema=False)
    @router.get("/vmix/hockey/channel/{token}/{language}/events.json")
    async def events(token: str, language: str) -> dict[str, Any]:
        try:
            return portable.events_payload(token, language)
        except (KeyError, ValueError) as error:
            raise translate_error(error) from error

    @router.get("/vmix/hockey/session/{token}/{language}/custom/{json_key}.json", include_in_schema=False)
    @router.get("/vmix/hockey/channel/{token}/{language}/custom/{json_key}.json")
    async def custom_json(
        token: str,
        language: str,
        json_key: str,
        limit: int | None = Query(None, ge=0, le=1000),
    ) -> list[dict[str, Any]]:
        try:
            return portable.configured_output(token, language, json_key, limit=limit)
        except (KeyError, ValueError) as error:
            raise translate_error(error) from error
        except Exception as error:
            raise HTTPException(
                status_code=500,
                detail=f"Ошибка сборки vMix JSON {json_key}: {error}",
            ) from error

    return router
