"""Payments through a payment_gateway Connector — used by Task Designer's
"Payment — Initiate" node (app/crud/tasks.py::_exec_payment_initiate) and
the Page Designer "Payment Initiator"/"Payment Callback" blocks
(app/routers/payments.py). The gateway-specific protocol lives in
app/crud/payment_providers.py; this module records every attempt as a
PaymentTransaction and gives every gateway the same shape:

1. initiate_payment -> {checkout_url, payment_reference}: send the browser to
   checkout_url. Gateways that need a form POST (Paytm) get a FlowFabric
   "launch" URL that submits it, so callers only ever redirect.
2. The gateway sends the payer back to /payments/callback/{tenant}/{transaction};
   handle_callback checks the result with the gateway's own status API,
   then the browser continues to the caller's return_url with
   ?payment_reference=…&payment_status=paid|pending|failed.
3. verify_payment re-checks a reference on demand (the Payment Callback block).
"""

import contextvars
import secrets
import uuid
from datetime import datetime, timezone

from sqlalchemy import or_, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings
from app.crud.connectors import get_connector
from app.crud.secrets import connector_config
from app.crud.payment_providers import PaymentGatewayError, adapter_for, provider_of
from app.models.reporting import PaymentTransaction

__all__ = ["PaymentGatewayError", "initiate_payment", "verify_payment", "handle_callback", "request_base_url", "public_api_url"]

# The API's own origin as the current request reached it — set per request
# by app/main.py's middleware, so gateway callback URLs point back at this
# server without extra configuration. settings.public_api_url overrides it
# (e.g. behind a proxy that rewrites the host).
request_base_url: contextvars.ContextVar[str] = contextvars.ContextVar("request_base_url", default="")


def public_api_url() -> str:
    return (settings.public_api_url or request_base_url.get() or "http://127.0.0.1:8000").rstrip("/")


def _new_reference() -> str:
    # Letters/digits only, 18 chars — valid as an order id at every supported
    # gateway (Paytm, for one, rejects "/" and caps the length).
    return f"FF{datetime.now(timezone.utc):%y%m%d}{secrets.token_hex(5).upper()}"


async def _connector(db: AsyncSession, tenant_id: uuid.UUID, connector_id: object):
    connector = await get_connector(db, tenant_id, connector_id, "payment_gateway")
    if connector is None:
        raise PaymentGatewayError("connector not found, disabled, or not a payment_gateway connector")
    return connector


async def initiate_payment(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    connector_id: object,
    amount: int,
    currency: str = "INR",
    reference: str = "",
    checkout_path: str | None = None,
    return_url: str | None = None,
    created_by: uuid.UUID | None = None,
) -> dict:
    """`amount` is in minor units (paise/cents). `reference` is the caller's
    own label for the payment (e.g. an application number); FlowFabric
    generates the order id actually sent to the gateway and returns it as
    payment_reference. `checkout_path` only matters for a custom REST gateway."""
    if not isinstance(amount, int) or amount <= 0:
        raise PaymentGatewayError("amount must be a positive whole number of minor units (e.g. 1000 = ₹10.00)")
    connector = await _connector(db, tenant_id, connector_id)
    config = dict(await connector_config(db, tenant_id, connector))
    provider = provider_of(config)
    if provider == "custom" and checkout_path and checkout_path != "/checkout":
        config["checkout_path"] = checkout_path

    txn = PaymentTransaction(
        tenant_id=tenant_id,
        connector_id=connector.id,
        provider=provider,
        reference=_new_reference(),
        label=(reference or None) and reference[:200],
        amount=amount,
        currency=(currency or "INR").upper()[:3],
        status="created",
        return_url=return_url,
        created_by=created_by,
    )
    db.add(txn)
    await db.flush()

    callback_url = f"{public_api_url()}/payments/callback/{tenant_id}/{txn.id}"
    try:
        checkout = await adapter_for(config).create_checkout(config, txn, callback_url)
    except PaymentGatewayError as exc:
        txn.status, txn.raw = "failed", {"error": str(exc)}
        await db.flush()
        raise
    except Exception as exc:  # network errors and the like
        txn.status, txn.raw = "failed", {"error": str(exc)}
        await db.flush()
        raise PaymentGatewayError(f"payment gateway request failed: {exc}") from exc

    txn.status = "pending"
    txn.gateway_reference = checkout.gateway_reference
    txn.checkout = {"kind": checkout.kind, "url": checkout.url, "fields": checkout.fields}
    txn.raw = checkout.raw
    await db.flush()
    checkout_url = checkout.url if checkout.kind == "redirect" else f"{public_api_url()}/payments/launch/{tenant_id}/{txn.id}"
    return {"checkout_url": checkout_url, "payment_reference": txn.reference, "transaction_id": str(txn.id)}


async def _refresh(db: AsyncSession, txn: PaymentTransaction, config: dict) -> None:
    status, provider_status, raw = await adapter_for(config).fetch_status(config, txn)
    txn.status, txn.provider_status = status, (provider_status or None) and str(provider_status)[:100]
    txn.raw = {**(txn.raw or {}), "status_check": raw}
    await db.flush()


def _result(txn: PaymentTransaction) -> dict:
    return {
        "status": txn.status,
        "provider_status": txn.provider_status,
        "reference": txn.reference,
        "label": txn.label,
        "amount": txn.amount,
        "currency": txn.currency,
        "raw": (txn.raw or {}).get("status_check") or {},
    }


async def verify_payment(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    connector_id: object,
    reference: str,
    status_path: str | None = None,
) -> dict:
    """Asks the gateway for the current status of `reference` (FlowFabric's
    payment_reference, or the gateway's own id). Returns status normalised
    to "paid" | "pending" | "failed" plus the gateway's own provider_status."""
    connector = await _connector(db, tenant_id, connector_id)
    config = dict(await connector_config(db, tenant_id, connector))
    if status_path and provider_of(config) == "custom":
        config["status_path"] = status_path
    txn = (
        await db.execute(
            select(PaymentTransaction).where(
                PaymentTransaction.tenant_id == tenant_id,
                PaymentTransaction.connector_id == connector.id,
                or_(PaymentTransaction.reference == reference, PaymentTransaction.gateway_reference == reference),
            )
        )
    ).scalars().first()
    if txn is None:
        if provider_of(config) != "custom":
            raise PaymentGatewayError(f"no payment with reference '{reference}' was started through this connector")
        # A payment started outside FlowFabric: check it directly.
        txn = PaymentTransaction(tenant_id=tenant_id, connector_id=connector.id, provider="custom", reference=reference[:50], gateway_reference=reference, amount=0)
        status, provider_status, raw = await adapter_for(config).fetch_status(config, txn)
        return {"status": status, "provider_status": provider_status, "reference": reference, "label": None, "amount": None, "currency": None, "raw": raw}
    try:
        await _refresh(db, txn, config)
    except PaymentGatewayError:
        raise
    except Exception as exc:
        raise PaymentGatewayError(f"payment gateway request failed: {exc}") from exc
    return _result(txn)


async def handle_callback(db: AsyncSession, transaction_id: uuid.UUID, params: dict) -> PaymentTransaction | None:
    """The gateway's redirect back (browser GET or form POST). The callback's
    own signature is recorded, but the verdict always comes from the
    gateway's status API — a forged or replayed callback can't mark a
    payment paid."""
    txn = await db.get(PaymentTransaction, transaction_id)
    if txn is None:
        return None
    connector = await get_connector(db, txn.tenant_id, txn.connector_id, "payment_gateway")
    if connector is None:
        txn.status = "failed"
        await db.flush()
        return txn
    config = dict(await connector_config(db, txn.tenant_id, connector))
    adapter = adapter_for(config)
    try:
        info = adapter.parse_callback(config, txn, params)
        txn.raw = {**(txn.raw or {}), "callback": {"signature_valid": info.signature_valid, "provider_status": info.provider_status, "params": info.raw}}
    except PaymentGatewayError:
        pass
    try:
        await _refresh(db, txn, config)
    except Exception as exc:  # keep the payer moving; status stays as it was
        txn.raw = {**(txn.raw or {}), "status_check_error": str(exc)}
        await db.flush()
    return txn
