"""Massive (formerly Polygon.io) API provider.

Stock/crypto/forex/options market data, snapshots, aggregates,
technical indicators, news, and reference data.

Authentication: set MASSIVE_API_KEY environment variable.
API docs: https://massive.com/docs
"""

from __future__ import annotations

import os
import re
import threading
from typing import Any

import httpx


_BASE = "https://api.massive.com"
_TIMEOUT = httpx.Timeout(connect=5.0, read=15.0, write=5.0, pool=5.0)

# Module-level reusable client (thread-safe for reads).
_client_lock = threading.Lock()
_client_instance: httpx.Client | None = None

_SAFE_SEGMENT = re.compile(r"^[A-Za-z0-9._:\-]+$")
_VALID_TIMESPANS = {"second", "minute", "hour", "day", "week", "month", "quarter", "year"}
_VALID_DIRECTIONS = {"gainers", "losers"}
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


def _validate_segment(value: str, name: str = "ticker") -> str:
    """Reject path segments containing unsafe characters."""
    if not value or not _SAFE_SEGMENT.match(value):
        raise ValueError(f"Invalid {name}: {value!r} — must be alphanumeric (with . - _ : allowed)")
    return value


def _validate_date(value: str, name: str = "date") -> str:
    """Validate YYYY-MM-DD format."""
    if not _DATE_RE.match(value):
        raise ValueError(f"Invalid {name}: {value!r} — must be YYYY-MM-DD")
    return value


def _get_client() -> httpx.Client:
    """Lazy singleton httpx.Client with connection reuse."""
    global _client_instance
    if _client_instance is not None:
        return _client_instance
    with _client_lock:
        if _client_instance is not None:
            return _client_instance
        key = os.environ.get("MASSIVE_API_KEY", "")
        if not key:
            raise ValueError(
                "MASSIVE_API_KEY environment variable is not set. "
                "Get one at https://massive.com/dashboard/api-keys"
            )
        _client_instance = httpx.Client(
            base_url=_BASE,
            headers={"Authorization": f"Bearer {key}", "Accept": "application/json"},
            timeout=_TIMEOUT,
        )
        return _client_instance


def _bool_param(v: bool | None) -> str | None:
    """Convert bool to lowercase string for query params, or None."""
    if v is None:
        return None
    return "true" if v else "false"


def _get(path: str, params: dict[str, Any] | None = None) -> Any:
    c = _get_client()
    cleaned = {k: v for k, v in (params or {}).items() if v is not None}
    r = c.get(path, params=cleaned)
    r.raise_for_status()
    body = r.json()
    return body.get("results", body)


def _get_values(path: str, params: dict[str, Any]) -> Any:
    """Call _get and extract 'values' key if present (for technical indicators)."""
    body = _get(path, params)
    if isinstance(body, dict) and "values" in body:
        return body["values"]
    return body


class MassiveProvider:
    """Thin wrapper around the Massive (Polygon.io) REST API."""

    # -- Aggregates (OHLCV bars) ----------------------------------------------

    @staticmethod
    def aggregates(
        ticker: str,
        multiplier: int = 1,
        timespan: str = "day",
        from_date: str = "",
        to_date: str = "",
        *,
        adjusted: bool = True,
        sort: str = "asc",
        limit: int = 5000,
    ) -> list[dict]:
        """Get OHLCV bars for a ticker.

        timespan: second, minute, hour, day, week, month, quarter, year.
        from_date/to_date: YYYY-MM-DD.
        """
        _validate_segment(ticker)
        if timespan not in _VALID_TIMESPANS:
            raise ValueError(f"Invalid timespan: {timespan!r} — must be one of {_VALID_TIMESPANS}")
        if not from_date or not to_date:
            raise ValueError("from_date and to_date are required (YYYY-MM-DD)")
        _validate_date(from_date, "from_date")
        _validate_date(to_date, "to_date")
        return _get(
            f"/v2/aggs/ticker/{ticker}/range/{multiplier}/{timespan}/{from_date}/{to_date}",
            {"adjusted": _bool_param(adjusted), "sort": sort, "limit": limit},
        )

    @staticmethod
    def previous_close(ticker: str, *, adjusted: bool = True) -> list[dict]:
        """Get previous day's OHLCV."""
        _validate_segment(ticker)
        return _get(f"/v2/aggs/ticker/{ticker}/prev", {"adjusted": _bool_param(adjusted)})

    @staticmethod
    def grouped_daily(date: str, *, adjusted: bool = True) -> list[dict]:
        """Get OHLCV for the entire market on a date."""
        _validate_date(date)
        return _get(f"/v2/aggs/grouped/locale/us/market/stocks/{date}", {"adjusted": _bool_param(adjusted)})

    # -- Snapshots ------------------------------------------------------------

    @staticmethod
    def snapshot(ticker: str) -> dict:
        """Get real-time snapshot for a single ticker (price, day, prevDay, lastTrade, lastQuote)."""
        _validate_segment(ticker)
        body = _get(f"/v2/snapshot/locale/us/markets/stocks/tickers/{ticker}")
        if isinstance(body, dict) and "ticker" in body:
            return body["ticker"]
        return body

    @staticmethod
    def gainers_losers(direction: str = "gainers") -> list[dict]:
        """Get top 20 gainers or losers. direction: 'gainers' or 'losers'."""
        if direction not in _VALID_DIRECTIONS:
            raise ValueError(f"Invalid direction: {direction!r} — must be 'gainers' or 'losers'")
        body = _get(f"/v2/snapshot/locale/us/markets/stocks/{direction}")
        if isinstance(body, dict) and "tickers" in body:
            return body["tickers"]
        return body

    # -- Trades & Quotes ------------------------------------------------------

    @staticmethod
    def last_trade(ticker: str) -> dict:
        """Get the most recent trade for a ticker."""
        _validate_segment(ticker)
        return _get(f"/v2/last/trade/{ticker}")

    @staticmethod
    def last_quote(ticker: str) -> dict:
        """Get the most recent NBBO quote for a ticker."""
        _validate_segment(ticker)
        return _get(f"/v2/last/nbbo/{ticker}")

    # -- Reference / Ticker Data ----------------------------------------------

    @staticmethod
    def ticker_details(ticker: str) -> dict:
        """Get ticker details: name, market cap, description, employees, SIC, branding."""
        _validate_segment(ticker)
        return _get(f"/v3/reference/tickers/{ticker}")

    @staticmethod
    def search_tickers(
        query: str = "",
        *,
        market: str | None = None,
        active: bool = True,
        limit: int = 20,
    ) -> list[dict]:
        """Search/list tickers. market: stocks, crypto, fx, otc."""
        return _get("/v3/reference/tickers", {
            "search": query or None,
            "market": market,
            "active": _bool_param(active),
            "limit": limit,
        })

    @staticmethod
    def market_status() -> dict:
        """Get current market status (open/closed/early hours)."""
        return _get("/v1/marketstatus/now")

    @staticmethod
    def market_holidays() -> list[dict]:
        """Get upcoming market holidays."""
        return _get("/v1/marketstatus/upcoming")

    # -- Options --------------------------------------------------------------

    @staticmethod
    def options_chain(
        underlying: str,
        *,
        contract_type: str | None = None,
        expiration_date: str | None = None,
        expiration_date_gte: str | None = None,
        strike_price_gte: float | None = None,
        strike_price_lte: float | None = None,
        limit: int = 250,
    ) -> list[dict]:
        """Get options chain snapshot with Greeks for an underlying asset."""
        _validate_segment(underlying)
        return _get(f"/v3/snapshot/options/{underlying}", {
            "contract_type": contract_type,
            "expiration_date": expiration_date,
            "expiration_date.gte": expiration_date_gte,
            "strike_price.gte": strike_price_gte,
            "strike_price.lte": strike_price_lte,
            "limit": limit,
        })

    @staticmethod
    def options_contracts(
        *,
        underlying_ticker: str | None = None,
        contract_type: str | None = None,
        expiration_date: str | None = None,
        strike_price: float | None = None,
        limit: int = 100,
    ) -> list[dict]:
        """List options contracts with filters."""
        return _get("/v3/reference/options/contracts", {
            "underlying_ticker": underlying_ticker,
            "contract_type": contract_type,
            "expiration_date": expiration_date,
            "strike_price": strike_price,
            "limit": limit,
        })

    # -- Technical Indicators -------------------------------------------------

    @staticmethod
    def sma(
        ticker: str,
        *,
        timespan: str = "day",
        window: int = 50,
        series_type: str = "close",
        limit: int = 50,
    ) -> list[dict]:
        """Get Simple Moving Average."""
        _validate_segment(ticker)
        return _get_values(f"/v1/indicators/sma/{ticker}", {
            "timespan": timespan, "window": window,
            "series_type": series_type, "limit": limit,
        })

    @staticmethod
    def ema(
        ticker: str,
        *,
        timespan: str = "day",
        window: int = 50,
        series_type: str = "close",
        limit: int = 50,
    ) -> list[dict]:
        """Get Exponential Moving Average."""
        _validate_segment(ticker)
        return _get_values(f"/v1/indicators/ema/{ticker}", {
            "timespan": timespan, "window": window,
            "series_type": series_type, "limit": limit,
        })

    @staticmethod
    def rsi(
        ticker: str,
        *,
        timespan: str = "day",
        window: int = 14,
        series_type: str = "close",
        limit: int = 50,
    ) -> list[dict]:
        """Get Relative Strength Index."""
        _validate_segment(ticker)
        return _get_values(f"/v1/indicators/rsi/{ticker}", {
            "timespan": timespan, "window": window,
            "series_type": series_type, "limit": limit,
        })

    @staticmethod
    def macd(
        ticker: str,
        *,
        timespan: str = "day",
        short_window: int = 12,
        long_window: int = 26,
        signal_window: int = 9,
        series_type: str = "close",
        limit: int = 50,
    ) -> list[dict]:
        """Get MACD (Moving Average Convergence Divergence)."""
        _validate_segment(ticker)
        return _get_values(f"/v1/indicators/macd/{ticker}", {
            "timespan": timespan, "short_window": short_window,
            "long_window": long_window, "signal_window": signal_window,
            "series_type": series_type, "limit": limit,
        })

    # -- News -----------------------------------------------------------------

    @staticmethod
    def news(
        *,
        ticker: str | None = None,
        limit: int = 20,
    ) -> list[dict]:
        """Get market news, optionally filtered by ticker."""
        return _get("/v2/reference/news", {"ticker": ticker, "limit": limit})

    # -- Corporate Actions ----------------------------------------------------

    @staticmethod
    def dividends(ticker: str, *, limit: int = 20) -> list[dict]:
        """Get dividend history for a ticker."""
        return _get("/v3/reference/dividends", {"ticker": ticker, "limit": limit})

    @staticmethod
    def splits(ticker: str, *, limit: int = 20) -> list[dict]:
        """Get stock split history for a ticker."""
        return _get("/v3/reference/splits", {"ticker": ticker, "limit": limit})

    # -- Crypto ---------------------------------------------------------------

    @staticmethod
    def crypto_snapshot(ticker: str) -> dict:
        """Get snapshot for a crypto pair (e.g. X:BTCUSD)."""
        _validate_segment(ticker)
        body = _get(f"/v2/snapshot/locale/global/markets/crypto/tickers/{ticker}")
        if isinstance(body, dict) and "ticker" in body:
            return body["ticker"]
        return body

    @staticmethod
    def crypto_gainers_losers(direction: str = "gainers") -> list[dict]:
        """Get top crypto gainers or losers."""
        if direction not in _VALID_DIRECTIONS:
            raise ValueError(f"Invalid direction: {direction!r} — must be 'gainers' or 'losers'")
        body = _get(f"/v2/snapshot/locale/global/markets/crypto/{direction}")
        if isinstance(body, dict) and "tickers" in body:
            return body["tickers"]
        return body

    # -- Forex ----------------------------------------------------------------

    @staticmethod
    def forex_conversion(from_currency: str, to_currency: str) -> dict:
        """Get real-time forex conversion rate."""
        _validate_segment(from_currency, "from_currency")
        _validate_segment(to_currency, "to_currency")
        return _get(f"/v1/conversion/{from_currency}/{to_currency}")

    @staticmethod
    def forex_snapshot(ticker: str) -> dict:
        """Get snapshot for a forex pair (e.g. C:EURUSD)."""
        _validate_segment(ticker)
        body = _get(f"/v2/snapshot/locale/global/markets/forex/tickers/{ticker}")
        if isinstance(body, dict) and "ticker" in body:
            return body["ticker"]
        return body
