"""Unusual Whales API provider.

Options flow, dark pool, Congress trades, insider activity, Greek exposure,
IV analytics, short interest, screeners, and market sentiment.

Authentication: set UNUSUAL_WHALES_API_KEY environment variable.
API docs: https://api.unusualwhales.com/docs
"""

from __future__ import annotations

import os
import re
import threading
from typing import Any

import httpx


_BASE = "https://api.unusualwhales.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._\-]+$")


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 _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("UNUSUAL_WHALES_API_KEY", "")
        if not key:
            raise ValueError(
                "UNUSUAL_WHALES_API_KEY environment variable is not set. "
                "Get one at https://unusualwhales.com/settings/api-dashboard"
            )
        _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("data", body)


class UnusualWhalesProvider:
    """Thin wrapper around the Unusual Whales REST API."""

    # -- Options Flow ---------------------------------------------------------

    @staticmethod
    def options_flow(
        *,
        ticker: str | None = None,
        min_premium: int | None = None,
        is_sweep: bool | None = None,
        is_call: bool | None = None,
        is_put: bool | None = None,
        is_otm: bool | None = None,
        min_dte: int | None = None,
        max_dte: int | None = None,
        limit: int = 50,
    ) -> list[dict]:
        """Get unusual options flow alerts with filters."""
        params: dict[str, Any] = {
            "ticker_symbol": ticker,
            "min_premium": min_premium,
            "is_sweep": _bool_param(is_sweep),
            "is_call": _bool_param(is_call),
            "is_put": _bool_param(is_put),
            "is_otm": _bool_param(is_otm),
            "min_dte": min_dte,
            "max_dte": max_dte,
            "limit": limit,
        }
        return _get("/api/option-trades/flow-alerts", params)

    @staticmethod
    def stock_flow(ticker: str, *, side: str | None = None, min_premium: int | None = None) -> list[dict]:
        """Get recent options flow for a specific ticker."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/flow-recent", {"side": side, "min_premium": min_premium})

    # -- Dark Pool ------------------------------------------------------------

    @staticmethod
    def dark_pool_recent() -> list[dict]:
        """Get recent market-wide dark pool prints."""
        return _get("/api/darkpool/recent")

    @staticmethod
    def dark_pool(
        ticker: str,
        *,
        date: str | None = None,
        min_size: int | None = None,
        limit: int = 50,
    ) -> list[dict]:
        """Get dark pool trades for a specific ticker."""
        _validate_segment(ticker)
        return _get(f"/api/darkpool/{ticker}", {"date": date, "min_size": min_size, "limit": limit})

    # -- Congress & Insiders --------------------------------------------------

    @staticmethod
    def congress_trades(*, limit: int = 50, ticker: str | None = None, date: str | None = None) -> list[dict]:
        """Get recent Congress member stock trades."""
        return _get("/api/congress/recent-trades", {"limit": limit, "ticker": ticker, "date": date})

    @staticmethod
    def congress_trader(name: str) -> list[dict]:
        """Get trades for a specific Congress member."""
        return _get("/api/congress/congress-trader", {"name": name})

    @staticmethod
    def insider_trades(
        *,
        ticker: str | None = None,
        min_value: int | None = None,
        is_officer: bool | None = None,
        is_director: bool | None = None,
        is_ten_percent_owner: bool | None = None,
        start_date: str | None = None,
        end_date: str | None = None,
        limit: int = 50,
    ) -> list[dict]:
        """Get insider transactions (Form 4 filings)."""
        return _get("/api/insider/transactions", {
            "ticker_symbol": ticker,
            "min_value": min_value,
            "is_officer": _bool_param(is_officer),
            "is_director": _bool_param(is_director),
            "is_ten_percent_owner": _bool_param(is_ten_percent_owner),
            "start_date": start_date,
            "end_date": end_date,
            "limit": limit,
        })

    # -- Market Sentiment -----------------------------------------------------

    @staticmethod
    def market_tide(*, date: str | None = None, otm_only: bool | None = None) -> list[dict]:
        """Get Market Tide sentiment (net premium flow)."""
        return _get("/api/market/market-tide", {"date": date, "otm_only": _bool_param(otm_only)})

    @staticmethod
    def spike() -> dict:
        """Get SPIKE volatility indicator."""
        return _get("/api/market/spike")

    @staticmethod
    def economic_calendar() -> list[dict]:
        """Get upcoming economic events."""
        return _get("/api/market/economic-calendar")

    @staticmethod
    def fda_calendar() -> list[dict]:
        """Get FDA calendar events."""
        return _get("/api/market/fda-calendar")

    # -- Stock Data -----------------------------------------------------------

    @staticmethod
    def stock_info(ticker: str) -> dict:
        """Get stock information (price, market cap, sector, etc.)."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/info")

    @staticmethod
    def stock_state(ticker: str) -> dict:
        """Get current stock state."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/stock-state")

    @staticmethod
    def earnings(ticker: str) -> list[dict]:
        """Get historical earnings for a ticker."""
        _validate_segment(ticker)
        return _get(f"/api/earnings/{ticker}")

    @staticmethod
    def earnings_premarket() -> list[dict]:
        """Get pre-market earnings schedule."""
        return _get("/api/earnings/premarket")

    @staticmethod
    def earnings_afterhours() -> list[dict]:
        """Get after-hours earnings schedule."""
        return _get("/api/earnings/afterhours")

    # -- Greeks & IV ----------------------------------------------------------

    @staticmethod
    def greek_exposure(ticker: str) -> dict:
        """Get aggregate Greek exposure (GEX, DEX, etc.)."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/greek-exposure")

    @staticmethod
    def greek_exposure_by_strike(ticker: str) -> list[dict]:
        """Get Greek exposure broken down by strike."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/greek-exposure/strike")

    @staticmethod
    def greek_exposure_by_expiry(ticker: str) -> list[dict]:
        """Get Greek exposure broken down by expiry."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/greek-exposure/expiry")

    @staticmethod
    def iv_rank(ticker: str) -> dict:
        """Get IV rank percentile for a ticker."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/iv-rank")

    @staticmethod
    def vol_term_structure(ticker: str) -> list[dict]:
        """Get IV term structure across expirations."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/volatility/term-structure")

    @staticmethod
    def max_pain(ticker: str) -> dict:
        """Get max pain levels."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/max-pain")

    @staticmethod
    def option_chains(ticker: str) -> list[dict]:
        """Get full option chains for a ticker."""
        _validate_segment(ticker)
        return _get(f"/api/stock/{ticker}/option-chains")

    # -- Short Interest -------------------------------------------------------

    @staticmethod
    def short_interest(ticker: str) -> dict:
        """Get short interest and float data."""
        _validate_segment(ticker)
        return _get(f"/api/shorts/{ticker}/interest-float/v2")

    @staticmethod
    def short_volume(ticker: str) -> list[dict]:
        """Get short volume and ratio history."""
        _validate_segment(ticker)
        return _get(f"/api/shorts/{ticker}/volume-and-ratio")

    @staticmethod
    def ftd(ticker: str) -> list[dict]:
        """Get failures to deliver."""
        _validate_segment(ticker)
        return _get(f"/api/shorts/{ticker}/ftds")

    # -- Screeners ------------------------------------------------------------

    @staticmethod
    def screener_stocks() -> list[dict]:
        """Screen stocks across multiple criteria."""
        return _get("/api/screener/stocks")

    @staticmethod
    def screener_options(
        *,
        limit: int = 50,
        min_premium: int | None = None,
        is_otm: bool | None = None,
        min_volume_oi_ratio: float | None = None,
    ) -> list[dict]:
        """Screen hottest option chains."""
        return _get("/api/screener/option-contracts", {
            "limit": limit,
            "min_premium": min_premium,
            "is_otm": _bool_param(is_otm),
            "min_volume_oi_ratio": min_volume_oi_ratio,
        })

    # -- Institutions ---------------------------------------------------------

    @staticmethod
    def institutions() -> list[dict]:
        """List all tracked institutions."""
        return _get("/api/institutions")

    @staticmethod
    def institution_holdings(name: str) -> list[dict]:
        """Get holdings for an institution."""
        _validate_segment(name, "institution name")
        return _get(f"/api/institution/{name}/holdings")

    @staticmethod
    def institution_ownership(ticker: str) -> list[dict]:
        """Get institutional ownership of a ticker."""
        _validate_segment(ticker)
        return _get(f"/api/institution/{ticker}/ownership")

    # -- ETFs -----------------------------------------------------------------

    @staticmethod
    def etf_holdings(ticker: str) -> list[dict]:
        """Get ETF holdings breakdown."""
        _validate_segment(ticker)
        return _get(f"/api/etfs/{ticker}/holdings")

    @staticmethod
    def etf_flow(ticker: str) -> list[dict]:
        """Get ETF inflow/outflow data."""
        _validate_segment(ticker)
        return _get(f"/api/etfs/{ticker}/in-outflow")

    # -- Seasonality ----------------------------------------------------------

    @staticmethod
    def seasonality(ticker: str) -> list[dict]:
        """Get average return per month for a ticker."""
        _validate_segment(ticker)
        return _get(f"/api/seasonality/{ticker}/monthly")

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

    @staticmethod
    def news() -> list[dict]:
        """Get financial news headlines."""
        return _get("/api/news/headlines")

    # -- Predictions ----------------------------------------------------------

    @staticmethod
    def prediction_whales() -> list[dict]:
        """Get prediction market whales."""
        return _get("/api/predictions/whales")

    @staticmethod
    def prediction_unusual() -> list[dict]:
        """Get unusual prediction market activity."""
        return _get("/api/predictions/unusual")

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

    @staticmethod
    def crypto_whales() -> list[dict]:
        """Get recent crypto whale transactions."""
        return _get("/api/crypto/whales/recent")
