from __future__ import annotations

from decimal import Decimal
from typing import List, Optional
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from pydantic import BaseModel
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.database import get_db
from app.models import Ingredient, IngredientGroceryMatch, IngredientMatchSource
from app.schemas.ingredient import (
    IngredientCreate,
    IngredientGroceryMatchRead,
    IngredientRead,
    IngredientUpdate,
)
from app.security import require_admin


public_router = APIRouter(prefix="/api/ingredients", tags=["ingredients"])
admin_router = APIRouter(
    prefix="/api/admin/ingredients",
    tags=["ingredients-admin"],
    dependencies=[Depends(require_admin)],
)


@public_router.get("", response_model=List[IngredientRead])
def list_ingredients(
    q: Optional[str] = Query(default=None),
    limit: int = Query(default=100, le=500),
    db: Session = Depends(get_db),
) -> List[Ingredient]:
    query = db.query(Ingredient)
    if q:
        like = f"%{q.lower()}%"
        query = query.filter(
            or_(
                Ingredient.name_lower.ilike(like),
                Ingredient.aliases.any(q),
            )
        )
    return query.order_by(Ingredient.name).limit(limit).all()


@public_router.get("/{ingredient_id}", response_model=IngredientRead)
def get_ingredient(ingredient_id: UUID, db: Session = Depends(get_db)) -> Ingredient:
    row = db.query(Ingredient).filter(Ingredient.id == ingredient_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="ingredient not found")
    return row


@public_router.post("", response_model=IngredientRead, status_code=status.HTTP_201_CREATED)
def create_ingredient_public(payload: IngredientCreate, db: Session = Depends(get_db)) -> Ingredient:
    name_lower = payload.name.lower()
    # idempotent: return existing if name or alias already known
    existing = (
        db.query(Ingredient)
        .filter(
            or_(
                Ingredient.name_lower == name_lower,
                Ingredient.aliases.any(payload.name.lower()),
            )
        )
        .first()
    )
    if existing:
        return existing

    row = Ingredient(
        name=payload.name,
        name_lower=name_lower,
        aliases=payload.aliases,
        aisle=payload.aisle,
        unit=payload.unit,
        typical_price=payload.typical_price,
    )
    db.add(row)
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        # race-window safety: re-check after rollback
        existing = (
            db.query(Ingredient)
            .filter(
                or_(
                    Ingredient.name_lower == name_lower,
                    Ingredient.aliases.any(payload.name.lower()),
                )
            )
            .first()
        )
        if existing:
            return existing
        raise HTTPException(status_code=409, detail="ingredient name already exists")
    db.refresh(row)
    return row


@admin_router.post("", response_model=IngredientRead, status_code=status.HTTP_201_CREATED)
def create_ingredient(payload: IngredientCreate, db: Session = Depends(get_db)) -> Ingredient:
    row = Ingredient(
        name=payload.name,
        name_lower=payload.name.lower(),
        aliases=payload.aliases,
        aisle=payload.aisle,
        unit=payload.unit,
        typical_price=payload.typical_price,
    )
    db.add(row)
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(status_code=409, detail="ingredient name already exists")
    db.refresh(row)
    return row


@admin_router.patch("/{ingredient_id}", response_model=IngredientRead)
def update_ingredient(
    ingredient_id: UUID,
    payload: IngredientUpdate,
    db: Session = Depends(get_db),
) -> Ingredient:
    row = db.query(Ingredient).filter(Ingredient.id == ingredient_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="ingredient not found")
    data = payload.model_dump(exclude_unset=True)
    if "name" in data:
        row.name = data["name"]
        row.name_lower = data["name"].lower()
    for field in ("aliases", "aisle", "unit", "typical_price"):
        if field in data:
            setattr(row, field, data[field])
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(status_code=409, detail="ingredient name conflict")
    db.refresh(row)
    return row


@admin_router.delete(
    "/{ingredient_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    response_class=Response,
)
def delete_ingredient(ingredient_id: UUID, db: Session = Depends(get_db)):
    row = db.query(Ingredient).filter(Ingredient.id == ingredient_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="ingredient not found")
    db.delete(row)
    db.commit()
    return Response(status_code=status.HTTP_204_NO_CONTENT)


class _PinMatchBody(BaseModel):
    grocery_item_id: UUID
    confidence: float = 1.0


@admin_router.post(
    "/{ingredient_id}/matches",
    response_model=IngredientGroceryMatchRead,
    status_code=status.HTTP_201_CREATED,
)
def pin_match(
    ingredient_id: UUID,
    payload: _PinMatchBody,
    db: Session = Depends(get_db),
):
    existing = (
        db.query(IngredientGroceryMatch)
        .filter(
            IngredientGroceryMatch.ingredient_id == ingredient_id,
            IngredientGroceryMatch.grocery_item_id == payload.grocery_item_id,
        )
        .first()
    )
    if existing:
        existing.source = IngredientMatchSource.MANUAL
        existing.confidence = Decimal(str(round(payload.confidence, 3)))
        db.commit()
        db.refresh(existing)
        return existing
    row = IngredientGroceryMatch(
        ingredient_id=ingredient_id,
        grocery_item_id=payload.grocery_item_id,
        confidence=Decimal(str(round(payload.confidence, 3))),
        source=IngredientMatchSource.MANUAL,
    )
    db.add(row)
    db.commit()
    db.refresh(row)
    return row


_match_admin_router = APIRouter(
    prefix="/api/admin/ingredient-matches",
    tags=["ingredients-admin"],
    dependencies=[Depends(require_admin)],
)


@_match_admin_router.delete(
    "/{match_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    response_class=Response,
)
def unpin_match(match_id: UUID, db: Session = Depends(get_db)) -> Response:
    row = db.query(IngredientGroceryMatch).filter(IngredientGroceryMatch.id == match_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="match not found")
    db.delete(row)
    db.commit()
    return Response(status_code=status.HTTP_204_NO_CONTENT)
