#!/usr/bin/env python3
"""종목별 매매 기록 — ka10170 당일매매일지를 일자별로 적재·조회.

키움 REST에는 기간 거래내역 API가 없어 매일 ka10170을 호출해 누적한다.
- 저장: state/trade_journal.jsonl (한 줄 = (date, account, code) 레코드)
- 재실행 시 같은 (date, account) 행을 제거 후 재적재 → idempotent
- 휴장일/주말은 기본 skip (--force로 강제)

CLI:
  collect [--date YYYYMMDD] [--force] [--quiet]
  show <code|name>
  query [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--account L] [--code C]
"""
from __future__ import annotations

import argparse
import json
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import kiwoom_client as kw

KST = timezone(timedelta(hours=9))
WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace')
JOURNAL = WORKSPACE / 'state' / 'trade_journal.jsonl'
HOLIDAYS = WORKSPACE / 'state' / 'market_holidays.json'


def is_market_open(d: datetime) -> bool:
    if d.weekday() >= 5:
        return False
    iso = d.strftime('%Y-%m-%d')
    try:
        data = json.loads(HOLIDAYS.read_text())
        return iso not in data.get('holidays', {})
    except FileNotFoundError:
        return True


def _owner(label: str) -> str:
    return '가희' if label.startswith('가희_') else '본인'


def _load_all() -> list[dict]:
    if not JOURNAL.exists():
        return []
    out: list[dict] = []
    for ln in JOURNAL.read_text().splitlines():
        ln = ln.strip()
        if not ln:
            continue
        try:
            out.append(json.loads(ln))
        except json.JSONDecodeError:
            continue
    return out


def _save_all(rows: list[dict]) -> None:
    tmp = JOURNAL.with_suffix('.jsonl.tmp')
    if rows:
        tmp.write_text('\n'.join(json.dumps(r, ensure_ascii=False) for r in rows) + '\n')
    else:
        tmp.write_text('')
    tmp.replace(JOURNAL)


def _advance(qty: int, avg: float, buy_qty: int, sell_qty: int, buy_avg: float) -> tuple[int, float]:
    """running 보유수량·평단 1스텝 진행.

    매도는 **전 보유분 우선 소진**(평단 유지) → 잔여 당일 매수분을 가중평균 블렌딩.
    이 순서가 '전일 보유 평단' 정의의 핵심 — 당일 매수가 매도 원가에 섞이지 않고,
    매도로 원물이 다 빠지면 남는 건 당일 매수분 평단이 된다.
    """
    consumed = min(sell_qty, qty)
    qty -= consumed
    rem_buy = buy_qty - (sell_qty - consumed)  # 당일매수분 중 매도로 안 빠진 잔량
    if rem_buy > 0:
        avg = (qty * avg + rem_buy * buy_avg) / (qty + rem_buy)
        qty += rem_buy
    qty = max(qty, 0)
    if qty == 0:
        avg = 0.0  # 전량 청산 → 평단 리셋 (이후 순수 데이트레이딩을 보유분으로 오인 방지)
    return qty, avg


def _apply_rebase(row: dict, sell_avg: float, sell_qty: int, pre_avg: float) -> None:
    """row의 pl_amt/prft_rt를 '전일 보유 평단(pre_avg)' 기준으로 덮어쓴다 (in-place).

    그로스 = (매도평단 - 전일평단) × 매도수량, pl_amt = 그로스 - 수수료·세금 (ka10170과 동일 net).
    """
    gross = (sell_avg - pre_avg) * sell_qty
    fee = int(row.get('cmsn_tax') or 0)
    row['pl_amt'] = round(gross) - fee
    cost = pre_avg * sell_qty
    row['prft_rt'] = round(row['pl_amt'] / cost * 100, 2) if cost else 0.0
    row['_pl_rebased'] = True
    row['prev_avg'] = round(pre_avg)


def rebase_pl(rows: list[dict]) -> list[dict]:
    """매도 실현손익을 '전일 보유 평단' 기준으로 재계산해 **새 리스트** 반환 (파일 미변경, 표시 전용).

    ka10170은 당일 매수+매도가 같이 있으면 당일 매수평단↔매도평단 round-trip으로 pl_amt를 매겨
    어제까지 보유한 원물의 손익을 가린다 (2026-06-29 0193W0: 6/26 보유 62@24,550 +
    당일 62@21,875 매수 후 62@21,885 매도 → ka10170 +220원, 실제 원물 손익 약 -165,630원).

    (account, code)별로 날짜순 재생(_advance)하며, **당일 매수가 동반된 매도 행**만
    당일 매수 전 보유 평단을 원가로 잡아 덮어쓴다. 순수 매도(당일 매수 없음)는 키움 값이
    정확하므로 건드리지 않는다.

    ⚠️ 입력 rows는 각 (account, code)의 **전체 이력**을 포함해야 재생이 정확하다.
    """
    out = [dict(r) for r in rows]
    by_key: dict[tuple, list[dict]] = {}
    for r in out:
        by_key.setdefault((r.get('account'), r.get('code')), []).append(r)
    for grp in by_key.values():
        grp.sort(key=lambda r: (r.get('date', ''), r.get('collected_at', '')))
        qty, avg = 0, 0.0
        for r in grp:
            bq = int(r.get('buy_qty') or 0)
            sq = int(r.get('sell_qty') or 0)
            ba = float(r.get('buy_avg') or 0)
            sa = float(r.get('sell_avg') or 0)
            pre_avg = avg  # 당일 매수 반영 전 = 전일 보유 평단
            if sq > 0 and bq > 0 and pre_avg > 0:
                _apply_rebase(r, sa, sq, pre_avg)
            qty, avg = _advance(qty, avg, bq, sq, ba)
    return out


def prev_avg_map(before_iso: str) -> dict:
    """파일 이력 중 before_iso **미만**(전일까지)을 (account, code)별 재생해 시작 평단 맵 반환.

    라이브 journal(당일분)의 round-trip 오염 행을 재계산할 때 원가(전일 보유 평단) 소스.
    {(account, code): avg}. 보유 0이면 0.
    """
    rows = [r for r in _load_all() if r.get('date', '') < before_iso]
    by_key: dict[tuple, list[dict]] = {}
    for r in rows:
        by_key.setdefault((r.get('account'), r.get('code')), []).append(r)
    out: dict[tuple, float] = {}
    for key, grp in by_key.items():
        grp.sort(key=lambda r: (r.get('date', ''), r.get('collected_at', '')))
        qty, avg = 0, 0.0
        for r in grp:
            qty, avg = _advance(
                qty, avg,
                int(r.get('buy_qty') or 0), int(r.get('sell_qty') or 0),
                float(r.get('buy_avg') or 0),
            )
        out[key] = avg
    return out


def record_trades(all_trades: dict, *, base_dt: str | None = None, quiet: bool = False, tag: str = 'collect') -> dict:
    """이미 받은 ka10170 응답({label: [trades]})을 jsonl에 적재 (idempotent).
    `(iso_date, account in all_trades)` 단위 재적재 — 다른 계좌는 영향 X.
    자산웹 페이지 RENDER hook, fill_watcher 등 ka10170 추가 호출 회피 경로에서 사용."""
    now = datetime.now(KST)
    if base_dt is None:
        # 휴장일/주말엔 ka10170이 직전 영업일 데이터를 반환하므로 오늘 날짜로 적재하면 중복.
        # base_dt 명시 호출(collect --date / --force)은 의도적이라 가드 해제.
        if not is_market_open(now):
            if not quiet:
                print(f'[{tag}] {now.strftime("%Y-%m-%d")} 휴장일/주말 — record_trades skip')
            return {'skipped': True, 'date': now.strftime('%Y-%m-%d')}
        base_dt = now.strftime('%Y%m%d')
    iso_date = f'{base_dt[:4]}-{base_dt[4:6]}-{base_dt[6:]}'
    collected_at = now.isoformat()

    existing = _load_all()
    keep = [
        r for r in existing
        if not (r.get('date') == iso_date and r.get('account') in all_trades)
    ]
    new_rows: list[dict] = []
    for label, trades in all_trades.items():
        for t in trades:
            new_rows.append({
                'date': iso_date,
                'account': label,
                'owner': _owner(label),
                **t,
                'collected_at': collected_at,
            })
    _save_all(keep + new_rows)
    counts = {label: len(trades) for label, trades in all_trades.items()}
    if not quiet:
        total = sum(counts.values())
        breakdown = ' '.join(f'{k}={v}' for k, v in counts.items())
        print(f'[{tag}] {iso_date} total={total} {breakdown}')
    return {'date': iso_date, 'counts': counts}


def collect(base_dt: str | None = None, *, force: bool = False, quiet: bool = False) -> dict:
    """ka10170 4계좌 → jsonl append (idempotent)."""
    now = datetime.now(KST)
    if base_dt is None:
        base_dt = now.strftime('%Y%m%d')
    iso_date = f'{base_dt[:4]}-{base_dt[4:6]}-{base_dt[6:]}'

    target = datetime.strptime(base_dt, '%Y%m%d').replace(tzinfo=KST)
    if not force and not is_market_open(target):
        if not quiet:
            print(f'[skip] {iso_date} 휴장일/주말 — 수집 안 함 (--force 로 강제)')
        return {'skipped': True, 'date': iso_date}

    all_trades = kw.get_trade_journal_all(base_dt=base_dt)
    return record_trades(all_trades, base_dt=base_dt, quiet=quiet, tag='collect')


def seed_initial(*, seed_date: str | None = None, quiet: bool = False) -> dict:
    """현재 4계좌 보유종목 → trade_journal 시드 행 적재.

    키움이 기간 거래내역 API를 제공하지 않아, jsonl 적재 시작일(2026-05-13) 이전의
    매수 이력은 "지금 평단가 × 보유수량"으로 단일 행 압축한다.

    시드 수량 = (현재 보유) - (seed_date 이후 jsonl 매수 합) + (seed_date 이후 jsonl 매도 합)
                - (오늘 ka10170 매수)  + (오늘 ka10170 매도)
    시드 단가 = avg_price (현재 평단가 — 가중평균이라 과거 단가와 다를 수 있음)

    seed=true 플래그로 ka10170 일자 행과 구분.
    같은 (date, account, code, seed=true) 행이 이미 있으면 skip → idempotent.

    seed_date 인자로 적재 날짜 지정 가능 (기본: 오늘 KST). 실거래 행 이전 날짜로
    박으면 모달에서 시각적으로 명확히 분리됨. 이 경우 seed_date 이후 jsonl 거래 효과도
    되돌려 시드 시점 실제 보유수량을 추정한다.
    """
    now = datetime.now(KST)
    if seed_date is None:
        seed_date = now.strftime('%Y-%m-%d')
    collected_at = now.isoformat()

    positions = kw.get_positions_all()  # {label: [positions]}

    existing = _load_all()
    seen = {
        (r['date'], r['account'], r['code'])
        for r in existing if r.get('seed') is True
    }
    # seed_date 초과, 오늘 미만의 비-시드 행 효과를 (account, code)별로 집계
    # (오늘 거래는 ka10170 tdy_buyq/tdy_sellq 로 처리 → 중복 방지)
    today_str = now.strftime('%Y-%m-%d')
    post_seed: dict[tuple[str, str], dict[str, int]] = {}
    for r in existing:
        if r.get('seed') is True:
            continue
        if r['date'] <= seed_date or r['date'] >= today_str:
            continue
        key = (r['account'], r['code'])
        agg = post_seed.setdefault(key, {'buy': 0, 'sell': 0})
        agg['buy'] += int(r.get('buy_qty') or 0)
        agg['sell'] += int(r.get('sell_qty') or 0)

    new_rows: list[dict] = []
    skipped_zero = 0
    skipped_dup = 0
    for label, items in positions.items():
        for p in items:
            agg = post_seed.get((label, p['code']), {'buy': 0, 'sell': 0})
            seed_qty = (
                p['qty']
                - p['tdy_buyq'] + p['tdy_sellq']
                - agg['buy'] + agg['sell']
            )
            if seed_qty <= 0:
                skipped_zero += 1
                continue
            key = (seed_date, label, p['code'])
            if key in seen:
                skipped_dup += 1
                continue
            new_rows.append({
                'date': seed_date,
                'account': label,
                'owner': _owner(label),
                'code': p['code'],
                'name': p['name'],
                'buy_qty': seed_qty,
                'buy_avg': p['avg_price'],
                'buy_amt': seed_qty * p['avg_price'],
                'sell_qty': 0,
                'sell_avg': 0,
                'sell_amt': 0,
                'pl_amt': 0,
                'cmsn_tax': 0,
                'prft_rt': 0.0,
                'seed': True,
                'collected_at': collected_at,
            })

    if new_rows:
        _save_all(existing + new_rows)
    if not quiet:
        per_label = {l: 0 for l in positions}
        for r in new_rows:
            per_label[r['account']] += 1
        breakdown = ' '.join(f'{k}={v}' for k, v in per_label.items())
        print(f'[seed] {seed_date} added={len(new_rows)} {breakdown}'
              + (f' (skip_zero={skipped_zero} skip_dup={skipped_dup})' if (skipped_zero or skipped_dup) else ''))
    return {'added': len(new_rows), 'skipped_zero': skipped_zero, 'skipped_dup': skipped_dup}


def _print_rows(rows: list[dict]) -> None:
    print(f'{"date":<12}{"account":<14}{"code":<10}{"name":<16}{"buy":>16}{"sell":>16}{"pl":>12}')
    total_pl = 0
    for r in rows:
        marker = '*' if r.get('seed') else ' '
        buy = f'{r["buy_qty"]}@{r["buy_avg"]:,}' if r['buy_qty'] else '-'
        sell = f'{r["sell_qty"]}@{r["sell_avg"]:,}' if r['sell_qty'] else '-'
        print(f'{r["date"]:<12}{r["account"]:<14}{r["code"]:<10}{marker}{r["name"]:<15}{buy:>16}{sell:>16}{r["pl_amt"]:>12,}')
        total_pl += r['pl_amt']
    print(f'\n실현손익 합계: {total_pl:,}원 ({len(rows)}건, *=시드)')


def show(code_or_name: str) -> None:
    rows = _load_all()
    q = code_or_name.strip()
    matched = [r for r in rows if r.get('code') == q or r.get('name') == q]
    if not matched:
        matched = [r for r in rows if q in (r.get('name') or '')]
    if not matched:
        print(f'[show] 일치 기록 없음: {q}')
        return
    matched.sort(key=lambda r: (r['date'], r['account']))
    print(f'[show] {q} — {len(matched)}건')
    _print_rows(matched)


def query(*, date_from: str | None, date_to: str | None, account: str | None, code: str | None) -> None:
    rows = _load_all()
    if date_from:
        rows = [r for r in rows if r['date'] >= date_from]
    if date_to:
        rows = [r for r in rows if r['date'] <= date_to]
    if account:
        rows = [r for r in rows if r['account'] == account]
    if code:
        rows = [r for r in rows if r['code'] == code or r['name'] == code]
    rows.sort(key=lambda r: (r['date'], r['account'], r['code']))
    if not rows:
        print('[query] 결과 없음')
        return
    _print_rows(rows)


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(description='종목별 매매기록 (ka10170 적재·조회)')
    sub = p.add_subparsers(dest='cmd', required=True)

    pc = sub.add_parser('collect', help='ka10170 4계좌 수집 → jsonl 적재')
    pc.add_argument('--date', help='YYYYMMDD (기본: 오늘 KST)')
    pc.add_argument('--force', action='store_true', help='휴장일/주말 강제 수집')
    pc.add_argument('--quiet', action='store_true')

    psd = sub.add_parser('seed', help='4계좌 보유종목 → 현재 평단가로 시드 적재 (최초 1회)')
    psd.add_argument('--date', help='YYYY-MM-DD (기본: 오늘 KST)')

    ps = sub.add_parser('show', help='종목별 거래내역')
    ps.add_argument('code_or_name')

    pq = sub.add_parser('query', help='기간/계좌 필터')
    pq.add_argument('--from', dest='date_from')
    pq.add_argument('--to', dest='date_to')
    pq.add_argument('--account')
    pq.add_argument('--code')

    args = p.parse_args(argv)
    if args.cmd == 'collect':
        collect(base_dt=args.date, force=args.force, quiet=args.quiet)
    elif args.cmd == 'seed':
        seed_initial(seed_date=args.date)
    elif args.cmd == 'show':
        show(args.code_or_name)
    elif args.cmd == 'query':
        query(date_from=args.date_from, date_to=args.date_to, account=args.account, code=args.code)
    return 0


if __name__ == '__main__':
    sys.exit(main())
