"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Gateway = void 0;
const telegraf_1 = require("telegraf");
const filters_1 = require("telegraf/filters");
const IntentClassifier_1 = require("./IntentClassifier");
const Gatekeeper_1 = require("./Gatekeeper");
const DreamGoalStore_1 = require("./DreamGoalStore");
const VoiceManager_1 = require("./VoiceManager");
const axios_1 = __importDefault(require("axios"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const express_1 = __importDefault(require("express"));
const GovernanceLayer_1 = require("./GovernanceLayer");
const WatchdogHeartbeat_1 = require("./util/WatchdogHeartbeat");
const ToolsetGuard_1 = require("./toolset/ToolsetGuard");
const GatewayManifest = __importStar(require("./util/GatewayManifest"));
const BRIDGE_EVENT_BUFFER_MAX = 1000;
class Gateway {
    bot = null;
    reasoningMode = (() => {
        const raw = String(process.env.OPENLIFE_REASONING_MODE || 'errors').toLowerCase();
        return raw === 'off' || raw === 'always' ? raw : 'errors';
    })();
    classifier;
    gatekeeper;
    voiceManager;
    app;
    server = null;
    ttsEnabled;
    governance;
    allowedTelegramUserId;
    isShuttingDown = false;
    watchdog = null;
    // Phase 1 (autonomous milestone) — background mission poller. Only
    // instantiated when OPENLIFE_PROFILE=autonomous (or --enable-queue).
    queueRunner = null;
    bootTs = Date.now();
    manifestPublished = false;
    /** Cached package version for the manifest (read once, lazily). */
    packageVersion;
    // v1.11.0 — bridge event bus
    bridgeEvents = [];
    bridgeEventSeq = 0;
    bridgeSubscribers = new Set();
    /** Public emit hook so other subsystems can publish bridge events. */
    emitBridgeEvent(kind, payload) {
        this.bridgeEventSeq += 1;
        const ev = {
            seq: this.bridgeEventSeq,
            ts: new Date().toISOString(),
            kind,
            payload,
        };
        this.bridgeEvents.push(ev);
        // Bounded ring — drop the oldest when over capacity. Splice keeps the
        // tail contiguous so Last-Event-Id lookup stays O(n).
        if (this.bridgeEvents.length > BRIDGE_EVENT_BUFFER_MAX) {
            this.bridgeEvents.splice(0, this.bridgeEvents.length - BRIDGE_EVENT_BUFFER_MAX);
        }
        for (const sub of this.bridgeSubscribers) {
            try {
                sub(ev);
            }
            catch (err) {
                console.error('[GATEWAY] bridge subscriber threw:', err);
            }
        }
    }
    /** Replay events with seq > since (or all if since is null/undefined). */
    bridgeEventsSince(since) {
        if (since == null || since <= 0)
            return [...this.bridgeEvents];
        return this.bridgeEvents.filter(e => e.seq > since);
    }
    constructor() {
        this.classifier = new IntentClassifier_1.IntentClassifier();
        this.gatekeeper = new Gatekeeper_1.Gatekeeper();
        this.voiceManager = new VoiceManager_1.VoiceManager();
        this.app = (0, express_1.default)();
        this.app.use(express_1.default.json());
        this.ttsEnabled = (process.env.OPENLIFE_ENABLE_TTS || '').toLowerCase() === 'true';
        this.governance = new GovernanceLayer_1.GovernanceLayer();
        this.allowedTelegramUserId = (process.env.OPENLIFE_TELEGRAM_ALLOWED_USER_ID || '1344110010').trim() || null;
        const token = process.env.TELEGRAM_BOT_TOKEN;
        if (token && token.trim() !== '') {
            this.bot = new telegraf_1.Telegraf(token);
        }
    }
    /**
     * Redact a Telegram bot token in any log line. Public so callers can
     * use the same helper to scrub their own logs. Token shape is
     * `<numeric_id>:<35-char_secret>`; we keep the numeric prefix and
     * mask the rest.
     *
     * Story 4.3 — OpenLife v1.3 Agent OS Integration.
     */
    static redactBotToken(text) {
        return String(text).replace(/(\d{6,12}):[A-Za-z0-9_-]{20,}/g, '$1:[REDACTED]');
    }
    static classifyTelegramLaunchError(err) {
        const raw = err instanceof Error ? err.message : String(err || '');
        const detail = Gateway.redactBotToken(raw).slice(0, 500);
        if (raw.includes('409') || raw.includes('terminated by other getUpdates request') || raw.toLowerCase().includes('conflict')) {
            return {
                ok: false,
                error: 'telegram_polling_conflict',
                recoveryOptions: ['stop_conflicting_instance', 'replace_token', 'skip_telegram_for_now'],
                detail,
            };
        }
        if (!raw)
            return { ok: true };
        return { ok: false, error: 'telegram_launch_failed', recoveryOptions: ['check_token', 'switch_to_webhook', 'skip_telegram_for_now'], detail };
    }
    /**
     * Validate the configured TELEGRAM_BOT_TOKEN by calling getMe.
     * Returns `{ ok: true, botId, botUsername }` on success;
     * `{ ok: false, error, status? }` on failure. Never throws.
     * Never logs the token itself.
     *
     * Story 4.3 — OpenLife v1.3 Agent OS Integration.
     */
    async validateBotToken(opts = {}) {
        const token = process.env.TELEGRAM_BOT_TOKEN;
        if (!token || token.trim() === '') {
            return { ok: false, error: 'token_missing' };
        }
        const timeoutMs = opts.timeoutMs ?? 8000;
        const url = `https://api.telegram.org/bot${token}/getMe`;
        try {
            const controller = new AbortController();
            const timer = setTimeout(() => controller.abort(), timeoutMs);
            const res = await fetch(url, { signal: controller.signal });
            clearTimeout(timer);
            if (!res.ok) {
                return { ok: false, error: 'getme_failed', status: res.status };
            }
            const body = await res.json();
            if (!body?.ok || !body.result?.id || !body.result.username) {
                return { ok: false, error: 'invalid_getme_response' };
            }
            return { ok: true, botId: body.result.id, botUsername: body.result.username };
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            return { ok: false, error: 'getme_exception', status: msg.includes('aborted') ? 408 : undefined };
        }
    }
    start() {
        (0, ToolsetGuard_1.assertToolsetAllowed)('gateway', 'Gateway.start');
        const adminUser = process.env.OPENLIFE_ADMIN_USER || 'admin';
        const adminPass = process.env.OPENLIFE_ADMIN_PASS || '';
        const adminAuth = (req, res, next) => {
            if (!adminPass)
                return next();
            const header = String(req.headers.authorization || '');
            if (!header.startsWith('Basic ')) {
                res.setHeader('WWW-Authenticate', 'Basic realm="OpenLife Admin"');
                return res.status(401).json({ ok: false, error: 'auth_required' });
            }
            const decoded = Buffer.from(header.slice(6), 'base64').toString('utf-8');
            const [user, pass] = decoded.split(':');
            if (user !== adminUser || pass !== adminPass) {
                return res.status(403).json({ ok: false, error: 'forbidden' });
            }
            next();
        };
        const triggerUser = (process.env.OPENLIFE_TRIGGER_USER || '').trim();
        const triggerPass = (process.env.OPENLIFE_TRIGGER_PASS || '').trim();
        const triggerAuthEnabled = !!(triggerUser && triggerPass);
        if (!triggerAuthEnabled) {
            console.warn('[GATEWAY] WARNING: /api/v1/trigger sem autenticação. Defina OPENLIFE_TRIGGER_USER e OPENLIFE_TRIGGER_PASS para internet-exposed deploys.');
        }
        const triggerAuth = (req, res, next) => {
            if (!triggerAuthEnabled)
                return next();
            const header = String(req.headers.authorization || '');
            if (!header.startsWith('Basic ')) {
                res.setHeader('WWW-Authenticate', 'Basic realm="OpenLife Trigger"');
                return res.status(401).json({ ok: false, error: 'auth_required' });
            }
            const decoded = Buffer.from(header.slice(6), 'base64').toString('utf-8');
            const sepIdx = decoded.indexOf(':');
            const user = sepIdx >= 0 ? decoded.slice(0, sepIdx) : decoded;
            const pass = sepIdx >= 0 ? decoded.slice(sepIdx + 1) : '';
            if (user !== triggerUser || pass !== triggerPass) {
                return res.status(403).json({ ok: false, error: 'forbidden' });
            }
            next();
        };
        // v1.11.0 — Core ↔ Agent Bridge: Basic Auth using the manifest token.
        // user is the fixed literal 'openlife-core', password is the rotating
        // 32-byte token published in ~/.openlife/gateway-manifest.json.
        const bridgeAuth = (req, res, next) => {
            const expected = GatewayManifest.peek()?.token;
            if (!expected) {
                return res.status(503).json({ ok: false, error: 'manifest_not_published' });
            }
            const header = String(req.headers.authorization || '');
            if (!header.startsWith('Basic ')) {
                res.setHeader('WWW-Authenticate', 'Basic realm="OpenLife Bridge"');
                return res.status(401).json({ ok: false, error: 'auth_required' });
            }
            const decoded = Buffer.from(header.slice(6), 'base64').toString('utf-8');
            const sepIdx = decoded.indexOf(':');
            const user = sepIdx >= 0 ? decoded.slice(0, sepIdx) : decoded;
            const pass = sepIdx >= 0 ? decoded.slice(sepIdx + 1) : '';
            if (user !== GatewayManifest.MANIFEST_AUTH_USER || pass !== expected) {
                return res.status(403).json({ ok: false, error: 'forbidden' });
            }
            next();
        };
        // v1.11.0 — POST /api/v1/core/run — symmetric endpoint to /trigger but
        // shaped for core CLI integration. Accepts {verb, args?, text?, sync?}.
        this.app.post('/api/v1/core/run', bridgeAuth, async (req, res) => {
            const body = (req.body && typeof req.body === 'object') ? req.body : {};
            const verb = typeof body.verb === 'string' ? body.verb : null;
            const args = Array.isArray(body.args) ? body.args.map(String) : [];
            const text = typeof body.text === 'string' ? body.text : null;
            const userId = typeof body.userId === 'string' ? body.userId : 'bridge';
            this.emitBridgeEvent('command', { verb, args, userId, source: 'http' });
            if (!verb) {
                return res.status(400).json({ ok: false, error: 'verb_required' });
            }
            try {
                switch (verb) {
                    case 'chat': {
                        if (!text)
                            return res.status(400).json({ ok: false, error: 'text_required_for_chat' });
                        const reply = await this.processChat(userId, text);
                        this.emitBridgeEvent('pipeline', { stage: 'finalize', verb, userId });
                        return res.status(200).json({ ok: true, verb, reply });
                    }
                    case 'status': {
                        const manifest = GatewayManifest.peek();
                        return res.status(200).json({
                            ok: true,
                            verb,
                            pid: process.pid,
                            uptime_s: Math.floor((Date.now() - this.bootTs) / 1000),
                            telegram: !!this.bot,
                            shutting_down: this.isShuttingDown,
                            manifest: manifest ? {
                                host: manifest.host,
                                port: manifest.port,
                                profile: manifest.profile,
                                startedAt: manifest.startedAt,
                                refreshedAt: manifest.refreshedAt,
                                version: manifest.version,
                            } : null,
                        });
                    }
                    case 'memory': {
                        const query = text || args.join(' ');
                        if (!query)
                            return res.status(400).json({ ok: false, error: 'query_required_for_memory' });
                        const { OmniMemory } = require('../memory/OmniMemory');
                        const omni = new OmniMemory();
                        const result = await omni.search(query);
                        this.emitBridgeEvent('memory', { query, hasResult: !!result });
                        return res.status(200).json({ ok: true, verb, query, result: result || null });
                    }
                    default:
                        return res.status(400).json({ ok: false, error: 'unknown_verb', verb });
                }
            }
            catch (err) {
                console.error('[GATEWAY] /api/v1/core/run failed:', err);
                return res.status(500).json({ ok: false, error: 'internal_error', detail: err instanceof Error ? err.message : String(err) });
            }
        });
        // v1.12.0 — Story HTTP endpoints. Same bridgeAuth as /core/run.
        // Lazy-imported so Gateway startup cost doesn't change for users
        // who never call these.
        this.app.get('/api/v1/stories', bridgeAuth, async (req, res) => {
            try {
                const { StoryRegistry } = require('./story/StoryRegistry');
                const reg = new StoryRegistry();
                await reg.load();
                const epicId = typeof req.query.epic === 'string' ? req.query.epic : undefined;
                const status = typeof req.query.status === 'string' ? req.query.status : undefined;
                const cluster = typeof req.query.cluster === 'string' ? req.query.cluster : undefined;
                const stories = reg.list({ epicId, status: status, cluster });
                return res.status(200).json({
                    ok: true,
                    count: stories.length,
                    stories: stories.map((s) => ({
                        id: s.id, epicId: s.epicId, title: s.title, status: s.status,
                        severity: s.severity, cluster: s.cluster, dependsOn: s.dependsOn,
                    })),
                });
            }
            catch (err) {
                return res.status(500).json({ ok: false, error: 'internal_error', detail: err instanceof Error ? err.message : String(err) });
            }
        });
        this.app.get('/api/v1/stories/:id', bridgeAuth, async (req, res) => {
            try {
                const { StoryRegistry } = require('./story/StoryRegistry');
                const reg = new StoryRegistry();
                await reg.load();
                const story = reg.get(req.params.id);
                if (!story)
                    return res.status(404).json({ ok: false, error: 'not_found', id: req.params.id });
                return res.status(200).json({ ok: true, story });
            }
            catch (err) {
                return res.status(500).json({ ok: false, error: 'internal_error', detail: err instanceof Error ? err.message : String(err) });
            }
        });
        this.app.get('/api/v1/epics', bridgeAuth, async (_req, res) => {
            try {
                const { StoryRegistry } = require('./story/StoryRegistry');
                const reg = new StoryRegistry();
                await reg.load();
                const epics = reg.listEpics();
                return res.status(200).json({
                    ok: true, count: epics.length,
                    epics: epics.map((e) => ({
                        id: e.id, title: e.title, status: e.status, storyIds: e.storyIds,
                    })),
                });
            }
            catch (err) {
                return res.status(500).json({ ok: false, error: 'internal_error', detail: err instanceof Error ? err.message : String(err) });
            }
        });
        this.app.get('/api/v1/epics/:id', bridgeAuth, async (req, res) => {
            try {
                const { StoryRegistry } = require('./story/StoryRegistry');
                const reg = new StoryRegistry();
                await reg.load();
                const epic = reg.getEpic(req.params.id);
                if (!epic)
                    return res.status(404).json({ ok: false, error: 'not_found', id: req.params.id });
                return res.status(200).json({ ok: true, epic });
            }
            catch (err) {
                return res.status(500).json({ ok: false, error: 'internal_error', detail: err instanceof Error ? err.message : String(err) });
            }
        });
        this.app.post('/api/v1/stories/:id/transition', bridgeAuth, async (req, res) => {
            try {
                const body = (req.body && typeof req.body === 'object') ? req.body : {};
                const to = typeof body.to === 'string' ? body.to : null;
                const role = typeof body.role === 'string' ? body.role : undefined;
                const author = typeof body.author === 'string' ? body.author : undefined;
                const note = typeof body.note === 'string' ? body.note : undefined;
                if (!to)
                    return res.status(400).json({ ok: false, error: 'to_required' });
                const { StoryRegistry } = require('./story/StoryRegistry');
                const { canTransition, currentRole } = require('./story/StoryLifecycle');
                const { updateStoryStatus } = require('./story/StoryWriter');
                const reg = new StoryRegistry();
                await reg.load();
                const story = reg.get(req.params.id);
                if (!story)
                    return res.status(404).json({ ok: false, error: 'not_found', id: req.params.id });
                const decision = canTransition(story.status, to, currentRole(role));
                if (!decision.allowed)
                    return res.status(409).json({ ok: false, error: 'transition_denied', reason: decision.reason });
                updateStoryStatus(story.sourcePath, story.status, to, { author, note });
                return res.status(200).json({ ok: true, id: story.id, from: story.status, to, path: story.sourcePath });
            }
            catch (err) {
                return res.status(500).json({ ok: false, error: 'internal_error', detail: err instanceof Error ? err.message : String(err) });
            }
        });
        // v1.11.0 — GET /api/v1/events — Server-Sent Events stream.
        // Supports Last-Event-Id header OR `?since=<seq>` query param to resume.
        this.app.get('/api/v1/events', bridgeAuth, (req, res) => {
            res.setHeader('Content-Type', 'text/event-stream');
            res.setHeader('Cache-Control', 'no-cache, no-transform');
            res.setHeader('Connection', 'keep-alive');
            res.flushHeaders?.();
            const sinceHeader = req.headers['last-event-id'];
            const sinceQs = typeof req.query.since === 'string' ? req.query.since : '';
            const since = Number(sinceHeader || sinceQs);
            const sinceSeq = Number.isFinite(since) && since > 0 ? since : null;
            // Replay missed events first
            for (const ev of this.bridgeEventsSince(sinceSeq)) {
                this.writeSseEvent(res, ev);
            }
            const sub = (ev) => this.writeSseEvent(res, ev);
            this.bridgeSubscribers.add(sub);
            // Periodic keep-alive comment so proxies don't time out idle connections.
            const keepAlive = setInterval(() => {
                try {
                    res.write(': keep-alive\n\n');
                }
                catch { /* client gone */ }
            }, 15_000);
            if (typeof keepAlive.unref === 'function')
                keepAlive.unref();
            const cleanup = () => {
                this.bridgeSubscribers.delete(sub);
                clearInterval(keepAlive);
            };
            req.on('close', cleanup);
            req.on('aborted', cleanup);
        });
        // Configuração do Webhook (Fase 4)
        this.app.post('/api/v1/trigger', triggerAuth, async (req, res) => {
            const payload = req.body;
            console.log('[WEBHOOK] Nova task recebida:', payload);
            if (process.env.OPENLIFE_TRIGGER_TEST_ECHO === '1') {
                return res.status(200).json({ status: 'success', testEcho: true });
            }
            // Simula o contexto do Telegram para o pipeline interno do Brain
            const mockCtx = {
                reply: async (text) => console.log('[WEBHOOK-REPLY]', text),
                sendChatAction: async (action) => { },
                message: { from: { id: payload.user_id || 'webhook' }, text: payload.text }
            };
            try {
                await this.processInput(mockCtx, mockCtx.message.from.id.toString(), payload.text);
                res.status(200).json({ status: 'success', message: 'Task enviada ao Córtex' });
            }
            catch (err) {
                res.status(500).json({ status: 'error', error: String(err) });
            }
        });
        this.app.get('/api/v1/admin/teams', adminAuth, (_req, res) => {
            const file = path.join(process.cwd(), '.catalog', 'teams.json');
            const teams = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf-8')) : [];
            res.status(200).json({ ok: true, teams });
        });
        this.app.get('/api/v1/admin/networks', adminAuth, (_req, res) => {
            const file = path.join(process.cwd(), '.catalog', 'skill-networks.json');
            const networks = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf-8')) : [];
            res.status(200).json({ ok: true, networks });
        });
        this.app.post('/api/v1/admin/teams/upsert', adminAuth, (req, res) => {
            const body = req.body || {};
            if (!body.id)
                return res.status(400).json({ ok: false, error: 'id obrigatório' });
            const dir = path.join(process.cwd(), '.catalog');
            const file = path.join(dir, 'teams.json');
            fs.mkdirSync(dir, { recursive: true });
            const arr = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf-8')) : [];
            const next = { id: String(body.id), domain: String(body.domain || 'general'), description: String(body.description || ''), slaProfile: String(body.slaProfile || 'balanced'), members: Array.isArray(body.members) ? body.members : [] };
            const idx = arr.findIndex((x) => x.id === next.id);
            if (idx >= 0)
                arr[idx] = { ...arr[idx], ...next };
            else
                arr.push(next);
            fs.writeFileSync(file, JSON.stringify(arr, null, 2), 'utf-8');
            res.status(200).json({ ok: true, team: next });
        });
        this.app.post('/api/v1/admin/networks/upsert', adminAuth, (req, res) => {
            const body = req.body || {};
            if (!body.id)
                return res.status(400).json({ ok: false, error: 'id obrigatório' });
            const dir = path.join(process.cwd(), '.catalog');
            const file = path.join(dir, 'skill-networks.json');
            fs.mkdirSync(dir, { recursive: true });
            const arr = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf-8')) : [];
            const next = { id: String(body.id), domain: String(body.domain || 'general'), nodes: Array.isArray(body.nodes) ? body.nodes : [] };
            const idx = arr.findIndex((x) => x.id === next.id);
            if (idx >= 0)
                arr[idx] = { ...arr[idx], ...next };
            else
                arr.push(next);
            fs.writeFileSync(file, JSON.stringify(arr, null, 2), 'utf-8');
            res.status(200).json({ ok: true, network: next });
        });
        this.app.get('/api/v1/admin/status', adminAuth, (_req, res) => {
            res.status(200).json({ ok: true, service: 'openlife-gateway', mode: this.reasoningMode, telegram: !!this.bot, now: new Date().toISOString() });
        });
        this.app.get('/admin', adminAuth, (_req, res) => {
            res.sendFile(path.join(process.cwd(), 'src', 'admin_panel.html'));
        });
        this.app.get('/', (_req, res) => {
            res.status(200).json({ ok: true, service: 'openlife-gateway' });
        });
        // Story 6.6 — watchdog/health endpoint for external supervisors
        this.app.get('/health', (_req, res) => {
            const uptime_s = Math.floor((Date.now() - this.bootTs) / 1000);
            res.status(200).json({
                ok: true,
                service: 'openlife-gateway',
                uptime_s,
                pid: process.pid,
                telegram: !!this.bot,
                shutting_down: this.isShuttingDown,
                ts: new Date().toISOString(),
            });
        });
        // Heartbeat file emission (best-effort; opt-out via OPENLIFE_HEARTBEAT_DISABLE).
        // v1.11.0: piggyback gateway-manifest refresh on the same tick so external
        // CLIs see a fresh `refreshedAt` every 30s without a second timer.
        if ((process.env.OPENLIFE_HEARTBEAT_DISABLE || '').toLowerCase() !== 'true') {
            try {
                const intervalMs = Number(process.env.OPENLIFE_HEARTBEAT_INTERVAL_MS || 30_000);
                this.watchdog = new WatchdogHeartbeat_1.WatchdogHeartbeat({
                    intervalMs,
                    onTick: () => {
                        if (this.manifestPublished)
                            GatewayManifest.refresh();
                    },
                });
                this.watchdog.start();
            }
            catch (err) {
                console.error('[GATEWAY] Failed to start watchdog heartbeat:', err);
            }
        }
        const port = Number(process.env.PORT || 3000);
        this.server = this.app.listen(port, () => {
            // Read actual bound port (handles PORT=0 auto-assignment).
            const addr = this.server?.address();
            const boundPort = (typeof addr === 'object' && addr && 'port' in addr)
                ? addr.port
                : port;
            console.log(`[GATEWAY] Servidor Express de Webhooks rodando na porta ${boundPort}`);
            // v1.11.0: publish discovery manifest now that the port is bound.
            try {
                GatewayManifest.publish({
                    port: boundPort,
                    profile: process.env.OPENLIFE_PROFILE || 'autonomous',
                    telegram: {
                        enabled: !!this.bot,
                        allowedUserId: this.allowedTelegramUserId,
                    },
                    version: this.resolvePackageVersion(),
                });
                this.manifestPublished = true;
                console.log('[GATEWAY] gateway-manifest publicado para discovery do CLI core');
            }
            catch (err) {
                console.error('[GATEWAY] Falha ao publicar gateway-manifest:', err);
            }
        });
        // Phase 1 (autonomous milestone) — start the background mission
        // poller when running the autonomous profile (or when explicitly
        // opted in via OPENLIFE_ENABLE_QUEUE). Off by default for the
        // framework/CLI profile so reactive behavior is unchanged.
        this.maybeStartQueueRunner();
        if (!this.bot) {
            console.log("[GATEWAY] Telegram Token não fornecido. Rodando apenas em modo CLI local.");
            return;
        }
        console.log("[GATEWAY] Inicializando receptor Multimodal do Telegram...");
        this.bot.catch((err, ctx) => {
            console.error('[GATEWAY] Erro global no Telegram update handler:', err);
            void this.safeReply(ctx, 'Desculpe — houve timeout no processamento. Tente novamente em instantes.');
        });
        // 1. Processamento de TEXTO
        this.bot.on((0, filters_1.message)('text'), async (ctx) => {
            if (ctx.message.from.is_bot)
                return;
            const userId = ctx.message.from.id.toString();
            const text = ctx.message.text;
            console.log(`\n[TELEGRAM] Nova mensagem de texto de ${userId}: "${text}"`);
            try {
                await this.processInput(ctx, userId, text);
            }
            catch (err) {
                console.error('[GATEWAY] Erro no handler de texto:', err);
            }
        });
        // 2. Processamento de ÁUDIO (Ouvidos)
        this.bot.on((0, filters_1.message)('voice'), async (ctx) => {
            if (ctx.message.from.is_bot)
                return;
            const userId = ctx.message.from.id.toString();
            console.log(`\n[TELEGRAM] Novo Áudio recebido de ${userId}`);
            await ctx.sendChatAction('record_voice');
            try {
                // Aqui o Telegram nos dá o Link do arquivo OGG.
                // O VoiceManager vai processar (via Whisper ou preparar para Gemini 2.5)
                const voiceFileId = ctx.message.voice.file_id;
                const fileLink = await ctx.telegram.getFileLink(voiceFileId);
                const transcribedText = await this.voiceManager.processAudioInput(fileLink.toString());
                await ctx.reply(`🎙️ (Ouvido): "${transcribedText}"`);
                await this.processInput(ctx, userId, transcribedText);
            }
            catch (err) {
                console.error("[GATEWAY] Erro ao processar áudio:", err);
                await ctx.reply("Desculpe, não consegui entender o áudio no momento.");
            }
        });
        // 3. Processamento de IMAGEM (Olhos)
        this.bot.on((0, filters_1.message)('photo'), async (ctx) => {
            if (ctx.message.from.is_bot)
                return;
            const userId = ctx.message.from.id.toString();
            console.log(`\n[TELEGRAM] Nova Imagem (Visão) recebida de ${userId}`);
            await ctx.sendChatAction('typing');
            try {
                const photo = ctx.message.photo.pop(); // Pega a maior resolução
                if (!photo)
                    throw new Error("Foto inválida.");
                const fileLink = await ctx.telegram.getFileLink(photo.file_id);
                // Download the image locally
                const tempDir = path.join(process.cwd(), '.temp_images');
                if (!fs.existsSync(tempDir))
                    fs.mkdirSync(tempDir, { recursive: true });
                const localPath = path.join(tempDir, `${photo.file_id}.jpg`);
                const response = await (0, axios_1.default)({ url: fileLink.toString(), responseType: 'stream' });
                await new Promise((resolve, reject) => {
                    response.data.pipe(fs.createWriteStream(localPath))
                        .on('finish', resolve)
                        .on('error', reject);
                });
                const caption = ctx.message.caption || "Descreva e analise esta imagem enviada pelo usuário.";
                // O texto enviado com a imagem + a tag do arquivo local vai pro Córtex (Brain.ts resolverá)
                this.voiceManager.logMediaRoute('vision', 'gemini', 'success', 'prefer_gemini_for_media=true');
                const multimodalInput = `${caption}\n[LOCAL_ATTACHMENT: ${localPath}]`;
                await this.processInput(ctx, userId, multimodalInput);
            }
            catch (err) {
                console.error("[GATEWAY] Erro ao processar imagem:", err);
                await ctx.reply("Erro no módulo de visão ao baixar a imagem.");
            }
        });
        // Decide Telegram delivery mode:
        //   - OPENLIFE_TELEGRAM_MODE=polling | webhook | auto (default: auto)
        //   - "auto" → webhook if a Railway env var is present, else polling.
        // This is the simplest UX for the "Railway + local" topology: Railway
        // deploys boot in webhook mode (no polling conflict with local), local
        // dev boots in polling mode (no webhook URL needed). User changes
        // nothing.
        const mode = String(process.env.OPENLIFE_TELEGRAM_MODE || 'auto').toLowerCase();
        const onRailway = !!(process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PUBLIC_DOMAIN || process.env.RAILWAY_STATIC_URL);
        const useWebhook = mode === 'webhook' || (mode === 'auto' && onRailway);
        if (useWebhook) {
            const webhookPath = process.env.OPENLIFE_WEBHOOK_PATH || '/api/v1/telegram/webhook';
            // Register webhook handler on the existing Express app — no separate
            // HTTP server, no port conflict. Telegraf's webhookCallback returns
            // an Express-compatible middleware that parses updates.
            // Important: do not mount the callback at `webhookPath` while also
            // passing `webhookPath` into Telegraf. Express strips the mount
            // prefix before the middleware sees `req.url`, so Telegraf would
            // see `/` and skip the update, causing Telegram to receive 404.
            this.app.use(this.bot.webhookCallback(webhookPath));
            const rawDomain = process.env.OPENLIFE_WEBHOOK_DOMAIN
                || process.env.RAILWAY_PUBLIC_DOMAIN
                || process.env.RAILWAY_STATIC_URL
                || '';
            if (rawDomain) {
                const domain = rawDomain.startsWith('http') ? rawDomain : `https://${rawDomain}`;
                const fullUrl = `${domain.replace(/\/$/, '')}${webhookPath}`;
                this.bot.telegram.setWebhook(fullUrl).then(() => {
                    console.log(`[GATEWAY] Telegram em modo WEBHOOK @ ${fullUrl}`);
                }).catch((err) => {
                    console.error('[GATEWAY] setWebhook falhou:', err instanceof Error ? err.message : String(err));
                });
            }
            else {
                console.warn('[GATEWAY] Webhook mode ativado mas OPENLIFE_WEBHOOK_DOMAIN (ou RAILWAY_PUBLIC_DOMAIN) não está definido. Telegram não saberá onde entregar mensagens. Defina o domínio público antes de subir.');
            }
            console.log("[GATEWAY] Telegram Bot pronto (modo webhook — sem polling).");
        }
        else {
            // Default polling mode (local dev, single-instance bots).
            // Clear any previously-registered webhook before polling, otherwise
            // Telegram rejects getUpdates with 409. Best-effort, non-fatal.
            this.bot.telegram.deleteWebhook({ drop_pending_updates: false }).catch(() => { });
            this.bot.launch().catch((err) => {
                const classified = Gateway.classifyTelegramLaunchError(err);
                if ('error' in classified && classified.error === 'telegram_polling_conflict') {
                    console.error(`[GATEWAY] Telegram 409 Conflict: outra instância está conectada com este bot. Recovery: ${classified.recoveryOptions.join(' | ')}. Detail: ${classified.detail}`);
                    return;
                }
                console.error('[GATEWAY] Falha ao iniciar Telegram bot:', err);
                process.exit(1);
            });
            console.log("[GATEWAY] Telegram Bot escutando (modo polling) com suporte Multimodal (Voz/Visão).");
        }
    }
    /**
     * Phase 1 — instantiate + start the QueueRunner when the autonomous
     * profile is active. Gated so the framework/CLI profile keeps its
     * reactive-only behavior. Lazy-require keeps the orchestration stack out
     * of `--help` and non-daemon command paths.
     */
    maybeStartQueueRunner() {
        const profile = (process.env.OPENLIFE_PROFILE || 'autonomous').toLowerCase();
        const optIn = (process.env.OPENLIFE_ENABLE_QUEUE || '').toLowerCase() === 'true';
        const enabled = profile === 'autonomous' || optIn;
        if (!enabled) {
            console.log('[GATEWAY] Queue runner desativado (profile != autonomous). Set OPENLIFE_ENABLE_QUEUE=true para forçar.');
            return;
        }
        try {
            // eslint-disable-next-line @typescript-eslint/no-var-requires
            const { QueueRunner } = require('./QueueRunner');
            this.queueRunner = new QueueRunner({
                // Phase 2 — surface goal-compiler decisions on the bridge so
                // the CLI/Telegram can observe held + blocked drafts.
                onConsentRequired: (draft, decision) => {
                    this.emitBridgeEvent('governance', {
                        event: 'goal_draft_consent_required',
                        goal: draft.goal,
                        riskLevel: decision.riskLevel,
                        blockedCapabilities: decision.blockedCapabilities,
                    });
                },
                onGoalDraftBlocked: (draft, decision) => {
                    this.emitBridgeEvent('governance', {
                        event: 'goal_draft_blocked',
                        goal: draft.goal,
                        riskLevel: decision.riskLevel,
                        blockedCapabilities: decision.blockedCapabilities,
                    });
                },
            });
            this.queueRunner.start();
            this.emitBridgeEvent('mission', { event: 'queue_runner_started', profile });
        }
        catch (err) {
            console.error('[GATEWAY] Falha ao iniciar QueueRunner:', err instanceof Error ? err.message : String(err));
            this.queueRunner = null;
        }
    }
    async shutdown(reason = 'manual') {
        if (this.isShuttingDown)
            return;
        this.isShuttingDown = true;
        console.log(`[GATEWAY] Iniciando shutdown gracioso (motivo: ${reason})...`);
        // Phase 1 — drain in-flight missions before tearing down the server.
        // This must run BEFORE server.close() so missions writing results
        // aren't cut off mid-flight (fixes the connection-leak class in
        // test_daemon_sigterm).
        if (this.queueRunner) {
            try {
                await this.queueRunner.stop();
            }
            catch (err) {
                console.error('[GATEWAY] Falha ao drenar QueueRunner:', err instanceof Error ? err.message : String(err));
            }
            this.queueRunner = null;
        }
        if (this.watchdog) {
            try {
                this.watchdog.emit(); // final beat so supervisors see fresh ts before we go
                this.watchdog.stop();
            }
            catch (err) {
                console.error('[GATEWAY] Falha ao parar watchdog heartbeat:', err);
            }
        }
        // v1.11.0: explicit manifest removal so AgentDiscovery sees "missing"
        // instead of an aging refreshedAt that would only classify as "dead"
        // after 5 minutes.
        if (this.manifestPublished) {
            try {
                GatewayManifest.remove();
                this.manifestPublished = false;
            }
            catch (err) {
                console.error('[GATEWAY] Falha ao remover gateway-manifest:', err);
            }
        }
        try {
            this.flushAgentQueue();
        }
        catch (err) {
            console.error('[GATEWAY] Falha ao flushar fila durante shutdown:', err);
        }
        if (this.bot) {
            try {
                this.bot.stop(reason);
            }
            catch (err) {
                console.error('[GATEWAY] Falha ao parar Telegraf bot:', err);
            }
        }
        if (this.server) {
            await new Promise((resolve) => {
                this.server.close((err) => {
                    if (err)
                        console.error('[GATEWAY] Falha ao fechar servidor Express:', err);
                    resolve();
                });
            });
            this.server = null;
        }
        console.log('[GATEWAY] Graceful shutdown complete');
    }
    writeSseEvent(res, ev) {
        try {
            res.write(`id: ${ev.seq}\n`);
            res.write(`event: ${ev.kind}\n`);
            res.write(`data: ${JSON.stringify({ seq: ev.seq, ts: ev.ts, kind: ev.kind, payload: ev.payload })}\n\n`);
        }
        catch (err) {
            console.error('[GATEWAY] SSE write failed:', err);
        }
    }
    resolvePackageVersion() {
        if (this.packageVersion !== undefined)
            return this.packageVersion;
        // Walk up from __dirname looking for package.json — robust to dist/src layouts.
        const candidates = [
            path.join(__dirname, '..', '..', 'package.json'),
            path.join(__dirname, '..', 'package.json'),
            path.join(process.cwd(), 'package.json'),
        ];
        for (const p of candidates) {
            try {
                if (fs.existsSync(p)) {
                    const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
                    if (typeof raw?.version === 'string') {
                        this.packageVersion = raw.version;
                        return this.packageVersion;
                    }
                }
            }
            catch { /* try next */ }
        }
        this.packageVersion = undefined;
        return undefined;
    }
    flushAgentQueue() {
        const stateDir = process.env.OPENLIFE_STATE_DIR || path.join(process.cwd(), '.openlife');
        if (!fs.existsSync(stateDir)) {
            fs.mkdirSync(stateDir, { recursive: true });
        }
        const queueFile = path.join(stateDir, 'agent-queue.json');
        if (!fs.existsSync(queueFile)) {
            fs.writeFileSync(queueFile, JSON.stringify({ items: [], lastFlushedAt: new Date().toISOString() }, null, 2), 'utf-8');
            return;
        }
        try {
            const raw = JSON.parse(fs.readFileSync(queueFile, 'utf-8'));
            raw.lastFlushedAt = new Date().toISOString();
            fs.writeFileSync(queueFile, JSON.stringify(raw, null, 2), 'utf-8');
        }
        catch {
            fs.writeFileSync(queueFile, JSON.stringify({ items: [], lastFlushedAt: new Date().toISOString(), note: 'recreated_after_parse_error' }, null, 2), 'utf-8');
        }
    }
    async processTextForTest(userId, text) {
        const detailed = await this.processTextForTestDetailed(userId, text);
        return detailed.finalText;
    }
    /**
     * Public chat entry point for non-Telegram surfaces (CLI chat TUI, HTTP
     * pilots, etc.). Wraps the same processInput pipeline used by the Telegram
     * flow but routes replies through a minimal in-memory ctx so no Telegraf
     * dependency is required at the call site.
     *
     * onReasoning fires for each pipeline trace event (classify → route →
     * finalize) so a TUI can stream the agent's thinking into the Matrix-rain
     * region instead of showing an opaque spinner.
     */
    async processChat(userId, text, opts) {
        let finalReply = '';
        let lastEdit = null;
        let mid = 0;
        const ctx = {
            sendChatAction: async () => { },
            reply: async (m) => {
                finalReply = m;
                return { message_id: ++mid };
            },
            sendVoice: async (_src, options) => {
                finalReply = options?.caption || finalReply;
                return { message_id: ++mid };
            },
            chat: { id: userId },
            telegram: {
                editMessageText: async (_chat, _msg, _inline, edited) => {
                    lastEdit = edited;
                    return true;
                }
            }
        };
        // Bridge: every line containing one of the Gateway trace markers also
        // becomes an onReasoning event. We patch the trace array indirectly by
        // wrapping safeReply / editMessageText to detect when the final ACK
        // edit lands. For per-step events we rely on processInput's existing
        // trace.push() calls — to expose those we use a console.log shim only
        // for THIS call (not global) when a callback is provided.
        const onReasoning = opts?.onReasoning;
        if (typeof onReasoning === 'function') {
            const originalLog = console.log;
            const restore = () => { console.log = originalLog; };
            console.log = ((...args) => {
                originalLog(...args);
                const line = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
                if (/\[BRAIN\]|\[GATEWAY\]|\[GATEKEEPER\]|classify_intent|route_task|finalize_reply/.test(line)) {
                    try {
                        onReasoning(line);
                    }
                    catch { /* ignore */ }
                }
            });
            const src = opts?.source ?? 'bridge';
            const sysCtx = opts?.systemContext;
            try {
                await this.processInput(ctx, userId, text, src, sysCtx);
            }
            finally {
                restore();
            }
        }
        else {
            await this.processInput(ctx, userId, text, opts?.source ?? 'bridge', opts?.systemContext);
        }
        return lastEdit ?? finalReply;
    }
    /**
     * Test seam that captures the full ACK + final timeline for the fast-ACK
     * pattern. Used by the regression test to assert the placeholder reply
     * lands within the ACK budget even when downstream model processing is
     * slow. Returns:
     *   - finalText: the user-visible final answer (edited ACK or last reply)
     *   - events: ordered timeline (kind = 'reply' | 'edit' | 'voice')
     *   - ackMs: ms until the first reply landed (null if none was sent)
     *   - finalMs: ms until the last event landed
     */
    async processTextForTestDetailed(userId, text) {
        const startTs = Date.now();
        const events = [];
        let ackMs = null;
        let lastReplyText = '';
        let lastEditText = null;
        let messageIdCounter = 0;
        let lastReplyMessageId = 0;
        const testCtx = {
            sendChatAction: async (_action) => { },
            reply: async (message) => {
                const tsMs = Date.now() - startTs;
                events.push({ kind: 'reply', text: message, tsMs });
                if (ackMs === null)
                    ackMs = tsMs;
                lastReplyText = message;
                lastReplyMessageId = ++messageIdCounter;
                return { message_id: lastReplyMessageId };
            },
            sendVoice: async (_source, options) => {
                const caption = options?.caption || '[voice_sent]';
                const tsMs = Date.now() - startTs;
                events.push({ kind: 'voice', text: caption, tsMs });
                lastReplyText = caption;
                return { message_id: ++messageIdCounter };
            },
            chat: { id: userId },
            telegram: {
                editMessageText: async (_chatId, _messageId, _inlineId, edited) => {
                    const tsMs = Date.now() - startTs;
                    events.push({ kind: 'edit', text: edited, tsMs });
                    lastEditText = edited;
                    return true;
                }
            }
        };
        await this.processInput(testCtx, userId, text);
        const finalText = lastEditText ?? lastReplyText;
        const finalMs = events.length ? events[events.length - 1].tsMs : 0;
        return { finalText, events, ackMs, finalMs };
    }
    async processImageForTest(userId, imagePath, caption = 'Descreva esta imagem.') {
        (0, ToolsetGuard_1.assertToolsetAllowed)('vision', 'Gateway.processImageForTest');
        const multimodalInput = `${caption}\n[LOCAL_ATTACHMENT: ${imagePath}]`;
        return this.processTextForTest(userId, multimodalInput);
    }
    isAuthorizedUser(userId, source) {
        if (source === 'cli')
            return true;
        if (!this.allowedTelegramUserId)
            return true;
        return userId === this.allowedTelegramUserId;
    }
    shouldRespondByVoice(text) {
        const normalized = text.toLowerCase().trim();
        if (/^\/voice\b|^\/voz\b/.test(normalized))
            return true;
        return /\b(responda|responder|manda|mande|envia|envie|fale|fala)\b[\s\S]{0,80}\b(por|em)\s+(voz|áudio|audio)\b/i.test(normalized)
            || /\bquero\s+(ouvir|áudio|audio|voz)\b/i.test(normalized);
    }
    stripVoiceDirective(text) {
        let cleaned = text.replace(/^\/(voice|voz)\s*/i, '').trim();
        cleaned = cleaned
            .replace(/\b(responda|responder|manda|mande|envia|envie)\s+(por|em)\s+(voz|áudio|audio)\b[:,\s-]*/ig, '')
            .replace(/\b(por favor,?\s*)?(responda|responder)\s+(isso\s+)?(por|em)\s+(voz|áudio|audio)\b/ig, '')
            .trim();
        return cleaned || text;
    }
    voiceUnavailableMessage(err) {
        const detail = err instanceof Error ? err.message : String(err);
        const safeDetail = this.governance.redact(detail).slice(0, 500);
        return [
            'Não consegui gerar a voz neste momento, então deixei em texto.',
            `Motivo técnico: ${safeDetail}`,
            'Para ativar voz sob demanda, configure um provider TTS no Railway: OPENAI_API_KEY ou ELEVENLABS_API_KEY.'
        ].join('\n');
    }
    isMarkdownTableSeparator(line) {
        return /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
    }
    splitMarkdownTableRow(line) {
        let stripped = line.trim();
        if (stripped.startsWith('|'))
            stripped = stripped.slice(1);
        if (stripped.endsWith('|'))
            stripped = stripped.slice(0, -1);
        return stripped.split('|').map(cell => this.stripInlineMarkdown(cell.trim()));
    }
    renderMarkdownTableForTelegram(tableBlock) {
        if (tableBlock.length < 3)
            return tableBlock.join('\n');
        const headers = this.splitMarkdownTableRow(tableBlock[0]);
        if (headers.length < 2)
            return tableBlock.join('\n');
        const rows = [];
        for (const row of tableBlock.slice(2)) {
            const cells = this.splitMarkdownTableRow(row);
            if (!cells.length)
                continue;
            const heading = cells[0] || 'Item';
            const values = cells.slice(1);
            rows.push(`• ${heading}`);
            for (let i = 1; i < headers.length; i += 1) {
                const label = headers[i];
                const value = values[i - 1] || '';
                if (label && value)
                    rows.push(`  ${label}: ${value}`);
            }
        }
        return rows.join('\n');
    }
    rewriteMarkdownTablesForTelegram(text) {
        if (!text.includes('|'))
            return text;
        const lines = text.split('\n');
        const output = [];
        let i = 0;
        let inFence = false;
        while (i < lines.length) {
            const line = lines[i];
            if (line.trimStart().startsWith('```')) {
                inFence = !inFence;
                output.push(line);
                i += 1;
                continue;
            }
            if (!inFence && line.includes('|') && i + 1 < lines.length && this.isMarkdownTableSeparator(lines[i + 1])) {
                const tableBlock = [line, lines[i + 1]];
                let j = i + 2;
                while (j < lines.length && lines[j].trim() !== '' && lines[j].includes('|')) {
                    tableBlock.push(lines[j]);
                    j += 1;
                }
                output.push(this.renderMarkdownTableForTelegram(tableBlock));
                i = j;
                continue;
            }
            output.push(line);
            i += 1;
        }
        return output.join('\n');
    }
    stripInlineMarkdown(text) {
        return text
            .replace(/`([^`]+)`/g, '$1')
            .replace(/\*\*([^*]+)\*\*/g, '$1')
            .replace(/__([^_]+)__/g, '$1')
            .replace(/~~([^~]+)~~/g, '$1')
            .replace(/\*([^*\n]+)\*/g, '$1')
            .replace(/(?<!\w)_([^_\n]+)_(?!\w)/g, '$1');
    }
    formatForTelegramPlainText(text) {
        const withoutTables = this.rewriteMarkdownTablesForTelegram(text);
        return withoutTables
            .split('\n')
            .map(line => line
            .replace(/^\s*#{1,6}\s+/, '')
            .replace(/^\s*>\s?/, ''))
            .filter(line => !/^\s*---+\s*$/.test(line))
            .map(line => this.stripInlineMarkdown(line))
            .join('\n')
            .replace(/\n{3,}/g, '\n\n')
            .trim();
    }
    async processInput(ctx, userId, text, source = 'telegram', systemContext) {
        // eslint-disable-next-line no-param-reassign
        if (systemContext)
            text = `${systemContext}\n\n${text}`;
        if (!this.isAuthorizedUser(userId, source)) {
            console.warn(`[GATEWAY] Acesso negado para userId=${userId}`);
            await this.safeReply(ctx, 'Acesso negado por policy de segurança deste bot.');
            return;
        }
        // v1.11.0 — Telegram `/core ...` family: route to CoreRemoteCommand
        // BEFORE classifier so it never gets misclassified as a regular intent.
        // The authorization check above already enforces the single-user
        // allowlist; this branch inherits it.
        if (text.trim().startsWith('/core')) {
            try {
                const { CoreRemoteCommand } = require('./CoreRemoteCommand');
                const result = await CoreRemoteCommand.handle(text);
                this.emitBridgeEvent('telegram', { kind: 'core_remote', userId, verb: text.split(/\s+/)[1] || 'status', synchronous: result.synchronous });
                await this.safeReply(ctx, result.reply);
            }
            catch (err) {
                console.error('[GATEWAY] /core router falhou:', err);
                await this.safeReply(ctx, `Erro no /core remote: ${err instanceof Error ? err.message : String(err)}`);
            }
            return;
        }
        const wantsVoiceReply = this.shouldRespondByVoice(text);
        const effectiveText = wantsVoiceReply ? this.stripVoiceDirective(text) : text;
        const dreamGoalStore = new DreamGoalStore_1.DreamGoalStore();
        const dreamGoalResponse = dreamGoalStore.handleSlashCommand(effectiveText);
        if (dreamGoalResponse) {
            await this.safeReply(ctx, dreamGoalResponse);
            return;
        }
        // Fast ACK pattern: fire a placeholder reply in parallel with the actual
        // classifier+gatekeeper work. When the work completes, edit the ACK with
        // the real answer (Telegraf editMessageText). Falls back to a fresh
        // reply when the surface does not expose `chat`/`telegram` (webhook mock
        // and similar internal callers).
        const ackText = '🔎 Recebido — processando…';
        let ackMessageId = null;
        const ackPromise = (async () => {
            try {
                const result = await ctx.reply(ackText);
                if (result && typeof result === 'object' && 'message_id' in result) {
                    const mid = result.message_id;
                    if (typeof mid === 'number')
                        ackMessageId = mid;
                }
            }
            catch (ackErr) {
                console.error('[GATEWAY] Falha ao enviar ACK rápido:', ackErr);
            }
        })();
        const sendFinal = async (finalText) => {
            await ackPromise;
            if (ackMessageId !== null &&
                ctx.chat &&
                ctx.telegram &&
                typeof ctx.telegram.editMessageText === 'function') {
                try {
                    await ctx.telegram.editMessageText(ctx.chat.id, ackMessageId, undefined, finalText);
                    return;
                }
                catch (editErr) {
                    console.error('[GATEWAY] editMessageText falhou, enviando nova mensagem:', editErr);
                }
            }
            await this.safeReply(ctx, finalText);
        };
        const stopTyping = this.startTypingIndicator(ctx);
        const trace = [];
        try {
            trace.push(`🔎 classify_intent: "${effectiveText.slice(0, 80).replace(/\n/g, ' ')}${effectiveText.length > 80 ? '...' : ''}"`);
            const task = await this.classifier.classify(effectiveText);
            trace.push(`🧭 route_task: "${String(task.intent)}"`);
            const response = await this.gatekeeper.routeTask(task, effectiveText, userId);
            trace.push(`✅ finalize_reply: "${String(task.intent)}"`);
            const safeResponse = this.formatForTelegramPlainText(this.governance.redact(response));
            const finalResponse = this.reasoningMode === 'always' ? `${trace.join('\n')}\n\n${safeResponse}` : safeResponse;
            if ((this.ttsEnabled || wantsVoiceReply) && typeof ctx.sendVoice === 'function') {
                try {
                    const audioPath = await this.voiceManager.generateSpeech(finalResponse);
                    await ctx.sendVoice({ source: audioPath }, { caption: finalResponse });
                }
                catch (ttsError) {
                    console.log("[GATEWAY] Fallback para texto (Erro no TTS).", ttsError);
                    await sendFinal(wantsVoiceReply ? `${finalResponse}\n\n${this.voiceUnavailableMessage(ttsError)}` : finalResponse);
                }
            }
            else {
                await sendFinal(finalResponse);
            }
        }
        catch (err) {
            console.error("[GATEWAY] Erro ao orquestrar a tarefa:", err);
            const errMsg = this.reasoningMode !== 'off'
                ? `${trace.join('\n')}\n\n❌ error: "${err instanceof Error ? err.message : String(err)}"\n\nErro no córtex ao processar a solicitação.`
                : "Erro no córtex ao processar a solicitação.";
            await sendFinal(errMsg);
        }
        finally {
            stopTyping();
        }
    }
    startTypingIndicator(ctx) {
        let stopped = false;
        const sendTyping = async () => {
            if (stopped)
                return;
            try {
                await ctx.sendChatAction('typing');
            }
            catch (err) {
                console.error('[GATEWAY] Falha ao enviar indicador de digitação:', err);
            }
        };
        void sendTyping();
        const timer = setInterval(() => { void sendTyping(); }, 4000);
        return () => {
            stopped = true;
            clearInterval(timer);
        };
    }
    async safeReply(ctx, text) {
        try {
            await ctx.reply(text);
        }
        catch (replyErr) {
            console.error('[GATEWAY] Falha ao responder no Telegram:', replyErr);
        }
    }
}
exports.Gateway = Gateway;
