from __future__ import annotations

import traceback
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any, Dict, List

import pandas as pd
import uvicorn
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from .config import ProjectPaths, RuntimeSettings
from .io import load_pickle
from .recommendation import (
    compute_compatibility,
    fetch_recs_from_filters,
    get_user_anime_status,
    predict_scores,
)


class PredictRequest(BaseModel):
    username: str


class FilteredPredictRequest(BaseModel):
    item_score_pairs_sorted: List[List[float]]
    selected_genres: List[str] = []
    selected_media_types: List[str] = []
    min_users: int = 0
    max_users: int = 4_200_000
    filter_sequels: bool = False
    include_plan_to_watch: bool = False
    plan_to_watch_anime_ids: List[int] = []
    page: int = 1
    anchors: Dict[str, List[int]] = {}


class CompatibilityRequest(BaseModel):
    username_b: str
    user_a_anime_details: List[Dict[str, Any]]
    user_a_item_scores: List[List[float]]


def create_app(paths: ProjectPaths | None = None) -> FastAPI:
    runtime_paths = paths or ProjectPaths()
    data_store: dict[str, Any] = {}

    @asynccontextmanager
    async def lifespan(_: FastAPI):
        catalog_path = runtime_paths.resolve_anime_catalog_input()
        model_path = runtime_paths.resolve_model_input()
        dataset_path = runtime_paths.resolve_dataset_input()
        atlas_path = runtime_paths.resolve_atlas_input()

        data_store["catalog"] = pd.read_csv(catalog_path, na_values=[], keep_default_na=False)
        data_store["model"] = load_pickle(model_path)
        data_store["dataset"] = load_pickle(dataset_path)
        data_store["atlas"] = pd.read_csv(atlas_path, na_values=[], keep_default_na=False)
        yield
        data_store.clear()

    app = FastAPI(title="AniRec API", version="2.0.0", lifespan=lifespan)
    app.add_middleware(
        CORSMiddleware,
        allow_origins=list(RuntimeSettings().allowed_origins),
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    @app.get("/")
    async def root():
        return {"message": "Hello from AniRec API"}

    @app.get("/hello")
    async def hello():
        return {"message": "Hello from AniRec API"}

    @app.get("/health")
    async def health():
        return {"status": "healthy"}

    @app.post("/predict")
    async def predict(request_data: PredictRequest):
        try:
            (
                recommendations,
                item_scores,
                user_stats,
                user_anime_details,
                anchors,
            ) = predict_scores(
                request_data.username,
                data_store["dataset"],
                data_store["model"],
                data_store["catalog"],
            )

            if not user_anime_details:
                raise HTTPException(
                    status_code=404,
                    detail=(
                        f"Username '{request_data.username}' not found on MyAnimeList. "
                        "Please check the username and try again."
                    ),
                )

            if not recommendations:
                raise HTTPException(
                    status_code=400,
                    detail=f"Unable to generate recommendations for user '{request_data.username}'",
                )

            return {
                "recommendations": recommendations,
                "item_score_pairs_sorted": item_scores,
                "user_stats": user_stats,
                "user_anime_details": user_anime_details,
                "anchors": anchors,
            }
        except HTTPException:
            raise
        except Exception:
            traceback.print_exc()
            raise HTTPException(
                status_code=500,
                detail="Internal Server Error during prediction.",
            )

    @app.post("/predict/filtered")
    async def predict_filtered(request: FilteredPredictRequest):
        recommendations, total_count = fetch_recs_from_filters(
            item_score_pairs_sorted=request.item_score_pairs_sorted,
            df=data_store["catalog"],
            dataset=data_store["dataset"],
            filters={
                "genres": request.selected_genres,
                "media_types": request.selected_media_types,
                "min_users": request.min_users,
                "max_users": request.max_users,
                "filter_sequels": request.filter_sequels,
                "include_plan_to_watch": request.include_plan_to_watch,
                "plan_to_watch_anime_ids": request.plan_to_watch_anime_ids,
            },
            page=request.page,
            page_size=20,
            anchors_by_anime_id=request.anchors,
        )
        return {"recommendations": recommendations, "total_count": total_count}

    @app.post("/compatibility")
    async def compatibility(request: CompatibilityRequest):
        try:
            result = compute_compatibility(
                user_a_anime_details=request.user_a_anime_details,
                user_a_item_scores=request.user_a_item_scores,
                username_b=request.username_b,
                dataset=data_store["dataset"],
                df=data_store["catalog"],
            )
            if result.get("error") == "USER_B_NOT_FOUND":
                raise HTTPException(
                    status_code=404,
                    detail=(
                        f"Username '{request.username_b}' not found on MyAnimeList. "
                        "Please check the username and try again."
                    ),
                )
            return result
        except HTTPException:
            raise
        except Exception:
            traceback.print_exc()
            raise HTTPException(
                status_code=500,
                detail="Internal server error during compatibility computation.",
            )

    @app.get("/atlas")
    async def get_atlas_data(username: str | None = None):
        try:
            atlas_df = data_store["atlas"].copy()
            anime_df = data_store["catalog"].copy()

            atlas_df["anime_id"] = atlas_df["anime_id"].astype(int)
            anime_df["anime_id"] = anime_df["anime_id"].astype(int)
            anime_info_df = anime_df[["anime_id", "num_list_users", "title"]]
            merged_df = pd.merge(atlas_df, anime_info_df, on="anime_id")

            required_columns = ["anime_id", "x_coord", "y_coord", "num_list_users", "title"]
            if not all(column in merged_df.columns for column in required_columns):
                raise HTTPException(
                    status_code=500,
                    detail="Atlas data is missing required columns.",
                )

            user_anime_status = {}
            if username:
                try:
                    user_anime_status = get_user_anime_status(username)
                except Exception:
                    traceback.print_exc()

            atlas_data = []
            for row in merged_df.to_dict(orient="records"):
                anime_id = int(row["anime_id"])
                row["user_status"] = user_anime_status.get(anime_id)
                atlas_data.append(row)

            return {"atlas_data": atlas_data}
        except FileNotFoundError as error:
            raise HTTPException(status_code=404, detail=f"Data file not found: {error}")
        except HTTPException:
            raise
        except Exception:
            traceback.print_exc()
            raise HTTPException(
                status_code=500,
                detail="An error occurred while loading the atlas data.",
            )

    @app.get("/get/anime/{anime_id}")
    async def get_anime_details(anime_id: int):
        try:
            anime_df = data_store["catalog"].copy()
            anime_df["anime_id"] = anime_df["anime_id"].astype(int)
            anime_data = anime_df[anime_df["anime_id"] == anime_id]

            if anime_data.empty:
                raise HTTPException(status_code=404, detail="Anime not found")

            anime_details = anime_data.iloc[0]
            return {
                "title": anime_details.get("title", "Unknown"),
                "image_url": anime_details.get("image_url", ""),
                "mean": float(anime_details.get("mean", 0.0)),
                "num_list_users": int(anime_details.get("num_list_users", 0)),
            }
        except HTTPException:
            raise
        except Exception:
            traceback.print_exc()
            raise HTTPException(
                status_code=500,
                detail="An error occurred while fetching anime details.",
            )

    return app


app = create_app()


def run() -> None:
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000,
        proxy_headers=True,
        forwarded_allow_ips="127.0.0.1",
    )
