"""
Financial Modeling Prep adapter — primary provider (docs/DATA_SOURCES.md §3).

Endpoint paths and field names follow FMP's documented v3 REST API. This adapter has NOT been
exercised against the live API in this build environment (network egress here is restricted to
a package-registry allowlist that does not include financialmodelingprep.com — see
docs/DATA_SOURCES.md §3 and TROUBLESHOOTING.md). Before relying on this in production: run
`scripts/probe_provider.py --provider fmp --ticker AAPL` with a real `FMP_API_KEY` and diff the
actual response shape against the field mapping below — FMP's field names do shift between API
versions and this mapping was written from documented/known conventions, not a live response.
"""
from __future__ import annotations

from datetime import date, datetime
from typing import Optional

import httpx

from app.adapters.base import ProviderAdapter, ProviderAuthError, ProviderNotFoundError, ProviderRateLimitError
from app.adapters.validation import ValidationReport, validate_line_items, validate_price_bar
from app.adapters.schemas import (
    ProviderCompanyProfile, ProviderDividendRow, ProviderEstimateRow, ProviderFinancialPeriod,
    ProviderPriceBar,
)

# FMP field name -> our canonical LineItems field name (see app/engines/types.py::LineItems)
_INCOME_MAP = {
    "revenue": "revenue", "costOfRevenue": "cogs", "grossProfit": "gross_profit",
    "operatingIncome": "operating_income", "ebitda": "ebitda", "netIncome": "net_income",
    "eps": "eps_basic", "epsdiluted": "eps_diluted", "incomeTaxExpense": "tax_expense",
    "incomeBeforeTax": "pretax_income", "interestExpense": "interest_expense",
    "depreciationAndAmortization": "depreciation_and_amortization",
}
_BALANCE_MAP = {
    "cashAndCashEquivalents": "cash_and_equivalents", "shortTermInvestments": "short_term_investments",
    "totalDebt": "total_debt", "shortTermDebt": "short_term_debt", "longTermDebt": "long_term_debt",
    "capitalLeaseObligations": "lease_liabilities", "totalAssets": "total_assets",
    "totalCurrentAssets": "current_assets", "totalCurrentLiabilities": "current_liabilities",
    "totalStockholdersEquity": "shareholders_equity", "minorityInterest": "minority_interest",
    "preferredStock": "preferred_equity", "goodwill": "goodwill",
    "intangibleAssets": "intangible_assets", "netReceivables": "receivables", "inventory": "inventory",
}
_CASHFLOW_MAP = {
    "operatingCashFlow": "operating_cash_flow", "capitalExpenditure": "capital_expenditure",
    "freeCashFlow": "free_cash_flow", "dividendsPaid": "dividends_paid",
    "commonStockRepurchased": "buybacks", "commonStockIssued": "stock_issuance",
    "netIncome": "net_income",
}


def _map_fields(raw: dict, mapping: dict) -> dict:
    out = {}
    for fmp_key, canonical_key in mapping.items():
        val = raw.get(fmp_key)
        if val is not None:
            # capex/buybacks come back negative (cash outflow) from FMP by convention — store as
            # positive magnitudes, matching LineItems' documented convention.
            if canonical_key in ("capital_expenditure", "dividends_paid", "buybacks") and val < 0:
                val = -val
            out[canonical_key] = val
    return out


def _parse_date(s: Optional[str]) -> Optional[date]:
    if not s:
        return None
    return datetime.strptime(s[:10], "%Y-%m-%d").date()


class FMPAdapter(ProviderAdapter):
    name = "FMP"
    tier = "PRIMARY"

    def __init__(
        self, api_key: str, base_url: str = "https://financialmodelingprep.com/api/v3",
        timeout: float = 20.0, transport: Optional[httpx.BaseTransport] = None,
    ):
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        # `transport` is exposed purely for testing (httpx.MockTransport) — production callers
        # never pass it, so real requests always go over the network via the default transport.
        self._client = httpx.Client(base_url=self._base_url, timeout=timeout, transport=transport)

    def _get(self, path: str, **params) -> list | dict:
        params["apikey"] = self._api_key
        resp = self._client.get(path, params=params)
        if resp.status_code == 401:
            raise ProviderAuthError(f"FMP auth failed for {path}")
        if resp.status_code == 429:
            raise ProviderRateLimitError(f"FMP rate limit hit for {path}")
        if resp.status_code == 404:
            raise ProviderNotFoundError(f"FMP 404 for {path}")
        resp.raise_for_status()
        return resp.json()

    def get_company_profile(self, ticker: str, exchange_mic: Optional[str] = None) -> ProviderCompanyProfile:
        data = self._get(f"/profile/{ticker}")
        if not data:
            raise ProviderNotFoundError(f"No FMP profile for {ticker}")
        row = data[0]
        return ProviderCompanyProfile(
            ticker=ticker, exchange_mic=row.get("exchangeShortName"), legal_name=row.get("companyName", ticker),
            display_name=row.get("companyName", ticker), country_iso2=row.get("country"),
            sector=row.get("sector"), industry=row.get("industry"), currency=row.get("currency", "USD"),
            isin=row.get("isin"), website=row.get("website"), description=row.get("description"),
            beta=row.get("beta"),
        )

    def _get_statements(self, endpoint: str, mapping: dict, ticker: str, period: str, limit: int) -> list[ProviderFinancialPeriod]:
        """AUDIT FIX (Part D): line items are now type-coerced and sanity-checked before leaving
        the adapter. A period is never rejected wholesale -- one impossible field does not
        invalidate the other thirty -- but a non-numeric or arithmetically-impossible value (a
        negative revenue, a negative share count) is dropped with a recorded reason instead of
        travelling into the metrics engine as a string or a nonsense number.
        `self.last_statement_validation` holds the report for the most recent call."""
        rows = self._get(f"/{endpoint}/{ticker}", period=period, limit=limit)
        report = ValidationReport()
        out = []
        for row in rows:
            period_end = _parse_date(row.get("date"))
            out.append(ProviderFinancialPeriod(
                period_end=period_end,
                period_type="FY" if period == "annual" else row.get("period", "Q"),
                filing_date=_parse_date(row.get("fillingDate") or row.get("acceptedDate")),
                currency=row.get("reportedCurrency", "USD"),
                line_items=validate_line_items(
                    _map_fields(row, mapping), report, row_key=str(period_end),
                ),
            ))
        self.last_statement_validation = report
        return out

    def get_income_statements(self, ticker: str, period: str = "annual", limit: int = 11) -> list[ProviderFinancialPeriod]:
        return self._get_statements("income-statement", _INCOME_MAP, ticker, period, limit)

    def get_balance_sheets(self, ticker: str, period: str = "annual", limit: int = 11) -> list[ProviderFinancialPeriod]:
        return self._get_statements("balance-sheet-statement", _BALANCE_MAP, ticker, period, limit)

    def get_cash_flows(self, ticker: str, period: str = "annual", limit: int = 11) -> list[ProviderFinancialPeriod]:
        return self._get_statements("cash-flow-statement", _CASHFLOW_MAP, ticker, period, limit)

    def get_prices(self, ticker: str, start: date, end: date) -> list[ProviderPriceBar]:
        """AUDIT FIX (Part D — docs/AUDIT_VALIDATION_D.md): see EODHDAdapter.get_prices() for the
        full reasoning. Same policy applied here so the two adapters cannot diverge: reject the
        row, not the batch, and record why. `self.last_price_validation` holds the report."""
        data = self._get(f"/historical-price-full/{ticker}", **{"from": start.isoformat(), "to": end.isoformat()})
        rows = data.get("historical", []) if isinstance(data, dict) else []
        report = ValidationReport()
        bars = []
        for r in rows:
            clean = validate_price_bar(
                {
                    "date": r.get("date"), "open": r.get("open"), "high": r.get("high"),
                    "low": r.get("low"), "close": r.get("close"),
                    "adjusted_close": r.get("adjClose"), "volume": r.get("volume"),
                },
                report,
            )
            if clean is None:
                continue
            bars.append(ProviderPriceBar(
                date=_parse_date(clean["date"]), open=clean["open"], high=clean["high"],
                low=clean["low"], close=clean["close"], adjusted_close=clean["adjusted_close"],
                volume=clean["volume"], currency="USD",
            ))
        self.last_price_validation = report
        return bars

    def get_estimates(self, ticker: str) -> list[ProviderEstimateRow]:
        rows = self._get(f"/analyst-estimates/{ticker}")
        out = []
        for r in rows:
            period_end = _parse_date(r.get("date"))
            for metric_key, fmp_key in (("eps", "estimatedEpsAvg"), ("revenue", "estimatedRevenueAvg")):
                if r.get(fmp_key) is not None:
                    out.append(ProviderEstimateRow(
                        period_end=period_end, metric=metric_key, consensus_value=r[fmp_key],
                        num_analysts=r.get("numberAnalystEstimatedRevenue"), as_of_date=date.today(),
                    ))
        return out

    def get_dividends(self, ticker: str) -> list[ProviderDividendRow]:
        data = self._get(f"/historical-price-full/stock_dividend/{ticker}")
        rows = data.get("historical", []) if isinstance(data, dict) else []
        return [
            ProviderDividendRow(
                ex_date=_parse_date(r["date"]), pay_date=_parse_date(r.get("paymentDate")),
                amount_per_share=r.get("dividend", 0.0), currency="USD",
            )
            for r in rows
        ]

    def list_universe(self, exchange_mic: Optional[str] = None, country_iso2: Optional[str] = None) -> list[str]:
        rows = self._get("/stock-screener", exchange=exchange_mic, country=country_iso2, limit=1000)
        return [r["symbol"] for r in rows if r.get("symbol")]
