import random

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

from app.core.database import get_session
from app.models.encounter import Encounter
from app.models.evolution import Evolution
from app.models.genlocke_transfer import GenlockeTransfer
from app.models.genlocke import GenlockeLeg
from app.models.nuzlocke_run import NuzlockeRun
from app.models.pokemon import Pokemon
from app.models.route import Route
from app.models.route_encounter import RouteEncounter
from app.schemas.encounter import (
    BulkRandomizeResponse,
    EncounterCreate,
    EncounterDetailResponse,
    EncounterResponse,
    EncounterUpdate,
)
from app.services.families import build_families

router = APIRouter()


@router.post(
    "/runs/{run_id}/encounters",
    response_model=EncounterResponse,
    status_code=201,
)
async def create_encounter(
    run_id: int,
    data: EncounterCreate,
    session: AsyncSession = Depends(get_session),
):
    # Validate run exists
    run = await session.get(NuzlockeRun, run_id)
    if run is None:
        raise HTTPException(status_code=404, detail="Run not found")

    # Validate route exists and load its children
    result = await session.execute(
        select(Route)
        .where(Route.id == data.route_id)
        .options(selectinload(Route.children))
    )
    route = result.scalar_one_or_none()
    if route is None:
        raise HTTPException(status_code=404, detail="Route not found")

    # Cannot create encounter on a parent route (routes with children)
    if route.children:
        raise HTTPException(
            status_code=400,
            detail="Cannot create encounter on a parent route. Use a child route instead.",
        )

    # Shiny clause: shiny encounters bypass the route-lock check
    shiny_clause_on = run.rules.get("shinyClause", True) if run.rules else True
    skip_route_lock = (data.is_shiny and shiny_clause_on) or data.origin in ("shed_evolution", "egg", "transfer")

    # If this route has a parent, check if sibling already has an encounter
    if route.parent_route_id is not None and not skip_route_lock:
        # Get all sibling routes (routes with same parent, including this one)
        siblings_result = await session.execute(
            select(Route).where(Route.parent_route_id == route.parent_route_id)
        )
        siblings = siblings_result.scalars().all()

        # Determine which siblings to check based on pinwheel clause
        pinwheel_on = run.rules.get("pinwheelClause", True) if run.rules else True
        any_has_zone = any(s.pinwheel_zone is not None for s in siblings)

        if pinwheel_on and any_has_zone:
            # Zone-aware: only check siblings in the same zone (null treated as 0)
            my_zone = route.pinwheel_zone if route.pinwheel_zone is not None else 0
            sibling_ids = [
                s.id for s in siblings
                if (s.pinwheel_zone if s.pinwheel_zone is not None else 0) == my_zone
            ]
        else:
            # No pinwheel clause or no zones defined: all siblings share
            sibling_ids = [s.id for s in siblings]

        # Check if any relevant sibling already has an encounter in this run
        # Exclude transfer-target encounters so they don't block the starter
        transfer_target_ids = select(GenlockeTransfer.target_encounter_id)
        existing_encounter = await session.execute(
            select(Encounter)
            .where(
                Encounter.run_id == run_id,
                Encounter.route_id.in_(sibling_ids),
                ~Encounter.id.in_(transfer_target_ids),
            )
        )
        if existing_encounter.scalar_one_or_none() is not None:
            raise HTTPException(
                status_code=409,
                detail="This location group already has an encounter. Only one encounter per location group is allowed.",
            )

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

    encounter = Encounter(
        run_id=run_id,
        route_id=data.route_id,
        pokemon_id=data.pokemon_id,
        nickname=data.nickname,
        status=data.status,
        catch_level=data.catch_level,
        is_shiny=data.is_shiny,
    )
    session.add(encounter)
    await session.commit()
    await session.refresh(encounter)
    return encounter


@router.patch("/encounters/{encounter_id}", response_model=EncounterDetailResponse)
async def update_encounter(
    encounter_id: int,
    data: EncounterUpdate,
    session: AsyncSession = Depends(get_session),
):
    encounter = await session.get(Encounter, encounter_id)
    if encounter is None:
        raise HTTPException(status_code=404, detail="Encounter not found")

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

    await session.commit()

    # Reload with relationships for detail response
    result = await session.execute(
        select(Encounter)
        .where(Encounter.id == encounter_id)
        .options(
            joinedload(Encounter.pokemon),
            joinedload(Encounter.current_pokemon),
            joinedload(Encounter.route),
        )
    )
    return result.scalar_one()


@router.delete("/encounters/{encounter_id}", status_code=204)
async def delete_encounter(
    encounter_id: int, session: AsyncSession = Depends(get_session)
):
    encounter = await session.get(Encounter, encounter_id)
    if encounter is None:
        raise HTTPException(status_code=404, detail="Encounter not found")

    # Block deletion if encounter is referenced by a genlocke transfer
    transfer_result = await session.execute(
        select(GenlockeTransfer.id).where(
            (GenlockeTransfer.source_encounter_id == encounter_id)
            | (GenlockeTransfer.target_encounter_id == encounter_id)
        )
    )
    if transfer_result.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=400,
            detail="Cannot delete an encounter that is part of a genlocke transfer.",
        )

    await session.delete(encounter)
    await session.commit()
    return Response(status_code=204)


@router.post(
    "/runs/{run_id}/encounters/bulk-randomize",
    response_model=BulkRandomizeResponse,
    status_code=201,
)
async def bulk_randomize_encounters(
    run_id: int,
    session: AsyncSession = Depends(get_session),
):
    # 1. Validate run
    run = await session.get(NuzlockeRun, run_id)
    if run is None:
        raise HTTPException(status_code=404, detail="Run not found")
    if run.status != "active":
        raise HTTPException(status_code=400, detail="Run is not active")

    game_id = run.game_id

    # 2. Get version_group_id from game
    from app.models.game import Game
    game = await session.get(Game, game_id)
    if game is None or game.version_group_id is None:
        raise HTTPException(status_code=400, detail="Game has no version group")
    version_group_id = game.version_group_id

    # 3. Load all routes for this version group (with children)
    routes_result = await session.execute(
        select(Route)
        .where(Route.version_group_id == version_group_id)
        .options(selectinload(Route.children))
        .order_by(Route.order)
    )
    all_routes = routes_result.scalars().unique().all()

    # 4. Load existing encounters for this run
    existing_result = await session.execute(
        select(Encounter).where(Encounter.run_id == run_id)
    )
    existing_encounters = existing_result.scalars().all()
    encountered_route_ids = {enc.route_id for enc in existing_encounters}

    # 5. Load all route_encounters for this game
    re_result = await session.execute(
        select(RouteEncounter).where(RouteEncounter.game_id == game_id)
    )
    all_route_encounters = re_result.scalars().all()

    # Build route_id → [pokemon_id, ...] mapping
    route_pokemon: dict[int, list[int]] = {}
    for re in all_route_encounters:
        route_pokemon.setdefault(re.route_id, [])
        if re.pokemon_id not in route_pokemon[re.route_id]:
            route_pokemon[re.route_id].append(re.pokemon_id)

    # 6. Load evolution families
    dupes_clause_on = run.rules.get("duplicatesClause", True) if run.rules else True
    pokemon_to_family: dict[int, list[int]] = {}
    if dupes_clause_on:
        evo_result = await session.execute(select(Evolution))
        evolutions = evo_result.scalars().all()
        pokemon_to_family = build_families(evolutions)

    # 7. Build initial duped set from existing caught encounters
    duped: set[int] = set()
    if dupes_clause_on:
        for enc in existing_encounters:
            if enc.status != "caught":
                continue
            duped.add(enc.pokemon_id)
            family = pokemon_to_family.get(enc.pokemon_id, [])
            for member in family:
                duped.add(member)

        # Seed duped set with retired Pokemon IDs from prior genlocke legs
        leg_result = await session.execute(
            select(GenlockeLeg).where(GenlockeLeg.run_id == run_id)
        )
        leg = leg_result.scalar_one_or_none()
        if leg:
            genlocke_result = await session.execute(
                select(GenlockeLeg.retired_pokemon_ids)
                .where(
                    GenlockeLeg.genlocke_id == leg.genlocke_id,
                    GenlockeLeg.leg_order < leg.leg_order,
                    GenlockeLeg.retired_pokemon_ids.isnot(None),
                )
            )
            for (retired_ids,) in genlocke_result:
                duped.update(retired_ids)

    # 8. Organize routes: identify top-level and children
    routes_by_id = {r.id: r for r in all_routes}
    top_level = [r for r in all_routes if r.parent_route_id is None]
    children_by_parent: dict[int, list[Route]] = {}
    for r in all_routes:
        if r.parent_route_id is not None:
            children_by_parent.setdefault(r.parent_route_id, []).append(r)

    pinwheel_on = run.rules.get("pinwheelClause", True) if run.rules else True

    # 9. Process routes in order, collecting target leaf routes
    created_encounters: list[Encounter] = []
    skipped = 0

    for parent_route in top_level:
        children = children_by_parent.get(parent_route.id, [])

        if len(children) == 0:
            # Standalone leaf route
            if parent_route.id in encountered_route_ids:
                continue
            available = route_pokemon.get(parent_route.id, [])
            eligible = [p for p in available if p not in duped] if dupes_clause_on else available
            if not eligible:
                skipped += 1
                continue
            picked = random.choice(eligible)
            enc = Encounter(
                run_id=run_id,
                route_id=parent_route.id,
                pokemon_id=picked,
                status="caught",
            )
            session.add(enc)
            created_encounters.append(enc)
            encountered_route_ids.add(parent_route.id)
            if dupes_clause_on:
                duped.add(picked)
                for member in pokemon_to_family.get(picked, []):
                    duped.add(member)
        else:
            # Route group — determine zone behavior
            any_has_zone = any(c.pinwheel_zone is not None for c in children)
            use_pinwheel = pinwheel_on and any_has_zone

            if use_pinwheel:
                # Zone-aware: one encounter per zone
                zones: dict[int, list[Route]] = {}
                for c in children:
                    zone = c.pinwheel_zone if c.pinwheel_zone is not None else 0
                    zones.setdefault(zone, []).append(c)

                for zone_num in sorted(zones.keys()):
                    zone_children = zones[zone_num]
                    # Check if any child in this zone already has an encounter
                    zone_has_encounter = any(
                        c.id in encountered_route_ids for c in zone_children
                    )
                    if zone_has_encounter:
                        continue

                    # Collect all pokemon from all children in this zone
                    zone_pokemon: list[int] = []
                    for c in zone_children:
                        for p in route_pokemon.get(c.id, []):
                            if p not in zone_pokemon:
                                zone_pokemon.append(p)

                    eligible = [p for p in zone_pokemon if p not in duped] if dupes_clause_on else zone_pokemon
                    if not eligible:
                        skipped += 1
                        continue

                    picked = random.choice(eligible)
                    # Pick a random child route in this zone to place the encounter
                    target_child = random.choice(zone_children)
                    enc = Encounter(
                        run_id=run_id,
                        route_id=target_child.id,
                        pokemon_id=picked,
                        status="caught",
                    )
                    session.add(enc)
                    created_encounters.append(enc)
                    encountered_route_ids.add(target_child.id)
                    if dupes_clause_on:
                        duped.add(picked)
                        for member in pokemon_to_family.get(picked, []):
                            duped.add(member)
            else:
                # Classic: one encounter for the whole group
                group_has_encounter = any(
                    c.id in encountered_route_ids for c in children
                )
                if group_has_encounter:
                    continue

                # Collect all pokemon from all children
                group_pokemon: list[int] = []
                for c in children:
                    for p in route_pokemon.get(c.id, []):
                        if p not in group_pokemon:
                            group_pokemon.append(p)

                eligible = [p for p in group_pokemon if p not in duped] if dupes_clause_on else group_pokemon
                if not eligible:
                    skipped += 1
                    continue

                picked = random.choice(eligible)
                # Pick a random child route to place the encounter
                target_child = random.choice(children)
                enc = Encounter(
                    run_id=run_id,
                    route_id=target_child.id,
                    pokemon_id=picked,
                    status="caught",
                )
                session.add(enc)
                created_encounters.append(enc)
                encountered_route_ids.add(target_child.id)
                if dupes_clause_on:
                    duped.add(picked)
                    for member in pokemon_to_family.get(picked, []):
                        duped.add(member)

    await session.commit()

    # Refresh all created encounters to get server-generated fields
    for enc in created_encounters:
        await session.refresh(enc)

    return BulkRandomizeResponse(
        created=created_encounters,
        skipped_routes=skipped,
    )
