<?php
/**
 * VR票务插件 - 基础服务
 *
 * @package vr_ticket\service
 */

namespace app\plugins\vr_ticket\service;

class BaseService
{
    /**
     * 获取插件表前缀
     */
    public static function table($name)
    {
        return 'vr_' . $name;
    }

    /**
     * 获取当前时间戳
     */
    public static function now()
    {
        return time();
    }

    /**
     * 生成 UUID v4 票码
     */
    public static function generateUuid()
    {
        $data = random_bytes(16);
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    }

    /**
     * AES-256-CBC 加密 QR 数据
     *
     * @param array $data 待加密数据
     * @param int|null $expire 过期时间戳（默认30天）
     * @return string base64 编码密文
     */
    public static function encryptQrData($data, $expire = null)
    {
        $secret = self::getQrSecret();
        $expire = $expire ?? (time() + 86400 * 30);

        $payload = json_encode(array_merge($data, [
            'exp' => $expire,
            'iat' => time(),
        ]), JSON_UNESCAPED_UNICODE);

        $iv = random_bytes(16);
        $encrypted = openssl_encrypt($payload, 'AES-256-CBC', $secret, OPENSSL_RAW_DATA, $iv);

        return base64_encode($iv . $encrypted);
    }

    /**
     * 解密 QR 数据
     *
     * @param string $encoded base64 编码密文
     * @return array|null
     */
    public static function decryptQrData($encoded)
    {
        $secret = self::getQrSecret();
        $combined = base64_decode($encoded);

        if (strlen($combined) < 16) {
            return null;
        }

        $iv = substr($combined, 0, 16);
        $encrypted = substr($combined, 16);

        $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $secret, OPENSSL_RAW_DATA, $iv);

        if ($decrypted === false) {
            return null;
        }

        $data = json_decode($decrypted, true);

        if (isset($data['exp']) && $data['exp'] < time()) {
            return null;
        }

        return $data;
    }

    /**
     * 获取 QR 加密密钥
     */
    private static function getQrSecret()
    {
        $secret = env('VR_TICKET_QR_SECRET', '');
        if (empty($secret)) {
            throw new \Exception('[vr_ticket] VR_TICKET_QR_SECRET 环境变量未配置，QR加密密钥不能为空。请在.env中设置VR_TICKET_QR_SECRET=<随机64字符字符串>');
        }
        return $secret;
    }

    /**
     * 判断商品是否为票务商品
     *
     * @param int $goods_id
     * @return bool
     */
    public static function isTicketGoods($goods_id)
    {
        $goods = \think\facade\Db::name('Goods')->find($goods_id);
        if (empty($goods)) {
            return false;
        }
        return !empty($goods['venue_data']) || ($goods['item_type'] ?? '') === 'ticket';
    }

    /**
     * 获取商品座位模板
     *
     * @param int $goods_id
     * @return array|null
     */
    public static function getSeatTemplateByGoods($goods_id)
    {
        $goods = \think\facade\Db::name('Goods')->find($goods_id);
        if (empty($goods) || empty($goods['category_id'])) {
            return null;
        }

        return \think\facade\Db::name(self::table('seat_templates'))
            ->where('category_id', $goods['category_id'])
            ->where('status', 1)
            ->find();
    }

    /**
     * 安全日志
     */
    public static function log($message, $context = [], $level = 'info')
    {
        $tag = '[vr_ticket]';
        $ctx = empty($context) ? '' : ' ' . json_encode($context, JSON_UNESCAPED_UNICODE);
        $log_func = "log_{$level}";
        if (function_exists($log_func)) {
            $log_func($tag . $message . $ctx);
        }
    }

    /**
     * 初始化票务商品规格
     *
     * 修复商品 112 的 broken 状态：
     * 1. 设置 is_exist_many_spec = 1（启用多规格模式）
     * 2. 插入 $vr- 规格类型（幂等，多次执行不重复）
     *
     * @param int $goodsId 商品ID
     * @return array ['code' => 0, 'msg' => '...', 'data' => [...]]
     */
    public static function initGoodsSpecs(int $goodsId): array
    {
        $goodsId = intval($goodsId);
        if ($goodsId <= 0) {
            return ['code' => -1, 'msg' => '商品ID无效'];
        }

        // 1. 检查商品是否存在
        $goods = \think\facade\Db::name('Goods')->where('id', $goodsId)->find();
        if (empty($goods)) {
            return ['code' => -2, 'msg' => "商品 {$goodsId} 不存在"];
        }

        $now = time();

        // 2. 启用多规格模式
        \think\facade\Db::name('Goods')->where('id', $goodsId)->update([
            'is_exist_many_spec' => 1,
            'upd_time' => $now,
        ]);

        // 3. 定义 $vr- 规格类型（5维：场次、场馆、演播室、分区、座位号）
        $specTypes = [
            '$vr-场次'   => '[{"name":"待选场次","images":""}]',
            '$vr-场馆'   => '[{"name":"' . ($goods['venue_data'] ?? '国家体育馆') . '","images":""}]',
            '$vr-演播室' => '[{"name":"主厅","images":""}]',
            '$vr-分区'   => '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]',
            '$vr-座位号' => '[{"name":"待选座位","images":""}]',
        ];

        $insertedCount = 0;
        foreach ($specTypes as $name => $value) {
            // 幂等：检查是否已存在
            $exists = \think\facade\Db::name('GoodsSpecType')
                ->where('goods_id', $goodsId)
                ->where('name', $name)
                ->find();

            if (empty($exists)) {
                \think\facade\Db::name('GoodsSpecType')->insert([
                    'goods_id' => $goodsId,
                    'name'     => $name,
                    'value'    => $value,
                    'add_time' => $now,
                ]);
                $insertedCount++;
                self::log('initGoodsSpecs: inserted spec_type', ['goods_id' => $goodsId, 'name' => $name]);
            }
        }

        self::log('initGoodsSpecs: done', ['goods_id' => $goodsId, 'inserted' => $insertedCount]);

        // 4. 返回当前所有 spec_type，便于验证
        $specTypes = \think\facade\Db::name('GoodsSpecType')
            ->where('goods_id', $goodsId)
            ->order('id', 'asc')
            ->select()
            ->toArray();

        return [
            'code' => 0,
            'msg'  => "初始化完成，插入 {$insertedCount} 条规格类型",
            'data' => [
                'goods_id'        => $goodsId,
                'is_exist_many_spec' => 1,
                'spec_types'      => $specTypes,
            ],
        ];
    }

    /**
     * 插件后台权限菜单
     *
     * ShopXO 通过 PluginsService::PluginsAdminPowerMenu() 调用此方法
     * 返回格式：二维数组，每项代表一个菜单分组
     *
     * @return array
     */
    public static function AdminPowerMenu()
    {
        return [
            // 座位模板
            [
                'name'    => '座位模板',
                'control' => 'seat_template',
                'action'  => 'list',
                'item'    => [
                    ['name' => '座位模板', 'action' => 'list'],
                    ['name' => '添加模板', 'action' => 'save'],
                ],
            ],
            // 电子票
            [
                'name'    => '电子票',
                'control' => 'ticket',
                'action'  => 'list',
                'item'    => [
                    ['name' => '电子票列表', 'action' => 'list'],
                    ['name' => '票详情',     'action' => 'detail'],
                    ['name' => '手动核销',   'action' => 'verify'],
                    ['name' => '导出票',     'action' => 'export'],
                ],
            ],
            // 核销员
            [
                'name'    => '核销员',
                'control' => 'verifier',
                'action'  => 'list',
                'item'    => [
                    ['name' => '核销员列表', 'action' => 'list'],
                    ['name' => '添加核销员', 'action' => 'save'],
                ],
            ],
            // 核销记录
            [
                'name'    => '核销记录',
                'control' => 'verification',
                'action'  => 'list',
                'item'    => [
                    ['name' => '核销记录', 'action' => 'list'],
                ],
            ],
        ];
    }

    /**
     * Phase 4: Feistel-8 混淆 + QR签名 + 短码编解码
     * ================================================================
     */

    /**
     * 获取 VR Ticket 主密钥
     * @throws \Exception 未配置密钥时抛出异常
     */
    private static function getVrSecret(): string
    {
        $secret = env('VR_TICKET_SECRET', '');
        if (empty($secret)) {
            throw new \Exception('[vr_ticket] VR_TICKET_SECRET 环境变量未配置！请在 .env 中设置 VR_TICKET_SECRET=<随机64字符字符串> 以确保票务安全');
        }
        return $secret;
    }

    /**
     * 获取 per-goods key
     * 由 master_secret 派生，保证不同商品的编码互相独立
     *
     * @param int $goods_id
     * @return string 16字节hex
     */
    public static function getGoodsKey(int $goods_id): string
    {
        static $cache = [];
        if (!isset($cache[$goods_id])) {
            $secret = self::getVrSecret();
            // HMAC-SHA256(master_secret, goods_id) 取前16字节
            $cache[$goods_id] = substr(hash_hmac('sha256', (string)$goods_id, $secret), 0, 16);
        }
        return $cache[$goods_id];
    }

    /**
     * Feistel Round 函数
     * F(R, i, key) = HMAC-SHA256(R . i, key) 的低19bit
     *
     * @param int $R 17bit 右半部分
     * @param int $round 轮次 [0-7]
     * @param string $key per-goods key
     * @return int 19bit 输出
     */
    private static function feistelRound(int $R, int $round, string $key): int
    {
        $hmac = hash_hmac('sha256', $R . '.' . $round, $key, true);
        // 取前3字节(24bit)，保留低19bit
        $val = (ord($hmac[0]) << 16) | (ord($hmac[1]) << 8) | ord($hmac[2]);
        return $val & 0x7FFFF; // 19bit mask
    }

    /**
     * Feistel-8 混淆编码
     *
     * 位分配：L=19bit, R=17bit（凑满36bit）
     * @param int $packed 33bit整数（goods_id<<17 | ticket_id）
     * @param string $key per-goods key
     * @return string base36编码
     */
    public static function feistelEncode(int $packed, string $key): string
    {
        // 分离 L(高19bit) 和 R(低17bit)
        $L = ($packed >> 17) & 0x7FFFF;
        $R = $packed & 0x1FFFF;

        // 8轮 Feistel 置换
        for ($i = 0; $i < 8; $i++) {
            $F = self::feistelRound($R, $i, $key);
            $L_new = $R;
            $R_new = $L ^ $F;
            $L = $L_new;
            $R = $R_new;
        }

        // 合并为36bit整数
        $result = ($L & 0x7FFFF) | (($R & 0x1FFFF) << 17);

        return base_convert($result, 10, 36);
    }

    /**
     * Feistel-8 解码（逆向8轮）
     *
     * @param string $code base36编码
     * @param string $key per-goods key
     * @return int 整数
     */
    public static function feistelDecode(string $code, string $key): int
    {
        $packed = intval(base_convert(strtolower($code), 36, 10));

        // 分离 L 和 R
        $L = ($packed >> 17) & 0x7FFFF;
        $R = $packed & 0x1FFFF;

        // 8轮逆向 Feistel 置换
        for ($i = 7; $i >= 0; $i--) {
            $F = self::feistelRound($L, $i, $key);
            $R_new = $L;
            $L_new = $R ^ $F;
            $R = $R_new;
            $L = $L_new;
        }

        // 合并
        return ($L & 0x7FFFF) | (($R & 0x1FFFF) << 17);
    }

    /**
     * 生成短码
     *
     * 编码结构：goods_id(4位base36) + ticket_id(5位base36) = 9位 → Feistel8 → 短码
     * - goods_id: 4位 base36 (范围 0-1,679,615，ShopXO 商品上限充足)
     * - ticket_id: 5位 base36 (范围 0-60,466,175，每商品可发约6000万张票)
     * - 解码 O(1): 直接取前4位=goods_id，后5位=ticket_id，无需暴力搜索
     *
     * @param int $goods_id 0-1679615
     * @param int $ticket_id 0-60466175
     * @return string base36小写短码
     * @throws \Exception 参数超范围时抛出
     */
    public static function shortCodeEncode(int $goods_id, int $ticket_id): string
    {
        // 校验 goods_id 不超过 4位 base36 (0xFFFFFF = 1679615)
        if ($goods_id > 0xFFFFFF) {
            throw new \Exception("goods_id 超出范围 (max=1679615), given={$goods_id}");
        }
        // 校验 ticket_id 不超过 5位 base36 (0x3FFFFFFF = 1073741823)
        if ($ticket_id > 0x3FFFFFFF) {
            throw new \Exception("ticket_id 超出范围 (max=1073741823), given={$ticket_id}");
        }

        // goods_id 固定4位 base36
        $goods_part = str_pad(base_convert($goods_id, 10, 36), 4, '0', STR_PAD_LEFT);
        // ticket_id 固定5位 base36
        $ticket_part = str_pad(base_convert($ticket_id, 10, 36), 5, '0', STR_PAD_LEFT);

        // 拼接为9位 base36 字符串，转为大整数
        // 使用 intval(string, 36) 转换
        $packed_str = $goods_part . $ticket_part;
        $packed = intval($packed_str, 36);

        // Feistel8 混淆
        $key = self::getGoodsKey($goods_id);
        return strtolower(self::feistelEncode($packed, $key));
    }

    /**
     * 解析短码（解码回 goods_id + ticket_id）
     *
     * 明文方案：短码经 Feistel8 解密后，前4位=goods_id，后5位=ticket_id
     * 解码 O(1)，无需暴力搜索
     *
     * @param string $code 短码（小写或大写均可）
     * @param int|null $goods_id_hint 可选提示（已不再需要，用于兼容）
     * @return array ['goods_id' => int, 'ticket_id' => int]
     * @throws \Exception 解码失败时抛出
     */
    public static function shortCodeDecode(string $code, ?int $goods_id_hint = null): array
    {
        $code = strtolower($code);

        // 如果有 hint，直接用 hint 的 key 解密
        // 如果没有 hint，暴力搜索（最多尝试 1-100000）
        $decoded_goods_id = null;
        $decoded_ticket_id = null;

        if ($goods_id_hint !== null) {
            $key = self::getGoodsKey($goods_id_hint);
            $packed = self::feistelDecode($code, $key);
            $packed_str = base_convert($packed, 10, 36);
            // 前4位 goods_id，后5位 ticket_id
            $decoded_goods_id = intval(substr($packed_str, 0, 4), 36);
            $decoded_ticket_id = intval(substr($packed_str, 4, 5), 36);
            // 验证解码出的 goods_id 是否与 hint 匹配
            if ($decoded_goods_id !== $goods_id_hint) {
                throw new \Exception("短码解码失败：hint 不匹配 (code={$code}, hint={$goods_id_hint}, decoded={$decoded_goods_id})");
            }
        } else {
            // 暴力搜索 goods_id（优化：只搜索实际存在的范围）
            $max_goods = 100000;
            for ($gid = 1; $gid <= $max_goods; $gid++) {
                $key = self::getGoodsKey($gid);
                $packed = self::feistelDecode($code, $key);
                $packed_str = base_convert($packed, 10, 36);
                $candidate_goods_id = intval(substr($packed_str, 0, 4), 36);
                if ($candidate_goods_id === $gid) {
                    $decoded_goods_id = $gid;
                    $decoded_ticket_id = intval(substr($packed_str, 4, 5), 36);
                    break;
                }
            }
        }

        if ($decoded_goods_id === null) {
            throw new \Exception("短码解码失败：无法找到匹配的 goods_id (code={$code})");
        }

        return [
            'goods_id'  => $decoded_goods_id,
            'ticket_id' => $decoded_ticket_id,
        ];
    }

    /**
     * 签名 QR payload（HMAC-SHA256 防篡改）
     *
     * @param array $payload ['id'=>int, 'g'=>int(goods_id), 'iat'=>int, 'exp'=>int]
     * @return string base64编码的签名内容
     */
    public static function signQrPayload(array $payload): string
    {
        $secret = self::getVrSecret();
        // 签名内容：id.g.iat.exp
        $sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp'];
        $sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8);
        $payload['sig'] = $sig;

        return base64_encode(json_encode($payload, JSON_UNESCAPED_UNICODE));
    }

    /**
     * 验证 QR payload
     *
     * @param string $encoded base64编码
     * @return array|null 验证失败返回null，成功返回 payload（含id/g/exp）
     */
    public static function verifyQrPayload(string $encoded)
    {
        $json = base64_decode($encoded);
        if ($json === false) {
            return null;
        }

        $payload = json_decode($json, true);
        if (!is_array($payload)) {
            return null;
        }

        // 必填字段检查
        if (!isset($payload['id'], $payload['g'], $payload['iat'], $payload['exp'], $payload['sig'])) {
            return null;
        }

        // 时间戳检查：是否过期
        if ($payload['exp'] < time()) {
            return null;
        }

        // 签名验证
        $secret = self::getVrSecret();
        $sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp'];
        $expected_sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8);

        if (!hash_equals($expected_sig, $payload['sig'])) {
            return null;
        }

        return [
            'id'  => intval($payload['id']),
            'g'   => intval($payload['g']),
            'exp' => intval($payload['exp']),
        ];
    }
}
