#!/usr/bin/env python3
"""KOSPI/KOSDAQ 일별 시장지표(ADR·투자자별 매매) 누적.

네이버 m.stock 비공식 API에서 한 콜로 받아 `state/market_indicators_history.jsonl`에 적재.
KRX 정보데이터시스템은 응답 패턴이 자주 바뀌어 별도 의존성 도입 전엔 보류 (2026-05 결정).

CLI:
  collect [--force] [--quiet]   # 휴장/주말은 self-skip (--force 우회)

idempotent: 같은 (date, market) 행 발견 시 제거 후 재적재.
launchd 트리거: 평일 17:30 — 정규장 마감(15:30) 동시호가 정산 안정화 후.
"""
from __future__ import annotations

import argparse
import json
import sys
import urllib.request
from datetime import datetime, timezone, timedelta
from pathlib import Path

KST = timezone(timedelta(hours=9))
WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace')
HISTORY = WORKSPACE / 'state' / 'market_indicators_history.jsonl'
HOLIDAYS = WORKSPACE / 'state' / 'market_holidays.json'
MARKETS = (('KOSPI', '코스피'), ('KOSDAQ', '코스닥'))


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 _parse_signed_int(s) -> int | None:
    if s is None:
        return None
    try:
        return int(str(s).replace(',', '').replace('+', '').strip())
    except (ValueError, TypeError):
        return None


def _parse_count(s) -> int:
    if s is None:
        return 0
    try:
        return int(str(s).replace(',', '').strip())
    except (ValueError, TypeError):
        return 0


def fetch_market(symbol: str, label: str) -> dict | None:
    url = f'https://m.stock.naver.com/api/index/{symbol}/integration'
    req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
    with urllib.request.urlopen(req, timeout=5.0) as resp:
        data = json.loads(resp.read().decode('utf-8', 'ignore'))
    up_down = data.get('upDownStockInfo') or {}
    rise = _parse_count(up_down.get('riseCount'))
    upper = _parse_count(up_down.get('upperCount'))
    fall = _parse_count(up_down.get('fallCount'))
    lower = _parse_count(up_down.get('lowerCount'))
    steady = _parse_count(up_down.get('steadyCount'))
    adv = rise + upper
    dec = fall + lower
    adr = round(adv / dec * 100.0, 2) if dec else None
    deal = data.get('dealTrendInfo') or {}
    bizdate = str(deal.get('bizdate') or '').strip() or None
    return {
        'market': symbol,
        'market_label': label,
        'bizdate': bizdate,
        'rise': rise,
        'upper': upper,
        'fall': fall,
        'lower': lower,
        'steady': steady,
        'adr': adr,
        'personal': _parse_signed_int(deal.get('personalValue')),
        'foreign': _parse_signed_int(deal.get('foreignValue')),
        'institutional': _parse_signed_int(deal.get('institutionalValue')),
    }


def _load_all() -> list[dict]:
    if not HISTORY.exists():
        return []
    out: list[dict] = []
    for ln in HISTORY.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 = HISTORY.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(HISTORY)


def collect(force: bool = False, quiet: bool = False) -> int:
    now = datetime.now(KST)
    if not force and not is_market_open(now):
        if not quiet:
            print(f'[skip] {now.strftime("%Y-%m-%d %H:%M %Z")} — 휴장/주말 (--force 우회)')
        return 0

    new_rows: list[dict] = []
    for symbol, label in MARKETS:
        try:
            r = fetch_market(symbol, label)
        except Exception as e:
            sys.stderr.write(f'[error] {symbol}: {e}\n')
            return 2
        if not r or not r.get('bizdate'):
            sys.stderr.write(f'[error] {symbol}: bizdate 없음 — 응답 이상\n')
            return 2
        bd = r['bizdate']
        iso = f'{bd[:4]}-{bd[4:6]}-{bd[6:]}' if len(bd) == 8 else bd
        new_rows.append({
            'date': iso,
            'market': symbol,
            'market_label': label,
            'rise': r['rise'],
            'upper': r['upper'],
            'fall': r['fall'],
            'lower': r['lower'],
            'steady': r['steady'],
            'adr': r['adr'],
            'personal': r['personal'],
            'foreign': r['foreign'],
            'institutional': r['institutional'],
            'captured_at': now.isoformat(),
        })

    existing = _load_all()
    new_keys = {(r['date'], r['market']) for r in new_rows}
    keep = [r for r in existing if (r.get('date'), r.get('market')) not in new_keys]
    merged = sorted(keep + new_rows, key=lambda r: (r.get('date', ''), r.get('market', '')))
    _save_all(merged)

    if not quiet:
        for r in new_rows:
            adr_s = f'{r["adr"]:.1f}' if r['adr'] is not None else '—'
            print(f'[collect] {r["date"]} {r["market"]:6s} ADR={adr_s} '
                  f'rise={r["rise"]} fall={r["fall"]} '
                  f'개인={r["personal"]} 외국인={r["foreign"]} 기관={r["institutional"]}')
        print(f'[ok] saved={len(new_rows)} total_rows={len(merged)} -> {HISTORY}')
    return 0


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(description='KOSPI/KOSDAQ 일별 ADR·투자자별 매매 누적')
    sub = p.add_subparsers(dest='cmd', required=True)
    c = sub.add_parser('collect', help='오늘 데이터 1회 누적')
    c.add_argument('--force', action='store_true', help='휴장/주말도 강제 실행')
    c.add_argument('--quiet', action='store_true', help='성공 로그 생략')
    args = p.parse_args(argv)

    if args.cmd == 'collect':
        return collect(force=args.force, quiet=args.quiet)
    return 1


if __name__ == '__main__':
    raise SystemExit(main())
