"""Provider-agnostic payments — StoreGod's replacement for the WooCommerce payment plugins (Square/PayPal/Stripe).

Every modern processor follows the SAME shape: the frontend SDK tokenises the card (raw card data never touches our
server → PCI SAQ A), the backend charges the token, optional refund/webhook. So they all implement ONE interface and
adding a processor = filling in one function. This is the anti-Ghost: bring whatever processor you already use.

The merchant's OWN keys live in the vault; money flows merchant → their processor → their bank. StoreGod never holds
funds. Amounts are integer CENTS (minor units), AUD by default.

Tested live here: `mock` (fake money, proves the flow). `square` is real + ready (the store's prod creds are set —
needs a Square application_id for the web SDK before a live web charge). `stripe`/`paypal` are coded to spec but
marked UNTESTED until their keys are added — the interface is the point, each is one function.
"""
import os
from uuid import uuid4

import httpx

SQUARE_VERSION = "2024-12-18"


def _vault():
    from . import vault   # lazy so this module imports cleanly for the standalone selfcheck
    return vault


# ── adapters: charge(db, token, cents, currency, ref) -> {ok, payment_id?, status?, provider, error?} ──

async def _mock_charge(db, token, cents, currency, ref):
    """Test provider — no money, proves the order→charge→paid flow. token 'mock-decline' simulates a decline."""
    if token == "mock-decline":
        return {"ok": False, "provider": "mock", "error": "card declined (mock)"}
    return {"ok": True, "provider": "mock", "payment_id": "mock_" + (ref or uuid4().hex[:8]), "status": "COMPLETED"}


async def _square_charge(db, token, cents, currency, ref):
    tok = await _vault().get_secret(db, "square_access_token")
    if not tok:
        return {"ok": False, "provider": "square", "error": "square not configured"}
    env = (await _vault().get_secret(db, "square_environment") or "production").lower()
    base = "https://connect.squareupsandbox.com" if env.startswith("sand") else "https://connect.squareup.com"
    loc = await _vault().get_secret(db, "square_location_id")
    body = {"source_id": token, "idempotency_key": (ref or uuid4().hex)[:45],
            "amount_money": {"amount": cents, "currency": currency}}
    if loc:
        body["location_id"] = loc
    try:
        async with httpx.AsyncClient(timeout=30) as c:
            r = await c.post(f"{base}/v2/payments", json=body,
                             headers={"Authorization": f"Bearer {tok}", "Square-Version": SQUARE_VERSION})
        d = r.json()
    except Exception as e:
        return {"ok": False, "provider": "square", "error": str(e)}
    if r.status_code == 200:
        p = d["payment"]
        return {"ok": True, "provider": "square", "payment_id": p["id"], "status": p["status"]}
    return {"ok": False, "provider": "square", "error": (d.get("errors") or [{}])[0].get("detail", r.text[:200])}


async def _stripe_charge(db, token, cents, currency, ref):
    # UNTESTED — needs stripe_secret_key. PaymentIntent create+confirm with a payment_method token from Stripe.js.
    sk = await _vault().get_secret(db, "stripe_secret_key")
    if not sk:
        return {"ok": False, "provider": "stripe", "error": "stripe not configured"}
    data = {"amount": str(cents), "currency": currency.lower(), "payment_method": token,
            "confirm": "true", "automatic_payment_methods[enabled]": "true",
            "automatic_payment_methods[allow_redirects]": "never"}
    try:
        async with httpx.AsyncClient(timeout=30) as c:
            r = await c.post("https://api.stripe.com/v1/payment_intents", data=data,
                             headers={"Authorization": f"Bearer {sk}"},
                             params={"idempotency_key": ref} if ref else None)
        d = r.json()
    except Exception as e:
        return {"ok": False, "provider": "stripe", "error": str(e)}
    if r.status_code == 200 and d.get("status") in ("succeeded", "requires_capture"):
        return {"ok": True, "provider": "stripe", "payment_id": d["id"], "status": d["status"]}
    return {"ok": False, "provider": "stripe", "error": (d.get("error") or {}).get("message", r.text[:200])}


async def _paypal_charge(db, token, cents, currency, ref):
    # UNTESTED — needs paypal_client_id/secret. `token` = a PayPal order id already approved client-side; we capture it.
    cid = await _vault().get_secret(db, "paypal_client_id")
    sec = await _vault().get_secret(db, "paypal_secret")
    if not (cid and sec):
        return {"ok": False, "provider": "paypal", "error": "paypal not configured"}
    sandbox = (await _vault().get_secret(db, "paypal_environment") or "live").lower().startswith("sand")
    base = "https://api-m.sandbox.paypal.com" if sandbox else "https://api-m.paypal.com"
    try:
        async with httpx.AsyncClient(timeout=30) as c:
            r = await c.post(f"{base}/v2/checkout/orders/{token}/capture", auth=(cid, sec),
                             headers={"Content-Type": "application/json"})
        d = r.json()
    except Exception as e:
        return {"ok": False, "provider": "paypal", "error": str(e)}
    if r.status_code in (200, 201) and d.get("status") == "COMPLETED":
        return {"ok": True, "provider": "paypal", "payment_id": d["id"], "status": d["status"]}
    return {"ok": False, "provider": "paypal", "error": d.get("message", r.text[:200])}


CHARGERS = {"mock": _mock_charge, "square": _square_charge, "stripe": _stripe_charge, "paypal": _paypal_charge}


async def charge(db, provider, token, cents, currency="AUD", ref=None):
    """Dispatch a charge to the chosen provider. cents = integer minor units."""
    fn = CHARGERS.get((provider or "").lower())
    if not fn:
        return {"ok": False, "error": f"unknown provider '{provider}'"}
    if not isinstance(cents, int) or cents <= 0:
        return {"ok": False, "error": "amount must be a positive integer (cents)"}
    return await fn(db, token, cents, currency, ref)


async def enabled_providers(db):
    """Which processors this store has keys for — the checkout offers exactly these. The `mock` provider is
    a footgun on a public checkout (free 'paid' orders), so it's gated behind STOREGOD_ALLOW_MOCK_PAY=1 —
    on for pre-launch testing, OFF before the storefront goes public."""
    out = ["mock"] if os.getenv("STOREGOD_ALLOW_MOCK_PAY") == "1" else []
    if await _vault().get_secret(db, "square_access_token"):
        out.append("square")
    if await _vault().get_secret(db, "stripe_secret_key"):
        out.append("stripe")
    if await _vault().get_secret(db, "paypal_client_id"):
        out.append("paypal")
    return out


def _selfcheck():
    import asyncio

    async def t():
        assert (await charge(None, "mock", "tok", 1999))["ok"] is True
        assert (await charge(None, "mock", "mock-decline", 1999))["ok"] is False
        assert (await charge(None, "nope", "x", 100))["ok"] is False          # unknown provider
        assert (await charge(None, "mock", "x", 0))["ok"] is False            # bad amount
        print("ok")
    asyncio.run(t())


if __name__ == "__main__":
    _selfcheck()
