// ============================================
// AI SUPPORT — Binding Troubleshooter + Universal AI Chat
// Two-tier diagnosis: rule engine (free) + Claude CLI proxy (Max subscription)
// Universal chat: Sonnet with repo access for users & admins
// ============================================
const express = require('express');
const safeEqual = require('./safe-equal');

// Lazy-load Anthropic client (only when ANTHROPIC_API_KEY is set)
let _anthropicClient = null;
function getAnthropicClient() {
    if (!_anthropicClient && process.env.ANTHROPIC_API_KEY) {
        _anthropicClient = require('./anthropic-client');
    }
    return _anthropicClient;
}

module.exports = function (devices, chatPool, { serverLog, getWebhookFixInstructions, feedbackModule }) {
    const router = express.Router();

    // ── Rate Limiter ────────────────────────────
    const supportRateLimit = {}; // key: deviceId -> { count, windowStart }
    const SUPPORT_MAX_PER_HOUR = 5;
    const SUPPORT_WINDOW_MS = 3600000; // 1 hour

    function checkRateLimit(deviceId) {
        const now = Date.now();
        const entry = supportRateLimit[deviceId];
        if (!entry || now - entry.windowStart > SUPPORT_WINDOW_MS) {
            supportRateLimit[deviceId] = { count: 1, windowStart: now };
            return { allowed: true };
        }
        if (entry.count >= SUPPORT_MAX_PER_HOUR) {
            const retryAfterMs = SUPPORT_WINDOW_MS - (now - entry.windowStart);
            return { allowed: false, retryAfterMs };
        }
        entry.count++;
        return { allowed: true };
    }

    // ── Chat Rate Limiter ──────────────────────
    const chatRateLimit = {}; // key: userId -> { count, windowStart }
    const CHAT_RATE_USER = 20;
    const CHAT_RATE_ADMIN = 100;

    function checkChatRateLimit(userId, isAdmin) {
        const max = isAdmin ? CHAT_RATE_ADMIN : CHAT_RATE_USER;
        const now = Date.now();
        const entry = chatRateLimit[userId];
        if (!entry || now - entry.windowStart > SUPPORT_WINDOW_MS) {
            chatRateLimit[userId] = { count: 1, windowStart: now };
            return { allowed: true, remaining: max - 1 };
        }
        if (entry.count >= max) {
            return { allowed: false, retryAfterMs: SUPPORT_WINDOW_MS - (now - entry.windowStart), remaining: 0 };
        }
        entry.count++;
        return { allowed: true, remaining: max - entry.count };
    }

    // Admin check — 5 routes below need this. Was duplicated as silent
    // `try{...} catch (_) {}` blocks which hid schema drift on user_accounts
    // (e.g. is_admin column rename would lock every admin out invisibly).
    async function resolveIsAdmin(userId) {
        if (!userId) return false;
        try {
            const r = await chatPool.query('SELECT is_admin FROM user_accounts WHERE id = $1', [userId]);
            return r.rows[0]?.is_admin || false;
        } catch (err) {
            console.warn('[AI-Support] resolveIsAdmin failed for', userId, ':', err.message);
            return false;
        }
    }

    // Clean up stale rate limit entries every 30 minutes
    setInterval(() => {
        const now = Date.now();
        for (const key of Object.keys(supportRateLimit)) {
            if (now - supportRateLimit[key].windowStart > SUPPORT_WINDOW_MS * 2) {
                delete supportRateLimit[key];
            }
        }
        for (const key of Object.keys(chatRateLimit)) {
            if (now - chatRateLimit[key].windowStart > SUPPORT_WINDOW_MS * 2) {
                delete chatRateLimit[key];
            }
        }
    }, 1800000);

    // ── claude-cli-proxy per-request caller auth (H3 multi-tenant) ──
    // Returns the extra ChatRequest fields the proxy uses to fetch THIS
    // tenant's GitHub PAT from their own vault instead of falling through
    // to the proxy's legacy EVAULT_LEGACY_BOOT_CLONE env-token path.
    // Returns {} when the deployment hasn't configured caller creds, so the
    // proxy keeps its legacy env-token behavior until the env vars land.
    function proxyCallerAuthFields() {
        const callerDeviceId = process.env.CLAUDE_PROXY_CALLER_DEVICE_ID || '';
        const callerBotSecret = process.env.CLAUDE_PROXY_CALLER_BOT_SECRET || '';
        if (!callerDeviceId || !callerBotSecret) return {};
        const fields = {
            caller_device_id: callerDeviceId,
            caller_bot_secret: callerBotSecret,
        };
        if (process.env.CLAUDE_PROXY_REPO_HOST_PATH) fields.repo_host_path = process.env.CLAUDE_PROXY_REPO_HOST_PATH;
        if (process.env.CLAUDE_PROXY_VAULT_KEY) fields.vault_key = process.env.CLAUDE_PROXY_VAULT_KEY;
        if (process.env.CLAUDE_PROXY_ALLOWED_ORGS) fields.allowed_orgs = process.env.CLAUDE_PROXY_ALLOWED_ORGS;
        return fields;
    }

    // ── Auto-create GitHub issue (for AI-initiated actions) ──
    async function autoCreateIssue(action, user) {
        const token = process.env.GITHUB_TOKEN;
        const repo = process.env.GITHUB_REPO || 'HankHuang0516/EClaw';
        if (!token) return null;

        try {
            const userInfo = user.email ? `User: ${user.email}` : `Device: ${(user.deviceId || '').slice(0, 8)}...`;
            const deviceState = feedbackModule ? feedbackModule.captureDeviceState(devices, user.deviceId) : null;
            const appVersion = deviceState?.appVersion || devices[user.deviceId]?.appVersion || null;
            const platformLabel = devices[user.deviceId]?.lastPlatform || 'unknown';
            const versionSuffix = appVersion ? ` (Platform: ${platformLabel}, App v${appVersion})` : '';
            const body = (action.body || action.title)
                + `\n\n---\n_Reported by ${userInfo} via E-Claw AI Assistant${versionSuffix}_`;

            const ghRes = await fetch(`https://api.github.com/repos/${repo}/issues`, {
                method: 'POST',
                headers: {
                    'Authorization': `token ${token}`,
                    'Accept': 'application/vnd.github.v3+json',
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    title: action.title,
                    body,
                    labels: Array.isArray(action.labels) ? action.labels : ['ai-assistant']
                })
            });

            if (!ghRes.ok) {
                console.error(`[AI Chat] Auto-issue failed (${ghRes.status})`);
                return null;
            }

            const data = await ghRes.json();
            serverLog('info', 'ai_chat', `Auto-created GitHub issue #${data.number} for user`, { userId: user.userId });
            return { url: data.html_url, number: data.number };
        } catch (err) {
            console.error('[AI Chat] Auto-issue error:', err.message);
            return null;
        }
    }

    // ── Close GitHub issue (for AI-initiated actions) ──
    async function closeIssue(issueNumber, comment) {
        const token = process.env.GITHUB_TOKEN;
        const repo = process.env.GITHUB_REPO || 'HankHuang0516/EClaw';
        if (!token) return { success: false, error: 'GITHUB_TOKEN not configured' };

        try {
            // Add comment if provided
            if (comment) {
                await fetch(`https://api.github.com/repos/${repo}/issues/${issueNumber}/comments`, {
                    method: 'POST',
                    headers: {
                        'Authorization': `token ${token}`,
                        'Accept': 'application/vnd.github.v3+json',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ body: comment })
                });
            }

            // Close the issue
            const ghRes = await fetch(`https://api.github.com/repos/${repo}/issues/${issueNumber}`, {
                method: 'PATCH',
                headers: {
                    'Authorization': `token ${token}`,
                    'Accept': 'application/vnd.github.v3+json',
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ state: 'closed' })
            });

            if (!ghRes.ok) {
                const errBody = await ghRes.text();
                console.error(`[AI Chat] Close issue #${issueNumber} failed (${ghRes.status}):`, errBody);
                return { success: false, error: `GitHub API error (${ghRes.status})` };
            }

            const data = await ghRes.json();
            serverLog('info', 'ai_chat', `Closed GitHub issue #${issueNumber}`, { issueNumber });
            return { success: true, url: data.html_url, number: data.number };
        } catch (err) {
            console.error('[AI Chat] Close issue error:', err.message);
            return { success: false, error: err.message };
        }
    }

    // ── Execute GitHub actions from AI response ──
    async function executeGitHubActions(actions, user) {
        const results = [];
        if (!actions || !Array.isArray(actions) || actions.length === 0) return results;

        for (const action of actions) {
            if (action.type === 'create_issue' && action.title) {
                const issueResult = await autoCreateIssue(action, user);
                if (issueResult) {
                    const feedback = await createFeedbackFromAiIssue(issueResult, action, user);
                    results.push({
                        type: 'create_issue',
                        text: `\n\n---\n📋 GitHub issue [#${issueResult.number}](${issueResult.url}) created` +
                            (feedback ? `\n📝 Feedback #${feedback.feedbackId} recorded` : ''),
                        feedbackId: feedback ? feedback.feedbackId : null
                    });
                }
            } else if (action.type === 'close_issue' && action.issue_number) {
                const closeResult = await closeIssue(action.issue_number, action.comment);
                if (closeResult.success) {
                    results.push({
                        type: 'close_issue',
                        text: `\n\n---\n✅ GitHub issue [#${action.issue_number}](${closeResult.url}) closed`
                    });
                } else {
                    results.push({
                        type: 'close_issue',
                        text: `\n\n---\n❌ Failed to close issue #${action.issue_number}: ${closeResult.error}`
                    });
                }
            }
        }
        return results;
    }

    // ── Dual-track: create feedback record alongside GitHub issue ──
    async function createFeedbackFromAiIssue(issueResult, action, user) {
        if (!feedbackModule || !issueResult) return null;
        const deviceId = user.deviceId;
        if (!deviceId) return null;

        try {
            const device = devices[deviceId];
            const deviceSecret = device ? device.deviceSecret : null;
            const logSnapshot = await feedbackModule.captureLogSnapshot(chatPool, deviceId);
            const deviceState = feedbackModule.captureDeviceState(devices, deviceId);

            const saved = await feedbackModule.saveFeedback(chatPool, {
                deviceId,
                deviceSecret,
                message: action.body || action.title,
                category: 'bug',
                appVersion: deviceState?.appVersion || device?.appVersion || '',
                source: 'ai_chat',
                logSnapshot,
                deviceState,
                markTs: null
            });

            if (saved) {
                await feedbackModule.updateFeedback(chatPool, saved.id, {
                    github_issue_url: issueResult.url
                });
                serverLog('info', 'ai_chat', `Feedback #${saved.id} created from AI issue #${issueResult.number}`, {
                    userId: user.userId, deviceId
                });
                return { feedbackId: saved.id, severity: saved.severity, tags: saved.tags };
            }
        } catch (err) {
            console.error('[AI Chat] createFeedbackFromAiIssue error:', err.message);
        }
        return null;
    }

    // ── Rule Patterns ───────────────────────────
    const RULE_PATTERNS = [
        {
            id: 'expired_code',
            regex: /invalid or expired binding code/i,
            diagnosis: 'The binding code has expired (codes are valid for 5 minutes) or was already used.',
            getSteps: () => [
                'Ask the device owner to open the E-Claw app and generate a new binding code.',
                'The new code will also be valid for 5 minutes — bind quickly after receiving it.',
                'Retry POST /api/bind with the new code.'
            ]
        },
        {
            id: 'device_not_found',
            regex: /device not found/i,
            diagnosis: 'The device ID is not registered on the server. The device may have been reset or the server restarted.',
            getSteps: () => [
                'Ask the device owner to open the E-Claw app (it auto-registers on startup).',
                'Then request a new binding code and retry POST /api/bind.'
            ]
        },
        {
            id: 'localhost_rejected',
            regex: /localhost|127\.0\.0\.1|0\.0\.0\.0|private.*address|local.*private/i,
            diagnosis: 'Your webhook URL points to a local/private IP address, which is unreachable from the internet.',
            getSteps: () => {
                const fix = getWebhookFixInstructions('localhost_rejected');
                return [fix];
            }
        },
        {
            id: 'http_401',
            regex: /401|unauthorized|credentials.*rejected|gateway rejected/i,
            diagnosis: 'Your gateway rejected the authentication credentials (HTTP 401). ' +
                'Most common cause: your gateway has SETUP_PASSWORD enabled (e.g. Railway deployment) but you did not include setup_username/setup_password in the registration.',
            getSteps: () => [
                'Step 1: Check if SETUP_PASSWORD is enabled:\n  exec: echo $SETUP_PASSWORD',
                'Step 2a: If Step 1 returned a non-empty value, retry POST /api/bot/register with:\n' +
                '  "setup_username": "admin",\n' +
                '  "setup_password": "<the value from Step 1>"',
                'Step 2b: If Step 1 was empty, re-read and verify your gateway token:\n' +
                '  exec: echo $OPENCLAW_GATEWAY_TOKEN\n' +
                '  Then retry POST /api/bot/register with the correct token.',
                'Step 3: If both values changed after a restart, re-read both and retry.'
            ]
        },
        {
            id: 'setup_password_required',
            regex: /setup_password|SETUP_PASSWORD.*enabled|setup.?password.*required/i,
            diagnosis: 'Your gateway has SETUP_PASSWORD enabled (common on Railway). HTTP requests are blocked by Basic Auth. ' +
                'You must include setup_username and setup_password so the server can use WebSocket authentication instead.',
            getSteps: () => [
                'Step 1: Read your SETUP_PASSWORD value:\n  exec: echo $SETUP_PASSWORD',
                'Step 2: Retry POST /api/bot/register with ALL your previous parameters PLUS these two new fields:\n' +
                '  "setup_username": "admin",\n' +
                '  "setup_password": "<the exact value from Step 1>"\n' +
                '  IMPORTANT: Use the EXACT output from echo — do NOT use a placeholder or variable name.',
                'Step 3: If it still fails with 401, also re-read your gateway token:\n  exec: echo $OPENCLAW_GATEWAY_TOKEN\n' +
                '  Then retry with the fresh token value.'
            ]
        },
        {
            id: 'tool_not_available',
            regex: /404|sessions_send|not available|cannot execute|tool.*blocked/i,
            diagnosis: 'The sessions_send tool is not allowed on your gateway. It needs to be explicitly enabled.',
            getSteps: () => {
                const fix = getWebhookFixInstructions('tool_not_available');
                return [fix];
            }
        },
        {
            id: 'pairing_required',
            regex: /pairing required|gateway.*closed|gateway.*disconnected|1008/i,
            diagnosis: 'Your gateway is disconnected or requires device re-pairing.',
            getSteps: () => {
                const fix = getWebhookFixInstructions('pairing_required');
                return [fix];
            }
        },
        {
            id: 'connection_failed',
            regex: /ECONNREFUSED|ENOTFOUND|DNS|cannot reach|network|connection.*failed/i,
            diagnosis: 'Cannot reach your webhook URL. There may be a DNS, network, or URL issue.',
            getSteps: () => [
                'Step 1: Verify your webhook URL is correct and publicly accessible.',
                'Step 2: Check if your bot platform (Zeabur/Railway) is running:\n  exec: echo $ZEABUR_WEB_URL',
                'Step 3: Ensure the URL uses HTTPS (not HTTP) and ends with /tools/invoke.',
                'Step 4: Retry POST /api/bot/register.'
            ]
        },
        {
            id: 'placeholder_token',
            regex: /placeholder|TOKEN_HERE|your[_-].*token|\[.*token.*\]|\{.*token.*\}/i,
            diagnosis: 'The token you provided appears to be a placeholder, not an actual gateway token.',
            getSteps: () => [
                'Step 1: Read the actual gateway token:\n  exec: echo $OPENCLAW_GATEWAY_TOKEN',
                'Step 2: If the above is empty, try:\n  exec: openclaw config get gateway.token',
                'Step 3: Use the real token value in POST /api/bot/register.'
            ]
        },
        {
            id: 'name_too_long',
            regex: /name.*20|characters or less|name.*too.*long/i,
            diagnosis: 'The entity name exceeds the 20-character limit.',
            getSteps: () => [
                'Shorten the name to 20 characters or fewer and retry POST /api/bind.'
            ]
        }
    ];

    // ── Stage A: Pattern Matching ───────────────
    function matchRulePatterns(errorMessages) {
        for (const msg of errorMessages) {
            for (const rule of RULE_PATTERNS) {
                if (rule.regex.test(msg)) {
                    return {
                        matched: true,
                        rule: rule.id,
                        diagnosis: rule.diagnosis,
                        suggested_steps: rule.getSteps()
                    };
                }
            }
        }
        return { matched: false };
    }

    // ── Stage B: Log Correlation ────────────────
    async function correlateWithLogs(deviceId) {
        try {
            // Check recent handshake failures
            const failuresResult = await chatPool.query(
                `SELECT error_type, error_message, webhook_url, source, created_at
                 FROM handshake_failures
                 WHERE device_id = $1 AND created_at > NOW() - INTERVAL '30 minutes'
                 ORDER BY created_at DESC LIMIT 10`,
                [deviceId]
            );

            if (failuresResult.rows.length > 0) {
                const latest = failuresResult.rows[0];
                const fix = getWebhookFixInstructions(latest.error_type);
                return {
                    matched: true,
                    rule: `log_correlation:${latest.error_type}`,
                    diagnosis: `Server logs show a recent "${latest.error_type}" failure: ${latest.error_message || 'no details'}`,
                    suggested_steps: [fix]
                };
            }

            // Check recent server logs for binding issues
            const logsResult = await chatPool.query(
                `SELECT level, category, message, metadata, created_at
                 FROM server_logs
                 WHERE device_id = $1 AND category IN ('bind', 'unbind', 'client_push')
                   AND created_at > NOW() - INTERVAL '30 minutes'
                 ORDER BY created_at DESC LIMIT 20`,
                [deviceId]
            );

            // Look for patterns in recent logs
            for (const log of logsResult.rows) {
                if (log.category === 'unbind') {
                    return {
                        matched: true,
                        rule: 'log_correlation:recent_unbind',
                        diagnosis: `This entity was recently unbound (${log.message}). You need to re-bind before registering a webhook.`,
                        suggested_steps: [
                            'Step 1: Request a new binding code from the device owner.',
                            'Step 2: Call POST /api/bind with the new code.',
                            'Step 3: Then call POST /api/bot/register to enable push.'
                        ]
                    };
                }
                if (log.category === 'client_push' && log.level === 'warn') {
                    // Push failure logs often contain the reason
                    const msg = log.message || '';
                    if (/pairing/i.test(msg)) {
                        const fix = getWebhookFixInstructions('pairing_required');
                        return {
                            matched: true,
                            rule: 'log_correlation:push_pairing',
                            diagnosis: 'Recent push delivery failed because the gateway requires re-pairing.',
                            suggested_steps: [fix]
                        };
                    }
                }
            }
        } catch (err) {
            // DB query failed — degrade gracefully
            console.error('[AI Support] Log correlation error:', err.message);
        }

        return { matched: false };
    }

    // ── Proxy Forwarding (Anthropic direct or CLI proxy) ──
    async function forwardToProxy(deviceId, entityId, problemDescription, errorMessages, recentLogs, recentFailures, opts = {}, recentCrashes = []) {
        // Priority: Anthropic direct API > CLI proxy > fallback
        const anthropic = getAnthropicClient();
        if (anthropic) {
            if (process.env.DEBUG === 'true') console.log('[AI Support] Using Anthropic direct API for analysis');
            serverLog('info', 'ai_support', '[AI Support] Using Anthropic direct API for analysis', { deviceId, entityId });
            const startTime = Date.now();
            try {
                const result = await anthropic.analyzeWithClaude({
                    problemDescription,
                    errorMessages,
                    logs: recentLogs,
                    handshakeFailures: recentFailures,
                    appCrashes: recentCrashes,
                    deviceContext: { deviceId, entityId, timestamp: new Date().toISOString(), role: opts.role || 'bot' }
                });
                const latencyMs = Date.now() - startTime;
                if (process.env.DEBUG === 'true') console.log(`[AI Support] Anthropic analysis done (${latencyMs}ms, confidence: ${result.confidence})`);
                serverLog('info', 'ai_support', `[AI Support] Anthropic analysis done (${latencyMs}ms, confidence: ${result.confidence})`, { deviceId, entityId });
                return {
                    success: true,
                    source: 'anthropic_direct',
                    diagnosis: result.diagnosis,
                    suggested_steps: result.suggested_steps,
                    confidence: result.confidence,
                    latency_ms: latencyMs
                };
            } catch (err) {
                console.error(`[AI Support] Anthropic analysis error: ${err.message}`);
                // Fall through to proxy or fallback
            }
        }

        const proxyUrl = process.env.CLAUDE_CLI_PROXY_URL;
        const proxyKey = process.env.SUPPORT_API_KEY;

        if (!proxyUrl || !proxyKey) {
            const missing = [];
            if (!process.env.ANTHROPIC_API_KEY) missing.push('ANTHROPIC_API_KEY');
            if (!proxyUrl) missing.push('CLAUDE_CLI_PROXY_URL');
            if (!proxyKey) missing.push('SUPPORT_API_KEY');
            console.warn(`[AI Support] No AI backend configured — missing env: ${missing.join(', ')}`);
            return {
                success: true,
                source: 'fallback',
                diagnosis: 'Advanced AI analysis is not available at this time. Please try the standard troubleshooting steps.',
                suggested_steps: [
                    'Check your webhook URL is reachable from the internet.',
                    'Verify your gateway token with: exec: echo $OPENCLAW_GATEWAY_TOKEN',
                    'If SETUP_PASSWORD is enabled, include setup_username and setup_password.',
                    'Retry POST /api/bot/register with openclaw_version included.'
                ],
                confidence: 0,
                debug: { reason: 'no_ai_backend', missing_env: missing }
            };
        }

        const targetUrl = proxyUrl + '/analyze';
        if (process.env.DEBUG === 'true') console.log(`[AI Support] Forwarding to proxy: ${targetUrl}`);
        serverLog('info', 'ai_support', `[AI Support] Forwarding to proxy: ${targetUrl}`, { deviceId, entityId });

        const startTime = Date.now();
        try {
            const response = await fetch(targetUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${proxyKey}`
                },
                body: JSON.stringify({
                    problem_description: problemDescription,
                    error_messages: errorMessages,
                    logs: (recentLogs || []).slice(0, 30),
                    handshake_failures: (recentFailures || []).slice(0, 10),
                    app_crashes: (recentCrashes || []).slice(0, 10),
                    device_context: { deviceId, entityId, timestamp: new Date().toISOString(), role: opts.role || 'bot' }
                }),
                signal: AbortSignal.timeout(65000)
            });

            const latencyMs = Date.now() - startTime;
            if (process.env.DEBUG === 'true') console.log(`[AI Support] Proxy responded: HTTP ${response.status} (${latencyMs}ms)`);
            serverLog('info', 'ai_support', `[AI Support] Proxy responded: HTTP ${response.status} (${latencyMs}ms)`, { deviceId, entityId });

            if (!response.ok) {
                const body = await response.text().catch(() => '(unreadable)');
                console.error(`[AI Support] Proxy error body: ${body.slice(0, 500)}`);
                throw new Error(`Proxy returned HTTP ${response.status}: ${body.slice(0, 200)}`);
            }

            const result = await response.json();
            return {
                success: true,
                source: 'claude_proxy',
                diagnosis: result.diagnosis || 'Analysis complete.',
                suggested_steps: result.suggested_steps || [],
                confidence: result.confidence || 0.5,
                latency_ms: latencyMs
            };
        } catch (err) {
            const latencyMs = Date.now() - startTime;
            const errDetail = err.cause ? `${err.message} (cause: ${err.cause.code || err.cause.message})` : err.message;
            console.error(`[AI Support] Proxy error (${latencyMs}ms): ${errDetail}`);

            return {
                success: true,
                source: 'fallback',
                diagnosis: 'AI analysis timed out or is unavailable. Please try the standard troubleshooting steps.',
                suggested_steps: [
                    'Check your webhook URL is reachable from the internet.',
                    'Verify your gateway token with: exec: echo $OPENCLAW_GATEWAY_TOKEN',
                    'If SETUP_PASSWORD is enabled, include setup_username and setup_password.',
                    'Retry POST /api/bot/register with openclaw_version included.'
                ],
                confidence: 0,
                debug: { reason: 'proxy_error', error: errDetail, latency_ms: latencyMs, target_url: targetUrl }
            };
        }
    }

    // ── Fetch Recent Data for Proxy ─────────────
    async function fetchRecentData(deviceId) {
        let recentLogs = [];
        let recentFailures = [];
        let recentCrashes = [];

        try {
            const logsResult = await chatPool.query(
                `SELECT level, category, message, entity_id, metadata, created_at
                 FROM server_logs
                 WHERE device_id = $1 AND category IN ('bind', 'unbind', 'client_push', 'transform')
                   AND created_at > NOW() - INTERVAL '1 hour'
                 ORDER BY created_at DESC LIMIT 30`,
                [deviceId]
            );
            recentLogs = logsResult.rows;
        } catch (err) { /* ignore */ }

        try {
            const failResult = await chatPool.query(
                `SELECT error_type, error_message, webhook_url, http_status, source, created_at
                 FROM handshake_failures
                 WHERE device_id = $1 AND created_at > NOW() - INTERVAL '1 hour'
                 ORDER BY created_at DESC LIMIT 10`,
                [deviceId]
            );
            recentFailures = failResult.rows;
        } catch (err) { /* ignore */ }

        try {
            const crashResult = await chatPool.query(
                `SELECT ts, action, meta, created_at
                 FROM device_telemetry
                 WHERE device_id = $1 AND type IN ('crash', 'error')
                   AND created_at > NOW() - INTERVAL '24 hours'
                 ORDER BY ts DESC LIMIT 20`,
                [deviceId]
            );
            recentCrashes = crashResult.rows;
        } catch (err) { /* ignore */ }

        return { recentLogs, recentFailures, recentCrashes };
    }

    // ── Fetch Full Device Diagnostics for AI Chat ──
    // Collects errors (24h), key activity (1h), entity states, platform/version info.
    // Used by both /chat and processRequest to give Claude full device context.
    async function fetchDeviceContext(deviceId, device) {
        const ctx = {
            platform: device?.lastPlatform || null,
            appVersion: device?.appVersion || null,
            entityStates: [],
            recentErrors: [],
            recentActivity: [],
            handshakeFailures: []
        };

        // Entity states from in-memory device object.
        // device.entities can be an Array or a plain object {0:{...},1:{...}} —
        // iterate over fixed slots 0-3 to handle both shapes safely.
        if (device?.entities) {
            const ents = device.entities;
            ctx.entityStates = [0, 1, 2, 3].map(i => {
                const e = Array.isArray(ents) ? ents[i] : ents[i];
                return {
                    slot: i,
                    type: i % 2 === 0 ? 'LOBSTER' : 'PIG',
                    bound: e?.isBound || false,
                    name: e?.name || null,
                    hasWebhook: !!(e?.webhookUrl)
                };
            });
        }

        // Recent errors — all categories, last 24h
        try {
            const r = await chatPool.query(
                `SELECT level, category, message, entity_id, metadata, created_at
                 FROM server_logs
                 WHERE device_id = $1 AND level = 'error'
                   AND created_at > NOW() - INTERVAL '24 hours'
                 ORDER BY created_at DESC LIMIT 20`,
                [deviceId]
            );
            ctx.recentErrors = r.rows;
        } catch (err) {
            console.warn('[AI-Support] buildDeviceContext.recentErrors failed:', err.message);
        }

        // Recent important activity — bind/unbind/push/transform/broadcast, last 1h
        try {
            const r = await chatPool.query(
                `SELECT level, category, message, entity_id, metadata, created_at
                 FROM server_logs
                 WHERE device_id = $1
                   AND category IN ('bind', 'unbind', 'broadcast', 'broadcast_push', 'speakto_push', 'transform', 'client_push', 'push_error')
                   AND created_at > NOW() - INTERVAL '1 hour'
                 ORDER BY created_at DESC LIMIT 30`,
                [deviceId]
            );
            ctx.recentActivity = r.rows;
        } catch (err) {
            console.warn('[AI-Support] buildDeviceContext.recentActivity failed:', err.message);
        }

        // Handshake failures — last 1h
        try {
            const r = await chatPool.query(
                `SELECT error_type, error_message, webhook_url, http_status, source, created_at
                 FROM handshake_failures
                 WHERE device_id = $1 AND created_at > NOW() - INTERVAL '1 hour'
                 ORDER BY created_at DESC LIMIT 10`,
                [deviceId]
            );
            ctx.handshakeFailures = r.rows;
        } catch (err) {
            console.warn('[AI-Support] buildDeviceContext.handshakeFailures failed:', err.message);
        }

        return ctx;
    }

    // ── Customer Service Tool Handlers ────────────
    // These handlers are passed to chatWithClaude() so Claude can call them via tool_use.

    function buildToolHandlers() {
        return {
            async lookup_device({ device_id }) {
                const device = devices[device_id];
                if (!device) {
                    return { found: false, error: 'Device not found or not currently registered' };
                }

                const entities = [];
                const ents = device.entities || [];
                const entArray = Array.isArray(ents) ? ents : Object.values(ents);
                for (let i = 0; i < entArray.length; i++) {
                    const e = entArray[i];
                    if (!e) continue;
                    entities.push({
                        slot: i,
                        type: i % 2 === 0 ? 'LOBSTER' : 'PIG',
                        bound: e.isBound || false,
                        name: e.name || null,
                        hasWebhook: !!e.webhookUrl,
                        publicCode: e.publicCode || null,
                        encryptionStatus: e.encryptionStatus || null
                    });
                }

                return {
                    found: true,
                    deviceId: device_id,
                    deviceSecret: device.deviceSecret || null,
                    platform: device.lastPlatform || null,
                    appVersion: device.appVersion || null,
                    entityCount: entities.length,
                    entities
                };
            },

            async query_device_logs({ device_id, category, level, hours, limit: maxRows }) {
                const device = devices[device_id];
                if (!device) {
                    return { found: false, error: 'Device not found' };
                }

                const queryHours = Math.min(hours || 24, 72);
                const queryLimit = Math.min(maxRows || 30, 100);

                const conditions = ['device_id = $1', `created_at > NOW() - INTERVAL '${queryHours} hours'`];
                const params = [device_id];
                let paramIdx = 2;

                if (category) {
                    conditions.push(`category = $${paramIdx}`);
                    params.push(category);
                    paramIdx++;
                }
                if (level) {
                    conditions.push(`level = $${paramIdx}`);
                    params.push(level);
                    paramIdx++;
                }

                try {
                    const result = await chatPool.query(
                        `SELECT level, category, message, entity_id, metadata, created_at
                         FROM server_logs
                         WHERE ${conditions.join(' AND ')}
                         ORDER BY created_at DESC LIMIT ${queryLimit}`,
                        params
                    );

                    return {
                        found: true,
                        deviceId: device_id,
                        logCount: result.rows.length,
                        logs: result.rows.map(r => ({
                            time: new Date(r.created_at).toISOString(),
                            level: r.level,
                            category: r.category,
                            entityId: r.entity_id,
                            message: r.message,
                            metadata: r.metadata
                        }))
                    };
                } catch (err) {
                    return { found: false, error: `Database query failed: ${err.message}` };
                }
            },

            async lookup_user_by_email({ email }) {
                try {
                    const result = await chatPool.query(
                        `SELECT id, email, device_id, device_secret, is_admin, created_at
                         FROM user_accounts
                         WHERE LOWER(email) = LOWER($1)
                         LIMIT 1`,
                        [email]
                    );

                    if (result.rows.length === 0) {
                        return { found: false, error: 'No user account found with this email' };
                    }

                    const user = result.rows[0];
                    const deviceId = user.device_id;
                    const device = deviceId ? devices[deviceId] : null;

                    return {
                        found: true,
                        userId: user.id,
                        email: user.email,
                        isAdmin: user.is_admin || false,
                        createdAt: user.created_at,
                        deviceId: deviceId || null,
                        deviceSecret: user.device_secret || (device ? device.deviceSecret : null),
                        deviceOnline: !!device
                    };
                } catch (err) {
                    return { found: false, error: `Database query failed: ${err.message}` };
                }
            }
        };
    }

    const toolHandlers = buildToolHandlers();

    // ── Sanitize raw Claude CLI session JSON ─────
    // The proxy sometimes returns raw Claude CLI session output (e.g. {"type":"result","subtype":"error_max_turns",...})
    // instead of clean text. Detect and replace with user-friendly message.
    function sanitizeProxyResponse(responseText) {
        if (!responseText || typeof responseText !== 'string') {
            return responseText || 'Sorry, I could not generate a response. Please try again.';
        }

        const trimmed = responseText.trim();
        // Detect raw JSON session results
        if (trimmed.startsWith('{') && trimmed.includes('"type"')) {
            try {
                const parsed = JSON.parse(trimmed);
                if (parsed.type === 'result' || parsed.subtype || parsed.session_id) {
                    console.warn(`[AI Chat] Raw session JSON detected (subtype: ${parsed.subtype}), sanitizing`);
                    if (parsed.result_text) {
                        return parsed.result_text;
                    } else if (parsed.subtype === 'error_max_turns') {
                        return 'Sorry, this question was too complex for me to fully analyze. Could you try asking a more specific question?';
                    } else if (parsed.subtype === 'error_tool_execution') {
                        return 'I encountered an error while looking into your issue. Please try again.';
                    } else {
                        return 'Sorry, I was unable to process your request. Please try rephrasing your question.';
                    }
                }
            } catch (_) {
                // Not valid JSON, leave as-is
            }
        }

        return responseText;
    }

    // ── Log Query to DB ─────────────────────────
    function logSupportQuery(deviceId, entityId, result) {
        chatPool.query(
            `INSERT INTO ai_support_queries
             (device_id, entity_id, matched_rule, diagnosis, source, confidence, proxy_latency_ms)
             VALUES ($1, $2, $3, $4, $5, $6, $7)`,
            [
                deviceId,
                entityId,
                result.rule || result.matched_rule || null,
                result.diagnosis || null,
                result.source || 'rule_engine',
                result.confidence ?? null,
                result.latency_ms ?? null
            ]
        ).catch(() => {}); // fire-and-forget
    }

    // ── Main Endpoint ───────────────────────────
    router.post('/binding', async (req, res) => {
        const { deviceId, entityId, botSecret, problem_description, error_messages } = req.body;

        // Validate required fields
        if (!deviceId || entityId === undefined || !botSecret) {
            return res.status(400).json({
                success: false,
                error: 'missing_fields',
                message: 'Required fields: deviceId, entityId, botSecret'
            });
        }

        // Authenticate bot
        const eId = parseInt(entityId);
        const device = devices[deviceId];
        if (!device) {
            return res.status(404).json({ success: false, error: 'device_not_found', message: 'Device not found' });
        }
        if (eId < 0 || eId >= (device.entities || []).length) {
            return res.status(400).json({ success: false, error: 'invalid_entity', message: 'Invalid entityId' });
        }
        const entity = device.entities[eId];
        if (!entity || !entity.isBound) {
            return res.status(400).json({ success: false, error: 'not_bound', message: 'Entity not bound' });
        }
        if (!safeEqual(botSecret, entity.botSecret)) {
            return res.status(403).json({ success: false, error: 'invalid_secret', message: 'Invalid botSecret' });
        }

        // Rate limit
        const rateCheck = checkRateLimit(deviceId);
        if (!rateCheck.allowed) {
            return res.status(429).json({
                success: false,
                error: 'rate_limited',
                message: `Maximum ${SUPPORT_MAX_PER_HOUR} support queries per hour per device. Try again later.`,
                retry_after_ms: rateCheck.retryAfterMs
            });
        }

        const msgs = Array.isArray(error_messages) ? error_messages.filter(m => typeof m === 'string') : [];
        const desc = typeof problem_description === 'string' ? problem_description : '';

        // Combine problem_description into msgs for pattern matching
        const allMessages = desc ? [desc, ...msgs] : msgs;

        // Stage A: Pattern matching
        const ruleResult = matchRulePatterns(allMessages);
        if (ruleResult.matched) {
            const response = {
                success: true,
                source: 'rule_engine',
                matched_rule: ruleResult.rule,
                diagnosis: ruleResult.diagnosis,
                suggested_steps: ruleResult.suggested_steps,
                confidence: 1.0
            };
            logSupportQuery(deviceId, eId, response);
            serverLog('info', 'ai_support', `Rule matched: ${ruleResult.rule}`, { deviceId, entityId: eId });
            return res.json(response);
        }

        // Stage B: Log correlation
        const logResult = await correlateWithLogs(deviceId);
        if (logResult.matched) {
            const response = {
                success: true,
                source: 'rule_engine',
                matched_rule: logResult.rule,
                diagnosis: logResult.diagnosis,
                suggested_steps: logResult.suggested_steps,
                confidence: 0.9
            };
            logSupportQuery(deviceId, eId, response);
            serverLog('info', 'ai_support', `Log correlation matched: ${logResult.rule}`, { deviceId, entityId: eId });
            return res.json(response);
        }

        // Stage C: Forward to Claude CLI proxy
        serverLog('info', 'ai_support', 'Forwarding to Claude CLI proxy', { deviceId, entityId: eId });
        const { recentLogs, recentFailures, recentCrashes } = await fetchRecentData(deviceId);
        const proxyResult = await forwardToProxy(deviceId, eId, desc, msgs, recentLogs, recentFailures, {}, recentCrashes);

        logSupportQuery(deviceId, eId, proxyResult);
        if (proxyResult.source === 'claude_proxy') {
            serverLog('info', 'ai_support', `Proxy response (${proxyResult.latency_ms}ms)`, { deviceId, entityId: eId });
        } else {
            serverLog('warn', 'ai_support', `Proxy unavailable, returned fallback`, { deviceId, entityId: eId });
        }

        return res.json(proxyResult);
    });

    // ── Debug: Proxy Connectivity Test ────────
    router.get('/proxy-status', async (req, res) => {
        const proxyUrl = process.env.CLAUDE_CLI_PROXY_URL;
        const proxyKey = process.env.SUPPORT_API_KEY;

        const status = {
            proxy_url: proxyUrl || '(not set)',
            api_key_set: !!proxyKey,
            connectivity: 'unknown'
        };

        if (!proxyUrl) {
            status.connectivity = 'not_configured';
            return res.json(status);
        }

        try {
            const healthUrl = proxyUrl + '/health';
            const startTime = Date.now();
            const response = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
            status.latency_ms = Date.now() - startTime;
            status.http_status = response.status;
            status.connectivity = response.ok ? 'ok' : 'error';
            if (response.ok) {
                status.health = await response.json().catch(() => null);
            } else {
                status.body = await response.text().catch(() => '(unreadable)');
            }
        } catch (err) {
            status.connectivity = 'unreachable';
            status.error = err.cause ? `${err.message} (${err.cause.code || err.cause.message})` : err.message;
        }

        res.json(status);
    });

    // ── Proxy Session Logs (admin-only, forwarded) ──
    router.get('/proxy-sessions', async (req, res) => {
        // Admin auth check
        if (!req.user || !req.user.userId) {
            return res.status(401).json({ error: 'Not authenticated' });
        }
        const isAdmin = await resolveIsAdmin(req.user.userId);
        if (!isAdmin) {
            return res.status(403).json({ error: 'Admin access required' });
        }

        const proxyUrl = process.env.CLAUDE_CLI_PROXY_URL;
        const proxyKey = process.env.SUPPORT_API_KEY;
        if (!proxyUrl || !proxyKey) {
            return res.status(503).json({ error: 'Proxy not configured' });
        }

        // Forward query params
        const qs = new URLSearchParams();
        if (req.query.limit) qs.set('limit', req.query.limit);
        if (req.query.status) qs.set('status', req.query.status);
        if (req.query.since) qs.set('since', req.query.since);

        try {
            const target = `${proxyUrl}/sessions?${qs}`;
            const response = await fetch(target, {
                headers: { 'Authorization': `Bearer ${proxyKey}` },
                signal: AbortSignal.timeout(10000)
            });
            const data = await response.json();
            return res.json(data);
        } catch (err) {
            return res.status(502).json({ error: 'Proxy unreachable', detail: err.message });
        }
    });

    router.get('/proxy-sessions/:id', async (req, res) => {
        // Admin auth check
        if (!req.user || !req.user.userId) {
            return res.status(401).json({ error: 'Not authenticated' });
        }
        const isAdmin = await resolveIsAdmin(req.user.userId);
        if (!isAdmin) {
            return res.status(403).json({ error: 'Admin access required' });
        }

        const proxyUrl = process.env.CLAUDE_CLI_PROXY_URL;
        const proxyKey = process.env.SUPPORT_API_KEY;
        if (!proxyUrl || !proxyKey) {
            return res.status(503).json({ error: 'Proxy not configured' });
        }

        try {
            const target = `${proxyUrl}/sessions/${req.params.id}`;
            const response = await fetch(target, {
                headers: { 'Authorization': `Bearer ${proxyKey}` },
                signal: AbortSignal.timeout(10000)
            });
            if (!response.ok) {
                return res.status(response.status).json(await response.json().catch(() => ({ error: 'Not found' })));
            }
            const data = await response.json();
            return res.json(data);
        } catch (err) {
            return res.status(502).json({ error: 'Proxy unreachable', detail: err.message });
        }
    });

    // ── Admin Chat Endpoint (no botSecret needed) ──
    router.post('/admin-chat', async (req, res) => {
        const { message, deviceId, entityId } = req.body;

        if (!message || typeof message !== 'string' || !message.trim()) {
            return res.status(400).json({ success: false, error: 'message is required' });
        }

        const msgs = [message.trim()];

        // Stage A: Pattern matching
        const ruleResult = matchRulePatterns(msgs);
        if (ruleResult.matched) {
            return res.json({
                success: true,
                source: 'rule_engine',
                matched_rule: ruleResult.rule,
                diagnosis: ruleResult.diagnosis,
                suggested_steps: ruleResult.suggested_steps,
                confidence: 1.0
            });
        }

        // Stage B: Log correlation (if deviceId provided)
        if (deviceId) {
            const logResult = await correlateWithLogs(deviceId);
            if (logResult.matched) {
                return res.json({
                    success: true,
                    source: 'log_correlation',
                    matched_rule: logResult.rule,
                    diagnosis: logResult.diagnosis,
                    suggested_steps: logResult.suggested_steps,
                    confidence: 0.9
                });
            }
        }

        // Stage C: Forward to Claude CLI proxy
        const { recentLogs, recentFailures, recentCrashes: _recentCrashes } = deviceId
            ? await fetchRecentData(deviceId)
            : { recentLogs: [], recentFailures: [], recentCrashes: [] };

        const proxyResult = await forwardToProxy(
            deviceId || 'admin-test',
            entityId ?? -1,
            message.trim(),
            msgs,
            recentLogs,
            recentFailures,
            { role: 'admin' }
        );

        return res.json(proxyResult);
    });

    // ── Universal AI Chat Endpoint ─────────────
    router.post('/chat', async (req, res) => {
        // Guard: if body parsing failed (e.g. payload too large), req.body may be empty
        if (!req.body || typeof req.body !== 'object') {
            return res.status(400).json({
                success: false,
                error: 'invalid_request',
                message: 'Request body could not be parsed. If you attached images, they may be too large.'
            });
        }

        // Auth: web users via cookie (softAuthMiddleware), Android via deviceId+deviceSecret
        if (!req.user || !req.user.userId) {
            const { deviceId, deviceSecret } = req.body;
            if (deviceId && deviceSecret) {
                const dev = devices[deviceId];
                if (dev && safeEqual(dev.deviceSecret, deviceSecret)) {
                    req.user = { userId: `device_${deviceId}`, deviceId, email: null };
                } else {
                    return res.status(401).json({ success: false, error: 'invalid_credentials', message: 'Invalid device credentials. Please restart the app to re-register.' });
                }
            } else {
                return res.status(401).json({ success: false, error: 'not_authenticated', message: 'Authentication required. Please restart the app.' });
            }
        }

        const { message, history, page, images } = req.body;
        if (!message || typeof message !== 'string' || !message.trim()) {
            return res.status(400).json({ success: false, error: 'message is required' });
        }

        // ── Pre-execute close_issue intent before calling AI ──
        // The LLM refuses to call close_github_issue due to safety training.
        // We detect the intent here and execute it, then inject the result as context.
        let preExecutedCloseResult = null;
        const closeMatch = message.match(/close\s+(?:github\s+)?issue\s+#?(\d+)/i)
            || message.match(/#?(\d+)\s*(?:issue)?\s*(?:請\s*)?(?:關閉|close)/i);
        if (closeMatch) {
            const issueNum = parseInt(closeMatch[1]);
            if (issueNum > 0) {
                const closeResult = await closeIssue(issueNum, 'Closed by E-Claw AI assistant per user request.');
                preExecutedCloseResult = closeResult.success
                    ? { success: true, number: issueNum, url: closeResult.url }
                    : { success: false, number: issueNum, error: closeResult.error };
            }
        }

        // Validate images (max 3, must have data + mimeType)
        const validImages = (images || []).filter(img =>
            img && typeof img.data === 'string' && typeof img.mimeType === 'string' && img.mimeType.startsWith('image/')
        ).slice(0, 3);

        // Admin check via DB
        const isAdmin = await resolveIsAdmin(req.user.userId);

        // Rate limit
        const rateCheck = checkChatRateLimit(req.user.userId, isAdmin);
        if (!rateCheck.allowed) {
            return res.status(429).json({
                success: false,
                error: 'rate_limited',
                message: 'Maximum messages per hour reached. Try again later.',
                retry_after_ms: rateCheck.retryAfterMs
            });
        }

        // Short-circuit: if close was pre-executed, return result directly (skip AI call)
        if (preExecutedCloseResult) {
            if (preExecutedCloseResult.success) {
                return res.json({
                    success: true,
                    response: `✅ GitHub issue [#${preExecutedCloseResult.number}](${preExecutedCloseResult.url}) has been closed.`
                });
            } else {
                return res.json({
                    success: true,
                    response: `❌ Failed to close GitHub issue #${preExecutedCloseResult.number}: ${preExecutedCloseResult.error}`
                });
            }
        }

        const messageForAI = message.trim();

        // Fetch device diagnostics (errors, activity, entity states) if deviceId available
        const deviceDiag = req.user.deviceId
            ? await fetchDeviceContext(req.user.deviceId, devices[req.user.deviceId])
            : null;

        // Priority: Anthropic direct API > CLI proxy > fallback
        const anthropic = getAnthropicClient();
        if (anthropic) {
            const startTime = Date.now();
            try {
                const device = req.user.deviceId ? devices[req.user.deviceId] : null;
                const result = await anthropic.chatWithClaude({
                    message: messageForAI,
                    history: (history || []).slice(-20),
                    images: validImages.length > 0 ? validImages : undefined,
                    deviceContext: {
                        deviceId: req.user.deviceId,
                        deviceSecret: device ? device.deviceSecret : null,
                        page: page || 'unknown',
                        role: isAdmin ? 'admin' : 'user',
                        email: req.user.email
                    },
                    deviceDiagnostics: deviceDiag,
                    toolHandlers
                });
                const latencyMs = Date.now() - startTime;

                chatPool.query(
                    'INSERT INTO ai_chat_queries (user_id, is_admin, page, latency_ms) VALUES ($1, $2, $3, $4)',
                    [req.user.userId, isAdmin, page || null, latencyMs]
                ).catch(() => {});

                // Auto-execute GitHub actions (create_issue, close_issue)
                let responseText = result.response;
                let feedbackId = null;
                const actionResults = await executeGitHubActions(result.actions, req.user);
                for (const ar of actionResults) {
                    responseText += ar.text;
                    if (ar.feedbackId) feedbackId = ar.feedbackId;
                }

                return res.json({
                    success: true,
                    response: responseText,
                    feedbackId,
                    remaining: rateCheck.remaining,
                    latency_ms: latencyMs
                });
            } catch (err) {
                console.error(`[AI Chat] Anthropic direct error: ${err.message}`);
                // Fall through to proxy
            }
        }

        // Proxy path
        const proxyUrl = process.env.CLAUDE_CLI_PROXY_URL;
        const proxyKey = process.env.SUPPORT_API_KEY;
        if (!proxyUrl || !proxyKey) {
            return res.status(503).json({ success: false, response: 'AI assistant is not available at this time.' });
        }

        const startTime = Date.now();
        try {
            // CLI proxy doesn't support image vision — strip images and include a text note
        const proxyMessage = validImages.length > 0
            ? `${messageForAI}\n\n[Note: ${validImages.length} image(s) were attached but image analysis is only supported via the direct Anthropic API. Please describe the image in text if possible.]`
            : messageForAI;
        const response = await fetch(proxyUrl + '/chat', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${proxyKey}` },
                body: JSON.stringify({
                    message: proxyMessage,
                    history: (history || []).slice(-20),
                    device_context: {
                        deviceId: req.user.deviceId,
                        page: page || 'unknown',
                        role: isAdmin ? 'admin' : 'user',
                        email: req.user.email,
                        diagnostics: deviceDiag
                    },
                    ...proxyCallerAuthFields()
                }),
                signal: AbortSignal.timeout(130000)
            });

            const latencyMs = Date.now() - startTime;

            if (response.status === 503) {
                // Proxy is busy (queue full or timeout)
                const body = await response.json().catch(() => ({}));
                const latencyMs = Date.now() - startTime;
                return res.json({
                    success: true,
                    response: null,
                    busy: true,
                    retry_after: body.retry_after || 15,
                    remaining: rateCheck.remaining,
                    latency_ms: latencyMs
                });
            }

            if (!response.ok) {
                const body = await response.text().catch(() => '');
                throw new Error(`Proxy HTTP ${response.status}: ${body.slice(0, 200)}`);
            }

            const result = await response.json();

            // Log chat query
            chatPool.query(
                'INSERT INTO ai_chat_queries (user_id, is_admin, page, latency_ms) VALUES ($1, $2, $3, $4)',
                [req.user.userId, isAdmin, page || null, latencyMs]
            ).catch(() => {});

            let responseText = sanitizeProxyResponse(result.response);

            // Auto-execute GitHub actions (create_issue, close_issue)
            let feedbackId = null;
            const actionResults = await executeGitHubActions(result.actions, req.user);
            for (const ar of actionResults) {
                responseText += ar.text;
                if (ar.feedbackId) feedbackId = ar.feedbackId;
            }

            return res.json({
                success: true,
                response: responseText,
                actions: isAdmin ? result.actions : undefined,
                feedbackId,
                remaining: rateCheck.remaining,
                latency_ms: latencyMs
            });
        } catch (err) {
            const latencyMs = Date.now() - startTime;
            console.error(`[AI Chat] Error (${latencyMs}ms): ${err.message}`);
            return res.json({
                success: true,
                response: 'Sorry, the AI assistant is temporarily unavailable. Please try again shortly.',
                remaining: rateCheck.remaining,
                latency_ms: latencyMs
            });
        }
    });

    // ── GitHub Issue Creation (admin only) ────
    router.post('/create-issue', async (req, res) => {
        if (!req.user || !req.user.userId) {
            return res.status(401).json({ success: false, error: 'Not authenticated' });
        }

        // Admin check
        const isAdmin = await resolveIsAdmin(req.user.userId);
        if (!isAdmin) {
            return res.status(403).json({ success: false, error: 'Admin access required' });
        }

        const { title, body, labels } = req.body;
        if (!title || !body) {
            return res.status(400).json({ success: false, error: 'title and body are required' });
        }

        const token = process.env.GITHUB_TOKEN;
        const repo = process.env.GITHUB_REPO || 'HankHuang0516/EClaw';
        if (!token) {
            return res.status(503).json({ success: false, error: 'GitHub integration not configured' });
        }

        try {
            const ghRes = await fetch(`https://api.github.com/repos/${repo}/issues`, {
                method: 'POST',
                headers: {
                    'Authorization': `token ${token}`,
                    'Accept': 'application/vnd.github.v3+json',
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    title,
                    body: body + '\n\n---\n_Created via E-Claw AI Assistant_',
                    labels: Array.isArray(labels) ? labels : ['ai-assistant']
                })
            });

            if (!ghRes.ok) {
                const err = await ghRes.text();
                console.error(`[AI Chat] GitHub issue failed (${ghRes.status}):`, err.slice(0, 300));
                return res.status(500).json({ success: false, error: 'Failed to create GitHub issue' });
            }

            const data = await ghRes.json();
            serverLog('info', 'ai_chat', `Admin created GitHub issue #${data.number}`, { userId: req.user.userId });
            return res.json({ success: true, url: data.html_url, number: data.number });
        } catch (err) {
            console.error('[AI Chat] GitHub API error:', err.message);
            return res.status(500).json({ success: false, error: 'GitHub API error' });
        }
    });

    // ── Async Chat: Background Processor ────────
    async function processRequest(id) {
        await chatPool.query(`UPDATE ai_chat_requests SET status = 'processing' WHERE id = $1`, [id]);

        const { rows } = await chatPool.query('SELECT * FROM ai_chat_requests WHERE id = $1', [id]);
        if (!rows[0]) return;
        const row = rows[0];

        const startTime = Date.now();
        const history = row.history || [];
        const images = row.images || [];
        const ctx = row.device_context || {};

        // Fetch device diagnostics for this async request
        const deviceDiag = ctx.deviceId
            ? await fetchDeviceContext(ctx.deviceId, devices[ctx.deviceId])
            : null;

        // Priority: Anthropic direct API > CLI proxy > fallback
        const anthropic = getAnthropicClient();
        if (anthropic) {
            try {
                // Enrich context with deviceSecret for tool use
                const device = ctx.deviceId ? devices[ctx.deviceId] : null;
                if (device && !ctx.deviceSecret) {
                    ctx.deviceSecret = device.deviceSecret;
                }

                const result = await anthropic.chatWithClaude({
                    message: row.message,
                    history,
                    images: images.length > 0 ? images : undefined,
                    deviceContext: ctx,
                    deviceDiagnostics: deviceDiag,
                    toolHandlers
                });

                const latencyMs = Date.now() - startTime;
                let responseText = result.response;

                // Auto-execute GitHub actions (create_issue, close_issue)
                const user = { userId: row.user_id, email: ctx.email, deviceId: ctx.deviceId };
                const actionResults = await executeGitHubActions(result.actions, user);
                for (const ar of actionResults) {
                    responseText += ar.text;
                }

                await chatPool.query(
                    `UPDATE ai_chat_requests SET status = 'completed', response = $2, actions = $3, latency_ms = $4, completed_at = NOW(), images = NULL WHERE id = $1`,
                    [id, responseText, result.actions ? JSON.stringify(result.actions) : null, latencyMs]
                );

                chatPool.query('INSERT INTO ai_chat_queries (user_id, is_admin, page, latency_ms) VALUES ($1, $2, $3, $4)',
                    [row.user_id, row.is_admin, row.page, latencyMs]).catch(() => {});
                return;
            } catch (err) {
                console.error(`[AI Chat] Anthropic direct error for ${id}: ${err.message}`);
                // Fall through to proxy
            }
        }

        // Proxy path
        const proxyUrl = process.env.CLAUDE_CLI_PROXY_URL;
        const proxyKey = process.env.SUPPORT_API_KEY;

        if (!proxyUrl || !proxyKey) {
            const latencyMs = Date.now() - startTime;
            await chatPool.query(
                `UPDATE ai_chat_requests SET status = 'failed', error = $2, latency_ms = $3, completed_at = NOW(), images = NULL WHERE id = $1`,
                [id, 'AI assistant is not available at this time.', latencyMs]
            );
            return;
        }

        try {
            // Use per-event idle timeout instead of a fixed 130s hard limit.
            // If the proxy is actively streaming events, the timer resets each time.
            // Only abort if no event arrives for IDLE_TIMEOUT_MS (AI appears stuck).
            const IDLE_TIMEOUT_MS = 90000;
            const controller = new AbortController();
            let eventTimer = setTimeout(() => controller.abort(new Error('Idle timeout: no event for 90s')), IDLE_TIMEOUT_MS);
            const resetTimer = () => {
                clearTimeout(eventTimer);
                eventTimer = setTimeout(() => controller.abort(new Error('Idle timeout: no event for 90s')), IDLE_TIMEOUT_MS);
            };

            // CLI proxy doesn't support image vision — strip images and include a text note
            const proxyMessage = images.length > 0
                ? `${row.message}\n\n[Note: ${images.length} image(s) were attached but image analysis is only supported via the direct Anthropic API. Please describe the image in text if possible.]`
                : row.message;
            const response = await fetch(proxyUrl + '/chat?stream=1', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${proxyKey}` },
                body: JSON.stringify({
                    message: proxyMessage,
                    history,
                    device_context: { ...ctx, diagnostics: deviceDiag },
                    ...proxyCallerAuthFields()
                }),
                signal: controller.signal
            });

            if (response.status === 503) {
                clearTimeout(eventTimer);
                const body = await response.json().catch(() => ({}));
                const latencyMs = Date.now() - startTime;
                await chatPool.query(
                    `UPDATE ai_chat_requests SET status = 'completed', busy = true, retry_after = $2, latency_ms = $3, completed_at = NOW(), images = NULL WHERE id = $1`,
                    [id, body.retry_after || 15, latencyMs]
                );
                return;
            }

            if (!response.ok) {
                clearTimeout(eventTimer);
                throw new Error(`Proxy HTTP ${response.status}`);
            }

            // Read NDJSON stream — each line is either a status update or the final 'complete' event
            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let buf = '';
            let finalEvent = null;

            try {
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
                    buf += decoder.decode(value, { stream: true });
                    const lines = buf.split('\n');
                    buf = lines.pop(); // keep incomplete last line
                    for (const line of lines) {
                        if (!line.trim()) continue;
                        let evt;
                        try { evt = JSON.parse(line); } catch { continue; }
                        resetTimer(); // reset idle timeout on every event
                        if (evt.type === 'complete') {
                            finalEvent = evt;
                        } else if (evt.type === 'status') {
                            // Update progress in DB (fire-and-forget)
                            chatPool.query(
                                `UPDATE ai_chat_requests SET progress = $2 WHERE id = $1`,
                                [id, JSON.stringify({ ...evt, updated_at: Date.now() })]
                            ).catch(() => {});
                        }
                    }
                }
            } finally {
                clearTimeout(eventTimer);
                reader.releaseLock();
            }

            if (!finalEvent) throw new Error('Stream ended without complete event');

            const latencyMs = Date.now() - startTime;
            let responseText = sanitizeProxyResponse(finalEvent.response);

            // Auto-execute GitHub actions (create_issue, close_issue)
            const user = { userId: row.user_id, email: ctx.email, deviceId: ctx.deviceId };
            const actionResults = await executeGitHubActions(finalEvent.actions, user);
            let feedbackId = null;
            for (const ar of actionResults) {
                responseText += ar.text;
                if (ar.feedbackId) feedbackId = ar.feedbackId;
            }

            const actionsToStore = feedbackId
                ? JSON.stringify({ feedbackId })
                : (finalEvent.actions ? JSON.stringify(finalEvent.actions) : null);
            await chatPool.query(
                `UPDATE ai_chat_requests SET status = 'completed', response = $2, actions = $3, latency_ms = $4, completed_at = NOW(), images = NULL, progress = NULL WHERE id = $1`,
                [id, responseText, actionsToStore, latencyMs]
            );

            // Log to ai_chat_queries for analytics
            chatPool.query('INSERT INTO ai_chat_queries (user_id, is_admin, page, latency_ms) VALUES ($1, $2, $3, $4)',
                [row.user_id, row.is_admin, row.page, latencyMs]).catch(() => {});

        } catch (err) {
            const latencyMs = Date.now() - startTime;
            console.error(`[AI Chat] processRequest error (${latencyMs}ms): ${err.message}`);
            await chatPool.query(
                `UPDATE ai_chat_requests SET status = 'failed', error = $2, latency_ms = $3, completed_at = NOW(), images = NULL, progress = NULL WHERE id = $1`,
                [id, 'AI assistant temporarily unavailable. Please try again.', latencyMs]
            ).catch(() => {});
        }
    }

    // ── Async Chat: Submit Endpoint ──────────────
    router.post('/chat/submit', async (req, res) => {
        // Guard: if body parsing failed (e.g. payload too large), req.body may be empty
        if (!req.body || typeof req.body !== 'object') {
            return res.status(400).json({
                success: false,
                error: 'invalid_request',
                message: 'Request body could not be parsed. If you attached images, they may be too large.'
            });
        }

        // Auth: same as /chat
        if (!req.user || !req.user.userId) {
            const { deviceId, deviceSecret } = req.body;
            if (deviceId && deviceSecret) {
                const dev = devices[deviceId];
                if (dev && safeEqual(dev.deviceSecret, deviceSecret)) {
                    req.user = { userId: `device_${deviceId}`, deviceId, email: null };
                } else {
                    return res.status(401).json({ success: false, error: 'invalid_credentials', message: 'Invalid device credentials. Please restart the app to re-register.' });
                }
            } else {
                return res.status(401).json({ success: false, error: 'not_authenticated', message: 'Authentication required. Please restart the app.' });
            }
        }

        const { requestId, message, history, page, images } = req.body;

        if (!requestId || !/^[0-9a-f-]{36}$/i.test(requestId)) {
            return res.status(400).json({ success: false, error: 'Invalid requestId (must be UUID)' });
        }
        if (!message || typeof message !== 'string' || !message.trim()) {
            return res.status(400).json({ success: false, error: 'message is required' });
        }

        const validImages = (images || []).filter(img =>
            img && typeof img.data === 'string' && typeof img.mimeType === 'string' && img.mimeType.startsWith('image/')
        ).slice(0, 3);

        // Admin check
        const isAdmin = await resolveIsAdmin(req.user.userId);

        // Rate limit
        const rateCheck = checkChatRateLimit(req.user.userId, isAdmin);
        if (!rateCheck.allowed) {
            return res.status(429).json({
                success: false,
                error: 'rate_limited',
                message: 'Maximum messages per hour reached. Try again later.',
                retry_after_ms: rateCheck.retryAfterMs
            });
        }

        // Idempotency: if requestId already exists, return existing status
        try {
            const existing = await chatPool.query('SELECT id, status FROM ai_chat_requests WHERE id = $1', [requestId]);
            if (existing.rows.length > 0) {
                return res.json({ success: true, requestId, status: existing.rows[0].status });
            }
        } catch (err) {
            console.warn('[AI-Support] idempotency lookup failed for', requestId, ':', err.message);
        }

        // Insert pending request
        try {
            await chatPool.query(
                `INSERT INTO ai_chat_requests (id, user_id, is_admin, page, message, history, images, device_context, status)
                 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending')`,
                [requestId, req.user.userId, isAdmin, page || null, message.trim(),
                 JSON.stringify((history || []).slice(-20)),
                 validImages.length > 0 ? JSON.stringify(validImages) : null,
                 JSON.stringify({ deviceId: req.user.deviceId, deviceSecret: req.user.deviceId ? (devices[req.user.deviceId]?.deviceSecret || null) : null, page, role: isAdmin ? 'admin' : 'user', email: req.user.email })]
            );
        } catch (err) {
            console.error('[AI Chat] Submit insert error:', err.message);
            return res.status(500).json({ success: false, error: 'Failed to save request' });
        }

        // Respond immediately
        res.json({ success: true, requestId });

        // Fire-and-forget background processing
        processRequest(requestId).catch(err => {
            console.error(`[AI Chat] Background processing error for ${requestId}:`, err.message);
        });
    });

    // ── Async Chat: Poll Endpoint ────────────────
    router.get('/chat/poll/:requestId', async (req, res) => {
        if (!req.user || !req.user.userId) {
            // Try device auth from query params
            const { deviceId, deviceSecret } = req.query;
            if (deviceId && deviceSecret) {
                const dev = devices[deviceId];
                if (dev && safeEqual(dev.deviceSecret, deviceSecret)) {
                    req.user = { userId: `device_${deviceId}`, deviceId, email: null };
                } else {
                    return res.status(401).json({ success: false, error: 'Not authenticated' });
                }
            } else {
                return res.status(401).json({ success: false, error: 'Not authenticated' });
            }
        }

        const { requestId } = req.params;

        try {
            const { rows } = await chatPool.query(
                'SELECT status, response, actions, busy, retry_after, error, latency_ms, progress FROM ai_chat_requests WHERE id = $1 AND user_id = $2',
                [requestId, req.user.userId]
            );

            if (!rows[0]) {
                return res.status(404).json({ success: false, error: 'Request not found' });
            }

            const r = rows[0];
            return res.json({
                success: true,
                status: r.status,
                response: r.response,
                actions: r.actions,
                busy: r.busy || false,
                retry_after: r.retry_after,
                error: r.error,
                latency_ms: r.latency_ms,
                progress: r.progress || null
            });
        } catch (err) {
            console.error('[AI Chat] Poll error:', err.message);
            return res.status(500).json({ success: false, error: 'Internal error' });
        }
    });

    // ── Async Chat: Cleanup Job ──────────────────
    setInterval(async () => {
        try {
            await chatPool.query(
                `UPDATE ai_chat_requests SET status = 'expired', error = 'Request timed out', images = NULL, completed_at = NOW()
                 WHERE status IN ('pending', 'processing') AND created_at < NOW() - INTERVAL '10 minutes'`
            );
            await chatPool.query(
                `DELETE FROM ai_chat_requests WHERE status IN ('completed', 'failed', 'expired') AND created_at < NOW() - INTERVAL '24 hours'`
            );
        } catch (err) {
            console.error('[AI Chat] Cleanup error:', err.message);
        }
    }, 300000); // every 5 minutes

    // ── DB Table Init ───────────────────────────
    function initSupportTable() {
        chatPool.query(`
            CREATE TABLE IF NOT EXISTS ai_support_queries (
                id SERIAL PRIMARY KEY,
                device_id TEXT NOT NULL,
                entity_id INTEGER NOT NULL,
                matched_rule TEXT,
                diagnosis TEXT,
                source VARCHAR(16) NOT NULL DEFAULT 'rule_engine',
                confidence REAL,
                proxy_latency_ms INTEGER,
                created_at TIMESTAMP DEFAULT NOW()
            );
            CREATE INDEX IF NOT EXISTS idx_asq_device ON ai_support_queries(device_id);
            CREATE INDEX IF NOT EXISTS idx_asq_created ON ai_support_queries(created_at DESC);
        `).catch(err => {
            console.error('[AI Support] Table init error:', err.message);
        });

        chatPool.query(`
            CREATE TABLE IF NOT EXISTS ai_chat_queries (
                id SERIAL PRIMARY KEY,
                user_id TEXT NOT NULL,
                is_admin BOOLEAN DEFAULT false,
                page VARCHAR(32),
                latency_ms INTEGER,
                created_at TIMESTAMP DEFAULT NOW()
            );
            CREATE INDEX IF NOT EXISTS idx_acq_user ON ai_chat_queries(user_id);
            CREATE INDEX IF NOT EXISTS idx_acq_created ON ai_chat_queries(created_at DESC);
        `).catch(err => {
            console.error('[AI Chat] Table init error:', err.message);
        });

        chatPool.query(`
            CREATE TABLE IF NOT EXISTS ai_chat_requests (
                id VARCHAR(36) PRIMARY KEY,
                user_id TEXT NOT NULL,
                is_admin BOOLEAN DEFAULT false,
                page VARCHAR(32),
                message TEXT NOT NULL,
                history JSONB,
                images JSONB,
                device_context JSONB,
                status VARCHAR(16) NOT NULL DEFAULT 'pending',
                response TEXT,
                actions JSONB,
                error TEXT,
                busy BOOLEAN DEFAULT false,
                retry_after INTEGER,
                remaining INTEGER,
                latency_ms INTEGER,
                created_at TIMESTAMP DEFAULT NOW(),
                completed_at TIMESTAMP
            );
            CREATE INDEX IF NOT EXISTS idx_acr_user ON ai_chat_requests(user_id);
            CREATE INDEX IF NOT EXISTS idx_acr_status ON ai_chat_requests(status);
            CREATE INDEX IF NOT EXISTS idx_acr_created ON ai_chat_requests(created_at DESC);
        `).catch(err => {
            console.error('[AI Chat] Requests table init error:', err.message);
        });
        // Migration: add progress column for real-time status updates
        chatPool.query(`ALTER TABLE ai_chat_requests ADD COLUMN IF NOT EXISTS progress JSONB DEFAULT NULL`)
            .catch(err => console.error('[AI Chat] progress column migration error:', err.message));
    }

    return { router, initSupportTable, closeIssue };
};
