from datetime import datetime, timezone
from fastapi import FastAPI, Header, HTTPException, Request
import os, hmac
from pydantic import BaseModel
from .engine import Candle, HORIZONS, ENGINE_VERSION, generate, resample
from .security import verify
from .database import CandleRow,FeatureRow,SignalRow,Session,init_db,EngineVersion
from .database import BacktestRun
from .backtest import walk_forward
from .backfill import fetch as fetch_backfill
from sqlalchemy.dialects.mysql import insert
from sqlalchemy import select

app = FastAPI(title="AST Trading Decision Engine", version=ENGINE_VERSION)
SYMBOLS: dict[int,dict] = {}
CANDLES: dict[int,list[Candle]] = {}
SIGNALS: dict[str,dict] = {}

@app.on_event("startup")
def startup():
    init_db()
    with Session.begin() as db:
        db.execute(insert(EngineVersion).values(version=ENGINE_VERSION,rules={"gate":{"score":70,"confidence":65,"risk_reward":1.5},"horizons":HORIZONS},active=True).prefix_with("IGNORE"))
        for row in db.scalars(select(CandleRow).where(CandleRow.timeframe=="1m").order_by(CandleRow.symbol_id,CandleRow.timestamp)):
            CANDLES.setdefault(row.symbol_id,[]).append(Candle(row.timestamp,row.open,row.high,row.low,row.close,row.volume))
        for row in db.scalars(select(SignalRow).order_by(SignalRow.generated_at.desc())):
            key=f"{row.symbol_id}:{row.horizon}"
            if key not in SIGNALS: SIGNALS[key]=row.payload

class SymbolPayload(BaseModel):
    id:int; symbol:str; feed:str|None=None; category:str; eligible:bool=True
class TickPayload(BaseModel):
    symbol_id:int; timestamp:int; price:float; volume:float|None=None

async def authenticated(request:Request, timestamp:str|None, signature:str|None):
    body=await request.body()
    if not verify(timestamp or "",body,signature or ""): raise HTTPException(401,"Invalid service signature")

@app.get("/v1/health")
def health(x_ast_token:str|None=Header(None)):
    if not hmac.compare_digest(x_ast_token or "",os.getenv("TRADING_AI_SHARED_SECRET","")): raise HTTPException(401,"Invalid service token")
    eligible=[sid for sid,meta in SYMBOLS.items() if meta.get("eligible")]
    ready=not eligible or all(len(CANDLES.get(sid,[]))>=210 for sid in eligible)
    return {"status":"ready" if ready else "collecting","engine_version":ENGINE_VERSION,"symbols":len(SYMBOLS),"eligible_symbols":len(eligible),"signals":len(SIGNALS),"time":datetime.now(timezone.utc).isoformat()}

@app.post("/v1/symbols")
async def symbols(items:list[SymbolPayload],request:Request,x_ast_timestamp:str|None=Header(None),x_ast_signature:str|None=Header(None)):
    await authenticated(request,x_ast_timestamp,x_ast_signature)
    for item in items: SYMBOLS[item.id]=item.model_dump()
    return {"accepted":len(items)}

@app.post("/v1/ticks")
async def tick(item:TickPayload,request:Request,x_ast_timestamp:str|None=Header(None),x_ast_signature:str|None=Header(None)):
    await authenticated(request,x_ast_timestamp,x_ast_signature)
    minute=item.timestamp//60*60; rows=CANDLES.setdefault(item.symbol_id,[])
    if rows and rows[-1].timestamp==minute:
        old=rows[-1]; rows[-1]=Candle(minute,old.open,max(old.high,item.price),min(old.low,item.price),item.price,(old.volume or 0)+(item.volume or 0) or None)
    else: rows.append(Candle(minute,item.price,item.price,item.price,item.price,item.volume))
    CANDLES[item.symbol_id]=rows[-50000:]
    with Session.begin() as db:
        stmt=insert(CandleRow).values(symbol_id=item.symbol_id,timeframe="1m",timestamp=minute,open=rows[-1].open,high=rows[-1].high,low=rows[-1].low,close=rows[-1].close,volume=rows[-1].volume,provider="twelvedata")
        stmt=stmt.on_duplicate_key_update(high=stmt.inserted.high,low=stmt.inserted.low,close=stmt.inserted.close,volume=stmt.inserted.volume)
        db.execute(stmt)
    return {"accepted":True}

@app.post("/v1/generate")
async def run(request:Request,x_ast_timestamp:str|None=Header(None),x_ast_signature:str|None=Header(None)):
    await authenticated(request,x_ast_timestamp,x_ast_signature); created=[]; now=int(datetime.now(timezone.utc).timestamp())
    for sid,meta in SYMBOLS.items():
        if not meta.get("eligible"): continue
        for horizon,seconds in HORIZONS.items():
            rows=resample(CANDLES.get(sid,[]),seconds)
            rows=[row for row in rows if row.timestamp+seconds<=now]
            if len(rows)<210: continue
            signal=generate(sid,meta["symbol"],horizon,rows).payload(); key=f"{sid}:{horizon}"; SIGNALS[key]=signal; created.append(signal)
            external=f"{sid}:{horizon}:{signal['candle_timestamp']}:{ENGINE_VERSION}"
            with Session.begin() as db:
                db.add(FeatureRow(symbol_id=sid,horizon=horizon,engine_version=ENGINE_VERSION,generated_at=datetime.fromisoformat(signal['generated_at']),features=signal['feature_snapshot']))
                db.execute(insert(SignalRow).values(external_key=external,symbol_id=sid,horizon=horizon,direction=signal['direction'],score=signal['score'],payload=signal,generated_at=datetime.fromisoformat(signal['generated_at']),expires_at=datetime.fromisoformat(signal['expires_at'])).prefix_with("IGNORE"))
    return {"engine_version":ENGINE_VERSION,"signals":created}

@app.get("/v1/opportunities")
def opportunities(x_ast_token:str|None=Header(None)):
    if not hmac.compare_digest(x_ast_token or "",os.getenv("TRADING_AI_SHARED_SECRET","")): raise HTTPException(401,"Invalid service token")
    return {"engine_version":ENGINE_VERSION,"signals":sorted(SIGNALS.values(),key=lambda s:s["score"],reverse=True)}

@app.get("/v1/backtests")
def backtests(x_ast_token:str|None=Header(None)):
    if not hmac.compare_digest(x_ast_token or "",os.getenv("TRADING_AI_SHARED_SECRET","")): raise HTTPException(401,"Invalid service token")
    with Session() as db:
        rows=db.scalars(select(BacktestRun).order_by(BacktestRun.created_at.desc()).limit(50)).all()
        return {"runs":[{"id":x.id,"engine_version":x.engine_version,"status":x.status,"parameters":x.parameters,"metrics":x.metrics,"created_at":x.created_at.isoformat()} for x in rows]}

class BacktestPayload(BaseModel): symbol_id:int; horizon:str; spread_bps:float=2; slippage_bps:float=2; fee_bps:float=1; funding_bps:float=0
class BackfillPayload(BaseModel): symbol_id:int; outputsize:int=5000

@app.post("/v1/backfill")
async def backfill(item:BackfillPayload,request:Request,x_ast_timestamp:str|None=Header(None),x_ast_signature:str|None=Header(None)):
    await authenticated(request,x_ast_timestamp,x_ast_signature)
    meta=SYMBOLS.get(item.symbol_id)
    if not meta or not meta.get("eligible"): raise HTTPException(422,"Symbol is not eligible")
    feed=meta.get("feed")
    if not feed: raise HTTPException(422,"Symbol has no Twelve Data mapping")
    imported=await fetch_backfill(item.symbol_id,feed,max(1,min(item.outputsize,5000)))
    with Session() as db:
        rows=db.scalars(select(CandleRow).where(CandleRow.symbol_id==item.symbol_id,CandleRow.timeframe=="1m").order_by(CandleRow.timestamp)).all()
        CANDLES[item.symbol_id]=[Candle(x.timestamp,x.open,x.high,x.low,x.close,x.volume) for x in rows][-50000:]
    return {"symbol_id":item.symbol_id,"feed":feed,"imported":imported}

@app.post("/v1/backtests")
async def backtest(item:BacktestPayload,request:Request,x_ast_timestamp:str|None=Header(None),x_ast_signature:str|None=Header(None)):
    await authenticated(request,x_ast_timestamp,x_ast_signature)
    if item.horizon not in HORIZONS: raise HTTPException(422,"Unsupported horizon")
    rows=resample(CANDLES.get(item.symbol_id,[]),HORIZONS[item.horizon]); meta=SYMBOLS.get(item.symbol_id)
    if not meta or len(rows)<230: raise HTTPException(422,"Insufficient candle history")
    params=item.model_dump(); metrics=walk_forward(item.symbol_id,meta["symbol"],item.horizon,rows,item.spread_bps,item.slippage_bps,item.fee_bps,item.funding_bps)
    with Session.begin() as db: db.add(BacktestRun(engine_version=ENGINE_VERSION,status="complete",parameters=params,metrics=metrics))
    return {"status":"complete","engine_version":ENGINE_VERSION,"metrics":metrics}
