import logging
from textwrap import dedent
from typing import Literal, Optional

from fastapi import APIRouter, FastAPI, HTTPException, Response, status
from fastapi.responses import FileResponse, RedirectResponse
from pydantic import BaseModel, Field

from rpmeta import __version__
from rpmeta.config import ModelBehavior
from rpmeta.constants import FAVICON_PATH
from rpmeta.dataset import InputRecord
from rpmeta.predictor import Predictor

logger = logging.getLogger(__name__)

app = FastAPI(
    title="RPMeta API",
    description=dedent(
        """
    API for predicting build times for RPM packages based on hardware information
    and package metadata.

    ## Overview

    This API allows you to predict how long it will take to build an RPM package
    based on the hardware specifications of the build machine and package information.

    ## Usage

    Send a POST request to `/predict` with the necessary package and hardware information.

    ### Example Input
    ```json
    {
      "package_name": "rust-winit",
      "epoch": 0,
      "version": "0.30.8",
      "mock_chroot": "fedora-41-x86_64",
      "hw_info": {
        "cpu_model_name": "Intel Xeon Processor (Cascadelake)",
        "cpu_arch": "x86_64",
        "cpu_model": "85",
        "cpu_cores": 6,
        "ram": 15324520,
        "swap": 8388604
      }
    }
    ```

    ### Example Response
    ```json
    {
      "prediction": 5
    }
    ```

    The prediction is the estimated build time in minutes by default.
    """,
    ),
    version=__version__,
    docs_url="/docs",
    redoc_url="/redoc",
)

# Store the predictor instance globally - loaded once when server starts
# This ensures the model is loaded into RAM only once
predictor = None


class PredictionResponse(BaseModel):
    """
    Response model for the prediction endpoint.

    This contains the predicted build duration in desired time format.
    """

    prediction: int = Field(
        description="Predicted build duration in desired time format",
        gt=0,
        examples=[5, 30, 120],
    )
    used_configuration: ModelBehavior = Field(
        ...,
        description="Configuration used for the prediction",
    )


class PredictionRequest(InputRecord):
    """
    Input body for the prediction endpoint.

    This is used to pass the input data to the model for prediction with all
    the necessary configurations.
    """

    configuration: Optional[ModelBehavior] = Field(
        default=None,
        description="Optional configuration for the server or model",
    )


class HealthResponse(BaseModel):
    """
    Response model for the health check endpoint.
    """

    status: Literal["healthy"] = Field(
        description="Health status of the API",
        default="healthy",
    )
    version: str = Field(
        ...,
        description="Version of the API",
    )
    model_name: str = Field(
        ...,
        description="Name of the currently loaded machine learning model",
    )


# API router for v1 endpoints
v1_router = APIRouter(prefix="/v1")


@v1_router.post(
    "/predict",
    summary="Predict build duration",
)
def predict_endpoint_v1(request_data: PredictionRequest) -> PredictionResponse:
    """
    Predict the build duration for an RPM package.

    This endpoint accepts package information and hardware specs and returns
    the predicted build duration in seconds.
    """

    logger.debug("Received request for prediction: %s", request_data)

    if predictor is None:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Model not initialized. Server not ready for predictions.",
        )

    if request_data.configuration:
        model_behavior = request_data.configuration
    else:
        # Use the default model behavior from the server config
        model_behavior = predictor.config.model.behavior

    package_data = InputRecord.model_validate(request_data)
    prediction = predictor.predict(package_data, model_behavior)
    logger.debug(
        "Prediction for %s: %s %s",
        package_data.package_name,
        prediction,
        model_behavior.time_format,
    )

    if prediction == -1:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Package '{package_data.package_name}' is not known for the model.",
        )

    return PredictionResponse(
        prediction=prediction,
        used_configuration=model_behavior,
    )


app.include_router(v1_router)


# Add an alias endpoint at the root path that redirects to the latest version (currently v1)
@app.post(
    "/predict",
    summary="Predict build duration (Latest API version)",
)
def predict_endpoint(request_data: PredictionRequest) -> PredictionResponse:
    """
    Alias to the latest API version (currently v1).

    This is a convenience endpoint that forwards requests to the current stable API version.
    For new implementations, consider using the versioned endpoint directly.
    """
    return predict_endpoint_v1(request_data)


def reload_predictor(new_predictor: Predictor) -> None:
    """
    Reload the model and categories map for the API server.

    Args:
        new_predictor: The predictor instance to use
    """
    # This allows the model to be loaded into RAM only once when the server starts,
    # and can be called to reload the model if needed without restarting the server.
    global predictor
    logger.info("Reloading predictor")
    predictor = new_predictor
    logger.info("Predictor loaded successfully")


@app.get("/health", summary="Simple health check")
def health_check() -> HealthResponse:
    """
    Perform a health check on the API. If the model is not loaded, returns 503 Service Unavailable.
    """
    if predictor is None:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="Model not loaded. Server not ready for predictions.",
        )

    return HealthResponse(
        version=__version__,
        model_name=predictor.model.name,
    )


@app.get("/", include_in_schema=False)
def root():
    return RedirectResponse(url="/docs")


@app.get("/favicon.ico", include_in_schema=False)
def favicon():
    if FAVICON_PATH.exists():
        return FileResponse(FAVICON_PATH)

    return Response(status_code=status.HTTP_204_NO_CONTENT)
