#!/usr/bin/env python3
"""whooing-manual: iMessage 없이 후잉 웹훅에 직접 한 건 등록.

두 가지 모드:
  structured: --item/--money/--left/--right [--date YYYYMMDD] [--memo]
  raw:        --message "원문 그대로" (후잉 자체 파서에 위임)

structured 모드는 left/right가 whooing_accounts.json 차트에 존재하는지 검증한다.
차트에 없는 계정명은 후잉이 거부하므로 사전에 막는다.

synced state는 건드리지 않는다 (SMS 동기화 흐름과 독립).
실패 시 whooing_failures.json에 기록.
"""
import argparse
import json
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo

import notify

KST = ZoneInfo("Asia/Seoul")
ROOT = Path("/Users/snowoyh/.openclaw")
WORKSPACE = ROOT / "agents" / "budget" / "workspace"
CREDENTIALS = ROOT / "credentials" / "whooing.json"
ACCOUNTS_FILE = WORKSPACE / "state" / "whooing_accounts.json"
FAILURES_FILE = WORKSPACE / "state" / "whooing_failures.json"

# 후잉 웹훅은 HTTP 200을 반환하면서 본문에 "fail" / "Error : ..." 를 줄 수 있어, 본문 검증 필수.
SUCCESS_BODY = "done"


def load_json(path, default):
    if path.exists():
        try:
            return json.loads(path.read_text())
        except Exception:
            return default
    return default


def save_json(path, data):
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2))


def load_webhook_url():
    cred = load_json(CREDENTIALS, {})
    url = (cred.get("webhook_url") or "").strip()
    if not url:
        print("❌ credentials/whooing.json 의 webhook_url 이 비어 있습니다.", file=sys.stderr)
        sys.exit(2)
    return url


def all_accounts():
    data = load_json(ACCOUNTS_FILE, {})
    names = set()
    for bucket in (data.get("categories") or {}).values():
        for name in bucket:
            names.add(name)
    return names


def post(webhook_url, payload, dry_run=False):
    """후잉 POST 결과 표준화. 반환: (ok: bool, status: int, body: str).
    HTTP 2xx + body == "done" 만 성공. 그 외(HTTP 200 + "fail" 포함) 모두 실패."""
    if dry_run:
        return True, 200, "dry-run"
    # 후잉은 '+' 를 공백으로 디코드하지 않음. quote_via=quote 로 공백을 %20 으로 보내야 함.
    encoded = urllib.parse.urlencode(payload, quote_via=urllib.parse.quote).encode()
    req = urllib.request.Request(webhook_url, data=encoded, method="POST")
    # 후잉 파서는 '; charset=utf-8' 가 붙으면 거절. 순수 미디어타입만 보냄.
    req.add_header("Content-Type", "application/x-www-form-urlencoded")
    try:
        with urllib.request.urlopen(req, timeout=15) as r:
            body = r.read().decode("utf-8", errors="replace")
            ok = (200 <= r.status < 300) and body.strip().lower().startswith(SUCCESS_BODY)
            return ok, r.status, body
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace") if hasattr(e, "read") else str(e)
        return False, e.code, body
    except Exception as e:
        return False, 0, str(e)


def record_failure(payload, status, body, mode):
    failures = load_json(FAILURES_FILE, {"failures": []})
    failures.setdefault("failures", []).append({
        "source": "manual",
        "mode": mode,
        "payload": payload,
        "status": status,
        "response": body[:500],
        "failed_at": datetime.now(KST).isoformat(),
    })
    save_json(FAILURES_FILE, failures)


def main():
    ap = argparse.ArgumentParser(description="후잉 수동 등록 (iMessage 없이 직접 POST)")
    ap.add_argument("--message", help="raw 모드: 후잉 파서에 맡길 원문")
    ap.add_argument("--item", help="structured: 항목명 (예: 스타벅스)")
    ap.add_argument("--money", type=int, help="structured: 금액 (원, 양수)")
    ap.add_argument("--left", help="structured: 차변 계정 (후잉 차트명)")
    ap.add_argument("--right", help="structured: 대변 계정 (후잉 차트명)")
    ap.add_argument("--date", help="structured: YYYYMMDD (기본: 오늘 KST)")
    ap.add_argument("--memo", default="", help="structured: 메모")
    ap.add_argument("--dry-run", action="store_true", help="POST 안 하고 계획만 출력")
    ap.add_argument("--no-validate", action="store_true", help="차트 검증 생략 (위험)")
    args = ap.parse_args()

    if args.message and any([args.item, args.money, args.left, args.right]):
        print("❌ --message 와 structured 인자(--item/--money/--left/--right)는 함께 쓸 수 없습니다.", file=sys.stderr)
        sys.exit(2)

    webhook_url = load_webhook_url() if not args.dry_run else "(dry-run)"

    if args.message:
        payload = {"message": args.message}
        mode = "raw"
        display = args.message[:60].replace("\n", " ")
    else:
        missing = [f for f in ("item", "money", "left", "right") if not getattr(args, f)]
        if missing:
            print(f"❌ structured 필수 인자 누락: {', '.join('--'+m for m in missing)}", file=sys.stderr)
            sys.exit(2)
        if args.money <= 0:
            print("❌ --money 는 양수여야 합니다.", file=sys.stderr)
            sys.exit(2)

        if not args.no_validate:
            chart = all_accounts()
            unknown = [n for n in (args.left, args.right) if n not in chart]
            if unknown:
                print(f"❌ 차트에 없는 계정명: {unknown}", file=sys.stderr)
                print("   whooing_accounts.json 의 categories 에서 정확한 이름을 확인하거나, --no-validate 로 우회.", file=sys.stderr)
                sys.exit(2)

        entry_date = args.date or datetime.now(KST).strftime("%Y%m%d")
        if len(entry_date) != 8 or not entry_date.isdigit():
            print(f"❌ --date 형식 오류 (YYYYMMDD 필요): {entry_date}", file=sys.stderr)
            sys.exit(2)

        # 후잉 structured 는 memo 필수(빈 문자열도 거절). 비면 공백 1개로 채워 통과.
        memo = args.memo if args.memo.strip() else " "
        payload = {
            "entry_date": entry_date,
            "item": args.item,
            "money": str(args.money),
            "left": args.left,
            "right": args.right,
            "memo": memo,
        }
        mode = "structured"
        display = f"{args.left} ← {args.right} {args.money:,}원 ({args.item})"

    ok, status, body = post(webhook_url, payload, dry_run=args.dry_run)
    if ok:
        print(f"✅ [{mode}] {status} | {display}")
        return

    print(f"❌ [{mode}] {status} body={body[:200]!r}")
    if not args.dry_run:
        record_failure(payload, status, body, mode)
        notify.send(_format_manual_failure(mode, payload, status, body))
    sys.exit(1)


def _format_manual_failure(mode: str, payload: dict, status: int, body: str) -> str:
    reason = notify.escape_html(notify.clean_reason(body, status))
    header = "❌ <b>후잉 수동입력 실패</b>"

    if mode == "structured":
        item = notify.escape_html(payload.get("item", ""))
        try:
            money_fmt = f"{int(payload.get('money', 0)):,}원"
        except Exception:
            money_fmt = f"{payload.get('money', '')}원"
        left = notify.escape_html(payload.get("left", ""))
        right = notify.escape_html(payload.get("right", ""))
        memo = (payload.get("memo") or "").strip()
        date = payload.get("entry_date", "")
        date_fmt = f"{date[:4]}-{date[4:6]}-{date[6:8]}" if len(date) == 8 else date

        lines = [
            f"\n\n💳 <b>{left}</b> ← <b>{right}</b>",
            f"   {money_fmt} · {item}",
            f"   날짜: {date_fmt}",
        ]
        if memo:
            lines.append(f"   메모: {notify.escape_html(memo)}")
        body_block = "\n".join(lines)
    else:
        msg = (payload.get("message") or "")[:240]
        body_block = f"\n\n📩 <b>원문</b>\n<code>{notify.escape_html(msg)}</code>"

    reason_block = f"\n\n⚠️ <b>사유</b>\n{reason}"
    return header + body_block + reason_block


if __name__ == "__main__":
    main()
