/**
 * QQ聊天记录导出工具安全管理器
 * 负责处理认证、密钥生成和IP获取
 */

import crypto from 'crypto';
import fs from 'fs';
import path from 'path';

export interface SecurityConfig {
    accessToken: string;
    secretKey: string;
    createdAt: Date;
    lastAccess?: Date;
    allowedIPs: string[];
    tokenExpired?: Date;
    /** 用户配置的服务器地址，用于外网访问 */
    serverHost?: string;
    /** 是否禁用IP白名单验证（Docker环境下可能需要） */
    disableIPWhitelist?: boolean;
}

/**
 * verifyToken 失败原因，便于上层根据具体原因返回差异化错误码。
 *
 * - `invalid_token`: token 不匹配 / 配置缺失
 * - `token_expired`: token 已过 tokenExpired 时间
 * - `ip_not_allowed`: token 正确，但 clientIP 不在白名单内
 */
export type VerifyTokenReason = 'invalid_token' | 'token_expired' | 'ip_not_allowed';

export type VerifyTokenResult =
    | { ok: true }
    | { ok: false; reason: VerifyTokenReason };

/**
 * 检测是否在Docker环境中运行
 */
function isDockerEnvironment(): boolean {
    try {
        // 检查 /.dockerenv 文件
        if (fs.existsSync('/.dockerenv')) {
            return true;
        }
        // 检查 /proc/self/cgroup 中是否包含 docker
        if (fs.existsSync('/proc/self/cgroup')) {
            const cgroup = fs.readFileSync('/proc/self/cgroup', 'utf8');
            if (cgroup.includes('docker') || cgroup.includes('kubepods')) {
                return true;
            }
        }
        // 检查容器环境变量
        if (process.env['container'] || process.env['DOCKER_CONTAINER']) {
            return true;
        }
    } catch {
        // 忽略错误
    }
    return false;
}

/**
 * 从 `/proc/net/route` 解析默认路由网关 IP（dotted decimal）。
 *
 * 在 Docker 容器里通过端口映射进来的请求会被 SNAT 改写成桥网关地址
 * （默认 bridge 是 `172.17.0.1`，自定义网络可能是 `172.x.0.1`），这个
 * IP 永远不会出现在 `/.dockerenv` 或环境变量里，只能从默认路由表反推。
 *
 * 失败 / 非 Linux / 没读到默认路由都返回空数组，调用方继续走兜底逻辑。
 */
function detectDockerBridgeGateways(): string[] {
    if (process.platform !== 'linux') return [];
    const gateways = new Set<string>();
    try {
        const routeFile = '/proc/net/route';
        if (!fs.existsSync(routeFile)) return [];
        const lines = fs.readFileSync(routeFile, 'utf8').split('\n');
        // 表头：Iface Destination Gateway Flags RefCnt Use Metric Mask ...
        for (let i = 1; i < lines.length; i++) {
            const cols = lines[i].trim().split(/\s+/);
            if (cols.length < 8) continue;
            const destination = cols[1];
            const gatewayHex = cols[2];
            if (destination !== '00000000') continue; // 只看默认路由
            if (!/^[0-9A-Fa-f]{8}$/.test(gatewayHex)) continue;
            if (gatewayHex === '00000000') continue;
            // `/proc/net/route` 里 gateway 是小端 hex（每两位一段，反序）。
            const bytes: number[] = [];
            for (let j = 0; j < 4; j++) {
                bytes.push(parseInt(gatewayHex.slice(j * 2, j * 2 + 2), 16));
            }
            const ip = bytes.reverse().join('.');
            if (ip !== '0.0.0.0') {
                gateways.add(ip);
            }
        }
    } catch {
        // 静默：探测失败不影响主流程
    }
    return Array.from(gateways);
}

/**
 * 解析CIDR格式的IP地址
 * @param cidr CIDR格式字符串，如 "172.16.0.0/12"
 * @returns 解析结果，包含网络地址和掩码位数
 */
function parseCIDR(cidr: string): { ip: number; maskBits: number } | null {
    const parts = cidr.split('/');
    if (parts.length !== 2) return null;
    
    const ipParts = parts[0].split('.');
    if (ipParts.length !== 4) return null;
    
    const maskBits = parseInt(parts[1], 10);
    if (isNaN(maskBits) || maskBits < 0 || maskBits > 32) return null;
    
    let ip = 0;
    for (const part of ipParts) {
        const num = parseInt(part, 10);
        if (isNaN(num) || num < 0 || num > 255) return null;
        ip = (ip << 8) | num;
    }
    
    return { ip: ip >>> 0, maskBits };
}

/**
 * 将IP地址字符串转换为数字
 */
function ipToNumber(ip: string): number | null {
    // 处理IPv6映射的IPv4地址
    const cleanIP = ip.replace(/^::ffff:/, '');
    
    const parts = cleanIP.split('.');
    if (parts.length !== 4) return null;
    
    let result = 0;
    for (const part of parts) {
        const num = parseInt(part, 10);
        if (isNaN(num) || num < 0 || num > 255) return null;
        result = (result << 8) | num;
    }
    
    return result >>> 0;
}

/**
 * 检查IP是否匹配CIDR规则
 */
function ipMatchesCIDR(ip: string, cidr: string): boolean {
    const cidrParsed = parseCIDR(cidr);
    if (!cidrParsed) return false;
    
    const ipNum = ipToNumber(ip);
    if (ipNum === null) return false;
    
    const mask = cidrParsed.maskBits === 0 ? 0 : (~0 << (32 - cidrParsed.maskBits)) >>> 0;
    return (ipNum & mask) === (cidrParsed.ip & mask);
}

/**
 * 检查是否为私有/局域网IP
 */
function isPrivateIP(ip: string): boolean {
    const cleanIP = ip.replace(/^::ffff:/, '');
    
    // 本地回环
    if (cleanIP === '127.0.0.1' || cleanIP === 'localhost' || cleanIP === '::1') {
        return true;
    }
    
    // 检查私有网段
    const privateRanges = [
        '10.0.0.0/8',      // Class A 私有网络
        '172.16.0.0/12',   // Class B 私有网络 (172.16.0.0 - 172.31.255.255)
        '192.168.0.0/16',  // Class C 私有网络
        '169.254.0.0/16',  // 链路本地地址
    ];
    
    return privateRanges.some(range => ipMatchesCIDR(cleanIP, range));
}

/**
 * 安全管理器
 */
export class SecurityManager {
    private configPath: string;
    private config: SecurityConfig | null = null;
    private publicIP: string | null = null;
    private isDocker: boolean = false;
    private configWatcher: fs.FSWatcher | null = null;

    constructor() {
        const securityDir = SecurityManager.resolveSecurityDir();

        // 确保目录存在
        if (!fs.existsSync(securityDir)) {
            fs.mkdirSync(securityDir, { recursive: true });
        }

        this.configPath = path.join(securityDir, 'security.json');
        this.isDocker = isDockerEnvironment();
    }

    /**
     * 计算 security.json 所在目录。
     *
     * 优先级（issue #272）：
     *   1. 环境变量 `QCE_CONFIG_DIR`（绝对路径），方便 Framework / Standalone 共用一份配置；
     *   2. `~/.qq-chat-exporter/`（USERPROFILE / HOME）；
     *   3. 当 USERPROFILE 落到 `C:\Windows\system32\config\systemprofile`
     *      （管理员模式下未 cd 到脚本目录就启动会出现这种情况）时回退到 HOMEDRIVE+HOMEPATH，
     *      最终再退到当前工作目录，避免把 token 写到 system32 里读不到。
     */
    static resolveSecurityDir(): string {
        const envOverride = process.env['QCE_CONFIG_DIR'];
        if (envOverride && envOverride.trim()) {
            return envOverride.trim();
        }

        const homeCandidates: Array<string | undefined> = [
            process.env['USERPROFILE'],
            process.env['HOME'],
        ];

        const driveBased =
            process.env['HOMEDRIVE'] && process.env['HOMEPATH']
                ? path.join(process.env['HOMEDRIVE'] || '', process.env['HOMEPATH'] || '')
                : '';
        if (driveBased) {
            homeCandidates.push(driveBased);
        }

        for (const candidate of homeCandidates) {
            if (!candidate) continue;
            const normalized = candidate.replace(/\\/g, '/').toLowerCase();
            // C:\Windows\system32\config\systemprofile（SYSTEM 账户）/ system32 下不可写：跳过
            if (
                normalized.includes('/windows/system32') ||
                normalized.includes('/windows/syswow64')
            ) {
                continue;
            }
            return path.join(candidate, '.qq-chat-exporter');
        }

        // 全部候选都不可用：用当前工作目录兜底
        return path.join(process.cwd(), '.qq-chat-exporter');
    }

    /**
     * 初始化安全配置
     */
    async initialize(): Promise<void> {
        // 初始化服务器地址（默认localhost，Docker环境下使用0.0.0.0）
        this.publicIP = this.isDocker ? '0.0.0.0' : '127.0.0.1';

        // Issue #272: 启动时把 configPath 打到 stdout，便于排查 Framework /
        // Standalone 切换 / admin 模式 USERPROFILE 漂移导致 token 不一致。
        try {
            console.log(`[QCE][SecurityManager] config: ${this.configPath}`);
        } catch {
            // ignore
        }

        // 加载或创建安全配置
        if (fs.existsSync(this.configPath)) {
            await this.loadConfig();
            // Issue #438: 插件商店发布的包会自带一份 "出厂" security.json
            // （`{disableIPWhitelist: false, allowedIPs: ['127.0.0.1','::1']}`），
            // 这种情况 generateInitialConfig 永远不会跑，Docker 自动配置 +
            // token / secret 生成都缺位。这里在 load 完之后再过一遍迁移逻辑，
            // 把缺失字段补齐、必要时打开 Docker 模式。
            await this.migrateFactoryConfigIfNeeded();
        } else {
            await this.generateInitialConfig();
        }

        // 从配置中设置服务器地址
        if (this.config?.serverHost) {
            this.setServerHost(this.config.serverHost);
        }
        
        // 启动配置文件监听（热加载）
        this.startConfigWatcher();
    }

    /**
     * Issue #438: 把「出厂打包 / 字段不完整」的 security.json 迁移成完整配置。
     *
     * 插件商店发布的包里自带一份占位 `security.json`：
     *   `{"disableIPWhitelist": false, "allowedIPs": ["127.0.0.1", "::1"]}`
     * 这份文件没有 `accessToken` / `secretKey` / `createdAt`，但 `loadConfig`
     * 依然能解析成功 —— 结果就是 `generateInitialConfig` 永远不会被调用，
     * Docker 自动识别 + 自动 `disableIPWhitelist` 的逻辑全部失效。
     *
     * 这里的判定原则：**任何缺核心字段（accessToken / secretKey / createdAt）
     * 的配置都视为「出厂占位」**，因为正常用户不可能手工创建一个没有
     * token 的 security.json。检测到出厂占位后：
     *   1. 补齐 accessToken / secretKey / createdAt / tokenExpired；
     *   2. 如果当前在 Docker 里，强行切到 Docker 友好模式
     *      （disableIPWhitelist=true + 加 Docker 网段 + 加运行时探测到的
     *      桥网关），等价于补跑 `generateInitialConfig` 的 Docker 分支。
     *
     * 用户已经手动改过的 security.json（accessToken 等核心字段都在）不会被
     * 这条路径触碰。
     */
    private async migrateFactoryConfigIfNeeded(): Promise<void> {
        if (!this.config) {
            // loadConfig 失败时 generateInitialConfig 已经被调用过，直接返回。
            return;
        }

        const cfg = this.config;
        const missingCoreFields =
            !cfg.accessToken ||
            !cfg.secretKey ||
            !(cfg.createdAt instanceof Date) ||
            isNaN(cfg.createdAt.getTime());

        if (!missingCoreFields) {
            // 用户已有完整配置，尊重用户的所有偏好，不动任何字段。
            return;
        }

        // 补齐核心字段
        if (!cfg.accessToken) {
            cfg.accessToken = this.generateSecureToken(40);
        }
        if (!cfg.secretKey) {
            cfg.secretKey = this.generateSecureToken(64);
        }
        if (!(cfg.createdAt instanceof Date) || isNaN(cfg.createdAt.getTime())) {
            cfg.createdAt = new Date();
        }
        if (!(cfg.tokenExpired instanceof Date) || isNaN(cfg.tokenExpired.getTime())) {
            cfg.tokenExpired = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
        }

        // allowedIPs 脏数据兜底
        if (!Array.isArray(cfg.allowedIPs)) {
            cfg.allowedIPs = ['127.0.0.1', '::1'];
        }

        if (this.isDocker) {
            // Docker 下出厂占位：补跑 generateInitialConfig 的 Docker 分支。
            const dockerAllowed = new Set<string>(cfg.allowedIPs);
            dockerAllowed.add('127.0.0.1');
            dockerAllowed.add('::1');
            dockerAllowed.add('172.16.0.0/12');
            dockerAllowed.add('192.168.0.0/16');
            dockerAllowed.add('10.0.0.0/8');
            for (const gw of detectDockerBridgeGateways()) {
                dockerAllowed.add(gw);
            }
            cfg.allowedIPs = Array.from(dockerAllowed);
            cfg.disableIPWhitelist = true;
            try {
                console.log(
                    '[QCE][SecurityManager] Detected factory-shipped security.json in Docker, ' +
                        'auto-enabling Docker-friendly auth (issue #438).',
                );
            } catch {
                // ignore
            }
        }

        await this.saveConfig();
    }
    
    /**
     * 启动配置文件监听，支持热加载
     */
    private startConfigWatcher(): void {
        if (this.configWatcher) {
            return;
        }
        
        try {
            let debounceTimer: NodeJS.Timeout | null = null;
            
            this.configWatcher = fs.watch(this.configPath, (eventType) => {
                if (eventType === 'change') {
                    if (debounceTimer) {
                        clearTimeout(debounceTimer);
                    }
                    debounceTimer = setTimeout(async () => {
                        await this.loadConfig();
                    }, 500);
                }
            });
        } catch (error) {
            // 静默处理
        }
    }
    
    /**
     * 停止配置文件监听
     */
    stopConfigWatcher(): void {
        if (this.configWatcher) {
            this.configWatcher.close();
            this.configWatcher = null;
        }
    }

    /**
     * 设置服务器地址（供外部配置使用）
     */
    setServerHost(host: string): void {
        // 标准化host地址，参考NapCat的做法
        if (host === '0.0.0.0' || host === '') {
            this.publicIP = '127.0.0.1';
        } else {
            this.publicIP = host;
        }
    }

    /**
     * 更新服务器地址配置并保存
     */
    async updateServerHost(host: string): Promise<void> {
        if (!this.config) return;
        
        this.config.serverHost = host;
        await this.saveConfig();
        this.setServerHost(host);
    }

    /**
     * 生成初始安全配置
     */
    private async generateInitialConfig(): Promise<void> {
        // 生成复杂的访问令牌（40 字符纯字母数字，issue #272）
        const accessToken = this.generateSecureToken(40);
        
        // 生成密钥 (64字符)
        const secretKey = this.generateSecureToken(64);
        
        // 默认白名单：本地 + Docker环境下添加常见的Docker网段
        const defaultAllowedIPs = ['127.0.0.1', '::1'];
        if (this.isDocker) {
            defaultAllowedIPs.push('172.16.0.0/12');
            defaultAllowedIPs.push('192.168.0.0/16');
            defaultAllowedIPs.push('10.0.0.0/8');
            // Issue #438: 进一步把默认路由网关（bridge SNAT 源 IP）追加进白名单，
            // 兜底自定义 docker 网络出现非标准网段的情况。
            for (const gw of detectDockerBridgeGateways()) {
                if (!defaultAllowedIPs.includes(gw)) {
                    defaultAllowedIPs.push(gw);
                }
            }
        }
        
        this.config = {
            accessToken,
            secretKey,
            createdAt: new Date(),
            allowedIPs: defaultAllowedIPs,
            tokenExpired: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
            disableIPWhitelist: this.isDocker
        };

        await this.saveConfig();
    }

    /**
     * 加载安全配置
     */
    private async loadConfig(): Promise<void> {
        try {
            const data = fs.readFileSync(this.configPath, 'utf-8');
            this.config = JSON.parse(data);
            
            // 转换日期字段
            if (this.config) {
                this.config.createdAt = new Date(this.config.createdAt);
                if (this.config.lastAccess) {
                    this.config.lastAccess = new Date(this.config.lastAccess);
                }
                if (this.config.tokenExpired) {
                    this.config.tokenExpired = new Date(this.config.tokenExpired);
                }
            }
            
            // 检查令牌是否过期
            if (this.config?.tokenExpired && new Date() > this.config.tokenExpired) {
                await this.regenerateToken();
            }
        } catch (error) {
            await this.generateInitialConfig();
        }
    }

    /**
     * 保存安全配置
     */
    private async saveConfig(): Promise<void> {
        try {
            const data = JSON.stringify(this.config, null, 2);
            fs.writeFileSync(this.configPath, data, 'utf-8');
        } catch (error) {
            // 静默处理
        }
    }

    /**
     * 生成安全令牌。
     *
     * Issue #272: 之前的字符表里包含 `!@#$%^&*`，复制 / URL / shell / 聊天
     * 软件转手时容易把这几个字符吃掉或解析成参数分隔符（典型场景：one-click
     * URL 里的 `&` 被裁成两个 query 参数），用户复制下来的 token 永远不等于
     * 落盘的那个。改成只用 A-Z / a-z / 0-9，长度从 32 拉到 40 维持熵预算
     * （log2(62^40) ≈ 238 bit）。
     */
    private generateSecureToken(length: number = 40): string {
        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        let result = '';

        for (let i = 0; i < length; i++) {
            result += chars.charAt(crypto.randomInt(0, chars.length));
        }

        return result;
    }

    /**
     * 重新生成访问令牌
     */
    private async regenerateToken(): Promise<void> {
        if (!this.config) return;
        
        this.config.accessToken = this.generateSecureToken(40);
        this.config.tokenExpired = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
        
        await this.saveConfig();
    }

    /**
     * 验证访问令牌（兼容旧调用，返回布尔值）。
     */
    verifyToken(token: string, clientIP?: string): boolean {
        return this.verifyTokenWithReason(token, clientIP).ok;
    }

    /**
     * 验证访问令牌，返回带具体失败原因的结果。
     *
     * Issue #438: 之前不论是 token 错、IP 不在白名单还是过期都收敛到
     * `"无效的访问令牌"` 同一条报错，用户根本判断不出是哪个环节挂了。
     * 这里把三种失败拆开成可识别的 reason，让 API 层能给前端不同的
     * error code / message。
     */
    verifyTokenWithReason(token: string, clientIP?: string): VerifyTokenResult {
        if (!this.config || !this.config.accessToken) {
            return { ok: false, reason: 'invalid_token' };
        }

        // 1. token 比对
        if (token !== this.config.accessToken) {
            return { ok: false, reason: 'invalid_token' };
        }

        // 2. 过期检查（在 IP 检查之前，过期是更明确的失败原因）
        if (this.config.tokenExpired && new Date() > this.config.tokenExpired) {
            return { ok: false, reason: 'token_expired' };
        }

        // 3. IP 白名单
        if (!this.config.disableIPWhitelist) {
            if (clientIP && this.config.allowedIPs.length > 0) {
                if (!this.checkIPAllowed(clientIP)) {
                    return { ok: false, reason: 'ip_not_allowed' };
                }
            }
        }

        // 更新最后访问时间
        this.config.lastAccess = new Date();
        this.saveConfig().catch(console.error);

        return { ok: true };
    }
    
    /**
     * 检查IP是否在白名单中（支持精确匹配、CIDR网段、通配符）
     */
    private checkIPAllowed(clientIP: string): boolean {
        if (!this.config) return false;
        
        // 清理IP地址（移除IPv6前缀）
        const cleanIP = clientIP.replace(/^::ffff:/, '');
        
        for (const allowedIP of this.config.allowedIPs) {
            // 通配符：允许所有
            if (allowedIP === '0.0.0.0' || allowedIP === '*') {
                return true;
            }
            
            // CIDR格式匹配
            if (allowedIP.includes('/')) {
                if (ipMatchesCIDR(cleanIP, allowedIP)) {
                    return true;
                }
                continue;
            }
            
            // 精确匹配
            if (allowedIP === cleanIP || allowedIP === clientIP) {
                return true;
            }
        }
        
        return false;
    }

    /**
     * 获取访问令牌（仅用于显示）
     */
    getAccessToken(): string | null {
        return this.config?.accessToken || null;
    }

    /**
     * 获取服务器地址
     */
    public getPublicIP(): string | null {
        return this.publicIP;
    }

    /**
     * 获取完整的服务器地址信息（用于显示）
     */
    public getServerAddresses(): { local: string; external?: string } {
        const result: { local: string; external?: string } = {
            local: 'http://127.0.0.1:40653'
        };

        // 如果配置的不是localhost，添加外部地址
        if (this.publicIP && this.publicIP !== '127.0.0.1' && this.publicIP !== 'localhost') {
            result.external = `http://${this.publicIP}:40653`;
        }

        return result;
    }

    /**
     * 获取安全状态信息
     */
    getSecurityStatus(): {
        hasConfig: boolean;
        tokenExpired: boolean;
        publicIP: string | null;
        createdAt: Date | null;
        lastAccess: Date | null;
    } {
        return {
            hasConfig: !!this.config,
            tokenExpired: !!(this.config?.tokenExpired && new Date() > this.config.tokenExpired),
            publicIP: this.publicIP,
            createdAt: this.config?.createdAt || null,
            lastAccess: this.config?.lastAccess || null
        };
    }

    /**
     * 添加IP到白名单
     */
    async addAllowedIP(ip: string): Promise<void> {
        if (!this.config) return;
        
        if (!this.config.allowedIPs.includes(ip)) {
            this.config.allowedIPs.push(ip);
            await this.saveConfig();
        }
    }
    
    /**
     * 从白名单移除IP
     */
    async removeAllowedIP(ip: string): Promise<boolean> {
        if (!this.config) return false;
        
        const index = this.config.allowedIPs.indexOf(ip);
        if (index > -1) {
            this.config.allowedIPs.splice(index, 1);
            await this.saveConfig();
            return true;
        }
        return false;
    }
    
    /**
     * 获取当前白名单列表
     */
    getAllowedIPs(): string[] {
        return this.config?.allowedIPs || [];
    }
    
    /**
     * 设置是否禁用IP白名单验证
     */
    async setDisableIPWhitelist(disable: boolean): Promise<void> {
        if (!this.config) return;
        
        this.config.disableIPWhitelist = disable;
        await this.saveConfig();
    }
    
    /**
     * 获取IP白名单是否禁用
     */
    isIPWhitelistDisabled(): boolean {
        return this.config?.disableIPWhitelist || false;
    }
    
    /**
     * 检查是否在Docker环境中
     */
    isInDocker(): boolean {
        return this.isDocker;
    }
    
    /**
     * 获取配置文件路径
     */
    getConfigPath(): string {
        return this.configPath;
    }

    /**
     * 生成新的访问令牌（手动）
     */
    async generateNewToken(): Promise<string> {
        if (!this.config) throw new Error('安全配置未初始化');
        
        await this.regenerateToken();
        return this.config.accessToken;
    }
}