"""
ferro-ta REST API
============================

A minimal FastAPI service that exposes ferro-ta indicators and backtest
over HTTP so that any client can compute technical analysis via REST.

Endpoints
---------
GET  /health                 — readiness / liveness probe
POST /indicators/sma         — Simple Moving Average
POST /indicators/ema         — Exponential Moving Average
POST /indicators/rsi         — Relative Strength Index
POST /indicators/macd        — MACD (line, signal, histogram)
POST /indicators/bbands      — Bollinger Bands
POST /backtest               — Vectorized backtest

Request / Response format
-------------------------
All indicator endpoints accept JSON:
    {
      "close": [1.0, 2.0, ...],   // required; array of floats
      "timeperiod": 14            // optional parameter
    }

And return:
    {
      "result": [null, null, ..., 14.0, ...]  // null for NaN warm-up
    }

Or for multi-output indicators (MACD, BBANDS):
    {
      "result": {
        "macd": [...],
        "signal": [...],
        "hist": [...]
      }
    }

For the backtest endpoint the request is:
    {
      "close": [1.0, 2.0, ...],
      "strategy": "rsi_30_70",     // or "sma_crossover", "macd_crossover"
      "commission_per_trade": 0.0,
      "slippage_bps": 0.0
    }

And the response is:
    {
      "final_equity": 1.123,
      "n_trades": 7,
      "equity": [1.0, ...]
    }

Running
-------
Development::

    uvicorn api.main:app --reload --port 8000

Production::

    uvicorn api.main:app --host 0.0.0.0 --port 8000 --workers 4

Docker::

    docker build -t ferro-ta-api ./api
    docker run -p 8000:8000 ferro-ta-api

Environment variables
---------------------
MAX_SERIES_LENGTH : int — maximum number of data points per request
  (default 100 000). Requests exceeding this limit return HTTP 413.
"""

from __future__ import annotations

import math
import os
from typing import Any

import numpy as np

try:
    from fastapi import FastAPI, HTTPException
    from pydantic import BaseModel, Field, field_validator
except ImportError as exc:  # pragma: no cover
    raise ImportError(
        "The ferro-ta API requires fastapi and pydantic.\n"
        "Install with: pip install 'ferro_ta[api]'"
    ) from exc

import ferro_ta as ft
from ferro_ta.analysis.backtest import backtest as _backtest

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

MAX_SERIES_LENGTH = int(os.environ.get("MAX_SERIES_LENGTH", "100000"))

# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------

app = FastAPI(
    title="ferro-ta API",
    description="REST API for ferro-ta technical analysis indicators and backtesting.",
    version=ft.__version__,
    docs_url="/docs",
    redoc_url="/redoc",
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _nan_to_none(arr: np.ndarray) -> list[float | None]:
    """Convert numpy array to list, replacing NaN/Inf with None."""
    return [None if not math.isfinite(v) else float(v) for v in arr]


def _validate_series(close: list[float]) -> np.ndarray:
    if len(close) > MAX_SERIES_LENGTH:
        raise HTTPException(
            status_code=413,
            detail=f"Series length {len(close)} exceeds maximum {MAX_SERIES_LENGTH}.",
        )
    if len(close) < 2:
        raise HTTPException(
            status_code=422,
            detail="Series must contain at least 2 values.",
        )
    return np.asarray(close, dtype=np.float64)


# ---------------------------------------------------------------------------
# Request / Response models
# ---------------------------------------------------------------------------


class IndicatorRequest(BaseModel):
    close: list[float] = Field(..., description="Close price series")
    timeperiod: int = Field(default=14, ge=1, description="Look-back period")

    @field_validator("close")
    @classmethod
    def close_must_be_finite(cls, v: list[float]) -> list[float]:
        if not all(math.isfinite(x) for x in v):
            raise ValueError("close series must contain only finite values")
        return v


class MACDRequest(BaseModel):
    close: list[float] = Field(..., description="Close price series")
    fastperiod: int = Field(default=12, ge=1)
    slowperiod: int = Field(default=26, ge=1)
    signalperiod: int = Field(default=9, ge=1)

    @field_validator("close")
    @classmethod
    def close_must_be_finite(cls, v: list[float]) -> list[float]:
        if not all(math.isfinite(x) for x in v):
            raise ValueError("close series must contain only finite values")
        return v


class BBANDSRequest(BaseModel):
    close: list[float] = Field(..., description="Close price series")
    timeperiod: int = Field(default=5, ge=2)
    nbdevup: float = Field(default=2.0, gt=0)
    nbdevdn: float = Field(default=2.0, gt=0)

    @field_validator("close")
    @classmethod
    def close_must_be_finite(cls, v: list[float]) -> list[float]:
        if not all(math.isfinite(x) for x in v):
            raise ValueError("close series must contain only finite values")
        return v


class BacktestRequest(BaseModel):
    close: list[float] = Field(..., description="Close price series")
    strategy: str = Field(default="rsi_30_70")
    commission_per_trade: float = Field(default=0.0, ge=0.0)
    slippage_bps: float = Field(default=0.0, ge=0.0)

    @field_validator("close")
    @classmethod
    def close_must_be_finite(cls, v: list[float]) -> list[float]:
        if not all(math.isfinite(x) for x in v):
            raise ValueError("close series must contain only finite values")
        return v


# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------


@app.get("/health", summary="Health check")
def health() -> dict[str, str]:
    """Readiness / liveness probe."""
    return {"status": "ok", "version": app.version}


@app.post("/indicators/sma", summary="Simple Moving Average")
def compute_sma(req: IndicatorRequest) -> dict[str, Any]:
    """Compute Simple Moving Average (SMA).

    Returns ``result``: list of floats (null for warm-up bars).
    """
    c = _validate_series(req.close)
    out = np.asarray(ft.SMA(c, timeperiod=req.timeperiod), dtype=np.float64)
    return {"result": _nan_to_none(out)}


@app.post("/indicators/ema", summary="Exponential Moving Average")
def compute_ema(req: IndicatorRequest) -> dict[str, Any]:
    """Compute Exponential Moving Average (EMA)."""
    c = _validate_series(req.close)
    out = np.asarray(ft.EMA(c, timeperiod=req.timeperiod), dtype=np.float64)
    return {"result": _nan_to_none(out)}


@app.post("/indicators/rsi", summary="Relative Strength Index")
def compute_rsi(req: IndicatorRequest) -> dict[str, Any]:
    """Compute Relative Strength Index (RSI)."""
    c = _validate_series(req.close)
    out = np.asarray(ft.RSI(c, timeperiod=req.timeperiod), dtype=np.float64)
    return {"result": _nan_to_none(out)}


@app.post("/indicators/macd", summary="MACD")
def compute_macd(req: MACDRequest) -> dict[str, Any]:
    """Compute MACD (line, signal, histogram).

    Returns ``result`` with keys ``macd``, ``signal``, ``hist``.
    """
    c = _validate_series(req.close)
    macd, signal, hist = ft.MACD(
        c,
        fastperiod=req.fastperiod,
        slowperiod=req.slowperiod,
        signalperiod=req.signalperiod,
    )
    return {
        "result": {
            "macd": _nan_to_none(np.asarray(macd, dtype=np.float64)),
            "signal": _nan_to_none(np.asarray(signal, dtype=np.float64)),
            "hist": _nan_to_none(np.asarray(hist, dtype=np.float64)),
        }
    }


@app.post("/indicators/bbands", summary="Bollinger Bands")
def compute_bbands(req: BBANDSRequest) -> dict[str, Any]:
    """Compute Bollinger Bands (upper, middle, lower).

    Returns ``result`` with keys ``upper``, ``middle``, ``lower``.
    """
    c = _validate_series(req.close)
    upper, middle, lower = ft.BBANDS(
        c,
        timeperiod=req.timeperiod,
        nbdevup=req.nbdevup,
        nbdevdn=req.nbdevdn,
    )
    return {
        "result": {
            "upper": _nan_to_none(np.asarray(upper, dtype=np.float64)),
            "middle": _nan_to_none(np.asarray(middle, dtype=np.float64)),
            "lower": _nan_to_none(np.asarray(lower, dtype=np.float64)),
        }
    }


@app.post("/backtest", summary="Vectorized backtest")
def run_backtest(req: BacktestRequest) -> dict[str, Any]:
    """Run a vectorized backtest using a named strategy.

    Strategies: ``rsi_30_70``, ``sma_crossover``, ``macd_crossover``.
    Returns ``final_equity``, ``n_trades``, and the full ``equity`` curve.
    """
    c = _validate_series(req.close)
    valid_strategies = {"rsi_30_70", "sma_crossover", "macd_crossover"}
    if req.strategy not in valid_strategies:
        raise HTTPException(
            status_code=422,
            detail=f"Unknown strategy '{req.strategy}'. "
            f"Available: {sorted(valid_strategies)}",
        )
    result = _backtest(
        c,
        strategy=req.strategy,
        commission_per_trade=req.commission_per_trade,
        slippage_bps=req.slippage_bps,
    )
    return {
        "final_equity": float(result.final_equity),
        "n_trades": int(result.n_trades),
        "equity": _nan_to_none(result.equity),
    }
