from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, selectinload

from app.core.database import get_session
from app.models.evolution import Evolution
from app.models.pokemon import Pokemon
from app.models.route import Route
from app.models.route_encounter import RouteEncounter
from app.models.game import Game
from app.schemas.pokemon import (
    BulkImportItem,
    BulkImportResult,
    EvolutionAdminResponse,
    EvolutionResponse,
    FamiliesResponse,
    PaginatedPokemonResponse,
    PokemonCreate,
    PokemonEncounterLocationItem,
    PokemonEncounterLocationResponse,
    PokemonResponse,
    PokemonUpdate,
    RouteEncounterCreate,
    RouteEncounterDetailResponse,
    RouteEncounterUpdate,
)

router = APIRouter()


@router.get("/pokemon", response_model=PaginatedPokemonResponse)
async def list_pokemon(
    search: str | None = Query(None),
    type: str | None = Query(None),
    limit: int = Query(50, ge=1, le=500),
    offset: int = Query(0, ge=0),
    session: AsyncSession = Depends(get_session),
):
    # Build base query with optional search filter
    base_query = select(Pokemon)
    if search:
        base_query = base_query.where(
            func.lower(Pokemon.name).contains(search.lower())
        )
    if type:
        base_query = base_query.where(Pokemon.types.any(type))

    # Get total count
    count_query = select(func.count()).select_from(base_query.subquery())
    total = (await session.execute(count_query)).scalar() or 0

    # Get paginated items
    items_query = base_query.order_by(Pokemon.national_dex, Pokemon.name).offset(offset).limit(limit)
    result = await session.execute(items_query)
    items = result.scalars().all()

    return PaginatedPokemonResponse(
        items=items,
        total=total,
        limit=limit,
        offset=offset,
    )


@router.post("/pokemon/bulk-import", response_model=BulkImportResult)
async def bulk_import_pokemon(
    items: list[BulkImportItem],
    session: AsyncSession = Depends(get_session),
):
    created = 0
    updated = 0
    errors: list[str] = []

    for item in items:
        try:
            existing = await session.execute(
                select(Pokemon).where(Pokemon.pokeapi_id == item.pokeapi_id)
            )
            pokemon = existing.scalar_one_or_none()

            if pokemon is not None:
                pokemon.national_dex = item.national_dex
                pokemon.name = item.name
                pokemon.types = item.types
                if item.sprite_url is not None:
                    pokemon.sprite_url = item.sprite_url
                updated += 1
            else:
                pokemon = Pokemon(**item.model_dump())
                session.add(pokemon)
                created += 1
        except Exception as e:
            errors.append(f"PokeAPI #{item.pokeapi_id} ({item.name}): {e}")

    await session.commit()
    return BulkImportResult(created=created, updated=updated, errors=errors)


@router.post("/pokemon", response_model=PokemonResponse, status_code=201)
async def create_pokemon(
    data: PokemonCreate, session: AsyncSession = Depends(get_session)
):
    existing = await session.execute(
        select(Pokemon).where(Pokemon.pokeapi_id == data.pokeapi_id)
    )
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=409,
            detail=f"Pokemon with PokeAPI ID #{data.pokeapi_id} already exists",
        )

    pokemon = Pokemon(**data.model_dump())
    session.add(pokemon)
    await session.commit()
    await session.refresh(pokemon)
    return pokemon


@router.get("/pokemon/families", response_model=FamiliesResponse)
async def get_pokemon_families(
    session: AsyncSession = Depends(get_session),
):
    """Return evolution families as connected components of Pokemon IDs."""
    from collections import deque

    result = await session.execute(select(Evolution))
    evolutions = result.scalars().all()

    # Build undirected adjacency list
    adj: dict[int, set[int]] = {}
    for evo in evolutions:
        adj.setdefault(evo.from_pokemon_id, set()).add(evo.to_pokemon_id)
        adj.setdefault(evo.to_pokemon_id, set()).add(evo.from_pokemon_id)

    # BFS to find connected components
    visited: set[int] = set()
    families: list[list[int]] = []
    for node in adj:
        if node in visited:
            continue
        component: list[int] = []
        queue = deque([node])
        while queue:
            current = queue.popleft()
            if current in visited:
                continue
            visited.add(current)
            component.append(current)
            for neighbor in adj.get(current, set()):
                if neighbor not in visited:
                    queue.append(neighbor)
        families.append(sorted(component))

    return FamiliesResponse(families=families)


@router.get("/pokemon/{pokemon_id}", response_model=PokemonResponse)
async def get_pokemon(
    pokemon_id: int, session: AsyncSession = Depends(get_session)
):
    pokemon = await session.get(Pokemon, pokemon_id)
    if pokemon is None:
        raise HTTPException(status_code=404, detail="Pokemon not found")
    return pokemon


@router.get("/pokemon/{pokemon_id}/forms", response_model=list[PokemonResponse])
async def get_pokemon_forms(
    pokemon_id: int, session: AsyncSession = Depends(get_session)
):
    pokemon = await session.get(Pokemon, pokemon_id)
    if pokemon is None:
        raise HTTPException(status_code=404, detail="Pokemon not found")

    result = await session.execute(
        select(Pokemon)
        .where(Pokemon.national_dex == pokemon.national_dex, Pokemon.id != pokemon_id)
        .order_by(Pokemon.pokeapi_id)
    )
    return result.scalars().all()


@router.get(
    "/pokemon/{pokemon_id}/encounter-locations",
    response_model=list[PokemonEncounterLocationResponse],
)
async def get_pokemon_encounter_locations(
    pokemon_id: int, session: AsyncSession = Depends(get_session)
):
    pokemon = await session.get(Pokemon, pokemon_id)
    if pokemon is None:
        raise HTTPException(status_code=404, detail="Pokemon not found")

    result = await session.execute(
        select(RouteEncounter)
        .where(RouteEncounter.pokemon_id == pokemon_id)
        .options(joinedload(RouteEncounter.route), joinedload(RouteEncounter.game))
        .order_by(RouteEncounter.game_id, RouteEncounter.route_id)
    )
    encounters = result.scalars().unique().all()

    grouped: dict[int, PokemonEncounterLocationResponse] = {}
    for enc in encounters:
        if enc.game_id not in grouped:
            grouped[enc.game_id] = PokemonEncounterLocationResponse(
                game_id=enc.game_id,
                game_name=enc.game.name,
                encounters=[],
            )
        grouped[enc.game_id].encounters.append(
            PokemonEncounterLocationItem(
                route_id=enc.route_id,
                route_name=enc.route.name,
                encounter_method=enc.encounter_method,
                encounter_rate=enc.encounter_rate,
                min_level=enc.min_level,
                max_level=enc.max_level,
            )
        )

    return list(grouped.values())


@router.get(
    "/pokemon/{pokemon_id}/evolution-chain",
    response_model=list[EvolutionAdminResponse],
)
async def get_pokemon_evolution_chain(
    pokemon_id: int, session: AsyncSession = Depends(get_session)
):
    from collections import deque

    pokemon = await session.get(Pokemon, pokemon_id)
    if pokemon is None:
        raise HTTPException(status_code=404, detail="Pokemon not found")

    # Load all evolutions to build adjacency
    result = await session.execute(select(Evolution))
    evolutions = result.scalars().all()

    adj: dict[int, set[int]] = {}
    for evo in evolutions:
        adj.setdefault(evo.from_pokemon_id, set()).add(evo.to_pokemon_id)
        adj.setdefault(evo.to_pokemon_id, set()).add(evo.from_pokemon_id)

    # BFS from pokemon_id to find family members
    family: set[int] = set()
    queue = deque([pokemon_id])
    while queue:
        current = queue.popleft()
        if current in family:
            continue
        family.add(current)
        for neighbor in adj.get(current, set()):
            if neighbor not in family:
                queue.append(neighbor)

    # Filter evolutions to only those in the family
    family_evo_ids = [
        evo.id for evo in evolutions
        if evo.from_pokemon_id in family and evo.to_pokemon_id in family
    ]

    if not family_evo_ids:
        return []

    # Reload with eager-loaded relationships
    result = await session.execute(
        select(Evolution)
        .where(Evolution.id.in_(family_evo_ids))
        .options(
            joinedload(Evolution.from_pokemon),
            joinedload(Evolution.to_pokemon),
        )
        .order_by(Evolution.from_pokemon_id, Evolution.to_pokemon_id)
    )
    return result.scalars().unique().all()


@router.get("/pokemon/{pokemon_id}/evolutions", response_model=list[EvolutionResponse])
async def get_pokemon_evolutions(
    pokemon_id: int,
    region: str | None = Query(None),
    session: AsyncSession = Depends(get_session),
):
    pokemon = await session.get(Pokemon, pokemon_id)
    if pokemon is None:
        raise HTTPException(status_code=404, detail="Pokemon not found")

    query = (
        select(Evolution)
        .where(Evolution.from_pokemon_id == pokemon_id)
        .options(joinedload(Evolution.to_pokemon))
    )
    if region is not None:
        query = query.where(
            or_(Evolution.region.is_(None), Evolution.region == region)
        )
    result = await session.execute(query)
    evolutions = result.scalars().unique().all()

    if region is not None:
        # Regional evolutions replace the non-regional one that shares the
        # same trigger + item (e.g. Pikachu + thunder-stone → Alolan Raichu
        # replaces Pikachu + thunder-stone → Raichu in Alola).
        regional_keys = {
            (e.trigger, e.item) for e in evolutions if e.region is not None
        }
        if regional_keys:
            evolutions = [
                e for e in evolutions
                if e.region is not None or (e.trigger, e.item) not in regional_keys
            ]

    return evolutions


@router.put("/pokemon/{pokemon_id}", response_model=PokemonResponse)
async def update_pokemon(
    pokemon_id: int,
    data: PokemonUpdate,
    session: AsyncSession = Depends(get_session),
):
    pokemon = await session.get(Pokemon, pokemon_id)
    if pokemon is None:
        raise HTTPException(status_code=404, detail="Pokemon not found")

    update_data = data.model_dump(exclude_unset=True)
    if "pokeapi_id" in update_data:
        existing = await session.execute(
            select(Pokemon).where(
                Pokemon.pokeapi_id == update_data["pokeapi_id"],
                Pokemon.id != pokemon_id,
            )
        )
        if existing.scalar_one_or_none() is not None:
            raise HTTPException(
                status_code=409,
                detail=f"Pokemon with PokeAPI ID #{update_data['pokeapi_id']} already exists",
            )

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

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


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

    if pokemon.encounters:
        raise HTTPException(
            status_code=409,
            detail="Cannot delete pokemon with existing encounters.",
        )

    await session.delete(pokemon)
    await session.commit()


@router.get(
    "/routes/{route_id}/pokemon",
    response_model=list[RouteEncounterDetailResponse],
)
async def list_route_encounters(
    route_id: int,
    game_id: int | None = Query(None),
    session: AsyncSession = Depends(get_session),
):
    # Verify route exists
    route = await session.get(Route, route_id)
    if route is None:
        raise HTTPException(status_code=404, detail="Route not found")

    query = (
        select(RouteEncounter)
        .where(RouteEncounter.route_id == route_id)
        .options(joinedload(RouteEncounter.pokemon))
        .order_by(RouteEncounter.encounter_rate.desc())
    )
    if game_id is not None:
        query = query.where(RouteEncounter.game_id == game_id)

    result = await session.execute(query)
    return result.scalars().unique().all()


@router.post(
    "/routes/{route_id}/pokemon",
    response_model=RouteEncounterDetailResponse,
    status_code=201,
)
async def add_route_encounter(
    route_id: int,
    data: RouteEncounterCreate,
    session: AsyncSession = Depends(get_session),
):
    route = await session.get(Route, route_id)
    if route is None:
        raise HTTPException(status_code=404, detail="Route not found")

    pokemon = await session.get(Pokemon, data.pokemon_id)
    if pokemon is None:
        raise HTTPException(status_code=404, detail="Pokemon not found")

    encounter = RouteEncounter(route_id=route_id, **data.model_dump())
    session.add(encounter)
    await session.commit()

    # Reload with pokemon relationship
    result = await session.execute(
        select(RouteEncounter)
        .where(RouteEncounter.id == encounter.id)
        .options(joinedload(RouteEncounter.pokemon))
    )
    return result.scalar_one()


@router.put(
    "/routes/{route_id}/pokemon/{encounter_id}",
    response_model=RouteEncounterDetailResponse,
)
async def update_route_encounter(
    route_id: int,
    encounter_id: int,
    data: RouteEncounterUpdate,
    session: AsyncSession = Depends(get_session),
):
    result = await session.execute(
        select(RouteEncounter)
        .where(RouteEncounter.id == encounter_id, RouteEncounter.route_id == route_id)
        .options(joinedload(RouteEncounter.pokemon))
    )
    encounter = result.scalar_one_or_none()
    if encounter is None:
        raise HTTPException(status_code=404, detail="Route encounter not found")

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

    await session.commit()
    await session.refresh(encounter)

    # Reload with pokemon relationship
    result = await session.execute(
        select(RouteEncounter)
        .where(RouteEncounter.id == encounter.id)
        .options(joinedload(RouteEncounter.pokemon))
    )
    return result.scalar_one()


@router.delete("/routes/{route_id}/pokemon/{encounter_id}", status_code=204)
async def remove_route_encounter(
    route_id: int,
    encounter_id: int,
    session: AsyncSession = Depends(get_session),
):
    encounter = await session.execute(
        select(RouteEncounter).where(
            RouteEncounter.id == encounter_id,
            RouteEncounter.route_id == route_id,
        )
    )
    encounter = encounter.scalar_one_or_none()
    if encounter is None:
        raise HTTPException(status_code=404, detail="Route encounter not found")

    await session.delete(encounter)
    await session.commit()
