<?php
// ============================================================
//  PayFast ITN (Instant Transaction Notification) receiver
// ============================================================
//
//  Handles all PayFast subscription events.
//
//  Invoice logic:
//    - Initial payment:   finds and marks the checkout invoice paid
//    - Recurring charge:  finds the pending invoice created by
//                         cron/monthly-invoicing.php, or creates one
//
//  Verification (4 layers):
//    1. Signature check
//    2. Source IP check (skipped in sandbox)
//    3. Amount matches invoice
//    4. Server-to-server postback (skipped in sandbox)
//
// ============================================================

require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/payfast.php';
require_once __DIR__ . '/includes/member_history.php';
require_once __DIR__ . '/includes/mailchimp.php';
require_once __DIR__ . '/includes/mail.php';

function pf_finish_request_early(): void {
    if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405); exit('Method not allowed');
}

// Read POST — handle both $_POST and php://input
$data = [];
if (!empty($_POST)) {
    foreach ($_POST as $k => $v) $data[$k] = stripslashes($v);
} else {
    $raw = file_get_contents('php://input');
    if ($raw) { parse_str($raw, $parsed); foreach ($parsed as $k => $v) $data[$k] = stripslashes($v); }
}

$raw_payload    = file_get_contents('php://input') ?: http_build_query($_POST);
$remote_ip      = $_SERVER['REMOTE_ADDR'] ?? '';
$pf_payment_id  = $data['pf_payment_id'] ?? null;
$m_payment_id   = $data['m_payment_id']  ?? null;
$payment_status = strtolower(trim($data['payment_status'] ?? ''));
$token          = $data['token'] ?? null;
$amount_gross   = isset($data['amount_gross']) ? (int)round((float)$data['amount_gross'] * 100) : 0;
$amount_fee     = isset($data['amount_fee'])   ? (int)round((float)$data['amount_fee']   * 100) : 0;
$amount_net     = isset($data['amount_net'])   ? (int)round((float)$data['amount_net']   * 100) : 0;

// Parse custom fields from checkout (echoed back on all ITNs)
$checkout_invoice_id = null;
$member_id           = null;
$payment_tier        = null;
foreach (['custom_str1','custom_str2','custom_str3','custom_str4'] as $k) {
    if (!empty($data[$k]) && strpos($data[$k], ':') !== false) {
        [$type, $val] = explode(':', $data[$k], 2);
        if ($type === 'invoice') $checkout_invoice_id = (int)$val;
        if ($type === 'member')  $member_id  = (int)$val;
        if ($type === 'tier')    $payment_tier = $val;
    }
}

// Fallback member lookup from token or invoice number
if (!$member_id && $token) {
    $tok = db_row('SELECT member_id FROM payment_tokens WHERE token = :t', ['t' => $token]);
    if ($tok) $member_id = (int)$tok['member_id'];
}
if (!$member_id && $m_payment_id) {
    $inv = db_row('SELECT member_id FROM invoices WHERE number = :n', ['n' => $m_payment_id]);
    if ($inv) $member_id = (int)$inv['member_id'];
}

// Verify
$sig_ok = pf_verify_signature($data);
$ip_ok  = pf_verify_source_ip($remote_ip);

mc_log("ITN received: pf_id={$pf_payment_id} status={$payment_status} member={$member_id} sig=" . ($sig_ok?'ok':'BAD'));

// Idempotency
if ($pf_payment_id) {
    $seen = db_row('SELECT id, processed FROM payments WHERE pf_payment_id = :p', ['p' => $pf_payment_id]);
    if ($seen && $seen['processed']) { http_response_code(200); echo 'duplicate'; exit; }
}

// Log raw ITN
$payment_row_id = db_insert('payments', [
    'member_id'          => $member_id,
    'invoice_id'         => $checkout_invoice_id,
    'pf_payment_id'      => $pf_payment_id,
    'pf_token'           => $token,
    'payment_status'     => $payment_status,
    'amount_gross_cents' => $amount_gross,
    'amount_fee_cents'   => $amount_fee,
    'amount_net_cents'   => $amount_net,
    'm_payment_id'       => $m_payment_id,
    'raw_payload'        => substr($raw_payload, 0, 16384),
    'signature_ok'       => $sig_ok ? 1 : 0,
    'ip_ok'              => $ip_ok  ? 1 : 0,
    'processed'          => 0,
]);

// ACK PayFast immediately
http_response_code(200);
echo 'OK';
pf_finish_request_early();

// ---- Response sent — process in background -----------------

if (!$sig_ok || !$ip_ok) {
    if (pf_is_sandbox()) {
        // In sandbox: signature failures are common due to passphrase differences.
        // Log but continue processing so we can test the full flow.
        mc_log("ITN sandbox: sig=" . ($sig_ok?'ok':'MISMATCH') . " ip=" . ($ip_ok?'ok':'BAD') . " — proceeding anyway");
    } else {
        // In production: hard reject
        mc_log("ITN rejected: verification failure (sig=" . ($sig_ok?'ok':'BAD') . " ip=" . ($ip_ok?'ok':'BAD') . ")");
        return;
    }
}

// ============================================================
//  Route by event type
// ============================================================

switch ($payment_status) {

    // ── Successful payment (initial OR recurring monthly) ───
    case 'complete':
    case 'subscr_payment':

        if (!pf_validate_via_postback($data)) {
            mc_log("ITN postback failed for pf_id={$pf_payment_id}");
            return;
        }
        if (!$member_id) {
            mc_log("ITN COMPLETE: cannot identify member");
            return;
        }

        // Load member for invoice description and email
        $member = db_row('SELECT * FROM members WHERE id = :id', ['id' => $member_id]);
        if (!$member) { mc_log("ITN: member {$member_id} not found"); return; }

        // Use tier from custom_str4 if passed, otherwise from member record
        $tier = $payment_tier ?? $member['tier'] ?? 'Bronze';

        // Look up the plan price — use PayFast amount as fallback
        $plan = db_row(
            'SELECT price_cents FROM subscription_plans WHERE slug = :s AND active = 1',
            ['s' => strtolower($tier)]
        );
        // Use actual charged amount (most accurate — what PayFast really charged)
        $invoice_amount = $amount_gross;

        // Create a new invoice for this payment.
        // This only runs once per pf_payment_id (idempotency guard above).
        // The `number` column is intentionally left NULL — it will be populated
        // by zoho_record_payment_full() below using the real Zoho invoice number.
        // If Zoho is unavailable, the invoice stays without a number until admin
        // retries the sync. We never invent our own number; customers and admins
        // see exactly the same reference Zoho assigned.
        $invoice_id = db_insert('invoices', [
            'member_id'    => $member_id,
            'type'         => 'membership',
            'number'       => null,
            'description'  => $tier . ' membership — ' . date('F Y'),
            'amount_cents' => $invoice_amount,
            'status'       => 'paid',         // paid immediately — PayFast confirmed
            'issued_at'    => date('Y-m-d'),
            'due_at'       => date('Y-m-d'),
            'paid_at'      => date('Y-m-d H:i:s'),
        ]);

        // Charge transaction (debit)
        db_insert('transactions', [
            'member_id'    => $member_id,
            'invoice_id'   => $invoice_id,
            'type'         => 'charge',
            'amount_cents' => $invoice_amount,
            'description'  => $tier . ' membership — ' . date('F Y'),
        ]);

        // Payment transaction (credit)
        db_insert('transactions', [
            'member_id'    => $member_id,
            'invoice_id'   => $invoice_id,
            'type'         => 'payment',
            'amount_cents' => -$invoice_amount,
            'description'  => 'Payment received via PayFast',
            'reference'    => $pf_payment_id,
        ]);

        // Store / update subscription token
        if ($token) {
            $existing_tok = db_row('SELECT id FROM payment_tokens WHERE token = :t', ['t' => $token]);
            if (!$existing_tok) {
                db_insert('payment_tokens', [
                    'member_id'      => $member_id,
                    'token'          => $token,
                    'purpose'        => 'membership',
                    'status'         => 'active',
                    'last_charge_at' => date('Y-m-d H:i:s'),
                    'next_charge_at' => date('Y-m-d', strtotime('+1 month')),
                ]);
            } else {
                db_exec(
                    'UPDATE payment_tokens
                        SET last_charge_at = NOW(),
                            next_charge_at = DATE_ADD(CURDATE(), INTERVAL 1 MONTH),
                            status = "active"
                        WHERE token = :t',
                    ['t' => $token]
                );
            }
        }

        // Activate member + extend renewal date
        db_exec(
            "UPDATE members
                SET status = 'active',
                    renewal_date = DATE_ADD(CURDATE(), INTERVAL 1 MONTH)
                WHERE id = :id",
            ['id' => $member_id]
        );

        // Run Zoho automation FIRST so we can email the customer using the
        // Zoho-assigned invoice number (rather than our local placeholder).
        // Non-blocking: if Zoho is down, log it locally but don't fail the ITN
        // response (PayFast retries on non-200, and the idempotency guard
        // prevents duplicate Zoho invoices).
        $member = db_row('SELECT * FROM members WHERE id = :id', ['id' => $member_id]);
        if ($member) {
            try {
                require_once __DIR__ . '/includes/zoho.php';
                $zoho_result = zoho_record_payment_full($invoice_id, 'payfast');
                if (!$zoho_result['ok']) {
                    app_log("Zoho automation failed for invoice #$invoice_id: " . ($zoho_result['error'] ?? 'unknown'));
                } else {
                    app_log("Zoho automation OK for invoice #$invoice_id (zoho_invoice_id={$zoho_result['invoice_id']}, number={$zoho_result['invoice_number']})");
                }
            } catch (Throwable $e) {
                app_log('Zoho automation threw: ' . $e->getMessage());
            }

            // Now re-fetch the invoice — number column was overwritten by Zoho on success
            $inv = db_row('SELECT * FROM invoices WHERE id = :id', ['id' => $invoice_id]);
            $amount_display = 'R ' . number_format($amount_gross / 100, 2, '.', ' ');
            $next_date = date('j F Y', strtotime('+1 month'));

            // Only send the receipt email if we have a real Zoho invoice number.
            // If Zoho was unavailable, the invoice has number=NULL — we don't want
            // to email the customer with a blank reference. The email will be sent
            // when admin retries the Zoho sync (which populates the number).
            if (!empty($inv['number'])) {
                require_once __DIR__ . '/includes/mailer.php';
                email_enqueue('payment_received', $member['email'],
                    trim($member['first_name'] . ' ' . $member['last_name']),
                    [
                        'first_name'       => $member['first_name'],
                        'business_name'    => $member['business_name'],
                        'tier'             => $member['tier'],
                        'amount'           => $amount_display,
                        'invoice_number'   => $inv['number'],
                        'next_charge_date' => $next_date,
                    ]
                );
            } else {
                app_log("payment_received email deferred for member {$member_id} — Zoho number not yet assigned (invoice #$invoice_id)");
            }
        }

        // Mark payment log as processed and link to the invoice
        db_exec('UPDATE payments SET processed = 1, invoice_id = :i WHERE id = :id',
                ['i' => $invoice_id, 'id' => $payment_row_id]);

        app_log("ITN processed: member {$member_id} active, invoice #$invoice_id"
            . (!empty($inv['number']) ? " ({$inv['number']})" : ' (Zoho number pending)'));

        // Member history
        $amt_disp = 'R ' . number_format($amount_gross/100, 2, '.', ' ');
        member_history_log(
            (int)$member_id,
            'payment_received',
            "Payment received — {$amt_disp}" . (!empty($inv['number']) ? " ({$inv['number']})" : ''),
            [
                'amount'       => $amt_disp,
                'gateway'      => 'payfast',
                'pf_payment'   => $pf_payment_id,
                'invoice'      => $inv['number'] ?? null,
            ],
            ['actor_type' => 'system', 'actor_name' => 'PayFast']
        );
        break;

    // ── Subscription cancelled ───────────────────────────────
    case 'cancelled':
    case 'subscr_cancel':
    case 'subscr_eot':
        if (!$member_id) { mc_log("ITN cancel: no member"); return; }

        if ($token) {
            db_exec(
                "UPDATE payment_tokens SET status='cancelled', cancelled_at=NOW() WHERE token=:t",
                ['t' => $token]
            );
        }
        db_exec("UPDATE members SET status='cancelled' WHERE id=:id", ['id' => $member_id]);

        $member = db_row('SELECT * FROM members WHERE id=:id', ['id' => $member_id]);
        if ($member) {
            require_once __DIR__ . '/includes/mailer.php';
            $end_date = $member['renewal_date']
                ? date('j F Y', strtotime($member['renewal_date']))
                : date('j F Y');
            email_enqueue('cancellation_scheduled', $member['email'],
                trim($member['first_name'] . ' ' . $member['last_name']),
                [
                    'first_name'    => $member['first_name'],
                    'business_name' => $member['business_name'],
                    'end_date'      => $end_date,
                ]
            );
        }

        db_exec('UPDATE payments SET processed=1 WHERE id=:id', ['id' => $payment_row_id]);
        mc_log("ITN: subscription cancelled for member {$member_id}");

        member_history_log(
            (int)$member_id,
            'cancellation_scheduled',
            'Subscription cancelled at PayFast — access continues until renewal date',
            null,
            ['actor_type' => 'system', 'actor_name' => 'PayFast']
        );
        break;

    // ── Payment failed ───────────────────────────────────────
    case 'failed':
    case 'subscr_failed':
        if (!$member_id) { mc_log("ITN failed: no member"); return; }

        if ($token) {
            db_exec("UPDATE payment_tokens SET status='failed' WHERE token=:t", ['t' => $token]);
        }
        db_exec("UPDATE members SET status='suspended' WHERE id=:id", ['id' => $member_id]);

        $member = db_row('SELECT * FROM members WHERE id=:id', ['id' => $member_id]);
        if ($member) {
            $amount_display = 'R ' . number_format($amount_gross / 100, 2, '.', ' ');
            mail_send(
                $member['email'],
                'Buy Local Lowveld — payment failed',
                "Hi {$member['first_name']},\n\n" .
                "Your monthly payment of {$amount_display} could not be processed.\n\n" .
                "Please update your payment method:\n" .
                SITE_URL . "/member/checkout-membership.php\n\n" .
                "Your listing has been suspended until payment is resolved.\n\n" .
                "— The Buy Local team\n"
            );
        }

        db_exec('UPDATE payments SET processed=1 WHERE id=:id', ['id' => $payment_row_id]);
        mc_log("ITN: payment failed for member {$member_id}");

        $amt_disp = 'R ' . number_format($amount_gross/100, 2, '.', ' ');
        member_history_log(
            (int)$member_id,
            'payment_failed',
            "Payment failed — {$amt_disp}. Member suspended pending payment resolution.",
            ['amount'=>$amt_disp, 'gateway'=>'payfast', 'pf_payment'=>$pf_payment_id],
            ['actor_type'=>'system', 'actor_name'=>'PayFast']
        );
        break;

    default:
        mc_log("ITN: unhandled status '{$payment_status}' for member {$member_id}");
        db_exec('UPDATE payments SET processed=1 WHERE id=:id', ['id' => $payment_row_id]);
}