<?php
// ============================================================
//  netcash-notify.php — Netcash Notify URL handler
// ============================================================
//
//  Receives transaction notifications from Netcash for ALL
//  payment methods (CC, EFT, Retail, Instant EFT, etc).
//
//  Mirrors payfast-itn.php structure:
//    1. Receive POST
//    2. ACK Netcash immediately (200 OK)
//    3. Verify via RequestTrace callback
//    4. Process payment idempotently
//    5. Mark invoice paid, sync to Zoho, queue email
//
//  Round-trip data sent in m4/m5/m6 is returned as
//  Extra1/Extra2/Extra3 in this post.
//
// ============================================================

require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/netcash.php';
require_once __DIR__ . '/includes/zoho.php';
require_once __DIR__ . '/includes/member_history.php';
require_once __DIR__ . '/includes/mailer.php';

function nc_finish_request_early(): void {
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    } else {
        // Fallback: tell the client we're done before continuing
        @ignore_user_abort(true);
        @ob_end_flush();
        @flush();
    }
}

// ── Read POST data ──────────────────────────────────────────
$data = $_POST;
if (empty($data)) {
    $raw = file_get_contents('php://input');
    if ($raw) {
        parse_str($raw, $parsed);
        foreach ($parsed as $k => $v) $data[$k] = stripslashes((string)$v);
    }
}

$raw_payload = file_get_contents('php://input') ?: http_build_query($_POST);
$remote_ip   = $_SERVER['REMOTE_ADDR'] ?? '';

// Notify fields per Netcash docs
$transaction_accepted = strtolower(trim((string)($data['TransactionAccepted'] ?? '')));
$reason              = (string)($data['Reason']           ?? '');
$request_trace       = (string)($data['RequestTrace']     ?? '');
$reference           = (string)($data['Reference']        ?? '');   // our p2
$extra1              = (string)($data['Extra1']           ?? '');   // our m4
$extra2              = (string)($data['Extra2']           ?? '');   // our m5
$extra3              = (string)($data['Extra3']           ?? '');   // our m6
$amount_str          = (string)($data['Amount']           ?? '0');
$method              = (int)   ($data['Method']           ?? 0);
$cc_token            = (string)($data['ccToken']          ?? '');
$cc_holder           = (string)($data['ccHolder']         ?? '');
$cc_masked           = (string)($data['ccMasked']         ?? '');
$cc_expiry           = (string)($data['ccExpiry']         ?? '');

$amount_gross = (int)round(((float)$amount_str) * 100);
$is_accepted  = ($transaction_accepted === 'true');

// ── Decode our round-trip extras ────────────────────────────
// We pack member_id, tier, and (optionally) invoice_id into Extra1/2/3.
// Format: "type:value" so we can route based on prefix.
$member_id           = null;
$checkout_invoice_id = null;
$payment_tier        = null;

foreach ([$extra1, $extra2, $extra3] as $extra) {
    if ($extra === '' || strpos($extra, ':') === false) continue;
    [$type, $val] = explode(':', $extra, 2);
    if ($type === 'member')  $member_id           = (int)$val;
    if ($type === 'invoice') $checkout_invoice_id = (int)$val;
    if ($type === 'tier')    $payment_tier        = $val;
}

// Fallback: lookup invoice by reference if we have one
if (!$checkout_invoice_id && $reference !== '') {
    $inv = db_row('SELECT id, member_id FROM invoices WHERE gateway_payment_id=:r LIMIT 1',
        ['r' => $reference]);
    if ($inv) {
        $checkout_invoice_id = (int)$inv['id'];
        if (!$member_id) $member_id = (int)$inv['member_id'];
    }
}

app_log("NC notify: ref={$reference} accepted={$transaction_accepted} method={$method} "
    . "amount={$amount_str} member={$member_id} ip={$remote_ip}");

// ── Idempotency — by RequestTrace ──────────────────────────
if ($request_trace !== '') {
    $seen = db_row('SELECT id, processed FROM payments WHERE nc_request_trace=:t',
        ['t' => $request_trace]);
    if ($seen && $seen['processed']) {
        http_response_code(200);
        echo 'duplicate';
        exit;
    }
}

// ── Soft IP check ──────────────────────────────────────────
$ip_ok = nc_soft_check_source($remote_ip);

// ── Log raw notify ─────────────────────────────────────────
$payment_row_id = db_insert('payments', [
    'member_id'          => $member_id,
    'invoice_id'         => $checkout_invoice_id,
    'gateway'            => 'netcash',
    'nc_request_trace'   => $request_trace ?: null,
    'nc_method'          => $method ?: null,
    'nc_cc_token'        => $cc_token ?: null,
    'nc_cc_masked'       => $cc_masked ?: null,
    'nc_cc_expiry'       => $cc_expiry ?: null,
    'payment_status'     => $is_accepted ? 'COMPLETE' : 'FAILED',
    'amount_gross_cents' => $amount_gross,
    'amount_fee_cents'   => 0,
    'amount_net_cents'   => $amount_gross,    // Netcash deducts fees post-settlement
    'm_payment_id'       => $reference ?: null,
    'raw_payload'        => substr($raw_payload, 0, 16384),
    'signature_ok'       => 0,                 // Netcash doesn't sign — left as 0 by design
    'ip_ok'              => $ip_ok ? 1 : 0,
    'processed'          => 0,
]);

// ── ACK Netcash immediately ────────────────────────────────
http_response_code(200);
echo 'OK';
nc_finish_request_early();

// ──────────────────────────────────────────────────────────
//  Response sent — process in background
// ──────────────────────────────────────────────────────────

// Verify via RequestTrace callback (independent integrity check)
$lookup     = $request_trace !== '' ? nc_verify_via_lookup($request_trace) : null;
$lookup_ok  = is_array($lookup);
$lookup_match = false;
if ($lookup_ok) {
    // Must agree on Reference + Amount + Accepted
    $remote_ref = (string)($lookup['Reference'] ?? '');
    $remote_amt = (string)($lookup['Amount']    ?? '');
    $remote_acc = (bool)  ($lookup['TransactionAccepted'] ?? false);
    $lookup_match = ($remote_ref === $reference)
                 && (number_format((float)$remote_amt, 2, '.', '') === number_format((float)$amount_str, 2, '.', ''))
                 && ($remote_acc === $is_accepted);
}

if (!$lookup_ok) {
    app_log("NC notify: RequestTrace lookup FAILED for {$request_trace} — "
        . ($is_accepted ? 'proceeding (sandbox or transient error)' : 'declined anyway, no harm'));
} elseif (!$lookup_match) {
    app_log("NC notify: RequestTrace lookup MISMATCH for {$request_trace} — rejecting");
    db_exec("UPDATE payments SET payment_status='FAILED' WHERE id=:id", ['id' => $payment_row_id]);
    return;
}

// ──────────────────────────────────────────────────────────
//  Decline path — log, history, stop
// ──────────────────────────────────────────────────────────
if (!$is_accepted) {
    db_exec("UPDATE payments SET processed=1 WHERE id=:id", ['id' => $payment_row_id]);

    if ($member_id) {
        $amt_disp = 'R ' . number_format($amount_gross / 100, 2, '.', ' ');
        $reason_human = $reason !== '' ? mb_substr($reason, 0, 200) : 'Declined';
        member_history_log(
            (int)$member_id,
            'payment_failed',
            "Netcash payment failed — {$amt_disp} ({$reason_human})",
            [
                'amount'        => $amt_disp,
                'gateway'       => 'netcash',
                'request_trace' => $request_trace,
                'reason'        => $reason_human,
                'method'        => nc_method_label($method),
            ],
            ['actor_type' => 'system', 'actor_name' => 'Netcash']
        );
    }
    return;
}

// ──────────────────────────────────────────────────────────
//  Accepted path — create invoice (or use existing), Zoho, email
// ──────────────────────────────────────────────────────────

if (!$member_id) {
    app_log("NC notify: accepted but no member_id resolvable! ref={$reference}");
    db_exec("UPDATE payments SET processed=1 WHERE id=:id", ['id' => $payment_row_id]);
    return;
}

$member = db_row('SELECT * FROM members WHERE id=:id', ['id' => $member_id]);
if (!$member) {
    app_log("NC notify: member {$member_id} not found!");
    db_exec("UPDATE payments SET processed=1 WHERE id=:id", ['id' => $payment_row_id]);
    return;
}

// 1. Find or create the invoice for this payment.
$invoice = null;
if ($checkout_invoice_id) {
    $invoice = db_row('SELECT * FROM invoices WHERE id=:id', ['id' => $checkout_invoice_id]);
}

if ($invoice && $invoice['status'] === 'paid') {
    // Already processed — link payment row and exit
    app_log("NC notify: invoice #{$invoice['id']} already paid, linking only.");
    db_exec("UPDATE payments SET processed=1, invoice_id=:i WHERE id=:id",
        ['i' => $invoice['id'], 'id' => $payment_row_id]);
    return;
}

if (!$invoice) {
    // Create a new invoice for this payment (mirrors payfast-itn pattern).
    // Number is left NULL — Zoho will assign one.
    $tier = $payment_tier ?: $member['tier'];
    $invoice_id = db_insert('invoices', [
        'member_id'          => $member_id,
        'type'               => 'membership',
        'number'             => null,
        'description'        => $tier . ' membership — ' . date('F Y'),
        'amount_cents'       => $amount_gross,
        'gateway'            => 'netcash',
        'gateway_payment_id' => $reference,
        'status'             => 'paid',
        'issued_at'          => date('Y-m-d'),
        'due_at'             => date('Y-m-d'),
        'paid_at'            => date('Y-m-d H:i:s'),
    ]);
    $invoice = db_row('SELECT * FROM invoices WHERE id=:id', ['id' => $invoice_id]);

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

    // Payment transaction
    db_insert('transactions', [
        'member_id'    => $member_id,
        'invoice_id'   => $invoice_id,
        'type'         => 'payment',
        'amount_cents' => -$amount_gross,
        'description'  => 'Payment received via Netcash',
        'reference'    => $request_trace ?: $reference,
    ]);
} else {
    // Invoice already exists — flip to paid + tag with gateway info
    db_exec(
        "UPDATE invoices
            SET status='paid', paid_at=NOW(),
                gateway='netcash', gateway_payment_id=:r
          WHERE id=:id",
        ['r' => $reference, 'id' => $invoice['id']]
    );
    $invoice = db_row('SELECT * FROM invoices WHERE id=:id', ['id' => $invoice['id']]);
}

// 2. Activate member + extend renewal
if (in_array($member['status'], ['pending', 'suspended'], true)) {
    db_exec(
        "UPDATE members
            SET status='active', renewal_date=DATE_ADD(CURDATE(), INTERVAL 1 MONTH)
          WHERE id=:id",
        ['id' => $member_id]
    );
} else {
    db_exec(
        "UPDATE members SET renewal_date=DATE_ADD(COALESCE(renewal_date, CURDATE()), INTERVAL 1 MONTH)
          WHERE id=:id",
        ['id' => $member_id]
    );
}
$member = db_row('SELECT * FROM members WHERE id=:id', ['id' => $member_id]);

// 3. Save the CC token if Netcash returned one (for future recurring use)
if ($cc_token !== '') {
    $existing_tok = db_row('SELECT id FROM payment_tokens WHERE token=:t', ['t' => $cc_token]);
    if (!$existing_tok) {
        db_insert('payment_tokens', [
            'member_id'      => $member_id,
            'gateway'        => 'netcash',
            'token'          => $cc_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 id=:id",
            ['id' => $existing_tok['id']]
        );
    }
}

// 4. Push payment to Zoho (creates contact if needed, creates invoice + payment)
try {
    $zoho_result = zoho_record_payment_full((int)$invoice['id'], 'netcash');
    if (!empty($zoho_result['ok']) && !empty($zoho_result['invoice_number'])) {
        db_exec(
            "UPDATE invoices SET number=:n, zoho_invoice_number=:n WHERE id=:id",
            ['n' => $zoho_result['invoice_number'], 'id' => $invoice['id']]
        );
        $invoice['number'] = $zoho_result['invoice_number'];
    }
} catch (Throwable $e) {
    app_log("NC notify: Zoho sync threw — " . $e->getMessage());
}

// 5. Mark payment row processed
db_exec(
    "UPDATE payments SET processed=1, invoice_id=:i WHERE id=:id",
    ['i' => $invoice['id'], 'id' => $payment_row_id]
);

// 6. Welcome / payment-received email — only after we have a number
if (!empty($invoice['number'])) {
    email_enqueue('payment_received', $member['email'],
        trim($member['first_name'] . ' ' . $member['last_name']),
        [
            'first_name'     => $member['first_name'],
            'business_name'  => $member['business_name'],
            'amount'         => 'R ' . number_format($amount_gross / 100, 2, '.', ' '),
            'invoice_number' => $invoice['number'],
            'method'         => nc_method_label($method),
        ]
    );
}

// 7. Member history
$amt_disp = 'R ' . number_format($amount_gross / 100, 2, '.', ' ');
member_history_log(
    (int)$member_id,
    'payment_received',
    "Netcash payment received — {$amt_disp}"
        . (!empty($invoice['number']) ? " ({$invoice['number']})" : ''),
    [
        'amount'        => $amt_disp,
        'gateway'       => 'netcash',
        'request_trace' => $request_trace,
        'method'        => nc_method_label($method),
        'invoice'       => $invoice['number'] ?? null,
    ],
    ['actor_type' => 'system', 'actor_name' => 'Netcash']
);

app_log("NC notify processed: invoice #{$invoice['id']} (" .
    ($invoice['number'] ?? 'pending') . ") for member {$member_id}");