"""carrier별 결제/입출금 SMS 파서.

Python 3.9 호환을 위해 union type은 문자열로 표기.

각 파서는 (raw_text, created_at_iso) -> "dict | None".
반환 dict 필드:
  - kind:    "withdrawal" | "deposit" | "card_approval" | "card_cancel"
  - entry_date: "YYYYMMDD"
  - amount:  int (원, 양수)
  - merchant: str (가맹점/메모/송수신처)
  - raw:     원문 그대로
파싱 실패 시 None을 반환해 raw 폴백 처리하도록 한다.
"""
import re
from datetime import datetime
from zoneinfo import ZoneInfo

KST = ZoneInfo("Asia/Seoul")


def _iso_to_kst_date(iso: str) -> str:
    """imsg created_at(UTC ISO) -> KST YYYYMMDD."""
    try:
        dt = datetime.fromisoformat(iso.replace("Z", "+00:00")).astimezone(KST)
        return dt.strftime("%Y%m%d")
    except Exception:
        return datetime.now(KST).strftime("%Y%m%d")


def _normalize(text: str) -> str:
    """전각 공백·전각 영문 → 일반화."""
    return text.replace("\u3000", " ").replace("\xa0", " ").strip()


HANA_RE = re.compile(
    r"\[Web발신\]\s*"
    r"하나,(?P<md>\d{2}/\d{2})[,\s]+(?P<hm>\d{2}:\d{2})\s+"
    r"(?P<acct>\S+)\s+"
    r"(?P<kind>입금|출금)\s*(?P<amount>[\d,]+)원\s*"
    r"(?P<merchant>.*?)"
    r"(?:\s+잔액\s*(?P<balance>[\d,]+)원?)?\s*$",
    re.DOTALL,
)


def parse_hana_bank(raw: str, created_at: str) -> "dict | None":
    text = raw.strip()
    m = HANA_RE.match(text)
    if not m:
        return None
    kind = "deposit" if m.group("kind") == "입금" else "withdrawal"
    amount = int(m.group("amount").replace(",", ""))
    merchant = _normalize(m.group("merchant")) or "(메모없음)"
    result = {
        "kind": kind,
        "entry_date": _iso_to_kst_date(created_at),
        "amount": amount,
        "merchant": merchant,
        "raw": raw,
    }
    if m.group("balance"):
        result["balance"] = int(m.group("balance").replace(",", ""))
    return result


# 신한카드 신형식: "신한카드(6847)승인 방*원님 아파트 관리비  249,710원 정상승인 되었습니다."
# entry_date 는 SMS created_at 기반(_iso_to_kst_date)이라 메시지 내 날짜 추출 불필요.
SHINHAN_APPROVE_RE = re.compile(
    r"신한(?:카드)?(?:\(\d+\))?\s*(?P<kind>승인|매입취소|승인취소)\s+"
    r"(?P<who>\S+?님)\s+"
    r"(?P<merchant>.+?)\s+"
    r"(?P<amount>[\d,]+)\s*원",
    re.DOTALL,
)


# 신한카드 자동납부 형식은 위 승인 SMS와 금액/상호 순서가 반대다:
#   "[신한카드] 자동납부 정상승인 방효원님 08/06 (일시불) 14,000원 삼천리도시가스"
# SHINHAN_APPROVE_RE 를 느슨하게 고치는 대신 별도 패턴으로 먼저 시도한다 —
# 느슨하게 고치면 merchant 자리에 "08/06 (일시불)" 이 잡힌다.
# kind 대안 순서 주의: `정상승인`·`승인취소` 가 `승인` 보다 앞에 와야 한다(왼쪽 우선).
SHINHAN_AUTOPAY_RE = re.compile(
    r"\[?신한카드\]?\s*자동납부\s*(?P<kind>정상승인|승인취소|매입취소|승인|취소)\s+"
    r"(?P<who>\S+?님)\s+"
    r"\d{2}/\d{2}\s+"
    r"(?:\([^)]*\)\s+)?"  # (일시불) / (3개월) — 없는 변형도 통과
    r"(?P<amount>[\d,]+)\s*원\s+"
    r"(?P<merchant>[^\n]+)",
    re.DOTALL,
)


SHINHAN_BANK_RE = re.compile(
    r"\[Web발신\]\s*\n"
    r"신한\s*(?P<md>\d{2}/\d{2})\s+(?P<hm>\d{2}:\d{2})\s*\n"
    r"(?P<acct>\S+)\s*\n"
    r"(?P<kind>입금|출금)\s+(?P<amount>[\d,]+)\s*\n"
    r"잔액\s+(?P<balance>[\d,]+)\s*\n?"
    r"(?P<merchant>.*)",
    re.DOTALL,
)


def parse_shinhan_bank(raw: str, created_at: str) -> "dict | None":
    text = raw.strip()
    m = SHINHAN_BANK_RE.match(text)
    if not m:
        return None
    kind = "deposit" if m.group("kind") == "입금" else "withdrawal"
    amount = int(m.group("amount").replace(",", ""))
    merchant = _normalize(m.group("merchant")).split("\n")[0].strip() or "(메모없음)"
    result = {
        "kind": kind,
        "entry_date": _iso_to_kst_date(created_at),
        "amount": amount,
        "merchant": merchant,
        "raw": raw,
    }
    if m.group("balance"):
        result["balance"] = int(m.group("balance").replace(",", ""))
    return result


def parse_shinhan_card(raw: str, created_at: str) -> "dict | None":
    text = _normalize(raw).replace("[Web발신]", "")
    m = SHINHAN_AUTOPAY_RE.search(text) or SHINHAN_APPROVE_RE.search(text)
    if not m:
        return None
    kind_raw = m.group("kind")
    kind = "card_cancel" if "취소" in kind_raw else "card_approval"
    amount = int(m.group("amount").replace(",", ""))
    merchant = m.group("merchant").strip().split("\n")[0]
    merchant = re.sub(r"\s+", " ", merchant).strip() or "(가맹점없음)"
    return {
        "kind": kind,
        "entry_date": _iso_to_kst_date(created_at),
        "amount": amount,
        "merchant": merchant,
        "raw": raw,
    }


KAKAOBANK_RE = re.compile(
    r"\[Web발신\]\s*\[카카오뱅크\]\s+"
    r"(?P<acct>\S+)\s+"
    r"(?P<md>\d{2}/\d{2})\s+(?P<hm>\d{2}:\d{2})\s+"
    r"(?P<kind>입금|출금)\s+(?P<amount>[\d,]+)원\s+"
    r"(?P<merchant>[^\n]+?)"
    r"(?:\s*\n잔액\s+(?P<balance>[\d,]+)원)?\s*$",
    re.DOTALL,
)


KAKAOBANK_CARD_RE = re.compile(
    r"\[Web발신\]\s*\[카카오뱅크\]\s+카드결제\s+"
    r"(?P<card>\S+)\s+"
    r"(?P<md>\d{2}/\d{2})\s+(?P<hm>\d{2}:\d{2})\s+"
    r"(?P<amount>[\d,]+)원\s+"
    r"(?P<merchant>[^\n]+?)"
    r"(?:\s+잔액\s+(?P<balance>[\d,]+)원?)?\s*$",
    re.DOTALL,
)


def parse_kakao_bank(raw: str, created_at: str) -> "dict | None":
    text = _normalize(raw)
    card = KAKAOBANK_CARD_RE.search(text)
    if card:
        result = {
            "kind": "withdrawal",
            "entry_date": _iso_to_kst_date(created_at),
            "amount": int(card.group("amount").replace(",", "")),
            "merchant": _normalize(card.group("merchant")).split("\n")[0].strip() or "(메모없음)",
            "raw": raw,
        }
        if card.group("balance"):
            result["balance"] = int(card.group("balance").replace(",", ""))
        return result

    m = KAKAOBANK_RE.search(text)
    if not m:
        return None
    kind = "deposit" if m.group("kind") == "입금" else "withdrawal"
    amount = int(m.group("amount").replace(",", ""))
    merchant = _normalize(m.group("merchant")).split("\n")[0].strip() or "(메모없음)"
    result = {
        "kind": kind,
        "entry_date": _iso_to_kst_date(created_at),
        "amount": amount,
        "merchant": merchant,
        "raw": raw,
    }
    if m.group("balance"):
        result["balance"] = int(m.group("balance").replace(",", ""))
    return result


# 현대카드 승인/취소 형식 (취소도 헤더 단어만 다르고 본문 구조는 같다):
# 현대카드 블루멤버스 승인        현대카드 블루멤버스 취소
# 방*원                          방*원
# 5,170원 일시불                 150,000원 일시불
# 04/25 11:34                    08/02 13:06
# 쿠팡                           현대가스
# 누적1,466,011원                누적4,696,608원
# 앞의 `현대카드[^\n]*` 가 greedy 라 "승인취소" 형태도 kind=취소 로 잡힌다.
HYUNDAI_CARD_RE = re.compile(
    r"^\s*현대카드[^\n]*(?P<kind>승인|취소)\s*\n"
    r"(?P<who>[^\n]+)\s*\n"
    r"(?P<amount>[\d,]+)\s*원[^\n]*\n"
    r"\d{2}/\d{2}\s+\d{2}:\d{2}\s*\n"
    r"(?P<merchant>[^\n]+)"
    r"(?:\s*\n누적\s*(?P<cumulative>[\d,]+)\s*원)?",
    re.MULTILINE,
)

# RCS/MAAP 형식은 한 줄로 들어온다:
# [Web발신] 현대카드 블루멤버스 승인 방*원 84,000원 일시불 05/16 11:57 네이버페이 누적820,000원
HYUNDAI_CARD_INLINE_RE = re.compile(
    r"현대카드[^\n]*(?P<kind>승인|취소)\s+"
    r"(?P<who>\S+)\s+"
    r"(?P<amount>[\d,]+)\s*원[^\n]*?"
    r"\d{2}/\d{2}\s+\d{2}:\d{2}\s+"
    r"(?P<merchant>.+?)"
    r"(?:\s+누적\s*(?P<cumulative>[\d,]+)\s*원?)?\s*$",
    re.MULTILINE,
)


def parse_hyundai_card(raw: str, created_at: str) -> "dict | None":
    text = _normalize(raw).replace("[Web발신]", "").strip()
    m = HYUNDAI_CARD_RE.search(text) or HYUNDAI_CARD_INLINE_RE.search(text)
    if not m:
        return None
    amount = int(m.group("amount").replace(",", ""))
    merchant = re.sub(r"\s+", " ", m.group("merchant").strip()) or "(가맹점없음)"
    result = {
        "kind": "card_cancel" if m.group("kind") == "취소" else "card_approval",
        "entry_date": _iso_to_kst_date(created_at),
        "amount": amount,
        "merchant": merchant,
        "raw": raw,
    }
    if m.group("cumulative"):
        result["cumulative"] = int(m.group("cumulative").replace(",", ""))
    return result


PARSERS = {
    "+8215991111": parse_hana_bank,
    "+8215447200": parse_shinhan_card,
    "+8215778000": parse_shinhan_bank,
    "+8215993333": parse_kakao_bank,
    "+8215776200": parse_hyundai_card,
    "15776200@botplatform.maapservice.com": parse_hyundai_card,
}


def parse(sender: str, raw: str, created_at: str) -> "dict | None":
    fn = PARSERS.get(sender)
    if fn is None:
        return None
    try:
        return fn(raw, created_at)
    except Exception:
        return None
