<?php
require_once __DIR__ . '/config.php';

// ============================================================
//  PayFast integration library
// ============================================================
//
//  Signature algorithm matches the working reference exactly:
//    - urlencode(stripslashes($value)) — no trim() on values
//    - passphrase: urlencode only, no stripslashes, no trim
//    - merchant_key excluded from ITN signature verification
//
// ============================================================

function pf_is_sandbox(): bool {
    return defined('PF_SANDBOX') && PF_SANDBOX;
}

function pf_process_url(): string {
    return pf_is_sandbox()
        ? 'https://sandbox.payfast.co.za/eng/process'
        : 'https://www.payfast.co.za/eng/process';
}

function pf_query_url(): string {
    return pf_is_sandbox()
        ? 'https://sandbox.payfast.co.za/eng/query/validate'
        : 'https://www.payfast.co.za/eng/query/validate';
}

/**
 * Generate a PayFast signature.
 *
 * Matches the working reference exactly:
 *   urlencode(stripslashes($v)) — NO trim()
 *   passphrase: urlencode only, no stripslashes, no trim
 */
function pf_signature(array $data, string $passphrase = ''): string {
    // Remove signature field if present — never included in sig calculation
    unset($data['signature']);

    $parts = [];
    foreach ($data as $k => $v) {
        // Skip empty values — PayFast rejects blank fields
        if ($v === '' || $v === null) continue;
        // urlencode(stripslashes()) — no trim() — matches reference exactly
        $parts[] = $k . '=' . urlencode(stripslashes((string)$v));
    }
    $str = implode('&', $parts);

    if ($passphrase !== '') {
        // Passphrase: urlencode only, no stripslashes, no trim
        $str .= '&passphrase=' . urlencode($passphrase);
    }

    return md5($str);
}

/**
 * Build the checkout form fields in the exact order PayFast expects.
 *
 * Field order matters for the signature — do not reorder.
 * Empty values are stripped before signing.
 */
function pf_build_payment_fields(array $params): array {
    // Build in the correct PayFast field order
    $ordered = [
        'merchant_id'       => PF_MERCHANT_ID,
        'merchant_key'      => PF_MERCHANT_KEY,
        'return_url'        => SITE_URL . '/member/payment-return.php',
        'cancel_url'        => SITE_URL . '/member/payment-cancel.php',
        'notify_url'        => SITE_URL . '/payfast-itn.php',
        'name_first'        => $params['name_first']    ?? '',
        'name_last'         => $params['name_last']     ?? '',
        'email_address'     => $params['email_address'] ?? '',
        'm_payment_id'      => $params['m_payment_id']  ?? '',
        'amount'            => $params['amount']         ?? '',
        'item_name'         => $params['item_name']      ?? '',
        'item_description'  => $params['item_description'] ?? '',
        'custom_str1'       => $params['custom_str1']   ?? '',
        'custom_str2'       => $params['custom_str2']   ?? '',
        'custom_str3'       => $params['custom_str3']   ?? '',
        'subscription_type' => $params['subscription_type'] ?? '',
        'billing_date'      => $params['billing_date']  ?? '',
        'recurring_amount'  => $params['recurring_amount'] ?? '',
        'frequency'         => $params['frequency']     ?? '',
        'cycles'            => $params['cycles']        ?? '',
    ];

    // Strip empty values — PayFast rejects fields that are present but blank
    $ordered = array_filter($ordered, fn($v) => $v !== '' && $v !== null);

    // Sign
    $ordered['signature'] = pf_signature($ordered, PF_PASSPHRASE);

    return $ordered;
}

/**
 * Verify an ITN signature.
 *
 * IMPORTANT: merchant_key must NOT be included in ITN signature verification.
 * This differs from the checkout signature where merchant_key IS included.
 */
function pf_verify_signature(array $post_data): bool {
    $received = $post_data['signature'] ?? '';
    if (!$received) return false;

    // Remove fields not part of ITN signature
    $data = $post_data;
    unset($data['signature']);
    unset($data['merchant_key']); // excluded from ITN sig — reference explicitly notes this

    $expected = pf_signature($data, PF_PASSPHRASE);
    return hash_equals($expected, $received);
}

/**
 * Verify the remote IP belongs to PayFast.
 * Skipped in sandbox mode — dev IPs vary.
 */
function pf_verify_source_ip(string $remote_ip): bool {
    if (pf_is_sandbox()) return true;

    $hosts = [
        'www.payfast.co.za',
        'sandbox.payfast.co.za',
        'w1w.payfast.co.za',
        'w2w.payfast.co.za',
    ];
    $valid_ips = [];
    foreach ($hosts as $h) {
        $ips = @gethostbynamel($h);
        if ($ips) $valid_ips = array_merge($valid_ips, $ips);
    }
    return in_array($remote_ip, array_unique($valid_ips), true);
}

/**
 * Server-to-server postback validation.
 * Ask PayFast to confirm the ITN is genuine.
 */
function pf_validate_via_postback(array $post_data): bool {
    // In sandbox mode, skip postback — sandbox endpoint is unreliable
    if (pf_is_sandbox()) return true;

    $body = http_build_query($post_data);
    $ch = curl_init(pf_query_url());
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/x-www-form-urlencoded'],
        CURLOPT_TIMEOUT        => 10,
    ]);
    $resp = curl_exec($ch);
    curl_close($ch);
    return trim((string)$resp) === 'VALID';
}

/** Convert cents to PayFast decimal string: 50000 → "500.00" */
function pf_cents_to_amount(int $cents): string {
    return number_format($cents / 100, 2, '.', '');
}

/**
 * Cancel a PayFast recurring subscription.
 *
 * Calls PUT https://api.payfast.co.za/subscriptions/{token}/cancel
 * Returns an array: [
 *   'ok'      => bool,
 *   'status'  => int  (HTTP status from PayFast; 0 if connect failed),
 *   'message' => string (human-readable outcome or error),
 *   'raw'     => string (raw response body, for logging/debugging),
 * ]
 *
 * Safe to call when sandbox is active — returns early with ok=true so local
 * flow can be tested without hitting live endpoints.
 */
function pf_cancel_subscription(string $token): array {
    $token = trim($token);
    if ($token === '') {
        return ['ok' => false, 'status' => 0, 'message' => 'Missing subscription token', 'raw' => ''];
    }

    $is_sandbox = pf_is_sandbox();
    $timestamp  = gmdate('c'); // ISO 8601 UTC

    // Header params for signature.
    // IMPORTANT for subscriptions API: passphrase is INCLUDED in the alphabetical sort
    // (unlike the checkout-form signature where it's appended at end).
    $sig_params = [
        'merchant-id' => (string)PF_MERCHANT_ID,
        'timestamp'   => $timestamp,
        'version'     => 'v1',
    ];
    if (defined('PF_PASSPHRASE') && PF_PASSPHRASE !== '') {
        $sig_params['passphrase'] = PF_PASSPHRASE;
    }
    ksort($sig_params);

    // Build signature string — keys alphabetical, values URL-encoded
    $parts = [];
    foreach ($sig_params as $k => $v) {
        if ($v === '' || $v === null) continue;
        $parts[] = $k . '=' . urlencode(stripslashes($v));
    }
    $sig_string = implode('&', $parts);
    $signature = md5($sig_string);

    // Same endpoint for live & sandbox — testing flag controls mode
    $url = "https://api.payfast.co.za/subscriptions/{$token}/cancel?testing="
         . ($is_sandbox ? 'true' : 'false');

    $headers = [
        'merchant-id: ' . PF_MERCHANT_ID,
        'version: v1',
        'timestamp: ' . $timestamp,
        'signature: ' . $signature,
        'accept: application/json',
        'content-length: 0',
    ];

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => 'PUT',
        CURLOPT_POSTFIELDS     => '',        // Explicit empty body
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);

    $response  = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curl_err  = curl_error($ch);
    curl_close($ch);

    if ($response === false) {
        return [
            'ok'      => false,
            'status'  => 0,
            'message' => 'Could not reach PayFast: ' . $curl_err,
            'raw'     => '',
        ];
    }

    $parsed = json_decode($response, true);
    $success = ($http_code >= 200 && $http_code < 300)
            && is_array($parsed)
            && isset($parsed['status'])
            && strtolower((string)$parsed['status']) === 'success';

    // Log debug info on failure so we can diagnose signature/auth issues
    if (!$success && function_exists('mc_log')) {
        mc_log('PF cancel debug: url=' . $url);
        mc_log('PF cancel debug: sig_string=' . $sig_string);
        mc_log('PF cancel debug: signature=' . $signature);
        mc_log('PF cancel debug: merchant-id=' . PF_MERCHANT_ID . ' timestamp=' . $timestamp);
    }

    return [
        'ok'      => $success,
        'status'  => (int)$http_code,
        'message' => $success
            ? ('Subscription cancelled' . ($is_sandbox ? ' (sandbox)' : '') . ' at PayFast')
            : ('PayFast returned ' . $http_code . ': ' . substr($response, 0, 400)),
        'raw'     => $response,
    ];
}

/**
 * Update a PayFast recurring subscription (amount, cycles, frequency, run_date).
 *
 * Calls PATCH https://api.payfast.co.za/subscriptions/{token}/update
 *
 * Params you can change (all optional — only pass what you want to update):
 *   amount   (int, in cents — e.g. 50000 = R500.00)
 *   cycles   (int, 0 = unlimited)
 *   frequency (int: 3=monthly, 4=quarterly, 5=biannual, 6=annual)
 *   run_date (string YYYY-MM-DD — next charge date)
 *
 * Returns same shape as pf_cancel_subscription().
 */
function pf_update_subscription(string $token, array $updates): array {
    $token = trim($token);
    if ($token === '') {
        return ['ok' => false, 'status' => 0, 'message' => 'Missing subscription token', 'raw' => ''];
    }
    if (empty($updates)) {
        return ['ok' => false, 'status' => 0, 'message' => 'No fields to update', 'raw' => ''];
    }

    $is_sandbox = pf_is_sandbox();
    $timestamp  = gmdate('c');

    // Allowed + normalised body params
    $allowed = ['amount', 'cycles', 'frequency', 'run_date'];
    $body = [];
    foreach ($allowed as $k) {
        if (isset($updates[$k]) && $updates[$k] !== '' && $updates[$k] !== null) {
            $body[$k] = (string)$updates[$k];
        }
    }
    if (empty($body)) {
        return ['ok' => false, 'status' => 0, 'message' => 'No valid fields to update', 'raw' => ''];
    }

    // Signature input: body params + header params + passphrase, all alphabetically sorted
    $sig_params = $body + [
        'merchant-id' => (string)PF_MERCHANT_ID,
        'timestamp'   => $timestamp,
        'version'     => 'v1',
    ];
    if (defined('PF_PASSPHRASE') && PF_PASSPHRASE !== '') {
        $sig_params['passphrase'] = PF_PASSPHRASE;
    }
    ksort($sig_params);
    $parts = [];
    foreach ($sig_params as $k => $v) {
        if ($v === '' || $v === null) continue;
        $parts[] = $k . '=' . urlencode(stripslashes($v));
    }
    $sig_string = implode('&', $parts);
    $signature  = md5($sig_string);

    // Body (excluding header/passphrase/signature) — sent as form-encoded body
    $post_body = http_build_query($body);

    $url = "https://api.payfast.co.za/subscriptions/{$token}/update?testing="
         . ($is_sandbox ? 'true' : 'false');

    $headers = [
        'merchant-id: ' . PF_MERCHANT_ID,
        'version: v1',
        'timestamp: ' . $timestamp,
        'signature: ' . $signature,
        'Content-Type: application/x-www-form-urlencoded',
        'Content-Length: ' . strlen($post_body),
        'accept: application/json',
    ];

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => 'PATCH',
        CURLOPT_POSTFIELDS     => $post_body,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);

    $response  = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curl_err  = curl_error($ch);
    curl_close($ch);

    if ($response === false) {
        return ['ok' => false, 'status' => 0, 'message' => 'Could not reach PayFast: '.$curl_err, 'raw' => ''];
    }

    $parsed = json_decode($response, true);
    $success = ($http_code >= 200 && $http_code < 300)
            && is_array($parsed)
            && isset($parsed['status'])
            && strtolower((string)$parsed['status']) === 'success';

    if (!$success && function_exists('mc_log')) {
        mc_log('PF update debug: url=' . $url);
        mc_log('PF update debug: sig_string=' . $sig_string);
        mc_log('PF update debug: body=' . $post_body);
    }

    return [
        'ok'      => $success,
        'status'  => (int)$http_code,
        'message' => $success
            ? ('Subscription updated' . ($is_sandbox ? ' (sandbox)' : '') . ' at PayFast')
            : ('PayFast returned ' . $http_code . ': ' . substr($response, 0, 400)),
        'raw'     => $response,
    ];
}

// ============================================================
//  Pause / unpause subscription
// ============================================================
//  Pausing temporarily stops billing without killing the token.
//  Used during the grace period of a cancellation: if the member
//  changes their mind we can unpause them with no new charge.
//  Once the grace period expires, cron actually CANCELS at PayFast
//  (which is irreversible — they'd need a new subscription).
// ============================================================

/**
 * Internal helper for cancel/pause/unpause — they all use the same
 * signature shape, just different URL paths and human messages.
 */
function pf_subscription_action(string $token, string $action, string $verb_past): array {
    $token = trim($token);
    if ($token === '') {
        return ['ok' => false, 'status' => 0, 'message' => 'Missing subscription token', 'raw' => ''];
    }
    if (!in_array($action, ['cancel', 'pause', 'unpause'], true)) {
        return ['ok' => false, 'status' => 0, 'message' => "Invalid action '{$action}'", 'raw' => ''];
    }

    $is_sandbox = pf_is_sandbox();
    $timestamp  = gmdate('c');

    $sig_params = [
        'merchant-id' => (string)PF_MERCHANT_ID,
        'timestamp'   => $timestamp,
        'version'     => 'v1',
    ];
    if (defined('PF_PASSPHRASE') && PF_PASSPHRASE !== '') {
        $sig_params['passphrase'] = PF_PASSPHRASE;
    }
    ksort($sig_params);

    $parts = [];
    foreach ($sig_params as $k => $v) {
        if ($v === '' || $v === null) continue;
        $parts[] = $k . '=' . urlencode(stripslashes($v));
    }
    $signature = md5(implode('&', $parts));

    $url = "https://api.payfast.co.za/subscriptions/{$token}/{$action}?testing="
         . ($is_sandbox ? 'true' : 'false');

    $headers = [
        'merchant-id: ' . PF_MERCHANT_ID,
        'version: v1',
        'timestamp: ' . $timestamp,
        'signature: ' . $signature,
        'accept: application/json',
        'content-length: 0',
    ];

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => 'PUT',
        CURLOPT_POSTFIELDS     => '',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    $response  = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curl_err  = curl_error($ch);
    curl_close($ch);

    if ($response === false) {
        return ['ok'=>false, 'status'=>0, 'message'=>'Could not reach PayFast: '.$curl_err, 'raw'=>''];
    }

    $parsed  = json_decode($response, true);
    $success = ($http_code >= 200 && $http_code < 300)
            && is_array($parsed)
            && isset($parsed['status'])
            && strtolower((string)$parsed['status']) === 'success';

    return [
        'ok'      => $success,
        'status'  => (int)$http_code,
        'message' => $success
            ? ('Subscription ' . $verb_past . ($is_sandbox ? ' (sandbox)' : '') . ' at PayFast')
            : ('PayFast returned ' . $http_code . ': ' . substr($response, 0, 400)),
        'raw'     => $response,
    ];
}

function pf_pause_subscription(string $token): array {
    return pf_subscription_action($token, 'pause', 'paused');
}

function pf_unpause_subscription(string $token): array {
    return pf_subscription_action($token, 'unpause', 'unpaused');
}