import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import type { ITelegramClient } from '../../integrations/telegram/telegramClient';
import type { TelegramBot } from '../../integrations/telegram/telegramBot';
import { logError, logInfo } from '../../utils';
import { discoverModels } from '../../lib/discoverModels';
import { pickConfigTarget } from '../../lib/paths';

/**
 * Astra Settings webview.
 *
 * Replaces the old `'openSettings'` shortcut (which dumped the user into the
 * raw VS Code settings UI for `g1nation`) with a feature-aware panel. Phase
 * 5-A only ships the Telegram section — other sections are stubs that link
 * back to VS Code Settings until 5-B fills them in.
 *
 * Why a webview instead of contributing more to VS Code Settings:
 *   - Token input must be password-masked AND end up in SecretStorage, which
 *     VS Code Settings cannot do.
 *   - Chat ID auto-detection needs an interactive flow with feedback ("polling
 *     for next message…") that VS Code Settings cannot host.
 *   - We want one place that knows "is the bot connected right now?" — VS
 *     Code Settings cannot show live status.
 *
 * State flow (uni-directional, like a tiny redux):
 *
 *   TelegramBot / SecretStorage / WorkspaceConfig ──┐
 *                                                   ├──► provider.refreshState() ──► postMessage({type:'state', ...}) ──► webview rerenders
 *   webview button click ──postMessage(action)──────┘
 *
 * The webview is rendered every time we `refreshState()` so we don't need
 * incremental DOM diffing — just a small string template.
 */

const TELEGRAM_TOKEN_SECRET_KEY = 'g1nation.telegram.botToken';

export interface SettingsPanelDeps {
    extensionUri: vscode.Uri;
    /** Used by features that store data in workspaceState / globalState (Google OAuth tokens etc). */
    context: vscode.ExtensionContext;
    secrets: vscode.SecretStorage;
    /** Returns the live Telegram client so we can call getMe for "test connection". */
    telegramClient: ITelegramClient;
    /** Returns the live bot instance for enrollNextChat. */
    telegramBot: TelegramBot;
    /**
     * LM Studio SDK 의 '다운로드된 모든 LLM' 목록 콜백 (보통 lmStudioClient.listDownloadedCached).
     * 모델 dropdown 이 보유 모델 전부를 보여주도록 discoverModels 에 전달한다.
     * 없으면 REST `/v1/models` 만 사용 → JIT 환경에서 로드된 1개만 나오는 회귀 발생.
     */
    lmStudioDownloaded?: () => Promise<string[]>;
}

interface SettingsState {
    telegram: {
        hasToken: boolean;
        enabled: boolean;
        connected: boolean;
        botName?: string;
        allowedChatIds: number[];
        enrolling: boolean;
        lastError?: string;
        lastSuccess?: string;
    };
    connection: {
        ollamaUrl: string;
        defaultModel: string;
        requestTimeout: number;
        availableModels: string[];
        modelsLoading: boolean;
    };
    memory: {
        memoryEnabled: boolean;
        memoryShortTermMessages: number;
        memoryMediumTermSessions: number;
        memoryLongTermFiles: number;
    };
    brain: {
        activeBrainId: string;
        activeBrainName: string;
        activeBrainPath: string;
        profileCount: number;
        autoPushBrain: boolean;
    };
    advanced: {
        dryRun: boolean;
        multiAgentEnabled: boolean;
        maxAutoSteps: number;
        maxContextSize: number;
        chatTemperature: number;
        chunkedSwitchTokens: number;
        chunkedMaxSections: number;
        polishPersonaOverride: string;
    };
    datacollect: {
        /** 'local' | 'nas' — 어느 Bridge 인스턴스를 호출할지. */
        bridgeTarget: string;
        bridgeUrl: string;
        /** NAS 경량 Bridge URL (nas 타깃일 때). */
        bridgeNasUrl: string;
        /** NAS Bridge 의 x-bridge-token (nas 타깃일 때 헤더로 전송). */
        bridgeNasToken: string;
        /** Empty → results saved to the Bridge's WIKI_RAW_PATH default. */
        savePath: string;
        crawlDepth: number;
        maxPages: number;
        synthesisTemperature: number;
    };
    google: {
        clientId: string;
        /** secret 자체는 client 에 echo 안 함 — *설정 여부* 만. true 면 input placeholder 가 "저장됨" 으로 바뀜. */
        hasClientSecret: boolean;
        calendarId: string;
        defaultEventDurationMinutes: number;
        /** iCal URL 도 capability 토큰성이라 *설정 여부* 만 전송. 사용자가 새 값 입력 시 덮어씀. */
        hasIcalUrl: boolean;
        icalDaysAhead: number;
        /** OAuth 연결 상태 — globalState 의 refresh token 존재 여부 + 누가 연결됐는지. */
        connected: boolean;
        connectedAs?: string;
        connectedAt?: string;
        lastIcalFetchAt?: string;
    };
    /**
     * Cloud LLM providers (OpenRouter / Anthropic / Gemini). API key 자체는 echo 안 함 —
     * hasApiKey boolean 만 전송. enabled 와 defaultModel 은 settings 에서 직접 읽음.
     */
    providers: {
        openrouter: { enabled: boolean; hasApiKey: boolean; defaultModel: string };
        anthropic:  { enabled: boolean; hasApiKey: boolean; defaultModel: string };
        gemini:     { enabled: boolean; hasApiKey: boolean; defaultModel: string };
    };
    devilAgent: {
        enabled: boolean;
    };
    /** Sectional banner shown when config.update fails (e.g. reload required). */
    bannerError?: string;
}

export class SettingsPanelProvider implements vscode.WebviewViewProvider {
    public static readonly viewType = 'g1nation-settings-panel';

    private _view?: vscode.WebviewView;
    private _panel?: vscode.WebviewPanel;
    private _enrolling = false;
    private _lastError?: string;
    private _lastSuccess?: string;
    private _botName?: string;
    private _bannerError?: string;
    private _modelsCache: { url: string; models: string[]; expiresAt: number } | undefined;
    private _modelsLoading = false;
    private static readonly MODELS_CACHE_TTL_MS = 30000;

    constructor(private readonly _deps: SettingsPanelDeps) {}

    public resolveWebviewView(view: vscode.WebviewView): void {
        this._view = view;
        this._setupWebview(view.webview);
        view.onDidDispose(() => { this._view = undefined; });
        void this._refreshState();
        void this._fetchModelsAndRefresh();
    }

    /**
     * Common webview wiring shared between sidebar `WebviewView` and floating
     * `WebviewPanel` paths. Sets options, message handler, and initial HTML.
     */
    private _setupWebview(webview: vscode.Webview): void {
        webview.options = {
            enableScripts: true,
            localResourceRoots: [this._deps.extensionUri],
        };
        webview.onDidReceiveMessage((msg) => { void this._handleMessage(msg); });
        webview.html = this._renderShell(webview);
    }

    public async focus(): Promise<void> {
        await this.openAsPanel();
    }

    /**
     * Open the settings UI as a stand-alone editor panel (Column 3 by default).
     * Astra's sidebar view container was removed in 2.81 — all three webviews
     * (Chat, Approvals, Settings) now live in the editor area.
     */
    public async openAsPanel(column: vscode.ViewColumn = vscode.ViewColumn.Three): Promise<vscode.WebviewPanel> {
        if (this._panel) {
            this._panel.reveal(column);
            return this._panel;
        }
        const panel = vscode.window.createWebviewPanel(
            SettingsPanelProvider.viewType,
            'Astra Settings',
            column,
            { enableScripts: true, localResourceRoots: [this._deps.extensionUri], retainContextWhenHidden: true }
        );
        this._panel = panel;
        this._setupWebview(panel.webview);
        panel.onDidDispose(() => { this._panel = undefined; });
        await this._refreshState();
        void this._fetchModelsAndRefresh();
        return panel;
    }

    /** Re-pull state from sources of truth and broadcast to the webview. */
    public async refresh(): Promise<void> {
        await this._refreshState();
        // If the cached URL drifted from the live config, refetch models so the
        // dropdown stays in sync with the sidebar (which may have triggered an
        // engine switch).
        const liveUrl = (vscode.workspace.getConfiguration('g1nation').get<string>('ollamaUrl', '') || '').trim();
        if (this._modelsCache && this._modelsCache.url !== liveUrl) {
            this._modelsCache = undefined;
            void this._fetchModelsAndRefresh();
        }
    }

    private async _handleMessage(msg: any): Promise<void> {
        if (!msg || typeof msg.type !== 'string') return;
        try {
            switch (msg.type) {
                case 'ready':
                    await this._refreshState();
                    return;
                case 'telegram.saveToken':
                    await this._handleSaveToken(String(msg.token ?? ''));
                    return;
                case 'telegram.clearToken':
                    await this._deps.secrets.delete(TELEGRAM_TOKEN_SECRET_KEY);
                    this._botName = undefined;
                    this._lastError = undefined;
                    await this._refreshState();
                    return;
                case 'telegram.toggleEnabled':
                    await this._safeConfigUpdate('telegram.enabled', !!msg.enabled);
                    return; // onDidChangeConfiguration listener triggers a refresh
                case 'telegram.testConnection':
                    await this._handleTestConnection();
                    return;
                case 'telegram.enroll':
                    await this._handleEnroll();
                    return;
                case 'telegram.cancelEnroll':
                    this._deps.telegramBot.cancelEnrollment();
                    this._enrolling = false;
                    await this._refreshState();
                    return;
                case 'telegram.removeChatId':
                    await this._handleRemoveChatId(Number(msg.chatId));
                    return;
                case 'connection.update':
                    await this._handleConnectionUpdate(msg);
                    return;
                case 'memory.update':
                    await this._handleMemoryUpdate(msg);
                    return;
                case 'brain.update':
                    await this._handleBrainUpdate(msg);
                    return;
                case 'advanced.update':
                    await this._handleAdvancedUpdate(msg);
                    return;
                case 'datacollect.update':
                    await this._handleDatacollectUpdate(msg);
                    return;
                case 'google.update':
                    await this._handleGoogleUpdate(msg);
                    return;
                case 'google.connect':
                    await vscode.commands.executeCommand('g1nation.calendar.connectOAuth');
                    await this._refreshState();
                    return;
                case 'google.disconnect':
                    await this._handleGoogleDisconnect();
                    return;
                case 'google.icalRefresh':
                    await this._handleGoogleIcalRefresh();
                    return;
                case 'providers.update':
                    await this._handleProvidersUpdate(msg);
                    return;
                case 'devilAgent.toggle':
                    await this._safeConfigUpdate('devilAgent.enabled', !!msg.enabled);
                    return;
                case 'openVscodeSettings':
                    await vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation');
                    return;
                default:
                    logInfo('SettingsPanel: unknown message', { type: msg.type });
            }
        } catch (e: any) {
            this._lastError = e?.message ?? String(e);
            logError('SettingsPanel message failed.', { type: msg?.type, error: this._lastError });
            await this._refreshState();
        }
    }

    private async _handleSaveToken(token: string): Promise<void> {
        const trimmed = token.trim();
        if (!/^\d+:[A-Za-z0-9_-]{20,}$/.test(trimmed)) {
            this._lastError = 'Token 형식이 올바르지 않습니다 (예: 123456789:AAH...).';
            this._lastSuccess = undefined;
            await this._refreshState();
            return;
        }
        await this._deps.secrets.store(TELEGRAM_TOKEN_SECRET_KEY, trimmed);
        this._lastError = undefined;
        this._lastSuccess = '토큰이 저장되었습니다. 자동 연결 테스트 중…';
        await this._refreshState();
        // Auto-test so the user gets immediate feedback.
        await this._handleTestConnection();
    }

    private async _handleTestConnection(): Promise<void> {
        this._lastError = undefined;
        try {
            const me = await this._deps.telegramClient.getMe();
            this._botName = me.username ? `@${me.username}` : me.first_name || `id ${me.id}`;
            this._lastSuccess = `연결 성공: ${this._botName}`;
            vscode.window.setStatusBarMessage(`Telegram 연결 성공: ${this._botName}`, 3000);
        } catch (e: any) {
            this._botName = undefined;
            this._lastError = e?.message ?? String(e);
            this._lastSuccess = undefined;
        }
        await this._refreshState();
    }

    /**
     * Update a g1nation config value with a friendly error path. VS Code throws
     * `Unable to write to User Settings because <key> is not a registered
     * configuration` when a stale extension build is loaded — we translate
     * that into a panel banner suggesting the reload.
     */
    private async _safeConfigUpdate(key: string, value: unknown): Promise<boolean> {
        try {
            const { target } = pickConfigTarget('g1nation', key);
            await vscode.workspace
                .getConfiguration('g1nation')
                .update(key, value, target);
            this._bannerError = undefined;
            return true;
        } catch (e: any) {
            const msg = e?.message ?? String(e);
            if (/not a registered configuration/i.test(msg)) {
                this._bannerError =
                    '설정 저장 실패: 확장이 새 설정 정의를 아직 못 읽었습니다. ' +
                    '"Developer: Reload Window" 를 실행한 뒤 다시 시도하세요. ' +
                    '(또는 .vsix 재설치)';
            } else {
                this._bannerError = `설정 저장 실패: ${msg}`;
            }
            logError('SettingsPanel config.update failed.', { key, error: msg });
            await this._refreshState();
            return false;
        }
    }

    private async _handleEnroll(): Promise<void> {
        const cfg = vscode.workspace.getConfiguration('g1nation');
        const token = (await this._deps.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '';
        if (!token.trim()) {
            this._lastError = '먼저 Bot Token을 저장하세요.';
            await this._refreshState();
            return;
        }
        // The bot must be running to receive the next message; auto-enable if needed.
        if (!cfg.get<boolean>('telegram.enabled', false)) {
            const ok = await this._safeConfigUpdate('telegram.enabled', true);
            if (!ok) return; // banner already shown
        }
        // The settings change above triggers refreshTelegramBot via
        // onDidChangeConfiguration, but that happens on a later microtask.
        // Start the bot now so enrollNextChat actually has a polling loop
        // that can intercept the next inbound update — otherwise the user
        // sees "no response" until the listener catches up.
        if (!this._deps.telegramBot.isRunning()) {
            this._deps.telegramBot.start();
        }
        this._enrolling = true;
        this._lastError = undefined;
        this._lastSuccess = '봇 폴링 시작됨. 텔레그램에서 봇에게 아무 메시지나 한 번 보내주세요.';
        await this._refreshState();

        try {
            const enrolled = await this._deps.telegramBot.enrollNextChat();
            const existing = cfg.get<number[]>('telegram.allowedChatIds', []) || [];
            const merged = existing.includes(enrolled.chatId) ? existing : [...existing, enrolled.chatId];
            await this._safeConfigUpdate('telegram.allowedChatIds', merged);
            const label = enrolled.username ? `@${enrolled.username}` : (enrolled.firstName || `id ${enrolled.chatId}`);
            this._lastSuccess = `채널 등록 완료: ${label} (id ${enrolled.chatId})`;
            vscode.window.showInformationMessage(`Telegram 채널이 등록되었습니다: ${label} (id ${enrolled.chatId}).`);
        } catch (e: any) {
            this._lastError = e?.message ?? String(e);
        } finally {
            this._enrolling = false;
            await this._refreshState();
        }
    }

    private async _handleRemoveChatId(chatId: number): Promise<void> {
        if (!Number.isFinite(chatId)) return;
        const cfg = vscode.workspace.getConfiguration('g1nation');
        const existing = cfg.get<number[]>('telegram.allowedChatIds', []) || [];
        const next = existing.filter((id) => id !== chatId);
        await this._safeConfigUpdate('telegram.allowedChatIds', next);
    }

    private async _handleConnectionUpdate(msg: any): Promise<void> {
        if (typeof msg.ollamaUrl === 'string') {
            const ok = await this._safeConfigUpdate('ollamaUrl', msg.ollamaUrl.trim());
            if (ok) this._modelsCache = undefined; // URL changed → invalidate model list
        }
        if (typeof msg.defaultModel === 'string') {
            await this._safeConfigUpdate('defaultModel', msg.defaultModel.trim());
        }
        if (typeof msg.requestTimeout === 'number' && Number.isFinite(msg.requestTimeout)) {
            await this._safeConfigUpdate('requestTimeout', Math.max(1, Math.floor(msg.requestTimeout)));
        }
        if (msg.refreshModels) {
            this._modelsCache = undefined;
            await this._fetchModelsAndRefresh();
        }
    }

    /**
     * Fetch the model list and broadcast (with a `loading` flag while in flight)
     * so the settings panel's dropdown stays in sync with the sidebar's.
     * Cached for `MODELS_CACHE_TTL_MS` per `ollamaUrl` to avoid hammering the
     * engine while the panel is open.
     */
    private async _fetchModelsAndRefresh(): Promise<void> {
        const cfg = vscode.workspace.getConfiguration('g1nation');
        const url = (cfg.get<string>('ollamaUrl', '') || '').trim();
        const now = Date.now();
        const cached = this._modelsCache;
        if (cached && cached.url === url && cached.expiresAt > now) return;

        this._modelsLoading = true;
        await this._refreshState();
        try {
            const models = await discoverModels(url, { lmStudioDownloaded: this._deps.lmStudioDownloaded });
            this._modelsCache = {
                url,
                models,
                expiresAt: Date.now() + SettingsPanelProvider.MODELS_CACHE_TTL_MS,
            };
        } finally {
            this._modelsLoading = false;
            await this._refreshState();
        }
    }

    private async _handleMemoryUpdate(msg: any): Promise<void> {
        if (typeof msg.memoryEnabled === 'boolean') {
            await this._safeConfigUpdate('memoryEnabled', msg.memoryEnabled);
        }
        const numericFields: Array<keyof SettingsState['memory']> = [
            'memoryShortTermMessages',
            'memoryMediumTermSessions',
            'memoryLongTermFiles',
        ];
        for (const f of numericFields) {
            const v = (msg as any)[f];
            if (typeof v === 'number' && Number.isFinite(v)) {
                await this._safeConfigUpdate(f, Math.max(0, Math.floor(v)));
            }
        }
    }

    private async _handleBrainUpdate(msg: any): Promise<void> {
        if (typeof msg.activeBrainId === 'string') {
            await this._safeConfigUpdate('activeBrainId', msg.activeBrainId);
        }
        if (typeof msg.autoPushBrain === 'boolean') {
            await this._safeConfigUpdate('autoPushBrain', msg.autoPushBrain);
        }
    }

    // ────────────── Google (Calendar + Sheets) ──────────────
    // Settings 패널이 보여주는 모든 Google 필드는 `g1nation.google.*` configuration 으로
    // 저장. secret(Client Secret / iCal URL / refresh token)은 *값 자체를 client 에 echo
    // 안 함* — 설정 여부만 true/false 로. 사용자가 새 값을 입력 시 덮어쓰는 단방향.

    private _buildGoogleState(): SettingsState['google'] {
        const ctx = this._deps.context;
        const { readCalendarConfig } = require('../calendar') as typeof import('../calendar');
        const cur = readCalendarConfig(ctx);
        return {
            clientId: cur.clientId ?? '',
            hasClientSecret: !!cur.clientSecret,
            calendarId: cur.calendarId ?? 'primary',
            defaultEventDurationMinutes: cur.defaultDurationMinutes ?? 60,
            hasIcalUrl: !!cur.icalUrl,
            icalDaysAhead: cur.daysAhead ?? 14,
            connected: !!cur.refreshToken,
            connectedAs: cur.connectedAs,
            connectedAt: cur.connectedAt,
            lastIcalFetchAt: cur.lastFetchAt,
        };
    }

    private async _handleGoogleUpdate(msg: any): Promise<void> {
        const { writeCalendarConfig } = require('../calendar') as typeof import('../calendar');
        // 필드별로 *명시적으로 전달된 것만* 패치. undefined / 누락은 무시.
        const patch: any = {};
        if (typeof msg.clientId === 'string') patch.clientId = msg.clientId.trim() || undefined;
        if (typeof msg.clientSecret === 'string') patch.clientSecret = msg.clientSecret.trim() || undefined;
        if (typeof msg.calendarId === 'string') patch.calendarId = msg.calendarId.trim() || 'primary';
        if (typeof msg.defaultEventDurationMinutes === 'number' && Number.isFinite(msg.defaultEventDurationMinutes)) {
            patch.defaultDurationMinutes = Math.max(5, Math.min(720, Math.floor(msg.defaultEventDurationMinutes)));
        }
        if (typeof msg.icalUrl === 'string') patch.icalUrl = msg.icalUrl.trim();
        if (typeof msg.icalDaysAhead === 'number' && Number.isFinite(msg.icalDaysAhead)) {
            patch.daysAhead = Math.max(1, Math.min(90, Math.floor(msg.icalDaysAhead)));
        }
        if (Object.keys(patch).length === 0) return;
        await writeCalendarConfig(this._deps.context, patch);
        this._lastSuccess = '저장되었습니다.';
        this._lastError = undefined;
        await this._refreshState();
    }

    private async _handleGoogleDisconnect(): Promise<void> {
        const { writeCalendarConfig } = require('../calendar') as typeof import('../calendar');
        await writeCalendarConfig(this._deps.context, {
            refreshToken: undefined,
            accessToken: undefined,
            accessTokenExpiresAt: undefined,
            connectedAs: undefined,
            connectedAt: undefined,
        });
        this._lastSuccess = 'OAuth 연결을 해제했습니다. https://myaccount.google.com/permissions 에서 권한 회수 권장.';
        this._lastError = undefined;
        await this._refreshState();
    }

    private async _handleGoogleIcalRefresh(): Promise<void> {
        try {
            const { refreshCalendarCache } = require('../calendar') as typeof import('../calendar');
            const r = await refreshCalendarCache(this._deps.context);
            if (r.ok) {
                this._lastSuccess = `iCal 새로고침 완료 — ${r.count}개 일정 동기화`;
                this._lastError = undefined;
            } else {
                this._lastError = r.error || 'iCal 새로고침 실패';
                this._lastSuccess = undefined;
            }
        } catch (e: any) {
            this._lastError = e?.message ?? String(e);
            this._lastSuccess = undefined;
        }
        await this._refreshState();
    }

    // ────────────── Cloud LLM Providers ──────────────
    // OpenRouter / Anthropic / Gemini API key + enable 토글. API key 는 Secret Storage 만.
    // settings 패널은 *값 자체는 안 보여줌* (hasApiKey boolean 만). 사용자가 새로 입력 시 덮어씀.

    private async _buildProvidersState(): Promise<SettingsState['providers']> {
        const { readProviderStatus } = require('../providers') as typeof import('../providers');
        const ctx = this._deps.context;
        const [or, an, ge] = await Promise.all([
            readProviderStatus(ctx, 'openrouter'),
            readProviderStatus(ctx, 'anthropic'),
            readProviderStatus(ctx, 'gemini'),
        ]);
        return { openrouter: or, anthropic: an, gemini: ge };
    }

    private async _handleProvidersUpdate(msg: any): Promise<void> {
        const { writeProviderConfig } = require('../providers') as typeof import('../providers');
        const id = msg.providerId;
        if (id !== 'openrouter' && id !== 'anthropic' && id !== 'gemini') return;
        const patch: any = {};
        if (typeof msg.enabled === 'boolean') patch.enabled = msg.enabled;
        if (typeof msg.apiKey === 'string') patch.apiKey = msg.apiKey;
        if (typeof msg.defaultModel === 'string') patch.defaultModel = msg.defaultModel;
        if (Object.keys(patch).length === 0) return;
        await writeProviderConfig(this._deps.context, id, patch);
        this._lastSuccess = `${id} 저장 완료`;
        this._lastError = undefined;
        await this._refreshState();
    }

    private async _handleAdvancedUpdate(msg: any): Promise<void> {
        if (typeof msg.dryRun === 'boolean') {
            await this._safeConfigUpdate('dryRun', msg.dryRun);
        }
        if (typeof msg.multiAgentEnabled === 'boolean') {
            await this._safeConfigUpdate('multiAgentEnabled', msg.multiAgentEnabled);
        }
        if (typeof msg.maxAutoSteps === 'number' && Number.isFinite(msg.maxAutoSteps)) {
            await this._safeConfigUpdate('maxAutoSteps', Math.max(1, Math.floor(msg.maxAutoSteps)));
        }
        if (typeof msg.maxContextSize === 'number' && Number.isFinite(msg.maxContextSize)) {
            await this._safeConfigUpdate('maxContextSize', Math.max(1000, Math.floor(msg.maxContextSize)));
        }
        if (typeof msg.chatTemperature === 'number' && Number.isFinite(msg.chatTemperature)) {
            await this._safeConfigUpdate('chatTemperature', Math.max(0, Math.min(2, msg.chatTemperature)));
        }
        if (typeof msg.chunkedSwitchTokens === 'number' && Number.isFinite(msg.chunkedSwitchTokens)) {
            await this._safeConfigUpdate('chunkedSwitchTokens', Math.max(1000, Math.floor(msg.chunkedSwitchTokens)));
        }
        if (typeof msg.chunkedMaxSections === 'number' && Number.isFinite(msg.chunkedMaxSections)) {
            await this._safeConfigUpdate('chunkedMaxSections', Math.max(1, Math.min(10, Math.floor(msg.chunkedMaxSections))));
        }
        if (typeof msg.polishPersonaOverride === 'string') {
            // 빈 문자열도 유효한 값 (default persona 로 되돌리기). trim 으로 공백만 입력 무력화.
            await this._safeConfigUpdate('polishPersonaOverride', msg.polishPersonaOverride.trim());
        }
    }

    // ────────────── Datacollect (slash 명령) ──────────────
    // /research·/benchmark·/youtube 가 호출하는 Bridge URL 과, 결과물 저장 위치.
    // savePath 가 비어 있으면 Bridge 의 WIKI_RAW_PATH 환경변수가 저장 위치를 결정한다.

    private async _handleDatacollectUpdate(msg: any): Promise<void> {
        if (typeof msg.bridgeTarget === 'string') {
            const t = msg.bridgeTarget.trim() === 'nas' ? 'nas' : 'local';
            await this._safeConfigUpdate('datacollectBridgeTarget', t);
        }
        if (typeof msg.bridgeUrl === 'string') {
            await this._safeConfigUpdate('datacollectBridgeUrl', msg.bridgeUrl.trim());
        }
        if (typeof msg.bridgeNasUrl === 'string') {
            await this._safeConfigUpdate('datacollectBridgeNasUrl', msg.bridgeNasUrl.trim());
        }
        if (typeof msg.bridgeNasToken === 'string') {
            await this._safeConfigUpdate('datacollectBridgeNasToken', msg.bridgeNasToken.trim());
        }
        if (typeof msg.savePath === 'string') {
            await this._safeConfigUpdate('datacollectSavePath', msg.savePath.trim());
        }
        if (typeof msg.crawlDepth === 'number' && Number.isFinite(msg.crawlDepth)) {
            await this._safeConfigUpdate('datacollectCrawlDepth', Math.max(0, Math.min(3, Math.floor(msg.crawlDepth))));
        }
        if (typeof msg.maxPages === 'number' && Number.isFinite(msg.maxPages)) {
            await this._safeConfigUpdate('datacollectMaxPages', Math.max(1, Math.min(20, Math.floor(msg.maxPages))));
        }
        if (typeof msg.synthesisTemperature === 'number' && Number.isFinite(msg.synthesisTemperature)) {
            await this._safeConfigUpdate('datacollectSynthesisTemperature', Math.max(0, Math.min(2, msg.synthesisTemperature)));
        }
    }

    private async _refreshState(): Promise<void> {
        if (!this._view && !this._panel) return;
        const cfg = vscode.workspace.getConfiguration('g1nation');
        const token = (await this._deps.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '';

        const profiles = (cfg.get<any[]>('brainProfiles', []) || []) as Array<{
            id?: string; name?: string; localBrainPath?: string;
        }>;
        const activeBrainId = cfg.get<string>('activeBrainId', '') || '';
        const activeProfile = profiles.find((p) => p.id === activeBrainId) || profiles[0];

        const state: SettingsState = {
            telegram: {
                hasToken: !!token.trim(),
                enabled: cfg.get<boolean>('telegram.enabled', false),
                connected: !!this._botName && this._deps.telegramBot.isRunning(),
                botName: this._botName,
                allowedChatIds: cfg.get<number[]>('telegram.allowedChatIds', []) || [],
                enrolling: this._enrolling,
                lastError: this._lastError,
                lastSuccess: this._lastSuccess,
            },
            connection: {
                ollamaUrl: cfg.get<string>('ollamaUrl', '') || '',
                defaultModel: cfg.get<string>('defaultModel', '') || '',
                requestTimeout: cfg.get<number>('requestTimeout', 300) ?? 300,
                availableModels: this._modelsCache?.models ?? [],
                modelsLoading: this._modelsLoading,
            },
            memory: {
                memoryEnabled: cfg.get<boolean>('memoryEnabled', true),
                memoryShortTermMessages: cfg.get<number>('memoryShortTermMessages', 8) ?? 8,
                memoryMediumTermSessions: cfg.get<number>('memoryMediumTermSessions', 5) ?? 5,
                memoryLongTermFiles: cfg.get<number>('memoryLongTermFiles', 6) ?? 6,
            },
            brain: {
                activeBrainId,
                activeBrainName: activeProfile?.name || '(없음)',
                activeBrainPath: activeProfile?.localBrainPath || '',
                profileCount: profiles.length,
                autoPushBrain: cfg.get<boolean>('autoPushBrain', false),
            },
            advanced: {
                dryRun: cfg.get<boolean>('dryRun', false),
                multiAgentEnabled: cfg.get<boolean>('multiAgentEnabled', false),
                maxAutoSteps: cfg.get<number>('maxAutoSteps', 50) ?? 50,
                maxContextSize: cfg.get<number>('maxContextSize', 32000) ?? 32000,
                chatTemperature: cfg.get<number>('chatTemperature', 0.3) ?? 0.3,
                chunkedSwitchTokens: cfg.get<number>('chunkedSwitchTokens', 50000) ?? 50000,
                chunkedMaxSections: cfg.get<number>('chunkedMaxSections', 3) ?? 3,
                polishPersonaOverride: cfg.get<string>('polishPersonaOverride', '') ?? '',
            },
            datacollect: {
                bridgeTarget: cfg.get<string>('datacollectBridgeTarget', 'local') || 'local',
                bridgeUrl: cfg.get<string>('datacollectBridgeUrl', '') || '',
                bridgeNasUrl: cfg.get<string>('datacollectBridgeNasUrl', '') || '',
                bridgeNasToken: cfg.get<string>('datacollectBridgeNasToken', '') || '',
                savePath: cfg.get<string>('datacollectSavePath', '') || '',
                crawlDepth: cfg.get<number>('datacollectCrawlDepth', 1) ?? 1,
                maxPages: cfg.get<number>('datacollectMaxPages', 8) ?? 8,
                synthesisTemperature: cfg.get<number>('datacollectSynthesisTemperature', 0.1) ?? 0.1,
            },
            google: this._buildGoogleState(),
            providers: await this._buildProvidersState(),
            devilAgent: { enabled: cfg.get<boolean>('devilAgent.enabled', false) },
            bannerError: this._bannerError,
        };
        const payload = { type: 'state', value: state };
        // Broadcast to whichever surface(s) are currently open.
        this._view?.webview.postMessage(payload);
        this._panel?.webview.postMessage(payload);
    }

    private _renderShell(webview: vscode.Webview): string {
        const mediaRoot = vscode.Uri.joinPath(this._deps.extensionUri, 'media');
        const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'settings-panel.css')).toString();
        const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'settings-panel.js')).toString();
        const tplPath = path.join(this._deps.extensionUri.fsPath, 'media', 'settings-panel.html');
        const tpl = fs.readFileSync(tplPath, 'utf8');
        const version = (this._deps.context?.extension?.packageJSON?.version as string) || '';
        return tpl
            .replace('__STYLES_URI__', stylesUri)
            .replace('__SCRIPT_URI__', scriptUri)
            .replace('__VERSION__', version);
    }
}
