"""Business logic for stations and recipe assignments.

Routers must call into these functions rather than manipulating models directly.
All functions are async and accept an AsyncSession; they flush but do NOT commit
(commit is handled by the FastAPI get_db dependency).
"""
from typing import Optional

from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.backend.models.orm.recipe import Recipe
from src.backend.models.orm.station import Station, StationRecipeAssignment
from src.backend.models.orm.user import User
from src.backend.models.api.station import StationCreate, StationUpdate


async def create_station(
    db: AsyncSession, data: StationCreate, creator: User,
) -> Station:
    existing = await db.execute(select(Station).where(Station.code == data.code))
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Station code '{data.code}' already exists",
        )
    station = Station(
        code=data.code,
        name=data.name,
        location=data.location,
        notes=data.notes,
        active=data.active,
        created_by=creator.id,
    )
    db.add(station)
    await db.flush()
    await db.refresh(station)
    return station


async def get_station(db: AsyncSession, station_id: int) -> Station:
    result = await db.execute(select(Station).where(Station.id == station_id))
    station = result.scalar_one_or_none()
    if station is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Station not found",
        )
    return station


async def get_station_by_code(db: AsyncSession, code: str) -> Optional[Station]:
    result = await db.execute(select(Station).where(Station.code == code))
    return result.scalar_one_or_none()


async def list_stations(db: AsyncSession, active_only: bool = False) -> list[Station]:
    query = select(Station).order_by(Station.code)
    if active_only:
        query = query.where(Station.active == True)  # noqa: E712
    result = await db.execute(query)
    return list(result.scalars().all())


async def update_station(
    db: AsyncSession, station_id: int, data: StationUpdate,
) -> Station:
    station = await get_station(db, station_id)
    for field, value in data.model_dump(exclude_unset=True).items():
        setattr(station, field, value)
    await db.flush()
    await db.refresh(station)
    return station


async def delete_station(db: AsyncSession, station_id: int) -> None:
    station = await get_station(db, station_id)
    # Explicitly delete assignments so the ORM cascade fires within the
    # current session (SQLite test DB does not enforce FK CASCADE without
    # PRAGMA foreign_keys = ON; production MySQL handles it at DB level too,
    # but explicit ORM deletion is engine-agnostic and safer).
    assignments = await db.execute(
        select(StationRecipeAssignment).where(
            StationRecipeAssignment.station_id == station_id
        )
    )
    for assignment in assignments.scalars().all():
        await db.delete(assignment)
    await db.delete(station)
    await db.flush()


async def assign_recipe(
    db: AsyncSession, station_id: int, recipe_id: int, assigner: User,
) -> StationRecipeAssignment:
    await get_station(db, station_id)
    recipe_row = await db.execute(select(Recipe).where(Recipe.id == recipe_id))
    if recipe_row.scalar_one_or_none() is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Recipe not found",
        )
    existing = await db.execute(
        select(StationRecipeAssignment).where(
            StationRecipeAssignment.station_id == station_id,
            StationRecipeAssignment.recipe_id == recipe_id,
        )
    )
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Recipe already assigned to this station",
        )
    assignment = StationRecipeAssignment(
        station_id=station_id, recipe_id=recipe_id, assigned_by=assigner.id,
    )
    db.add(assignment)
    await db.flush()
    await db.refresh(assignment)
    return assignment


async def unassign_recipe(
    db: AsyncSession, station_id: int, recipe_id: int,
) -> None:
    result = await db.execute(
        select(StationRecipeAssignment).where(
            StationRecipeAssignment.station_id == station_id,
            StationRecipeAssignment.recipe_id == recipe_id,
        )
    )
    assignment = result.scalar_one_or_none()
    if assignment is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Assignment not found",
        )
    await db.delete(assignment)
    await db.flush()


async def list_station_recipes(
    db: AsyncSession, station_id: int,
) -> list[Recipe]:
    """Return active recipes assigned to this station, ordered by code."""
    await get_station(db, station_id)
    result = await db.execute(
        select(Recipe)
        .join(StationRecipeAssignment, StationRecipeAssignment.recipe_id == Recipe.id)
        .where(
            StationRecipeAssignment.station_id == station_id,
            Recipe.active == True,  # noqa: E712
        )
        .order_by(Recipe.code)
    )
    return list(result.scalars().all())
