"""urllib FundamentalsClient — Tiingo fundamentals for the equity-factor sleeve. Two reads:

  metrics(symbol):            latest DAILY valuation metrics (marketCap, enterpriseVal, peRatio,
                              pbRatio, trailingPEG1Y, ...). Endpoint serves a flat JSON list of
                              dated rows; we read the LATEST (max date) row.
                                GET {base}/tiingo/fundamentals/{ticker}/daily?startDate=YYYY-MM-DD
                                → [{date, marketCap, enterpriseVal, peRatio, pbRatio, trailingPEG1Y}]

  statement_metrics(symbol):  latest reported STATEMENT metrics (piotroskiFScore, roe, roa,
                              debtEquity, grossMargin, profitMargin, revenueQoQ, bookVal, epsDil, ...).
                              Endpoint serves a JSON list of period entries; we take the LATEST
                              (max date) entry and flatten its statementData sections (overview +
                              incomeStatement + balanceSheet + cashFlow) into {dataCode: float(value)}.
                                GET {base}/tiingo/fundamentals/{ticker}/statements?startDate=YYYY-MM-DD
                                → [{date, year, quarter, statementData: {overview: [{dataCode, value}],
                                    incomeStatement: [...], balanceSheet: [...], cashFlow: [...]}}]

Both default a startDate window of ~800 days (well inside entitlement; gives several recent quarters
of statements and a long daily-metrics history) and read the most-recent available period.

Auth: header `Authorization: Token <key>` with TIINGO_API_KEY (env). The key is used ONLY in the
request header — never logged or interpolated into a URL.

NOTE: shapes above are coded to the documented/verified Tiingo response shapes; this build env had
no TIINGO_API_KEY, so the first live run will confirm. None-valued fields are skipped.
"""
from __future__ import annotations

import datetime as dt
import json
import os
import time
import urllib.parse
import urllib.request
from typing import Any

_DAILY = "{base}/tiingo/fundamentals/{sym}/daily?startDate={frm}&format=json"
_STATEMENTS = "{base}/tiingo/fundamentals/{sym}/statements?startDate={frm}&format=json"

# Valuation fields read from a fundamentals/daily row (all but `date`).
_DAILY_FIELDS = ("marketCap", "enterpriseVal", "peRatio", "pbRatio", "trailingPEG1Y")
# statementData sections flattened by statement_metrics.
_STATEMENT_SECTIONS = ("overview", "incomeStatement", "balanceSheet", "cashFlow")


class TiingoFundamentalsClient:
    def __init__(
        self,
        api_key: str | None = None,
        lookback_days: int = 800,
        base_url: str = "https://api.tiingo.com",
    ) -> None:
        self._key = api_key if api_key is not None else os.environ["TIINGO_API_KEY"]
        self._lookback_days = lookback_days
        self._base = base_url.rstrip("/")

    def _get(self, url: str, tries: int = 4) -> Any:
        for a in range(tries):
            try:
                # The key lives ONLY in this header — never in the URL or any log line.
                req = urllib.request.Request(url, headers={"Authorization": f"Token {self._key}"})
                return json.loads(urllib.request.urlopen(req, timeout=30).read())
            except Exception:  # noqa: BLE001 — transient HTTP; retry then re-raise
                if a == tries - 1:
                    raise
                time.sleep(2 * (a + 1))
        raise RuntimeError("unreachable")

    def _start(self) -> str:
        return (dt.date.today() - dt.timedelta(days=self._lookback_days)).isoformat()

    @staticmethod
    def _to_float(value: Any) -> float | None:
        if value is None:
            return None
        try:
            return float(value)
        except (TypeError, ValueError):
            return None

    @staticmethod
    def _latest(rows: list[dict[str, Any]]) -> dict[str, Any]:
        """The row/entry with the max `date` (lexical compare on ISO/`YYYY-MM-DD...` strings)."""
        return max(rows, key=lambda r: str(r.get("date", "")))

    def metrics(self, symbol: str) -> dict[str, float]:
        sym = urllib.parse.quote(symbol)
        rows: list[dict[str, Any]] = self._get(_DAILY.format(base=self._base, sym=sym, frm=self._start())) or []
        if not rows:
            return {}
        latest = self._latest(rows)
        out: dict[str, float] = {}
        for field in _DAILY_FIELDS:
            v = self._to_float(latest.get(field))
            if v is not None:
                out[field] = v
        return out

    def statement_metrics(self, symbol: str) -> dict[str, float]:
        sym = urllib.parse.quote(symbol)
        entries: list[dict[str, Any]] = (
            self._get(_STATEMENTS.format(base=self._base, sym=sym, frm=self._start())) or []
        )
        if not entries:
            return {}
        latest = self._latest(entries)
        data: dict[str, Any] = latest.get("statementData") or {}
        out: dict[str, float] = {}
        for section in _STATEMENT_SECTIONS:
            for item in data.get(section) or []:
                code = item.get("dataCode")
                if code is None:
                    continue
                v = self._to_float(item.get("value"))
                if v is not None:
                    out[str(code)] = v
        return out
