#!/usr/bin/env python3
"""Turn a Shopify order-export CSV into a Gigi & Mimi envelope mail-out list.

Usage:
  python3 scripts/mailout.py orders_export.csv
  python3 scripts/mailout.py orders_export.csv -o envelopes.csv
  python3 scripts/mailout.py orders_export.csv --split
  python3 scripts/mailout.py --watch ~/Downloads

By default only paid, not-fulfilled orders are included.
"""

from __future__ import annotations

import argparse
import csv
import re
import sys
import time
from collections import OrderedDict
from datetime import date
from pathlib import Path

NOTE_SPLIT = re.compile(r"\s*,\s*(?=[^,:]+:)")

HEADER = [
    "child_name",
    "shipping_name",
    "address1",
    "address2",
    "city",
    "province",
    "zip",
    "country",
    "phone",
    "email",
    "child_age",
    "plan",
    "sku",
    "gift_note",
    "order_number",
    "order_date",
    "financial_status",
    "fulfillment_status",
]


def _get(row: dict[str, str], *names: str) -> str:
    lower = {k.strip().lower(): (v or "").strip() for k, v in row.items() if k}
    for name in names:
        value = lower.get(name.lower())
        if value:
            return value
    return ""


def parse_note_attributes(raw: str) -> dict[str, str]:
    out: dict[str, str] = {}
    if not raw:
        return out
    for part in NOTE_SPLIT.split(raw):
        if ":" not in part:
            continue
        key, value = part.split(":", 1)
        out[key.strip().lower()] = value.strip()
    return out


def cadence_of(plan: str, sku: str) -> str:
    blob = f"{plan} {sku}".lower()
    if "twice" in blob or "biweekly" in blob or "2week" in blob:
        return "twice"
    if "annual" in blob and "twice" in blob:
        return "twice"
    if "month" in blob or "gmm-monthly" in blob or "gmm-annual-monthly" in blob:
        return "monthly"
    return "other"


def rows_from_shopify(path: Path) -> list[dict[str, str]]:
    with path.open(newline="", encoding="utf-8-sig") as handle:
        reader = csv.DictReader(handle)
        if not reader.fieldnames:
            raise SystemExit(f"No headers in {path}")
        grouped: OrderedDict[str, dict[str, str]] = OrderedDict()
        for raw in reader:
            order = _get(raw, "Name", "Order", "Order Number")
            if not order:
                continue
            notes = parse_note_attributes(
                _get(raw, "Note Attributes", "Notes", "Note")
            )
            child = (
                notes.get("child's name")
                or notes.get("child name")
                or _get(raw, "Child's name", "Childs name", "Lineitem property Child's name")
            )
            plan = _get(raw, "Lineitem name", "Line item name")
            sku = _get(raw, "Lineitem sku", "Line item sku", "SKU")
            if order not in grouped:
                grouped[order] = {
                    "child_name": child,
                    "shipping_name": _get(raw, "Shipping Name", "Billing Name"),
                    "address1": _get(
                        raw, "Shipping Address1", "Shipping Street", "Billing Address1"
                    ),
                    "address2": _get(raw, "Shipping Address2", "Billing Address2"),
                    "city": _get(raw, "Shipping City", "Billing City"),
                    "province": _get(
                        raw,
                        "Shipping Province",
                        "Shipping Province Name",
                        "Billing Province",
                    ),
                    "zip": _get(raw, "Shipping Zip", "Billing Zip"),
                    "country": _get(raw, "Shipping Country", "Billing Country"),
                    "phone": _get(raw, "Shipping Phone", "Billing Phone"),
                    "email": _get(raw, "Email"),
                    "child_age": notes.get("child's age")
                    or notes.get("child age")
                    or _get(raw, "Child's age"),
                    "plan": plan,
                    "sku": sku,
                    "gift_note": notes.get("gift note") or _get(raw, "Gift note"),
                    "order_number": order,
                    "order_date": _get(raw, "Created at", "Processed at"),
                    "financial_status": _get(raw, "Financial Status").lower(),
                    "fulfillment_status": _get(raw, "Fulfillment Status").lower(),
                }
            else:
                existing = grouped[order]
                if plan and plan not in existing["plan"]:
                    existing["plan"] = f"{existing['plan']} + {plan}"
                if child and not existing["child_name"]:
                    existing["child_name"] = child
        return list(grouped.values())


def apply_filters(
    rows: list[dict[str, str]], *, paid_only: bool, unfulfilled_only: bool
) -> list[dict[str, str]]:
    out = []
    for row in rows:
        financial = row["financial_status"]
        fulfillment = row["fulfillment_status"]
        if paid_only and financial not in {"paid", "partially_paid", "authorized", ""}:
            continue
        if unfulfilled_only and fulfillment in {"fulfilled", "shipped"}:
            continue
        out.append(row)
    return out


def write_csv(path: Path, rows: list[dict[str, str]]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=HEADER)
        writer.writeheader()
        for row in rows:
            writer.writerow({key: row.get(key, "") for key in HEADER})


def summarize(rows: list[dict[str, str]]) -> str:
    monthly = sum(1 for r in rows if cadence_of(r["plan"], r["sku"]) == "monthly")
    twice = sum(1 for r in rows if cadence_of(r["plan"], r["sku"]) == "twice")
    missing = sum(1 for r in rows if not r["child_name"] or not r["address1"])
    return (
        f"{len(rows)} envelopes  ·  {monthly} monthly  ·  {twice} twice-monthly"
        + (f"  ·  {missing} missing name or street" if missing else "")
    )


def convert(src: Path, dest: Path, *, paid_only: bool, unfulfilled_only: bool, split: bool) -> None:
    rows = apply_filters(
        rows_from_shopify(src), paid_only=paid_only, unfulfilled_only=unfulfilled_only
    )
    write_csv(dest, rows)
    print(f"Wrote {dest}")
    print(summarize(rows))
    if split:
        buckets = {"monthly": [], "twice": [], "other": []}
        for row in rows:
            buckets[cadence_of(row["plan"], row["sku"])].append(row)
        for name, bucket in buckets.items():
            if not bucket:
                continue
            part = dest.with_name(f"{dest.stem}-{name}{dest.suffix}")
            write_csv(part, bucket)
            print(f"Wrote {part} ({len(bucket)})")


def watch(folder: Path, args: argparse.Namespace) -> None:
    folder = folder.expanduser().resolve()
    seen: set[str] = set()
    print(f"Watching {folder} for Shopify CSVs. Ctrl+C to stop.")
    while True:
        for path in sorted(folder.glob("*.csv")):
            key = f"{path.name}:{path.stat().st_mtime}"
            if key in seen or path.name.startswith("gigi-mimi-envelopes"):
                continue
            seen.add(key)
            dest = path.with_name(f"gigi-mimi-envelopes-{date.today().isoformat()}.csv")
            try:
                convert(
                    path,
                    dest,
                    paid_only=not args.all,
                    unfulfilled_only=not args.all,
                    split=args.split,
                )
            except Exception as exc:  # noqa: BLE001 — keep watcher alive
                print(f"Skip {path.name}: {exc}", file=sys.stderr)
        time.sleep(2)


def main() -> None:
    parser = argparse.ArgumentParser(description="Shopify orders CSV → envelope mail-out list")
    parser.add_argument("csv", nargs="?", help="Shopify order export CSV")
    parser.add_argument("-o", "--out", help="Output CSV path")
    parser.add_argument("--split", action="store_true", help="Also write monthly/twice files")
    parser.add_argument("--all", action="store_true", help="Include unpaid and already fulfilled")
    parser.add_argument("--watch", metavar="DIR", help="Watch a folder for new Shopify CSVs")
    args = parser.parse_args()

    if args.watch:
        watch(Path(args.watch), args)
        return
    if not args.csv:
        parser.print_help()
        raise SystemExit(2)

    src = Path(args.csv).expanduser()
    if not src.exists():
        raise SystemExit(f"File not found: {src}")
    dest = Path(args.out).expanduser() if args.out else Path(
        f"gigi-mimi-envelopes-{date.today().isoformat()}.csv"
    )
    convert(
        src,
        dest,
        paid_only=not args.all,
        unfulfilled_only=not args.all,
        split=args.split,
    )


if __name__ == "__main__":
    main()
