from __future__ import annotations

import html as html_lib
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from datetime import date, datetime, timedelta, timezone
from typing import Any
from zoneinfo import ZoneInfo

import uvicorn
from apscheduler.schedulers.background import BackgroundScheduler  # type: ignore[import-untyped]
from fastapi import FastAPI, Header, HTTPException, Query
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
from sqlalchemy import func, select
from sqlalchemy.orm import Session

from investly.domain import Bar, Market, Strategy
from investly.evaluation.experiment_replay import ExperimentReplayRunner
from investly.evaluation.pit_fundamentals import PITFundamentalsStore
from investly.evaluation.replay import HistoricalReplayRunner
from investly.learning.loop import LearningLoop
from investly.providers.saudi_exchange_filings import fetch_and_parse_financial_results
from investly.runtime.accounts import AccountStore
from investly.runtime.benchmarks import BenchmarkStore
from investly.runtime.clock import next_session_times
from investly.runtime.experiments import ExperimentStore
from investly.runtime.store import AuditRow, ObservationRow, RecommendationRow, RuntimeStore
from investly.runtime.v1_orchestrator import V1MarketOrchestrator
from investly.settings import Settings
from investly.web import app_html
from investly.web.dashboard import accountability_payload, holdings_payload, recommendation_detail

settings = Settings.from_env()
store = RuntimeStore(settings.database_url)
accounts = AccountStore(settings.database_url)
experiments = ExperimentStore(settings.database_url)
benchmarks = BenchmarkStore(settings.database_url)
pit_fundamentals = PITFundamentalsStore(settings.database_url)
learning = LearningLoop(settings.database_url)
orchestrator = V1MarketOrchestrator(settings, store)
scheduler = BackgroundScheduler(timezone="UTC")

_ZONE = {
    Market.TASI: ZoneInfo("Asia/Riyadh"),
    Market.SP500: ZoneInfo("America/New_York"),
}
_BENCHMARK = {Market.TASI: "^TASI.SR", Market.SP500: "^GSPC"}


class OpenSimulationRequest(BaseModel):
    budget: float = Field(gt=0)
    market: Market = Market.TASI


class ExperimentRequest(BaseModel):
    budget: float = Field(gt=0)
    market: Market = Market.TASI
    name: str | None = Field(default=None, max_length=120)


class AccountView(BaseModel):
    market: str
    strategy: str
    opening_budget: float
    opened_at: date


class PITFilingIngestRequest(BaseModel):
    symbol: str = Field(min_length=1, max_length=32)
    url: str = Field(min_length=1)


def _require_admin(authorization: str | None) -> None:
    if settings.admin_token is None:
        return
    expected = f"Bearer {settings.admin_token}"
    if authorization != expected:
        raise HTTPException(status_code=401, detail="invalid admin token")


def _engine_replay_start(today: date) -> date:
    return today - timedelta(days=30)


def _canonical_account_exists(market: Market) -> bool:
    return accounts.get_account(market.value, Strategy.SWING.value) is not None


def _ensure_canonical_accounts(market: Market, budget: float, opened_at: date) -> None:
    for strategy in Strategy:
        existing = accounts.get_account(market.value, strategy.value)
        if existing is None:
            account = accounts.open_account(market.value, strategy.value, budget, opened_at)
            store.load_or_create_portfolio(market.value, strategy.value, account.opening_budget)


def _bar_for_session(market: Market, session: str, symbol: str) -> Bar | None:
    bars = orchestrator.provider_for(market).history(market, symbol, 5)
    matches: list[Bar] = []
    for bar in bars:
        ts = bar.ts if bar.ts.tzinfo else bar.ts.replace(tzinfo=timezone.utc)
        if ts.astimezone(_ZONE[market]).date().isoformat() == session:
            matches.append(bar)
    return matches[-1] if matches else None


def _simulation_complete(market: Market, session: str) -> bool:
    with Session(store.engine) as db:
        audit_id = db.scalar(
            select(AuditRow.id).where(
                AuditRow.event_type == "POSTCLOSE_SIMULATION_COMPLETE",
                AuditRow.entity_id == f"{market.value}:{session}",
            )
        )
    return audit_id is not None


def _fanout_experiments(market: Market, session: str) -> None:
    recommendations = store.recommendations_for_session(market.value, session)
    if not recommendations:
        return
    bars = {rec.symbol: _bar_for_session(market, session, rec.symbol) for rec in recommendations}
    if any(bar is None for bar in bars.values()):
        return
    benchmark_bar = _bar_for_session(market, session, _BENCHMARK[market])
    if benchmark_bar is None:
        return
    prices = {symbol: bar.close for symbol, bar in bars.items() if bar is not None}
    for experiment in experiments.list(market.value):
        if experiment.status != "ACTIVE":
            continue
        for rec in recommendations:
            bar = bars[rec.symbol]
            if bar is None:
                continue
            experiments.apply_recommendation(
                experiment.experiment_id,
                rec,
                session=session,
                session_open=bar.open,
                high=bar.high,
                low=bar.low,
            )
        for strategy in Strategy:
            experiments.save_snapshot(experiment.experiment_id, strategy, session, prices)
        benchmarks.save(
            experiment_id=experiment.experiment_id,
            session=session,
            opening_budget=experiment.opening_budget,
            session_open=benchmark_bar.open,
            close=benchmark_bar.close,
        )


def _tick_market(market: Market) -> None:
    if not _canonical_account_exists(market):
        return
    now = datetime.now(timezone.utc)
    clock = next_session_times(market, now)
    if clock.preopen_report_at <= now < clock.open_at:
        orchestrator.run_preopen(market, clock)
    if now >= clock.postclose_ready_at:
        orchestrator.run_postclose(market, clock)
        if _simulation_complete(market, clock.session):
            _fanout_experiments(market, clock.session)
            recent = learning.recent(market.value, 5)
            already = any(
                row.get("type") == "diagnosis" and row.get("session") == clock.session
                for row in recent
            )
            if not already:
                learning.run_daily(market.value, clock.session)


def _tick() -> None:
    _tick_market(Market.TASI)
    _tick_market(Market.SP500)


@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
    if settings.scheduler_enabled and not scheduler.running:
        scheduler.add_job(
            _tick,
            "interval",
            minutes=1,
            id="investly-market-tick",
            max_instances=1,
            coalesce=True,
            replace_existing=True,
        )
        scheduler.start()
    try:
        yield
    finally:
        if scheduler.running:
            scheduler.shutdown(wait=False)


app = FastAPI(title="Investly", version="1.0.0", lifespan=lifespan)


@app.get("/", response_class=HTMLResponse)
def index() -> HTMLResponse:
    return HTMLResponse(app_html())


@app.get("/healthz")
def health() -> dict[str, object]:
    return {"ok": store.health(), "service": "investly", "version": "1.0.0"}


@app.get("/readyz")
def ready() -> dict[str, object]:
    blockers: list[str] = []
    if not store.health():
        blockers.append("database")
    if not settings.sahmk_api_key:
        blockers.append("SAHMK_API_KEY required for full TASI universe discovery")
    return {"ready": not blockers, "blockers": blockers}


@app.post("/simulation/open", response_model=list[AccountView])
def open_simulation(
    request: OpenSimulationRequest,
    authorization: str | None = Header(default=None),
) -> list[AccountView]:
    _require_admin(authorization)
    opened_at = _engine_replay_start(datetime.now(timezone.utc).date())
    created: list[AccountView] = []
    for strategy in Strategy:
        account = accounts.open_account(
            request.market.value, strategy.value, request.budget, opened_at
        )
        store.load_or_create_portfolio(
            request.market.value, strategy.value, account.opening_budget
        )
        created.append(
            AccountView(
                market=account.market,
                strategy=account.strategy,
                opening_budget=account.opening_budget,
                opened_at=account.opened_at,
            )
        )
    return created


@app.post("/api/experiments")
def create_experiment(request: ExperimentRequest) -> dict[str, object]:
    today = datetime.now(timezone.utc).date()
    first_for_market = not experiments.list(request.market.value)
    opened_at = _engine_replay_start(today) if first_for_market else today
    experiment = experiments.create(
        market=request.market.value,
        budget=request.budget,
        opened_at=opened_at,
        name=request.name,
    )
    _ensure_canonical_accounts(request.market, request.budget, _engine_replay_start(today))
    return {
        "experiment_id": experiment.experiment_id,
        "name": experiment.name,
        "market": experiment.market,
        "opening_budget": experiment.opening_budget,
        "opened_at": experiment.opened_at,
        "status": experiment.status,
        "historical_bootstrap_required": first_for_market and request.market == Market.TASI,
    }


@app.get("/api/experiments")
def list_experiments(market: Market | None = None) -> list[dict[str, object]]:
    return [
        {
            "experiment_id": item.experiment_id,
            "name": item.name,
            "market": item.market,
            "opening_budget": item.opening_budget,
            "opened_at": item.opened_at,
            "status": item.status,
        }
        for item in experiments.list(market.value if market else None)
    ]


@app.get("/api/experiments/{experiment_id}/{strategy}/history")
def experiment_history(
    experiment_id: str, strategy: Strategy
) -> list[dict[str, object]]:
    return [snapshot.__dict__ for snapshot in experiments.history(experiment_id, strategy)]


def _latest_session(market: Market) -> str | None:
    with Session(store.engine) as db:
        return db.scalar(
            select(func.max(RecommendationRow.session)).where(
                RecommendationRow.market == market.value
            )
        )


def _maturity_pct(market: Market) -> float:
    with Session(store.engine) as db:
        rec_ids = list(
            db.scalars(
                select(RecommendationRow.recommendation_id).where(
                    RecommendationRow.market == market.value
                )
            )
        )
        if not rec_ids:
            return 0.0
        observations = list(
            db.scalars(
                select(ObservationRow).where(ObservationRow.recommendation_id.in_(rec_ids))
            )
        )
    closed = [o for o in observations if str(o.verdict).upper() in {"RIGHT", "WRONG"}]
    if not closed:
        return 0.0
    right = sum(str(o.verdict).upper() == "RIGHT" for o in closed)
    return right / len(closed) * 100


@app.get("/api/dashboard")
def dashboard(market: Market = Query(default=Market.TASI)) -> dict[str, Any]:
    experiment_rows = experiments.list(market.value)
    experiment_payload = [
        {
            "experiment_id": item.experiment_id,
            "name": html_lib.escape(item.name),
            "market": item.market,
            "opening_budget": item.opening_budget,
            "opened_at": item.opened_at.isoformat(),
            "status": item.status,
        }
        for item in experiment_rows
    ]
    primary = experiment_rows[0] if experiment_rows else None
    latest_metrics: dict[str, Any] | None = None
    series: list[dict[str, Any]] = []
    if primary is not None:
        for strategy in Strategy:
            history = experiments.history(primary.experiment_id, strategy)
            if history:
                if strategy == Strategy.INVESTMENT:
                    latest_metrics = history[-1].__dict__
                series.append(
                    {
                        "name": strategy.value,
                        "values": [
                            {"session": snapshot.session, "value": snapshot.total_value}
                            for snapshot in history
                        ],
                    }
                )
        benchmark_history = benchmarks.history(primary.experiment_id)
        if benchmark_history:
            series.append(
                {
                    "name": "benchmark",
                    "values": [
                        {"session": snapshot.session, "value": snapshot.total_value}
                        for snapshot in benchmark_history
                    ],
                }
            )
    session = _latest_session(market)
    recommendations = (
        store.recommendations_for_session(market.value, session) if session else []
    )
    rec_payload = [
        {
            "recommendation_id": rec.recommendation_id,
            "symbol": html_lib.escape(rec.symbol),
            "name": html_lib.escape(rec.name),
            "strategy": rec.strategy.value,
            "action": rec.action.value,
            "score": rec.score,
            "confidence": rec.confidence,
            "reference_price": rec.reference_price,
            "target": rec.target,
            "stop": rec.stop,
            "horizon": html_lib.escape(rec.horizon),
            "reasons_for": [html_lib.escape(value) for value in rec.reasons_for],
            "reasons_against": [html_lib.escape(value) for value in rec.reasons_against],
            "engine_version": html_lib.escape(rec.engine_version),
        }
        for rec in sorted(recommendations, key=lambda item: item.score, reverse=True)
    ]
    engine_version = recommendations[0].engine_version if recommendations else None
    return {
        "market": market.value,
        "latest_session": session,
        "primary": latest_metrics,
        "experiments": experiment_payload,
        "recommendations": rec_payload,
        "series": series,
        "maturity_pct": _maturity_pct(market),
        "learning": learning.recent(market.value, 10),
        "holdings": holdings_payload(store, experiments, primary),
        "accountability": accountability_payload(store, market.value, 50),
        "engine_version": engine_version,
        "evidence_status": "100% required per scored symbol",
    }


@app.get("/api/recommendations/{recommendation_id}")
def recommendation_evidence(recommendation_id: str) -> dict[str, Any]:
    detail = recommendation_detail(store, recommendation_id)
    if detail is None:
        raise HTTPException(status_code=404, detail="recommendation not found")
    return detail


@app.post("/internal/bootstrap/{market}")
def bootstrap_market(
    market: Market,
    authorization: str | None = Header(default=None),
) -> dict[str, object]:
    _require_admin(authorization)
    if market != Market.TASI:
        raise HTTPException(
            status_code=422,
            detail=(
                "PIT historical bootstrap is currently TASI-grade; "
                "S&P 500 activates forward as the control market"
            ),
        )
    account = accounts.get_account(market.value, Strategy.SWING.value)
    if account is None:
        raise HTTPException(status_code=409, detail="create a simulation experiment first")
    end = datetime.now(timezone.utc).date()
    start = account.opened_at
    symbols = orchestrator.symbols_for(market)
    runner = HistoricalReplayRunner(store, accounts, fundamentals_store=pit_fundamentals)
    canonical = runner.run(
        market=market,
        symbols=symbols,
        budget=account.opening_budget,
        start=start,
        end=end,
        top_x=settings.top_x,
    )
    experiment_replay = ExperimentReplayRunner(store, experiments, benchmarks).run(
        market, start, end
    )
    return {
        "canonical": canonical.__dict__,
        "experiments": experiment_replay.__dict__,
    }


@app.post("/internal/pit/filing")
def ingest_pit_filing(
    request: PITFilingIngestRequest,
    authorization: str | None = Header(default=None),
) -> dict[str, object]:
    _require_admin(authorization)
    try:
        parsed = fetch_and_parse_financial_results(request.url, symbol=request.symbol)
        inserted = pit_fundamentals.append(parsed.snapshot)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    return {
        "inserted": inserted,
        "symbol": parsed.snapshot.symbol,
        "period_end": parsed.snapshot.period_end,
        "published_at": parsed.snapshot.published_at.isoformat(),
        "source": parsed.snapshot.source,
    }


@app.get("/internal/learning/{market}")
def learning_status(
    market: Market,
    authorization: str | None = Header(default=None),
) -> list[dict[str, Any]]:
    _require_admin(authorization)
    return learning.recent(market.value, 50)


@app.post("/internal/learning/challengers/{challenger_id}/validate")
def validate_challenger(
    challenger_id: str,
    authorization: str | None = Header(default=None),
) -> dict[str, str]:
    _require_admin(authorization)
    status = learning.validate(challenger_id)
    return {"challenger_id": challenger_id, "status": status}


@app.get("/simulation/{market}/{strategy}/history")
def simulation_history(
    market: Market, strategy: Strategy
) -> list[dict[str, object]]:
    return [
        snapshot.__dict__
        for snapshot in accounts.snapshot_history(market.value, strategy.value)
    ]


def main() -> None:
    port = int(os.getenv("PORT", "8000"))
    uvicorn.run("investly.api:app", host="0.0.0.0", port=port, reload=False)


if __name__ == "__main__":
    main()
