from fastapi import FastAPI, Form, Request, Query, HTTPException, WebSocket, WebSocketDisconnect
from datetime import datetime
from fastapi.responses import (
    HTMLResponse,
    RedirectResponse,
    JSONResponse,
    RedirectResponse,
    StreamingResponse,
)
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
from typing import List, Dict
from pathlib import Path
import io
import asyncio
import time
from urllib.parse import quote
import traceback
import contextlib
from parsers.parser_game import run_parser_game
from parsers.parser_players import run_parser_players
from parsers.parser_schedule import run_parser_schedule
from parsers.parser_standings import run_parser_standings


from db import get_connection
from repositories.match_formation_repository import (
    get_match_formations,
    replace_match_formations,
    apply_formation_preset_to_players,
)

from parsers.parser_game import run_parser_game
from repositories.match_session_repository import (
    create_match_session,
    get_match_session_by_token,
    deactivate_match_session,
    update_match_session_vmix_path,
    get_match_by_id,
    list_matches_for_admin,
    list_available_tours,
)
from repositories.match_view_repository import get_match_lineups_grouped
from vmix.vmix_service import build_vmix_project_bytes, build_vmix_filename
from repositories.match_referee_repository import get_match_referees
from repositories.match_repository import get_tour_schedule_by_match_id
from repositories.standings_repository import get_standings_by_match_id
from repositories.match_coach_repository import get_match_coaches_grouped
from repositories.player_repository import (
    search_players_for_admin,
    get_player_by_id,
    update_player_admin,
)

from repositories.referee_repository import (
    search_referees_for_admin,
    get_referee_by_id,
    update_referee_admin,
    get_all_referees,
    # replace_match_referees,
)

from repositories.match_referee_repository import replace_match_referees

from repositories.coach_repository import (
    search_coaches_for_admin,
    get_coach_by_id,
    update_coach_admin,
)

from repositories.stadium_repository import (
    search_stadiums_for_admin,
    get_stadium_by_id,
    update_stadium_admin,
)

from repositories.team_repository import (
    search_teams_for_admin,
    get_team_by_id,
    update_team_admin,
)

from repositories.match_event_repository import (
    create_event,
    get_events,
    delete_event,
    clear_events,
    update_event,
)
from repositories.auth_repository import get_user_by_username
from services.auth_service import (
    create_auth_session,
    hash_password,
    verify_password,
    get_current_user_from_request,
    build_not_authenticated_response,
    revoke_auth_session_by_request,
)

from repositories.audit_log_repository import create_audit_log

from repositories.team_squad_repository import get_team_players_for_match_editor
from repositories.team_coach_repository import get_team_coaches_for_match_editor
from repositories.match_lineup_repository import (
    get_match_lineup_for_editor,
    save_match_lineup_for_editor,
)

from services.vmix_json_service import (
    build_lineup_json,
    get_vmix_match_info_by_token,
    get_vmix_standings,
    get_vmix_schedule,
    get_vmix_team_formations,
    get_vmix_scoreboard_info,
    get_vmix_players_goal,
)

from scheduler import run_scheduler

BASE_DIR = Path(__file__).resolve().parent
app = FastAPI(
    docs_url=None,  # ❌ отключает /docs
    redoc_url=None,  # ❌ отключает /redoc
    openapi_url=None,  # ❌ отключает /openapi.json

)

templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")

@app.on_event("startup")
def start_scheduler():
    import threading

    thread = threading.Thread(
        target=run_scheduler,
        kwargs={"with_signal_handlers": False},
        daemon=True
    )
    thread.start()
    
    
class PublishVmixCommandPayload(BaseModel):
    match_id: int
    session_token: str | None = None
    commands: List[str] = Field(default_factory=list)
    target_client_id: str | None = None
    target_group: str | None = None
    target_login: str | None = None
    meta: dict | None = None


class RegisterVmixClientPayload(BaseModel):
    client_id: str
    match_id: int | None = None
    session_token: str | None = None
    operator_name: str | None = None
    group_name: str | None = None


class VmixConnectionManager:
    def __init__(self):
        self.connections: Dict[str, dict] = {}
        self.lock = asyncio.Lock()

    async def connect(
        self,
        websocket: WebSocket,
        client_id: str,
        match_id: int | None = None,
        session_token: str | None = None,
        operator_name: str | None = None,
        group_name: str | None = None,
    ):
        await websocket.accept()

        async with self.lock:
            old = self.connections.get(client_id)
            if old:
                try:
                    await old["websocket"].close(code=4000)
                except Exception:
                    pass

            self.connections[client_id] = {
                "websocket": websocket,
                "client_id": client_id,
                "match_id": match_id,
                "session_token": session_token,
                "operator_name": operator_name,
                "group_name": group_name,
                "connected_at": time.time(),
                "last_seen": time.time(),
            }

    async def disconnect(self, client_id: str):
        async with self.lock:
            self.connections.pop(client_id, None)

    async def heartbeat(self, client_id: str):
        async with self.lock:
            if client_id in self.connections:
                self.connections[client_id]["last_seen"] = time.time()

    async def update_meta(
        self,
        client_id: str,
        match_id: int | None = None,
        session_token: str | None = None,
        operator_name: str | None = None,
        group_name: str | None = None,
    ):
        async with self.lock:
            conn = self.connections.get(client_id)
            if not conn:
                return

            conn["last_seen"] = time.time()
            if match_id is not None:
                conn["match_id"] = match_id
            if session_token is not None:
                conn["session_token"] = session_token
            if operator_name is not None:
                conn["operator_name"] = operator_name
            if group_name is not None:
                conn["group_name"] = group_name

    async def list_clients(self):
        async with self.lock:
            return [
                {
                    "client_id": c["client_id"],
                    "match_id": c["match_id"],
                    "session_token": c["session_token"],
                    "operator_name": c["operator_name"],
                    "group_name": c["group_name"],
                    "connected_at": c["connected_at"],
                    "last_seen": c["last_seen"],
                }
                for c in self.connections.values()
            ]

    async def send_to_client(self, client_id: str, message: dict) -> bool:
        async with self.lock:
            conn = self.connections.get(client_id)
            if not conn:
                return False
            ws = conn["websocket"]

        try:
            await ws.send_json(message)
            await self.heartbeat(client_id)
            return True
        except Exception:
            await self.disconnect(client_id)
            return False
        
    async def send_to_match(
        self,
        match_id: int,
        message: dict,
        target_group: str | None = None,
        target_login: str | None = None,
    ):
        async with self.lock:
            items = list(self.connections.values())

        matched = []
        for conn in items:
            if conn["match_id"] != match_id:
                continue
            if target_group and conn.get("group_name") != target_group:
                continue
            if target_login and conn.get("operator_name") != target_login:
                continue
            matched.append(conn["client_id"])

        results = []
        for client_id in matched:
            ok = await self.send_to_client(client_id, message)
            results.append({"client_id": client_id, "ok": ok})

        return results


vmix_ws_manager = VmixConnectionManager()

def get_current_user(request: Request):
    return getattr(request.state, "current_user", None)


def require_role(request: Request, allowed_roles: set[str]):
    user = get_current_user(request)
    if not user or user["role"] not in allowed_roles:
        if request.headers.get(
            "x-requested-with"
        ) == "XMLHttpRequest" or "application/json" in request.headers.get(
            "accept", ""
        ):
            return JSONResponse({"error": "forbidden"}, status_code=403)
        return RedirectResponse("/admin/matches", status_code=303)
    return None

@app.get("/")
def root(request: Request):
    user = get_current_user_from_request(request)
    if user:
        return RedirectResponse(url="/admin/matches")
    return RedirectResponse(url="/login")


def log_action(
    request: Request,
    action: str,
    entity_type=None,
    entity_id=None,
    match_id=None,
    session_token=None,
    details=None,
):
    user = getattr(request.state, "current_user", None) or {}
    create_audit_log(
        user_id=user.get("id") or user.get("user_id"),
        username=user.get("username"),
        role=user.get("role"),
        action=action,
        entity_type=entity_type,
        entity_id=str(entity_id) if entity_id is not None else None,
        match_id=match_id,
        session_token=session_token,
        ip_address=request.client.host if request.client else None,
        user_agent=request.headers.get("user-agent"),
        details=details,
    )


@app.middleware("http")
async def admin_auth_middleware(request: Request, call_next):
    path = request.url.path

    if path.startswith("/admin"):
        user = get_current_user_from_request(request)
        if not user:
            return build_not_authenticated_response(request)

        current_user = get_current_user_from_request(request)
        request.state.current_user = {
            "id": current_user.get("user_id") or current_user.get("id"),
            "username": current_user.get("username"),
            "role": current_user.get("role", "operator"),
        }

    response = await call_next(request)
    return response


@app.get("/login", response_class=HTMLResponse)
def login_page(request: Request, reason: str | None = Query(default=None)):
    if get_current_user_from_request(request):
        return RedirectResponse(url="/admin/matches", status_code=303)

    reason_map = {
        "expired": "Сессия истекла. Войдите заново.",
        "idle": "Сессия завершена из-за 2 часов бездействия.",
        "logged_out": "Вы вышли из системы.",
        "invalid": "Нужно войти в систему.",
    }

    return templates.TemplateResponse(
        name="login.html",
        request=request,
        context={
            "error": "",
            "message": reason_map.get(reason or "", ""),
        },
    )


@app.post("/login", response_class=HTMLResponse)
def login_submit(
    request: Request,
    username: str = Form(...),
    password: str = Form(...),
):
    user = get_user_by_username(username.strip())

    if not user or not verify_password(password, user[2]) or not user[3]:
        return templates.TemplateResponse(
            request=request,
            name="login.html",
            context={
                "error": "Неверный логин или пароль",
                "message": "",
            },
            status_code=401,
        )

    token = create_auth_session(
        user_id=user[0],
        ip_address=request.client.host if request.client else None,
        user_agent=request.headers.get("user-agent"),
    )

    response = RedirectResponse(url="/admin/matches", status_code=303)
    response.set_cookie(
        key="auth_token",
        value=token,
        httponly=True,
        secure=True,
        samesite="lax",
        max_age=7200,
    )
    return response


@app.post("/logout")
def logout(request: Request):
    revoke_auth_session_by_request(request)
    response = RedirectResponse(url="/login?reason=logged_out", status_code=303)
    response.delete_cookie("auth_token", path="/")
    return response


class MatchEventPayload(BaseModel):
    side: str
    type: str
    player_name: str = ""
    minute: int | None = None
    seconds: int = 0
    meta: str | None = None

    player_id: int | None = None
    player_out_id: int | None = None
    player_in_id: int | None = None


class FormationApplyPayload(BaseModel):
    side: str
    preset: str


class FormationPlayerPayload(BaseModel):
    player_id: int | None = None
    player_name: str = ""
    number: str = ""
    position: str = ""
    is_captain: bool = False
    x: float
    y: float


class FormationSavePayload(BaseModel):
    side: str
    players: list[FormationPlayerPayload]


class SquadEditorPlayerPayload(BaseModel):
    player_id: int | None = None
    player_name: str = ""
    last_name: str = ""
    first_name: str = ""
    number: str = ""
    position: str = ""
    is_captain: bool = False


class SquadEditorCoachPayload(BaseModel):
    coach_id: int | None = None
    coach_name: str = ""
    role: str = ""


class SquadEditorSavePayload(BaseModel):
    home_starting: list[SquadEditorPlayerPayload] = Field(default_factory=list)
    home_bench: list[SquadEditorPlayerPayload] = Field(default_factory=list)
    away_starting: list[SquadEditorPlayerPayload] = Field(default_factory=list)
    away_bench: list[SquadEditorPlayerPayload] = Field(default_factory=list)

    home_coaches: list[SquadEditorCoachPayload] = Field(default_factory=list)
    away_coaches: list[SquadEditorCoachPayload] = Field(default_factory=list)


@app.get("/admin/matches", response_class=HTMLResponse)
def admin_matches(
    request: Request,
    today_only: bool = Query(default=False),
    tour: str | None = Query(default=None),
    hide_finished: str = Query(default="true"),
):
    hide_finished_bool = str(hide_finished).lower() == "true"

    matches = list_matches_for_admin(
        today_only=today_only,
        tour=tour,
        hide_finished=hide_finished_bool,
    )
    tours = list_available_tours()

    return templates.TemplateResponse(
        name="matches.html",
        request=request,
        context={
            "matches": matches,
            "today_only": today_only,
            "hide_finished": hide_finished_bool,
            "selected_tour": tour or "",
            "tours": tours,
            "current_user": getattr(request.state, "current_user", None),
        },
    )
    

@app.get("/admin/matches/{match_id}/select")
def select_match(
    request: Request, match_id: int, operator_name: str = Query(default="")
):
    match_row = get_match_by_id(match_id)
    if not match_row:
        return JSONResponse({"error": "match_not_found"}, status_code=404)

    session_row = create_match_session(
        match_id=match_id,
        operator_name=None,
        vmix_project_path=None,
    )

    session_token = session_row[3]

    log_action(
        request,
        action="match_selected",
        entity_type="match",
        entity_id=match_id,
        match_id=match_id,
        session_token=session_token,
        details={
            "operator_name": None,
        },
    )

    return JSONResponse({
        "success": True,
        "session_token": session_token,
        "session_url": f"/admin/session/{session_token}",
    })


@app.get("/admin/session/{session_token}/download-vmix-page", response_class=HTMLResponse)
def download_vmix_project_page(request: Request, session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return RedirectResponse(url="/admin/matches", status_code=303)

    return templates.TemplateResponse(
        name="download_vmix.html",
        request=request,
        context={
            "session": session_row,
            "session_token": session_token,
            "download_url": f"/admin/session/{session_token}/download-vmix",
            "back_url": f"/admin/session/{session_token}?tab=game",
        },
    )


@app.get("/admin/session/{session_token}/download-vmix")
def download_vmix_project(request: Request, session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    current_user = getattr(request.state, "current_user", None) or {}
    operator_login = current_user.get("username") or None

    try:
        vmix_bytes = build_vmix_project_bytes(
            session_token=session_token,
            match_id=session_row[1],
            operator_login=operator_login,
        )
        filename = build_vmix_filename(session_row)

        return StreamingResponse(
            io.BytesIO(vmix_bytes),
            media_type="application/octet-stream",
            headers={
                "Content-Disposition": (
                    f"attachment; filename*=UTF-8''{quote(filename)}"
                )
            },
        )
    except Exception as e:
        return JSONResponse({"error": str(e)}, status_code=500)    
    

@app.get("/admin/session/{session_token}", response_class=HTMLResponse)
def session_workspace(
    request: Request, session_token: str, tab: str = Query(default="game")
):
    session_row = get_match_session_by_token(session_token)

    if not session_row:
        return RedirectResponse(url="/admin/matches", status_code=303)

    match_id = session_row[1]
    home_team_id = session_row[13]
    away_team_id = session_row[17]
    referees = get_match_referees(match_id)
    referee_pool = get_all_referees()
    tour_schedule = get_tour_schedule_by_match_id(match_id)
    standings = get_standings_by_match_id(match_id)
    coaches = get_match_coaches_grouped(
        match_id=match_id,
        home_team_id=home_team_id,
        away_team_id=away_team_id,
    )

    lineups = get_match_lineup_for_editor(
        match_id=match_id,
        home_team_id=home_team_id,
        away_team_id=away_team_id,
    )

    home_formations = get_match_formations(match_id, home_team_id)
    away_formations = get_match_formations(match_id, away_team_id)
    

    return templates.TemplateResponse(
        name="match_workspace.html",
        request=request,
        context={
            "session": session_row,
            "tab": tab,
            "lineups": lineups,
            "referees": referees,
            "referee_pool": referee_pool,
            "tour_schedule": tour_schedule,
            "standings": standings,
            "coaches": coaches,
            "home_formations": home_formations,
            "away_formations": away_formations,
            "current_user": getattr(request.state, "current_user", None),
            "auth_idle_timeout_seconds": 7200,
        },
    )


@app.post("/admin/session/{session_token}/load-match-data")
def load_match_data(request: Request, session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        if request.headers.get("x-requested-with") == "XMLHttpRequest":
            return JSONResponse(
                {
                    "success": False,
                    "error": "Сессия не найдена",
                    "redirect_url": "/admin/matches",
                },
                status_code=404,
            )
        return RedirectResponse(url="/admin/matches", status_code=303)

    match_id = session_row[1]
    match_external_id = session_row[8]

    try:
        from db import get_connection

        # Сначала очищаем ручные данные матча,
        # чтобы парсер потом записал свежие составы и тренеров
        conn = get_connection()
        try:
            with conn.cursor() as cur:
                cur.execute(
                    "DELETE FROM match_lineup_players WHERE match_id = %s",
                    (match_id,),
                )
                cur.execute(
                    "DELETE FROM match_coaches WHERE match_id = %s",
                    (match_id,),
                )
            conn.commit()
        finally:
            conn.close()

        # Потом загружаем свежие данные с сайта
        run_parser_game(str(match_external_id))

    except Exception as e:
        error_text = str(e) or "Ошибка загрузки данных с сайта"

        if request.headers.get("x-requested-with") == "XMLHttpRequest":
            return JSONResponse(
                {
                    "success": False,
                    "error": error_text,
                    "redirect_url": f"/admin/session/{session_token}?tab=game",
                },
                status_code=500,
            )

        return RedirectResponse(
            url=f"/admin/session/{session_token}?tab=game",
            status_code=303,
        )

    if request.headers.get("x-requested-with") == "XMLHttpRequest":
        return JSONResponse(
            {
                "success": True,
                "message": "Данные успешно загружены",
                "redirect_url": f"/admin/session/{session_token}?tab=game",
            }
        )

    return RedirectResponse(
        url=f"/admin/session/{session_token}?tab=game",
        status_code=303,
    )    
    
    
@app.post("/admin/session/{session_token}/close")
def close_session(session_token: str):
    deactivate_match_session(session_token)
    return RedirectResponse(url="/admin/matches", status_code=303)


@app.get("/admin/db", response_class=HTMLResponse)
def admin_db_index(request: Request):
    return templates.TemplateResponse(
        name="admin_db_index.html",
        request=request,
        context={
            "current_user": getattr(request.state, "current_user", None),
        },
    )


@app.get("/admin/db/players", response_class=HTMLResponse)
def admin_db_players(request: Request, q: str = Query(default="")):
    # denied = require_role(request, {"admin"})
    # if denied:
    #     return denied

    players = search_players_for_admin(q)

    return templates.TemplateResponse(
        name="admin_db_players.html",
        request=request,
        context={
            "q": q,
            "players": players,
        },
    )


@app.get("/admin/db/players/{player_id}/edit", response_class=HTMLResponse)
def admin_db_player_edit(request: Request, player_id: int):
    player = get_player_by_id(player_id)
    if not player:
        return RedirectResponse(url="/admin/db/players", status_code=303)

    return templates.TemplateResponse(
        name="admin_db_player_edit.html",
        request=request,
        context={
            "player": player,
        },
    )


@app.post("/admin/db/players/{player_id}/edit")
def admin_db_player_edit_submit(
    player_id: int,
    full_name: str = Form(default=""),
    first_name: str = Form(default=""),
    last_name: str = Form(default=""),
    external_id: str = Form(default=""),
    position: str = Form(default=""),
    birth_date: str = Form(default=""),
    photo: str = Form(default=""),
    video: str = Form(default=""),
):
    update_player_admin(
        player_id=player_id,
        full_name=full_name,
        first_name=first_name,
        last_name=last_name,
        external_id=external_id,
        position=position,
        birth_date=birth_date,
        photo=photo,
        video=video,
    )

    return RedirectResponse(
        url=f"/admin/db/players/{player_id}/edit",
        status_code=303,
    )


@app.get("/admin/db/referees", response_class=HTMLResponse)
def admin_db_referees(request: Request, q: str = Query(default="")):
    referees = search_referees_for_admin(q)
    return templates.TemplateResponse(
        name="admin_db_referees.html",
        request=request,
        context={
            "q": q,
            "referees": referees,
        },
    )


@app.get("/admin/db/referees/{referee_id}/edit", response_class=HTMLResponse)
def admin_db_referee_edit(request: Request, referee_id: int):
    referee = get_referee_by_id(referee_id)
    if not referee:
        return RedirectResponse(url="/admin/db/referees", status_code=303)

    return templates.TemplateResponse(
        name="admin_db_referee_edit.html",
        request=request,
        context={
            "referee": referee,
        },
    )


@app.post("/admin/db/referees/{referee_id}/edit")
def admin_db_referee_edit_submit(
    referee_id: int,
    full_name: str = Form(default=""),
    external_id: str = Form(default=""),
):
    update_referee_admin(
        referee_id=referee_id,
        full_name=full_name,
        external_id=external_id,
    )
    return RedirectResponse(
        url=f"/admin/db/referees/{referee_id}/edit",
        status_code=303,
    )


@app.get("/admin/db/coaches", response_class=HTMLResponse)
def admin_db_coaches(request: Request, q: str = Query(default="")):
    coaches = search_coaches_for_admin(q)
    return templates.TemplateResponse(
        name="admin_db_coaches.html",
        request=request,
        context={
            "q": q,
            "coaches": coaches,
        },
    )


@app.get("/admin/db/coaches/{coach_id}/edit", response_class=HTMLResponse)
def admin_db_coach_edit(request: Request, coach_id: int):
    coach = get_coach_by_id(coach_id)
    if not coach:
        return RedirectResponse(url="/admin/db/coaches", status_code=303)

    return templates.TemplateResponse(
        name="admin_db_coach_edit.html",
        request=request,
        context={
            "coach": coach,
        },
    )


@app.post("/admin/db/coaches/{coach_id}/edit")
def admin_db_coach_edit_submit(
    coach_id: int,
    full_name: str = Form(default=""),
    external_id: str = Form(default=""),
    role: str = Form(default=""),
):
    update_coach_admin(
        coach_id=coach_id,
        full_name=full_name,
        external_id=external_id,
        role=role,
    )
    return RedirectResponse(
        url=f"/admin/db/coaches/{coach_id}/edit",
        status_code=303,
    )


@app.get("/admin/db/stadiums", response_class=HTMLResponse)
def admin_db_stadiums(request: Request, q: str = Query(default="")):
    stadiums = search_stadiums_for_admin(q)
    return templates.TemplateResponse(
        name="admin_db_stadiums.html",
        request=request,
        context={
            "q": q,
            "stadiums": stadiums,
        },
    )


@app.get("/admin/db/stadiums/{stadium_id}/edit", response_class=HTMLResponse)
def admin_db_stadium_edit(request: Request, stadium_id: int):
    stadium = get_stadium_by_id(stadium_id)
    if not stadium:
        return RedirectResponse(url="/admin/db/stadiums", status_code=303)

    return templates.TemplateResponse(
        name="admin_db_stadium_edit.html",
        request=request,
        context={
            "stadium": stadium,
        },
    )


@app.post("/admin/db/stadiums/{stadium_id}/edit")
def admin_db_stadium_edit_submit(
    stadium_id: int,
    stadium_gfx: str = Form(default=""),
    city: str = Form(default=""),
    address: str = Form(default=""),
    external_id: str = Form(default=""),
):
    update_stadium_admin(
        stadium_id=stadium_id,
        stadium_gfx=stadium_gfx,
        city=city,
        address=address,
        external_id=external_id,
    )
    return RedirectResponse(
        url=f"/admin/db/stadiums/{stadium_id}/edit",
        status_code=303,
    )


@app.get("/admin/db/teams", response_class=HTMLResponse)
def admin_db_teams(request: Request, q: str = Query(default="")):
    teams = search_teams_for_admin(q)
    return templates.TemplateResponse(
        name="admin_db_teams.html",
        request=request,
        context={
            "q": q,
            "teams": teams,
        },
    )


@app.get("/admin/db/teams/{team_id}/edit", response_class=HTMLResponse)
def admin_db_team_edit(request: Request, team_id: int):
    team = get_team_by_id(team_id)
    if not team:
        return RedirectResponse(url="/admin/db/teams", status_code=303)

    return templates.TemplateResponse(
        name="admin_db_team_edit.html",
        request=request,
        context={
            "team": team,
        },
    )


@app.post("/admin/db/teams/{team_id}/edit")
def admin_db_team_edit_submit(
    team_id: int,
    name: str = Form(default=""),
    full_name: str = Form(default=""),
    short_name_3: str = Form(default=""),
    city: str = Form(default=""),
    logo_path: str = Form(default=""),
    external_id: str = Form(default=""),
):
    update_team_admin(
        team_id=team_id,
        name=name,
        full_name=full_name,
        short_name_3=short_name_3,
        city=city,
        logo_path=logo_path,
        external_id=external_id,
    )
    return RedirectResponse(
        url=f"/admin/db/teams/{team_id}/edit",
        status_code=303,
    )


@app.post("/admin/session/{session_token}/formations/apply")
def apply_formation(session_token: str, payload: FormationApplyPayload):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    match_id = session_row[1]
    home_team_id = session_row[13]
    away_team_id = session_row[17]

    lineups = get_match_lineup_for_editor(
        match_id=match_id,
        home_team_id=home_team_id,
        away_team_id=away_team_id,
    )

    if payload.side == "home":
        team_id = home_team_id
        players = lineups["home_starting"]
    elif payload.side == "away":
        team_id = away_team_id
        players = lineups["away_starting"]
    else:
        return JSONResponse({"error": "invalid_side"}, status_code=400)

    applied = apply_formation_preset_to_players(players, payload.preset)
    replace_match_formations(match_id, team_id, applied)

    return {"players": applied}


@app.post("/admin/session/{session_token}/formations/save")
def save_formation(session_token: str, payload: FormationSavePayload):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    match_id = session_row[1]
    home_team_id = session_row[13]
    away_team_id = session_row[17]

    if payload.side == "home":
        team_id = home_team_id
    elif payload.side == "away":
        team_id = away_team_id
    else:
        return JSONResponse({"error": "invalid_side"}, status_code=400)

    players = [
        {
            "player_id": p.player_id,
            "player_name": p.player_name,
            "number": p.number,
            "position": p.position,
            "is_captain": p.is_captain,
            "x": p.x,
            "y": p.y,
        }
        for p in payload.players
    ]

    replace_match_formations(match_id, team_id, players)

    return {"success": True}


@app.get("/admin/session/{session_token}/events")
def api_get_events(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    match_id = session_row[1]
    return get_events(match_id)


@app.post("/admin/session/{session_token}/event")
def api_add_event(session_token: str, payload: MatchEventPayload):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    match_id = session_row[1]

    event_id = create_event(
        match_id=match_id,
        side=payload.side,
        type_=payload.type,
        player_name=payload.player_name,
        minute=payload.minute,
        seconds=payload.seconds,
        meta=payload.meta,
        player_id=payload.player_id,
        player_out_id=payload.player_out_id,
        player_in_id=payload.player_in_id,
    )
    return {"id": event_id}


@app.put("/admin/session/{session_token}/event/{event_id}")
def api_update_event(session_token: str, event_id: int, payload: MatchEventPayload):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    update_event(
        event_id=event_id,
        side=payload.side,
        type_=payload.type,
        player_name=payload.player_name,
        minute=payload.minute,
        seconds=payload.seconds,
        meta=payload.meta,
        player_id=payload.player_id,
        player_out_id=payload.player_out_id,
        player_in_id=payload.player_in_id,
    )
    return {"ok": True}


@app.delete("/admin/session/{session_token}/events")
def api_clear_events(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    match_id = session_row[1]
    clear_events(match_id)
    return {"ok": True}


@app.delete("/admin/session/{session_token}/event/{event_id}")
def api_delete_event(session_token: str, event_id: int):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    delete_event(event_id)
    return {"ok": True}


@app.get("/admin/session/{session_token}/squad-editor-data")
def api_get_squad_editor_data(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    match_id = session_row[1]
    home_team_id = session_row[13]
    away_team_id = session_row[17]

    current_lineup = get_match_lineup_for_editor(
        match_id=match_id,
        home_team_id=home_team_id,
        away_team_id=away_team_id,
    )

    home_squad_pool = get_team_players_for_match_editor(
        home_team_id,
        match_id=match_id,
        home_team_id=home_team_id,
        away_team_id=away_team_id,
    )

    away_squad_pool = get_team_players_for_match_editor(
        away_team_id,
        match_id=match_id,
        home_team_id=home_team_id,
        away_team_id=away_team_id,
    )

    home_coach_pool = get_team_coaches_for_match_editor(
        home_team_id,
        match_id=match_id,
        home_team_id=home_team_id,
        away_team_id=away_team_id,
    )

    away_coach_pool = get_team_coaches_for_match_editor(
        away_team_id,
        match_id=match_id,
        home_team_id=home_team_id,
        away_team_id=away_team_id,
    )

    return {
        "home_starting": current_lineup.get("home_starting", []),
        "home_bench": current_lineup.get("home_bench", []),
        "away_starting": current_lineup.get("away_starting", []),
        "away_bench": current_lineup.get("away_bench", []),
        "home_coaches": current_lineup.get("home_coaches", []),
        "away_coaches": current_lineup.get("away_coaches", []),
        "home_squad_pool": home_squad_pool,
        "away_squad_pool": away_squad_pool,
        "home_coach_pool": home_coach_pool,
        "away_coach_pool": away_coach_pool,
    }


@app.post("/admin/session/{session_token}/squad-editor-save")
def api_save_squad_editor_data(
    session_token: str,
    payload: SquadEditorSavePayload,
):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    match_id = session_row[1]
    home_team_id = session_row[13]
    away_team_id = session_row[17]

    save_match_lineup_for_editor(
        match_id=match_id,
        home_team_id=home_team_id,
        away_team_id=away_team_id,
        home_starting=[p.model_dump() for p in payload.home_starting],
        home_bench=[p.model_dump() for p in payload.home_bench],
        away_starting=[p.model_dump() for p in payload.away_starting],
        away_bench=[p.model_dump() for p in payload.away_bench],
        home_coaches=[c.model_dump() for c in payload.home_coaches],
        away_coaches=[c.model_dump() for c in payload.away_coaches],
    )

    return {"success": True}


@app.post("/admin/session/{session_token}/referees/save")
async def save_session_referees(request: Request, session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return {"success": False, "error": "Сессия не найдена"}

    match_id = session_row[1]
    payload = await request.json()
    referees = payload.get("referees", [])

    replace_match_referees(match_id, referees)

    return {"success": True}


EMPTY_PLAYER = {
    "last_name": " ",
    "first_name": " ",
    "full_name": " ",
    "full_name_K": " ",
    "number": " ",
    "position": " ",
    "pos": " ",
    "number_lastname_amp_K": " ",
    "number_fullname": " ",
    "photo": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo\EMPTY.png",
}


def get_roster_data(session_token: str, name: str, count_player: int):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    match_id = session_row[1]
    home_team_id = session_row[13]
    home_team_name = session_row[14].replace("«", "").replace("»", "")
    away_team_id = session_row[17]
    away_team_name = session_row[18].replace("«", "").replace("»", "")
    # print(session_row)

    data = build_lineup_json(
        match_id, home_team_id, away_team_id, name, home_team_name, away_team_name
    )

    players = data.get("players", [])
    players.extend([EMPTY_PLAYER.copy() for _ in range(count_player - len(players))])

    data["players"] = players[:count_player]
    return data


@app.get("/vmix/session/{session_token}/home-lineup")
def vmix_home_lineup(session_token: str):
    return get_roster_data(session_token, "home_starting", 11)


@app.get("/vmix/session/{session_token}/away-lineup")
def vmix_away_lineup(session_token: str):
    return get_roster_data(session_token, "away_starting", 11)


@app.get("/vmix/session/{session_token}/home-bench")
def vmix_home_bench(session_token: str):
    return get_roster_data(session_token, "home_bench", 12)


@app.get("/vmix/session/{session_token}/away-bench")
def vmix_away_bench(session_token: str):
    return get_roster_data(session_token, "away_bench", 12)


MONTHS_RU = [
    "",  # чтобы месяц = 1 → январь
    "января",
    "февраля",
    "марта",
    "апреля",
    "мая",
    "июня",
    "июля",
    "августа",
    "сентября",
    "октября",
    "ноября",
    "декабря",
]


def format_date_ru(value) -> str:
    if isinstance(value, datetime):
        dt = value
    else:
        dt = datetime.fromisoformat(value)

    return f"{dt.day} {MONTHS_RU[dt.month]}"


@app.get("/vmix/session/{session_token}/info")
def vmix_info(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    row = get_vmix_match_info_by_token(session_token)

    if not row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    return [
        {
            "date_and_stadium": f'{format_date_ru(row[1])} | {row[12] or ""}',
            "tour": row[13].replace(" ", "-й ") or "",
            "tour_ID": row[13].replace(" ", "-й ") or "",
            "home_team": {
                "full_name": row[6] or "",
                "short_name": row[7] or "",
                "logo": row[14] or "",
                "logo_1": row[20] or "",
                "logo_2": row[22] or "",
                "city": f'{row[6] or ""}   {row[24] or ""}',
            },
            "away_team": {
                "full_name": row[10] or "",
                "short_name": row[11] or "",
                "logo": row[15] or "",
                "logo_1": row[21] or "",
                "logo_2": row[23] or "",
                "city": f'{row[10] or ""}   {row[25] or ""}',
            },
            "referees1": row[16] or "",
            "referees2": row[17] or "",
            "referees3": row[18] or "",
            "referees4": row[19] or "",
            "coach1": row[26] or "",
            "coach2": row[28] or "",
            "amplua1": row[27] or "",
            "amplua2": row[29] or "",
        }
    ]


@app.get("/vmix/session/{session_token}/standings")
def vmix_standings(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    home_team_id = session_row[13]
    away_team_id = session_row[17]

    rows = get_vmix_standings(session_row)
    return [
        {
            "position": row[0],
            "team_name": row[1],
            "logo": row[2],
            "played": row[3],
            "wins": row[4],
            "losses": row[5],
            "draws": row[6],
            "score": row[7],
            "points": row[8],
            "color": (
                "#FF00FF"
                if row[9] == home_team_id
                else "#7300FF" if row[9] == away_team_id else "#FFFFFF00"
            ),
        }
        for row in rows
    ]


@app.get("/vmix/session/{session_token}/schedule")
def vmix_schedule(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    rows = get_vmix_schedule(session_row)
    return [
        {
            "logo1": row[0],
            "logo2": row[1],
            "score": (
                f"{row[2]} - {row[3]}"
                if row[2] is not None and row[3] is not None
                else " "
            ),
            "date": format_date_ru(row[4]),
            "time": (
                row[4].strftime("%H:%M") if row[2] is None and row[3] is None else " "
            ),
            "status": "ИДЁТ" if row[5] not in ["scheduled", "finished"] else " ",
            "color": (
                "#37F193" if row[5] not in ["scheduled", "finished"] else "#FFFFFF00"
            ),
            "mask_time": "#FFFFFF00" if row[2] is not None and row[3] is not None else "#FFFFFF",
            "mask_score": "#FFFFFF00" if row[2] is None and row[3] is None else "#FFFFFF",
        }
        for row in rows
    ]


EMPTY_PLAYER_FORMATION = {
    "number": " ",
    "number_lastname_amp_K": " ",
    "position": " ",
    "photo": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo\EMPTY.png",
}


def build_vmix_formation_response(rows, team_id, team_name):
    result = []

    for p in rows:
        suffix = []
        lastname = p[0] or ""
        is_captain = p[1]
        number = p[2]
        position = p[3] or ""

        if "вратарь" in position.lower():
            suffix.append("ВР")

        if is_captain:
            suffix.append("К")

        suffix_str = f' ({", ".join(suffix)})' if suffix else ""

        result.append(
            {
                "number_lastname_amp_K": f"{number} {lastname}{suffix_str}".strip(),
                "number": number,
                "position": position,
                "photo": (
                    r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo"
                    + "\\"
                    + team_name
                    + "\\"
                    + (p[0] + " " + p[4]).strip()
                    + ".png"
                ),
            }
        )
    result.extend([EMPTY_PLAYER_FORMATION.copy() for _ in range(11 - len(result))])

    return result


@app.get("/vmix/session/{session_token}/home-formations")
def vmix_home_formations(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    home_team_id = session_row[13]
    home_team = session_row[14].replace("«", "").replace("»", "")
    rows = get_vmix_team_formations(session_row, home_team_id)
    return build_vmix_formation_response(rows, home_team_id, home_team)


@app.get("/vmix/session/{session_token}/away-formations")
def vmix_away_formations(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    away_team_id = session_row[17]
    away_team = session_row[18].replace("«", "").replace("»", "")
    rows = get_vmix_team_formations(session_row, away_team_id)
    return build_vmix_formation_response(rows, away_team_id, away_team)


@app.get("/vmix/session/{session_token}/scoreboard")
def vmix_scoreboard(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    row = get_vmix_scoreboard_info(session_row)
    if not row:
        return JSONResponse({"error": "data_not_found"}, status_code=404)
    return [
        {
            "home_score": row[0],
            "away_score": row[1],
            "red_home_1": row[2],
            "red_home_2": row[3],
            "red_home_3": row[4],
            "red_home_4": row[5],
            "red_away_1": row[6],
            "red_away_2": row[7],
            "red_away_3": row[8],
            "red_away_4": row[9],
        }
    ]


EMPTY_GOALS = {
    "player_home": " ",
    "player_away": " ",
}


@app.get("/vmix/session/{session_token}/match-events")
def vmix_match_events(session_token: str):
    session_row = get_match_session_by_token(session_token)
    if not session_row:
        return JSONResponse({"error": "session_not_found"}, status_code=404)

    match_id = session_row[1]
    events = get_vmix_players_goal(match_id)
    new_events = [
        {
            "player_home": event[0],
            "player_away": event[1],
        }
        for event in events
    ]
    new_events.extend([EMPTY_GOALS.copy() for _ in range(12 - len(new_events))])

    return new_events

@app.websocket("/ws/vmix-client")
async def ws_vmix_client(
    websocket: WebSocket,
    client_id: str = Query(...),
    match_id: int | None = Query(default=None),
    session_token: str | None = Query(default=None),
    operator_name: str | None = Query(default=None),
    group_name: str | None = Query(default=None),
):
    await vmix_ws_manager.connect(
        websocket=websocket,
        client_id=client_id,
        match_id=match_id,
        session_token=session_token,
        operator_name=operator_name,
        group_name=group_name,
    )

    try:
        await websocket.send_json({
            "type": "connected",
            "client_id": client_id,
            "match_id": match_id,
            "session_token": session_token,
            "operator_name": operator_name,
        })

        while True:
            msg = await websocket.receive_json()

            msg_type = str(msg.get("type") or "").strip()

            if msg_type == "ping":
                await vmix_ws_manager.heartbeat(client_id)
                await websocket.send_json({"type": "pong"})
                continue

            if msg_type == "register":
                await vmix_ws_manager.update_meta(
                    client_id=client_id,
                    match_id=msg.get("match_id"),
                    session_token=msg.get("session_token"),
                    operator_name=msg.get("operator_name"),
                    group_name=msg.get("group_name"),
                )
                await websocket.send_json({"type": "registered", "client_id": client_id})
                continue

            if msg_type == "vmix_result":
                # тут можно логировать delivery/result при желании
                await vmix_ws_manager.heartbeat(client_id)
                continue

    except WebSocketDisconnect:
        await vmix_ws_manager.disconnect(client_id)
    except Exception:
        await vmix_ws_manager.disconnect(client_id)
        try:
            await websocket.close()
        except Exception:
            pass

@app.post("/api/vmix/publish-command")
async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Request):
    commands = [str(x).strip() for x in (payload.commands or []) if str(x).strip()]
    if not commands:
        raise HTTPException(status_code=400, detail="commands is empty")

    message = {
        "type": "vmix_command",
        "match_id": payload.match_id,
        "commands": commands,
        "target_login": payload.target_login,
        "meta": payload.meta or {},
        "sent_at": time.time(),
    }

    if payload.target_client_id:
        ok = await vmix_ws_manager.send_to_client(payload.target_client_id, message)
        results = [{"client_id": payload.target_client_id, "ok": ok}]
    else:
        results = await vmix_ws_manager.send_to_match(
            match_id=payload.match_id,
            message=message,
            target_group=payload.target_group,
            target_login=payload.target_login,
        )

    log_action(
        request,
        action="vmix_publish_command",
        entity_type="match",
        entity_id=payload.match_id,
        match_id=payload.match_id,
        session_token=payload.session_token,
        details={
            "target_client_id": payload.target_client_id,
            "target_group": payload.target_group,
            "target_login": payload.target_login,
            "commands_count": len(commands),
            "results": results,
        },
    )

    return {
        "success": True,
        "delivered_to": results,
        "commands_count": len(commands),
    }
    
@app.get("/api/vmix/clients")
async def list_vmix_clients():
    clients = await vmix_ws_manager.list_clients()
    return {"items": clients}

@app.get("/info", response_class=HTMLResponse)
def info_page(request: Request):
    return templates.TemplateResponse(
        name="info.html",
        request=request,
        context={}
    )
    
def render_admin_db_index(request: Request, parser_output: str | None = None, parser_name: str | None = None, parser_success: bool | None = None):
    return templates.TemplateResponse(
        name="admin_db_index.html",
        request=request,
        context={
            "current_user": getattr(request.state, "current_user", None),
            "parser_output": parser_output,
            "parser_name": parser_name,
            "parser_success": parser_success,
        },
    )


@app.get("/admin/db", response_class=HTMLResponse)
def admin_db_index(request: Request):
    return render_admin_db_index(request)


@app.post("/admin/db/run-parser", response_class=HTMLResponse)
def admin_db_run_parser(request: Request, parser_name: str = Form(...)):
    current_user = getattr(request.state, "current_user", None)
    if not current_user or current_user.get("role") != "admin":
        return RedirectResponse(url="/login", status_code=303)

    parser_map = {
        "players": ("Игроки", run_parser_players),
        "schedule": ("Расписание", run_parser_schedule),
        "standings": ("Турнирка", run_parser_standings),
    }

    parser_meta = parser_map.get(parser_name)
    if not parser_meta:
        return render_admin_db_index(
            request,
            parser_output="Неизвестный парсер.",
            parser_name=parser_name,
            parser_success=False,
        )

    title, parser_func = parser_meta
    buffer = io.StringIO()
    success = True

    with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer):
        print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Запуск парсера: {title}")
        try:
            parser_func()
            print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Парсер завершён успешно")
        except Exception:
            success = False
            print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Ошибка при выполнении парсера")
            print(traceback.format_exc())

    output = buffer.getvalue().strip() or "Парсер не вернул сообщений."
    return render_admin_db_index(
        request,
        parser_output=output,
        parser_name=title,
        parser_success=success,
    )


    
    
@app.post("/admin/db/create-account", response_class=HTMLResponse)
def admin_db_create_account(
    request: Request,
    username: str = Form(...),
    password: str = Form(...),
    role: str = Form(default="operator"),
):
    current_user = getattr(request.state, "current_user", None)
    if not current_user or current_user.get("role") != "admin":
        return RedirectResponse(url="/login", status_code=303)

    username = username.strip()
    role = (role or "operator").strip().lower()

    if not username:
        return render_admin_db_index(request, parser_output="Логин обязателен.", parser_success=False)

    if not password:
        return render_admin_db_index(request, parser_output="Пароль обязателен.", parser_success=False)

    if len(password) < 6:
        return render_admin_db_index(request, parser_output="Пароль должен быть не короче 6 символов.", parser_success=False)

    if role not in {"admin", "operator"}:
        return render_admin_db_index(request, parser_output="Роль должна быть admin или operator.", parser_success=False)

    password_hash = hash_password(password)
    conn = get_connection()

    try:
        with conn.cursor() as cur:
            cur.execute(
                """
                INSERT INTO admin_users (username, password_hash, role, is_active)
                VALUES (%s, %s, %s, TRUE)
                ON CONFLICT (username) DO NOTHING
                RETURNING id;
                """,
                (username, password_hash, role),
            )
            row = cur.fetchone()
            conn.commit()

        if row:
            return render_admin_db_index(
                request,
                parser_output=f"Пользователь создан: {username} (id={row[0]})\nРоль: {role}",
                parser_success=True,
            )

        return render_admin_db_index(
            request,
            parser_output=f"Пользователь '{username}' уже существует.",
            parser_success=False,
        )

    except Exception:
        conn.rollback()
        return render_admin_db_index(
            request,
            parser_output="Ошибка при создании аккаунта:\n" + traceback.format_exc(),
            parser_success=False,
        )
    finally:
        conn.close()