"""Assemble the additive fundamentals API v1 objects (earnings, metrics,
valuation, reads) from SEC snapshots + Dolt earnings + the latest price.

Strictly additive: the router merges these into the existing FundamentalResponse
without touching legacy fields. Valuation ratios are computed at REQUEST TIME from
the stored snapshots + the latest ohlcv close (no stored valuation). Peer stats are
batched and CIK-deduplicated; invalid valuation inputs are guarded to null.
"""

from __future__ import annotations

import math
from collections import defaultdict
from datetime import date, datetime
from typing import Any
from zoneinfo import ZoneInfo

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.earnings_event import EarningsEvent
from app.models.fundamental_snapshot import FundamentalSnapshot
from app.models.ohlcv import OHLCVRecord
from app.models.ticker import Ticker
from app.services import fundamentals_derivation as deriv
from app.services import fundamentals_peers as peers
from app.services import fundamentals_reads as reads

# The fixed metric row set — every key always present, value null when unavailable.
METRIC_KEYS = (
    "revenue_growth_yoy", "eps_growth_yoy", "operating_margin", "fcf_margin",
    "net_debt", "net_debt_to_ebitda", "share_count_change_yoy",
)


async def build_fundamentals_v1(db: AsyncSession, symbol: str, *, today: date | None = None) -> dict[str, Any]:
    today = today or _ny_today()
    ticker = await _ticker_by_symbol(db, symbol)

    earnings = await _build_earnings(db, ticker.id, today) if ticker else _empty_earnings()
    if ticker is None or not ticker.cik:
        # No SEC identity: metrics present but null, valuation null, empty reads.
        return {"earnings": earnings, "metrics": _empty_metrics(), "valuation": None,
                "reads": _empty_reads()}

    subject_cik = ticker.cik
    derived = deriv.derive((await _snapshots_for(db, [subject_cik])).get(subject_cik, []))

    two = peers.two_digit_sic(ticker.sic)
    peer_derived: dict[str, deriv.DerivedFundamentals] = {}
    peer_price_by_cik: dict[str, tuple[float, date] | None] = {}
    if two:
        # Subject's representative is the REQUESTED ticker (so its price is used for
        # the subject in the peer set); other issuers pick a deterministic-by-symbol rep.
        group = await _peer_group(db, two, subject_cik, ticker.id)
        peer_snaps = await _snapshots_for(db, list(group))
        peer_derived = {cik: deriv.derive(rows) for cik, rows in peer_snaps.items()}
        closes = await _latest_closes(db, set(group.values()))
        peer_price_by_cik = {cik: closes.get(tid) for cik, tid in group.items()}

    subject_price = await _latest_close(db, ticker.id)
    metrics = _build_metrics(derived, peer_derived, two)
    valuation = _build_valuation(derived, subject_price, peer_derived, peer_price_by_cik, two)
    reads_obj = _build_reads(metrics, valuation)
    return {"earnings": earnings, "metrics": metrics, "valuation": valuation, "reads": reads_obj}


# -- earnings ----------------------------------------------------------------

async def _build_earnings(db, ticker_id: int, today: date) -> dict[str, Any]:
    rows = (await db.execute(
        select(EarningsEvent).where(EarningsEvent.ticker_id == ticker_id)
    )).scalars().all()
    # Same-day earnings are UPCOMING (days_until 0); recent is strictly earlier.
    upcoming = sorted((e for e in rows if e.announce_date >= today), key=lambda e: e.announce_date)
    past = sorted((e for e in rows if e.announce_date < today), key=lambda e: e.announce_date, reverse=True)

    nxt = None
    if upcoming:
        e = upcoming[0]
        nxt = {"date": e.announce_date.isoformat(), "session": e.session,
               "days_until": (e.announce_date - today).days}
    recent = [{
        "announce_date": e.announce_date.isoformat(),
        "period_end": _iso(e.period_end),
        "eps_estimate": e.eps_estimate,
        "eps_actual": e.eps_actual,
        "surprise_pct": _surprise_pct(e.eps_estimate, e.eps_actual),
    } for e in past[:4]]
    return {"next": nxt, "recent": recent}


def _surprise_pct(estimate, actual):
    if estimate is None or actual is None or estimate == 0:
        return None
    return round((actual - estimate) / abs(estimate) * 100.0, 2)


# -- metrics -----------------------------------------------------------------

def _build_metrics(derived, peer_derived, two: str | None) -> list[dict[str, Any]]:
    out = []
    for key in METRIC_KEYS:
        series = derived.metrics.get(key)
        value = series.value if series else None
        history = [{"period_end": _iso(p.period_end), "value": p.value} for p in (series.history if series else [])]
        industry = None
        if two and peer_derived and key in peers.HIGHER_IS_BETTER:
            group_values = [
                (pd.metrics.get(key).value if pd.metrics.get(key) else None)
                for pd in peer_derived.values()
            ]
            stat = peers.peer_stat_for(key, value, group_values)
            if stat:
                industry = {"label": f"SIC {two} peers", "median": round(stat.median, 4),
                            "favorable_percentile": stat.favorable_percentile, "peer_count": stat.peer_count}
        out.append({
            "key": key,
            "value": value,
            "history": history,
            "industry": industry,
            "period_end": _iso(series.period_end) if series else None,
            "filed_date": _iso(series.filed_date) if series else None,
            "caveat": series.caveat if series else None,
            "source": "sec",
        })
    return out


# -- valuation (request-time) ------------------------------------------------

def _build_valuation(derived, subject_price, peer_derived, peer_price_by_cik, two) -> dict[str, Any] | None:
    if derived.latest_period_end is None:
        return None  # no snapshots yet
    price = subject_price[0] if subject_price else None
    price_date = subject_price[1] if subject_price else None
    if not _finite(price) or price <= 0:
        return None  # no usable price -> valuation null (approved contract)

    pe = _pe(price, derived.ttm_diluted_eps)
    market_cap = _market_cap(price, derived.shares_outstanding)
    fcf_yield = _fcf_yield(derived.ttm_fcf, market_cap)

    pe_industry = fcf_yield_industry = None
    if two and peer_derived:
        pe_values = [_pe(_p(peer_price_by_cik.get(cik)), pd.ttm_diluted_eps) for cik, pd in peer_derived.items()]
        fy_values = [
            _fcf_yield(pd.ttm_fcf, _market_cap(_p(peer_price_by_cik.get(cik)), pd.shares_outstanding))
            for cik, pd in peer_derived.items()
        ]
        pe_industry = _industry("pe", pe, pe_values, two)
        fcf_yield_industry = _industry("fcf_yield", fcf_yield, fy_values, two)

    return {
        "pe": _round(pe, 2),
        "fcf_yield": _round(fcf_yield, 2),
        "market_cap_est": _round(market_cap, 0),
        # market_cap_est and fcf_yield both rest on the share count. When it came
        # from the weighted-average diluted fallback (multi-class issuers, whose
        # per-class cover-page count is absent from companyfacts), say so rather
        # than presenting a period average as a point-in-time count.
        "shares_estimated": bool(
            market_cap is not None and derived.shares_outstanding_estimated
        ),
        # A null P/E is ambiguous: no earnings data, or earnings we deliberately
        # suppressed. Only the latter carries a caveat, so a split-contaminated
        # TTM says why instead of looking like missing data.
        "pe_caveat": derived.ttm_diluted_eps_caveat if pe is None else None,
        "pe_industry": pe_industry,
        "fcf_yield_industry": fcf_yield_industry,
        "price_date": _iso(price_date),
    }


def _pe(price, ttm_eps):
    if not _finite(price) or price <= 0 or not _finite(ttm_eps) or ttm_eps <= 0:
        return None
    return price / ttm_eps


def _market_cap(price, shares):
    if not _finite(price) or price <= 0 or not _finite(shares) or shares <= 0:
        return None
    return price * shares


def _fcf_yield(ttm_fcf, market_cap):
    if not _finite(ttm_fcf) or not _finite(market_cap) or market_cap <= 0:
        return None
    return ttm_fcf / market_cap * 100.0


def _industry(key, subject, group_values, two):
    stat = peers.peer_stat_for(key, subject, group_values)
    if stat is None:
        return None
    return {"label": f"SIC {two} peers", "median": round(stat.median, 4),
            "favorable_percentile": stat.favorable_percentile, "peer_count": stat.peer_count}


# -- reads -------------------------------------------------------------------

_READ_KEYS = METRIC_KEYS + ("pe", "fcf_yield")


def _build_reads(metrics: list[dict], valuation: dict | None) -> dict[str, Any]:
    by_metric = {m["key"]: m for m in metrics}

    def hist(key):
        return [_Pt(p["value"]) for p in by_metric.get(key, {}).get("history", [])]

    growth = reads.growth_read(hist("revenue_growth_yoy"))
    eps_growth = reads.growth_read(hist("eps_growth_yoy"))
    op_margin = reads.margin_read(hist("operating_margin"))
    fcf_margin = reads.margin_read(hist("fcf_margin"))
    share = reads.share_count_read(by_metric.get("share_count_change_yoy", {}).get("value"))
    leverage = reads.peer_read("net_debt_to_ebitda", _pct(by_metric.get("net_debt_to_ebitda", {}).get("industry")))
    pe_read = reads.peer_read("pe", _pct(valuation.get("pe_industry"))) if valuation else None
    fcf_yield_read = reads.peer_read("fcf_yield", _pct(valuation.get("fcf_yield_industry"))) if valuation else None

    # Fixed by_key map over every metric + pe + fcf_yield (null where unavailable).
    by_key: dict[str, str | None] = {k: None for k in _READ_KEYS}
    by_key.update({
        "revenue_growth_yoy": growth,
        "eps_growth_yoy": eps_growth,
        "operating_margin": op_margin,
        "fcf_margin": fcf_margin,
        "share_count_change_yoy": share,
        "net_debt_to_ebitda": leverage,
        "pe": pe_read,
        "fcf_yield": fcf_yield_read,
    })
    header = reads.header_sentence(growth, op_margin, pe_read or fcf_yield_read) or None
    return {"header": header, "by_key": by_key}


def _empty_reads() -> dict[str, Any]:
    return {"header": None, "by_key": {k: None for k in _READ_KEYS}}


class _Pt:
    __slots__ = ("value",)

    def __init__(self, value):
        self.value = value


def _pct(industry: dict | None):
    return industry.get("favorable_percentile") if industry else None


# -- queries -----------------------------------------------------------------

async def _ticker_by_symbol(db, symbol: str) -> Ticker | None:
    return (await db.execute(
        select(Ticker).where(Ticker.symbol == symbol.strip().upper())
    )).scalar_one_or_none()


async def _snapshots_for(db, ciks) -> dict[str, list]:
    out: dict[str, list] = defaultdict(list)
    if not ciks:
        return out
    rows = (await db.execute(
        select(FundamentalSnapshot).where(FundamentalSnapshot.cik.in_(list(ciks)))
    )).scalars().all()
    for r in rows:
        out[r.cik].append(r)
    return out


async def _peer_group(db, two: str, subject_cik: str, subject_tid: int) -> dict[str, int]:
    """{cik: representative ticker_id} for tracked issuers in the 2-digit SIC group,
    CIK-deduplicated. Each issuer's representative is its lexicographically-smallest
    symbol (deterministic), EXCEPT the subject issuer, which uses the requested
    ticker — so a multi-class subject (GOOGL) is priced by the requested class, not
    an arbitrary sibling (GOOG)."""
    rows = (await db.execute(
        select(Ticker.cik, Ticker.id, Ticker.symbol)
        .where(Ticker.cik.is_not(None), func.substr(Ticker.sic, 1, 2) == two)
    )).all()
    rep: dict[str, tuple[int, str]] = {}
    for cik, tid, sym in rows:
        key = sym or ""
        if cik not in rep or key < rep[cik][1]:
            rep[cik] = (tid, key)
    group = {cik: tid for cik, (tid, _) in rep.items()}
    if subject_cik in group:
        group[subject_cik] = subject_tid  # requested ticker prices the subject
    return group


async def _latest_closes(db, ticker_ids: set[int]) -> dict[int, tuple[float, date]]:
    if not ticker_ids:
        return {}
    latest = (
        select(OHLCVRecord.ticker_id, func.max(OHLCVRecord.date).label("d"))
        .where(OHLCVRecord.ticker_id.in_(list(ticker_ids)))
        .group_by(OHLCVRecord.ticker_id)
        .subquery()
    )
    rows = (await db.execute(
        select(OHLCVRecord.ticker_id, OHLCVRecord.close, OHLCVRecord.date).join(
            latest, (OHLCVRecord.ticker_id == latest.c.ticker_id) & (OHLCVRecord.date == latest.c.d)
        )
    )).all()
    return {tid: (close, d) for tid, close, d in rows}


async def _latest_close(db, ticker_id: int) -> tuple[float, date] | None:
    return (await _latest_closes(db, {ticker_id})).get(ticker_id)


# -- helpers -----------------------------------------------------------------

def _empty_metrics() -> list[dict[str, Any]]:
    return [{"key": k, "value": None, "history": [], "industry": None,
             "period_end": None, "filed_date": None, "caveat": None,
             "source": "sec"} for k in METRIC_KEYS]


def _empty_earnings() -> dict[str, Any]:
    return {"next": None, "recent": []}


def _p(price_tuple):
    return price_tuple[0] if price_tuple else None


def _finite(v) -> bool:
    return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v)


def _round(v, ndigits):
    return round(v, ndigits) if _finite(v) else None


def _iso(d) -> str | None:
    return d.isoformat() if d else None


def _ny_today() -> date:
    """Today's New York calendar date — the market's day, not the server's."""
    return datetime.now(ZoneInfo("America/New_York")).date()
