<?php
// VPN 서버 -> 판매자센터 '내 VPN서버관리' 등록/하트비트 수신 엔드포인트 (샵 서버 vpnlink.kr 측)
// URL: https://vpnlink.kr/mylib/ikev2_partner_server_register.php
//
// mode=register : 쇼핑몰 판매자(mb_id/mb_password)를 1회 인증한 뒤 서버를 등록하고
//                 이후 하트비트에 사용할 server_key 토큰을 발급합니다.
// mode=heartbeat: 등록시 발급받은 server_key만으로 최신 상태를 갱신합니다(비밀번호 불필요).
//
// 공유 비밀키(X-VPN-Sync-Signature, IKEV2_SHOP_SYNC_SHARED_SECRET)를 필수로 요구합니다
// (비밀번호를 다루는 엔드포인트이므로 선택이 아닌 필수).

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

@date_default_timezone_set('Asia/Seoul');

$projectRoot = realpath(__DIR__ . '/..');
if ($projectRoot === false) {
    $projectRoot = dirname(__DIR__);
}

function register_out($ok, $extra = array(), $code = 200) {
    if (!headers_sent()) {
        http_response_code((int)$code);
    }
    $base = array('ok' => (bool)$ok, 'ts' => date('c'));
    echo json_encode(array_merge($base, is_array($extra) ? $extra : array()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function register_load_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 = __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;
                }
            }
        }
    }
    if ($secret === '') {
        $secretFile = __DIR__ . '/ikev2_shop_sync_secret.php';
        if (is_file($secretFile) && is_readable($secretFile)) {
            $loaded = @include $secretFile;
            if (is_array($loaded) && isset($loaded['IKEV2_SHOP_SYNC_SHARED_SECRET'])) {
                $secret = trim((string)$loaded['IKEV2_SHOP_SYNC_SHARED_SECRET']);
            } elseif (is_string($loaded)) {
                $secret = trim($loaded);
            }
        }
    }

    return $secret;
}

function register_header($name) {
    $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
    return isset($_SERVER[$key]) ? trim((string)$_SERVER[$key]) : '';
}

// 간단한 IP당 등록 시도 횟수 제한 (비밀번호 무차별대입 방지)
function register_rate_limit_check($ip) {
    $dir = __DIR__ . '/../data';
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }
    $file = $dir . '/ikev2_partner_register_ratelimit.json';

    $map = array();
    if (is_file($file)) {
        $decoded = json_decode((string)@file_get_contents($file), true);
        if (is_array($decoded)) {
            $map = $decoded;
        }
    }

    $now = time();
    $windowSec = 60;
    $maxAttempts = 8;

    $attempts = isset($map[$ip]) && is_array($map[$ip]) ? $map[$ip] : array();
    $attempts = array_values(array_filter($attempts, function ($ts) use ($now, $windowSec) {
        return is_numeric($ts) && ($now - (int)$ts) < $windowSec;
    }));

    $allowed = count($attempts) < $maxAttempts;

    $attempts[] = $now;
    $map[$ip] = $attempts;

    // 오래된 IP 항목 정리 (파일 비대화 방지)
    foreach ($map as $k => $v) {
        $recent = is_array($v) ? array_filter($v, function ($ts) use ($now) {
            return is_numeric($ts) && ($now - (int)$ts) < 3600;
        }) : array();
        if (empty($recent)) {
            unset($map[$k]);
        } else {
            $map[$k] = array_values($recent);
        }
    }

    @file_put_contents($file, json_encode($map), LOCK_EX);

    return $allowed;
}

if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
    register_out(false, array('error' => 'method_not_allowed'), 405);
}

$secret = register_load_secret();
if ($secret === '') {
    register_out(false, array('error' => 'server_misconfigured', 'message' => '공유 비밀키(IKEV2_SHOP_SYNC_SHARED_SECRET)가 설정되어 있지 않습니다.'), 500);
}

$rawBody = file_get_contents('php://input');
if (!is_string($rawBody) || trim($rawBody) === '') {
    register_out(false, array('error' => 'empty_body'), 400);
}

$sigHeader = register_header('X-VPN-Sync-Signature');
if ($sigHeader === '') {
    register_out(false, array('error' => 'missing_signature'), 401);
}
$calc = hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($calc, $sigHeader)) {
    register_out(false, array('error' => 'bad_signature'), 401);
}

$body = json_decode($rawBody, true);
if (!is_array($body)) {
    register_out(false, array('error' => 'invalid_json'), 400);
}

// 변수명을 $mode 가 아닌 $vsMode 로 사용: extend/apms.extend.php 가 로드될 때
// 전역 $mode 를 apms_escape('mode', 0) 로 덮어써서 충돌하기 때문.
$vsMode = isset($body['mode']) ? trim((string)$body['mode']) : '';
if ($vsMode !== 'register' && $vsMode !== 'heartbeat') {
    register_out(false, array('error' => 'invalid_mode'), 400);
}

// GNUBoard5 코어 로드 (회원/DB 접근에 필수)
$commonFile = $projectRoot . '/_common.php';
if (!is_file($commonFile)) {
    register_out(false, array('error' => 'shop_core_missing'), 500);
}
$prevCwd = getcwd();
if (is_dir($projectRoot)) {
    @chdir($projectRoot);
}
require_once $commonFile;
if (is_string($prevCwd) && $prevCwd !== '') {
    @chdir($prevCwd);
}

if (!function_exists('sql_fetch') || !function_exists('sql_query') || !function_exists('sql_real_escape_string')) {
    register_out(false, array('error' => 'shop_db_unavailable'), 500);
}
if (!isset($GLOBALS['g5']) || !is_array($GLOBALS['g5']) || empty($GLOBALS['g5']['apms_vpn_server'])) {
    register_out(false, array('error' => 'vpn_server_table_not_configured'), 500);
}
$g5 = $GLOBALS['g5'];
$table = $g5['apms_vpn_server'];
$logTable = isset($g5['apms_vpn_server_log']) ? $g5['apms_vpn_server_log'] : '';

// 마이그레이션이 아직 적용되지 않아 컬럼이 없는 상태에서도 UPDATE/INSERT 전체가 실패하지
// 않도록, 옵션 컬럼은 실제 존재 여부를 확인한 뒤에만 SET 절에 포함시킨다.
function register_column_exists($table, $column) {
    static $cache = array();
    $key = $table . '.' . $column;
    if (isset($cache[$key])) {
        return $cache[$key];
    }
    $row = sql_fetch("show columns from {$table} like '" . sql_real_escape_string($column) . "'", false);
    $cache[$key] = is_array($row) && count($row) > 0;
    return $cache[$key];
}
$hasVpnTypesColumn = register_column_exists($table, 'vs_vpn_types');

$remoteIp = trim((string)($_SERVER['REMOTE_ADDR'] ?? ''));

// 등록/소유권 변경 감사 로그 기록 (해킹 등 비정상 변경 추적용).
// 성공/실패 여부와 무관하게 이 시점에 클라이언트가 보고한 자동측정 서버정보(등급/점수/권장개수/지표)도
// 함께 남겨, 추후 이력을 열람할 때 "언제 어떤 스펙으로 어떤 계정이 시도했는지"를 확인할 수 있게 한다.
// 로그 테이블이 없으면 조용히 무시(핵심 등록 기능이 로그 실패로 막히지 않도록).
function register_log_event($logTable, $ctx, $result, $message) {
    if ($logTable === '' || !function_exists('sql_query') || !function_exists('sql_real_escape_string')) {
        return;
    }
    $cols = array(
        'vs_ip' => $ctx['ip'] ?? '',
        'vs_hostname' => $ctx['hostname'] ?? '',
        'pt_id_attempted' => $ctx['pt_id'] ?? '',
        'previous_pt_id' => $ctx['previous_pt_id'] ?? '',
        'result' => $result,
        'message' => $message,
        'request_ip' => $ctx['request_ip'] ?? '',
        'vs_key_prefix' => $ctx['vs_key_prefix'] ?? '',
        'grade' => $ctx['grade'] ?? '',
        'grade_score' => (int)($ctx['grade_score'] ?? 0),
        'recommended_vpn_id_count' => (int)($ctx['recommended_vpn_id_count'] ?? 0),
        'current_vpn_id_count' => (int)($ctx['current_vpn_id_count'] ?? 0),
        'metrics_json' => $ctx['metrics_json'] ?? '',
        'created_at' => date('Y-m-d H:i:s')
    );
    $names = array();
    $vals = array();
    foreach ($cols as $c => $v) {
        $names[] = "`{$c}`";
        $vals[] = "'" . sql_real_escape_string((string)$v) . "'";
    }
    @sql_query("insert into {$logTable} (" . implode(',', $names) . ") values (" . implode(',', $vals) . ")", false);
}

if ($vsMode === 'register') {
    if (!register_rate_limit_check($remoteIp !== '' ? $remoteIp : 'unknown')) {
        register_out(false, array('error' => 'rate_limited', 'message' => '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.'), 429);
    }

    $mbId = trim((string)($body['mb_id'] ?? ''));
    $mbPassword = (string)($body['mb_password'] ?? '');
    $server = isset($body['server']) && is_array($body['server']) ? $body['server'] : array();
    $ip = trim((string)($server['ip'] ?? ''));
    $hostname = trim((string)($server['hostname'] ?? ''));
    $alias = trim((string)($server['alias'] ?? ''));
    $adminUrl = trim((string)($server['admin_url'] ?? ''));

    // 자동측정 서버정보는 인증 성공 여부와 무관하게 클라이언트가 항상 함께 보내므로,
    // 감사 로그에 남기기 위해 인증 체크 이전에 미리 읽어둔다.
    $grade = trim((string)($body['grade'] ?? ''));
    $gradeScore = (int)($body['grade_score'] ?? 0);
    $recommended = (int)($body['recommended_vpn_id_count'] ?? 0);
    $current = (int)($body['current_vpn_id_count'] ?? 0);
    $metrics = isset($body['metrics']) ? json_encode($body['metrics'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : '';
    $vpnTypes = trim((string)($body['vpn_types'] ?? ''));

    $logCtx = array(
        'ip' => $ip, 'hostname' => $hostname, 'request_ip' => $remoteIp,
        'grade' => $grade, 'grade_score' => $gradeScore,
        'recommended_vpn_id_count' => $recommended, 'current_vpn_id_count' => $current,
        'metrics_json' => $metrics
    );

    if ($mbId === '' || $mbPassword === '') {
        register_log_event($logTable, $logCtx + array('pt_id' => $mbId), 'missing_credentials', '아이디/비밀번호 누락');
        register_out(false, array('error' => 'missing_credentials', 'message' => '아이디/비밀번호가 필요합니다.'), 400);
    }

    $mbIdEsc = sql_real_escape_string($mbId);
    $member = sql_fetch("select mb_id, mb_password, mb_nick from {$g5['member_table']} where mb_id = '{$mbIdEsc}'", false);

    // 비밀번호가 틀린 경우와 계정이 없는 경우를 구분하지 않고 동일한 메시지로 응답 (계정 존재 여부 노출 방지)
    if (!is_array($member) || empty($member['mb_id']) || !function_exists('login_password_check') || !login_password_check($member, $mbPassword, $member['mb_password'])) {
        register_log_event($logTable, $logCtx + array('pt_id' => $mbId), 'invalid_credentials', '아이디/비밀번호 불일치');
        register_out(false, array('error' => 'invalid_credentials', 'message' => '아이디 또는 비밀번호가 올바르지 않습니다.'), 401);
    }

    $partner = function_exists('apms_partner') ? apms_partner($member['mb_id']) : null;
    if (!is_array($partner) || empty($partner['pt_partner'])) {
        register_log_event($logTable, $logCtx + array('pt_id' => $member['mb_id']), 'not_approved_seller', '승인된 판매자 아님');
        register_out(false, array('error' => 'not_approved_seller', 'message' => '승인된 판매자(셀러) 계정만 서버를 등록할 수 있습니다.'), 403);
    }

    if ($ip === '') {
        register_log_event($logTable, $logCtx + array('pt_id' => $member['mb_id']), 'missing_server_ip', '서버 IP 누락');
        register_out(false, array('error' => 'missing_server_ip', 'message' => '서버 IP 정보가 필요합니다.'), 400);
    }

    $vsKey = bin2hex(random_bytes(20));
    $now = date('Y-m-d H:i:s');

    $ptIdEsc = sql_real_escape_string($member['mb_id']);
    $ipEsc = sql_real_escape_string($ip);
    $hostnameEsc = sql_real_escape_string($hostname);

    $existing = sql_fetch("select vs_no from {$table} where pt_id = '{$ptIdEsc}' and vs_ip = '{$ipEsc}' and vs_hostname = '{$hostnameEsc}'", false);

    // 소유권 이전: 이 VPN 박스가 이전에 다른 판매자 계정으로 등록되어 있었다면(previous_server_key),
    // 새 계정으로 재등록하는 시점에 이전 판매자의 등록을 제거하여 "유령 카드"가 남지 않도록 한다.
    $previousServerKey = trim((string)($body['previous_server_key'] ?? ''));
    $transferredFrom = null;
    if ($previousServerKey !== '') {
        $prevKeyEsc = sql_real_escape_string($previousServerKey);
        $prevRow = sql_fetch("select vs_no, pt_id from {$table} where vs_key = '{$prevKeyEsc}'", false);
        if (is_array($prevRow) && !empty($prevRow['vs_no']) && (string)$prevRow['pt_id'] !== (string)$member['mb_id']) {
            $transferredFrom = (string)$prevRow['pt_id'];
            sql_query("delete from {$table} where vs_no = " . (int)$prevRow['vs_no']);
        }
    }

    $fields = array(
        'vs_key' => $vsKey,
        'pt_id' => $member['mb_id'],
        'vs_ip' => $ip,
        'vs_hostname' => $hostname,
        'vs_alias' => $alias,
        'vs_admin_url' => $adminUrl,
        'vs_grade' => $grade,
        'vs_grade_score' => $gradeScore,
        'vs_recommended_vpn_count' => $recommended,
        'vs_current_vpn_count' => $current,
        'vs_metrics_json' => $metrics,
        'vs_status' => 'online',
        'vs_last_seen_at' => $now,
        'vs_updated_at' => $now
    );
    if ($hasVpnTypesColumn) {
        $fields['vs_vpn_types'] = $vpnTypes;
    }

    if (is_array($existing) && !empty($existing['vs_no'])) {
        $setParts = array();
        foreach ($fields as $col => $val) {
            $setParts[] = "`{$col}` = '" . sql_real_escape_string((string)$val) . "'";
        }
        $vsNo = (int)$existing['vs_no'];
        sql_query("update {$table} set " . implode(', ', $setParts) . " where vs_no = {$vsNo}");
    } else {
        $fields['vs_registered_at'] = $now;
        $cols = array();
        $vals = array();
        foreach ($fields as $col => $val) {
            $cols[] = "`{$col}`";
            $vals[] = "'" . sql_real_escape_string((string)$val) . "'";
        }
        sql_query("insert into {$table} (" . implode(',', $cols) . ") values (" . implode(',', $vals) . ")");
    }

    register_log_event(
        $logTable,
        $logCtx + array('pt_id' => $member['mb_id'], 'previous_pt_id' => (string)$transferredFrom, 'vs_key_prefix' => substr($vsKey, 0, 8)),
        'success',
        $transferredFrom !== null ? '등록 성공 (소유권 이전됨: ' . $transferredFrom . ' -> ' . $member['mb_id'] . ')' : '등록 성공'
    );

    register_out(true, array(
        'server_key' => $vsKey,
        'mb_nick' => trim((string)($member['mb_nick'] ?? '')),
        'transferred_from' => $transferredFrom,
        'message' => $transferredFrom !== null
            ? '서버가 등록되었습니다. (이전 계정의 등록은 제거되었습니다)'
            : '서버가 등록되었습니다.'
    ));
}

// mode === 'heartbeat'
$serverKey = trim((string)($body['server_key'] ?? ''));
if ($serverKey === '') {
    register_out(false, array('error' => 'missing_server_key'), 400);
}
$keyEsc = sql_real_escape_string($serverKey);
$row = sql_fetch("select vs_no, pt_id from {$table} where vs_key = '{$keyEsc}'", false);
if (!is_array($row) || empty($row['vs_no'])) {
    register_out(false, array('error' => 'unknown_server_key'), 404);
}

// 비밀번호 재인증 없이도 닉네임을 최신 상태로 유지할 수 있도록 하트비트마다 함께 조회해 반환
$ownerNick = '';
if (!empty($row['pt_id'])) {
    $ptIdEsc2 = sql_real_escape_string($row['pt_id']);
    $ownerRow = sql_fetch("select mb_nick from {$g5['member_table']} where mb_id = '{$ptIdEsc2}'", false);
    if (is_array($ownerRow)) {
        $ownerNick = trim((string)($ownerRow['mb_nick'] ?? ''));
    }
}

$status = trim((string)($body['status'] ?? 'online'));
if ($status !== 'online' && $status !== 'offline') {
    $status = 'online';
}
$now = date('Y-m-d H:i:s');

$setParts = array();
$setParts[] = "vs_status = '" . sql_real_escape_string($status) . "'";
$setParts[] = "vs_last_seen_at = '" . $now . "'";
$setParts[] = "vs_updated_at = '" . $now . "'";
if (isset($body['current_vpn_id_count'])) {
    $setParts[] = "vs_current_vpn_count = " . (int)$body['current_vpn_id_count'];
}
if (isset($body['grade']) && trim((string)$body['grade']) !== '') {
    $setParts[] = "vs_grade = '" . sql_real_escape_string((string)$body['grade']) . "'";
}
if (isset($body['grade_score'])) {
    $setParts[] = "vs_grade_score = " . (int)$body['grade_score'];
}
if (isset($body['recommended_vpn_id_count'])) {
    $setParts[] = "vs_recommended_vpn_count = " . (int)$body['recommended_vpn_id_count'];
}
if (isset($body['metrics'])) {
    $metricsJson = json_encode($body['metrics'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $setParts[] = "vs_metrics_json = '" . sql_real_escape_string((string)$metricsJson) . "'";
}
if (isset($body['vpn_types']) && $hasVpnTypesColumn) {
    $setParts[] = "vs_vpn_types = '" . sql_real_escape_string((string)$body['vpn_types']) . "'";
}

$vsNo = (int)$row['vs_no'];
$updateResult = sql_query("update {$table} set " . implode(', ', $setParts) . " where vs_no = {$vsNo}", false);
if ($updateResult === false) {
    // 쿼리 자체가 실패한 경우 절대 "성공"으로 응답하지 않는다 (실패가 조용히 숨겨지면
    // VPN 박스는 계속 하트비트가 성공한 줄 알고 last_seen_at 이 멈춘 것을 알아채지 못하게 된다)
    register_out(false, array('error' => 'update_failed', 'message' => '상태 갱신 쿼리가 실패했습니다.'), 500);
}

register_out(true, array('message' => 'heartbeat_ok', 'mb_nick' => $ownerNick));
