"""
운영 포트폴리오 스냅샷·최근 24시간 수익률 텔레그램 리포트.
"""

from __future__ import annotations

import json
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo

from config import (
    COIN_NAME,
    COIN_TELEGRAM_BOT_TOKEN,
    COIN_TELEGRAM_CHAT_ID,
    DAILY_PNL_REPORT_ENABLED,
    DAILY_PNL_REPORT_HOUR,
    DAILY_PNL_REPORT_MINUTE,
    DAILY_PNL_REPORT_TZ,
    DAILY_PNL_SNAPSHOT_ON_LIVE,
    DAILY_PNL_SNAPSHOT_RETENTION_DAYS,
    ENTRY_INTERVAL,
    GT_INITIAL_CASH_KRW,
    SYMBOL,
)
from deepcoin.ops.monitor import Monitor
from deepcoin.paths import LIVE_TRADES_LOG, PORTFOLIO_SNAPSHOTS_LOG, ensure_dirs

logger = logging.getLogger(__name__)

ROLLING_HOURS = 24
MAX_ANCHOR_SKEW_HOURS = 6


def _report_tz() -> ZoneInfo:
    """
    리포트 기준 타임존.

    Returns:
        ZoneInfo (잘못된 이름이면 UTC).
    """
    try:
        return ZoneInfo(DAILY_PNL_REPORT_TZ)
    except Exception:
        logger.warning("DAILY_PNL_REPORT_TZ=%s invalid, using UTC", DAILY_PNL_REPORT_TZ)
        return ZoneInfo("UTC")


def _parse_ts(value: str) -> datetime:
    """ISO 시각 → timezone-aware datetime."""
    dt = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt


def fetch_mark_price(monitor: Monitor, symbol: str = SYMBOL) -> float:
    """
    최근 종가(3분봉)를 조회합니다.

    Args:
        monitor: Monitor 인스턴스.
        symbol: 통화 코드.

    Returns:
        종가. 실패 시 0.
    """
    try:
        data = monitor.get_coin_data(symbol, interval=ENTRY_INTERVAL)
        if data is None or data.empty:
            return 0.0
        return float(data["Close"].iloc[-1])
    except Exception as exc:
        logger.warning("mark price fetch failed: %s", exc)
        return 0.0


def portfolio_from_balances(
    balances: dict[str, dict[str, float]],
    mark_price: float,
    *,
    ts: datetime | None = None,
) -> dict[str, Any]:
    """
    잔고·시세로 총자산 스냅샷 dict를 만듭니다.

    Args:
        balances: load_balances_dict() 결과.
        mark_price: 코인 평가 단가.
        ts: 기록 시각 (None이면 now UTC).

    Returns:
        스냅샷 필드 dict.
    """
    when = ts or datetime.now(timezone.utc)
    krw = float((balances.get("KRW") or {}).get("balance") or 0.0)
    qty = float((balances.get(SYMBOL) or {}).get("balance") or 0.0)
    holding_value = qty * mark_price if mark_price > 0 else 0.0
    total = krw + holding_value
    return {
        "ts": when.isoformat(timespec="seconds"),
        "symbol": SYMBOL,
        "krw": round(krw, 0),
        "coin_qty": qty,
        "mark_price": mark_price,
        "holding_value_krw": round(holding_value, 0),
        "total_asset_krw": round(total, 0),
    }


def fetch_portfolio_snapshot(monitor: Monitor) -> dict[str, Any]:
    """
    API 잔고·시세로 현재 포트폴리오 스냅샷을 조회합니다.

    Args:
        monitor: Monitor 인스턴스.

    Returns:
        portfolio_from_balances 결과.

    Raises:
        RuntimeError: 잔고 API 오류.
    """
    balances = monitor.load_balances_dict()
    mark = fetch_mark_price(monitor)
    if mark <= 0:
        raise RuntimeError("mark price unavailable")
    return portfolio_from_balances(balances, mark)


def append_portfolio_snapshot(snapshot: dict[str, Any]) -> None:
    """portfolio_snapshots.jsonl에 1행 추가 후 오래된 행 정리."""
    ensure_dirs()
    PORTFOLIO_SNAPSHOTS_LOG.parent.mkdir(parents=True, exist_ok=True)
    with PORTFOLIO_SNAPSHOTS_LOG.open("a", encoding="utf-8") as f:
        f.write(json.dumps(snapshot, ensure_ascii=False) + "\n")
    _prune_snapshots(DAILY_PNL_SNAPSHOT_RETENTION_DAYS)


def _prune_snapshots(retention_days: int) -> None:
    """보관 일수 초과 스냅샷 삭제."""
    if not PORTFOLIO_SNAPSHOTS_LOG.is_file():
        return
    cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)
    kept: list[str] = []
    try:
        for line in PORTFOLIO_SNAPSHOTS_LOG.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if not line:
                continue
            row = json.loads(line)
            if _parse_ts(row["ts"]) >= cutoff:
                kept.append(line)
    except (json.JSONDecodeError, OSError, KeyError) as exc:
        logger.warning("snapshot prune skipped: %s", exc)
        return
    PORTFOLIO_SNAPSHOTS_LOG.write_text(
        "\n".join(kept) + ("\n" if kept else ""),
        encoding="utf-8",
    )


def load_snapshots() -> list[dict[str, Any]]:
    """스냅샷 jsonl 전체 로드 (시각 오름차순)."""
    if not PORTFOLIO_SNAPSHOTS_LOG.is_file():
        return []
    rows: list[dict[str, Any]] = []
    for line in PORTFOLIO_SNAPSHOTS_LOG.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            rows.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    rows.sort(key=lambda r: _parse_ts(r["ts"]))
    return rows


def _nearest_snapshot(
    snapshots: list[dict[str, Any]],
    target: datetime,
) -> tuple[dict[str, Any] | None, float]:
    """
    target에 가장 가까운 스냅샷.

    Returns:
        (스냅샷, |시차| 시간).
    """
    if not snapshots:
        return None, 0.0
    best = snapshots[0]
    best_hours = abs((_parse_ts(best["ts"]) - target).total_seconds()) / 3600.0
    for row in snapshots[1:]:
        hours = abs((_parse_ts(row["ts"]) - target).total_seconds()) / 3600.0
        if hours < best_hours:
            best = row
            best_hours = hours
    return best, best_hours


def count_trades_since(path: Path, since: datetime) -> dict[str, int]:
    """
    live_trades.jsonl에서 구간 내 체결 건수.

    Returns:
        {"buy": n, "sell": n, "ok": n}.
    """
    out = {"buy": 0, "sell": 0, "ok": 0}
    if not path.is_file():
        return out
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            row = json.loads(line)
        except json.JSONDecodeError:
            continue
        if not row.get("ok"):
            continue
        try:
            ts = _parse_ts(str(row.get("ts", "")))
        except (TypeError, ValueError):
            continue
        if ts < since:
            continue
        side = str(row.get("side", "")).lower()
        if side in out:
            out[side] += 1
        out["ok"] += 1
    return out


def compute_24h_return(
    current: dict[str, Any],
    snapshots: list[dict[str, Any]] | None = None,
    *,
    baseline_krw: float | None = None,
) -> dict[str, Any]:
    """
    최근 24시간 총자산 수익률을 계산합니다.

    Args:
        current: 현재 스냅샷.
        snapshots: 과거 스냅샷 목록 (None이면 파일 로드).
        baseline_krw: 앵커 없을 때 대체 기준(초기 자금).

    Returns:
        리포트 dict (pnl_pct, anchor, trade_counts 등).
    """
    snapshots = snapshots if snapshots is not None else load_snapshots()
    now = _parse_ts(current["ts"])
    target = now - timedelta(hours=ROLLING_HOURS)
    anchor, skew_h = _nearest_snapshot(snapshots, target)

    base_asset = float(baseline_krw or GT_INITIAL_CASH_KRW)
    anchor_ts = None
    anchor_note = f"기준=초기자금 ₩{base_asset:,.0f} (24h 스냅샷 없음)"

    if anchor is not None:
        base_asset = float(anchor["total_asset_krw"])
        anchor_ts = anchor["ts"]
        if skew_h <= MAX_ANCHOR_SKEW_HOURS:
            anchor_note = f"기준=24h 전 스냅샷 ({anchor_ts}, 시차 {skew_h:.1f}h)"
        else:
            anchor_note = (
                f"기준=가장 가까운 스냅샷 ({anchor_ts}, 24h 대비 시차 {skew_h:.1f}h)"
            )

    final_asset = float(current["total_asset_krw"])
    pnl_krw = final_asset - base_asset
    pnl_pct = (pnl_krw / base_asset * 100.0) if base_asset > 0 else 0.0
    cum_pct = (
        (final_asset - GT_INITIAL_CASH_KRW) / GT_INITIAL_CASH_KRW * 100.0
        if GT_INITIAL_CASH_KRW > 0
        else 0.0
    )

    trades = count_trades_since(LIVE_TRADES_LOG, target)

    return {
        "current": current,
        "anchor": anchor,
        "anchor_ts": anchor_ts,
        "anchor_note": anchor_note,
        "anchor_skew_hours": skew_h if anchor else None,
        "base_asset_krw": base_asset,
        "final_asset_krw": final_asset,
        "pnl_krw": pnl_krw,
        "pnl_pct": pnl_pct,
        "cumulative_pnl_pct": cum_pct,
        "trade_counts": trades,
        "window_hours": ROLLING_HOURS,
    }


def build_daily_pnl_message(report: dict[str, Any]) -> str:
    """
    텔레그램 본문 생성.

    Args:
        report: compute_24h_return 결과.

    Returns:
        메시지 문자열.
    """
    cur = report["current"]
    sign = "+" if report["pnl_pct"] >= 0 else ""
    cum_sign = "+" if report["cumulative_pnl_pct"] >= 0 else ""
    tc = report["trade_counts"]
    tz = _report_tz()
    now_local = _parse_ts(cur["ts"]).astimezone(tz).strftime("%Y-%m-%d %H:%M")

    lines = [
        f"[DeepCoin 일일 리포트] {COIN_NAME} ({SYMBOL})",
        f"시각: {now_local} ({DAILY_PNL_REPORT_TZ})",
        "",
        f"최근 {report['window_hours']}시간 수익률: {sign}{report['pnl_pct']:.2f}%",
        f"  손익: {sign}₩{report['pnl_krw']:,.0f}",
        f"  {report['anchor_note']}",
        f"  24h 전 총자산: ₩{report['base_asset_krw']:,.0f}",
        f"  현재 총자산: ₩{report['final_asset_krw']:,.0f}",
        "",
        f"누적(초기 ₩{GT_INITIAL_CASH_KRW:,} 대비): {cum_sign}{report['cumulative_pnl_pct']:.2f}%",
        "",
        "현재 잔고",
        f"  현금: ₩{float(cur['krw']):,.0f}",
        f"  {SYMBOL}: {float(cur['coin_qty']):.4f} (₩{float(cur['holding_value_krw']):,.0f} @ ₩{float(cur['mark_price']):,.0f})",
        "",
        f"24h 체결(성공): 매수 {tc['buy']} · 매도 {tc['sell']} · 합계 {tc['ok']}",
        "※ 총자산=현금+보유×종가. 미실현 포함.",
    ]
    return "\n".join(lines)


def maybe_record_portfolio_snapshot(monitor: Monitor) -> None:
    """
    06 루프용: 설정 시 스냅샷만 기록 (텔레그램 없음).

    Args:
        monitor: Monitor/LiveTrader 인스턴스.
    """
    if not DAILY_PNL_SNAPSHOT_ON_LIVE:
        return
    try:
        snap = fetch_portfolio_snapshot(monitor)
        append_portfolio_snapshot(snap)
    except Exception as exc:
        logger.warning("portfolio snapshot skip: %s", exc)


def send_daily_pnl_report(
    monitor: Monitor | None = None,
    *,
    send_telegram: bool = True,
) -> dict[str, Any]:
    """
    스냅샷 저장 → 24h 수익률 계산 → 텔레그램 발송.

    Args:
        monitor: None이면 Monitor() 생성.
        send_telegram: False면 메시지만 반환·콘솔 출력.

    Returns:
        compute_24h_return 결과 + message 키.
    """
    if not DAILY_PNL_REPORT_ENABLED:
        raise RuntimeError("DAILY_PNL_REPORT_ENABLED=0")

    mon = monitor or Monitor(cooldown_file=None)
    current = fetch_portfolio_snapshot(mon)
    append_portfolio_snapshot(current)
    report = compute_24h_return(current)
    msg = build_daily_pnl_message(report)
    report["message"] = msg

    print(msg)
    if send_telegram:
        if not COIN_TELEGRAM_BOT_TOKEN or not COIN_TELEGRAM_CHAT_ID:
            print("[telegram skip] COIN_TELEGRAM_BOT_TOKEN/CHAT_ID 미설정")
        else:
            mon._send_coin_msg(msg)
    return report


def seconds_until_next_report(
    hour: int | None = None,
    minute: int | None = None,
) -> float:
    """
    다음 리포트 시각(로컬 TZ)까지 대기 초.

    Args:
        hour: 시 (기본 DAILY_PNL_REPORT_HOUR).
        minute: 분 (기본 DAILY_PNL_REPORT_MINUTE).

    Returns:
        초 (>= 1).
    """
    tz = _report_tz()
    h = DAILY_PNL_REPORT_HOUR if hour is None else hour
    m = DAILY_PNL_REPORT_MINUTE if minute is None else minute
    now = datetime.now(tz)
    target = now.replace(hour=h, minute=m, second=0, microsecond=0)
    if target <= now:
        target += timedelta(days=1)
    return max((target - now).total_seconds(), 1.0)


def run_schedule_loop(monitor: Monitor | None = None) -> None:
    """
    매일 지정 시각에 리포트를 발송하는 무한 루프.

    Args:
        monitor: 재사용할 Monitor (None이면 매 회 생성).
    """
    tz = _report_tz()
    print(
        f"[07] 일일 수익률 텔레그램 · {DAILY_PNL_REPORT_HOUR:02d}:"
        f"{DAILY_PNL_REPORT_MINUTE:02d} {tz.key} · Ctrl+C 종료"
    )
    while True:
        wait = seconds_until_next_report()
        next_at = datetime.now(tz) + timedelta(seconds=wait)
        print(f"[07] 다음 발송: {next_at:%Y-%m-%d %H:%M:%S} (대기 {wait / 3600:.2f}h)")
        import time

        time.sleep(wait)
        try:
            send_daily_pnl_report(monitor)
        except Exception as exc:
            print(f"[07] 리포트 오류: {exc}")
