<?php
/**
 * WebEngine CMS - PayPal IPN (PAQUETES + LOG + DISCORD WEBHOOK + IPWHO.IS + BALANCE OLD/NEW)
 * custom = userid|SITE|REALM|PACKID|IP|SIGNATURE
 */

define('access', 'api');

// Load WebEngine
$webenginePath = rtrim(str_replace('\\','/', dirname(__DIR__)), '/') . '/includes/webengine.php';
if(!@include_once($webenginePath)) {
    header("HTTP/1.1 500 Internal Server Error");
    die("Could not load WebEngine.");
}

/**
 * CONFIG: Discord Webhook
 */
$privateConfigFile = __PATH_CONFIGS__ . 'nexora.private.php';
$privateConfig = is_file($privateConfigFile) ? include($privateConfigFile) : array();
$environmentWebhook = getenv('NEXORA_DISCORD_PAYPAL_WEBHOOK');
$DISCORD_WEBHOOK_URL = $environmentWebhook !== false && $environmentWebhook !== ''
    ? $environmentWebhook
    : (isset($privateConfig['discord_paypal_webhook']) ? $privateConfig['discord_paypal_webhook'] : '');

/**
 * Log file
 */
$logFile = rtrim(str_replace('\\','/', dirname(__DIR__)), '/') . '/includes/cache/paypal_ipn.log';

function ipn_log($msg) {
    global $logFile;
    $msg = preg_replace('/[\x00-\x1F\x7F]+/', ' ', (string)$msg);
    $msg = substr($msg, 0, 2000);
    $line = '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL;
    @file_put_contents($logFile, $line, FILE_APPEND);
}

/**
 * Fuerza UTF-8 (PayPal viene con charset windows-1252 a veces)
 */
function utf8ize($mixed) {
    if(is_array($mixed)) {
        foreach($mixed as $k => $v) {
            unset($mixed[$k]);
            $mixed[utf8ize($k)] = utf8ize($v);
        }
        return $mixed;
    }
    if(is_string($mixed)) {
        if(!mb_check_encoding($mixed, 'UTF-8')) {
            $mixed = @mb_convert_encoding($mixed, 'UTF-8', 'Windows-1252, ISO-8859-1, UTF-8');
        }
        // elimina chars de control raros
        $mixed = preg_replace('/[^\P{C}\n\t]/u', '', $mixed);
        return $mixed;
    }
    return $mixed;
}

/**
 * Discord helpers: sanitiza y respeta límites
 */
function d_trim($s, $max) {
    $s = (string)$s;
    $s = preg_replace('/\s+/', ' ', $s);
    $s = trim($s);
    if(mb_strlen($s, 'UTF-8') > $max) $s = mb_substr($s, 0, $max-3, 'UTF-8') . '...';
    return $s;
}

function normalize_fields_for_discord($fields) {
    $out = [];
    foreach($fields as $f) {
        if(!is_array($f)) continue;
        $name  = isset($f['name']) ? d_trim($f['name'], 256) : 'Field';
        $value = isset($f['value']) ? d_trim($f['value'], 1024) : 'N/A';
        $inline = isset($f['inline']) ? (bool)$f['inline'] : false;
        $out[] = ['name' => $name, 'value' => $value, 'inline' => $inline];
        if(count($out) >= 25) break; // límite Discord
    }
    return $out;
}

/**
 * POST JSON robusto (UTF-8 safe + detecta json_encode fail)
 */
function http_post_json($url, $payload, $timeout = 10) {
    // Forzar payload a UTF-8 para evitar "invalid JSON" en Discord
    $payload = utf8ize($payload);

    $json = json_encode($payload, JSON_UNESCAPED_UNICODE);

    if($json === false) {
        $err = json_last_error_msg();
        ipn_log("JSON_ENCODE_FAILED err=$err payload_preview=" . substr(print_r($payload,true), 0, 400));

        // fallback mínimo para que Discord reciba ALGO
        $payload = [
            'username' => 'PayPal IPN',
            'content'  => 'PayPal IPN: JSON_ENCODE_FAILED: ' . $err,
            'allowed_mentions' => ['parse' => []],
        ];
        $json = json_encode($payload, JSON_UNESCAPED_UNICODE);
    }

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json; charset=utf-8']);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
    $resp = curl_exec($ch);
    $err  = curl_error($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    return ['ok' => ($err === ''), 'http' => $code, 'resp' => $resp, 'err' => $err];
}

/**
 * GET JSON con debug (usamos ipwho.is)
 */
function http_get_json_with_debug($url, $timeout = 8) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);

    $resp = curl_exec($ch);
    $err  = curl_error($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    ipn_log("IPWHOIS http=$code err=$err");

    if($err !== '' || $code < 200 || $code >= 300) return null;
    $j = json_decode($resp, true);
    return is_array($j) ? $j : null;
}

/**
 * Intenta leer balance de créditos antes/después
 */
function try_get_credits_balance($creditSystem) {
    try {
        if(method_exists($creditSystem, 'getCredits')) return (int)$creditSystem->getCredits();
        if(method_exists($creditSystem, 'getBalance')) return (int)$creditSystem->getBalance();
        if(method_exists($creditSystem, 'creditsBalance')) return (int)$creditSystem->creditsBalance();
    } catch(Exception $e) {}
    return null;
}

function moneyEquals($a, $b, $tolerance = 0.01) {
    return abs((float)$a - (float)$b) <= $tolerance;
}

/**
 * DISCORD: mantengo tu función pero con UTF-8 safe ya desde http_post_json
 */
function send_discord_webhook($title, $fields, $extra = [], $plainTextFallback = '') {
    global $DISCORD_WEBHOOK_URL;

    if(!$DISCORD_WEBHOOK_URL) return;

    // Sanitiza fields
    $fields = normalize_fields_for_discord($fields);

    // Embed estable
    $embed = [
        'title' => d_trim($title, 256),
        'description' => d_trim($plainTextFallback ?: 'IPN Notification', 2048),
        'fields' => $fields,
        'timestamp' => gmdate('c'),
    ];

    if(isset($extra['color'])) $embed['color'] = (int)$extra['color'];
    if(isset($extra['footer'])) $embed['footer'] = ['text' => d_trim((string)$extra['footer'], 2048)];

    // Mandamos content también
    $payload = [
        'username' => 'PayPal IPN',
        'content' => d_trim($plainTextFallback ?: $title, 1800),
        'embeds' => [$embed],
        'allowed_mentions' => ['parse' => []],
    ];

    $res = http_post_json($DISCORD_WEBHOOK_URL, $payload, 10);
    ipn_log('DISCORD webhook http=' . $res['http'] . ' ok=' . ($res['ok'] ? '1' : '0') . ' err=' . $res['err']);
}

/**
 * PAQUETES (igual que cliente)
 */
$PACKAGES = [
    'P100'  => ['usd' => 1.00,  'credits' => 1000],
    'P200'  => ['usd' => 1.99,  'credits' => 2000],
    'P300'  => ['usd' => 2.99,  'credits' => 3000],
    'P400'  => ['usd' => 3.99,  'credits' => 4000],
    'P550'  => ['usd' => 4.99,  'credits' => 5500],
    'P781'  => ['usd' => 6.99,  'credits' => 7810],
    'P1125' => ['usd' => 9.99,  'credits' => 11250],
    'P1703' => ['usd' => 14.99, 'credits' => 17030],
    'P2290' => ['usd' => 19.99, 'credits' => 22900],
    'P2888' => ['usd' => 24.99, 'credits' => 28880],
    'P3495' => ['usd' => 29.99, 'credits' => 34950],
    'P5925' => ['usd' => 49.99, 'credits' => 59250],
];

// --- Start log ---
ipn_log('================ IPN HIT ================');
ipn_log('REMOTE_ADDR=' . ($_SERVER['REMOTE_ADDR'] ?? ''));
ipn_log('REQUEST_URI=' . ($_SERVER['REQUEST_URI'] ?? ''));
ipn_log('POST_META status=' . (isset($_POST['payment_status']) ? $_POST['payment_status'] : '') . ' txn_id=' . (isset($_POST['txn_id']) ? $_POST['txn_id'] : ''));

// Load Config
$cfg = loadConfigurations('donation.paypal');
if(!is_array($cfg)) {
    ipn_log('ERROR: loadConfigurations(donation.paypal) failed');
    header("HTTP/1.1 500 Internal Server Error");
    die();
}

$enable_sandbox    = (int)$cfg['paypal_enable_sandbox'];
$seller_email      = (string)$cfg['paypal_email'];
$expectedCurrency  = isset($cfg['paypal_currency']) ? (string)$cfg['paypal_currency'] : '';

ipn_log("CFG sandbox=$enable_sandbox currency=$expectedCurrency");

// IPN verify
$ipn = new PaypalIPN();
if($enable_sandbox === 1) $ipn->useSandbox();

$verified = $ipn->verifyIPN();
ipn_log('VERIFY=' . ($verified ? 'VERIFIED' : 'FAILED'));

$paypal_ipn_status = "VERIFICATION FAILED";

if($verified) {
    try {
        // Fields (forzamos utf8 por si viene windows-1252)
        $post = utf8ize($_POST);

        $receiver_email   = isset($post['receiver_email']) ? (string)$post['receiver_email'] : '';
        $payment_status   = isset($post['payment_status']) ? (string)$post['payment_status'] : '';
        $payment_amount   = isset($post['mc_gross']) ? (float)$post['mc_gross'] : 0.0;
        $payment_currency = isset($post['mc_currency']) ? (string)$post['mc_currency'] : '';
        $txn_id           = isset($post['txn_id']) ? (string)$post['txn_id'] : '';
        $payer_email      = isset($post['payer_email']) ? (string)$post['payer_email'] : '';
        $item_number      = isset($post['item_number']) ? (string)$post['item_number'] : '';
        $item_name        = isset($post['item_name']) ? (string)$post['item_name'] : '';
        $custom_raw       = isset($post['custom']) ? (string)$post['custom'] : '';

        ipn_log("FIELDS status=$payment_status amount=$payment_amount currency=$payment_currency txn_id=$txn_id item_number=$item_number");

        // Receiver validation
        if($receiver_email === '') throw new Exception('MISSING receiver_email');
        if(strtolower($receiver_email) !== strtolower($seller_email)) throw new Exception('RECEIVER EMAIL MISMATCH');

        // Currency validation
        if($expectedCurrency !== '' && strtoupper($payment_currency) !== strtoupper($expectedCurrency)) {
            throw new Exception('CURRENCY MISMATCH');
        }

        // Parse and verify custom: userid|SITE|REALM|PACKID|IP|SIGNATURE
        $user_id = 0;
		$site_id = '';
		$realm_id = '';
        $pack_id = '';
        $user_ip = '';
		$custom_signature = '';

        $parts = explode('|', $custom_raw);
		if(count($parts) !== 6) throw new Exception('INVALID CUSTOM FORMAT');
		$user_id = (int)$parts[0];
		$site_id = strtoupper(trim($parts[1]));
		$realm_id = strtolower(trim($parts[2]));
		$pack_id = trim($parts[3]);
		$user_ip = trim($parts[4]);
		$custom_signature = trim($parts[5]);

		if($site_id !== 'S6') throw new Exception('INVALID SITE');
		if(!NexoraRealm::exists($realm_id)) throw new Exception('INVALID REALM');
		if($user_id <= 0 || !isset($PACKAGES[$pack_id])) throw new Exception('INVALID ACCOUNT OR PACK');
		$expectedSignature = nexoraPaypalCustomSignature($user_id, $site_id, $realm_id, $pack_id, $user_ip, $item_number);
		if(!hash_equals($expectedSignature, $custom_signature)) throw new Exception('INVALID CUSTOM SIGNATURE');

		NexoraRealm::selectPublic($realm_id, false);
		ipn_log("PARSED user_id=$user_id realm=$realm_id pack_id=$pack_id");

		$common = new common($realm_id);
		if($txn_id === '') throw new Exception('MISSING TXN ID');

        if($payment_status === 'Completed') {

            if($user_id <= 0) throw new Exception('invalid userid');

            // account info
			$Account = new Account($realm_id);
            $accountInfo = $Account->accountInformation($user_id);
            if(!is_array($accountInfo)) throw new Exception("invalid account");

            // Decide credits
            $add_credits = 0;

            if($pack_id !== '' && isset($PACKAGES[$pack_id])) {
                $expectedUsd = (float)$PACKAGES[$pack_id]['usd'];

                if(!moneyEquals($payment_amount, $expectedUsd, 0.01)) {
                    throw new Exception("AMOUNT MISMATCH FOR PACK (expected $expectedUsd)");
                }

                $add_credits = (int)$PACKAGES[$pack_id]['credits'];
                ipn_log("PACK OK => add_credits=$add_credits");
            } else {
                $rate = isset($cfg['paypal_conversion_rate']) ? (float)$cfg['paypal_conversion_rate'] : 0.0;
                if($rate <= 0) throw new Exception("invalid conversion rate (fallback)");
                $add_credits = (int)floor($payment_amount * $rate);
                ipn_log("FALLBACK => rate=$rate add_credits=$add_credits");
            }

            // Credit system
            $creditSystem = new CreditSystem();
            $creditSystem->setConfigId($cfg['credit_config']);
            $configSettings = $creditSystem->showConfigs(true);

            $creditIdentifier = null;
            switch($configSettings['config_user_col_id']) {
                case 'userid':
                    $creditIdentifier = $accountInfo[_CLMN_MEMBID_];
                    break;
                case 'username':
                    $creditIdentifier = $accountInfo[_CLMN_USERNM_];
                    break;
                case 'email':
                    $creditIdentifier = $accountInfo[_CLMN_EMAIL_];
                    break;
                default:
                    throw new Exception("invalid identifier");
            }
            $creditSystem->setIdentifier($creditIdentifier);

            $_GET['page'] = 'api';
            $_GET['subpage'] = 'paypal';

            // Balance previo solo para observabilidad; la acreditación real ocurre en una sola transacción SQL.
            $oldBalance = try_get_credits_balance($creditSystem);
            ipn_log("BALANCE old=" . (is_null($oldBalance) ? 'NULL' : $oldBalance));

            $economy = new NexoraEconomy($realm_id);
            $paymentResult = $economy->paypalCredit(
                $txn_id,
                $user_id,
                $payment_amount,
                $payer_email,
                $item_number,
                $cfg['credit_config'],
                $creditIdentifier,
                $add_credits
            );
            if(!empty($paymentResult['duplicate'])) {
                ipn_log("DUPLICATE TXN IGNORED ATOMICALLY txn_id=$txn_id realm=$realm_id");
                header("HTTP/1.1 200 OK");
                exit;
            }
            $newBalance = isset($paymentResult['balance']) ? $paymentResult['balance'] : try_get_credits_balance($creditSystem);
            ipn_log("CREDITS + TRANSACTION COMMITTED => $add_credits balance=" . (is_null($newBalance) ? 'NULL' : $newBalance));
            $paypal_ipn_status = "Completed Successfully";

            // Optional IP geolocation. Disabled by default to avoid disclosing player IPs to third parties.
            $ipInfo = null;
            $localConfig = nexoraLocalConfig();
            $geoLookupEnabled = !empty($localConfig['NEXORA_PAYPAL_GEOLOOKUP']);
            if($geoLookupEnabled && $user_ip !== '' && $user_ip !== '0.0.0.0') {
                $ipInfo = http_get_json_with_debug('https://ipwho.is/' . urlencode($user_ip), 8);
                ipn_log("IPWHOIS " . ($ipInfo ? "OK" : "FAILED"));
            }

            // Discord fields
            $accEmail = isset($accountInfo[_CLMN_EMAIL_]) ? (string)$accountInfo[_CLMN_EMAIL_] : '';
            $accUser  = isset($accountInfo[_CLMN_USERNM_]) ? (string)$accountInfo[_CLMN_USERNM_] : '';

            $plain = "✅ PayPal Completed | UserID: $user_id | Pack: $pack_id | +$add_credits | $payment_amount $payment_currency";

            $fields = [
                ['name' => 'Status', 'value' => $payment_status, 'inline' => true],
                ['name' => 'Amount', 'value' => '$' . number_format($payment_amount, 2, '.', '') . ' ' . $payment_currency, 'inline' => true],
				['name' => 'Pack', 'value' => ($pack_id !== '' ? $pack_id : 'FALLBACK'), 'inline' => true],
				['name' => 'Realm', 'value' => NexoraRealm::label($realm_id), 'inline' => true],
                ['name' => 'Credits Added', 'value' => (string)$add_credits, 'inline' => true],
                ['name' => 'Old → New', 'value' => (is_null($oldBalance) || is_null($newBalance)) ? 'N/A' : ($oldBalance . ' → ' . $newBalance), 'inline' => true],
                ['name' => 'UserID', 'value' => (string)$user_id, 'inline' => true],
                ['name' => 'Account Username', 'value' => ($accUser !== '' ? $accUser : 'N/A'), 'inline' => true],
                ['name' => 'Account Email', 'value' => ($accEmail !== '' ? $accEmail : 'N/A'), 'inline' => true],
                ['name' => 'PayPal Payer Email', 'value' => ($payer_email !== '' ? $payer_email : 'N/A'), 'inline' => true],
                ['name' => 'TXN ID', 'value' => ($txn_id !== '' ? $txn_id : 'N/A'), 'inline' => false],
                ['name' => 'Order/Item Number', 'value' => ($item_number !== '' ? $item_number : 'N/A'), 'inline' => false],
                ['name' => 'Item Name', 'value' => ($item_name !== '' ? $item_name : 'N/A'), 'inline' => false],
            ];

            if($user_ip !== '') {
                $fields[] = ['name' => 'User IP', 'value' => $user_ip, 'inline' => true];
            }

            // Parse ipwho.is
            if(is_array($ipInfo) && isset($ipInfo['success']) && $ipInfo['success'] == true) {
                $geo = [];
                if(!empty($ipInfo['city'])) $geo[] = $ipInfo['city'];
                if(!empty($ipInfo['region'])) $geo[] = $ipInfo['region'];
                if(!empty($ipInfo['country'])) $geo[] = $ipInfo['country'];
                $geoTxt = $geo ? implode(', ', $geo) : 'N/A';

                $fields[] = ['name' => 'IP Location', 'value' => $geoTxt, 'inline' => false];
                $fields[] = ['name' => 'ISP', 'value' => ($ipInfo['connection']['isp'] ?? 'N/A'), 'inline' => false];
                $fields[] = ['name' => 'Org', 'value' => ($ipInfo['connection']['org'] ?? 'N/A'), 'inline' => false];
                $fields[] = ['name' => 'ASN', 'value' => ($ipInfo['connection']['asn'] ?? 'N/A'), 'inline' => true];
            }

            send_discord_webhook('✅ PayPal Payment Completed', $fields, [
                'color' => 3066993,
                'footer' => 'WebEngine IPN • ' . date('Y-m-d H:i:s')
            ], $plain);

        } else {
            ipn_log("NOT COMPLETED => $payment_status");
			$reversalStatuses = array('Reversed', 'Refunded', 'Chargeback');
			$isReversal = in_array($payment_status, $reversalStatuses, true);

			if($isReversal && $user_id > 0) {
				$economy = new NexoraEconomy($realm_id);
				$reversal = $economy->paypalReverse($item_number, $user_id, $payment_status);
				if(!empty($reversal['matched'])) {
					ipn_log("PAYMENT REVERSED + ACCOUNT BLOCKED user_id=$user_id order=$item_number realm=$realm_id");
				} else {
					ipn_log("REVERSAL DID NOT MATCH A CREDITED ORDER; NO ACCOUNT ACTION order=$item_number realm=$realm_id");
				}
			} else {
				ipn_log("NO ACCOUNT ACTION FOR STATUS=$payment_status");
			}

            $plain = "⚠️ PayPal NOT Completed | Status: $payment_status | UserID: $user_id | Amount: $payment_amount $payment_currency";

            $fields = [
                ['name' => 'Status', 'value' => $payment_status, 'inline' => true],
                ['name' => 'Amount', 'value' => '$' . number_format($payment_amount, 2, '.', '') . ' ' . $payment_currency, 'inline' => true],
                ['name' => 'UserID', 'value' => (string)$user_id, 'inline' => true],
                ['name' => 'PayPal Payer Email', 'value' => ($payer_email !== '' ? $payer_email : 'N/A'), 'inline' => true],
                ['name' => 'TXN ID', 'value' => (isset($post['txn_id']) ? (string)$post['txn_id'] : 'N/A'), 'inline' => false],
                ['name' => 'Order/Item Number', 'value' => ($item_number !== '' ? $item_number : 'N/A'), 'inline' => false],
            ];

            send_discord_webhook('⚠️ PayPal Payment NOT Completed', $fields, [
                'color' => 15158332,
                'footer' => 'WebEngine IPN • ' . date('Y-m-d H:i:s')
            ], $plain);
        }

    } catch(Exception $ex) {
        $paypal_ipn_status = $ex->getMessage();
        ipn_log("EXCEPTION: " . $paypal_ipn_status);

        $plain = "❌ PayPal IPN Error | " . $paypal_ipn_status;

        send_discord_webhook('❌ PayPal IPN Error', [
            ['name' => 'Error', 'value' => $paypal_ipn_status, 'inline' => false],
            ['name' => 'TXN ID', 'value' => (isset($_POST['txn_id']) ? (string)$_POST['txn_id'] : 'N/A'), 'inline' => false],
        ], [
            'color' => 15105570,
            'footer' => 'WebEngine IPN • ' . date('Y-m-d H:i:s')
        ], $plain);
    }
} else {
    ipn_log("NOT VERIFIED");
}

header("HTTP/1.1 200 OK");
ipn_log("DONE status=" . $paypal_ipn_status);
