#!/usr/bin/env python3
"""stripe-cli - bounded, agent-first command line for the Stripe API.

Reads Stripe balance, payments, and subscriptions and, with explicit
confirmation, performs guarded mutations (canceling a subscription) over
HTTPS using only the Python standard library. The read surface is primary;
every state-changing command requires --dry-run to preview, then --yes to
confirm. Mutation requires explicit confirmation.

Design rules:

- Read-only first: balance, payment, and subscription queries run freely.
- Guarded mutations: canceling a subscription requires --dry-run then --yes.
- Bounded reads: every listing caps results with --limit and never pages past
  the requested cap.
- --json emits machine-readable JSON; the default is human-readable text.
- --help works with no STRIPE_API_KEY set and makes no network calls.

Environment:
  STRIPE_API_KEY         Stripe secret or restricted API key (sk_... / rk_...)

Exit codes: 0 success, 1 Stripe API error or failed check, 2 usage error.
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Dict, List, Optional

API_BASE = os.environ.get("STRIPE_API_BASE", "https://api.stripe.com/v1")
DEFAULT_LIMIT = 20
MAX_LIMIT = 100
REQUEST_TIMEOUT = 15
TEXT_TRUNCATE = 500


class StripeError(Exception):
    """Raised when the Stripe API returns an error or transport fails."""


def get_api_key() -> str:
    key = os.environ.get("STRIPE_API_KEY", "")
    if not key:
        raise StripeError("STRIPE_API_KEY environment variable is not set")
    return key


def _encode_form_fields(fields: Dict[str, Any]) -> bytes:
    """Encode form fields, mapping booleans to Stripe's lowercase true/false."""
    normalized = {
        key: "true" if value is True else "false" if value is False else value
        for key, value in fields.items()
    }
    return urllib.parse.urlencode(normalized).encode("utf-8")


def api_request(method: str, path: str, api_key: str,
                fields: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    base = f"{API_BASE}/{path.lstrip('/')}"
    if fields and method == "GET":
        query = "&".join(f"{key}={urllib.parse.quote(str(value))}" for key, value in fields.items())
        url = f"{base}?{query}"
        data = None
    elif fields:
        url = base
        data = _encode_form_fields(fields)
    else:
        url = base
        data = None
    request = urllib.request.Request(
        url,
        data=data,
        headers={"Authorization": f"Bearer {api_key}"},
        method=method,
    )
    try:
        with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT) as response:
            return json.loads(response.read().decode("utf-8"))
    except urllib.error.HTTPError as error:
        detail = ""
        try:
            detail = json.loads(error.read().decode("utf-8")).get("error", {}).get("message", "")
        except (json.JSONDecodeError, UnicodeDecodeError):
            pass
        raise StripeError(f"Stripe API HTTP {error.code}: {detail or error.reason}") from error
    except urllib.error.URLError as error:
        raise StripeError(f"Stripe API unreachable: {error.reason}") from error
    except json.JSONDecodeError as error:
        raise StripeError(f"Stripe API returned non-JSON: {error}") from error


def usd_cents_to_display(cents: Optional[int]) -> str:
    if cents is None:
        return ""
    return f"{cents / 100:.2f}"


def summarize_balance(balance: Dict[str, Any]) -> Dict[str, Any]:
    available = [{"amount": usd_cents_to_display(b.get("amount")), "currency": b.get("currency", "").upper()}
                 for b in balance.get("available", [])]
    pending = [{"amount": usd_cents_to_display(b.get("amount")), "currency": b.get("currency", "").upper()}
               for b in balance.get("pending", [])]
    return {"available": available, "pending": pending}


def summarize_payment(payment: Dict[str, Any]) -> Dict[str, Any]:
    return {
        "id": payment.get("id", ""),
        "amount": usd_cents_to_display(payment.get("amount")),
        "currency": (payment.get("currency") or "").upper(),
        "status": payment.get("status", ""),
        "customer": payment.get("customer", ""),
        "created": payment.get("created"),
    }


def summarize_subscription(subscription: Dict[str, Any]) -> Dict[str, Any]:
    items = []
    for item in subscription.get("items", {}).get("data", []):
        price = item.get("price", {})
        items.append({
            "id": item.get("id", ""),
            "price_id": price.get("id", ""),
            "amount": usd_cents_to_display(price.get("unit_amount")),
            "currency": (price.get("currency") or "").upper(),
            "interval": (price.get("recurring") or {}).get("interval", ""),
        })
    return {
        "id": subscription.get("id", ""),
        "status": subscription.get("status", ""),
        "customer": subscription.get("customer", ""),
        "current_period_end": subscription.get("current_period_end"),
        "cancel_at_period_end": subscription.get("cancel_at_period_end", False),
        "items": items,
    }


# --------------------------------------------------------------------------
# Command implementations
# --------------------------------------------------------------------------


def cmd_balance(args: argparse.Namespace, api_key: str) -> Dict[str, Any]:
    payload = api_request("GET", "balance", api_key)
    return {"ok": True, "balance": summarize_balance(payload)}


def cmd_payments_list(args: argparse.Namespace, api_key: str) -> Dict[str, Any]:
    payload = api_request("GET", "payment_intents", api_key, {"limit": str(args.limit)})
    payments = [summarize_payment(p) for p in payload.get("data", [])]
    return {"ok": True, "payments": payments, "has_more": payload.get("has_more", False)}


def cmd_subscriptions_list(args: argparse.Namespace, api_key: str) -> Dict[str, Any]:
    payload = api_request("GET", "subscriptions", api_key, {"limit": str(args.limit)})
    subscriptions = [summarize_subscription(s) for s in payload.get("data", [])]
    return {"ok": True, "subscriptions": subscriptions, "has_more": payload.get("has_more", False)}


def cmd_subscriptions_get(args: argparse.Namespace, api_key: str) -> Dict[str, Any]:
    payload = api_request("GET", f"subscriptions/{args.subscription_id}", api_key)
    return {"ok": True, "subscription": summarize_subscription(payload)}


def cmd_subscriptions_cancel(args: argparse.Namespace, api_key: str) -> Dict[str, Any]:
    if not args.dry_run and not args.yes:
        raise StripeError(
            "refusing to cancel a subscription without confirmation: pass "
            "--dry-run to preview or --yes to confirm the mutation"
        )
    if args.dry_run:
        return {"ok": True, "dry_run": True, "would_cancel": {"subscription_id": args.subscription_id}}
    # Safer default: schedule cancellation at the period end (reversible by
    # setting cancel_at_period_end back to false) rather than cancelling
    # immediately.
    payload = api_request("POST", f"subscriptions/{args.subscription_id}", api_key,
                          {"cancel_at_period_end": True})
    if payload.get("cancel_at_period_end") is not True:
        raise StripeError(
            "Stripe did not confirm the cancellation (cancel_at_period_end is "
            f"{payload.get('cancel_at_period_end')!r}); no state change assumed"
        )
    return {"ok": True, "subscription": summarize_subscription(payload)}


# --------------------------------------------------------------------------
# Output helpers
# --------------------------------------------------------------------------


def emit(data: Dict[str, Any], json_mode: bool) -> int:
    if json_mode:
        print(json.dumps(data, indent=2, sort_keys=True))
    else:
        _emit_human(data)
    return 0


def _emit_human(data: Dict[str, Any]) -> None:
    if "balance" in data:
        for entry in data["balance"]["available"]:
            print(f"available: {entry['currency']} {entry['amount']}")
        for entry in data["balance"]["pending"]:
            print(f"pending:   {entry['currency']} {entry['amount']}")
    elif "payments" in data:
        print(f"payment intents (has_more={data['has_more']}):")
        for payment in data["payments"]:
            print(f"  {payment['id']}  {payment['currency']} {payment['amount']}  "
                  f"status={payment['status']}  customer={payment['customer']}")
    elif "subscriptions" in data:
        print(f"subscriptions (has_more={data['has_more']}):")
        for subscription in data["subscriptions"]:
            print(f"  {subscription['id']}  status={subscription['status']}  "
                  f"customer={subscription['customer']}")
    elif "subscription" in data and "dry_run" not in data:
        subscription = data["subscription"]
        print(f"subscription {subscription['id']}  status={subscription['status']}  "
              f"customer={subscription['customer']}")
        for item in subscription["items"]:
            print(f"  item {item['id']}: {item['currency']} {item['amount']}/{item['interval']}")
    elif data.get("dry_run"):
        print("DRY RUN (nothing canceled):")
        print(f"  subscription: {data['would_cancel']['subscription_id']}")
    else:
        print(json.dumps(data, indent=2, sort_keys=True))


# --------------------------------------------------------------------------
# CLI
# --------------------------------------------------------------------------


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="stripe-cli",
        description=(
            "Bounded, agent-first CLI for the Stripe API. Read-only-first "
            "surface: balance, payment intents, and subscriptions. Guarded "
            "mutation: canceling a subscription requires --dry-run then --yes."
        ),
    )
    parser.add_argument("--json", action="store_true", help="emit machine-readable JSON output")
    parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT, metavar="N",
                        help=f"cap reads at N results (default {DEFAULT_LIMIT}, max {MAX_LIMIT})")
    sub = parser.add_subparsers(dest="command", required=True)

    balance = sub.add_parser("balance", help="read account balance (read-only)")
    balance.add_argument("action", nargs="?", default="show", choices=["show"])

    payments = sub.add_parser("payments", help="read payment intents (read-only)")
    pay_sub = payments.add_subparsers(dest="action", required=True)
    pay_list = pay_sub.add_parser("list", help="list recent payment intents")

    subscriptions = sub.add_parser("subscriptions", help="read or cancel subscriptions")
    sub_sub = subscriptions.add_subparsers(dest="action", required=True)
    sub_list = sub_sub.add_parser("list", help="list subscriptions (read-only)")
    sub_get = sub_sub.add_parser("get", help="get one subscription (read-only)")
    sub_get.add_argument("--id", dest="subscription_id", required=True, help="subscription ID (sub_...)")
    sub_cancel = sub_sub.add_parser("cancel", help="cancel a subscription (guarded mutation)")
    sub_cancel.add_argument("--id", dest="subscription_id", required=True,
                            help="subscription ID (sub_...)")
    sub_cancel.add_argument("--dry-run", action="store_true",
                            help="preview the cancellation without applying")
    sub_cancel.add_argument("--yes", action="store_true", help="confirm the mutation and cancel")

    return parser


def main(argv: Optional[List[str]] = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    if args.limit < 1 or args.limit > MAX_LIMIT:
        parser.error(f"--limit must be between 1 and {MAX_LIMIT}")
    try:
        api_key = get_api_key()
        if args.command == "balance":
            result = cmd_balance(args, api_key)
        elif args.command == "payments":
            result = cmd_payments_list(args, api_key)
        elif args.command == "subscriptions":
            if args.action == "list":
                result = cmd_subscriptions_list(args, api_key)
            elif args.action == "get":
                result = cmd_subscriptions_get(args, api_key)
            else:
                result = cmd_subscriptions_cancel(args, api_key)
        else:  # pragma: no cover - argparse prevents this
            parser.error(f"unknown command: {args.command}")
        return emit(result, args.json)
    except StripeError as error:
        if args.json:
            print(json.dumps({"ok": False, "error": str(error)}, indent=2, sort_keys=True))
        else:
            print(f"stripe-cli: {error}", file=sys.stderr)
        return 1


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