"""
DemoDataAdapter — the ONLY adapter that runs without a real API key (docs/DATA_SOURCES.md §9).

Produces 11 years of internally-consistent, deterministic SYNTHETIC financials for a set of
real, well-known tickers spanning multiple regions/sectors, so the full pipeline (ingestion ->
metrics -> scoring -> valuation -> recommendation -> screener -> UI) is demonstrable end to end
without live provider access. Every row this adapter returns must be persisted with
`sources.is_demo = True` by the caller — see `app/workers/ingest.py` — and the numbers here are
NOT real reported financials for these companies; they are a plausible synthetic trajectory
generated from a small seed profile per company, with a fixed random seed per ticker so the
same "demo run" is reproducible.
"""
from __future__ import annotations

import random
from dataclasses import dataclass
from datetime import date, timedelta
from typing import Optional

from app.adapters.base import ProviderAdapter, ProviderNotFoundError
from app.adapters.schemas import (
    ProviderCompanyProfile, ProviderDividendRow, ProviderEstimateRow, ProviderFinancialPeriod,
    ProviderPriceBar,
)

YEARS_OF_HISTORY = 11  # current + 10 prior, enough for every CAGR window in FINANCIAL_FORMULAS.md


@dataclass(frozen=True)
class DemoSeedProfile:
    ticker: str
    exchange_mic: str
    legal_name: str
    country_iso2: str
    sector_code: str
    industry_code: str
    currency: str
    starting_revenue_musd: float     # 10 years ago
    revenue_cagr: float
    gross_margin: float
    operating_margin: float
    net_margin_adj: float            # adjustment vs. a naive EBIT*(1-tax) estimate, for realism
    tax_rate: float
    capex_pct_revenue: float
    debt_to_ebitda_target: float
    dividend_payout_pct: float       # of net income; 0 for non-payers
    buyback_pct_of_fcf: float
    shares_outstanding_musd_equiv: float  # millions of shares, current
    beta: float
    pe_assumption: float             # used only to derive a plausible demo price


# A representative global set spanning the spec's minimum region list (§5). Real, well-known
# companies; ALL financial figures below are illustrative seed assumptions, not reported data.
DEMO_SEED_PROFILES: list[DemoSeedProfile] = [
    DemoSeedProfile("AAPL", "XNAS", "Apple Inc. (DEMO)", "US", "TECHNOLOGY", "CONSUMER_ELECTRONICS", "USD", 180000, 0.08, 0.42, 0.30, 0.0, 0.15, 0.02, 0.5, 0.15, 0.75, 15500, 1.25, 28),
    DemoSeedProfile("MSFT", "XNAS", "Microsoft Corp. (DEMO)", "US", "TECHNOLOGY", "SOFTWARE", "USD", 90000, 0.13, 0.68, 0.42, 0.0, 0.17, 0.10, 1.0, 0.28, 0.25, 7400, 0.90, 32),
    DemoSeedProfile("NVDA", "XNAS", "NVIDIA Corp. (DEMO)", "US", "TECHNOLOGY", "SEMICONDUCTORS", "USD", 11000, 0.35, 0.72, 0.45, 0.0, 0.16, 0.05, 0.1, 0.02, 0.10, 24500, 1.7, 45),
    DemoSeedProfile("JNJ", "XNYS", "Johnson & Johnson (DEMO)", "US", "HEALTHCARE", "PHARMACEUTICALS", "USD", 70000, 0.04, 0.66, 0.24, 0.0, 0.15, 0.05, 1.2, 0.55, 0.20, 2600, 0.55, 17),
    DemoSeedProfile("JPM", "XNYS", "JPMorgan Chase (DEMO)", "US", "FINANCIALS", "BANKS", "USD", 100000, 0.06, None, 0.35, 0.0, 0.22, None, None, 0.28, 0.20, 2900, 1.1, 12),
    DemoSeedProfile("PG", "XNYS", "Procter & Gamble (DEMO)", "US", "CONSUMER_STAPLES", "HOUSEHOLD_PRODUCTS", "USD", 65000, 0.03, 0.49, 0.22, 0.0, 0.19, 0.04, 1.0, 0.58, 0.20, 2350, 0.45, 24),
    DemoSeedProfile("XOM", "XNYS", "ExxonMobil (DEMO)", "US", "ENERGY", "OIL_GAS", "USD", 240000, 0.02, 0.22, 0.10, 0.0, 0.20, 0.07, 1.0, 0.40, 0.15, 4000, 1.15, 13),
    DemoSeedProfile("SAP", "XETR", "SAP SE (DEMO)", "DE", "TECHNOLOGY", "SOFTWARE", "EUR", 24000, 0.07, 0.72, 0.24, 0.0, 0.24, 0.03, 1.5, 0.35, 0.10, 1230, 0.95, 26),
    DemoSeedProfile("ASML", "XAMS", "ASML Holding (DEMO)", "NL", "TECHNOLOGY", "SEMICONDUCTOR_EQUIPMENT", "EUR", 11000, 0.15, 0.50, 0.32, 0.0, 0.16, 0.06, 0.3, 0.20, 0.05, 393, 1.15, 33),
    DemoSeedProfile("NESN", "XSWX", "Nestle S.A. (DEMO)", "CH", "CONSUMER_STAPLES", "PACKAGED_FOODS", "CHF", 88000, 0.02, 0.47, 0.16, 0.0, 0.20, 0.05, 1.3, 0.65, 0.15, 2650, 0.55, 20),
    DemoSeedProfile("MC", "XPAR", "LVMH (DEMO)", "FR", "CONSUMER_DISCRETIONARY", "LUXURY_GOODS", "EUR", 35000, 0.10, 0.68, 0.26, 0.0, 0.23, 0.05, 0.9, 0.45, 0.05, 500, 1.05, 24),
    DemoSeedProfile("NOVO-B", "XCSE", "Novo Nordisk (DEMO)", "DK", "HEALTHCARE", "PHARMACEUTICALS", "DKK", 90000, 0.14, 0.83, 0.44, 0.0, 0.20, 0.06, 0.6, 0.45, 0.05, 4480, 0.35, 34),
    DemoSeedProfile("SHEL", "XLON", "Shell plc (DEMO)", "GB", "ENERGY", "OIL_GAS", "USD", 260000, 0.02, 0.20, 0.09, 0.0, 0.30, 0.06, 1.0, 0.40, 0.20, 6900, 1.05, 11),
    DemoSeedProfile("7203", "XTKS", "Toyota Motor (DEMO)", "JP", "CONSUMER_DISCRETIONARY", "AUTOMOBILES", "JPY", 25000000, 0.03, 0.19, 0.08, 0.0, 0.24, 0.05, 1.8, 0.30, 0.05, 14300, 0.75, 10),
    DemoSeedProfile("005930", "XKRX", "Samsung Electronics (DEMO)", "KR", "TECHNOLOGY", "SEMICONDUCTORS", "KRW", 240000000, 0.05, 0.38, 0.16, 0.0, 0.20, 0.14, 0.4, 0.25, 0.02, 5970000, 1.0, 14),
    DemoSeedProfile("0700", "XHKG", "Tencent Holdings (DEMO)", "HK", "TECHNOLOGY", "INTERNET_SERVICES", "HKD", 400000, 0.14, 0.48, 0.32, 0.0, 0.15, 0.06, 0.5, 0.0, 0.10, 9400, 1.1, 22),
    DemoSeedProfile("005380", "XKRX", "Hyundai Motor (DEMO)", "KR", "CONSUMER_DISCRETIONARY", "AUTOMOBILES", "KRW", 100000000, 0.04, 0.20, 0.08, 0.0, 0.23, 0.04, 1.2, 0.25, 0.05, 208000, 1.1, 6),
    DemoSeedProfile("RELIANCE", "XBOM", "Reliance Industries (DEMO)", "IN", "ENERGY", "CONGLOMERATE", "INR", 4500000, 0.12, 0.28, 0.14, 0.0, 0.24, 0.09, 1.5, 0.10, 0.02, 6770, 0.95, 25),
    DemoSeedProfile("CBA", "XASX", "Commonwealth Bank (DEMO)", "AU", "FINANCIALS", "BANKS", "AUD", 20000, 0.04, None, 0.42, 0.0, 0.28, None, None, 0.70, 0.05, 1700, 0.85, 17),
    DemoSeedProfile("4SBK", "XBUL", "First Investment Bank (DEMO)", "BG", "FINANCIALS", "BANKS", "EUR", 500, 0.05, None, 0.30, 0.0, 0.10, None, None, 0.15, 0.0, 176, 0.90, 8),
]

_INDEX_BY_TICKER = {p.ticker: p for p in DEMO_SEED_PROFILES}


def _seeded_random(ticker: str) -> random.Random:
    return random.Random(f"STOCKLAB-DEMO-{ticker}")


def _jitter(rng: random.Random, base: float, pct: float = 0.06) -> float:
    return base * (1 + rng.uniform(-pct, pct))


def _generate_annual_series(profile: DemoSeedProfile) -> list[dict]:
    """Returns YEARS_OF_HISTORY dicts, oldest first, each a full LineItems-shaped field set."""
    rng = _seeded_random(profile.ticker)
    is_bank = profile.sector_code == "FINANCIALS"
    revenue = profile.starting_revenue_musd
    years = []
    today_year = date.today().year
    for i in range(YEARS_OF_HISTORY):
        year = today_year - (YEARS_OF_HISTORY - 1 - i)
        if i > 0:
            revenue = revenue * (1 + _jitter(rng, profile.revenue_cagr))
        gross_margin = profile.gross_margin if profile.gross_margin is not None else None
        cogs = revenue * (1 - gross_margin) if gross_margin is not None else None
        gross_profit = revenue - cogs if cogs is not None else None
        operating_income = revenue * _jitter(rng, profile.operating_margin, 0.04)
        ebit = operating_income
        d_and_a = revenue * 0.03
        ebitda = ebit + d_and_a
        interest_expense = revenue * 0.01 if not is_bank else revenue * 0.005
        pretax_income = ebit - interest_expense
        tax_expense = max(pretax_income, 0) * profile.tax_rate
        net_income = pretax_income - tax_expense

        shares = profile.shares_outstanding_musd_equiv * ((1 - profile.buyback_pct_of_fcf * 0.01) ** i)
        eps_diluted = net_income / shares if shares else None

        capex = revenue * profile.capex_pct_revenue if profile.capex_pct_revenue else revenue * 0.03
        operating_cash_flow = net_income + d_and_a * 1.1
        fcf = operating_cash_flow - capex
        dividends_paid = max(net_income, 0) * profile.dividend_payout_pct * 0.5 if profile.dividend_payout_pct else 0.0
        buybacks = max(fcf, 0) * profile.buyback_pct_of_fcf if profile.buyback_pct_of_fcf else 0.0

        ebitda_for_debt = ebitda if ebitda else revenue * 0.15
        total_debt = ebitda_for_debt * profile.debt_to_ebitda_target if profile.debt_to_ebitda_target is not None else revenue * 0.6
        cash = revenue * 0.15
        shareholders_equity = revenue * 0.35 if not is_bank else revenue * 1.2
        current_assets = revenue * 0.30
        current_liabilities = revenue * 0.20

        # AUDIT FIX (StockLab final engineering pass, Part A9): the demo generator never emitted
        # total_assets, short_term_debt, long_term_debt or lease_liabilities. total_assets in
        # particular is an input to ROA, asset turnover, the goodwill-concentration red flag and
        # the acquisition-driven-growth red flag -- all of which were therefore permanently
        # SKIPPED_INSUFFICIENT_DATA in demo mode, which is the mode this platform runs in by
        # default. Derived from the liabilities-and-equity side so the synthetic balance sheet
        # actually balances (A = L + E) instead of being an independently invented number:
        other_liabilities = revenue * 0.05
        total_assets = shareholders_equity + total_debt + current_liabilities + other_liabilities
        # A conventional maturity split; the demo profiles carry no maturity schedule of their own.
        short_term_debt = total_debt * 0.15
        long_term_debt = total_debt - short_term_debt
        lease_liabilities = revenue * 0.02

        years.append(dict(
            period_end=date(year, 12, 31), period_type="FY", filing_date=date(year + 1, 2, 15),
            currency=profile.currency,
            revenue=round(revenue, 1), cogs=round(cogs, 1) if cogs else None,
            gross_profit=round(gross_profit, 1) if gross_profit else None,
            operating_income=round(operating_income, 1), ebit=round(ebit, 1), ebitda=round(ebitda, 1),
            net_income=round(net_income, 1), eps_diluted=round(eps_diluted, 4) if eps_diluted else None,
            tax_expense=round(tax_expense, 1), pretax_income=round(pretax_income, 1),
            interest_expense=round(interest_expense, 1),
            depreciation_and_amortization=round(d_and_a, 1),
            cash_and_equivalents=round(cash, 1), short_term_investments=0.0,
            total_debt=round(total_debt, 1), shareholders_equity=round(shareholders_equity, 1),
            total_assets=round(total_assets, 1), short_term_debt=round(short_term_debt, 1),
            long_term_debt=round(long_term_debt, 1), lease_liabilities=round(lease_liabilities, 1),
            minority_interest=0.0, preferred_equity=0.0, goodwill=round(revenue * 0.1, 1),
            intangible_assets=round(revenue * 0.05, 1),
            current_assets=round(current_assets, 1), current_liabilities=round(current_liabilities, 1),
            receivables=round(revenue * 0.08, 1), inventory=round(revenue * 0.06, 1) if gross_margin else 0.0,
            shares_outstanding=round(shares, 2), diluted_shares=round(shares, 2),
            operating_cash_flow=round(operating_cash_flow, 1), capital_expenditure=round(capex, 1),
            dividends_paid=round(dividends_paid, 1), buybacks=round(buybacks, 1), stock_issuance=0.0,
            stock_based_compensation=round(revenue * 0.01, 1),
        ))
    return years


class DemoDataAdapter(ProviderAdapter):
    name = "DEMO"
    tier = "DEMO"

    def _profile(self, ticker: str) -> DemoSeedProfile:
        p = _INDEX_BY_TICKER.get(ticker.upper())
        if not p:
            raise ProviderNotFoundError(f"No DEMO seed profile for {ticker}. Available: {sorted(_INDEX_BY_TICKER)}")
        return p

    def get_company_profile(self, ticker: str, exchange_mic: Optional[str] = None) -> ProviderCompanyProfile:
        p = self._profile(ticker)
        return ProviderCompanyProfile(
            ticker=p.ticker, exchange_mic=p.exchange_mic, legal_name=p.legal_name, display_name=p.legal_name,
            country_iso2=p.country_iso2, sector=p.sector_code, industry=p.industry_code, currency=p.currency,
            beta=p.beta, description="DEMO DATA — synthetic financials, not reported figures. See docs/DATA_SOURCES.md §9.",
        )

    def _statements(self, ticker: str) -> list[dict]:
        return _generate_annual_series(self._profile(ticker))

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

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

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

    def _to_periods(self, ticker: str, limit: int) -> list[ProviderFinancialPeriod]:
        rows = self._statements(ticker)[-limit:]
        p = self._profile(ticker)
        return [
            ProviderFinancialPeriod(
                period_end=r["period_end"], period_type=r["period_type"], filing_date=r["filing_date"],
                currency=p.currency, line_items=r,
            )
            for r in reversed(rows)  # most recent first, matching FinancialSnapshot's convention
        ]

    def get_prices(self, ticker: str, start: date, end: date) -> list[ProviderPriceBar]:
        p = self._profile(ticker)
        latest = self._statements(ticker)[-1]
        eps = latest["eps_diluted"] or 1.0
        base_price = max(eps * p.pe_assumption, 1.0)
        rng = _seeded_random(ticker + "-price")
        bars = []
        d = start
        price = base_price * 0.85  # start a bit below today's synthetic price, drift up to it
        while d <= end:
            if d.weekday() < 5:
                price = max(price * (1 + rng.uniform(-0.01, 0.011)), 0.01)
                bars.append(ProviderPriceBar(date=d, open=price, high=price * 1.01, low=price * 0.99,
                                              close=price, adjusted_close=price, volume=1_000_000, currency=p.currency))
            d += timedelta(days=1)
        if bars:
            bars[-1] = ProviderPriceBar(bars[-1].date, bars[-1].open, bars[-1].high, bars[-1].low,
                                         base_price, base_price, bars[-1].volume, p.currency)
        return bars

    def get_estimates(self, ticker: str) -> list[ProviderEstimateRow]:
        rows = self._statements(ticker)
        last_eps = rows[-1]["eps_diluted"]
        p = self._profile(ticker)
        if last_eps is None:
            return []
        forward = last_eps * (1 + p.revenue_cagr)
        return [ProviderEstimateRow(
            period_end=date(date.today().year + 1, 12, 31), metric="eps", consensus_value=round(forward, 4),
            num_analysts=12, as_of_date=date.today(),
        )]

    def get_dividends(self, ticker: str) -> list[ProviderDividendRow]:
        p = self._profile(ticker)
        if not p.dividend_payout_pct:
            return []
        rows = self._statements(ticker)
        out = []
        for r in rows[-5:]:
            if r["dividends_paid"] and r["shares_outstanding"]:
                dps = r["dividends_paid"] / r["shares_outstanding"]
                out.append(ProviderDividendRow(
                    ex_date=date(r["period_end"].year, 11, 15), pay_date=date(r["period_end"].year, 12, 1),
                    amount_per_share=round(dps, 4), currency=p.currency,
                ))
        return out

    def list_universe(self, exchange_mic: Optional[str] = None, country_iso2: Optional[str] = None) -> list[str]:
        return [
            p.ticker for p in DEMO_SEED_PROFILES
            if (exchange_mic is None or p.exchange_mic == exchange_mic)
            and (country_iso2 is None or p.country_iso2 == country_iso2)
        ]
