<cfscript>
/**
 * Stripe Webhook Handler
 * Handles payment confirmations, refunds, and disputes
 *
 * Webhook events to configure in Stripe:
 * - payment_intent.succeeded
 * - payment_intent.payment_failed
 * - charge.refunded
 * - charge.dispute.created
 */

response = { "OK": false };

try {
    // Get raw request body
    payload = toString(getHttpRequestData().content);

    // Get Stripe signature header
    sigHeader = getHttpRequestData().headers["Stripe-Signature"] ?: "";

    // Webhook secret (set in Stripe dashboard)
    webhookSecret = application.stripeWebhookSecret ?: "";

    // Verify webhook signature (Stripe HMAC-SHA256)
    if (len(trim(webhookSecret)) > 0 && len(trim(sigHeader)) > 0) {
        // Parse signature header: t=timestamp,v1=signature
        sigParts = {};
        for (part in listToArray(sigHeader, ",")) {
            kv = listToArray(trim(part), "=");
            if (arrayLen(kv) >= 2) sigParts[trim(kv[1])] = trim(kv[2]);
        }

        sigTimestamp = sigParts["t"] ?: "";
        sigV1 = sigParts["v1"] ?: "";

        if (len(sigTimestamp) == 0 || len(sigV1) == 0) {
            response["ERROR"] = "invalid_signature";
            writeOutput(serializeJSON(response));
            abort;
        }

        // Check timestamp tolerance (5 minutes)
        timestampAge = dateDiff("s", dateAdd("s", val(sigTimestamp), createDate(1970,1,1)), now());
        if (abs(timestampAge) > 300) {
            response["ERROR"] = "timestamp_expired";
            writeOutput(serializeJSON(response));
            abort;
        }

        // Compute expected signature: HMAC-SHA256(timestamp + "." + payload, secret)
        signedPayload = sigTimestamp & "." & payload;
        expectedSig = lcase(hmac(signedPayload, webhookSecret, "HmacSHA256"));

        if (expectedSig != lcase(sigV1)) {
            writeLog(file="stripe_webhooks", text="Signature mismatch! Expected=#left(expectedSig,16)#... Got=#left(sigV1,16)#...");
            response["ERROR"] = "signature_mismatch";
            writeOutput(serializeJSON(response));
            abort;
        }
    }

    // Parse event
    event = deserializeJSON(payload);
    eventType = event.type ?: "";
    eventData = event.data.object ?: {};

    // Log the webhook
    writeLog(file="stripe_webhooks", text="Received: #eventType# - #eventData.id ?: 'unknown'#");

    switch (eventType) {

        case "payment_intent.succeeded":
            // Payment was successful
            paymentIntentID = eventData.id;
            orderID = val(eventData.metadata.order_id ?: 0);

            if (orderID > 0) {
                // Update order status to paid/submitted (status 1)
                // Note: Task is created later when order status changes to Ready (3) in updateStatus.cfm
                queryExecute("
                    UPDATE Orders
                    SET PaymentStatus = 'paid',
                        PaymentCompletedOn = NOW(),
                        StatusID = CASE WHEN StatusID = 0 THEN 1 ELSE StatusID END
                    WHERE ID = :orderID
                ", { orderID: orderID });

                writeLog(file="stripe_webhooks", text="Order #orderID# marked as paid");
            }

            // === WORKER PAYOUT TRANSFER ===
            // Find ledger row for this PaymentIntent
            try {
                qLedger = queryExecute("
                    SELECT wpl.ID, wpl.UserID, wpl.TaskID, wpl.NetTransferCents,
                           wpl.StripeTransferID, wpl.Status
                    FROM WorkPayoutLedgers wpl
                    WHERE wpl.StripePaymentIntentID = :piID
                    LIMIT 1
                ", { piID: paymentIntentID });

                if (qLedger.recordCount > 0 && isNull(qLedger.StripeTransferID) && qLedger.Status == "pending_charge") {
                    // Mark as charged
                    queryExecute("
                        UPDATE WorkPayoutLedgers SET Status = 'charged', UpdatedAt = NOW()
                        WHERE ID = :ledgerID
                    ", { ledgerID: qLedger.ID });

                    // Look up worker's Stripe Connected Account
                    qWorker = queryExecute("
                        SELECT StripeConnectedAccountID FROM Users WHERE ID = :userID
                    ", { userID: qLedger.UserID });

                    workerAccountID = qWorker.recordCount > 0 ? (qWorker.StripeConnectedAccountID ?: "") : "";

                    if (len(trim(workerAccountID)) > 0 && qLedger.NetTransferCents > 0) {
                        // Create Stripe Transfer — regardless of payoutsEnabled (Stripe holds funds)
                        stripeSecretKey = application.stripeSecretKey ?: "";
                        httpTransfer = new http();
                        httpTransfer.setMethod("POST");
                        httpTransfer.setUrl("https://api.stripe.com/v1/transfers");
                        httpTransfer.setUsername(stripeSecretKey);
                        httpTransfer.setPassword("");
                        httpTransfer.addParam(type="formfield", name="amount", value=qLedger.NetTransferCents);
                        httpTransfer.addParam(type="formfield", name="currency", value="usd");
                        httpTransfer.addParam(type="formfield", name="destination", value=workerAccountID);
                        httpTransfer.addParam(type="formfield", name="metadata[user_id]", value=qLedger.UserID);
                        httpTransfer.addParam(type="formfield", name="metadata[task_id]", value=qLedger.TaskID);
                        httpTransfer.addParam(type="formfield", name="metadata[ledger_id]", value=qLedger.ID);
                        httpTransfer.addParam(type="formfield", name="metadata[activation_withheld_cents]", value=0);
                        httpTransfer.addParam(type="header", name="Idempotency-Key", value="transfer-ledger-#qLedger.ID#");

                        transferResult = httpTransfer.send().getPrefix();
                        transferData = deserializeJSON(transferResult.fileContent);

                        if (structKeyExists(transferData, "id")) {
                            queryExecute("
                                UPDATE WorkPayoutLedgers
                                SET StripeTransferID = :transferID, Status = 'transferred', UpdatedAt = NOW()
                                WHERE ID = :ledgerID
                            ", { transferID: transferData.id, ledgerID: qLedger.ID });

                            writeLog(file="stripe_webhooks", text="Transfer #transferData.id# created for ledger #qLedger.ID#");
                        } else {
                            writeLog(file="stripe_webhooks", text="Transfer failed for ledger #qLedger.ID#: #transferData.error.message ?: 'unknown'#");
                        }
                    } else if (qLedger.NetTransferCents == 0) {
                        // Nothing to transfer (entire amount withheld for activation)
                        queryExecute("
                            UPDATE WorkPayoutLedgers SET Status = 'transferred', UpdatedAt = NOW()
                            WHERE ID = :ledgerID
                        ", { ledgerID: qLedger.ID });
                    }
                    // else: no connected account yet — stays 'charged', transfer deferred
                }
            } catch (any transferErr) {
                writeLog(file="stripe_webhooks", text="Worker transfer error: #transferErr.message#");
            }

            break;

        case "payment_intent.payment_failed":
            // Payment failed
            paymentIntentID = eventData.id;
            orderID = val(eventData.metadata.order_id ?: 0);
            failureMessage = eventData.last_payment_error.message ?: "Payment failed";

            if (orderID > 0) {
                queryExecute("
                    UPDATE Orders
                    SET PaymentStatus = 'failed',
                        PaymentError = :failureMessage
                    WHERE ID = :orderID
                ", {
                    orderID: orderID,
                    failureMessage: failureMessage
                });

                writeLog(file="stripe_webhooks", text="Order #orderID# payment failed: #failureMessage#");
            }
            break;

        case "charge.refunded":
            // Handle refund
            chargeID = eventData.id;
            paymentIntentID = eventData.payment_intent ?: "";
            refundAmount = eventData.amount_refunded / 100;

            if (paymentIntentID != "") {
                qOrder = queryExecute("
                    SELECT ID FROM Orders
                    WHERE OrderStripePaymentIntentID = :paymentIntentID
                ", { paymentIntentID: paymentIntentID });

                if (qOrder.recordCount > 0) {
                    queryExecute("
                        UPDATE Orders
                        SET PaymentStatus = 'refunded',
                            OrderRefundAmount = :refundAmount,
                            OrderRefundedOn = NOW()
                        WHERE OrderStripePaymentIntentID = :paymentIntentID
                    ", {
                        paymentIntentID: paymentIntentID,
                        refundAmount: refundAmount
                    });

                    writeLog(file="stripe_webhooks", text="Order refunded: $#refundAmount#");
                }
            }
            break;

        case "charge.dispute.created":
            // Customer disputed a charge
            chargeID = eventData.id;
            paymentIntentID = eventData.payment_intent ?: "";

            if (paymentIntentID != "") {
                qOrder = queryExecute("
                    SELECT ID, BusinessID FROM Orders
                    WHERE OrderStripePaymentIntentID = :paymentIntentID
                ", { paymentIntentID: paymentIntentID });

                if (qOrder.recordCount > 0) {
                    queryExecute("
                        UPDATE Orders
                        SET PaymentStatus = 'disputed'
                        WHERE OrderStripePaymentIntentID = :paymentIntentID
                    ", { paymentIntentID: paymentIntentID });

                    // Create a task for the dispute
                    queryExecute("
                        INSERT INTO Tasks (BusinessID, CategoryID, Title, Details, CreatedOn, StatusID, SourceType, SourceID)
                        VALUES (:businessID, 4, 'Payment Dispute', 'Order ###qOrder.ID# has been disputed. Review immediately.', NOW(), 0, 'dispute', :orderID)
                    ", {
                        businessID: qOrder.BusinessID,
                        orderID: qOrder.ID
                    });

                    writeLog(file="stripe_webhooks", text="Dispute created for order #qOrder.ID#");
                }
            }
            break;

        case "account.updated":
            // Connected account was updated
            accountID = eventData.id;
            chargesEnabled = eventData.charges_enabled ?: false;
            payoutsEnabled = eventData.payouts_enabled ?: false;

            // Business accounts
            if (chargesEnabled && payoutsEnabled) {
                queryExecute("
                    UPDATE Businesses
                    SET StripeOnboardingComplete = 1
                    WHERE StripeAccountID = :accountID
                ", { accountID: accountID });

                writeLog(file="stripe_webhooks", text="Business account #accountID# is now fully active");
            }

            // Worker accounts — update StripePayoutsEnabled for tier derivation
            try {
                qWorkerAcct = queryExecute("
                    SELECT ID FROM Users
                    WHERE StripeConnectedAccountID = :accountID
                    LIMIT 1
                ", { accountID: accountID });

                if (qWorkerAcct.recordCount > 0) {
                    queryExecute("
                        UPDATE Users
                        SET StripePayoutsEnabled = :payoutsEnabled
                        WHERE StripeConnectedAccountID = :accountID
                    ", {
                        payoutsEnabled: payoutsEnabled ? 1 : 0,
                        accountID: accountID
                    });

                    writeLog(file="stripe_webhooks", text="Worker user #qWorkerAcct.ID# payoutsEnabled=#payoutsEnabled#");
                }
            } catch (any workerErr) {
                writeLog(file="stripe_webhooks", text="Worker account update error: #workerErr.message#");
            }

            break;

        case "checkout.session.completed":
            // Activation early unlock payment completed
            try {
                sessionMetadata = eventData.metadata ?: {};
                metaType = sessionMetadata.type ?: "";
                metaUserID = val(sessionMetadata.user_id ?: 0);

                if (metaType == "activation_unlock" && metaUserID > 0) {
                    queryExecute("
                        UPDATE Users
                        SET ActivationBalanceCents = ActivationCapCents
                        WHERE ID = :userID
                    ", { userID: metaUserID });

                    writeLog(file="stripe_webhooks", text="Activation completed via payment for user #metaUserID#");
                }
            } catch (any checkoutErr) {
                writeLog(file="stripe_webhooks", text="Checkout session error: #checkoutErr.message#");
            }
            break;

        default:
            writeLog(file="stripe_webhooks", text="Unhandled event type: #eventType#");
    }

    response["OK"] = true;
    response["RECEIVED"] = true;

} catch (any e) {
    writeLog(file="stripe_webhooks", text="Error: #e.message#");
    response["ERROR"] = e.message;
}

writeOutput(serializeJSON(response));
</cfscript>
