"""Per-ticker validation + historical-price endpoints.

These power the dashboard's "Add a position" form. Neither endpoint
persists holdings — they wrap the existing Yahoo chart fetcher and
optionally seed anonymous ticker_universe (validate only).

Both endpoints are gated behind ``require_paid`` so they match the rest
of the import surface.
"""
from __future__ import annotations

from datetime import datetime, timezone

import httpx
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession

from app.auth import require_auth
from app.db import get_session, utcnow
from app.logging import get_logger
from app.models import Quote as QuoteModel
from app.services.access import require_paid
from app.services.market import (
    UA, YAHOO_CHART, Quote, _yahoo_range_covering, fetch_yahoo,
)
from app.services.ticker_universe import upsert_tickers


log = get_logger("ticker_validate")

router = APIRouter(dependencies=[Depends(require_auth)])


@router.get(
    "/ticker/validate",
    dependencies=[Depends(require_paid)],
)
async def validate_ticker(
    symbol: str,
    session: AsyncSession = Depends(get_session),
) -> dict:
    """Live quote for one ticker.

    Returns ``{ok: true, symbol, price, currency, as_of}`` on success
    or ``{ok: false, error}`` when the symbol isn't recognised. Seeds
    ticker_universe + writes a Quote row as a side-effect on success
    so the dashboard's /api/universe call picks it up on the next
    refresh."""
    symbol = symbol.strip().upper()[:32]
    if not symbol:
        return {"ok": False, "error": "symbol required"}

    async with httpx.AsyncClient(follow_redirects=True, timeout=15) as client:
        quote = await fetch_yahoo(client, symbol, symbol, "")

    if quote.error or quote.price is None:
        log.info("ticker.validate.miss", symbol=symbol,
                 error=(quote.error or "no price")[:120])
        return {"ok": False, "error": "Symbol not recognised"}

    # Side-effect: seed the universe + write the quote so /api/universe
    # has data on the next minute-cycle refresh.
    await upsert_tickers(session, [symbol])
    session.add(QuoteModel(
        symbol=quote.symbol, source=quote.source, label=quote.label,
        group_name="universe", price=quote.price, currency=quote.currency,
        as_of=quote.as_of, changes=quote.changes or None, error=None,
        fetched_at=utcnow(),
    ))
    await session.commit()

    return {
        "ok": True,
        "symbol": quote.symbol,
        "price": quote.price,
        "currency": quote.currency,
        "as_of": quote.as_of,
    }


async def fetch_yahoo_historical(
    client: httpx.AsyncClient,
    symbol: str,
    target_iso: str,
) -> tuple[float | None, str | None, str | None]:
    """Fetch the close on ``target_iso`` or the nearest preceding trading
    day's close (within the available history window).

    Returns ``(close, currency, actual_iso)`` or ``(None, None, None)``
    when no usable data exists. Raises on provider-level HTTP errors
    (the caller wraps these into a friendly ``ok:false`` response).
    """
    range_param = _yahoo_range_covering(target_iso)
    r = await client.get(
        YAHOO_CHART.format(symbol=symbol),
        params={"interval": "1d", "range": range_param,
                "includePrePost": "false"},
        headers=UA,
        timeout=15,
    )
    r.raise_for_status()
    result = r.json().get("chart", {}).get("result")
    if not result:
        return None, None, None
    res = result[0]
    currency = (res.get("meta") or {}).get("currency")
    timestamps = res.get("timestamp") or []
    closes = (res.get("indicators", {}).get("quote") or [{}])[0].get("close") or []
    series = [(t, c) for t, c in zip(timestamps, closes) if c is not None]
    if not series:
        return None, None, None
    target_dt = datetime.strptime(target_iso, "%Y-%m-%d").replace(tzinfo=timezone.utc)
    # Add a 24h buffer so the target day itself is included (Yahoo
    # timestamps are at market open, not midnight).
    cutoff_ts = int(target_dt.timestamp()) + 86400
    selected: tuple[int, float] | None = None
    for t, c in series:
        if t <= cutoff_ts:
            selected = (t, c)
        else:
            break
    if selected is None:
        return None, None, None
    actual_iso = datetime.fromtimestamp(selected[0], timezone.utc).strftime("%Y-%m-%d")
    return selected[1], currency, actual_iso


@router.get(
    "/ticker/historical",
    dependencies=[Depends(require_paid)],
)
async def get_historical(symbol: str, date: str) -> dict:
    """Historical daily close. If ``date`` is a non-trading day we walk
    back to the last preceding trading day and surface ``actual_date``
    so the UI can show the user which date we actually used."""
    symbol = symbol.strip().upper()[:32]
    if not symbol:
        return {"ok": False, "error": "symbol required"}
    try:
        target = datetime.strptime(date, "%Y-%m-%d").date()
    except ValueError:
        raise HTTPException(status_code=400, detail="invalid date format (YYYY-MM-DD)")
    if target > datetime.now(timezone.utc).date():
        raise HTTPException(status_code=400, detail="date cannot be in the future")

    async with httpx.AsyncClient(follow_redirects=True, timeout=15) as client:
        try:
            close, currency, actual = await fetch_yahoo_historical(client, symbol, date)
        except Exception as e:
            log.warning("ticker.historical.failed", symbol=symbol,
                        date=date, error=str(e)[:200])
            return {"ok": False, "error": "Couldn't fetch historical price"}

    if close is None:
        return {"ok": False, "error": "No data for that date"}
    return {"ok": True, "close": close, "currency": currency, "actual_date": actual}
