import json
from pathlib import Path

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.core.database import get_session
from app.models.boss_battle import BossBattle
from app.models.game import Game
from app.models.pokemon import Pokemon
from app.models.route import Route
from app.models.route_encounter import RouteEncounter
from app.schemas.game import (
    GameCreate,
    GameDetailResponse,
    GameResponse,
    GameUpdate,
    RegionResponse,
    RouteCreate,
    RouteReorderRequest,
    RouteResponse,
    RouteUpdate,
    RouteWithChildrenResponse,
)
from app.schemas.pokemon import BulkImportResult, BulkRouteItem
from app.seeds.loader import upsert_route_encounters, upsert_routes

router = APIRouter()


async def _get_game_or_404(session: AsyncSession, game_id: int) -> Game:
    game = await session.get(Game, game_id)
    if game is None:
        raise HTTPException(status_code=404, detail="Game not found")
    return game


async def _get_version_group_id(session: AsyncSession, game_id: int) -> int:
    game = await _get_game_or_404(session, game_id)
    if game.version_group_id is None:
        raise HTTPException(status_code=400, detail="Game has no version group assigned")
    return game.version_group_id


@router.get("", response_model=list[GameResponse])
async def list_games(session: AsyncSession = Depends(get_session)):
    result = await session.execute(select(Game).order_by(Game.id))
    return result.scalars().all()


@router.get("/by-region", response_model=list[RegionResponse])
async def list_games_by_region(session: AsyncSession = Depends(get_session)):
    """Return games grouped by region with generation metadata and genlocke preset defaults."""
    regions_path = Path(__file__).parent.parent / "seeds" / "data" / "regions.json"
    with open(regions_path) as f:
        regions_data = json.load(f)

    result = await session.execute(select(Game).order_by(Game.release_year, Game.name))
    all_games = result.scalars().all()

    games_by_region: dict[str, list[Game]] = {}
    for game in all_games:
        games_by_region.setdefault(game.region, []).append(game)

    response = []
    for region in regions_data:
        region_games = games_by_region.get(region["name"], [])
        defaults = region["genlocke_defaults"]
        response.append({
            "name": region["name"],
            "generation": region["generation"],
            "order": region["order"],
            "genlocke_defaults": {
                "true_genlocke": defaults["true"],
                "normal_genlocke": defaults["normal"],
            },
            "games": region_games,
        })

    return response


@router.get("/{game_id}", response_model=GameDetailResponse)
async def get_game(game_id: int, session: AsyncSession = Depends(get_session)):
    game = await _get_game_or_404(session, game_id)
    vg_id = game.version_group_id

    # Load routes via version_group_id
    result = await session.execute(
        select(Route)
        .where(Route.version_group_id == vg_id)
        .order_by(Route.order)
    )
    routes = result.scalars().all()

    # Attach routes to game for serialization
    return {
        "id": game.id,
        "name": game.name,
        "slug": game.slug,
        "generation": game.generation,
        "region": game.region,
        "category": game.category,
        "box_art_url": game.box_art_url,
        "release_year": game.release_year,
        "color": game.color,
        "version_group_id": game.version_group_id,
        "routes": [
            {
                "id": r.id,
                "name": r.name,
                "version_group_id": r.version_group_id,
                "order": r.order,
                "parent_route_id": r.parent_route_id,
                "pinwheel_zone": r.pinwheel_zone,
                "encounter_methods": [],
            }
            for r in routes
        ],
    }


@router.get(
    "/{game_id}/routes",
    response_model=list[RouteWithChildrenResponse] | list[RouteResponse],
)
async def list_game_routes(
    game_id: int,
    flat: bool = False,
    session: AsyncSession = Depends(get_session),
):
    """
    List routes for a game.

    By default, returns a hierarchical structure with top-level routes containing
    nested children. Use `flat=True` to get a flat list of all routes.
    """
    vg_id = await _get_version_group_id(session, game_id)

    result = await session.execute(
        select(Route)
        .where(Route.version_group_id == vg_id)
        .options(selectinload(Route.route_encounters))
        .order_by(Route.order)
    )
    all_routes = result.scalars().all()

    def route_to_dict(route: Route) -> dict:
        # Only show encounter methods for the requested game
        methods = sorted({
            re.encounter_method for re in route.route_encounters
            if re.game_id == game_id
        })
        return {
            "id": route.id,
            "name": route.name,
            "version_group_id": route.version_group_id,
            "order": route.order,
            "parent_route_id": route.parent_route_id,
            "pinwheel_zone": route.pinwheel_zone,
            "encounter_methods": methods,
        }

    if flat:
        return [route_to_dict(r) for r in all_routes]

    # Build hierarchical structure
    # Group children by parent_route_id
    children_by_parent: dict[int, list[dict]] = {}
    top_level_routes: list[Route] = []

    for route in all_routes:
        if route.parent_route_id is None:
            top_level_routes.append(route)
        else:
            children_by_parent.setdefault(route.parent_route_id, []).append(
                route_to_dict(route)
            )

    # Build response with nested children
    response = []
    for route in top_level_routes:
        route_dict = route_to_dict(route)
        route_dict["children"] = children_by_parent.get(route.id, [])
        response.append(route_dict)

    return response


# --- Admin endpoints ---


@router.post("", response_model=GameResponse, status_code=201)
async def create_game(
    data: GameCreate, session: AsyncSession = Depends(get_session)
):
    existing = await session.execute(
        select(Game).where(Game.slug == data.slug)
    )
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(status_code=409, detail="Game with this slug already exists")

    game = Game(**data.model_dump())
    session.add(game)
    await session.commit()
    await session.refresh(game)
    return game


@router.put("/{game_id}", response_model=GameResponse)
async def update_game(
    game_id: int, data: GameUpdate, session: AsyncSession = Depends(get_session)
):
    game = await session.get(Game, game_id)
    if game is None:
        raise HTTPException(status_code=404, detail="Game not found")

    update_data = data.model_dump(exclude_unset=True)
    if "slug" in update_data:
        existing = await session.execute(
            select(Game).where(Game.slug == update_data["slug"], Game.id != game_id)
        )
        if existing.scalar_one_or_none() is not None:
            raise HTTPException(status_code=409, detail="Game with this slug already exists")

    for field, value in update_data.items():
        setattr(game, field, value)

    await session.commit()
    await session.refresh(game)
    return game


@router.delete("/{game_id}", status_code=204)
async def delete_game(
    game_id: int, session: AsyncSession = Depends(get_session)
):
    result = await session.execute(
        select(Game).where(Game.id == game_id).options(selectinload(Game.runs))
    )
    game = result.scalar_one_or_none()
    if game is None:
        raise HTTPException(status_code=404, detail="Game not found")

    if game.runs:
        raise HTTPException(
            status_code=409,
            detail="Cannot delete game with existing runs. Delete the runs first.",
        )

    vg_id = game.version_group_id

    # Delete game-specific route_encounters
    await session.execute(
        delete(RouteEncounter).where(RouteEncounter.game_id == game_id)
    )

    # Check if this is the last game in the version group
    other_games = await session.execute(
        select(Game).where(Game.version_group_id == vg_id, Game.id != game_id)
    )
    is_last_in_group = other_games.scalar_one_or_none() is None

    if is_last_in_group and vg_id is not None:
        # Delete boss battles
        await session.execute(
            delete(BossBattle).where(BossBattle.version_group_id == vg_id)
        )
        # Delete routes (children first due to parent FK)
        child_routes = await session.execute(
            select(Route).where(
                Route.version_group_id == vg_id,
                Route.parent_route_id.isnot(None),
            )
        )
        for route in child_routes.scalars().all():
            await session.delete(route)
        await session.flush()
        parent_routes = await session.execute(
            select(Route).where(Route.version_group_id == vg_id)
        )
        for route in parent_routes.scalars().all():
            await session.delete(route)

    await session.delete(game)
    await session.commit()


@router.post("/{game_id}/routes", response_model=RouteResponse, status_code=201)
async def create_route(
    game_id: int, data: RouteCreate, session: AsyncSession = Depends(get_session)
):
    vg_id = await _get_version_group_id(session, game_id)

    route = Route(version_group_id=vg_id, **data.model_dump())
    session.add(route)
    await session.commit()
    await session.refresh(route)
    return route


@router.put("/{game_id}/routes/reorder", response_model=list[RouteResponse])
async def reorder_routes(
    game_id: int,
    data: RouteReorderRequest,
    session: AsyncSession = Depends(get_session),
):
    vg_id = await _get_version_group_id(session, game_id)

    for item in data.routes:
        route = await session.get(Route, item.id)
        if route is None or route.version_group_id != vg_id:
            raise HTTPException(
                status_code=400,
                detail=f"Route {item.id} not found in this game",
            )
        route.order = item.order

    await session.commit()

    result = await session.execute(
        select(Route).where(Route.version_group_id == vg_id).order_by(Route.order)
    )
    return result.scalars().all()


@router.put("/{game_id}/routes/{route_id}", response_model=RouteResponse)
async def update_route(
    game_id: int,
    route_id: int,
    data: RouteUpdate,
    session: AsyncSession = Depends(get_session),
):
    vg_id = await _get_version_group_id(session, game_id)

    route = await session.get(Route, route_id)
    if route is None or route.version_group_id != vg_id:
        raise HTTPException(status_code=404, detail="Route not found in this game")

    for field, value in data.model_dump(exclude_unset=True).items():
        setattr(route, field, value)

    await session.commit()
    await session.refresh(route)
    return route


@router.delete("/{game_id}/routes/{route_id}", status_code=204)
async def delete_route(
    game_id: int,
    route_id: int,
    session: AsyncSession = Depends(get_session),
):
    vg_id = await _get_version_group_id(session, game_id)

    result = await session.execute(
        select(Route)
        .where(Route.id == route_id, Route.version_group_id == vg_id)
        .options(selectinload(Route.encounters))
    )
    route = result.scalar_one_or_none()
    if route is None:
        raise HTTPException(status_code=404, detail="Route not found in this game")

    if route.encounters:
        raise HTTPException(
            status_code=409,
            detail="Cannot delete route with existing encounters. Delete the encounters first.",
        )

    await session.delete(route)
    await session.commit()


@router.post("/{game_id}/routes/bulk-import", response_model=BulkImportResult)
async def bulk_import_routes(
    game_id: int,
    items: list[BulkRouteItem],
    session: AsyncSession = Depends(get_session),
):
    vg_id = await _get_version_group_id(session, game_id)

    # Build pokeapi_id -> id mapping for encounter resolution
    result = await session.execute(select(Pokemon.pokeapi_id, Pokemon.id))
    dex_to_id = {row.pokeapi_id: row.id for row in result}

    errors: list[str] = []

    # Upsert routes using the seed loader (handles parent/child hierarchy)
    routes_data = [item.model_dump() for item in items]
    try:
        route_name_to_id = await upsert_routes(session, vg_id, routes_data)
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Failed to import routes: {e}")

    # Upsert encounters for each route
    encounter_count = 0
    for item in items:
        route_id = route_name_to_id.get(item.name)
        if route_id is None:
            errors.append(f"Route '{item.name}' not found after upsert")
            continue

        if item.encounters:
            try:
                count = await upsert_route_encounters(
                    session, route_id, [e.model_dump() for e in item.encounters],
                    dex_to_id, game_id,
                )
                encounter_count += count
            except Exception as e:
                errors.append(f"Encounters for '{item.name}': {e}")

        for child in item.children:
            child_id = route_name_to_id.get(child.name)
            if child_id is None:
                errors.append(f"Child route '{child.name}' not found after upsert")
                continue

            if child.encounters:
                try:
                    count = await upsert_route_encounters(
                        session, child_id, [e.model_dump() for e in child.encounters],
                        dex_to_id, game_id,
                    )
                    encounter_count += count
                except Exception as e:
                    errors.append(f"Encounters for '{child.name}': {e}")

    await session.commit()

    route_count = len(route_name_to_id)
    return BulkImportResult(
        created=route_count,
        updated=encounter_count,
        errors=errors,
    )
