<?php
/**
 * strongswan_profile_extended_order.php
 * - 만료 프로필 연장 견적/주문/승인 처리 단일 파일
 * - PHP 7.4 호환
 */

include_once('./_common.php');

global $g5;

if (!$link) {
	$link = $g5['connect_db'];
}

$mysqli = ($link instanceof mysqli) ? $link : null;

header('X-Content-Type-Options: nosniff');
header('Access-Control-Allow-Origin: *');

date_default_timezone_set('Asia/Seoul');

// 서버 반영 여부 점검용 버전 마커 (배포 시 갱신)
define('VPN_EXTEND_ORDER_VERSION', '2026-04-19-order-history-business-no-r6');
header('X-VPNLink-Extend-Version: ' . VPN_EXTEND_ORDER_VERSION);

$ikev2ProvisionLib = (defined('G5_PATH') ? G5_PATH . '/mylib/ikev2_provision.lib.php' : (__DIR__ . '/ikev2_provision.lib.php'));
if (is_file($ikev2ProvisionLib)) {
	require_once $ikev2ProvisionLib;
}

function extend_debug_log($message, $context = array())
{
	// debug logging disabled
}

if (isset($_REQUEST['vcheck']) && (string)$_REQUEST['vcheck'] === '1') {
	header('Content-Type: application/json; charset=UTF-8');
	echo json_encode(array(
		'result' => 'success',
		'version' => VPN_EXTEND_ORDER_VERSION,
		'file' => 'strongswan_profile_extended_order.php',
		'time' => date('Y-m-d H:i:s'),
	), JSON_UNESCAPED_UNICODE);
	exit;
}

function is_api_request()
{
	if (isset($_REQUEST['api']) && $_REQUEST['api'] === '1') {
		return true;
	}

	$response = isset($_REQUEST['response']) ? strtolower(trim((string)$_REQUEST['response'])) : '';
	if ($response === 'json') {
		return true;
	}

	$xhr = isset($_SERVER['HTTP_X_REQUESTED_WITH']) ? strtolower((string)$_SERVER['HTTP_X_REQUESTED_WITH']) : '';
	if ($xhr === 'xmlhttprequest') {
		return true;
	}

	$contentType = isset($_SERVER['CONTENT_TYPE']) ? strtolower((string)$_SERVER['CONTENT_TYPE']) : '';
	if (strpos($contentType, 'application/json') !== false) {
		return true;
	}

	return false;
}

function out_json($arr)
{
	header('Content-Type: application/json; charset=UTF-8');
	echo json_encode($arr, JSON_UNESCAPED_UNICODE);
	exit;
}

function redirect_with_html_fallback($url)
{
	if (!headers_sent()) {
		header('Location: ' . $url);
	}
	echo '<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=' . h($url) . '"></head>'
		. '<body><script>location.replace(' . json_encode($url) . ');</script></body></html>';
	exit;
}

function success_json($msg, $data = array())
{
	$res = array(
		'result' => 'success',
		'msg' => $msg,
		'status' => 'success',
		'message' => $msg,
	);
	if (!empty($data)) {
		$res = array_merge($res, $data);
	}
	out_json($res);
}

function fail_json($msg, $data = array())
{
	$res = array(
		'result' => 'failure',
		'msg' => $msg,
		'status' => 'failure',
		'message' => $msg,
	);
	if (!empty($data)) {
		$res = array_merge($res, $data);
	}
	out_json($res);
}

function h($s)
{
	return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}

function read_json_request_body_assoc()
{
	static $loaded = false;
	static $data = array();

	if ($loaded) {
		return $data;
	}

	$loaded = true;
	$raw = @file_get_contents('php://input');
	if (!is_string($raw) || trim($raw) === '') {
		return $data;
	}

	$contentType = isset($_SERVER['CONTENT_TYPE']) ? strtolower(trim((string)$_SERVER['CONTENT_TYPE'])) : '';
	if (strpos($contentType, 'application/json') === false && strpos(ltrim($raw), '{') !== 0) {
		return $data;
	}

	$decoded = json_decode($raw, true);
	if (is_array($decoded)) {
		$data = $decoded;
	}

	return $data;
}

function req($key, $default = '', $aliases = array())
{
	$key = (string)$key;
	$aliases = is_array($aliases) ? $aliases : array();
	$candidates = array_merge(array($key), $aliases);

	foreach ($candidates as $candidate) {
		$candidate = (string)$candidate;
		if ($candidate === '') {
			continue;
		}
		if (isset($_POST[$candidate])) {
			return trim((string)$_POST[$candidate]);
		}
		if (isset($_GET[$candidate])) {
			return trim((string)$_GET[$candidate]);
		}
	}

	$jsonBody = read_json_request_body_assoc();
	foreach ($candidates as $candidate) {
		$candidate = (string)$candidate;
		if ($candidate === '' || !array_key_exists($candidate, $jsonBody)) {
			continue;
		}
		$val = $jsonBody[$candidate];
		if (is_array($val) || is_object($val) || $val === null) {
			continue;
		}
		return trim((string)$val);
	}

	return $default;
}

function sql_esc($value)
{
	$value = (string)$value;
	if (function_exists('sql_real_escape_string')) {
		return sql_real_escape_string($value);
	}
	return addslashes($value);
}

function extend_load_shop_sync_secret()
{
	$secret = '';

	if (isset($_SERVER['IKEV2_SHOP_SYNC_SHARED_SECRET'])) {
		$secret = trim((string)$_SERVER['IKEV2_SHOP_SYNC_SHARED_SECRET']);
	}
	if ($secret === '' && isset($_ENV['IKEV2_SHOP_SYNC_SHARED_SECRET'])) {
		$secret = trim((string)$_ENV['IKEV2_SHOP_SYNC_SHARED_SECRET']);
	}
	if ($secret === '' && function_exists('getenv')) {
		$envVal = getenv('IKEV2_SHOP_SYNC_SHARED_SECRET');
		if (is_string($envVal)) {
			$secret = trim($envVal);
		}
	}

	if ($secret === '') {
		$envPath = dirname(__DIR__) . '/.env';
		if (is_file($envPath) && is_readable($envPath)) {
			$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
			if (is_array($lines)) {
				foreach ($lines as $line) {
					$line = trim((string)$line);
					if ($line === '' || strpos($line, '#') === 0) {
						continue;
					}
					$pos = strpos($line, '=');
					if ($pos === false) {
						continue;
					}
					$k = trim(substr($line, 0, $pos));
					if ($k !== 'IKEV2_SHOP_SYNC_SHARED_SECRET') {
						continue;
					}
					$v = trim(substr($line, $pos + 1));
					if ((strlen($v) >= 2) && (($v[0] === '"' && substr($v, -1) === '"') || ($v[0] === '\'' && substr($v, -1) === '\''))) {
						$v = substr($v, 1, -1);
					}
					$secret = trim((string)$v);
					break;
				}
			}
		}
	}

	return $secret;
}

function extend_extract_gateway_base($rawGateway)
{
	$rawGateway = trim((string)$rawGateway);
	if ($rawGateway === '') {
		return '';
	}

	if (preg_match('#^https?://#i', $rawGateway)) {
		$parsed = @parse_url($rawGateway);
		if (!is_array($parsed) || empty($parsed['host'])) {
			return '';
		}
		$scheme = isset($parsed['scheme']) ? strtolower((string)$parsed['scheme']) : 'http';
		$host = trim((string)$parsed['host']);
		if ($host === '') {
			return '';
		}
		$port = isset($parsed['port']) ? ':' . (int)$parsed['port'] : '';
		return $scheme . '://' . $host . $port;
	}

	$hostPart = preg_split('#[/?]#', $rawGateway, 2);
	$host = trim((string)$hostPart[0]);
	if ($host === '') {
		return '';
	}
	return 'http://' . $host;
}

function extend_default_vpn_users_endpoint($profileGateway = '')
{
	$gateway = extend_extract_gateway_base($profileGateway);
	if ($gateway !== '') {
		return $gateway . '/IKEv2SellerOffice4U/vpn_users.php';
	}

	$host = isset($_SERVER['HTTP_HOST']) ? trim((string)$_SERVER['HTTP_HOST']) : '';
	if ($host === '') {
		return '';
	}
	$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
	return $scheme . '://' . $host . '/IKEv2SellerOffice4U/vpn_users.php';
}

function extend_build_vpn_users_endpoints($profileGateway = '')
{
	$candidates = array();

	$appendPair = function ($base) use (&$candidates) {
		$base = rtrim((string)$base, '/');
		if ($base === '') {
			return;
		}
		$candidates[] = $base . '/IKEv2SellerOffice4U/vpn_users.php';
		$candidates[] = $base . '/IKEv2SellerOffice/vpn_users.php';
	};

	$rawGateway = trim((string)$profileGateway);
	if ($rawGateway !== '') {
		if (preg_match('#^https?://#i', $rawGateway)) {
			$parsed = @parse_url($rawGateway);
			if (is_array($parsed) && !empty($parsed['host'])) {
				$scheme = isset($parsed['scheme']) ? strtolower((string)$parsed['scheme']) : 'http';
				$host = trim((string)$parsed['host']);
				$port = isset($parsed['port']) ? ':' . (int)$parsed['port'] : '';
				if ($host !== '') {
					$primary = $scheme . '://' . $host . $port;
					$secondary = (($scheme === 'https') ? 'http' : 'https') . '://' . $host . $port;
					$appendPair($primary);
					$appendPair($secondary);
				}
			}
		} else {
			$hostPart = preg_split('#[/?]#', $rawGateway, 2);
			$host = trim((string)$hostPart[0]);
			if ($host !== '') {
				// IP 서버는 http:// 우선, 도메인 서버는 https:// 우선
				if (function_exists('vpnlink_ikev2_is_ip_address') && vpnlink_ikev2_is_ip_address($host)) {
					$appendPair('http://' . $host);
					$appendPair('https://' . $host);
				} else {
					$appendPair('https://' . $host);
					$appendPair('http://' . $host);
				}
			}
		}
	}

	$defaultEndpoint = extend_default_vpn_users_endpoint($profileGateway);
	if ($defaultEndpoint !== '') {
		$candidates[] = $defaultEndpoint;
	}

	$uniq = array();
	$out = array();
	foreach ($candidates as $endpoint) {
		$endpoint = trim((string)$endpoint);
		if ($endpoint === '' || isset($uniq[$endpoint])) {
			continue;
		}
		$uniq[$endpoint] = true;
		$out[] = $endpoint;
	}

	return $out;
}

function extend_call_vpn_users_extend_absolute($vpnUserId, $mbId, $profileGateway, $targetDatetime, $memo = '')
{
	$vpnUserId = trim((string)$vpnUserId);
	$mbId = trim((string)$mbId);
	$targetDatetime = trim((string)$targetDatetime);
	$memo = trim((string)$memo);

	if ($vpnUserId === '') {
		return array('attempted' => false, 'status' => 'skip_empty_user_id', 'success' => false);
	}
	if ($targetDatetime === '') {
		return array('attempted' => false, 'status' => 'skip_empty_target_datetime', 'success' => false);
	}

	$endpoints = extend_build_vpn_users_endpoints($profileGateway);
	if (empty($endpoints)) {
		return array('attempted' => false, 'status' => 'skip_no_endpoint', 'success' => false);
	}

	$secret = extend_load_shop_sync_secret();
	if ($secret === '') {
		return array('attempted' => false, 'status' => 'skip_no_shared_secret', 'endpoint' => isset($endpoints[0]) ? $endpoints[0] : '', 'success' => false);
	}

	$postFields = array(
		'action' => 'extend_expire',
		'user_id' => $vpnUserId,
		'extend_mode' => 'absolute',
		'target_datetime' => $targetDatetime,
		'sync_origin' => 'shop_backend',
		'reason' => 'app_extend_approve',
		'mb_id' => $mbId,
	);
	if ($memo !== '') {
		$postFields['memo'] = $memo;
	}

	$payload = http_build_query($postFields, '', '&');
	$signature = hash_hmac('sha256', $payload, $secret);
	$postFields['sync_signature'] = $signature;
	$requestPayload = http_build_query($postFields, '', '&');

	$headers = array(
		'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
		'Accept: application/json',
		'X-Requested-With: XMLHttpRequest',
		'X-VPN-Sync-Origin: shop_backend',
		'X-VPN-Sync-Signature: ' . $signature,
	);

	$sendRequest = function ($targetEndpoint) use ($headers, $requestPayload) {
		$raw = '';
		$httpCode = 0;
		$transportError = '';
		$location = '';

		if (function_exists('curl_init')) {
			$ch = curl_init($targetEndpoint);
			curl_setopt($ch, CURLOPT_POST, true);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
			curl_setopt($ch, CURLOPT_HEADER, true);
			curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 4);
			curl_setopt($ch, CURLOPT_TIMEOUT, 8);
			curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $requestPayload);

			$rawResp = curl_exec($ch);
			if ($rawResp === false) {
				$transportError = (string)curl_error($ch);
			} else {
				$headerSize = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE);
				$headerBlob = (string)substr((string)$rawResp, 0, $headerSize);
				$raw = (string)substr((string)$rawResp, $headerSize);
				if (preg_match('/\r?\nLocation:\s*([^\r\n]+)/i', $headerBlob, $mLoc)) {
					$location = trim((string)$mLoc[1]);
				}
			}
			$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
			curl_close($ch);
		} else {
			$ctx = stream_context_create(array(
				'http' => array(
					'method' => 'POST',
					'header' => implode("\r\n", $headers),
					'content' => $requestPayload,
					'timeout' => 8,
					'ignore_errors' => true,
				),
			));
			$rawResp = @file_get_contents($targetEndpoint, false, $ctx);
			if ($rawResp === false) {
				$transportError = 'file_get_contents_failed';
				$raw = '';
			} else {
				$raw = (string)$rawResp;
			}
			if (isset($http_response_header) && is_array($http_response_header)) {
				foreach ($http_response_header as $line) {
					if ($httpCode === 0 && preg_match('/^HTTP\/\S+\s+(\d{3})/i', $line, $m)) {
						$httpCode = (int)$m[1];
					}
					if ($location === '' && preg_match('/^Location:\s*(.+)$/i', $line, $mLoc2)) {
						$location = trim((string)$mLoc2[1]);
					}
				}
			}
		}

		return array(
			'endpoint' => $targetEndpoint,
			'http_code' => $httpCode,
			'transport_error' => $transportError,
			'raw' => $raw,
			'location' => $location,
		);
	};

	$attemptEndpoints = $endpoints;
	$seen = array();
	$attemptTrace = array();
	$last = array(
		'endpoint' => isset($endpoints[0]) ? $endpoints[0] : '',
		'http_code' => 0,
		'transport_error' => '',
		'raw' => '',
		'location' => '',
	);
	$ok = false;
	$decoded = null;
	$isJson = false;

	$maxAttempts = 6;
	while (!empty($attemptEndpoints) && count($attemptTrace) < $maxAttempts) {
		$targetEndpoint = array_shift($attemptEndpoints);
		if ($targetEndpoint === '' || isset($seen[$targetEndpoint])) {
			continue;
		}
		$seen[$targetEndpoint] = true;

		$last = $sendRequest($targetEndpoint);
		$attemptTrace[] = array(
			'endpoint' => $last['endpoint'],
			'http_code' => (int)$last['http_code'],
			'transport_error' => (string)$last['transport_error'],
			'location' => (string)$last['location'],
		);

		$decoded = json_decode((string)$last['raw'], true);
		$isJson = is_array($decoded);
		$remoteSuccess = false;
		if ($isJson) {
			$statusVal = strtolower(trim((string)($decoded['status'] ?? '')));
			$resultVal = strtolower(trim((string)($decoded['result'] ?? '')));
			$remoteSuccess = !empty($decoded['success']) || !empty($decoded['ok']) || $statusVal === 'success' || $statusVal === 'ok' || $resultVal === 'success';
		}

		if ($last['transport_error'] === '' && $last['http_code'] >= 200 && $last['http_code'] < 300 && $remoteSuccess) {
			$ok = true;
			break;
		}

		if ($last['http_code'] >= 300 && $last['http_code'] < 400) {
			$location = trim((string)$last['location']);
			if ($location !== '') {
				if (preg_match('#^https?://#i', $location)) {
					$attemptEndpoints[] = $location;
				} else {
					$base = @parse_url($targetEndpoint);
					if (is_array($base) && !empty($base['scheme']) && !empty($base['host'])) {
						$portPart = isset($base['port']) ? (':' . (int)$base['port']) : '';
						$prefix = strtolower((string)$base['scheme']) . '://' . $base['host'] . $portPart;
						if (substr($location, 0, 1) === '/') {
							$attemptEndpoints[] = $prefix . $location;
						} else {
							$basePath = isset($base['path']) ? (string)$base['path'] : '/';
							$dir = rtrim(str_replace('\\', '/', dirname($basePath)), '/');
							$attemptEndpoints[] = $prefix . ($dir === '' ? '' : $dir) . '/' . $location;
						}
					}
				}
			}
		}
	}

	$status = $ok ? 'ok' : 'unknown';
	if (!$ok) {
		$err = strtolower((string)$last['transport_error']);
		$loc = strtolower((string)$last['location']);
		if ($last['http_code'] >= 300 && $last['http_code'] < 400 && $loc !== '' && strpos($loc, 'login.php') !== false) {
			$status = 'auth_redirect';
		} else if ($err !== '' && strpos($err, 'connection refused') !== false) {
			$status = 'transport_connect_refused';
		} else if ($err !== '') {
			$status = 'transport_error';
		} else if ($last['http_code'] >= 300 && $last['http_code'] < 400) {
			$status = 'redirect_unresolved';
		} else if ($last['http_code'] >= 400) {
			$status = 'http_error';
		} else if ($last['http_code'] >= 200 && $last['http_code'] < 300) {
			$status = 'response_not_success';
		}
	}

	return array(
		'attempted' => true,
		'success' => $ok,
		'endpoint' => isset($last['endpoint']) ? (string)$last['endpoint'] : '',
		'http_code' => isset($last['http_code']) ? (int)$last['http_code'] : 0,
		'transport_error' => isset($last['transport_error']) ? (string)$last['transport_error'] : '',
		'location' => isset($last['location']) ? (string)$last['location'] : '',
		'status' => $status,
		'attempt_trace' => $attemptTrace,
		'json' => $isJson ? $decoded : null,
		'raw' => (strlen((string)(isset($last['raw']) ? $last['raw'] : '')) > 400) ? substr((string)$last['raw'], 0, 400) . '...[truncated]' : (string)(isset($last['raw']) ? $last['raw'] : ''),
	);
}

function extend_call_vpn_users_update_user($vpnUserId, $mbId, $profileGateway, $targetDatetime, $memo)
{
	$vpnUserId = trim((string)$vpnUserId);
	$mbId = trim((string)$mbId);
	$targetDatetime = trim((string)$targetDatetime);
	$memo = trim((string)$memo);

	if ($vpnUserId === '' || $targetDatetime === '') {
		return array('attempted' => false, 'status' => 'skip_invalid_params', 'success' => false);
	}

	$endpoints = extend_build_vpn_users_endpoints($profileGateway);
	if (empty($endpoints)) {
		return array('attempted' => false, 'status' => 'skip_no_endpoint', 'success' => false);
	}

	$secret = extend_load_shop_sync_secret();
	if ($secret === '') {
		return array('attempted' => false, 'status' => 'skip_no_shared_secret', 'endpoint' => isset($endpoints[0]) ? $endpoints[0] : '', 'success' => false);
	}

	$postFields = array(
		'action' => 'update_user',
		'user_id' => $vpnUserId,
		'expire' => $targetDatetime,
		'sync_origin' => 'shop_backend',
		'reason' => 'app_extend_approve_force_memo',
		'mb_id' => $mbId,
	);
	if ($memo !== '') {
		$postFields['memo'] = $memo;
	}

	$payload = http_build_query($postFields, '', '&');
	$signature = hash_hmac('sha256', $payload, $secret);
	$postFields['sync_signature'] = $signature;
	$requestPayload = http_build_query($postFields, '', '&');

	$headers = array(
		'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
		'Accept: application/json',
		'X-Requested-With: XMLHttpRequest',
		'X-VPN-Sync-Origin: shop_backend',
		'X-VPN-Sync-Signature: ' . $signature,
	);

	$attemptTrace = array();
	$seen = array();
	$last = array('endpoint' => '', 'http_code' => 0, 'transport_error' => '', 'raw' => '');
	$ok = false;

	foreach ($endpoints as $endpoint) {
		$endpoint = trim((string)$endpoint);
		if ($endpoint === '' || isset($seen[$endpoint])) {
			continue;
		}
		$seen[$endpoint] = true;

		$resp = array('endpoint' => $endpoint, 'http_code' => 0, 'transport_error' => '', 'raw' => '');
		if (function_exists('curl_init')) {
			$ch = curl_init($endpoint);
			curl_setopt($ch, CURLOPT_POST, true);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
			curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 4);
			curl_setopt($ch, CURLOPT_TIMEOUT, 8);
			curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $requestPayload);
			$raw = curl_exec($ch);
			if ($raw === false) {
				$resp['transport_error'] = (string)curl_error($ch);
			} else {
				$resp['raw'] = (string)$raw;
			}
			$resp['http_code'] = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
			curl_close($ch);
		} else {
			$ctx = stream_context_create(array(
				'http' => array(
					'method' => 'POST',
					'header' => implode("\r\n", $headers),
					'content' => $requestPayload,
					'timeout' => 8,
					'ignore_errors' => true,
				),
			));
			$raw = @file_get_contents($endpoint, false, $ctx);
			if ($raw === false) {
				$resp['transport_error'] = 'file_get_contents_failed';
			} else {
				$resp['raw'] = (string)$raw;
			}
			if (isset($http_response_header) && is_array($http_response_header)) {
				foreach ($http_response_header as $line) {
					if (preg_match('/^HTTP\/\S+\s+(\d{3})/i', $line, $m)) {
						$resp['http_code'] = (int)$m[1];
						break;
					}
				}
			}
		}

		$last = $resp;
		$attemptTrace[] = array(
			'endpoint' => $resp['endpoint'],
			'http_code' => (int)$resp['http_code'],
			'transport_error' => (string)$resp['transport_error'],
		);

		$decoded = json_decode((string)$resp['raw'], true);
		$remoteSuccess = is_array($decoded)
			&& (!empty($decoded['success']) || !empty($decoded['ok']) || strtolower(trim((string)($decoded['status'] ?? ''))) === 'success' || strtolower(trim((string)($decoded['result'] ?? ''))) === 'success');

		if ($resp['transport_error'] === '' && $resp['http_code'] >= 200 && $resp['http_code'] < 300 && $remoteSuccess) {
			$ok = true;
			break;
		}
	}

	return array(
		'attempted' => !empty($attemptTrace),
		'success' => $ok,
		'endpoint' => isset($last['endpoint']) ? (string)$last['endpoint'] : '',
		'http_code' => isset($last['http_code']) ? (int)$last['http_code'] : 0,
		'transport_error' => isset($last['transport_error']) ? (string)$last['transport_error'] : '',
		'attempt_trace' => $attemptTrace,
		'raw' => (strlen((string)(isset($last['raw']) ? $last['raw'] : '')) > 400) ? substr((string)$last['raw'], 0, 400) . '...[truncated]' : (string)(isset($last['raw']) ? $last['raw'] : ''),
		'status' => $ok ? 'ok' : 'update_user_failed',
	);
}

function extend_sync_vpn_server_secure_upsert($profile, $afterExpire, $memo, $triggerSource)
{
	$ret = array(
		'attempted' => false,
		'ok' => false,
		'endpoint' => '',
		'final_endpoint' => '',
		'attempted_endpoints' => array(),
		'error' => '',
		'message' => ''
	);

	if (!function_exists('vpnlink_ikev2_send_secure_payload')) {
		$ret['error'] = 'secure_payload_function_missing';
		return $ret;
	}

	$serverName = isset($profile['server_name']) ? trim((string)$profile['server_name']) : '';
	$vpnUserId = isset($profile['vpn_user_id']) ? trim((string)$profile['vpn_user_id']) : '';
	$vpnUserPwd = isset($profile['vpn_user_pwd']) ? trim((string)$profile['vpn_user_pwd']) : '';
	$odId = isset($profile['od_id']) ? trim((string)$profile['od_id']) : '';
	$memo = trim((string)$memo);
	$afterExpire = trim((string)$afterExpire);

	if ($serverName === '' || $vpnUserId === '' || $afterExpire === '') {
		$ret['error'] = 'secure_upsert_required_fields_missing';
		return $ret;
	}

	$endpointCandidates = array();
	$endpoint = '';
	if (function_exists('vpnlink_ikev2_build_endpoint')) {
		$endpoint = trim((string)vpnlink_ikev2_build_endpoint($serverName));
	}
	if ($endpoint !== '') {
		$endpointCandidates[] = $endpoint;
	}

	$serverNameRaw = trim((string)$serverName);
	$hasScheme = (bool)preg_match('#^https?://#i', $serverNameRaw);
	if (!$hasScheme && $serverNameRaw !== '') {
		$_isIpServer = (function_exists('vpnlink_ikev2_is_ip_address') && vpnlink_ikev2_is_ip_address($serverNameRaw));
		// IP 서버는 http:// 우선, 도메인 서버는 https:// 우선
		$fallbackCandidates = $_isIpServer ? array(
			'http://' . rtrim($serverNameRaw, '/') . '/mylib4ikev2svr/ikev2_ipsec.secrets_update.php',
			'http://' . rtrim($serverNameRaw, '/') . '/mylib/ikev2_ipsec.secrets_update.php',
			'https://' . rtrim($serverNameRaw, '/') . '/mylib4ikev2svr/ikev2_ipsec.secrets_update.php',
		) : array(
			'https://' . rtrim($serverNameRaw, '/') . '/mylib4ikev2svr/ikev2_ipsec.secrets_update.php',
			'http://' . rtrim($serverNameRaw, '/') . '/mylib4ikev2svr/ikev2_ipsec.secrets_update.php',
			'http://' . rtrim($serverNameRaw, '/') . '/mylib/ikev2_ipsec.secrets_update.php',
		);
		foreach ($fallbackCandidates as $candidateUrl) {
			if (!in_array($candidateUrl, $endpointCandidates, true)) {
				$endpointCandidates[] = $candidateUrl;
			}
		}
	}

	if (empty($endpointCandidates)) {
		$_defaultScheme = (function_exists('vpnlink_ikev2_is_ip_address') && vpnlink_ikev2_is_ip_address($serverName)) ? 'http' : 'https';
		$endpointCandidates[] = $_defaultScheme . '://' . rtrim($serverName, '/') . '/mylib4ikev2svr/ikev2_ipsec.secrets_update.php';
	}

	$payload = array(
		'operation' => 'upsert',
		'source' => 'auto_shop_order',
		'trigger_source' => trim((string)$triggerSource) !== '' ? trim((string)$triggerSource) : 'extended_order_secure_upsert',
		'admin' => 'shop_auto',
		'mb_id' => $vpnUserId,
		'od_id' => $odId,
		'vpn_user_id' => $vpnUserId,
		'expire' => $afterExpire
	);
	if ($vpnUserPwd !== '') {
		$payload['vpn_user_pw'] = $vpnUserPwd;
	}
	if ($memo !== '') {
		$payload['memo'] = $memo;
	}

	$ret['attempted'] = true;
	$ret['endpoint'] = isset($endpointCandidates[0]) ? (string)$endpointCandidates[0] : '';

	$lastRet = array('ok' => false, 'error' => 'sync_failed');
	foreach ($endpointCandidates as $candidateEndpoint) {
		$ret['attempted_endpoints'][] = (string)$candidateEndpoint;
		$sendRet = vpnlink_ikev2_send_secure_payload($candidateEndpoint, $payload);
		$lastRet = is_array($sendRet) ? $sendRet : array('ok' => false, 'error' => 'invalid_send_result');

		if (!empty($sendRet['ok'])) {
			$ret['ok'] = true;
			$ret['final_endpoint'] = (string)$candidateEndpoint;
			$ret['error'] = '';
			$ret['message'] = isset($sendRet['message']) ? (string)$sendRet['message'] : '';
			$ret['raw'] = $sendRet;
			return $ret;
		}

		$errorCode = isset($sendRet['error']) ? (string)$sendRet['error'] : '';
		$isTransportFailure = (strpos($errorCode, 'transport_error:') === 0 || $errorCode === 'http_status_0');
		if (!$isTransportFailure) {
			$ret['final_endpoint'] = (string)$candidateEndpoint;
			break;
		}
	}

	$ret['ok'] = false;
	$ret['error'] = isset($lastRet['error']) ? (string)$lastRet['error'] : 'sync_failed';
	$ret['message'] = isset($lastRet['message']) ? (string)$lastRet['message'] : '';
	$ret['raw'] = $lastRet;
	if ($ret['final_endpoint'] === '' && !empty($ret['attempted_endpoints'])) {
		$ret['final_endpoint'] = (string)$ret['attempted_endpoints'][count($ret['attempted_endpoints']) - 1];
	}

	return $ret;
}

function resolve_extend_period_day_hour($beforeExpire, $afterExpire)
{
	$beforeTs = strtotime((string)$beforeExpire);
	$afterTs = strtotime((string)$afterExpire);
	if ($beforeTs === false || $afterTs === false || $afterTs <= $beforeTs) {
		return array(0, 0);
	}

	$deltaSec = (int)($afterTs - $beforeTs);
	$deltaHours = (int)floor($deltaSec / 3600);
	$days = (int)floor($deltaHours / 24);
	$hours = (int)($deltaHours % 24);

	if ($days <= 0 && $hours <= 0) {
		$hours = 1;
	}

	return array($days, $hours);
}

function extend_build_shop_order_memo($mbId, $orderRow, $resolvedOrderContext, $afterExpire)
{
	$mbId = trim((string)$mbId);
	$afterExpire = trim((string)$afterExpire);
	$orderRow = is_array($orderRow) ? $orderRow : array();
	$resolvedOrderContext = is_array($resolvedOrderContext) ? $resolvedOrderContext : array();

	$mbName = '';
	if ($mbId !== '' && function_exists('get_member')) {
		$rowMember = get_member($mbId);
		if (is_array($rowMember) && isset($rowMember['mb_name'])) {
			$mbName = trim((string)$rowMember['mb_name']);
		}
	}

	$odId = trim((string)($resolvedOrderContext['od_id'] ?? ''));
	if ($odId === '') {
		$odId = trim((string)($orderRow['order_no'] ?? ''));
	}

	$itemName = trim((string)($orderRow['it_name'] ?? ''));
	$itId = trim((string)($resolvedOrderContext['it_id'] ?? ''));
	$extendDays = (int)($resolvedOrderContext['extend_days'] ?? 0);
	$extendHours = (int)($resolvedOrderContext['extend_hours'] ?? 0);
	$amount = (int)($orderRow['amount'] ?? 0);

	$periodText = $extendDays . '일' . $extendHours . '시간';

	$memoParts = array();
	if ($mbName !== '' && $mbId !== '') {
		$memoParts[] = '회원:' . $mbName . '(' . $mbId . ')';
	} elseif ($mbId !== '') {
		$memoParts[] = '회원ID:' . $mbId;
	}
	if ($odId !== '') {
		$memoParts[] = '주문번호:' . $odId;
	}
	if ($itemName !== '') {
		$memoParts[] = '상품명:' . $itemName;
	}
	$memoParts[] = '주문기간:' . $periodText;
	if ($afterExpire !== '') {
		$memoParts[] = '만료일시:' . $afterExpire;
	}
	$memoParts[] = '결제총액:' . $amount . '원';
	$memoParts[] = '실결제:0원';
	$memoParts[] = '포인트결제:' . $amount . 'P';

	$memo = trim(implode(' Œ ', $memoParts));

	$metaMarkers = array('[source:auto_shop_order]');
	if ($odId !== '') {
		$metaMarkers[] = '[od:' . $odId . ']';
	}
	if ($itId !== '') {
		$metaMarkers[] = '[it:' . $itId . ']';
	}

	if ($memo !== '') {
		$memo = trim($memo . ' ' . implode('', $metaMarkers));
	}

	return $memo;
}

function find_extend_order_context($mysqli, $mbId, $preferredOdId, $preferredItId, $forUpdate)
{
	global $g5;

	$mbId = trim((string)$mbId);
	$preferredOdId = trim((string)$preferredOdId);
	$preferredItId = (int)$preferredItId;
	$lockSql = '';

	if ($mbId === '' || $preferredItId <= 0) {
		return null;
	}

	if (!$mysqli) {
		extend_debug_log('find_extend_order_context_invalid_db', array('mb_id' => $mbId));
		return null;
	}

	$cart = null;
	$cartTable = isset($g5['g5_shop_cart_table']) ? (string)$g5['g5_shop_cart_table'] : 'g5_shop_cart';
	$cartCount = -1;
	$sqlCartCount = "SELECT COUNT(*) AS cnt FROM {$cartTable} WHERE it_id=? AND mb_id=?";
	$stmt = $mysqli->prepare($sqlCartCount);
	if ($stmt) {
		$itIdStr = (string)$preferredItId;
		$stmt->bind_param('ss', $itIdStr, $mbId);
		if ($stmt->execute()) {
			$res = $stmt->get_result();
			$rowCnt = $res ? $res->fetch_assoc() : null;
			$cartCount = ($rowCnt && isset($rowCnt['cnt'])) ? (int)$rowCnt['cnt'] : -1;
		}
		$stmt->close();
	}

	if ($preferredOdId !== '') {
		$sqlCartByOd = "SELECT * FROM {$g5['g5_shop_cart_table']} WHERE od_id=? AND it_id=? AND mb_id=? AND ct_status IN ('입금','완료') ORDER BY ct_id DESC LIMIT 1{$lockSql}";
		$stmt = $mysqli->prepare($sqlCartByOd);
		if ($stmt) {
			$itIdStr = (string)$preferredItId;
			$stmt->bind_param('sss', $preferredOdId, $itIdStr, $mbId);
			if ($stmt->execute()) {
				$res = $stmt->get_result();
				$cart = $res ? $res->fetch_assoc() : null;
			}
			$stmt->close();
		}
	}

	if (!$cart) {
		$sqlCartByStatus = "SELECT * FROM {$g5['g5_shop_cart_table']} WHERE it_id=? AND mb_id=? AND ct_status IN ('입금','완료') ORDER BY ct_id DESC LIMIT 1{$lockSql}";
		$stmt = $mysqli->prepare($sqlCartByStatus);
		if ($stmt) {
			$itIdStr = (string)$preferredItId;
			$stmt->bind_param('ss', $itIdStr, $mbId);
			if ($stmt->execute()) {
				$res = $stmt->get_result();
				$cart = $res ? $res->fetch_assoc() : null;
			}
			$stmt->close();
		}
	}
	if (!$cart) {
		$sqlCartAny = "SELECT * FROM {$g5['g5_shop_cart_table']} WHERE it_id=? AND mb_id=? ORDER BY ct_id DESC LIMIT 1{$lockSql}";
		$stmt = $mysqli->prepare($sqlCartAny);
		if ($stmt) {
			$itIdStr = (string)$preferredItId;
			$stmt->bind_param('ss', $itIdStr, $mbId);
			if ($stmt->execute()) {
				$res = $stmt->get_result();
				$cart = $res ? $res->fetch_assoc() : null;
			}
			$stmt->close();
		}
	}

	if (!$cart || !isset($cart['od_id']) || trim((string)$cart['od_id']) === '') {
		return null;
	}

	$odId = trim((string)$cart['od_id']);
	$order = null;
	$item = null;
	$sqlOrder = "SELECT od_id, od_receipt_point_for_extension_of_period, od_mod_history FROM {$g5['g5_shop_order_table']} WHERE od_id=? AND mb_id=? LIMIT 1{$lockSql}";
	$stmt = $mysqli->prepare($sqlOrder);
	if ($stmt) {
		$stmt->bind_param('ss', $odId, $mbId);
		if ($stmt->execute()) {
			$res = $stmt->get_result();
			$order = $res ? $res->fetch_assoc() : null;
		}
		$stmt->close();
	}

	$sqlItem = "SELECT it_id, it_spec_to_datetime FROM {$g5['g5_shop_item_table']} WHERE it_id=? AND it_spec_buyer_mb_id=? LIMIT 1{$lockSql}";
	$stmt = $mysqli->prepare($sqlItem);
	if ($stmt) {
		$itIdStr = (string)$preferredItId;
		$stmt->bind_param('ss', $itIdStr, $mbId);
		if ($stmt->execute()) {
			$res = $stmt->get_result();
			$item = $res ? $res->fetch_assoc() : null;
		}
		$stmt->close();
	}

	if (!$order || !$item) {
		return null;
	}

	return array(
		'od_id' => $odId,
		'it_id' => $preferredItId,
		'order' => $order,
		'cart' => $cart,
		'item' => $item,
	);
}

function apply_extend_history_to_shop($mysqli, $mbId, $odId, $itId, $beforeExpire, $afterExpire, $amount)
{
	global $g5, $member;

	$mbId = trim((string)$mbId);
	$odId = trim((string)$odId);
	$itId = (int)$itId;
	$amount = (int)$amount;

	if ($mbId === '' || $itId <= 0 || $amount <= 0) {
		throw new Exception('주문 연장 이력 반영 파라미터가 올바르지 않습니다.');
	}

	$ctx = find_extend_order_context($mysqli, $mbId, $odId, $itId, true);
	if (!$ctx) {
		throw new Exception('주문 연장 이력 반영 대상 주문을 찾지 못했습니다.');
	}

	list($extendDays, $extendHours) = resolve_extend_period_day_hour($beforeExpire, $afterExpire);
	$odEsc = sql_esc($ctx['od_id']);
	$itEsc = sql_esc((string)$itId);
	$mbEsc = sql_esc($mbId);
	$beforeEsc = sql_esc((string)$beforeExpire);
	$afterEsc = sql_esc((string)$afterExpire);

	$cart = $ctx['cart'];
	$order = $ctx['order'];

	$existingDate = isset($cart['extension_of_period_date']) ? trim((string)$cart['extension_of_period_date']) : '';
	$existingToDt = isset($cart['extension_of_period_to_datetime']) ? trim((string)$cart['extension_of_period_to_datetime']) : '';
	$existingDays = isset($cart['extension_of_period_days']) ? trim((string)$cart['extension_of_period_days']) : '';
	$existingHours = isset($cart['extension_of_period_hours']) ? trim((string)$cart['extension_of_period_hours']) : '';
	$existingPrice = isset($cart['extension_of_period_price']) ? trim((string)$cart['extension_of_period_price']) : '';

	$appendDate = G5_TIME_YMDHIS;
	$newDate = ($existingDate !== '') ? ($existingDate . "\n" . $appendDate) : $appendDate;
	$newToDt = ($existingToDt !== '') ? ($existingToDt . "\n" . $afterExpire) : $afterExpire;
	$newDays = ($existingDays !== '') ? ($existingDays . "\n" . (int)$extendDays) : (string)((int)$extendDays);
	$newHours = ($existingHours !== '') ? ($existingHours . "\n" . (int)$extendHours) : (string)((int)$extendHours);
	$newPrice = ($existingPrice !== '') ? ($existingPrice . "\n" . (int)$amount) : (string)((int)$amount);

	$newDateEsc = sql_esc($newDate);
	$newToDtEsc = sql_esc($newToDt);
	$newDaysEsc = sql_esc($newDays);
	$newHoursEsc = sql_esc($newHours);
	$newPriceEsc = sql_esc($newPrice);

	$ctQtyExt = (int)(isset($cart['ct_qty_for_extension_of_period']) ? $cart['ct_qty_for_extension_of_period'] : 0) + (int)$extendDays;
	$ctQtyHourExt = (int)(isset($cart['ct_qty_hour_for_extension_of_period']) ? $cart['ct_qty_hour_for_extension_of_period'] : 0) + (int)$extendHours;
	$ctPriceExt = (int)(isset($cart['ct_price_for_extension_of_period']) ? $cart['ct_price_for_extension_of_period'] : 0) + (int)$amount;

	$sqlItemUpdate = "UPDATE {$g5['g5_shop_item_table']} SET it_spec_to_datetime=? WHERE it_id=? AND it_spec_buyer_mb_id=?";
	$stmt = $mysqli->prepare($sqlItemUpdate);
	if (!$stmt) {
		throw new Exception('shop_item 연장 종료일 갱신 prepare 실패: ' . $mysqli->error);
	}
	$itIdStr = (string)$itId;
	$stmt->bind_param('sss', $afterExpire, $itIdStr, $mbId);
	if (!$stmt->execute()) {
		$err = $stmt->error;
		$stmt->close();
		throw new Exception('shop_item 연장 종료일 갱신 execute 실패: ' . $err);
	}
	if ($stmt->affected_rows < 1) {
		$stmt->close();
		throw new Exception('shop_item 연장 종료일 갱신 대상이 없습니다.');
	}
	$stmt->close();

	$sqlCartUpdate = "UPDATE {$g5['g5_shop_cart_table']} SET extension_of_period_date=?, extension_of_period_to_datetime=?, extension_of_period_days=?, extension_of_period_hours=?, extension_of_period_price=?, ct_qty_for_extension_of_period=?, ct_qty_hour_for_extension_of_period=?, ct_price_for_extension_of_period=? WHERE od_id=? AND it_id=? AND mb_id=?";
	$stmt = $mysqli->prepare($sqlCartUpdate);
	if (!$stmt) {
		throw new Exception('shop_cart 연장 이력 갱신 prepare 실패: ' . $mysqli->error);
	}
	$odIdCtx = (string)$ctx['od_id'];
	$itIdStr = (string)$itId;
	$stmt->bind_param('sssssiiisss', $newDate, $newToDt, $newDays, $newHours, $newPrice, $ctQtyExt, $ctQtyHourExt, $ctPriceExt, $odIdCtx, $itIdStr, $mbId);
	if (!$stmt->execute()) {
		$err = $stmt->error;
		$stmt->close();
		throw new Exception('shop_cart 연장 이력 갱신 execute 실패: ' . $err);
	}
	if ($stmt->affected_rows < 1) {
		$stmt->close();
		throw new Exception('shop_cart 연장 이력 갱신 대상이 없습니다.');
	}
	$stmt->close();

	$odReceiptExt = (int)(isset($order['od_receipt_point_for_extension_of_period']) ? $order['od_receipt_point_for_extension_of_period'] : 0) + (int)$amount;
	$mbName = '';
	if (is_array($member) && isset($member['mb_id']) && (string)$member['mb_id'] === $mbId) {
		$mbName = isset($member['mb_name']) ? (string)$member['mb_name'] : '';
	}
	if ($mbName === '') {
		$rowMember = get_member($mbId);
		$mbName = isset($rowMember['mb_name']) ? (string)$rowMember['mb_name'] : $mbId;
	}
	$cartItName = (isset($cart['it_name']) && trim((string)$cart['it_name']) !== '') ? trim((string)$cart['it_name']) : 'VPN-' . $itId;
	$odModHistory = trim((string)(isset($order['od_mod_history']) ? $order['od_mod_history'] : '') . "\n" . G5_TIME_YMDHIS . " 계약연장 ({$extendDays}일{$extendHours}시간) {$cartItName} 결제 " . number_format($amount) . " Point (이용자: {$mbId} / {$mbName}) ");
	$odModHistoryEsc = sql_esc($odModHistory);

	$sqlOrderUpdate = "UPDATE {$g5['g5_shop_order_table']} SET od_receipt_point_for_extension_of_period=?, od_mod_history=? WHERE od_id=? AND mb_id=?";
	$stmt = $mysqli->prepare($sqlOrderUpdate);
	if (!$stmt) {
		throw new Exception('shop_order 연장 합계/이력 갱신 prepare 실패: ' . $mysqli->error);
	}
	$stmt->bind_param('isss', $odReceiptExt, $odModHistory, $odIdCtx, $mbId);
	if (!$stmt->execute()) {
		$err = $stmt->error;
		$stmt->close();
		throw new Exception('shop_order 연장 합계/이력 갱신 execute 실패: ' . $err);
	}
	if ($stmt->affected_rows < 1) {
		$stmt->close();
		throw new Exception('shop_order 연장 합계/이력 갱신 대상이 없습니다.');
	}
	$stmt->close();

	return array(
		'od_id' => $ctx['od_id'],
		'it_id' => $itId,
		'extend_days' => (int)$extendDays,
		'extend_hours' => (int)$extendHours,
		'ct_price_for_extension_of_period' => (int)$ctPriceExt,
	);
}

function table_has_column($mysqli, $tableName, $columnName)
{
	static $tableColumnsCache = array();

	$tableName = trim((string)$tableName);
	$columnName = trim((string)$columnName);
	if ($tableName === '' || $columnName === '' || !$mysqli) {
		return false;
	}

	if (!isset($tableColumnsCache[$tableName])) {
		$tableColumnsCache[$tableName] = array();
		$sql = "SHOW COLUMNS FROM `" . str_replace('`', '', $tableName) . "`";
		$res = $mysqli->query($sql);
		if ($res) {
			while ($col = $res->fetch_assoc()) {
				if (isset($col['Field']) && $col['Field'] !== '') {
					$tableColumnsCache[$tableName][$col['Field']] = true;
				}
			}
			$res->free();
		}
	}

	return isset($tableColumnsCache[$tableName][$columnName]);
}

function parse_product_code_from_profile_name($profileName)
{
	if (preg_match('/(VPN-\d+)/i', (string)$profileName, $m)) {
		return strtoupper($m[1]);
	}
	return '';
}

function parse_vpn_number_from_it_name($itName)
{
	if ($itName !== '' && preg_match('/VPN-(\d+)/i', (string)$itName, $m)) {
		return (int)$m[1];
	}
	return 0;
}

function extend_build_vpn_user_alias_candidates($vpnUserId)
{
	$vpnUserId = trim((string)$vpnUserId);
	if ($vpnUserId === '') {
		return array();
	}

	$candidates = array($vpnUserId);
	$swapVw = strtr($vpnUserId, array('v' => 'w', 'V' => 'W', 'w' => 'v', 'W' => 'V'));
	if ($swapVw !== '' && $swapVw !== $vpnUserId) {
		$candidates[] = $swapVw;
	}

	return array_values(array_unique($candidates));
}

function to_datetime_str($ts)
{
	return date('Y-m-d H:i:s', $ts);
}

function resolve_extend_base_ts($currentExpire, $referenceTs)
{
	$referenceTs = (int)$referenceTs;
	if ($referenceTs <= 0) {
		$referenceTs = time();
	}

	$expireTs = strtotime((string)$currentExpire);
	if ($expireTs === false || $expireTs < $referenceTs) {
		return $referenceTs;
	}

	return $expireTs;
}

function calc_seconds($unit, $qty)
{
	$qty = (int)$qty;
	if ($qty < 1) {
		$qty = 1;
	}

	switch ($unit) {
		case 'hour':
			return 3600 * $qty;
		case 'day':
			return 86400 * $qty;
		case 'week':
			return 86400 * 7 * $qty;
		case 'month':
			return -1;
		case 'year':
			return -2;
		default:
			return 0;
	}
}

function extend_get_shop_item_pricing_row($vpnBusinessNo)
{
	global $g5;
	global $mysqli;

	$vpnBusinessNo = (int)$vpnBusinessNo;
	if ($vpnBusinessNo <= 0 || empty($g5['g5_shop_item_table'])) {
		return array();
	}

	$itemTable = $g5['g5_shop_item_table'];
	$hourPriceField = table_has_column($mysqli, $itemTable, 'it_is_hour_sale_price') ? 'it_is_hour_sale_price' : '0';
	$it4Field = table_has_column($mysqli, $itemTable, 'it_4') ? 'it_4' : '0';
	$it5Field = table_has_column($mysqli, $itemTable, 'it_5') ? 'it_5' : '0';
	$it6Field = table_has_column($mysqli, $itemTable, 'it_6') ? 'it_6' : '0';

	$safeItId = sql_esc((string)$vpnBusinessNo);
	$row = sql_fetch("SELECT it_id, it_price, {$hourPriceField} AS it_is_hour_sale_price, {$it4Field} AS it_4, {$it5Field} AS it_5, {$it6Field} AS it_6 FROM {$itemTable} WHERE it_id='{$safeItId}' LIMIT 1");
	return is_array($row) ? $row : array();
}

function extend_effective_hour_price($itemRow)
{
	$hourPrice = isset($itemRow['it_is_hour_sale_price']) ? (int)$itemRow['it_is_hour_sale_price'] : 0;
	if ($hourPrice > 0) {
		return $hourPrice;
	}
	$fallback = isset($itemRow['it_price']) ? (int)$itemRow['it_price'] : 0;
	return ($fallback > 0) ? $fallback : 0;
}

function extend_unit_apply_hours($itemRow, $unit)
{
	if ($unit === 'day') {
		$hours = isset($itemRow['it_4']) ? (float)$itemRow['it_4'] : 0;
		return (int)max(1, ceil($hours > 0 ? $hours : 24));
	}
	if ($unit === 'month') {
		$hours = isset($itemRow['it_5']) ? (float)$itemRow['it_5'] : 0;
		return (int)max(1, ceil($hours > 0 ? $hours : 720));
	}
	if ($unit === 'year') {
		$hours = isset($itemRow['it_6']) ? (float)$itemRow['it_6'] : 0;
		return (int)max(1, ceil($hours > 0 ? $hours : 8760));
	}
	if ($unit === 'week') {
		return 168;
	}
	return 1;
}

function extend_get_unit_price_map($vpnBusinessNo)
{
	static $cache = array();
	$key = (int)$vpnBusinessNo;
	if (isset($cache[$key])) {
		return $cache[$key];
	}

	$itemRow = extend_get_shop_item_pricing_row($vpnBusinessNo);
	$hourPrice = extend_effective_hour_price($itemRow);
	if ($hourPrice <= 0) {
		$hourPrice = 75;
	}

	$map = array(
		'hour' => (int)$hourPrice,
		'day' => (int)($hourPrice * extend_unit_apply_hours($itemRow, 'day')),
		'month' => (int)($hourPrice * extend_unit_apply_hours($itemRow, 'month')),
		'year' => (int)($hourPrice * extend_unit_apply_hours($itemRow, 'year')),
		// 하위호환: 기존 주문 데이터에 week가 남아 있을 수 있어 서버 계산은 유지
		'week' => (int)($hourPrice * extend_unit_apply_hours($itemRow, 'week')),
	);

	$cache[$key] = $map;
	return $map;
}

function get_unit_price($unit, $vpnBusinessNo)
{
	$map = extend_get_unit_price_map($vpnBusinessNo);
	return isset($map[$unit]) ? (int)$map[$unit] : 0;
}

function extend_unit_display_name($unit)
{
	if ($unit === 'hour') {
		return '1시간 이용권';
	}
	if ($unit === 'day') {
		return '1일 이용권';
	}
	if ($unit === 'month') {
		return '1개월 이용권';
	}
	if ($unit === 'year') {
		return '1년 이용권';
	}
	if ($unit === 'week') {
		return '1주 이용권';
	}
	return '이용권';
}

function resolve_member_id($rawMemberId, $rawMemberEmail)
{
	global $g5;

	$mbId = $rawMemberId;
	if ($rawMemberEmail !== '') {
		$mbId = $rawMemberEmail;
	}

	if ($mbId === '' || preg_match('/^VpNgueSt/', $mbId)) {
		return $mbId;
	}

	$resolvedMember = array();
	if ($rawMemberEmail !== '' || filter_var($mbId, FILTER_VALIDATE_EMAIL)) {
		$safeIdentifier = function_exists('sql_real_escape_string')
			? sql_real_escape_string($mbId)
			: addslashes($mbId);
		$resolvedMember = sql_fetch(" select * from {$g5['member_table']} where mb_email = '{$safeIdentifier}' ");
	}

	if (!isset($resolvedMember['mb_id']) || !$resolvedMember['mb_id']) {
		$resolvedMember = get_member($mbId);
	}

	if (isset($resolvedMember['mb_id']) && $resolvedMember['mb_id']) {
		return $resolvedMember['mb_id'];
	}

	return $mbId;
}

function find_target_profile($mysqli, $mbId, $profileId, $profileName, $profileGateway, $profileUsername)
{
	$debug = array();

	$itName = parse_product_code_from_profile_name($profileName);
	if ($itName === '' && req('it_name') !== '') {
		$itName = req('it_name');
	}

	$profileIdInt = (ctype_digit((string)$profileId)) ? (int)$profileId : 0;
	$vpnNoFromName = 0;
	if (preg_match('/VPN-(\d+)/i', $itName, $mNo)) {
		$vpnNoFromName = (int)$mNo[1];
	}

	$profileIdMismatch = ($profileIdInt > 0 && $vpnNoFromName > 0 && $profileIdInt !== $vpnNoFromName);
	if ($profileIdMismatch) {
		$debug[] = 'profile_id mismatch detected: profile_id=' . $profileIdInt . ', vpn_no_from_name=' . $vpnNoFromName;
	}

	$selectBase = "SELECT no, mb_id, remark, server_name, it_name, vpn_user_id, vpn_id_expire, enable
				FROM vpnid_list
				WHERE enable=1";

	$attempts = array();
	$attemptErrors = array();

	$addAttempt = function($label, $whereSql, $types, $vals) use (&$attempts, $selectBase) {
		$attempts[] = array(
			'label' => $label,
			'sql' => $selectBase . ' AND ' . $whereSql . ' ORDER BY no DESC LIMIT 1',
			'types' => $types,
			'vals' => $vals,
		);
	};

	$hasSecondaryId = ($profileUsername !== '' || $profileGateway !== '' || $itName !== '' || $vpnNoFromName > 0 || $profileIdInt > 0);
	if (!$hasSecondaryId) {
		return array(
			'ok' => false,
			'msg' => '연장 대상 조건을 구성하지 못했습니다.',
			'debug' => array(
				'mb_id' => $mbId,
				'profile_id' => $profileId,
				'profile_name' => $profileName,
				'it_name_parsed' => $itName,
				'profile_gateway' => $profileGateway,
				'profile_username' => $profileUsername,
			),
		);
	}

	if ($profileIdInt > 0 && !$profileIdMismatch) {
		$addAttempt('strict_mb_id_no', 'mb_id=? AND no=?', 'si', array($mbId, $profileIdInt));
		$addAttempt('mb_or_remark_no', '(mb_id=? OR remark=?) AND no=?', 'ssi', array($mbId, $mbId, $profileIdInt));
	}

	if ($vpnNoFromName > 0) {
		$addAttempt('strict_mb_id_vpn_no_from_name', 'mb_id=? AND no=?', 'si', array($mbId, $vpnNoFromName));
		$addAttempt('mb_or_remark_vpn_no_from_name', '(mb_id=? OR remark=?) AND no=?', 'ssi', array($mbId, $mbId, $vpnNoFromName));
	}

	if ($itName !== '' && $profileUsername !== '' && $profileGateway !== '') {
		$addAttempt('strict_mb_it_user_server', 'mb_id=? AND it_name=? AND vpn_user_id=? AND server_name=?', 'ssss', array($mbId, $itName, $profileUsername, $profileGateway));
		$addAttempt('mb_or_remark_it_user_server', '(mb_id=? OR remark=?) AND it_name=? AND vpn_user_id=? AND server_name=?', 'sssss', array($mbId, $mbId, $itName, $profileUsername, $profileGateway));
	}

	if ($itName !== '') {
		$addAttempt('strict_mb_it_name', 'mb_id=? AND it_name=?', 'ss', array($mbId, $itName));
		$addAttempt('mb_or_remark_it_name', '(mb_id=? OR remark=?) AND it_name=?', 'sss', array($mbId, $mbId, $itName));
	}

	if ($profileUsername !== '' && $profileGateway !== '') {
		$addAttempt('strict_mb_user_server', 'mb_id=? AND vpn_user_id=? AND server_name=?', 'sss', array($mbId, $profileUsername, $profileGateway));
		$addAttempt('mb_or_remark_user_server', '(mb_id=? OR remark=?) AND vpn_user_id=? AND server_name=?', 'ssss', array($mbId, $mbId, $profileUsername, $profileGateway));
	}

	if ($profileUsername !== '') {
		$addAttempt('strict_mb_user', 'mb_id=? AND vpn_user_id=?', 'ss', array($mbId, $profileUsername));
		$addAttempt('mb_or_remark_user', '(mb_id=? OR remark=?) AND vpn_user_id=?', 'sss', array($mbId, $mbId, $profileUsername));
	}

	$row = null;
	$matchedAttempt = '';
	foreach ($attempts as $attempt) {
		$stmt = $mysqli->prepare($attempt['sql']);
		if (!$stmt) {
			$attemptErrors[] = array('label' => $attempt['label'], 'stage' => 'prepare', 'error' => $mysqli->error);
			continue;
		}

		$stmt->bind_param($attempt['types'], ...$attempt['vals']);
		if (!$stmt->execute()) {
			$attemptErrors[] = array('label' => $attempt['label'], 'stage' => 'execute', 'error' => $stmt->error);
			$stmt->close();
			continue;
		}

		$res = $stmt->get_result();
		$found = $res ? $res->fetch_assoc() : null;
		$stmt->close();

		if ($found) {
			$row = $found;
			$matchedAttempt = $attempt['label'];
			$debug[] = 'matched_attempt=' . $matchedAttempt;
			break;
		}
	}

	if (!$row) {
		return array(
			'ok' => false,
			'msg' => '대상 프로필을 찾지 못했습니다.',
			'debug' => array(
				'attempt_labels' => array_map(function($a) { return $a['label']; }, $attempts),
				'attempt_errors' => $attemptErrors,
				'parsed_it_name' => $itName,
				'profile_id_mismatch' => $profileIdMismatch,
				'profile_id_raw' => $profileIdInt,
				'vpn_no_from_name' => $vpnNoFromName,
			),
		);
	}

	$selectedNo = isset($row['no']) ? (int)$row['no'] : 0;
	$selectedItName = isset($row['it_name']) ? (string)$row['it_name'] : '';
	$rowVpnNoFromItName = 0;
	if ($selectedItName !== '' && preg_match('/VPN-(\d+)/i', $selectedItName, $rowNoMatch)) {
		$rowVpnNoFromItName = (int)$rowNoMatch[1];
	}

	$vpnNoMatchByNo = ($vpnNoFromName > 0 && $selectedNo === $vpnNoFromName);
	$vpnNoMatchByItName = ($vpnNoFromName > 0 && $rowVpnNoFromItName > 0 && $rowVpnNoFromItName === $vpnNoFromName);

	if ($vpnNoFromName > 0 && !$vpnNoMatchByNo && !$vpnNoMatchByItName) {
		return array(
			'ok' => false,
			'msg' => '프로필 식별값이 일치하지 않습니다.',
			'debug' => array(
				'profile_id_mismatch' => $profileIdMismatch,
				'profile_id_raw' => $profileIdInt,
				'vpn_no_from_name' => $vpnNoFromName,
				'selected_no' => $selectedNo,
				'selected_it_name' => $selectedItName,
				'selected_vpn_no_from_it_name' => $rowVpnNoFromItName,
			),
		);
	}

	if ($vpnNoFromName > 0 && !$vpnNoMatchByNo && $vpnNoMatchByItName) {
		$debug[] = 'verify_fallback_ok=it_name_vpn_no_match';
	}

	if ($itName !== '' && isset($row['it_name']) && strcasecmp((string)$row['it_name'], (string)$itName) !== 0) {
		return array(
			'ok' => false,
			'msg' => '프로필 상품코드가 일치하지 않습니다.',
			'debug' => array(
				'expected_it_name' => $itName,
				'selected_it_name' => $row['it_name'],
				'selected_no' => (int)$row['no'],
			),
		);
	}

	return array('ok' => true, 'row' => $row, 'debug' => $debug);
}

function ensure_order_table($mysqli)
{
	$sql = "CREATE TABLE IF NOT EXISTS vpn_extend_orders (
		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
		order_no VARCHAR(40) NOT NULL,
		mb_id VARCHAR(100) NOT NULL,
		vpn_no INT NOT NULL,
		vpn_business_no INT NULL,
		it_name VARCHAR(100) NOT NULL,
		vpn_user_id VARCHAR(100) NOT NULL,
		unit_type VARCHAR(10) NOT NULL,
		qty INT NOT NULL,
		unit_price INT NOT NULL,
		amount INT NOT NULL,
		status VARCHAR(20) NOT NULL DEFAULT 'pending',
		created_at DATETIME NOT NULL,
		paid_at DATETIME NULL,
		approved_by VARCHAR(50) NULL,
		memo VARCHAR(255) NULL,
		PRIMARY KEY (id),
		UNIQUE KEY uq_order_no (order_no),
		KEY idx_mb_status (mb_id, status),
		KEY idx_vpn_no (vpn_no),
		KEY idx_vpn_business_no (vpn_business_no)
	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";

	if (!$mysqli->query($sql)) {
		return false;
	}

	$colRes = $mysqli->query("SHOW COLUMNS FROM vpn_extend_orders LIKE 'vpn_business_no'");
	if (!$colRes || $colRes->num_rows === 0) {
		if (!$mysqli->query("ALTER TABLE vpn_extend_orders ADD COLUMN vpn_business_no INT NULL AFTER vpn_no")) {
			return false;
		}
		$mysqli->query("ALTER TABLE vpn_extend_orders ADD KEY idx_vpn_business_no (vpn_business_no)");
	}

	return true;
}

function make_order_no()
{
	return date('YmdHis') . mt_rand(1000, 9999);
}

function extend_fetch_order_history($mysqli, $mbId, $vpnUserId, $limit, $vpnBusinessNo = 0)
{
	$mbId = trim((string)$mbId);
	$vpnUserId = trim((string)$vpnUserId);
	$vpnBusinessNo = (int)$vpnBusinessNo;
	$limit = (int)$limit;
	if ($limit < 1) {
		$limit = 1;
	}
	if ($limit > 100) {
		$limit = 100;
	}

	$history = array();
	$vpnUserAliases = extend_build_vpn_user_alias_candidates($vpnUserId);

	$pickFirstLine = function($rawValue) {
		$text = trim((string)$rawValue);
		if ($text === '') {
			return '';
		}

		$text = str_replace("\r\n", "\n", $text);
		$text = str_replace("\r", "\n", $text);
		$lines = preg_split('/\n+/', $text);
		if (!is_array($lines)) {
			return $text;
		}

		foreach ($lines as $line) {
			$line = trim((string)$line);
			if ($line !== '') {
				return $line;
			}
		}

		return '';
	};

	$parseFirstLineInt = function($rawValue) use ($pickFirstLine) {
		$line = $pickFirstLine($rawValue);
		if ($line === '') {
			return 0;
		}

		if (preg_match('/-?\d+/', $line, $m)) {
			return (int)$m[0];
		}

		return (int)$line;
	};

	if (!$mysqli) {
		return $history;
	}

	$selectParts = array('no', 'vpn_user_id', 'mb_id', 'remark', 'it_name', 'vpn_id_expire');
	$selectParts[] = table_has_column($mysqli, 'vpnid_list', 'od_id') ? 'od_id' : 'NULL AS od_id';
	$selectParts[] = table_has_column($mysqli, 'vpnid_list', 'vpn_id_create_time') ? 'vpn_id_create_time' : 'NULL AS vpn_id_create_time';

	$where = array();
	$params = array();
	$types = '';

	if (!empty($vpnUserAliases)) {
		$aliasPlaceholders = implode(',', array_fill(0, count($vpnUserAliases), '?'));
		$where[] = 'vpn_user_id IN (' . $aliasPlaceholders . ')';
		foreach ($vpnUserAliases as $aliasId) {
			$params[] = $aliasId;
			$types .= 's';
		}
	} else if ($vpnUserId !== '') {
		$where[] = 'vpn_user_id=?';
		$params[] = $vpnUserId;
		$types .= 's';
	}
	if ($mbId !== '') {
		$where[] = '(mb_id=? OR remark=?)';
		$params[] = $mbId;
		$params[] = $mbId;
		$types .= 'ss';
	}

	if (empty($where)) {
		return $history;
	}

	$sqlProfile = 'SELECT ' . implode(', ', $selectParts) . ' FROM vpnid_list WHERE (' . implode(' OR ', $where) . ')';
	if ($vpnBusinessNo > 0) {
		$sqlProfile .= " AND UPPER(it_name) LIKE '%VPN-" . (int)$vpnBusinessNo . "%'";
	}
	$sqlProfile .= ' ORDER BY update_datetime DESC, no DESC LIMIT 1';
	$stmtProfile = $mysqli->prepare($sqlProfile);
	$profile = null;
	if ($stmtProfile) {
		if (!empty($params)) {
			$stmtProfile->bind_param($types, ...$params);
		}
		if ($stmtProfile->execute()) {
			$profile = $stmtProfile->get_result()->fetch_assoc();
		}
		$stmtProfile->close();
	}

	if (!is_array($profile)) {
		return $history;
	}

	$resolvedMbId = trim((string)(isset($profile['mb_id']) ? $profile['mb_id'] : ''));
	if ($resolvedMbId === '') {
		$resolvedMbId = trim((string)(isset($profile['remark']) ? $profile['remark'] : ''));
	}
	$resolvedVpnUserId = trim((string)(isset($profile['vpn_user_id']) ? $profile['vpn_user_id'] : ''));
	$itemName = trim((string)(isset($profile['it_name']) ? $profile['it_name'] : ''));

	// 최초주문은 vpnid_list가 아니라 g5_shop_cart(it_id) 기준으로 조회한다.
	$resolvedItId = 0;
	if ($vpnBusinessNo > 0) {
		$resolvedItId = (int)$vpnBusinessNo;
	} else if ($itemName !== '' && preg_match('/(\d{3,})/', $itemName, $mItId)) {
		$resolvedItId = (int)$mItId[1];
	}

	if ($resolvedMbId === '') {
		$resolvedMbId = $mbId;
	}

	$cartTable = isset($GLOBALS['g5']) && is_array($GLOBALS['g5']) && !empty($GLOBALS['g5']['g5_shop_cart_table'])
		? (string)$GLOBALS['g5']['g5_shop_cart_table']
		: '';
	$orderTable = isset($GLOBALS['g5']) && is_array($GLOBALS['g5']) && !empty($GLOBALS['g5']['g5_shop_order_table'])
		? (string)$GLOBALS['g5']['g5_shop_order_table']
		: '';

	if ($cartTable !== '' && $resolvedMbId !== '' && $resolvedItId > 0) {
		$hasCtStatus = table_has_column($mysqli, $cartTable, 'ct_status');
		$hasCtTime = table_has_column($mysqli, $cartTable, 'ct_time');
		$hasCtId = table_has_column($mysqli, $cartTable, 'ct_id');
		$hasCtPrice = table_has_column($mysqli, $cartTable, 'ct_price');
		$hasExtDate = table_has_column($mysqli, $cartTable, 'extension_of_period_date');
		$hasExtToDatetime = table_has_column($mysqli, $cartTable, 'extension_of_period_to_datetime');
		$hasExtDays = table_has_column($mysqli, $cartTable, 'extension_of_period_days');
		$hasExtHours = table_has_column($mysqli, $cartTable, 'extension_of_period_hours');

		$statusClause = $hasCtStatus ? " AND ct_status IN ('입금','완료')" : '';
		$orderByParts = array();
		if ($hasCtTime) {
			$orderByParts[] = 'ct_time ASC';
		}
		if ($hasCtId) {
			$orderByParts[] = 'ct_id ASC';
		}
		if (empty($orderByParts)) {
			$orderByParts[] = 'od_id ASC';
		}

		$selectCartCols = array(
			'od_id',
			'it_id',
			table_has_column($mysqli, $cartTable, 'it_name') ? 'it_name' : "'' AS it_name",
			$hasCtTime ? 'ct_time' : "'' AS ct_time",
			$hasCtPrice ? 'ct_price' : "0 AS ct_price",
			$hasExtDate ? 'extension_of_period_date' : "'' AS extension_of_period_date",
			$hasExtToDatetime ? 'extension_of_period_to_datetime' : "'' AS extension_of_period_to_datetime",
			$hasExtDays ? 'extension_of_period_days' : "0 AS extension_of_period_days",
			$hasExtHours ? 'extension_of_period_hours' : "0 AS extension_of_period_hours"
		);

		$sqlInitialCart = "SELECT " . implode(', ', $selectCartCols)
			. " FROM {$cartTable}"
			. " WHERE mb_id='" . sql_esc($resolvedMbId) . "'"
			. "   AND it_id='" . sql_esc((string)$resolvedItId) . "'"
			. $statusClause
			. " ORDER BY " . implode(', ', $orderByParts)
			. " LIMIT 1";

		$initialCart = @sql_fetch($sqlInitialCart, false);
		if (is_array($initialCart) && !empty($initialCart['od_id'])) {
			$initialOdId = trim((string)$initialCart['od_id']);
			$initialItemName = trim((string)($initialCart['it_name'] ?? ''));
			if ($initialItemName === '') {
				$initialItemName = $itemName !== '' ? $itemName : ('VPN-' . (string)$resolvedItId);
			}

			$initialStartedAtByExt = $pickFirstLine($initialCart['extension_of_period_date'] ?? '');
			$initialExpireByExt = $pickFirstLine($initialCart['extension_of_period_to_datetime'] ?? '');
			$initialDays = $parseFirstLineInt($initialCart['extension_of_period_days'] ?? '0');
			$initialHours = $parseFirstLineInt($initialCart['extension_of_period_hours'] ?? '0');
			$initialStartedAt = $initialStartedAtByExt !== '' ? $initialStartedAtByExt : trim((string)($initialCart['ct_time'] ?? ''));
			$paidAmount = 0;
			if ($hasCtPrice && isset($initialCart['ct_price'])) {
				$paidAmount = (float)$initialCart['ct_price'];
			}

			if ($orderTable !== '') {
				$orderSelectCols = array();
				if (table_has_column($mysqli, $orderTable, 'od_receipt_point')) {
					$orderSelectCols[] = 'od_receipt_point';
				}
				if (table_has_column($mysqli, $orderTable, 'od_receipt_time')) {
					$orderSelectCols[] = 'od_receipt_time';
				}
				if (table_has_column($mysqli, $orderTable, 'od_time')) {
					$orderSelectCols[] = 'od_time';
				}
				if (!empty($orderSelectCols)) {
					$sqlInitialOrder = "SELECT " . implode(', ', $orderSelectCols)
						. " FROM {$orderTable} WHERE od_id='" . sql_esc($initialOdId) . "' LIMIT 1";
					$initialOrder = @sql_fetch($sqlInitialOrder, false);
					if (is_array($initialOrder)) {
						if (isset($initialOrder['od_receipt_point']) && (float)$initialOrder['od_receipt_point'] > 0) {
							$paidAmount = (float)$initialOrder['od_receipt_point'];
						}
						$orderPaidAt = trim((string)($initialOrder['od_receipt_time'] ?? ''));
						$orderCreatedAt = trim((string)($initialOrder['od_time'] ?? ''));
						if ($orderPaidAt !== '' && $orderPaidAt !== '0000-00-00 00:00:00') {
							$initialStartedAt = $orderPaidAt;
						} else if ($initialStartedAt === '' && $orderCreatedAt !== '' && $orderCreatedAt !== '0000-00-00 00:00:00') {
							$initialStartedAt = $orderCreatedAt;
						}
					}
				}
			}

			$periodParts = array();
			if ($initialDays > 0) {
				$periodParts[] = $initialDays . '일';
			}
			if ($initialHours > 0) {
				$periodParts[] = $initialHours . '시간';
			}
			$initialPeriodText = !empty($periodParts) ? implode('', $periodParts) : '-';
			$initialExpireText = $initialExpireByExt !== '' ? $initialExpireByExt : '-';

			$history[] = array(
				'order_no' => $initialOdId,
				'order_type' => '최초주문',
				'item_name' => $initialItemName,
				'started_at' => $initialStartedAt,
				'order_at' => $initialStartedAt,
				'paid_at' => $initialStartedAt,
				'order_period' => $initialPeriodText,
				'expire' => $initialExpireText,
				'payment_info' => ($paidAmount > 0 ? (rtrim(rtrim(number_format($paidAmount, 2, '.', ''), '0'), '.') . '원') : '-'),
				'source' => 'g5_shop_cart'
			);
		}
	}

	if ($resolvedVpnUserId === '') {
		$resolvedVpnUserId = $vpnUserId;
	}
	if ($resolvedMbId === '') {
		$resolvedMbId = $mbId;
	}

	if ($resolvedVpnUserId !== '' && table_has_column($mysqli, 'vpn_extend_orders', 'vpn_user_id')) {
		$resolvedAliases = extend_build_vpn_user_alias_candidates($resolvedVpnUserId);
		$safeResolvedAliases = array();
		foreach ($resolvedAliases as $aliasId2) {
			$safeResolvedAliases[] = "'" . sql_esc($aliasId2) . "'";
		}

		$whereExt = !empty($safeResolvedAliases)
			? ("vpn_user_id IN (" . implode(', ', $safeResolvedAliases) . ")")
			: ("vpn_user_id='" . sql_esc($resolvedVpnUserId) . "'");
		if ($resolvedMbId !== '' && table_has_column($mysqli, 'vpn_extend_orders', 'mb_id')) {
			$whereExt = "({$whereExt} OR mb_id='" . sql_esc($resolvedMbId) . "')";
		}
		if ($vpnBusinessNo > 0) {
			if (table_has_column($mysqli, 'vpn_extend_orders', 'vpn_business_no')) {
				$whereExt .= " AND vpn_business_no=" . (int)$vpnBusinessNo;
			} else if (table_has_column($mysqli, 'vpn_extend_orders', 'it_name')) {
				$whereExt .= " AND UPPER(it_name) LIKE '%VPN-" . (int)$vpnBusinessNo . "%'";
			}
		}

		$sqlExt = "SELECT order_no, it_name, qty, unit_type, amount, status, created_at, paid_at, memo
				   FROM vpn_extend_orders
				  WHERE {$whereExt}";
		$sqlExt .= " ORDER BY COALESCE(paid_at, created_at) DESC, id DESC LIMIT " . (int)$limit;

		$resExt = @sql_query($sqlExt, false);
		if ($resExt) {
			while ($rowExt = sql_fetch_array($resExt)) {
				if (!is_array($rowExt)) {
					continue;
				}

				$statusRaw = strtolower(trim((string)(isset($rowExt['status']) ? $rowExt['status'] : '')));
				if (in_array($statusRaw, array('cancel', 'cancelled', 'canceled', 'failed', 'void', 'deleted', 'refund', 'refunded'), true)) {
					continue;
				}
				$orderNo = trim((string)(isset($rowExt['order_no']) ? $rowExt['order_no'] : ''));
				if ($orderNo === '') {
					continue;
				}

				$unitType = strtolower(trim((string)(isset($rowExt['unit_type']) ? $rowExt['unit_type'] : '')));
				$qty = (int)(isset($rowExt['qty']) ? $rowExt['qty'] : 0);
				$periodText = '-';
				if ($qty > 0) {
					if (in_array($unitType, array('hour', 'hours', 'h'), true)) {
						$periodText = '0일' . $qty . '시간';
					} else if (in_array($unitType, array('day', 'days', 'd'), true)) {
						$periodText = $qty . '일0시간';
					} else if (in_array($unitType, array('month', 'months', 'm'), true)) {
						$periodText = $qty . '개월';
					} else if (in_array($unitType, array('year', 'years', 'y'), true)) {
						$periodText = $qty . '년';
					}
				}

				$expireText = '';
				$memoText = trim((string)(isset($rowExt['memo']) ? $rowExt['memo'] : ''));
				if ($memoText !== '' && preg_match('/만료\s*일시\s*:\s*([^|·Œ\n\r]+)/u', $memoText, $mExpire)) {
					$expireText = trim((string)$mExpire[1]);
				} else if ($memoText !== '' && preg_match('/만료일시\s*:\s*([^|·Œ\n\r]+)/u', $memoText, $mExpire2)) {
					$expireText = trim((string)$mExpire2[1]);
				}

				$history[] = array(
					'order_no' => $orderNo,
					'order_type' => '연장주문',
					'item_name' => trim((string)(isset($rowExt['it_name']) ? $rowExt['it_name'] : '')),
					'started_at' => trim((string)(isset($rowExt['paid_at']) ? $rowExt['paid_at'] : '')) !== ''
						? trim((string)$rowExt['paid_at'])
						: trim((string)(isset($rowExt['created_at']) ? $rowExt['created_at'] : '')),
					'paid_at' => trim((string)(isset($rowExt['paid_at']) ? $rowExt['paid_at'] : '')),
					'created_at' => trim((string)(isset($rowExt['created_at']) ? $rowExt['created_at'] : '')),
					'order_at' => trim((string)(isset($rowExt['paid_at']) ? $rowExt['paid_at'] : '')) !== ''
						? trim((string)$rowExt['paid_at'])
						: trim((string)(isset($rowExt['created_at']) ? $rowExt['created_at'] : '')),
					'order_period' => $periodText,
					'expire' => $expireText,
					'payment_info' => ((int)(isset($rowExt['amount']) ? $rowExt['amount'] : 0)) . '원',
					'source' => 'vpn_extend_orders',
					'status' => $statusRaw !== '' ? $statusRaw : 'unknown'
				);
			}
		}
	}

	return $history;
}

$action = req('action', '', array('act'));

$postMbEmail = req('mb_email', '', array('mbEmail', 'member_email', 'memberEmail'));
$postMbId = req('mb_id', '', array('mbId', 'member_id', 'memberId'));
$postUserId = req('user_id', '', array('userId'));
$rawMemberId = $postMbId !== '' ? $postMbId : $postUserId;
$mbId = resolve_member_id($rawMemberId, $postMbEmail);

$profileId = req('profile_id', '', array('profileId', 'vpn_no', 'vpnNo'));
$profileName = req('profile_name', '', array('profileName'));
$profileGateway = req('profile_gateway', '', array('profileGateway', 'server_name', 'serverName', 'gateway'));
$profileUsername = req('profile_username', '', array('profileUsername', 'vpn_user_id', 'vpnUserId'));

extend_debug_log('request_received', array(
	'action' => $action,
	'mb_id_raw' => $postMbId,
	'user_id_raw' => $postUserId,
	'mb_id_resolved' => $mbId,
	'profile_id' => $profileId,
	'profile_name' => $profileName,
	'profile_gateway' => $profileGateway,
	'profile_username' => $profileUsername,
	'it_name' => req('it_name', ''),
	'vpn_no' => req('vpn_no', ''),
	'req_ts' => req('req_ts', ''),
));

if ($action === 'order_history') {
	if (!$mysqli) {
		fail_json('DB 연결 미구성');
	}

	$historyOrderNo = req('order_no', '', array('od_id', 'orderNo', 'odId'));
	$historyMbIdRaw = req('mb_id', '', array('mbId', 'member_id', 'memberId', 'user_id', 'userId'));
	$historyMbId = resolve_member_id($historyMbIdRaw, req('mb_email', '', array('mbEmail', 'member_email', 'memberEmail')));
	$historyVpnUserId = req('vpn_user_id', '', array('vpnUserId', 'profile_username', 'profileUsername', 'username', 'id'));
	if ($historyVpnUserId === '') {
		$historyVpnUserId = req('user_id', '', array('userId'));
	}
	$historyLimit = (int)req('limit', '30');
	$historyVpnBusinessNo = (int)req('vpn_business_no', '0', array('vpnBusinessNo', 'business_no', 'vpn_no'));

	if ($historyOrderNo !== '') {
		$safeOrderNo = sql_esc($historyOrderNo);

		if ($historyMbId === '' && isset($g5['g5_shop_order_table']) && $g5['g5_shop_order_table']) {
			$orderTable = (string)$g5['g5_shop_order_table'];
			$orderRow = @sql_fetch("SELECT mb_id FROM {$orderTable} WHERE od_id='{$safeOrderNo}' LIMIT 1", false);
			if (is_array($orderRow) && !empty($orderRow['mb_id'])) {
				$historyMbId = trim((string)$orderRow['mb_id']);
			}
		}

		if ($historyVpnBusinessNo <= 0 && isset($g5['g5_shop_cart_table']) && $g5['g5_shop_cart_table']) {
			$cartTable = (string)$g5['g5_shop_cart_table'];
			$cartRow = @sql_fetch("SELECT it_id FROM {$cartTable} WHERE od_id='{$safeOrderNo}' ORDER BY ct_id DESC LIMIT 1", false);
			if (is_array($cartRow) && !empty($cartRow['it_id'])) {
				$historyVpnBusinessNo = (int)$cartRow['it_id'];
			}
		}

		if ($historyVpnUserId === '') {
			$profileRow = @sql_fetch("SELECT vpn_user_id FROM vpnid_list WHERE od_id='{$safeOrderNo}' ORDER BY no DESC LIMIT 1", false);
			if (is_array($profileRow) && !empty($profileRow['vpn_user_id'])) {
				$historyVpnUserId = trim((string)$profileRow['vpn_user_id']);
			}
		}
	}

	if ($historyMbId === '' && $historyVpnUserId === '' && $historyOrderNo === '') {
		fail_json('mb_id 또는 vpn_user_id 또는 order_no가 필요합니다.');
	}

	$historyRows = extend_fetch_order_history($mysqli, $historyMbId, $historyVpnUserId, $historyLimit, $historyVpnBusinessNo);
	success_json('order_history_loaded', array(
		'action' => 'order_history',
		'order_no' => $historyOrderNo,
		'mb_id' => $historyMbId,
		'vpn_user_id' => $historyVpnUserId,
		'vpn_business_no' => $historyVpnBusinessNo,
		'count' => count($historyRows),
		'orders' => $historyRows
	));
}

if ($mbId === '') {
	if (is_api_request() || $action !== '') {
		fail_json('mb_id가 필요합니다.');
	}
	echo 'mb_id required';
	exit;
}

if (!$mysqli) {
	if (is_api_request() || $action !== '') {
		fail_json('DB 연결 미구성');
	}
	echo 'DB connection not configured';
	exit;
}

$target = null;
$currentExpire = '';
$internalProfileNo = 0;
$businessVpnNo = 0;

// pay/approve는 order_no 기반 처리이므로 프로필 선조회가 필요 없다.
$requiresTargetLookup = !in_array($action, array('pay', 'approve'), true);
if ($requiresTargetLookup) {
	$found = find_target_profile($mysqli, $mbId, $profileId, $profileName, $profileGateway, $profileUsername);
	if (!$found['ok']) {
		extend_debug_log('target_profile_not_found', array(
			'mb_id' => $mbId,
			'profile_id' => $profileId,
			'profile_name' => $profileName,
			'debug' => $found['debug'],
		));

		if (is_api_request() || $action !== '') {
			fail_json($found['msg'], array('debug' => $found['debug']));
		}
		echo '<pre>대상 조회 실패: ' . h($found['msg']) . "\n" . h(print_r($found['debug'], true)) . '</pre>';
		exit;
	}

	extend_debug_log('target_profile_found', array(
		'mb_id' => $mbId,
		'profile_id' => $profileId,
		'target_no' => isset($found['row']['no']) ? (int)$found['row']['no'] : 0,
		'target_it_name' => isset($found['row']['it_name']) ? $found['row']['it_name'] : '',
		'target_mb_id' => isset($found['row']['mb_id']) ? $found['row']['mb_id'] : '',
		'target_remark' => isset($found['row']['remark']) ? $found['row']['remark'] : '',
	));

	$target = $found['row'];
	$currentExpire = $target['vpn_id_expire'];
	$internalProfileNo = (int)$target['no'];
	$businessVpnNo = parse_vpn_number_from_it_name(isset($target['it_name']) ? $target['it_name'] : '');
}

if ($action === 'quote') {
	$unit = req('unit', 'hour');
	$qty = (int)req('qty', 1);
	if ($qty < 1) {
		$qty = 1;
	}

	$unitPriceMap = extend_get_unit_price_map($businessVpnNo);
	$unitPriceMapOut = array(
		'hour' => isset($unitPriceMap['hour']) ? (int)$unitPriceMap['hour'] : 0,
		'day' => isset($unitPriceMap['day']) ? (int)$unitPriceMap['day'] : 0,
		'month' => isset($unitPriceMap['month']) ? (int)$unitPriceMap['month'] : 0,
		'year' => isset($unitPriceMap['year']) ? (int)$unitPriceMap['year'] : 0,
	);
	$unitTitleMapOut = array();
	foreach ($unitPriceMapOut as $uKey => $uPrice) {
		$unitTitleMapOut[$uKey] = extend_unit_display_name($uKey) . ' (' . (int)$uPrice . ')';
	}

	$unitPrice = get_unit_price($unit, $businessVpnNo);
	if ($unitPrice <= 0) {
		fail_json('지원하지 않는 기간 단위입니다.');
	}

	$amount = $unitPrice * $qty;

	$quoteTs = time();
	$baseTs = resolve_extend_base_ts($currentExpire, $quoteTs);

	if ($unit === 'month' || $unit === 'year') {
		$newTs = strtotime('+' . $qty . ' month', $baseTs);
		if ($unit === 'year') {
			$newTs = strtotime('+' . $qty . ' year', $baseTs);
		}
	} else {
		$sec = calc_seconds($unit, $qty);
		$newTs = $baseTs + $sec;
	}

	success_json('견적 계산 완료', array(
		'action' => 'quote',
		'current_expire' => $currentExpire,
		'new_expire' => to_datetime_str($newTs),
		'expire_before' => $currentExpire,
		'expire_after' => to_datetime_str($newTs),
		'unit' => $unit,
		'qty' => $qty,
		'unit_price' => $unitPrice,
		'unit_price_map' => $unitPriceMapOut,
		'unit_title_map' => $unitTitleMapOut,
		'amount' => $amount,
		'vpn_no' => $internalProfileNo,
		'internal_profile_no' => $internalProfileNo,
		'business_vpn_no' => $businessVpnNo,
		'it_name' => $target['it_name'],
		'vpn_user_id' => $target['vpn_user_id'],
	));
}

if ($action === 'create_order') {
	if (!ensure_order_table($mysqli)) {
		fail_json('주문 테이블 준비 실패: ' . $mysqli->error);
	}

	$unit = req('unit', 'hour');
	$qty = (int)req('qty', 1);
	if ($qty < 1) {
		$qty = 1;
	}

	$unitPrice = get_unit_price($unit, $businessVpnNo);
	if ($unitPrice <= 0) {
		fail_json('지원하지 않는 기간 단위입니다.');
	}

	$amount = $unitPrice * $qty;
	$orderNo = make_order_no();

	$sql = "INSERT INTO vpn_extend_orders
		(order_no, mb_id, vpn_no, vpn_business_no, it_name, vpn_user_id, unit_type, qty, unit_price, amount, memo, status, created_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NOW())";
	$stmt = $mysqli->prepare($sql);
	if (!$stmt) {
		fail_json('주문 생성 준비 실패: ' . $mysqli->error);
	}

	$vpnNo = $internalProfileNo;
	$vpnBusinessNo = $businessVpnNo > 0 ? $businessVpnNo : 0;
	$itName = $target['it_name'];
	$vpnUserId = $target['vpn_user_id'];
	$orderMemo = 'req_profile_id=' . $profileId . ';req_vpn_no=' . req('vpn_no', '') . ';resolved_internal_no=' . $vpnNo . ';resolved_business_no=' . $vpnBusinessNo;

	$stmt->bind_param('ssiisssiiis', $orderNo, $mbId, $vpnNo, $vpnBusinessNo, $itName, $vpnUserId, $unit, $qty, $unitPrice, $amount, $orderMemo);
	if (!$stmt->execute()) {
		$err = $stmt->error;
		$stmt->close();
		fail_json('주문 생성 실패: ' . $err);
	}
	$stmt->close();

	$payUrl = $_SERVER['PHP_SELF'] . '?action=pay&order_no=' . urlencode($orderNo) . '&mb_id=' . urlencode($mbId);

	success_json('연장 주문이 생성되었습니다.', array(
		'action' => 'create_order',
		'order_no' => $orderNo,
		'amount' => $amount,
		'vpn_no' => $vpnNo,
		'internal_profile_no' => $vpnNo,
		'business_vpn_no' => $vpnBusinessNo,
		'pay_url' => $payUrl,
	));
}

if ($action === 'approve') {
	$orderNo = req('order_no', '', array('orderNo'));
	if ($orderNo === '') {
		fail_json('order_no가 필요합니다.');
	}

	$approveApiMode = is_api_request();
	$forceHtmlApprove = (req('approve_mode', '', array('approveMode')) === 'html');
	if ($forceHtmlApprove) {
		$approveApiMode = false;
	}
	extend_debug_log('approve_response_mode', array(
		'order_no' => $orderNo,
		'is_api_request' => $approveApiMode,
		'force_html_approve' => $forceHtmlApprove,
		'http_accept' => isset($_SERVER['HTTP_ACCEPT']) ? (string)$_SERVER['HTTP_ACCEPT'] : '',
		'http_x_requested_with' => isset($_SERVER['HTTP_X_REQUESTED_WITH']) ? (string)$_SERVER['HTTP_X_REQUESTED_WITH'] : '',
		'content_type' => isset($_SERVER['CONTENT_TYPE']) ? (string)$_SERVER['CONTENT_TYPE'] : '',
	));

	try {
		$sql = 'SELECT * FROM vpn_extend_orders WHERE order_no=? AND mb_id=? LIMIT 1 FOR UPDATE';
		$stmt = $mysqli->prepare($sql);
		if (!$stmt) {
			throw new Exception('주문 조회 prepare 실패: ' . $mysqli->error);
		}
		$stmt->bind_param('ss', $orderNo, $mbId);
		if (!$stmt->execute()) {
			throw new Exception('주문 조회 execute 실패: ' . $stmt->error);
		}
		$order = $stmt->get_result()->fetch_assoc();
		$stmt->close();

		if (!$order) {
			throw new Exception('주문을 찾을 수 없습니다.');
		}
		if ($order['status'] === 'paid') {
			throw new Exception('이미 승인된 주문입니다.');
		}

		$vpnNo = (int)$order['vpn_no'];
		$vpnBusinessNo = isset($order['vpn_business_no']) ? (int)$order['vpn_business_no'] : 0;

		$selectCols = array('no', 'mb_id', 'remark', 'server_name', 'vpn_user_id', 'vpn_user_pwd', 'vpn_id_expire');
		$selectCols[] = table_has_column($mysqli, 'vpnid_list', 'od_id') ? 'od_id' : 'NULL AS od_id';
		$selectCols[] = table_has_column($mysqli, 'vpnid_list', 'memo') ? 'memo' : 'NULL AS memo';
		$sql = 'SELECT ' . implode(', ', $selectCols) . ' FROM vpnid_list WHERE no=? AND (mb_id=? OR remark=?) AND enable=1 LIMIT 1 FOR UPDATE';
		$stmt = $mysqli->prepare($sql);
		if (!$stmt) {
			throw new Exception('프로필 조회 prepare 실패: ' . $mysqli->error);
		}
		$stmt->bind_param('iss', $vpnNo, $mbId, $mbId);
		if (!$stmt->execute()) {
			throw new Exception('프로필 조회 execute 실패: ' . $stmt->error);
		}
		$profile = $stmt->get_result()->fetch_assoc();
		$stmt->close();

		if (!$profile) {
			throw new Exception('연장 대상 프로필이 없거나 비활성 상태입니다.');
		}

		extend_debug_log('approve_profile_locked', array(
			'order_no' => $orderNo,
			'internal_profile_no' => $vpnNo,
			'business_vpn_no' => $vpnBusinessNo,
			'mb_id' => $mbId,
			'profile_mb_id' => isset($profile['mb_id']) ? $profile['mb_id'] : '',
			'profile_remark' => isset($profile['remark']) ? $profile['remark'] : '',
		));

		$paymentTs = time();
		$before = $profile['vpn_id_expire'];
		$baseTs = resolve_extend_base_ts($before, $paymentTs);
		// 구독이 이미 만료된 경우 $baseTs는 현재 시각이 된다.
		// 이력/포인트 메시지의 일수·시간 계산 기준을 $before(과거 만료일)가 아닌
		// $baseTs(실제 연장 기산점)로 맞춰야 "0일1시간" 등 주문한 기간이 정확하게 표시된다.
		$effectiveBefore = to_datetime_str($baseTs);

		$unit = $order['unit_type'];
		$qty = (int)$order['qty'];

		if ($unit === 'month' || $unit === 'year') {
			$afterTs = strtotime('+' . $qty . ' month', $baseTs);
			if ($unit === 'year') {
				$afterTs = strtotime('+' . $qty . ' year', $baseTs);
			}
		} else {
			$sec = calc_seconds($unit, $qty);
			if ($sec <= 0) {
				throw new Exception('기간 단위 계산 실패');
			}
			$afterTs = $baseTs + $sec;
		}

		$after = to_datetime_str($afterTs);

		$sql = 'UPDATE vpnid_list
				SET vpn_id_expire=?, enable=1, update_datetime=NOW()
				WHERE no=? AND (mb_id=? OR remark=?) AND enable=1 LIMIT 1';
		$stmt = $mysqli->prepare($sql);
		if (!$stmt) {
			throw new Exception('프로필 갱신 prepare 실패: ' . $mysqli->error);
		}
		$stmt->bind_param('siss', $after, $vpnNo, $mbId, $mbId);
		if (!$stmt->execute()) {
			throw new Exception('프로필 갱신 execute 실패: ' . $stmt->error);
		}
		$stmt->close();

		$resolvedOrderContext = null;
		$profileOdId = isset($profile['od_id']) ? trim((string)$profile['od_id']) : '';
		$resolvedItId = $vpnBusinessNo > 0 ? $vpnBusinessNo : parse_vpn_number_from_it_name(isset($order['it_name']) ? (string)$order['it_name'] : '');
		extend_debug_log('approve_order_sync_resolve', array(
			'order_no' => $orderNo,
			'profile_od_id' => $profileOdId,
			'vpn_business_no' => $vpnBusinessNo,
			'order_it_name' => isset($order['it_name']) ? (string)$order['it_name'] : '',
			'resolved_it_id' => $resolvedItId,
		));
		if ($resolvedItId > 0) {
			$resolvedOrderContext = apply_extend_history_to_shop($mysqli, $mbId, $profileOdId, $resolvedItId, $effectiveBefore, $after, (int)$order['amount']);
		}
		if (!$resolvedOrderContext) {
			throw new Exception('주문상세 동기화 대상 식별에 실패했습니다.');
		}

		// ──────────────────────────────────────────────────────────────────────
		// [BUG-FIX] 계약연장 포인트 차감 누락 수정
		// apply_extend_history_to_shop() 는 shop 레코드(cart/item/order)만 갱신하고
		// insert_point()를 호출하지 않아 회원 포인트 원장에 차감이 기록되지 않았다.
		// rel_action=$orderNo 를 idempotency 키로 사용하므로 재시도 시 중복 차감 없음.
		// ──────────────────────────────────────────────────────────────────────
		$extPointAmount = (int)$order['amount'];
		if ($extPointAmount > 0 && function_exists('insert_point')) {
			$extOdId   = isset($resolvedOrderContext['od_id'])        ? trim((string)$resolvedOrderContext['od_id'])  : '';
			$extDays   = isset($resolvedOrderContext['extend_days'])   ? (int)$resolvedOrderContext['extend_days']    : 0;
			$extHours  = isset($resolvedOrderContext['extend_hours'])  ? (int)$resolvedOrderContext['extend_hours']   : 0;
			$mbNamePt  = '';
			if (function_exists('get_member')) {
				$mbInfoPt = get_member($mbId);
				$mbNamePt = isset($mbInfoPt['mb_name']) ? trim((string)$mbInfoPt['mb_name']) : '';
			}

			// ── 포인트 차감 직전 행 잠금 + 잔액 확인 (이중 지출 방지) ──
			$_safeMbExt = $mysqli->real_escape_string($mbId);
			$mysqli->begin_transaction();
			$_balRowExt = $mysqli->query("SELECT mb_point FROM {$g5['member_table']} WHERE mb_id='{$_safeMbExt}' LIMIT 1 FOR UPDATE");
			$_balExt    = $_balRowExt ? $_balRowExt->fetch_assoc() : null;
			$_pointsExt = isset($_balExt['mb_point']) ? (int)$_balExt['mb_point'] : 0;
			if ($_pointsExt < $extPointAmount) {
				$mysqli->rollback();
				throw new Exception('포인트 잔액이 부족합니다. (보유: ' . number_format($_pointsExt) . 'P / 결제 필요: ' . number_format($extPointAmount) . 'P)');
			}

			$extItName = isset($order['it_name']) ? trim((string)$order['it_name']) : 'VPN-' . $resolvedItId;
			$pointContent = "계약연장 주문번호 {$extOdId} {$extItName} ({$extDays}일{$extHours}시간) 추가요금 결제 (이용자: {$mbId} / {$mbNamePt})";
			$ptResult = insert_point(
				$mbId,
				-$extPointAmount,
				$pointContent,
				'@extendOrderApprove',
				$mbId . '|' . $mbId,
				$orderNo        // 주문번호 = 중복 방지 키 (insert_point 자체 중복 검사)
			);
			$mysqli->commit();
			// $ptResult === -1 : 이미 차감됨(idempotent skip), 0 : 포인트 시스템 비활성 등
			extend_debug_log('point_deduct_result', array(
				'order_no'   => $orderNo,
				'mb_id'      => $mbId,
				'amount'     => -$extPointAmount,
				'pt_result'  => $ptResult,
				'od_id'      => $extOdId,
			));
		}

		// ──────────────────────────────────────────────────────────────────────
		// 프로비저닝 큐 등록
		// - 모니터(ikev2_provision_monitor.php)에 항목 노출
		// - 실패 시 크론 자동 재시도
		// - 성공 시 vpnlink_ikev2_attempt_queue 가 프로비저닝 성공 시각 기준으로
		//   만료일을 재산정하여 cart/item/vpnid_list 를 갱신한다.
		// ON DUPLICATE KEY UPDATE 로 기존 큐 항목을 재활성화(status='queued')한다.
		// ──────────────────────────────────────────────────────────────────────
		$queueOdId = isset($resolvedOrderContext['od_id']) ? trim((string)$resolvedOrderContext['od_id']) : '';
		if ($queueOdId !== '' && function_exists('vpnlink_ikev2_queue_order')) {
			vpnlink_ikev2_queue_order($queueOdId, 'extend_order_approve');
		}

		// 연장 주문도 신규 주문과 동일하게 주문 정보를 포함한 메모를 강제 재생성한다.
		$resolvedSyncMemo = extend_build_shop_order_memo($mbId, $order, $resolvedOrderContext, $after);

		if ($resolvedSyncMemo === '' || $resolvedSyncMemo === '-' || $resolvedSyncMemo === '—') {
			$fallbackMemoParts = array('쇼핑몰 자동등록');
			if ($mbId !== '') {
				$fallbackMemoParts[] = '회원ID: ' . $mbId;
			}
			if (isset($order['it_name']) && trim((string)$order['it_name']) !== '') {
				$fallbackMemoParts[] = '상품명: ' . trim((string)$order['it_name']);
			}
			$resolvedOdId = isset($resolvedOrderContext['od_id']) ? trim((string)$resolvedOrderContext['od_id']) : '';
			if ($resolvedOdId !== '') {
				$fallbackMemoParts[] = '주문번호: ' . $resolvedOdId;
			}
			if ($resolvedItId > 0) {
				$fallbackMemoParts[] = '상품코드: ' . (string)$resolvedItId;
			}

			$resolvedSyncMemo = implode(' Œ ', $fallbackMemoParts);
			$metaMarkers = array('[source:auto_shop_order]');
			if ($resolvedOdId !== '') {
				$metaMarkers[] = '[od:' . $resolvedOdId . ']';
			}
			if ($resolvedItId > 0) {
				$metaMarkers[] = '[it:' . (string)$resolvedItId . ']';
			}
			$resolvedSyncMemo = trim($resolvedSyncMemo . ' ' . implode('', $metaMarkers));
		}

		extend_debug_log('approve_memo_resolved', array(
			'order_no' => $orderNo,
			'memo_len' => strlen((string)$resolvedSyncMemo),
			'memo_preview' => (strlen((string)$resolvedSyncMemo) > 220) ? (substr((string)$resolvedSyncMemo, 0, 220) . '...[truncated]') : (string)$resolvedSyncMemo,
		));

		if ($resolvedSyncMemo !== '' && table_has_column($mysqli, 'vpnid_list', 'memo')) {
			$sqlMemoUpdate = 'UPDATE vpnid_list
						SET memo=?, update_datetime=NOW()
						WHERE no=? AND (mb_id=? OR remark=?) AND enable=1 LIMIT 1';
			$stmtMemo = $mysqli->prepare($sqlMemoUpdate);
			if ($stmtMemo) {
				$stmtMemo->bind_param('siss', $resolvedSyncMemo, $vpnNo, $mbId, $mbId);
				$stmtMemo->execute();
				$stmtMemo->close();
			}
		}

		$sql = "UPDATE vpn_extend_orders
				SET status='paid', paid_at=NOW(), approved_by='app'
				WHERE order_no=? AND mb_id=? LIMIT 1";
		$stmt = $mysqli->prepare($sql);
		if (!$stmt) {
			throw new Exception('주문 상태 갱신 prepare 실패: ' . $mysqli->error);
		}
		$stmt->bind_param('ss', $orderNo, $mbId);
		if (!$stmt->execute()) {
			throw new Exception('주문 상태 갱신 execute 실패: ' . $stmt->error);
		}
		$stmt->close();

		$syncAttempted = false;
		$syncOk = false;
		$syncMessage = '';
		$syncError = '';
		$syncAutoEnabled = false;
		$sellerEnabledBeforeSync = false;
		$sellerAccountStateBefore = '';
		$sellerEnabledAfterSync = false;
		$sellerAccountState = '';
		$activationTransition = 'unknown';
		$syncAttemptedEndpoints = array();
		$syncFinalEndpoint = '';
		$syncFallbackRetried = false;
		$syncSecurePayloadRet = array(
			'attempted' => false,
			'ok' => false,
			'endpoint' => '',
			'error' => '',
			'message' => ''
		);
		$syncUserId = isset($profile['vpn_user_id']) ? trim((string)$profile['vpn_user_id']) : '';
		$syncGateway = isset($profile['server_name']) ? trim((string)$profile['server_name']) : '';
		$syncViaVpnUsersRet = array(
			'attempted' => false,
			'success' => false,
			'status' => 'not_attempted'
		);
		$syncViaVpnUsersUpdateRet = array(
			'attempted' => false,
			'success' => false,
			'status' => 'not_attempted'
		);

		if ($syncUserId !== '') {
			$syncViaVpnUsersRet = extend_call_vpn_users_extend_absolute($syncUserId, $mbId, $syncGateway, $after, $resolvedSyncMemo);
			if (!empty($syncViaVpnUsersRet['attempted'])) {
				$syncAttempted = true;
				if (isset($syncViaVpnUsersRet['attempt_trace']) && is_array($syncViaVpnUsersRet['attempt_trace'])) {
					foreach ($syncViaVpnUsersRet['attempt_trace'] as $tr) {
						if (isset($tr['endpoint']) && trim((string)$tr['endpoint']) !== '') {
							$syncAttemptedEndpoints[] = trim((string)$tr['endpoint']);
						}
					}
				}
				if (empty($syncAttemptedEndpoints) && isset($syncViaVpnUsersRet['endpoint'])) {
					$syncAttemptedEndpoints[] = (string)$syncViaVpnUsersRet['endpoint'];
				}
				$syncFinalEndpoint = isset($syncViaVpnUsersRet['endpoint']) ? (string)$syncViaVpnUsersRet['endpoint'] : '';
			}
			if (!empty($syncViaVpnUsersRet['success'])) {
				$syncOk = true;
				$syncMessage = 'vpn_users_extend_expire_synced';
			}

			$syncViaVpnUsersUpdateRet = extend_call_vpn_users_update_user($syncUserId, $mbId, $syncGateway, $after, $resolvedSyncMemo);
			if (!empty($syncViaVpnUsersUpdateRet['attempted'])) {
				$syncAttempted = true;
				if (isset($syncViaVpnUsersUpdateRet['attempt_trace']) && is_array($syncViaVpnUsersUpdateRet['attempt_trace'])) {
					foreach ($syncViaVpnUsersUpdateRet['attempt_trace'] as $tr2) {
						if (isset($tr2['endpoint']) && trim((string)$tr2['endpoint']) !== '') {
							$syncAttemptedEndpoints[] = trim((string)$tr2['endpoint']);
						}
					}
				}
				if (!empty($syncViaVpnUsersUpdateRet['endpoint'])) {
					$syncFinalEndpoint = (string)$syncViaVpnUsersUpdateRet['endpoint'];
				}
			}
			if (!empty($syncViaVpnUsersUpdateRet['success'])) {
				$syncOk = true;
				if ($syncMessage === '') {
					$syncMessage = 'vpn_users_update_user_synced';
				}
			}
		}

		$syncSecurePayloadRet = extend_sync_vpn_server_secure_upsert($profile, $after, $resolvedSyncMemo, 'extended_order_secure_upsert');
		if (!empty($syncSecurePayloadRet['attempted'])) {
			$syncAttempted = true;
			if (!empty($syncSecurePayloadRet['endpoint'])) {
				$syncAttemptedEndpoints[] = (string)$syncSecurePayloadRet['endpoint'];
				$syncFinalEndpoint = (string)$syncSecurePayloadRet['endpoint'];
			}
			if (!empty($syncSecurePayloadRet['ok'])) {
				$syncOk = true;
				if ($syncMessage === '') {
					$syncMessage = isset($syncSecurePayloadRet['message']) && $syncSecurePayloadRet['message'] !== ''
						? (string)$syncSecurePayloadRet['message']
						: 'secure_upsert_synced';
				}
			} else if (!$syncOk && $syncError === '') {
				$syncError = isset($syncSecurePayloadRet['error']) && $syncSecurePayloadRet['error'] !== ''
					? (string)$syncSecurePayloadRet['error']
					: 'secure_upsert_failed';
			}
		}

		if (!$syncOk && function_exists('vpnlink_ikev2_force_sync_vpnid_member')) {
			$syncFallbackRetried = !empty($syncViaVpnUsersRet['attempted']);
			$syncAttempted = true;
			$syncRet = vpnlink_ikev2_force_sync_vpnid_member(
				isset($profile['server_name']) ? (string)$profile['server_name'] : '',
				isset($profile['vpn_user_id']) ? (string)$profile['vpn_user_id'] : ''
			);
			$syncOk = !empty($syncRet['ok']);
			$syncAttemptedEndpoints = (isset($syncRet['attempted_endpoints']) && is_array($syncRet['attempted_endpoints'])) ? $syncRet['attempted_endpoints'] : array();
			$syncFinalEndpoint = isset($syncRet['final_endpoint']) ? (string)$syncRet['final_endpoint'] : '';
			if ($syncOk) {
				$syncMessage = isset($syncRet['message']) ? (string)$syncRet['message'] : 'synced';
				$syncAutoEnabled = !empty($syncRet['auto_enabled']);
				$sellerEnabledBeforeSync = !empty($syncRet['seller_enabled_before_sync']);
				$sellerAccountStateBefore = isset($syncRet['seller_account_state_before']) ? (string)$syncRet['seller_account_state_before'] : '';
				$sellerEnabledAfterSync = !empty($syncRet['seller_enabled_after_sync']);
				$sellerAccountState = isset($syncRet['seller_account_state']) ? (string)$syncRet['seller_account_state'] : '';
				$activationTransition = isset($syncRet['activation_transition']) ? (string)$syncRet['activation_transition'] : 'unknown';
			} else {
				$syncError = isset($syncRet['error']) ? (string)$syncRet['error'] : 'sync_failed';

				if (!$syncOk && function_exists('vpnlink_ikev2_send_secure_payload') && !empty($profile['server_name']) && !empty($profile['vpn_user_id']) && !empty($profile['vpn_user_pwd'])) {
					$serverNameRaw = trim((string)$profile['server_name']);
					$hasScheme = (bool)preg_match('#^https?://#i', $serverNameRaw);
					if (!$hasScheme) {
						$_legacyScheme = (function_exists('vpnlink_ikev2_is_ip_address') && vpnlink_ikev2_is_ip_address($serverNameRaw)) ? 'http' : 'https';
						$legacyEndpoint = $_legacyScheme . '://' . rtrim($serverNameRaw, '/') . '/mylib4ikev2svr/ikev2_ipsec.secrets_update.php';
						$payload = array(
							'operation' => 'upsert',
							'source' => 'auto_shop_order',
							'trigger_source' => 'extended_order_legacy_payload_fallback',
							'admin' => 'shop_auto',
							'mb_id' => (string)$profile['vpn_user_id'],
							'od_id' => isset($profile['od_id']) ? (string)$profile['od_id'] : '',
							'vpn_user_id' => (string)$profile['vpn_user_id'],
							'vpn_user_pw' => (string)$profile['vpn_user_pwd'],
							'expire' => (string)$after,
							'memo' => (string)$resolvedSyncMemo
						);

						$legacyRet = vpnlink_ikev2_send_secure_payload($legacyEndpoint, $payload);
						$syncFallbackRetried = true;
						$syncAttemptedEndpoints[] = $legacyEndpoint;

						if (!empty($legacyRet['ok'])) {
							$syncOk = true;
							$syncError = '';
							$syncMessage = isset($legacyRet['message']) ? (string)$legacyRet['message'] : 'synced_legacy_payload_fallback';
							$syncFinalEndpoint = $legacyEndpoint;
						} else {
							$syncError = isset($legacyRet['error']) ? (string)$legacyRet['error'] : $syncError;
							if ($syncFinalEndpoint === '') {
								$syncFinalEndpoint = $legacyEndpoint;
							}
						}
					}
				}
			}
		}

		if (!$syncOk && !$syncAttempted && !empty($syncViaVpnUsersRet['attempted'])) {
			$syncAttempted = true;
			$syncError = isset($syncViaVpnUsersRet['status']) ? (string)$syncViaVpnUsersRet['status'] : 'vpn_users_sync_failed';
		}

		extend_debug_log('approve_remote_sync', array(
			'order_no' => $orderNo,
			'internal_profile_no' => $vpnNo,
			'business_vpn_no' => $vpnBusinessNo,
			'server_name' => isset($profile['server_name']) ? (string)$profile['server_name'] : '',
			'vpn_user_id' => isset($profile['vpn_user_id']) ? (string)$profile['vpn_user_id'] : '',
			'sync_via_vpn_users' => $syncViaVpnUsersRet,
			'sync_via_vpn_users_update' => $syncViaVpnUsersUpdateRet,
			'sync_secure_payload' => $syncSecurePayloadRet,
			'sync_attempted' => $syncAttempted,
			'sync_ok' => $syncOk,
			'sync_message' => $syncMessage,
			'sync_error' => $syncError,
			'sync_auto_enabled' => $syncAutoEnabled,
			'seller_enabled_before_sync' => $sellerEnabledBeforeSync,
			'seller_account_state_before' => $sellerAccountStateBefore,
			'seller_enabled_after_sync' => $sellerEnabledAfterSync,
			'seller_account_state' => $sellerAccountState,
			'activation_transition' => $activationTransition,
			'sync_attempted_endpoints' => $syncAttemptedEndpoints,
			'sync_final_endpoint' => $syncFinalEndpoint,
			'sync_fallback_retried' => $syncFallbackRetried,
		));

		if ($approveApiMode) {
			success_json('결제가 승인되어 사용기간이 연장되었습니다.', array(
				'action' => 'approve',
				'order_no' => $orderNo,
				'vpn_no' => $vpnNo,
				'internal_profile_no' => $vpnNo,
				'business_vpn_no' => $vpnBusinessNo,
				'expire_before' => $before,
				'expire_after' => $after,
				'before_expire' => $before,
				'after_expire' => $after,
				'sync_attempted' => $syncAttempted,
				'sync_ok' => $syncOk,
				'sync_message' => $syncMessage,
				'sync_error' => $syncError,
				'sync_auto_enabled' => $syncAutoEnabled,
				'seller_enabled_before_sync' => $sellerEnabledBeforeSync,
				'seller_account_state_before' => $sellerAccountStateBefore,
				'seller_enabled_after_sync' => $sellerEnabledAfterSync,
				'seller_account_state' => $sellerAccountState,
				'activation_transition' => $activationTransition,
				'sync_attempted_endpoints' => $syncAttemptedEndpoints,
				'sync_final_endpoint' => $syncFinalEndpoint,
				'sync_fallback_retried' => $syncFallbackRetried,
				'order_sync_od_id' => isset($resolvedOrderContext['od_id']) ? $resolvedOrderContext['od_id'] : '',
				'order_sync_it_id' => isset($resolvedOrderContext['it_id']) ? (int)$resolvedOrderContext['it_id'] : 0,
				'order_sync_extend_days' => isset($resolvedOrderContext['extend_days']) ? (int)$resolvedOrderContext['extend_days'] : 0,
				'order_sync_extend_hours' => isset($resolvedOrderContext['extend_hours']) ? (int)$resolvedOrderContext['extend_hours'] : 0,
			));
		}

		$redirectUrl = $_SERVER['PHP_SELF']
			. '?action=pay&order_no=' . urlencode($orderNo)
			. '&mb_id=' . urlencode($mbId)
			. '&status=paid'
			. '&sync=' . urlencode($syncOk ? 'ok' : ($syncAttempted ? 'failed' : 'skipped'));
		if (!$syncOk && $syncError !== '') {
			$redirectUrl .= '&sync_error=' . urlencode($syncError);
		}
		if ($syncFinalEndpoint !== '') {
			$redirectUrl .= '&sync_endpoint=' . urlencode($syncFinalEndpoint);
		}
		$redirectUrl .= '&sync_auto_enabled=' . ($syncAutoEnabled ? '1' : '0');
		$redirectUrl .= '&seller_enabled_before_sync=' . ($sellerEnabledBeforeSync ? '1' : '0');
		if ($sellerAccountStateBefore !== '') {
			$redirectUrl .= '&seller_account_state_before=' . urlencode($sellerAccountStateBefore);
		}
		$redirectUrl .= '&seller_enabled_after_sync=' . ($sellerEnabledAfterSync ? '1' : '0');
		if ($sellerAccountState !== '') {
			$redirectUrl .= '&seller_account_state=' . urlencode($sellerAccountState);
		}
		$redirectUrl .= '&activation_transition=' . urlencode($activationTransition);
		redirect_with_html_fallback($redirectUrl);
	} catch (Exception $e) {
		if ($mysqli && method_exists($mysqli, 'rollback')) {
			@$mysqli->rollback();
		}
		$errorMsg = '승인 처리 실패: ' . $e->getMessage();
		extend_debug_log('approve_failed', array(
			'order_no' => $orderNo,
			'mb_id' => $mbId,
			'error' => $errorMsg,
		));

		if ($approveApiMode) {
			fail_json($errorMsg);
		}

		$redirectUrl = $_SERVER['PHP_SELF']
			. '?action=pay&order_no=' . urlencode($orderNo)
			. '&mb_id=' . urlencode($mbId)
			. '&status=failure'
			. '&error=' . urlencode($errorMsg);
		redirect_with_html_fallback($redirectUrl);
	}
}

if ($action === 'pay') {
	if (!ensure_order_table($mysqli)) {
		echo '주문 테이블 준비 실패';
		exit;
	}

	$orderNo = req('order_no', '', array('orderNo'));
	$sql = 'SELECT * FROM vpn_extend_orders WHERE order_no=? AND mb_id=? LIMIT 1';
	$stmt = $mysqli->prepare($sql);
	if (!$stmt) {
		echo '주문 조회 실패';
		exit;
	}
	$stmt->bind_param('ss', $orderNo, $mbId);
	$stmt->execute();
	$order = $stmt->get_result()->fetch_assoc();
	$stmt->close();

	if (!$order) {
		echo '주문 정보 없음';
		exit;
	}

	$uiStatus = strtolower((string)req('status', isset($order['status']) ? $order['status'] : 'pending'));
	if ($uiStatus === '') {
		$uiStatus = 'pending';
	}
	$statusLabel = '상태 미정';
	$statusClass = 'status-unknown';
	$errorNotice = req('error', '');
	$syncStatus = strtolower((string)req('sync', ''));
	$syncErrorNotice = req('sync_error', '');
	$syncEndpointNotice = req('sync_endpoint', '');
	$syncAutoEnabled = req('sync_auto_enabled', '0') === '1';
	$sellerEnabledBeforeSync = req('seller_enabled_before_sync', '0') === '1';
	$sellerAccountStateBefore = req('seller_account_state_before', '');
	$sellerEnabledAfterSync = req('seller_enabled_after_sync', '0') === '1';
	$sellerAccountState = req('seller_account_state', '');
	$activationTransition = req('activation_transition', 'unknown');
	if ($uiStatus === 'pending') {
		$statusLabel = '결제 대기';
		$statusClass = 'status-pending';
	} else if ($uiStatus === 'paid') {
		$statusLabel = '결제 승인 완료';
		$statusClass = 'status-paid';
	} else if ($uiStatus === 'failure' || $uiStatus === 'failed') {
		$statusLabel = '결제 실패';
		$statusClass = 'status-failed';
	}
	$isPaid = ($uiStatus === 'paid');
	?>
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>연장 결제 승인</title>
<style>
body{font-family:sans-serif;background:#f3f4f6;margin:0;padding:24px}
.box{max-width:520px;background:#fff;border:1px solid #ddd;padding:18px}
.btn{display:inline-block;border:0;background:#1f6feb;color:#fff;padding:10px 16px;cursor:pointer}
.status-badge{display:inline-block;margin:6px 0 12px 0;padding:6px 10px;border-radius:999px;font-size:13px;font-weight:700}
.status-pending{background:#fff3cd;color:#856404;border:1px solid #ffeeba}
.status-paid{background:#d4edda;color:#155724;border:1px solid #c3e6cb}
.status-failed{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb}
.status-unknown{background:#e2e3e5;color:#383d41;border:1px solid #d6d8db}
.status-note{margin-top:10px;color:#2f2f33;font-size:14px}
.status-auto-close{margin-top:8px;color:#155724;font-size:13px}
</style>
</head>
<body>
<div class="box">
	<h3>연장 결제 승인</h3>
	<div class="status-badge <?php echo h($statusClass); ?>"><?php echo h($statusLabel); ?></div>
	<p>주문번호: <?php echo h($order['order_no']); ?></p>
	<p>상품: <?php echo h($order['it_name']); ?> / VPN ID: <?php echo h($order['vpn_user_id']); ?></p>
	<p>내부 프로필 번호: <?php echo h(isset($order['vpn_no']) ? $order['vpn_no'] : ''); ?></p>
	<p>비즈니스 VPN 번호: <?php echo h(isset($order['vpn_business_no']) ? $order['vpn_business_no'] : ''); ?></p>
	<p>기간: <?php echo h($order['unit_type']); ?> x <?php echo h($order['qty']); ?></p>
	<p>결제금액: <?php echo h($order['amount']); ?></p>

	<?php if (!$isPaid) { ?>
	<form method="post" action="<?php echo h($_SERVER['PHP_SELF']); ?>">
		<input type="hidden" name="action" value="approve">
		<input type="hidden" name="approve_mode" value="html">
		<input type="hidden" name="mb_id" value="<?php echo h($mbId); ?>">
		<input type="hidden" name="order_no" value="<?php echo h($order['order_no']); ?>">
		<button class="btn" type="submit">결제 승인</button>
	</form>
	<?php } else { ?>
	<div class="status-auto-close" id="autoCloseNote">잠시 후 자동으로 이전 화면으로 돌아갑니다.</div>
	<?php } ?>
	<?php if ($uiStatus === 'failure' || $uiStatus === 'failed') { ?>
	<div class="status-note"><?php echo h($errorNotice !== '' ? $errorNotice : '승인 처리 중 오류가 발생했습니다. 다시 시도해 주세요.'); ?></div>
	<?php } ?>
	<?php if ($uiStatus === 'paid' && $syncStatus === 'failed') { ?>
	<div class="status-note">IKEv2 서버 즉시 반영에 실패했습니다. 잠시 후 자동 재동기화되거나 관리자 확인이 필요합니다. (<?php echo h($syncErrorNotice !== '' ? $syncErrorNotice : 'sync_failed'); ?>)</div>
	<?php if ($syncEndpointNotice !== '') { ?>
	<div class="status-note">동기화 시도 엔드포인트: <?php echo h($syncEndpointNotice); ?></div>
	<?php } ?>
	<?php } ?>
	<?php if ($uiStatus === 'paid' && $syncStatus === 'ok') { ?>
	<div class="status-note">IKEv2 서버 동기화 완료<?php echo $syncAutoEnabled ? ' (계정 자동 활성화 적용)' : ''; ?>.</div>
	<div class="status-note">판매자 계정 상태(동기화 전): <?php echo $sellerEnabledBeforeSync ? '활성' : '비활성'; ?><?php echo $sellerAccountStateBefore !== '' ? ' (' . h($sellerAccountStateBefore) . ')' : ''; ?></div>
	<div class="status-note">판매자 계정 상태(동기화 후): <?php echo $sellerEnabledAfterSync ? '활성' : '비활성'; ?><?php echo $sellerAccountState !== '' ? ' (' . h($sellerAccountState) . ')' : ''; ?></div>
	<div class="status-note">활성화 전이: <?php echo h($activationTransition); ?></div>
	<?php } ?>
</div>
<?php if ($isPaid) { ?>
<script>
(function(){
	setTimeout(function(){
		try { window.close(); } catch (e) {}
		try { window.history.back(); } catch (e2) {}
	}, 1200);
})();
</script>
<?php } ?>
</body>
</html>
<?php
	exit;
}

?>
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light only">
<title>계약연장 · <?php echo h($target['it_name']); ?></title>
<style>
:root{color-scheme:light only}
html,body{background:#e9eaee;color:#1f1f1f}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;margin:0}
.wrap{max-width:780px;margin:20px auto;background:#fff;border:1px solid #d0d0d0}
.head{background:#1f2a44;color:#fff;padding:14px 16px;font-size:28px;font-weight:700}
.inner{padding:18px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:12px 0}
input{height:42px;border:1px solid #cfcfcf;padding:0 12px;font-size:20px}
input[type=number]{width:120px}
.btn{height:42px;border:0;background:#2f2f33;color:#fff;padding:0 18px;cursor:pointer;font-size:20px}
.sum{margin-top:18px;font-size:28px;font-weight:700;text-align:right}
.meta{font-size:22px;color:#333}
.msg{margin-top:10px;color:#1f6feb;font-size:18px}
.unit-picker{position:relative;min-width:260px}
.unit-btn{height:42px;min-width:260px;border:1px solid #cfcfcf;background:#fff;color:#111;padding:0 12px;font-size:20px;text-align:left;cursor:pointer}
.unit-menu{position:absolute;left:0;right:0;top:44px;z-index:30;background:#fff;border:1px solid #cfcfcf;box-shadow:0 6px 18px rgba(0,0,0,.15)}
.unit-option{display:block;width:100%;height:42px;border:0;border-top:1px solid #ececec;background:#fff;color:#111;padding:0 12px;font-size:18px;text-align:left;cursor:pointer}
.unit-option:first-child{border-top:0}
</style>
</head>
<body>
<div class="wrap">
	<div class="head">계약연장 · <?php echo h($target['it_name']); ?></div>
	<div class="inner">
		<div class="meta">현재 종료일 <?php echo h($currentExpire); ?></div>
		<?php
		$unitPriceMapUi = extend_get_unit_price_map($businessVpnNo);
		$unitOrderUi = array('hour', 'day', 'month', 'year');
		$unitTitlesUi = array();
		foreach ($unitOrderUi as $unitKey) {
			if (!isset($unitPriceMapUi[$unitKey])) {
				continue;
			}
			$unitTitlesUi[$unitKey] = extend_unit_display_name($unitKey) . ' (' . (int)$unitPriceMapUi[$unitKey] . ')';
		}
		if (empty($unitTitlesUi)) {
			$unitTitlesUi = array('hour' => '1시간 이용권 (75)');
		}
		$firstUnitUi = array_key_first($unitTitlesUi);
		$firstTitleUi = $unitTitlesUi[$firstUnitUi];
		$firstPriceUi = isset($unitPriceMapUi[$firstUnitUi]) ? (int)$unitPriceMapUi[$firstUnitUi] : 75;
		?>

		<div class="row">
			<div class="unit-picker">
				<button id="unitBtn" class="unit-btn" type="button"><?php echo h($firstTitleUi); ?></button>
				<div id="unitMenu" class="unit-menu" style="display:none">
					<?php foreach ($unitTitlesUi as $unitKey => $unitTitle) { ?>
					<button type="button" class="unit-option" data-unit="<?php echo h($unitKey); ?>"><?php echo h($unitTitle); ?></button>
					<?php } ?>
				</div>
			</div>
			<input type="number" id="qty" min="1" value="1">
			<button class="btn" id="payBtn" type="button">결제하기</button>
		</div>

		<div class="meta" id="preview">선택기간: 1시간</div>
		<div class="sum" id="sum">연장 주문합계: <?php echo h($firstPriceUi); ?></div>
		<div class="msg" id="msg"></div>
	</div>
</div>

<script>
(function(){
	var unitBtn = document.getElementById('unitBtn');
	var unitMenu = document.getElementById('unitMenu');
	var qtyEl = document.getElementById('qty');
	var sumEl = document.getElementById('sum');
	var previewEl = document.getElementById('preview');
	var msgEl = document.getElementById('msg');
	var payBtn = document.getElementById('payBtn');
	var selectedUnit = <?php echo json_encode($firstUnitUi, JSON_UNESCAPED_UNICODE); ?>;

	var unitTitles = <?php echo json_encode($unitTitlesUi, JSON_UNESCAPED_UNICODE); ?>;

	function unitLabel(u){
		if(u==='hour') return '시간';
		if(u==='day') return '일';
		if(u==='week') return '주';
		if(u==='month') return '개월';
		if(u==='year') return '년';
		return '';
	}

	function recalc(){
		var f = new FormData();
		f.append('action','quote');
		f.append('api','1');
		f.append('mb_id','<?php echo h($mbId); ?>');
		f.append('profile_id','<?php echo h($target['no']); ?>');
		f.append('unit', selectedUnit);
		f.append('qty', qtyEl.value || '1');

		fetch('<?php echo h($_SERVER['PHP_SELF']); ?>', {method:'POST', body:f})
			.then(function(r){ return r.json(); })
			.then(function(j){
				if(j.result!=='success'){
					msgEl.textContent = j.msg || '실패';
					return;
				}
				sumEl.textContent = '연장 주문합계: ' + j.amount;
				previewEl.textContent = '선택기간: ' + j.qty + unitLabel(j.unit) + ' / 연장 후: ' + j.new_expire;
				unitBtn.textContent = unitTitles[selectedUnit] || unitTitles.hour;
				msgEl.textContent = '';
			})
			.catch(function(){
				msgEl.textContent = '통신 오류';
			});
	}

	unitBtn.addEventListener('click', function(){
		unitMenu.style.display = unitMenu.style.display === 'none' ? 'block' : 'none';
	});

	Array.prototype.forEach.call(document.querySelectorAll('.unit-option'), function(btn){
		btn.addEventListener('click', function(){
			selectedUnit = btn.getAttribute('data-unit') || 'hour';
			unitBtn.textContent = unitTitles[selectedUnit] || unitTitles.hour;
			unitMenu.style.display = 'none';
			recalc();
		});
	});

	document.addEventListener('click', function(e){
		if (!unitMenu.contains(e.target) && e.target !== unitBtn) {
			unitMenu.style.display = 'none';
		}
	});

	qtyEl.addEventListener('input', recalc);

	payBtn.addEventListener('click', function(){
		var f = new FormData();
		f.append('action','create_order');
		f.append('api','1');
		f.append('mb_id','<?php echo h($mbId); ?>');
		f.append('profile_id','<?php echo h($target['no']); ?>');
		f.append('unit', selectedUnit);
		f.append('qty', qtyEl.value || '1');

		fetch('<?php echo h($_SERVER['PHP_SELF']); ?>', {method:'POST', body:f})
			.then(function(r){ return r.json(); })
			.then(function(j){
				if(j.result!=='success'){
					msgEl.textContent = j.msg || '주문 실패';
					return;
				}
				window.location.href = j.pay_url;
			})
			.catch(function(){
				msgEl.textContent = '주문 통신 오류';
			});
	});

	recalc();
})();
</script>
</body>
</html>

