"""Chained fundamentals provider with fallback adapters.

Order:
1) FMP (if configured)
2) Finnhub (if configured)
3) Alpha Vantage (if configured)
"""

from __future__ import annotations

import logging
import os
from datetime import date, datetime, timedelta, timezone
from pathlib import Path

import httpx

from app.config import settings
from app.exceptions import ProviderError, RateLimitError
from app.providers.fmp import FMPFundamentalProvider
from app.providers.protocol import FundamentalData, FundamentalProvider

logger = logging.getLogger(__name__)

_CA_BUNDLE = os.environ.get("SSL_CERT_FILE", "")
if not _CA_BUNDLE or not Path(_CA_BUNDLE).exists():
    _CA_BUNDLE_PATH: str | bool = True
else:
    _CA_BUNDLE_PATH = _CA_BUNDLE


def _safe_float(value: object) -> float | None:
    if value is None:
        return None
    try:
        return float(value)
    except (TypeError, ValueError):
        return None


def _to_api_symbol(symbol: str) -> str:
    """Convert internal symbol format (BRK-B) to API format (BRK.B).

    Finnhub and Alpha Vantage use dot-separated share class notation.
    """
    return symbol.replace("-", ".")


class FinnhubFundamentalProvider:
    """Fundamentals provider backed by Finnhub free endpoints."""

    def __init__(self, api_key: str) -> None:
        if not api_key:
            raise ProviderError("Finnhub API key is required")
        self._api_key = api_key
        self._base_url = "https://finnhub.io/api/v1"

    async def fetch_fundamentals(self, ticker: str) -> FundamentalData:
        unavailable: dict[str, str] = {}
        api_symbol = _to_api_symbol(ticker)

        today = date.today()
        async with httpx.AsyncClient(timeout=30.0, verify=_CA_BUNDLE_PATH) as client:
            profile_resp = await client.get(
                f"{self._base_url}/stock/profile2",
                params={"symbol": api_symbol, "token": self._api_key},
            )
            metric_resp = await client.get(
                f"{self._base_url}/stock/metric",
                params={"symbol": api_symbol, "metric": "all", "token": self._api_key},
            )
            earnings_resp = await client.get(
                f"{self._base_url}/stock/earnings",
                params={"symbol": api_symbol, "limit": 1, "token": self._api_key},
            )
            calendar_resp = await client.get(
                f"{self._base_url}/calendar/earnings",
                params={
                    "symbol": api_symbol,
                    "from": today.isoformat(),
                    "to": (today + timedelta(days=120)).isoformat(),
                    "token": self._api_key,
                },
            )

        for resp, endpoint in (
            (profile_resp, "profile2"),
            (metric_resp, "stock/metric"),
            (earnings_resp, "stock/earnings"),
            (calendar_resp, "calendar/earnings"),
        ):
            if resp.status_code == 429:
                raise RateLimitError(f"Finnhub rate limit hit for {ticker} ({endpoint})")
            if resp.status_code in (401, 403):
                raise ProviderError(f"Finnhub access denied for {ticker} ({endpoint}): HTTP {resp.status_code}")
            if resp.status_code != 200:
                raise ProviderError(f"Finnhub error for {ticker} ({endpoint}): HTTP {resp.status_code}")

        profile_payload = profile_resp.json() if profile_resp.text else {}
        metric_payload = metric_resp.json() if metric_resp.text else {}
        earnings_payload = earnings_resp.json() if earnings_resp.text else []

        metrics = metric_payload.get("metric", {}) if isinstance(metric_payload, dict) else {}
        market_cap = _safe_float((profile_payload or {}).get("marketCapitalization"))
        pe_ratio = _safe_float(metrics.get("peTTM") or metrics.get("peNormalizedAnnual"))
        revenue_growth = _safe_float(metrics.get("revenueGrowthTTMYoy") or metrics.get("revenueGrowth5Y"))

        earnings_surprise = None
        if isinstance(earnings_payload, list) and earnings_payload:
            first = earnings_payload[0] if isinstance(earnings_payload[0], dict) else {}
            earnings_surprise = _safe_float(first.get("surprisePercent"))

        next_earnings_date = self._next_earnings(calendar_resp)

        if pe_ratio is None:
            unavailable["pe_ratio"] = "not available from provider payload"
        if revenue_growth is None:
            unavailable["revenue_growth"] = "not available from provider payload"
        if earnings_surprise is None:
            unavailable["earnings_surprise"] = "not available from provider payload"
        if market_cap is None:
            unavailable["market_cap"] = "not available from provider payload"

        return FundamentalData(
            ticker=ticker,
            pe_ratio=pe_ratio,
            revenue_growth=revenue_growth,
            earnings_surprise=earnings_surprise,
            market_cap=market_cap,
            fetched_at=datetime.now(timezone.utc),
            next_earnings_date=next_earnings_date,
            unavailable_fields=unavailable,
        )

    @staticmethod
    def _next_earnings(resp: httpx.Response) -> date | None:
        """Earliest upcoming earnings date from Finnhub's calendar payload."""
        try:
            payload = resp.json() if resp.text else {}
        except ValueError:
            return None
        entries = payload.get("earningsCalendar", []) if isinstance(payload, dict) else []
        dates: list[date] = []
        today = date.today()
        for entry in entries if isinstance(entries, list) else []:
            raw = entry.get("date") if isinstance(entry, dict) else None
            if not raw:
                continue
            try:
                parsed = date.fromisoformat(raw)
            except ValueError:
                continue
            if parsed >= today:
                dates.append(parsed)
        return min(dates) if dates else None


class AlphaVantageFundamentalProvider:
    """Fundamentals provider backed by Alpha Vantage free endpoints."""

    def __init__(self, api_key: str) -> None:
        if not api_key:
            raise ProviderError("Alpha Vantage API key is required")
        self._api_key = api_key
        self._base_url = "https://www.alphavantage.co/query"

    async def fetch_fundamentals(self, ticker: str) -> FundamentalData:
        unavailable: dict[str, str] = {}
        api_symbol = _to_api_symbol(ticker)

        async with httpx.AsyncClient(timeout=30.0, verify=_CA_BUNDLE_PATH) as client:
            overview_resp = await client.get(
                self._base_url,
                params={"function": "OVERVIEW", "symbol": api_symbol, "apikey": self._api_key},
            )
            earnings_resp = await client.get(
                self._base_url,
                params={"function": "EARNINGS", "symbol": api_symbol, "apikey": self._api_key},
            )
            income_resp = await client.get(
                self._base_url,
                params={"function": "INCOME_STATEMENT", "symbol": api_symbol, "apikey": self._api_key},
            )

        for resp, endpoint in (
            (overview_resp, "OVERVIEW"),
            (earnings_resp, "EARNINGS"),
            (income_resp, "INCOME_STATEMENT"),
        ):
            if resp.status_code == 429:
                raise RateLimitError(f"Alpha Vantage rate limit hit for {ticker} ({endpoint})")
            if resp.status_code != 200:
                raise ProviderError(f"Alpha Vantage error for {ticker} ({endpoint}): HTTP {resp.status_code}")

        overview = overview_resp.json() if overview_resp.text else {}
        earnings = earnings_resp.json() if earnings_resp.text else {}
        income = income_resp.json() if income_resp.text else {}

        if isinstance(overview, dict) and overview.get("Information"):
            raise ProviderError(f"Alpha Vantage unavailable for {ticker}: {overview.get('Information')}")
        if isinstance(overview, dict) and overview.get("Note"):
            raise RateLimitError(f"Alpha Vantage rate limit for {ticker}: {overview.get('Note')}")

        pe_ratio = _safe_float((overview or {}).get("PERatio"))
        market_cap = _safe_float((overview or {}).get("MarketCapitalization"))

        earnings_surprise = None
        quarterly = earnings.get("quarterlyEarnings", []) if isinstance(earnings, dict) else []
        if isinstance(quarterly, list) and quarterly:
            first = quarterly[0] if isinstance(quarterly[0], dict) else {}
            earnings_surprise = _safe_float(first.get("surprisePercentage"))

        revenue_growth = None
        annual = income.get("annualReports", []) if isinstance(income, dict) else []
        if isinstance(annual, list) and len(annual) >= 2:
            curr = _safe_float((annual[0] or {}).get("totalRevenue"))
            prev = _safe_float((annual[1] or {}).get("totalRevenue"))
            if curr is not None and prev not in (None, 0):
                revenue_growth = ((curr - prev) / abs(prev)) * 100.0

        if pe_ratio is None:
            unavailable["pe_ratio"] = "not available from provider payload"
        if revenue_growth is None:
            unavailable["revenue_growth"] = "not available from provider payload"
        if earnings_surprise is None:
            unavailable["earnings_surprise"] = "not available from provider payload"
        if market_cap is None:
            unavailable["market_cap"] = "not available from provider payload"

        return FundamentalData(
            ticker=ticker,
            pe_ratio=pe_ratio,
            revenue_growth=revenue_growth,
            earnings_surprise=earnings_surprise,
            market_cap=market_cap,
            fetched_at=datetime.now(timezone.utc),
            unavailable_fields=unavailable,
        )


_FUNDAMENTAL_FIELDS = ("pe_ratio", "revenue_growth", "earnings_surprise", "market_cap")


class ChainedFundamentalProvider:
    """Merge fundamentals across providers, filling gaps from later sources.

    A single provider rarely covers everything on free tiers — FMP's free plan,
    for example, returns only market cap (the ratios/growth/earnings endpoints
    402). Rather than stop at the first provider with *any* field, we take each
    field from the first provider that supplies it, so FMP's market cap is
    combined with Finnhub's P/E and earnings surprise.
    """

    def __init__(self, providers: list[tuple[str, FundamentalProvider]]) -> None:
        if not providers:
            raise ProviderError("No fundamental providers configured")
        self._providers = providers

    async def fetch_fundamentals(self, ticker: str, allow_partial: bool = False) -> FundamentalData:
        """Merge fundamentals across providers.

        ``allow_partial`` controls behaviour when a fallback provider is *rate
        limited* and we end up with missing fields. By default we raise
        RateLimitError so the caller (the bulk collector) can back off and retry
        the ticker once the window frees — otherwise a transient 429 on Finnhub
        would be silently stored as market-cap-only. Pass ``allow_partial=True``
        (manual single fetches, or the collector's final give-up attempt) to
        accept whatever was gathered instead of raising.
        """
        merged: dict[str, float | None] = {f: None for f in _FUNDAMENTAL_FIELDS}
        field_source: dict[str, str] = {}
        errors: list[str] = []
        rate_limited = False
        next_earnings_date = None

        for provider_name, provider in self._providers:
            if all(merged[f] is not None for f in _FUNDAMENTAL_FIELDS) and next_earnings_date:
                break
            try:
                data = await provider.fetch_fundamentals(ticker)
            except RateLimitError as exc:
                rate_limited = True
                errors.append(f"{provider_name}: RateLimitError: {exc}")
                continue
            except Exception as exc:
                errors.append(f"{provider_name}: {type(exc).__name__}: {exc}")
                continue

            if next_earnings_date is None and data.next_earnings_date is not None:
                next_earnings_date = data.next_earnings_date

            for field in _FUNDAMENTAL_FIELDS:
                if merged[field] is None:
                    value = getattr(data, field)
                    if value is not None:
                        merged[field] = value
                        field_source[field] = provider_name

        missing = [f for f in _FUNDAMENTAL_FIELDS if merged[f] is None]

        # A rate limit left data incomplete: signal it (unless partial is OK) so
        # the collector backs off rather than persisting a degraded record.
        if rate_limited and missing and not allow_partial:
            attempts = "; ".join(errors[:6])
            raise RateLimitError(
                f"Fundamentals incomplete for {ticker} due to provider rate limits "
                f"(missing {', '.join(missing)}). Attempts: {attempts}"
            )

        if all(merged[f] is None for f in _FUNDAMENTAL_FIELDS):
            attempts = "; ".join(errors[:6]) if errors else "no usable metrics from any provider"
            raise ProviderError(f"All fundamentals providers failed for {ticker}. Attempts: {attempts}")

        unavailable: dict[str, str] = {
            field: "not available from any configured provider"
            for field in _FUNDAMENTAL_FIELDS
            if merged[field] is None
        }
        # Record which provider supplied each field for transparency.
        for field, src in field_source.items():
            unavailable[f"source_{field}"] = src

        return FundamentalData(
            ticker=ticker,
            pe_ratio=merged["pe_ratio"],
            revenue_growth=merged["revenue_growth"],
            earnings_surprise=merged["earnings_surprise"],
            market_cap=merged["market_cap"],
            fetched_at=datetime.now(timezone.utc),
            next_earnings_date=next_earnings_date,
            unavailable_fields=unavailable,
        )


def build_fundamental_provider_chain() -> FundamentalProvider:
    providers: list[tuple[str, FundamentalProvider]] = []

    if settings.fmp_api_key:
        providers.append(("fmp", FMPFundamentalProvider(settings.fmp_api_key)))
    if settings.finnhub_api_key:
        providers.append(("finnhub", FinnhubFundamentalProvider(settings.finnhub_api_key)))
    if settings.alpha_vantage_api_key:
        providers.append(("alpha_vantage", AlphaVantageFundamentalProvider(settings.alpha_vantage_api_key)))

    if not providers:
        raise ProviderError(
            "No fundamentals provider configured. Set one of FMP_API_KEY, FINNHUB_API_KEY, ALPHA_VANTAGE_API_KEY"
        )

    logger.info("Fundamentals provider chain configured: %s", [name for name, _ in providers])
    return ChainedFundamentalProvider(providers)
