"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GatewayStateService = void 0;
const contracts_1 = require("@teachagenthub/contracts");
const common_1 = require("@nestjs/common");
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const sqlite3_1 = __importDefault(require("sqlite3"));
const path_constants_1 = require("../../../constants/path.constants");
let GatewayStateService = class GatewayStateService {
    sessions = new Map();
    connections = new Map();
    audits = [];
    db;
    recentUserInput = new Map();
    userInputDedupWindowMs;
    constructor() {
        this.userInputDedupWindowMs = this.parsePositiveInt(contracts_1.APP_CONFIG.GATEWAY_USER_INPUT_DEDUP_WINDOW_MS, 3000);
        const dbPath = path_constants_1.TEACH_AGENT_HUB_DBPATH;
        (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(dbPath), { recursive: true });
        this.db = new sqlite3_1.default.Database(dbPath);
        this.db.serialize(() => {
            this.db.run('PRAGMA journal_mode = WAL;');
            this.db.run(`
      CREATE TABLE IF NOT EXISTS voice_chat_record (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        chat_id TEXT NOT NULL,
        text TEXT NOT NULL,
        role TEXT NOT NULL DEFAULT 'user_input',
        is_final INTEGER NOT NULL DEFAULT 1,
        created_at TEXT NOT NULL
      );
      `);
            this.db.run(`
      CREATE INDEX IF NOT EXISTS idx_voice_chat_record_chat_id_id
      ON voice_chat_record(chat_id, id);
      `);
            this.db.run(`
      CREATE TABLE IF NOT EXISTS chat_message_feedback (
        chat_id TEXT NOT NULL,
        message_id INTEGER NOT NULL,
        feedback TEXT NOT NULL CHECK (feedback IN ('like', 'dislike')),
        updated_at TEXT NOT NULL,
        PRIMARY KEY (chat_id, message_id)
      );
      `);
            this.db.run(`
      CREATE INDEX IF NOT EXISTS idx_chat_message_feedback_chat_message
      ON chat_message_feedback(chat_id, message_id);
      `);
            this.db.run(`
      CREATE TABLE IF NOT EXISTS ipc_connection (
        connection_id TEXT PRIMARY KEY,
        ipc_ip TEXT NOT NULL,
        name TEXT NOT NULL DEFAULT '',
        status TEXT NOT NULL,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      );
      `);
            this.db.run(`
      CREATE INDEX IF NOT EXISTS idx_ipc_connection_ip_status
      ON ipc_connection(ipc_ip, status);
      `);
            this.db.all(`SELECT connection_id AS connectionId,
                ipc_ip AS ipcIp,
                name,
                status,
                created_at AS createdAt,
                updated_at AS updatedAt
         FROM ipc_connection`, [], (err, rows) => {
                if (err || !rows) {
                    return;
                }
                for (const raw of rows) {
                    const row = raw;
                    const connection = {
                        connectionId: String(row.connectionId ?? ''),
                        ipcIp: String(row.ipcIp ?? ''),
                        ...(String(row.name ?? '').trim() ? { name: String(row.name) } : {}),
                        status: String(row.status ?? 'disconnected'),
                        createdAt: String(row.createdAt ?? new Date().toISOString()),
                        updatedAt: String(row.updatedAt ?? new Date().toISOString())
                    };
                    if (connection.connectionId) {
                        this.connections.set(connection.connectionId, connection);
                    }
                }
            });
        });
    }
    connectIpc(req) {
        const existing = Array.from(this.connections.values()).find((item) => item.ipcIp === req.ipcIp && item.status === 'connected');
        if (existing) {
            existing.name = req.name ?? existing.name;
            existing.updatedAt = new Date().toISOString();
            this.connections.set(existing.connectionId, existing);
            this.persistIpcConnection(existing);
            return existing;
        }
        const now = new Date().toISOString();
        const connection = {
            connectionId: `ipc_${Math.random().toString(36).slice(2, 10)}`,
            ipcIp: req.ipcIp,
            name: req.name,
            status: 'connected',
            createdAt: now,
            updatedAt: now
        };
        this.connections.set(connection.connectionId, connection);
        this.persistIpcConnection(connection);
        return connection;
    }
    listConnections() {
        return Array.from(this.connections.values());
    }
    getConnection(connectionId) {
        return this.connections.get(connectionId);
    }
    disconnectIpc(connectionId) {
        const connection = this.connections.get(connectionId);
        if (!connection) {
            return undefined;
        }
        connection.status = 'disconnected';
        connection.updatedAt = new Date().toISOString();
        this.connections.set(connectionId, connection);
        this.persistIpcConnection(connection);
        return connection;
    }
    createSession(req) {
        const connection = this.connections.get(req.connectionId);
        if (!connection) {
            throw new Error('connection not found');
        }
        const now = new Date().toISOString();
        const session = {
            sessionId: `sess_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
            connectionId: req.connectionId,
            ipcIp: connection.ipcIp,
            targetType: req.targetType,
            status: 'active',
            createdAt: now,
            updatedAt: now
        };
        this.sessions.set(session.sessionId, session);
        return session;
    }
    recoverSession(sessionId, req) {
        const connection = this.connections.get(req.connectionId);
        if (!connection) {
            throw new Error('connection not found');
        }
        const existing = this.sessions.get(sessionId);
        const now = new Date().toISOString();
        const recovered = {
            sessionId,
            connectionId: req.connectionId,
            ipcIp: connection.ipcIp,
            targetType: req.targetType,
            status: 'active',
            createdAt: existing?.createdAt ?? now,
            updatedAt: now
        };
        this.sessions.set(sessionId, recovered);
        return recovered;
    }
    listSessions() {
        return Array.from(this.sessions.values()).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
    }
    getSession(sessionId) {
        return this.sessions.get(sessionId);
    }
    ensureSession(sessionId, targetType = 'both') {
        const existing = this.sessions.get(sessionId);
        if (existing) {
            return existing;
        }
        const activeConnections = this.listActiveConnections();
        const selected = activeConnections[0];
        if (!selected) {
            return undefined;
        }
        return this.recoverSession(sessionId, {
            connectionId: selected.connectionId,
            targetType
        });
    }
    listActiveConnections() {
        return Array.from(this.connections.values())
            .filter((connection) => connection.status === 'connected')
            .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
    }
    appendAudit(event) {
        this.audits.unshift(event);
    }
    getAudits() {
        return this.audits.slice(0, 100);
    }
    appendChatRecord(chatId, text, isFinal, role = 'user_input') {
        const normalizedChatId = chatId || 'default';
        const normalizedText = text?.trim();
        if (!normalizedText) {
            return;
        }
        if (this.shouldSkipDuplicateUserInput(normalizedChatId, normalizedText, role)) {
            return;
        }
        const createdAt = this.formatNow();
        this.db.run(`INSERT INTO voice_chat_record (chat_id, text, role, is_final, created_at)
       VALUES (?, ?, ?, ?, ?)`, [normalizedChatId, normalizedText, role || 'user_input', isFinal ? 1 : 0, createdAt], () => {
            // no-op callback; persistence is best effort in event path
        });
    }
    async appendChatRecordWithId(chatId, text, isFinal, role = 'user_input') {
        const normalizedChatId = chatId || 'default';
        const normalizedText = text?.trim();
        if (!normalizedText) {
            return undefined;
        }
        if (this.shouldSkipDuplicateUserInput(normalizedChatId, normalizedText, role)) {
            return undefined;
        }
        const createdAt = this.formatNow();
        const messageId = await this.runInsert(`INSERT INTO voice_chat_record (chat_id, text, role, is_final, created_at)
       VALUES (?, ?, ?, ?, ?)`, [normalizedChatId, normalizedText, role || 'user_input', isFinal ? 1 : 0, createdAt]);
        return { messageId, timestamp: createdAt };
    }
    async findChatRecordById(chatId, messageId) {
        const normalizedChatId = chatId || 'default';
        const row = await this.get(`SELECT r.id,
              r.chat_id AS chatId,
              r.text,
              r.role,
              r.is_final AS isFinal,
              r.created_at AS createdAt,
              f.feedback AS feedback
       FROM voice_chat_record r
       LEFT JOIN chat_message_feedback f
         ON f.chat_id = r.chat_id AND f.message_id = r.id
       WHERE r.chat_id = ? AND r.id = ?
       LIMIT 1`, [normalizedChatId, messageId]);
        if (!row) {
            return undefined;
        }
        return {
            id: Number(row.id ?? 0),
            chatId: String(row.chatId ?? ''),
            text: String(row.text ?? ''),
            role: String(row.role ?? ''),
            isFinal: Number(row.isFinal ?? 0),
            createdAt: String(row.createdAt ?? ''),
            ...(row.feedback === 'like' || row.feedback === 'dislike'
                ? { feedback: row.feedback }
                : {})
        };
    }
    async findPreviousUserInput(chatId, beforeMessageId) {
        const normalizedChatId = chatId || 'default';
        const row = await this.get(`SELECT id,
              chat_id AS chatId,
              text,
              role,
              is_final AS isFinal,
              created_at AS createdAt
       FROM voice_chat_record
       WHERE chat_id = ? AND id < ? AND role = 'user_input'
       ORDER BY id DESC
       LIMIT 1`, [normalizedChatId, beforeMessageId]);
        if (!row) {
            return undefined;
        }
        return {
            id: Number(row.id ?? 0),
            chatId: String(row.chatId ?? ''),
            text: String(row.text ?? ''),
            role: String(row.role ?? ''),
            isFinal: Number(row.isFinal ?? 0),
            createdAt: String(row.createdAt ?? '')
        };
    }
    async setMessageFeedback(chatId, messageId, feedback) {
        const normalizedChatId = chatId || 'default';
        const updatedAt = this.formatNow();
        await this.run(`INSERT INTO chat_message_feedback (chat_id, message_id, feedback, updated_at)
       VALUES (?, ?, ?, ?)
       ON CONFLICT(chat_id, message_id)
       DO UPDATE SET feedback = excluded.feedback, updated_at = excluded.updated_at`, [normalizedChatId, messageId, feedback, updatedAt]);
    }
    async clearMessageFeedback(chatId, messageId) {
        const normalizedChatId = chatId || 'default';
        await this.run('DELETE FROM chat_message_feedback WHERE chat_id = ? AND message_id = ?', [normalizedChatId, messageId]);
    }
    async overwriteChatRecordText(chatId, messageId, text) {
        const normalizedChatId = chatId || 'default';
        const normalizedText = text?.trim();
        if (!normalizedText) {
            return undefined;
        }
        const updatedTimestamp = this.formatNow();
        const changed = await this.run(`UPDATE voice_chat_record
       SET text = ?, is_final = 1, created_at = ?
       WHERE chat_id = ? AND id = ?`, [normalizedText, updatedTimestamp, normalizedChatId, messageId]);
        if (changed <= 0) {
            return undefined;
        }
        return {
            messageId,
            timestamp: updatedTimestamp
        };
    }
    async findChatLatest(chatId, limit, roles) {
        const normalizedChatId = chatId || 'default';
        const take = Math.max(1, Math.min(limit || 200, 2000));
        const sql = roles && roles.length > 0
            ? `SELECT r.id,
                r.chat_id AS chatId,
                r.text,
                r.role,
                r.is_final AS isFinal,
                r.created_at AS createdAt,
                f.feedback AS feedback
         FROM voice_chat_record r
         LEFT JOIN chat_message_feedback f
           ON f.chat_id = r.chat_id AND f.message_id = r.id
         WHERE r.chat_id = ? AND r.role IN (${roles.map(() => '?').join(',')})
         ORDER BY r.id DESC
         LIMIT ?`
            : `SELECT r.id,
                r.chat_id AS chatId,
                r.text,
                r.role,
                r.is_final AS isFinal,
                r.created_at AS createdAt,
                f.feedback AS feedback
         FROM voice_chat_record r
         LEFT JOIN chat_message_feedback f
           ON f.chat_id = r.chat_id AND f.message_id = r.id
         WHERE r.chat_id = ?
         ORDER BY r.id DESC
         LIMIT ?`;
        const params = roles && roles.length > 0 ? [normalizedChatId, ...roles, take] : [normalizedChatId, take];
        const rows = await this.all(sql, params);
        return rows.reverse().map((row) => ({
            ...(row.feedback === 'like' || row.feedback === 'dislike'
                ? { feedback: row.feedback }
                : {}),
            id: Number(row.id ?? 0),
            chatId: String(row.chatId ?? ''),
            text: String(row.text ?? ''),
            role: String(row.role ?? ''),
            isFinal: Number(row.isFinal ?? 0),
            createdAt: String(row.createdAt ?? '')
        }));
    }
    async findChatPage(chatId, limit, beforeId, roles) {
        const normalizedChatId = chatId || 'default';
        const take = Math.max(1, Math.min(limit || 20, 200));
        const normalizedBeforeId = typeof beforeId === 'number' && Number.isFinite(beforeId) && beforeId > 0 ? beforeId : undefined;
        const whereParts = ['r.chat_id = ?'];
        const params = [normalizedChatId];
        if (roles && roles.length > 0) {
            whereParts.push(`r.role IN (${roles.map(() => '?').join(',')})`);
            params.push(...roles);
        }
        if (normalizedBeforeId) {
            whereParts.push('r.id < ?');
            params.push(normalizedBeforeId);
        }
        const sql = `SELECT r.id,
                        r.chat_id AS chatId,
                        r.text,
                        r.role,
                        r.is_final AS isFinal,
                        r.created_at AS createdAt,
                        f.feedback AS feedback
      FROM voice_chat_record r
      LEFT JOIN chat_message_feedback f
        ON f.chat_id = r.chat_id AND f.message_id = r.id
      WHERE ${whereParts.join(' AND ')}
      ORDER BY r.id DESC
      LIMIT ?`;
        params.push(take + 1);
        const rows = await this.all(sql, params);
        const hasMore = rows.length > take;
        const pageRows = hasMore ? rows.slice(0, take) : rows;
        const records = pageRows
            .reverse()
            .map((row) => ({
            ...(row.feedback === 'like' || row.feedback === 'dislike'
                ? { feedback: row.feedback }
                : {}),
            id: Number(row.id ?? 0),
            chatId: String(row.chatId ?? ''),
            text: String(row.text ?? ''),
            role: String(row.role ?? ''),
            isFinal: Number(row.isFinal ?? 0),
            createdAt: String(row.createdAt ?? '')
        }));
        const nextBeforeId = records.length > 0 ? records[0].id : undefined;
        return {
            records,
            hasMore,
            ...(nextBeforeId ? { nextBeforeId } : {})
        };
    }
    async clearChatRecords(chatId) {
        const normalizedChatId = chatId || 'default';
        return this.run('DELETE FROM voice_chat_record WHERE chat_id = ?', [normalizedChatId]);
    }
    all(sql, params) {
        return new Promise((resolve, reject) => {
            this.db.all(sql, params, (err, rows) => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve((rows ?? []));
            });
        });
    }
    get(sql, params) {
        return new Promise((resolve, reject) => {
            this.db.get(sql, params, (err, row) => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve(row ?? undefined);
            });
        });
    }
    run(sql, params) {
        return new Promise((resolve, reject) => {
            this.db.run(sql, params, function (err) {
                if (err) {
                    reject(err);
                    return;
                }
                resolve(this.changes ?? 0);
            });
        });
    }
    runInsert(sql, params) {
        return new Promise((resolve, reject) => {
            this.db.run(sql, params, function (err) {
                if (err) {
                    reject(err);
                    return;
                }
                resolve(Number(this.lastID ?? 0));
            });
        });
    }
    formatNow() {
        const now = new Date();
        const pad = (n) => n.toString().padStart(2, '0');
        return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
    }
    shouldSkipDuplicateUserInput(chatId, text, role) {
        if (role !== 'user_input') {
            return false;
        }
        const normalized = text.replace(/\s+/g, ' ').trim();
        if (!normalized) {
            return true;
        }
        const nowMs = Date.now();
        const last = this.recentUserInput.get(chatId);
        if (last && last.text === normalized && nowMs - last.tsMs <= this.userInputDedupWindowMs) {
            return true;
        }
        this.recentUserInput.set(chatId, { text: normalized, tsMs: nowMs });
        if (this.recentUserInput.size > 1000) {
            const expiredBefore = nowMs - Math.max(this.userInputDedupWindowMs * 4, 15000);
            for (const [key, value] of this.recentUserInput.entries()) {
                if (value.tsMs < expiredBefore) {
                    this.recentUserInput.delete(key);
                }
            }
        }
        return false;
    }
    parsePositiveInt(raw, fallback) {
        const parsed = Number.parseInt(raw ?? '', 10);
        if (!Number.isFinite(parsed) || parsed <= 0) {
            return fallback;
        }
        return parsed;
    }
    persistIpcConnection(connection) {
        this.db.run(`INSERT INTO ipc_connection (connection_id, ipc_ip, name, status, created_at, updated_at)
       VALUES (?, ?, ?, ?, ?, ?)
       ON CONFLICT(connection_id)
       DO UPDATE SET
         ipc_ip = excluded.ipc_ip,
         name = excluded.name,
         status = excluded.status,
         updated_at = excluded.updated_at`, [
            connection.connectionId,
            connection.ipcIp,
            connection.name ?? '',
            connection.status,
            connection.createdAt,
            connection.updatedAt
        ], () => {
            // persistence is best-effort for runtime path
        });
    }
};
exports.GatewayStateService = GatewayStateService;
exports.GatewayStateService = GatewayStateService = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [])
], GatewayStateService);
//# sourceMappingURL=gateway-state.service.js.map