<?php
header('Content-Type: application/json; charset=utf-8');
/**
 * سودانا — Payment Gateway Webhook Handler
 * POST /api/payment/webhook.php
 *
 * SECTION 2 — Dual Payment: Online Gateway Flow
 * ✔ Signature verification
 * ✔ reference_code matching
 * ✔ Amount validation
 * ✔ Duplicate txn_id rejection (UNIQUE constraint)
 * ✔ Fires EventBus::emit('payment.confirmed') on success
    Logger::info('payment_confirmed', ['invoice_id' => $invoice['id'] ?? null, 'amount' => $invoice['amount'] ?? 0, 'txn_id' => $txnId ?? null]);
 * ✔ Maintains idempotency
 */

require_once __DIR__ . '/../../config.php';
require_once __DIR__ . '/../../engine/InvoiceEngine.php';
require_once __DIR__ . '/../../engine/FinancialAgent.php';
require_once __DIR__ . '/../../engine/EventBus.php';
require_once __DIR__ . '/../../engine/Logger.php'; // BRIDGE: financial audit
Logger::traceId();
header('X-Trace-Id: ' . Logger::traceId());
require_once __DIR__ . '/../../engine/QueueEngine.php';

header('Content-Type: application/json; charset=utf-8');

// ── Only POST accepted ───────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    die(json_encode(['error' => 'method_not_allowed']));
}

$rawBody = file_get_contents('php://input');
$data    = json_decode($rawBody, true);

if (!$data || !is_array($data)) {
    http_response_code(400);
    die(json_encode(['error' => 'invalid_payload']));
}

// ── STEP 1: Signature Verification ──────────────────────────────────
// Each gateway sends a signature header — verified with WEBHOOK_SECRET
$gateway  = strtolower($data['gateway'] ?? 'unknown');
$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE']
         ?? $_SERVER['HTTP_X_STRIPE_SIGNATURE']
         ?? $_SERVER['HTTP_X_SUDANNA_SIGN']
         ?? '';

if (!self_verifySignature($rawBody, $received, $gateway)) {
    http_response_code(401);
    error_log("[Webhook] Signature failed from gateway=$gateway");
    die(json_encode(['error' => 'signature_invalid']));
}

// ── STEP 2: Extract required fields ─────────────────────────────────
$refCode   = trim($data['reference_code'] ?? '');
$txnId     = trim($data['transaction_id'] ?? $data['txn_id'] ?? '');
$amount    = (float)($data['amount'] ?? 0);
$currency  = strtoupper($data['currency'] ?? 'SDG');
$status    = strtolower($data['status'] ?? '');

if (!$refCode || !$txnId || $amount <= 0) {
    http_response_code(422);
    die(json_encode(['error' => 'missing_required_fields',
                     'need'  => ['reference_code','transaction_id','amount']]));
}

if ($status !== 'success' && $status !== 'completed') {
    // Not a success event — acknowledge but do nothing
    http_response_code(200);
    die(json_encode(['received' => true, 'action' => 'ignored', 'reason' => "status=$status"]));
}

// ── STEP 3: Find invoice by reference_code ───────────────────────────
$inv = db()->prepare(
    "SELECT * FROM invoices WHERE reference_code = ? AND payment_method = 'gateway'"
);
$inv->execute([$refCode]);
$invoice = $inv->fetch(PDO::FETCH_ASSOC);

if (!$invoice) {
    // Auto-detect offline invoices promoted to gateway
    $inv2 = db()->prepare("SELECT * FROM invoices WHERE reference_code = ?");
    $inv2->execute([$refCode]);
    $invoice = $inv2->fetch(PDO::FETCH_ASSOC);
}

if (!$invoice) {
    http_response_code(404);
    die(json_encode(['error' => 'invoice_not_found', 'reference_code' => $refCode]));
}

// ── STEP 3b: Expiry check ───────────────────────────────────────────
if (!empty($invoice['expires_at']) && strtotime($invoice['expires_at']) < time()) {
    http_response_code(410);
    error_log("[Webhook] Expired invoice {$invoice['id']} — expires_at={$invoice['expires_at']}");
    die(json_encode(['error' => 'invoice_expired', 'expired_at' => $invoice['expires_at']]));
}

// ── STEP 4: Idempotency — already processed? ─────────────────────────
if ($invoice['status'] === 'paid' || $invoice['status'] === 'refunded') {
    // Already done — return 200 to stop gateway retrying
    http_response_code(200);
    die(json_encode(['received' => true, 'action' => 'already_processed',
                     'invoice_id' => $invoice['id'], 'status' => $invoice['status']]));
}

// ── STEP 5: Duplicate txn_id check ──────────────────────────────────
if (!empty($invoice['gateway_txn_id']) && $invoice['gateway_txn_id'] !== $txnId) {
    http_response_code(409);
    error_log("[Webhook] Duplicate txn attempt for invoice {$invoice['id']}: new=$txnId old={$invoice['gateway_txn_id']}");
    die(json_encode(['error' => 'duplicate_transaction', 'existing_txn' => $invoice['gateway_txn_id']]));
}

// ── STEP 6: Amount validation ────────────────────────────────────────
$expected = (float)$invoice['amount'];
$tolerance = 0.01; // cents tolerance
if (abs($amount - $expected) > $tolerance) {
    http_response_code(422);
    error_log("[Webhook] Amount mismatch invoice {$invoice['id']}: got=$amount expected=$expected");
    die(json_encode(['error' => 'amount_mismatch',
                     'expected' => $expected, 'received' => $amount]));
}

// ── STEP 7: Record txn_id + update invoice ───────────────────────────
try {
    db()->beginTransaction();

    // Store webhook payload for audit trail
    db()->prepare(
        "UPDATE invoices
         SET gateway_txn_id  = ?,
             webhook_payload = ?,
             status          = 'under_review',
             payment_method  = 'gateway',
             gateway_provider= ?
         WHERE id = ?"
    )->execute([$txnId, json_encode($data), $gateway, $invoice['id']]);

    db()->commit();
} catch (Throwable $e) {
    db()->rollBack();
    // UNIQUE constraint on gateway_txn_id → duplicate webhook
    if (str_contains($e->getMessage(), 'Duplicate entry')) {
        http_response_code(200);
        die(json_encode(['received'=>true,'action'=>'duplicate_webhook_ignored']));
    }
    http_response_code(500);
    error_log("[Webhook] DB error: " . $e->getMessage());
    die(json_encode(['error' => 'db_error']));
}

// ── STEP 8: FinancialAgent::confirmPayment → ledger entries ─────────
$result = FinancialAgent::confirmPayment((int)$invoice['id'], 0); // 0 = system auto-approve

if (!($result['success'] ?? false)) {
    http_response_code(500);
    error_log("[Webhook] FinancialAgent failed: " . ($result['error'] ?? 'unknown'));
    die(json_encode(['error' => 'financial_processing_failed', 'detail' => $result['error'] ?? '']));
}

// ── STEP 9: EventBus::emit('payment.confirmed') — UNIFIED PIPELINE ──
// SAME event as offline flow → same Worker → same AIOrchestrator chain
EventBus::emit('payment.confirmed', [
    'user_id'         => (int)$invoice['user_id'],
    'invoice_id'      => (int)$invoice['id'],
    'amount'          => $expected,
    'reference_code'  => $refCode,
    'gateway'         => $gateway,
    'txn_id'          => $txnId,
    'method'          => 'gateway',
]);

// Audit: structured Logger entry (moved from comment to actual call)
Logger::info('payment_gateway_confirmed', [
    'invoice_id'  => (int)$invoice['id'],
    'user_id'     => (int)$invoice['user_id'],
    'amount_sdg'  => $expected,
    'gateway'     => $gateway,
    'txn_id'      => $txnId,
    'ref_code'    => $refCode,
]);
Logger::setContext('invoice_id', (int)$invoice['id']);

// ── STEP 10: Insert audit log ────────────────────────────────────────
try {
    db()->prepare(
        "INSERT INTO audit_logs (action, entity_type, entity_id, data, created_at)
         VALUES ('webhook.payment.confirmed', 'invoice', ?, ?, NOW())"
    )->execute([$invoice['id'], json_encode([
        'gateway' => $gateway, 'txn_id' => $txnId, 'amount' => $amount
    ])]);
} catch (Throwable $e) {
    error_log("[Webhook::audit] " . $e->getMessage());
}

// ── Return 200 OK — gateway stops retrying ───────────────────────────
http_response_code(200);
echo json_encode([
    'received'       => true,
    'action'         => 'payment_confirmed',
    'invoice_id'     => $invoice['id'],
    'amount'         => $expected,
    'txn_id'         => $txnId,
    'ledger_entries' => $result['ledger_entries'] ?? 4,
    'timestamp'      => date('Y-m-d H:i:s'),
    'trace_id'       => Logger::traceId(),
]);

// ── Signature verification function ─────────────────────────────────
function self_verifySignature(string $body, string $received, string $gateway): bool {
    if (!$received) return false;
    $secret = defined('WEBHOOK_SECRET') ? WEBHOOK_SECRET : (GEMINI_API_KEY . '_webhook');

    // Standard HMAC-SHA256 verification
    $computed = hash_hmac('sha256', $body, $secret);
    if (hash_equals($computed, ltrim($received, 'sha256='))) return true;

    // Stripe format: t=timestamp,v1=signature
    // This format includes a timestamp — enforce 5-minute replay window
    if (preg_match('/t=(\d+)/', $received, $tm)) {
        $webhookAge = time() - (int)$tm[1];
        if ($webhookAge > 300) {
            // Replay attack: valid HMAC but stale timestamp
            error_log("[Webhook] Replay rejected — age={$webhookAge}s gateway={$gateway}");
            return false;
        }
        // Verify signature with timestamped body (Stripe-style: "t.body")
        $signedPayload = $tm[1] . '.' . $body;
        $computedTs    = hash_hmac('sha256', $signedPayload, $secret);
        if (preg_match('/v1=([a-f0-9]+)/', $received, $m)) {
            if (hash_equals($m[1], $computedTs)) return true;
        }
    }

    // Fallback: plain HMAC (gateways that don't include timestamps)
    if (preg_match('/v1=([a-f0-9]+)/', $received, $m)) {
        if (hash_equals($m[1], $computed)) return true;
    }

    // ⚠️ NO test bypass in production — real signatures only
    return false;
}
