from __future__ import annotations

import asyncio
from typing import Dict, Optional

import httpx
from fastapi import Depends, FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import SQLModel, select
from sqlalchemy.exc import IntegrityError

from services.api.models import User
from services.common.auth import create_token, decode_token, hash_password, verify_password
from services.common.db import SessionLocal, engine
from services.common.schemas import (
    ActionRequest,
    AddAIRequest,
    CreateGameRequest,
    JoinGameRequest,
    TradeOfferRequest,
    TradeRespondRequest,
)
from services.common.settings import settings

app = FastAPI(title="Catan API Gateway")
cors_origins = [origin.strip() for origin in settings.cors_origins.split(",") if origin.strip()]
app.add_middleware(
    CORSMiddleware,
    allow_origins=cors_origins or ["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

clients: Dict[str, httpx.AsyncClient] = {}
ws_connections: Dict[str, Dict[WebSocket, str]] = {}
ai_tasks: Dict[str, asyncio.Task] = {}


@app.on_event("startup")
async def _startup() -> None:
    SQLModel.metadata.create_all(engine)
    clients["game"] = httpx.AsyncClient(base_url=settings.game_service_url)
    clients["analytics"] = httpx.AsyncClient(base_url=settings.analytics_service_url)
    clients["ai"] = httpx.AsyncClient(base_url=settings.ai_service_url)


@app.on_event("shutdown")
async def _shutdown() -> None:
    for client in clients.values():
        await client.aclose()


def _get_current_user(request: Request) -> User:
    auth = request.headers.get("authorization", "")
    if not auth.lower().startswith("bearer "):
        raise HTTPException(status_code=401, detail="Missing token")
    token = auth.split(" ", 1)[1].strip()
    try:
        payload = decode_token(token)
    except Exception:
        raise HTTPException(status_code=401, detail="Invalid token")
    user_id = payload.get("sub")
    if not user_id:
        raise HTTPException(status_code=401, detail="Invalid token")
    with SessionLocal() as session:
        user = session.get(User, int(user_id))
    if not user:
        raise HTTPException(status_code=401, detail="Unknown user")
    return user


def _get_user_from_ws(websocket: WebSocket) -> User:
    token = websocket.query_params.get("token")
    if not token:
        raise HTTPException(status_code=401, detail="Missing token")
    try:
        payload = decode_token(token)
    except Exception:
        raise HTTPException(status_code=401, detail="Invalid token")
    user_id = payload.get("sub")
    if not user_id:
        raise HTTPException(status_code=401, detail="Invalid token")
    with SessionLocal() as session:
        user = session.get(User, int(user_id))
    if not user:
        raise HTTPException(status_code=401, detail="Unknown user")
    return user


def _mask_state_for_user(state: dict, username: str) -> dict:
    data = dict(state)
    game = data.get("game")
    if not game:
        return data
    masked_players = {}
    for name, info in game.get("players", {}).items():
        pdata = dict(info)
        if name != username:
            resources = pdata.get("resources") or {}
            total = sum(resources.values()) if isinstance(resources, dict) else 0
            pdata["resources"] = {"hidden": total}
        masked_players[name] = pdata
    game["players"] = masked_players
    data["game"] = game
    return data


async def _broadcast(game_id: str, state: dict) -> None:
    connections = ws_connections.get(game_id, {})
    dead = []
    for ws, username in connections.items():
        try:
            await ws.send_json({"type": "state", "data": _mask_state_for_user(state, username)})
        except Exception:
            dead.append(ws)
    for ws in dead:
        connections.pop(ws, None)


def _is_ai_only(state: dict) -> bool:
    slots = state.get("players", [])
    if not slots:
        return False
    return all(slot.get("is_ai") for slot in slots)


async def _auto_advance(game_id: str) -> None:
    while True:
        response = await clients["game"].post(f"/games/{game_id}/advance")
        if response.status_code >= 400:
            break
        state = response.json()
        await _broadcast(game_id, state)
        if state.get("status") != "running":
            break
        await asyncio.sleep(0.2)


def _ensure_task(game_id: str) -> None:
    if game_id in ai_tasks and not ai_tasks[game_id].done():
        return
    ai_tasks[game_id] = asyncio.create_task(_auto_advance(game_id))


@app.get("/health")
def health() -> dict:
    return {"status": "ok"}


@app.post("/api/auth/register")
def register(payload: dict):
    username = (payload.get("username") or "").strip()
    password = (payload.get("password") or "").strip()
    if not username or not password:
        raise HTTPException(status_code=400, detail="Username and password required")
    user = User(username=username, password_hash=hash_password(password))
    with SessionLocal() as session:
        session.add(user)
        try:
            session.commit()
        except IntegrityError:
            session.rollback()
            raise HTTPException(status_code=400, detail="Username already exists")
        session.refresh(user)
    token = create_token(user.id, user.username)
    return {"token": token, "username": user.username}


@app.post("/api/auth/login")
def login(payload: dict):
    username = (payload.get("username") or "").strip()
    password = (payload.get("password") or "").strip()
    with SessionLocal() as session:
        user = session.exec(select(User).where(User.username == username)).first()
    if not user or not verify_password(password, user.password_hash):
        raise HTTPException(status_code=401, detail="Invalid credentials")
    token = create_token(user.id, user.username)
    return {"token": token, "username": user.username}


@app.get("/api/me")
def me(user: User = Depends(_get_current_user)):
    return {"id": user.id, "username": user.username}


@app.get("/api/lobby")
async def lobby(user: User = Depends(_get_current_user)):
    response = await clients["game"].get("/games")
    response.raise_for_status()
    return response.json()


@app.post("/api/games")
async def create_game(payload: CreateGameRequest, user: User = Depends(_get_current_user)):
    data = payload.model_dump()
    data["created_by"] = user.username
    response = await clients["game"].post("/games", json=data)
    response.raise_for_status()
    return response.json()


@app.post("/api/games/{game_id}/join")
async def join_game(game_id: str, user: User = Depends(_get_current_user)):
    payload = JoinGameRequest(username=user.username, user_id=user.id)
    response = await clients["game"].post(f"/games/{game_id}/join", json=payload.model_dump())
    response.raise_for_status()
    state = await clients["game"].get(f"/games/{game_id}")
    state.raise_for_status()
    await _broadcast(game_id, state.json())
    return response.json()


@app.post("/api/games/{game_id}/leave")
async def leave_game(game_id: str, user: User = Depends(_get_current_user)):
    payload = JoinGameRequest(username=user.username, user_id=user.id)
    response = await clients["game"].post(f"/games/{game_id}/leave", json=payload.model_dump())
    response.raise_for_status()
    state = await clients["game"].get(f"/games/{game_id}")
    state.raise_for_status()
    await _broadcast(game_id, state.json())
    return response.json()


@app.post("/api/games/{game_id}/add-ai")
async def add_ai(game_id: str, payload: AddAIRequest, user: User = Depends(_get_current_user)):
    response = await clients["game"].post(f"/games/{game_id}/add_ai", json=payload.model_dump())
    response.raise_for_status()
    state = await clients["game"].get(f"/games/{game_id}")
    state.raise_for_status()
    await _broadcast(game_id, state.json())
    return response.json()


@app.post("/api/games/{game_id}/start")
async def start_game(game_id: str, user: User = Depends(_get_current_user)):
    response = await clients["game"].post(f"/games/{game_id}/start")
    response.raise_for_status()
    state = response.json()
    await _broadcast(game_id, state)
    if _is_ai_only(state):
        _ensure_task(game_id)
    return _mask_state_for_user(state, user.username)


@app.get("/api/games/{game_id}")
async def game_state(game_id: str, user: User = Depends(_get_current_user)):
    response = await clients["game"].get(f"/games/{game_id}")
    response.raise_for_status()
    return _mask_state_for_user(response.json(), user.username)


@app.post("/api/games/{game_id}/action")
async def take_action(game_id: str, payload: dict, user: User = Depends(_get_current_user)):
    action = payload.get("action") or {"type": payload.get("type"), "payload": payload.get("payload")}
    req = ActionRequest(actor=user.username, action=action)
    response = await clients["game"].post(f"/games/{game_id}/action", json=req.model_dump())
    if response.status_code >= 400:
        raise HTTPException(status_code=response.status_code, detail=response.json().get("detail"))
    state = response.json()
    await _broadcast(game_id, state)
    if _is_ai_only(state):
        _ensure_task(game_id)
    return _mask_state_for_user(state, user.username)


@app.post("/api/games/{game_id}/trade/offer")
async def offer_trade(game_id: str, payload: TradeOfferRequest, user: User = Depends(_get_current_user)):
    if payload.from_player != user.username:
        raise HTTPException(status_code=403, detail="Not your player")
    response = await clients["game"].post(f"/games/{game_id}/trade/offer", json=payload.model_dump())
    response.raise_for_status()
    state = await clients["game"].get(f"/games/{game_id}")
    state.raise_for_status()
    await _broadcast(game_id, state.json())
    return response.json()


@app.post("/api/games/{game_id}/trade/{trade_id}/respond")
async def respond_trade(game_id: str, trade_id: str, payload: TradeRespondRequest, user: User = Depends(_get_current_user)):
    if payload.player != user.username:
        raise HTTPException(status_code=403, detail="Not your player")
    response = await clients["game"].post(
        f"/games/{game_id}/trade/{trade_id}/respond",
        json=payload.model_dump(),
    )
    response.raise_for_status()
    state = await clients["game"].get(f"/games/{game_id}")
    state.raise_for_status()
    await _broadcast(game_id, state.json())
    return response.json()


@app.get("/api/models")
async def list_models(user: User = Depends(_get_current_user)):
    response = await clients["ai"].get("/models")
    response.raise_for_status()
    return response.json()


@app.get("/api/replays")
async def list_replays(user: User = Depends(_get_current_user)):
    response = await clients["analytics"].get("/replays")
    response.raise_for_status()
    return response.json()


@app.get("/api/replays/{replay_id}")
async def replay_detail(replay_id: str, user: User = Depends(_get_current_user)):
    response = await clients["analytics"].get(f"/replays/{replay_id}")
    response.raise_for_status()
    return response.json()


@app.get("/api/replays/{replay_id}/state")
async def replay_state(replay_id: str, step: int = 0, user: User = Depends(_get_current_user)):
    response = await clients["analytics"].get(f"/replays/{replay_id}/state", params={"step": step})
    response.raise_for_status()
    return response.json()


@app.get("/api/replays/{replay_id}/export")
async def replay_export(replay_id: str, user: User = Depends(_get_current_user)):
    response = await clients["analytics"].get(f"/replays/{replay_id}/export")
    response.raise_for_status()
    return response.json()


@app.post("/api/replays/import")
async def replay_import(payload: dict, user: User = Depends(_get_current_user)):
    response = await clients["analytics"].post("/replays/import", json=payload)
    if response.status_code >= 400:
        raise HTTPException(status_code=response.status_code, detail=response.json().get("detail"))
    return response.json()


@app.get("/api/stats")
async def stats(user: User = Depends(_get_current_user)):
    response = await clients["analytics"].get("/stats", params={"user": user.username})
    response.raise_for_status()
    return response.json()


@app.websocket("/ws/games/{game_id}")
async def ws_game(websocket: WebSocket, game_id: str):
    await websocket.accept()
    try:
        user = _get_user_from_ws(websocket)
    except HTTPException:
        await websocket.close(code=1008)
        return
    ws_connections.setdefault(game_id, {})[websocket] = user.username
    try:
        response = await clients["game"].get(f"/games/{game_id}")
        if response.status_code == 200:
            await websocket.send_json({"type": "state", "data": _mask_state_for_user(response.json(), user.username)})
        while True:
            await websocket.receive_text()
    except WebSocketDisconnect:
        ws_connections.get(game_id, {}).pop(websocket, None)
