import os
import json
import logging
from datetime import date as date_type
from typing import Optional
from fastapi import FastAPI, Query, Header, Depends, HTTPException, BackgroundTasks
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
import requests
from apscheduler.schedulers.background import BackgroundScheduler
from pydantic import BaseModel
from _shared.access_log import install as install_access_log

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s %(message)s")
logger = logging.getLogger("stock")

from .db import (
    init_db, save_articles, get_latest_articles,
    add_portfolio_item, get_all_portfolio, get_portfolio_item,
    update_portfolio_item, delete_portfolio_item,
    upsert_broker_cash, get_all_broker_cash, delete_broker_cash,
    upsert_asset_snapshot, get_asset_snapshots,
    add_sell_history, get_sell_history, update_sell_history, delete_sell_history,
    add_watchlist, remove_watchlist, get_watchlist, get_alert_history,
    get_alert_state_firing, set_alert_firing, touch_alert_seen, add_alert_history,
    get_alert_last_fired_map, get_ticker_name,
)
from .scraper import fetch_market_news, fetch_major_indices
from .price_fetcher import get_current_prices, get_current_prices_detail
from .ai_summarizer import summarize_news, OllamaError
from .auth import verify_webai_key
from . import webai_cache
from . import holdings_intel
from . import trade_alerts
from .trade_alerts import (
    build_monitor_set, current_session, diff_firing, DEFAULT_EXIT_PARAMS, DEFAULT_BUY_PARAMS,
)

app = FastAPI()
install_access_log(app)

# Screener 라우터 등록
from app.screener.router import router as screener_router
app.include_router(screener_router)

# CORS 설정 (프론트엔드 접근 허용)
_cors_origins = os.getenv("CORS_ALLOW_ORIGINS", "http://localhost:3007,http://localhost:8080").split(",")
app.add_middleware(
    CORSMiddleware,
    allow_origins=[o.strip() for o in _cors_origins],
    allow_credentials=False,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type"],
)

scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul"))

# Windows AI Server URL (NAS .env에서 설정)
WINDOWS_AI_SERVER_URL = os.getenv("WINDOWS_AI_SERVER_URL", "http://192.168.0.5:8000")

# Admin API Key 인증 — /api/trade/* 보호 (CODE_REVIEW F2)
# 빈 키 + 명시적 dev flag 없으면 503으로 거부. 운영 .env에 ADMIN_API_KEY 누락 시
# 무인증 통과되던 버그 차단.
ADMIN_API_KEY = os.getenv("ADMIN_API_KEY", "")

def verify_admin(x_admin_key: str = Header(None)):
    """admin/trade 엔드포인트 보호용 API 키 검증.

    - ADMIN_API_KEY 설정됨 + 키 일치 → 통과
    - ADMIN_API_KEY 설정됨 + 키 불일치 → 401 Unauthorized
    - ADMIN_API_KEY 미설정 + ALLOW_UNAUTHENTICATED_ADMIN=true → 통과 (개발 모드)
    - ADMIN_API_KEY 미설정 + dev flag 없음 → 503 (보호 강화, 운영 .env 누락 차단)
    """
    if not ADMIN_API_KEY:
        if os.getenv("ALLOW_UNAUTHENTICATED_ADMIN", "false").lower() == "true":
            return  # 개발 환경 명시적 허용
        raise HTTPException(
            status_code=503,
            detail=(
                "admin endpoint protected — ADMIN_API_KEY not configured. "
                "Set ADMIN_API_KEY in .env, or set ALLOW_UNAUTHENTICATED_ADMIN=true "
                "for development only."
            ),
        )
    if x_admin_key != ADMIN_API_KEY:
        raise HTTPException(status_code=401, detail="Unauthorized")

# Anthropic API 프록시용 키 (서버 측 보관)
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")

# 공휴일 목록 로드
_HOLIDAYS_PATH = os.path.join(os.path.dirname(__file__), "holidays.json")
try:
    with open(_HOLIDAYS_PATH, "r") as f:
        _HOLIDAYS: set = set(json.load(f))
except Exception:
    _HOLIDAYS = set()

def is_market_open(d: date_type) -> bool:
    return d.weekday() < 5 and d.strftime("%Y-%m-%d") not in _HOLIDAYS


def _calc_portfolio_totals(items, prices):
    """포트폴리오 총 매입/평가금 계산 (snapshot과 API에서 공용)"""
    total_buy = 0
    total_eval = 0
    for item in items:
        buy_amount = item["avg_price"] * item["quantity"]
        current_price = prices.get(item["ticker"], item["avg_price"])
        total_buy += buy_amount
        total_eval += current_price * item["quantity"]
    return total_buy, total_eval


def save_daily_snapshot():
    today = date_type.today()
    if not is_market_open(today):
        logger.info(f"Snapshot: {today} 휴장일 — 스킵")
        return

    today_str = today.strftime("%Y-%m-%d")
    items = get_all_portfolio()
    cash_rows = get_all_broker_cash()
    total_cash = sum(r["cash"] for r in cash_rows)

    if items:
        tickers = list({item["ticker"] for item in items})
        prices = get_current_prices(tickers)
        _, total_eval = _calc_portfolio_totals(items, prices)
    else:
        total_eval = 0

    total_assets = total_eval + total_cash
    upsert_asset_snapshot(today_str, total_eval, total_cash, total_assets)
    logger.info(f"Snapshot: {today_str} 저장 — eval={total_eval}, cash={total_cash}, total={total_assets}")

@app.on_event("startup")
def on_startup():
    init_db()
    
    # 매일 아침 8시 뉴스 스크랩 (NAS 자체 수행)
    scheduler.add_job(run_scraping_job, "cron", hour="8", minute="0")

    # 평일 15:40 총 자산 스냅샷 저장
    scheduler.add_job(save_daily_snapshot, "cron", day_of_week="mon-fri", hour=15, minute=40)
    
    # 앱 시작 시에도 한 번 실행 (데이터 없으면)
    if not get_latest_articles(1):
        run_scraping_job()
        
    scheduler.start()

def run_scraping_job():
    logger.info("뉴스 스크래핑 시작")

    articles_kr = fetch_market_news()
    count_kr = save_articles(articles_kr)

    logger.info(f"스크래핑 완료: 국내 {count_kr}건")

@app.get("/health")
def health():
    return {"ok": True}

@app.get("/api/stock/news")
def get_news(limit: int = 20, category: str = None):
    """최신 주식 뉴스 조회 (category: 'domestic' | 'overseas')"""
    return get_latest_articles(limit, category)

@app.get("/api/stock/indices")
def get_indices():
    """주요 지표(KOSPI 등) 실시간 크롤링 조회"""
    return fetch_major_indices()

@app.get("/api/stock/holidays")
def get_holidays():
    """task-watcher가 조회하는 휴장일 목록. holidays.json(list) 노출 (인증 불필요)."""
    try:
        with open(_HOLIDAYS_PATH, encoding="utf-8") as f:
            data = json.load(f)
        holidays = data if isinstance(data, list) else []
    except (OSError, ValueError):
        holidays = []
    return {"holidays": holidays}

@app.post("/api/stock/scrap")
def trigger_scrap():
    """수동 스크랩 트리거"""
    run_scraping_job()
    return {"ok": True}


class NewsSummarizeRequest(BaseModel):
    limit: Optional[int] = 10


@app.post("/api/stock/news/summarize")
async def summarize_latest_news(req: NewsSummarizeRequest = NewsSummarizeRequest()):
    """최근 뉴스를 Ollama(qwen3:14b)로 요약"""
    limit = req.limit if (req and req.limit) else 10
    articles = get_latest_articles(limit)
    if not articles:
        raise HTTPException(status_code=404, detail="요약할 뉴스가 없습니다.")

    try:
        result = await summarize_news(articles)
    except OllamaError as e:
        logger.error(f"뉴스 요약 실패: {e}")
        raise HTTPException(status_code=500, detail=f"Ollama 호출 실패: {e}")
    except Exception as e:
        logger.exception("뉴스 요약 중 예상치 못한 오류")
        raise HTTPException(status_code=500, detail=f"뉴스 요약 실패: {e}")

    # 상위 기사 메타 일부만 노출 (클라이언트가 본문 조립에 사용)
    top_articles = [
        {
            "title": (a.get("title") or "").strip(),
            "link": a.get("link") or "",
            "press": a.get("press") or "",
            "pub_date": a.get("pub_date") or "",
        }
        for a in articles[:8]
    ]
    return {
        **result,
        "article_count": len(articles),
        "articles": top_articles,
    }

# --- Trading API (Windows Proxy, 인증 필요) ---

@app.get("/api/trade/balance", dependencies=[Depends(verify_admin)])
def get_balance():
    """계좌 잔고 조회 (Windows AI Server Proxy)"""
    logger.info(f"Requesting Balance from {WINDOWS_AI_SERVER_URL}")
    resp = None
    try:
        resp = requests.get(f"{WINDOWS_AI_SERVER_URL}/trade/balance", timeout=5)
        if resp.status_code != 200:
            logger.error(f"Balance Error: {resp.status_code}")
            return JSONResponse(status_code=resp.status_code, content=resp.json())
        return resp.json()
    except ValueError:
        status = resp.status_code if resp is not None else 502
        return JSONResponse(status_code=status, content={"error": f"Upstream error {status}"})
    except Exception as e:
        logger.error(f"Balance Connection Failed: {e}")
        return JSONResponse(status_code=500, content={"error": "Connection Failed"})

class OrderRequest(BaseModel):
    ticker: str
    action: str
    quantity: int
    price: int = 0
    reason: Optional[str] = "Manual Order"

@app.post("/api/trade/order", dependencies=[Depends(verify_admin)])
def order_stock(req: OrderRequest):
    """주식 매수/매도 주문 (Windows AI Server Proxy)"""
    logger.info(f"Order Request: {req.action} {req.ticker} x{req.quantity}")
    resp = None
    try:
        resp = requests.post(f"{WINDOWS_AI_SERVER_URL}/trade/order", json=req.model_dump(), timeout=10)
        if resp.status_code != 200:
            logger.error(f"Order Error: {resp.status_code}")
            return JSONResponse(status_code=resp.status_code, content=resp.json())
        logger.info(f"Order Response: {req.action} {req.ticker} x{req.quantity} → OK")
        return resp.json()
    except ValueError:
        status = resp.status_code if resp is not None else 502
        return JSONResponse(status_code=status, content={"error": f"Upstream error {status}"})
    except Exception as e:
        logger.error(f"Order Connection Failed: {e}")
        return JSONResponse(status_code=500, content={"error": "Connection Failed"})

# --- AI Coach 프록시 (API 키를 서버에 보관) ---

class AiCoachRequest(BaseModel):
    model: str = "claude-haiku-4-5-20251001"
    prompt: str
    max_tokens: int = 1024

@app.post("/api/stock/ai-coach")
def ai_coach(req: AiCoachRequest):
    """AI 포트폴리오 코치 — Anthropic API 프록시 (API 키 서버 보관)"""
    if not ANTHROPIC_API_KEY:
        raise HTTPException(503, "AI Coach not configured (ANTHROPIC_API_KEY missing)")

    allowed_models = {"claude-haiku-4-5-20251001", "claude-sonnet-4-6"}
    model = req.model if req.model in allowed_models else "claude-haiku-4-5-20251001"
    logger.info(f"AI Coach 호출: model={model}, prompt_len={len(req.prompt)}")

    try:
        resp = requests.post(
            "https://api.anthropic.com/v1/messages",
            headers={
                "Content-Type": "application/json",
                "x-api-key": ANTHROPIC_API_KEY,
                "anthropic-version": "2023-06-01",
            },
            json={
                "model": model,
                "max_tokens": req.max_tokens,
                "messages": [{"role": "user", "content": req.prompt}],
            },
            timeout=30,
        )
        if resp.status_code != 200:
            logger.error(f"Anthropic API error: {resp.status_code}")
            return JSONResponse(status_code=resp.status_code, content={"error": "AI API error"})
        data = resp.json()
        usage = data.get("usage", {})
        logger.info(f"AI Coach 응답 완료: model={model}, input={usage.get('input_tokens', '?')}, output={usage.get('output_tokens', '?')}")
        return data
    except requests.Timeout:
        return JSONResponse(status_code=504, content={"error": "AI API timeout"})
    except Exception as e:
        logger.error(f"AI Coach error: {e}")
        return JSONResponse(status_code=500, content={"error": "AI Coach failed"})




@app.get("/api/version")
def version():
    return {"version": os.getenv("APP_VERSION", "dev")}


# --- Portfolio API ---

class PortfolioItemRequest(BaseModel):
    broker: str
    ticker: str
    name: str
    quantity: int
    avg_price: int                          # 평균단가 (현재가 평가/손익 계산용)
    purchase_price: Optional[int] = None    # 매입가 (총 매입 금액 계산용, 미입력 시 avg_price로 자동 설정)


class PortfolioUpdateRequest(BaseModel):
    broker: Optional[str] = None
    ticker: Optional[str] = None
    name: Optional[str] = None
    quantity: Optional[int] = None
    avg_price: Optional[int] = None
    purchase_price: Optional[int] = None


@app.get("/api/portfolio")
def get_portfolio():
    """전체 포트폴리오 조회 (현재가 + 손익 + 예수금 포함)"""
    items = get_all_portfolio()
    cash_rows = get_all_broker_cash()
    total_cash = sum(r["cash"] for r in cash_rows)

    if not items:
        return {
            "holdings": [],
            "cash": cash_rows,
            "summary": {
                "total_buy": 0,
                "total_eval": 0,
                "total_profit": 0,
                "total_profit_rate": 0.0,
                "total_cash": total_cash,
                "total_assets": total_cash,
            },
        }

    tickers = list({item["ticker"] for item in items})
    details = get_current_prices_detail(tickers)

    holdings = []
    total_buy = 0           # 요약 표시용 (purchase_price 기반)
    total_cost_basis = 0    # 손익률 계산용 (avg_price 기반)
    total_eval = 0

    for item in items:
        detail = details.get(item["ticker"])
        current_price = detail["price"] if detail else None
        price_session = detail["session"] if detail else None
        price_as_of = detail["as_of"] if detail else None
        # avg_price: 평균단가 — 손익(평가금액 - 매입원가) 계산 기준
        # purchase_price: 매입 단가(1주당) — 없으면 avg_price로 폴백 (CODE_REVIEW F4)
        purchase_price = item.get("purchase_price") if item.get("purchase_price") is not None else item["avg_price"]
        cost_basis = item["avg_price"] * item["quantity"]
        # 총 매입 금액 = 단가 × 보유 수량. API_SPEC.md 예시(qty 100·avg 72000 → 7,200,000)와 일치
        buy_amount = purchase_price * item["quantity"]
        eval_amount = current_price * item["quantity"] if current_price is not None else None
        profit_amount = (eval_amount - cost_basis) if eval_amount is not None else None
        profit_rate = round((profit_amount / cost_basis) * 100, 2) if (profit_amount is not None and cost_basis) else None

        holdings.append({
            "id": item["id"],
            "broker": item["broker"],
            "ticker": item["ticker"],
            "name": item["name"],
            "quantity": item["quantity"],
            "avg_price": item["avg_price"],
            "purchase_price": purchase_price,
            "current_price": current_price,
            "price_session": price_session,
            "price_as_of": price_as_of,
            "eval_amount": eval_amount,
            "profit_amount": profit_amount,
            "profit_rate": profit_rate,
        })

        total_buy += buy_amount
        total_cost_basis += cost_basis
        if eval_amount is not None:
            total_eval += eval_amount

    # 손익은 실제 평균단가(cost_basis) 기준으로 계산
    total_profit = total_eval - total_cost_basis
    total_profit_rate = round((total_profit / total_cost_basis) * 100, 2) if total_cost_basis else 0.0

    return {
        "holdings": holdings,
        "cash": cash_rows,
        "summary": {
            "total_buy": total_buy,
            "total_eval": total_eval,
            "total_profit": total_profit,
            "total_profit_rate": total_profit_rate,
            "total_cash": total_cash,
            "total_assets": total_eval + total_cash,
        },
    }


def _augment_portfolio_with_pnl_pct(raw: dict) -> dict:
    """Add pnl_pct (ratio) to each holding and total_pnl_pct to summary."""
    holdings = []
    for h in raw["holdings"]:
        pnl_pct = round(h["profit_rate"] / 100, 6) if h.get("profit_rate") is not None else None
        holdings.append({**h, "pnl_pct": pnl_pct})

    summary = dict(raw["summary"])
    rate = summary.get("total_profit_rate")
    summary["total_pnl_pct"] = round(rate / 100, 6) if rate is not None else 0.0

    return {"holdings": holdings, "cash": raw["cash"], "summary": summary}


@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)])
def get_webai_portfolio():
    """web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가).

    SP-A2 server-side TTLCache 적용. V1+V2 동시 호출도 NAS에서 1회 계산.
    """
    cached = webai_cache.cache_get_portfolio()
    if cached is not None:
        return cached
    result = _augment_portfolio_with_pnl_pct(get_portfolio())
    webai_cache.cache_set_portfolio(result)
    return result


def _fetch_news_sentiment_dump(date: str | None) -> dict:
    """news_sentiment 일별 dump (krx_master JOIN, score DESC)."""
    from .db import _conn
    conn = _conn()
    try:
        # 1) date resolve — None 이면 최신 date
        if date is None:
            row = conn.execute(
                "SELECT MAX(date) FROM news_sentiment"
            ).fetchone()
            date = row[0] if row and row[0] else None

        if date is None:
            return {"date": None, "count": 0, "items": []}

        # 2) JOIN krx_master.name (없으면 ticker 그대로)
        rows = conn.execute(
            """
            SELECT ns.ticker,
                   COALESCE(km.name, ns.ticker) AS name,
                   ns.score_raw,
                   ns.reason,
                   ns.news_count,
                   ns.source
            FROM news_sentiment ns
            LEFT JOIN krx_master km ON km.ticker = ns.ticker
            WHERE ns.date = ?
            ORDER BY ns.score_raw DESC
            """,
            (date,)
        ).fetchall()
    finally:
        conn.close()

    items = [
        {"ticker": r[0], "name": r[1], "score": r[2],
         "reason": r[3], "news_count": r[4], "source": r[5]}
        for r in rows
    ]
    return {"date": date, "count": len(items), "items": items}


@app.get("/api/webai/news-sentiment", dependencies=[Depends(verify_webai_key)])
def get_webai_news_sentiment(date: str | None = None):
    """web-ai 전용 news sentiment 일별 dump.

    SP-A2 server-side TTLCache 적용. date 파라미터별로 별도 슬롯.
    """
    cached = webai_cache.cache_get_news(date)
    if cached is not None:
        return cached
    result = _fetch_news_sentiment_dump(date)
    webai_cache.cache_set_news(date, result)
    return result


@app.get("/api/webai/trade-alert/monitor-set", dependencies=[Depends(verify_webai_key)])
def get_trade_alert_monitor_set():
    """web-ai(Windows 워커) 전용 — 실시간 매매 알람 감시대상 조립 (계약 §5.1).

    session은 KST 시각으로 pre/regular/after 판정 후, 평일·휴장 여부(is_market_open)를
    함께 게이팅해 최종 closed 여부를 결정한다.
    """
    from datetime import datetime, timezone, timedelta
    kst = timezone(timedelta(hours=9))
    now_kst = datetime.now(kst)
    session = current_session(now_kst)
    if not is_market_open(now_kst.date()):
        session = "closed"

    from .db import _conn
    conn = _conn()
    try:
        return build_monitor_set(conn, session, DEFAULT_EXIT_PARAMS, DEFAULT_BUY_PARAMS)
    finally:
        conn.close()


class TradeAlertReport(BaseModel):
    as_of: str | None = None
    firing: list[dict] = []


@app.post("/api/webai/trade-alert/report", dependencies=[Depends(verify_webai_key)])
def post_trade_alert_report(req: TradeAlertReport):
    """web-ai(Windows 워커) 전용 — 발화 보고 수신 (계약 §5.2).

    직전 발화상태 대비 edge diff(diff_firing) 후, 신규 alert는
    agent-office 전송 성공 시에만 상태(firing=True)+이력 반영한다.
    전송 실패 시 상태를 채택하지 않아 다음 사이클에 동일 alert가 다시
    "신규"로 잡혀 재시도된다(멱등). 해제(cleared)는 전송과 무관하게 firing=False.
    """
    from datetime import datetime, timedelta
    cooldown_h = float(os.getenv("TRADE_ALERT_COOLDOWN_HOURS", "6"))
    now = datetime.utcnow()

    prev = get_alert_state_firing()
    last_fired = get_alert_last_fired_map()
    d = diff_firing(req.firing, prev)

    new_count = 0
    suppressed = 0
    for a in d["new"]:
        key = (a["ticker"], a["kind"], a["condition"])
        # 쿨다운: 같은 종목·조건이 최근 발동됐으면(해제→재발화 오실레이션) 재알림 억제
        lf = last_fired.get(key)
        if cooldown_h > 0 and _within_cooldown(now, lf, timedelta(hours=cooldown_h)):
            set_alert_firing(*key, firing=True, mark_fired=False)   # firing 유지, 발동시각 미갱신
            suppressed += 1
            continue
        name = a.get("name") or get_ticker_name(a["ticker"])
        alert = {**a, "name": name}
        if trade_alerts.notify_agent_office([alert]):
            set_alert_firing(*key, firing=True)                    # 발동시각 갱신(UTC)
            add_alert_history(
                a["ticker"], name, a["kind"], a["condition"],
                a.get("price"), a.get("detail") or {},
            )
            new_count += 1

    for ticker, kind, condition in d["cleared"]:
        set_alert_firing(ticker, kind, condition, firing=False)

    touch_alert_seen(d["seen"], req.as_of or "")

    return {"new_alerts": new_count, "cleared": len(d["cleared"]), "suppressed": suppressed}


def _within_cooldown(now, last_iso, cooldown) -> bool:
    """last_iso(UTC ISO `%Y-%m-%dT%H:%M:%fZ`)가 now 기준 cooldown 이내면 True."""
    if not last_iso:
        return False
    from datetime import datetime
    try:
        lf = datetime.strptime(last_iso, "%Y-%m-%dT%H:%M:%fZ")
    except (ValueError, TypeError):
        return False
    return (now - lf) < cooldown


@app.post("/api/portfolio", status_code=201)
def create_portfolio_item(req: PortfolioItemRequest):
    """포트폴리오 종목 추가"""
    item_id = add_portfolio_item(
        req.broker, req.ticker, req.name, req.quantity, req.avg_price,
        purchase_price=req.purchase_price,
    )
    return {"id": item_id, "ok": True}


# --- Broker Cash API ---
# /{item_id} 라우트보다 반드시 먼저 정의해야 /cash가 item_id로 매칭되지 않음

class BrokerCashRequest(BaseModel):
    broker: str
    cash: int


@app.get("/api/portfolio/cash")
def list_broker_cash():
    """증권사별 예수금 전체 조회"""
    return get_all_broker_cash()


@app.put("/api/portfolio/cash")
def set_broker_cash(req: BrokerCashRequest):
    """증권사 예수금 등록 또는 수정 (upsert)"""
    upsert_broker_cash(req.broker, req.cash)
    return {"ok": True, "broker": req.broker, "cash": req.cash}


@app.delete("/api/portfolio/cash/{broker}")
def remove_broker_cash(broker: str):
    """증권사 예수금 삭제"""
    if not delete_broker_cash(broker):
        return JSONResponse(status_code=404, content={"error": "Broker not found"})
    return {"ok": True}


@app.put("/api/portfolio/{item_id}")
def update_portfolio(item_id: int, req: PortfolioUpdateRequest):
    """포트폴리오 종목 수정"""
    if get_portfolio_item(item_id) is None:
        return JSONResponse(status_code=404, content={"error": "Item not found"})
    update_portfolio_item(item_id, **req.model_dump())
    return {"ok": True}


@app.delete("/api/portfolio/{item_id}")
def delete_portfolio(item_id: int):
    """포트폴리오 종목 삭제"""
    if not delete_portfolio_item(item_id):
        return JSONResponse(status_code=404, content={"error": "Item not found"})
    return {"ok": True}


# --- Asset Snapshot API ---

@app.post("/api/portfolio/snapshot")
def create_snapshot():
    """총 자산 스냅샷 수동 저장 (오늘 날짜 기준)"""
    today = date_type.today()
    today_str = today.strftime("%Y-%m-%d")

    items = get_all_portfolio()
    cash_rows = get_all_broker_cash()
    total_cash = sum(r["cash"] for r in cash_rows)

    if items:
        tickers = list({item["ticker"] for item in items})
        prices = get_current_prices(tickers)
        total_eval = sum(
            prices.get(item["ticker"], item["avg_price"]) * item["quantity"]
            for item in items
        )
    else:
        total_eval = 0

    total_assets = total_eval + total_cash
    upsert_asset_snapshot(today_str, total_eval, total_cash, total_assets)

    return {
        "ok": True,
        "snapshot": {
            "date": today_str,
            "total_eval": total_eval,
            "total_cash": total_cash,
            "total_assets": total_assets,
        },
    }


@app.get("/api/portfolio/snapshot/history")
def get_snapshot_history(days: int = Query(30, ge=0)):
    """총 자산 스냅샷 이력 조회 (days=0: 전체, days=N: 최근 N일)"""
    snapshots = get_asset_snapshots(days)
    return {"snapshots": snapshots}


# --- Sell History API ---

class SellHistoryRequest(BaseModel):
    broker: str
    ticker: str
    name: str
    quantity: int
    avg_price: float
    sell_price: float
    commission: float = 0
    buy_amount: float
    sell_amount: float
    realized_profit: float
    realized_rate: float
    sold_at: str


@app.get("/api/portfolio/sell-history")
def list_sell_history(broker: Optional[str] = None, days: Optional[int] = None):
    """매도 내역 조회 (broker, days 필터 선택)"""
    records = get_sell_history(broker=broker, days=days)
    return {"records": records}


@app.post("/api/portfolio/sell-history")
def create_sell_history(req: SellHistoryRequest):
    """매도 기록 저장"""
    record = add_sell_history(req.model_dump())
    return record


@app.put("/api/portfolio/sell-history/{record_id}")
def modify_sell_history(record_id: int, req: SellHistoryRequest):
    """매도 기록 수정"""
    record = update_sell_history(record_id, req.model_dump())
    if record is None:
        return JSONResponse(status_code=404, content={"error": "not found"})
    return record


@app.delete("/api/portfolio/sell-history/{record_id}")
def remove_sell_history(record_id: int):
    """매도 기록 삭제"""
    if not delete_sell_history(record_id):
        return JSONResponse(status_code=404, content={"error": "not found"})
    return {"ok": True}


# --- Watchlist & Trade Alerts API (실시간 매매 알람) ---

class WatchlistItemRequest(BaseModel):
    ticker: str
    name: str | None = None
    note: str | None = None


@app.get("/api/stock/watchlist")
def list_watchlist():
    """관심종목 목록 조회"""
    return {"watchlist": get_watchlist()}


@app.post("/api/stock/watchlist", status_code=201)
def create_watchlist_item(req: WatchlistItemRequest):
    """관심종목 추가 (이미 존재하면 name/note 갱신, 멱등)"""
    add_watchlist(req.ticker, req.name, req.note)
    return {"ok": True}


@app.delete("/api/stock/watchlist/{ticker}")
def delete_watchlist_item(ticker: str):
    """관심종목 삭제"""
    if not remove_watchlist(ticker):
        raise HTTPException(status_code=404, detail="not in watchlist")
    return {"ok": True}


@app.get("/api/stock/trade-alerts")
def list_trade_alerts(days: int = 7):
    """매매 알람 이력 조회 (최근 N일). 조건별 근거(reason) 문자열 포함 — FE가 detail 객체 대신 렌더."""
    alerts = get_alert_history(days)
    for a in alerts:
        a["reason"] = trade_alerts.condition_reason(a.get("condition", ""))
    return {"alerts": alerts}


# --- Holdings Intelligence API ---

@app.get("/api/stock/holdings/intel")
def holdings_intel_brief():
    """보유종목 인텔리전스 브리핑 (최신 시그널 + 포트 건강)"""
    return holdings_intel.build_holdings_brief()


@app.get("/api/stock/holdings/intel/history")
def holdings_intel_history(ticker: str, days: int = 30):
    """종목별 시그널 이력 조회"""
    from . import db
    return {"ticker": ticker, "history": db.get_holdings_signal_history(ticker, days)}


@app.post("/api/stock/holdings/intel/run")
def holdings_intel_run(background_tasks: BackgroundTasks, use_llm: bool = True):
    """보유종목 시그널 계산 트리거 (BackgroundTask)"""
    background_tasks.add_task(holdings_intel.compute_and_store, None, use_llm)
    return {"ok": True, "queued": True}


