import { LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { i18n, I18nController, isSupportedLocale } from "../i18n/index.ts";
import {
  handleChannelConfigReload as handleChannelConfigReloadInternal,
  handleChannelConfigSave as handleChannelConfigSaveInternal,
  handleNostrProfileCancel as handleNostrProfileCancelInternal,
  handleNostrProfileEdit as handleNostrProfileEditInternal,
  handleNostrProfileFieldChange as handleNostrProfileFieldChangeInternal,
  handleNostrProfileImport as handleNostrProfileImportInternal,
  handleNostrProfileSave as handleNostrProfileSaveInternal,
  handleNostrProfileToggleAdvanced as handleNostrProfileToggleAdvancedInternal,
  handleWhatsAppLogout as handleWhatsAppLogoutInternal,
  handleWhatsAppStart as handleWhatsAppStartInternal,
  handleWhatsAppWait as handleWhatsAppWaitInternal,
} from "./app-channels.ts";
import {
  handleAbortChat as handleAbortChatInternal,
  handleSendChat as handleSendChatInternal,
  removeQueuedMessage as removeQueuedMessageInternal,
} from "./app-chat.ts";
import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults.ts";
import type { EventLogEntry } from "./app-events.ts";
import { connectGateway as connectGatewayInternal } from "./app-gateway.ts";
import {
  handleConnected,
  handleDisconnected,
  handleFirstUpdated,
  handleUpdated,
} from "./app-lifecycle.ts";
import { renderApp } from "./app-render.ts";
import {
  exportLogs as exportLogsInternal,
  handleChatScroll as handleChatScrollInternal,
  handleLogsScroll as handleLogsScrollInternal,
  resetChatScroll as resetChatScrollInternal,
  scheduleChatScroll as scheduleChatScrollInternal,
} from "./app-scroll.ts";
import {
  applySettings as applySettingsInternal,
  loadCron as loadCronInternal,
  loadOverview as loadOverviewInternal,
  setTab as setTabInternal,
  setTheme as setThemeInternal,
  onPopState as onPopStateInternal,
} from "./app-settings.ts";
import {
  resetToolStream as resetToolStreamInternal,
  type ToolStreamEntry,
  type CompactionStatus,
  type FallbackStatus,
} from "./app-tool-stream.ts";
import type { AppViewState, BackgroundJobToast } from "./app-view-state.ts";
import { normalizeAssistantIdentity } from "./assistant-identity.ts";
import { loadAssistantIdentity as loadAssistantIdentityInternal } from "./controllers/assistant-identity.ts";
import type { CronFieldErrors } from "./controllers/cron.ts";
import type { DevicePairingList } from "./controllers/devices.ts";
import type { ExecApprovalRequest } from "./controllers/exec-approval.ts";
import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals.ts";
import type { SkillMessage } from "./controllers/skills.ts";
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts";
import type { Tab } from "./navigation.ts";
import { loadSettings, type UiSettings } from "./storage.ts";
import type { ResolvedTheme, ThemeMode } from "./theme.ts";
import type {
  AgentsListResult,
  AgentsFilesListResult,
  AgentIdentityResult,
  ConfigSnapshot,
  ConfigUiHints,
  CronJob,
  CronRunLogEntry,
  CronStatus,
  HealthSnapshot,
  LogEntry,
  LogLevel,
  PresenceEntry,
  ChannelsStatusSnapshot,
  SessionsListResult,
  SkillStatusReport,
  ToolsCatalogResult,
  StatusSummary,
  NostrProfile,
} from "./types.ts";
import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types.ts";
import { generateUUID } from "./uuid.ts";
import type { NostrProfileFormState } from "./views/channels.nostr-profile-form.ts";

declare global {
  interface Window {
    __OPENCLAW_CONTROL_UI_BASE_PATH__?: string;
  }
}

const bootAssistantIdentity = normalizeAssistantIdentity({});

function resolveOnboardingMode(): boolean {
  if (!window.location.search) {
    return false;
  }
  const params = new URLSearchParams(window.location.search);
  const raw = params.get("onboarding");
  if (!raw) {
    return false;
  }
  const normalized = raw.trim().toLowerCase();
  return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
}

@customElement("openclaw-app")
export class OpenClawApp extends LitElement {
  private i18nController = new I18nController(this);
  clientInstanceId = generateUUID();
  connectGeneration = 0;
  @state() settings: UiSettings = loadSettings();
  constructor() {
    super();
    if (isSupportedLocale(this.settings.locale)) {
      void i18n.setLocale(this.settings.locale);
    }
  }
  @state() password = "";
  @state() tab: Tab = "chat";
  @state() onboarding = resolveOnboardingMode();
  @state() connected = false;
  @state() theme: ThemeMode = this.settings.theme ?? "system";
  @state() themeResolved: ResolvedTheme = "dark";
  @state() hello: GatewayHelloOk | null = null;
  @state() lastError: string | null = null;
  @state() lastErrorCode: string | null = null;
  @state() eventLog: EventLogEntry[] = [];
  private eventLogBuffer: EventLogEntry[] = [];
  private toolStreamSyncTimer: number | null = null;
  private sidebarCloseTimer: number | null = null;

  @state() assistantName = bootAssistantIdentity.name;
  @state() assistantAvatar = bootAssistantIdentity.avatar;
  @state() assistantAgentId = bootAssistantIdentity.agentId ?? null;
  @state() serverVersion: string | null = null;

  @state() sessionKey = this.settings.sessionKey;
  @state() chatLoading = false;
  @state() chatSending = false;
  @state() chatMessage = "";
  @state() chatMessages: unknown[] = [];
  @state() chatToolMessages: unknown[] = [];
  @state() chatStream: string | null = null;
  @state() chatStreamStartedAt: number | null = null;
  @state() chatStreamSegments: Array<{ text: string; ts: number }> = [];
  @state() chatRunId: string | null = null;
  @state() chatAuthMode: "oauth" | "api" | "fallback" | "unknown" | null = null;
  @state() compactionStatus: CompactionStatus | null = null;
  @state() backgroundJobToasts: BackgroundJobToast[] = [];
  @state() fallbackStatus: FallbackStatus | null = null;
  @state() chatAvatarUrl: string | null = null;
  @state() chatThinkingLevel: string | null = null;
  @state() chatQueue: ChatQueueItem[] = [];
  @state() chatAttachments: ChatAttachment[] = [];
  @state() chatManualRefreshInFlight = false;
  @state() navDrawerOpen = false;
  // Sidebar state for tool output viewing
  @state() sidebarOpen = false;
  @state() sidebarContent: string | null = null;
  @state() sidebarError: string | null = null;
  @state() splitRatio = this.settings.splitRatio;

  @state() nodesLoading = false;
  @state() nodes: Array<Record<string, unknown>> = [];
  @state() devicesLoading = false;
  @state() devicesError: string | null = null;
  @state() devicesList: DevicePairingList | null = null;
  @state() execApprovalsLoading = false;
  @state() execApprovalsSaving = false;
  @state() execApprovalsDirty = false;
  @state() execApprovalsSnapshot: ExecApprovalsSnapshot | null = null;
  @state() execApprovalsForm: ExecApprovalsFile | null = null;
  @state() execApprovalsSelectedAgent: string | null = null;
  @state() execApprovalsTarget: "gateway" | "node" = "gateway";
  @state() execApprovalsTargetNodeId: string | null = null;
  @state() execApprovalQueue: ExecApprovalRequest[] = [];
  @state() execApprovalBusy = false;
  @state() execApprovalError: string | null = null;
  @state() pendingGatewayUrl: string | null = null;

  @state() configLoading = false;
  @state() configRaw = "{\n}\n";
  @state() configRawOriginal = "";
  @state() configValid: boolean | null = null;
  @state() configIssues: unknown[] = [];
  @state() configSaving = false;
  @state() configApplying = false;
  @state() updateRunning = false;
  @state() updateInProgress = false;
  @state() applySessionKey = this.settings.lastActiveSessionKey;
  @state() configSnapshot: ConfigSnapshot | null = null;
  @state() configSchema: unknown = null;
  @state() configSchemaVersion: string | null = null;
  @state() configSchemaLoading = false;
  @state() configUiHints: ConfigUiHints = {};
  @state() configForm: Record<string, unknown> | null = null;
  @state() configFormOriginal: Record<string, unknown> | null = null;
  @state() configFormDirty = false;
  @state() configFormMode: "form" | "raw" = "form";
  @state() configSearchQuery = "";
  @state() configActiveSection: string | null = null;
  @state() configActiveSubsection: string | null = null;

  @state() channelsLoading = false;
  @state() channelsSnapshot: ChannelsStatusSnapshot | null = null;
  @state() channelsError: string | null = null;
  @state() channelsLastSuccess: number | null = null;
  @state() whatsappLoginMessage: string | null = null;
  @state() whatsappLoginQrDataUrl: string | null = null;
  @state() whatsappLoginConnected: boolean | null = null;
  @state() whatsappBusy = false;
  @state() nostrProfileFormState: NostrProfileFormState | null = null;
  @state() nostrProfileAccountId: string | null = null;

  @state() presenceLoading = false;
  @state() presenceEntries: PresenceEntry[] = [];
  @state() presenceError: string | null = null;
  @state() presenceStatus: string | null = null;

  @state() agentsLoading = false;
  @state() agentsList: AgentsListResult | null = null;
  @state() agentsError: string | null = null;
  @state() agentsSelectedId: string | null = null;
  @state() agentsAvailableModels: Array<{
    id: string;
    name?: string;
    provider: string;
    contextWindow?: number;
  }> = [];
  @state() toolsCatalogLoading = false;
  @state() toolsCatalogError: string | null = null;
  @state() toolsCatalogResult: ToolsCatalogResult | null = null;
  @state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" =
    "overview";
  @state() agentToolsSubTab: "core" | "pipedream" | "zapier" = "core";
  @state() agentPipedreamState: import("./views/agents-panel-pipedream.ts").AgentPipedreamState = {
    loading: false,
    error: null,
    success: null,
    configured: false,
    globalConfigured: false,
    externalUserId: "",
    enabledApps: [],
    connectedApps: [],
    saving: false,
    editingUserId: false,
    draftUserId: "",
    showAppBrowser: false,
    appBrowserSearch: "",
    connectingApp: null,
    testingApp: null,
    disconnectingApp: null,
    manualSlug: "",
    expandedApps: new Set(),
  };
  @state() agentZapierState: import("./views/agents-panel-zapier.ts").AgentZapierState = {
    loading: false,
    error: null,
    configured: false,
    globalConfigured: false,
    mcpUrl: "",
    enabledTools: [],
    availableTools: [],
    saving: false,
    testing: false,
    testResult: null,
    editingUrl: false,
    draftUrl: "",
    toolsLoading: false,
  };
  @state() agentFilesLoading = false;
  @state() agentFilesError: string | null = null;
  @state() agentFilesList: AgentsFilesListResult | null = null;
  @state() agentFileContents: Record<string, string> = {};
  @state() agentFileDrafts: Record<string, string> = {};
  @state() agentFileActive: string | null = null;
  @state() agentFileSaving = false;
  @state() agentIdentityLoading = false;
  @state() agentIdentityError: string | null = null;
  @state() agentIdentityById: Record<string, AgentIdentityResult> = {};
  @state() agentSkillsLoading = false;
  @state() agentSkillsError: string | null = null;
  @state() showCreateForm = false;
  @state() createMode: "manual" | "wizard" = "manual";
  @state() createName = "";
  @state() createWorkspace = "";
  @state() createEmoji = "";
  @state() creating = false;
  @state() createError: string | null = null;
  @state() wizardDescription = "";
  @state() wizardLoading = false;
  @state() wizardResult: { name: string; emoji: string; soul: string } | null = null;
  @state() editAgentName = "";
  @state() editAgentWorkspace = "";
  @state() editAgentEmoji = "";
  @state() editAgentDirty = false;
  @state() editAgentSaving = false;
  @state() editAgentError: string | null = null;
  @state() agentAvatarTheme = "professional";
  @state() agentAvatarInstructions = "";
  @state() agentAvatarBusy = false;
  @state() agentAvatarStatus: string | null = null;
  @state() agentAvatarError: string | null = null;
  @state() agentAvatarPreviewUrl: string | null = null;
  @state() confirmDeleteAgentId: string | null = null;
  @state() deleteAgentInProgress = false;
  @state() deleteAgentError: string | null = null;
  @state() agentSkillsReport: SkillStatusReport | null = null;
  @state() agentSkillsAgentId: string | null = null;
  @state() teamsLoading = false;
  @state() teamsError: string | null = null;
  @state() teamsResult: import("./types.ts").TeamsListResult | null = null;
  @state() teamsSelectedId: string | null = null;
  @state() teamsEditName = "";
  @state() teamsEditDescription = "";
  @state() teamsEditParentId: string | null = null;
  @state() teamsEditAgentIds: string[] = [];
  @state() teamsDirty = false;
  @state() teamsSaving = false;
  @state() teamsDeleteInProgress = false;
  @state() teamsDeleteError: string | null = null;
  @state() teamsCollapsedIds: string[] = [];

  @state() sessionsLoading = false;
  @state() sessionsResult: SessionsListResult | null = null;
  @state() sessionsError: string | null = null;
  @state() sessionsFilterActive = "";
  @state() sessionsFilterLimit = "120";
  @state() sessionsIncludeGlobal = true;
  @state() sessionsIncludeUnknown = false;
  @state() sessionsAgentFilter = "";
  @state() sessionsHideCron = true;
  // Background Sessions Panel
  @state() bgSessionsPanelOpen = false;
  @state() bgSessionsList: Array<{
    key: string;
    sessionId: string;
    label: string;
    updatedAt: number;
    running: boolean;
  }> | null = null;
  @state() bgSessionsLoading = false;
  @state() bgSessionsSelectedKey: string | null = null;
  @state() bgSessionsHistory: Array<{
    role: string;
    text: string;
    timestamp?: number;
    toolName?: string;
  }> | null = null;
  @state() bgSessionsHistoryLoading = false;
  @state() bgSessionsInput = "";
  @state() bgSessionsSending = false;

  // Team Chat Drawer
  @state() teamChatOpen = false;
  @state() teamChatProject: unknown = null;
  @state() teamChatSessions: unknown[] = [];
  @state() teamChatActivity: unknown[] = [];
  @state() teamChatLoading = false;
  @state() teamChatInput = "";
  @state() teamChatTasksCollapsed = false;

  @state() recentArchivedSessions: Array<{
    sessionId: string;
    sessionKey: string;
    displayName?: string;
    firstMessage?: string;
    updatedAt: number;
  }> | null = null;
  @state() archivedSessionsLoading = false;
  @state() archivedSessionsResult:
    | import("./controllers/sessions.ts").ArchivedSessionsResult
    | null = null;
  @state() archivedSessionsError: string | null = null;
  @state() archivedSessionsSearch = "";
  @state() liveSessionsPage = 1;
  @state() liveSessionsPageSize = 10;
  @state() archivedSessionsPage = 1;
  @state() archivedSessionsPageSize = 10;

  // Session history modal state
  @state() sessionHistoryOpen = false;
  @state() sessionHistoryKey = "";
  @state() sessionHistoryAgentFilter = "";
  @state() sessionHistorySearch = "";
  @state() sessionHistoryRoleFilter = "all";
  @state() sessionHistoryLoading = false;
  @state() sessionHistoryError: string | null = null;
  @state() sessionHistoryResult:
    | import("./views/sessions-history-modal.ts").SessionHistoryResult
    | null = null;

  @state() usageLoading = false;
  @state() usageResult: import("./types.js").SessionsUsageResult | null = null;
  @state() usageCostSummary: import("./types.js").CostUsageSummary | null = null;
  @state() usageError: string | null = null;
  @state() usageStartDate = (() => {
    const d = new Date();
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
  })();
  @state() usageEndDate = (() => {
    const d = new Date();
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
  })();
  @state() usageSelectedSessions: string[] = [];
  @state() usageSelectedDays: string[] = [];
  @state() usageSelectedHours: number[] = [];
  @state() usageChartMode: "tokens" | "cost" = "tokens";
  @state() usageDailyChartMode: "total" | "by-type" = "by-type";
  @state() usageTimeSeriesMode: "cumulative" | "per-turn" = "per-turn";
  @state() usageTimeSeriesBreakdownMode: "total" | "by-type" = "by-type";
  @state() usageTimeSeries: import("./types.js").SessionUsageTimeSeries | null = null;
  @state() usageTimeSeriesLoading = false;
  @state() usageTimeSeriesCursorStart: number | null = null;
  @state() usageTimeSeriesCursorEnd: number | null = null;
  @state() usageSessionLogs: import("./views/usage.js").SessionLogEntry[] | null = null;
  @state() usageSessionLogsLoading = false;
  @state() usageSessionLogsExpanded = false;
  // Applied query (used to filter the already-loaded sessions list client-side).
  @state() usageQuery = "";
  // Draft query text (updates immediately as the user types; applied via debounce or "Search").
  @state() usageQueryDraft = "";
  @state() usageSessionSort: "tokens" | "cost" | "recent" | "messages" | "errors" = "recent";
  @state() usageSessionSortDir: "desc" | "asc" = "desc";
  @state() usageRecentSessions: string[] = [];
  @state() usageTimeZone: "local" | "utc" = "local";
  @state() usageContextExpanded = false;
  @state() usageHeaderPinned = false;
  @state() usageSessionsTab: "all" | "recent" = "all";
  @state() usageVisibleColumns: string[] = [
    "channel",
    "agent",
    "provider",
    "model",
    "messages",
    "tools",
    "errors",
    "duration",
  ];
  @state() usageLogFilterRoles: import("./views/usage.js").SessionLogRole[] = [];
  @state() usageLogFilterTools: string[] = [];
  @state() usageLogFilterHasTools = false;
  @state() usageLogFilterQuery = "";

  // Non-reactive (don’t trigger renders just for timer bookkeeping).
  usageQueryDebounceTimer: number | null = null;

  @state() cronLoading = false;
  @state() cronJobsLoadingMore = false;
  @state() cronJobs: CronJob[] = [];
  @state() cronJobsTotal = 0;
  @state() cronJobsHasMore = false;
  @state() cronJobsNextOffset: number | null = null;
  @state() cronJobsLimit = 50;
  @state() cronJobsQuery = "";
  @state() cronJobsEnabledFilter: import("./types.js").CronJobsEnabledFilter = "all";
  @state() cronJobsScheduleKindFilter: import("./controllers/cron.js").CronJobsScheduleKindFilter =
    "all";
  @state() cronJobsLastStatusFilter: import("./controllers/cron.js").CronJobsLastStatusFilter =
    "all";
  @state() cronJobsSortBy: import("./types.js").CronJobsSortBy = "nextRunAtMs";
  @state() cronJobsSortDir: import("./types.js").CronSortDir = "asc";
  @state() cronStatus: CronStatus | null = null;
  @state() cronError: string | null = null;
  @state() cronForm: CronFormState = { ...DEFAULT_CRON_FORM };
  @state() cronFieldErrors: CronFieldErrors = {};
  @state() cronEditingJobId: string | null = null;
  @state() cronRunsJobId: string | null = null;
  @state() cronRunsLoadingMore = false;
  @state() cronRuns: CronRunLogEntry[] = [];
  @state() cronRunsTotal = 0;
  @state() cronRunsHasMore = false;
  @state() cronRunsNextOffset: number | null = null;
  @state() cronRunsLimit = 50;
  @state() cronRunsScope: import("./types.js").CronRunScope = "all";
  @state() cronRunsStatuses: import("./types.js").CronRunsStatusValue[] = [];
  @state() cronRunsDeliveryStatuses: import("./types.js").CronDeliveryStatus[] = [];
  @state() cronRunsStatusFilter: import("./types.js").CronRunsStatusFilter = "all";
  @state() cronRunsQuery = "";
  @state() cronRunsSortDir: import("./types.js").CronSortDir = "desc";
  @state() cronModelSuggestions: string[] = [];
  @state() cronBusy = false;

  @state() updateAvailable: import("./types.js").UpdateAvailable | null = null;
  @state() upstreamDivergence: {
    behind: number;
    ahead: number;
    upstreamRef: string;
    localRef: string;
    error?: string;
  } | null = null;
  @state() updateModalState: "closed" | "confirm" | "checking" | "result" = "closed";
  @state() mergeModel: string = "";
  @state() safeMergeCronEnabled: boolean | null = null; // null = not yet loaded
  @state() safeMergeCronJobId: string | null = null;

  @state() skillsLoading = false;
  @state() skillsReport: SkillStatusReport | null = null;
  @state() skillsError: string | null = null;
  @state() skillsFilter = "";
  @state() skillEdits: Record<string, string> = {};
  @state() skillsBusyKey: string | null = null;
  @state() skillMessages: Record<string, SkillMessage> = {};
  @state() vaultKeys: import("./controllers/skills.ts").VaultKeyEntry[] = [];
  @state() vaultKeysLoading = false;
  @state() skillAddingKey: Record<string, boolean> = {};
  @state() skillNewKeyName: Record<string, string> = {};
  @state() skillNewKeyValue: Record<string, string> = {};

  @state() debugLoading = false;
  @state() debugStatus: StatusSummary | null = null;
  @state() debugHealth: HealthSnapshot | null = null;
  @state() debugModels: unknown[] = [];
  @state() debugHeartbeat: unknown = null;
  @state() debugCallMethod = "";
  @state() debugCallParams = "{}";
  @state() debugCallResult: string | null = null;
  @state() debugCallError: string | null = null;

  @state() logsLoading = false;
  @state() logsError: string | null = null;
  @state() logsFile: string | null = null;
  @state() logsEntries: LogEntry[] = [];
  @state() logsFilterText = "";
  @state() logsLevelFilters: Record<LogLevel, boolean> = {
    ...DEFAULT_LOG_LEVEL_FILTERS,
  };
  @state() logsAutoFollow = true;
  @state() logsTruncated = false;
  @state() logsCursor: number | null = null;
  @state() logsLastFetchAt: number | null = null;
  @state() logsLimit = 500;
  @state() logsMaxBytes = 250_000;
  @state() logsAtBottom = true;

  // Mode (Agent Loop Mode) state
  @state() modeLoading = false;
  @state() modeCurrentMode: "core" | "enhanced" = "core";
  @state() modeConfig: import("./views/mode.ts").EnhancedLoopConfig | null = null;
  @state() modeSaving = false;
  @state() modeError: string | null = null;
  @state() modeSuccess: string | null = null;
  @state() modeAvailableModels: import("./views/mode.ts").ModelInfo[] = [];

  // 1Password state
  @state() onePasswordLoading = false;
  @state() onePasswordMode: "cli" | "connect" | "unknown" = "unknown";
  @state() onePasswordStatus: {
    installed?: boolean;
    signedIn?: boolean;
    connected?: boolean;
    account?: string;
    email?: string;
    host?: string;
    error?: string;
  } = {};
  @state() onePasswordError: string | null = null;
  @state() onePasswordSigningIn = false;

  // Discord state
  @state() discordLoading = false;
  @state() discordError: string | null = null;
  @state() discordStatus: {
    connected: boolean;
    configured: boolean;
    token?: string;
    user?: { id: string; username: string; discriminator: string; avatar: string | null };
    error?: string;
  } | null = null;
  @state() discordHealth: {
    checks: Array<{
      check: string;
      status: "pass" | "fail" | "warn";
      message: string;
      details?: unknown;
    }>;
    healthy: boolean;
  } | null = null;
  @state() discordGuilds: Array<{
    id: string;
    name: string;
    icon: string | null;
    memberCount?: number;
  }> = [];
  @state() discordInviteUrl: string | null = null;
  @state() discordTestTokenInput = "";
  @state() discordSavingToken = false;
  @state() discordSaveTokenError: string | null = null;
  @state() discordSaveTokenSuccess = false;
  // Discord voice settings
  @state() discordVoiceConfig: {
    enabled: boolean;
    followUser: string;
    triggerChannels: string[];
    transcribeEngine: "whisper-api" | "whisper-local";
    transcribeLanguage: string;
    transcribeUsers: string[];
    responseMode: "text" | "voice" | "both";
    ttsProvider: "openai" | "elevenlabs" | "edge";
    ttsVoice: string;
  } = {
    enabled: false,
    followUser: "",
    triggerChannels: [],
    transcribeEngine: "whisper-api",
    transcribeLanguage: "en",
    transcribeUsers: [],
    responseMode: "both",
    ttsProvider: "elevenlabs",
    ttsVoice: "",
  };
  @state() discordVoiceSaving = false;
  @state() discordVoiceSaveError: string | null = null;
  @state() discordVoiceSaveSuccess = false;
  @state() discordVoiceRuntimeStatus: {
    available: boolean;
    responseEnabled: boolean;
    connectedGuilds: string[];
  } | null = null;
  @state() discordVoiceResponseToggling = false;
  @state() discordAllowFrom = "";
  @state() discordActivationPhrase = "";
  @state() discordSafeguardsSaving = false;
  @state() discordSafeguardsSaveError: string | null = null;
  @state() discordSafeguardsSaveSuccess = false;
  @state() discordPairingRequests: Array<{
    id: string;
    code: string;
    createdAt: string;
    meta?: Record<string, string>;
  }> = [];
  @state() discordPairingCodeInput = "";
  @state() discordPairingApproving = false;
  @state() discordPairingError: string | null = null;
  @state() discordPairingSuccess: string | null = null;

  // API Keys state
  @state() apikeysLoading = false;
  @state() apikeysError: string | null = null;
  @state() apikeysKeys: import("./controllers/apikeys.ts").DiscoveredKey[] = [];
  @state() apikeysEdits: Record<string, string> = {};
  @state() apikeysEditing: Record<string, boolean> = {};
  @state() apikeysBusyKey: string | null = null;
  @state() apikeysMessage: { kind: "success" | "error"; text: string } | null = null;
  @state() apikeysConfigHash: string | null = null;
  @state() apikeysRefreshSuccess = false;
  @state() apikeysVaultStatus: import("./controllers/apikeys.ts").VaultStatus | null = null;
  @state() apikeysMigrating = false;
  @state() authProfiles: import("./controllers/apikeys.ts").AuthProfile[] = [];
  @state() authProfilesLoading = false;
  @state() addSecretShow = false;
  @state() addSecretName = "";
  @state() addSecretValue = "";
  @state() addSecretBusy = false;
  @state() restartNeeded = false;
  @state() vaultAllKeys: { id: string; masked: string }[] = [];

  // AI Providers tab state
  @state() aiProviders: import("./controllers/ai-providers.ts").AiProvidersState["aiProviders"] =
    [];
  @state() aiProvidersLoading = false;
  @state() aiProvidersConnecting: string | null = null;
  @state() aiProvidersMessage: { kind: "success" | "error"; text: string } | null = null;
  @state() aiProvidersRestartNeeded = false;

  // Pipedream state (single object for functional setState pattern)
  @state() pipedreamState: import("./views/pipedream.ts").PipedreamState = {
    loading: false,
    configured: false,
    credentials: { clientId: "", clientSecret: "", projectId: "", environment: "development" },
    showCredentialsForm: false,
    connectedApps: [],
    availableApps: [],
    error: null,
    success: null,
    testingApp: null,
    connectingApp: null,
    refreshingApp: null,
    externalUserId: "koda",
    showAppBrowser: false,
    appBrowserSearch: "",
    allApps: [],
    loadingApps: false,
    manualSlug: "",
    agentSummaries: [],
  };

  // Zapier state
  @state() zapierState: import("./views/zapier.ts").ZapierState = {
    loading: false,
    configured: false,
    mcpUrl: "",
    showForm: false,
    error: null,
    success: null,
    testing: false,
    tools: [],
    expandedGroups: new Set(),
    agentSummaries: [],
  };

  @state() hostingerState: import("./views/hostinger.ts").HostingerState = {
    loading: false,
    configured: false,
    apiToken: "",
    githubRepo: "",
    showForm: false,
    error: null,
    success: null,
    tools: [],
    expandedGroups: new Set(),
  };

  client: GatewayBrowserClient | null = null;
  private chatScrollFrame: number | null = null;
  private chatScrollTimeout: number | null = null;
  private chatHasAutoScrolled = false;
  private chatUserNearBottom = true;
  @state() chatNewMessagesBelow = false;
  private nodesPollInterval: number | null = null;
  private logsPollInterval: number | null = null;
  private debugPollInterval: number | null = null;
  private logsScrollFrame: number | null = null;
  private toolStreamById = new Map<string, ToolStreamEntry>();
  private toolStreamOrder: string[] = [];
  refreshSessionsAfterChat = new Set<string>();
  resetSessionAfterChat = new Set<string>();
  basePath = "";
  private popStateHandler = () =>
    onPopStateInternal(this as unknown as Parameters<typeof onPopStateInternal>[0]);
  private themeMedia: MediaQueryList | null = null;
  private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null;
  private topbarObserver: ResizeObserver | null = null;

  createRenderRoot() {
    return this;
  }

  connectedCallback() {
    super.connectedCallback();
    handleConnected(this as unknown as Parameters<typeof handleConnected>[0]);
  }

  protected firstUpdated() {
    handleFirstUpdated(this as unknown as Parameters<typeof handleFirstUpdated>[0]);
  }

  disconnectedCallback() {
    handleDisconnected(this as unknown as Parameters<typeof handleDisconnected>[0]);
    super.disconnectedCallback();
  }

  protected updated(changed: Map<PropertyKey, unknown>) {
    handleUpdated(this as unknown as Parameters<typeof handleUpdated>[0], changed);
  }

  connect() {
    connectGatewayInternal(this as unknown as Parameters<typeof connectGatewayInternal>[0]);
  }

  handleChatScroll(event: Event) {
    handleChatScrollInternal(
      this as unknown as Parameters<typeof handleChatScrollInternal>[0],
      event,
    );
  }

  handleLogsScroll(event: Event) {
    handleLogsScrollInternal(
      this as unknown as Parameters<typeof handleLogsScrollInternal>[0],
      event,
    );
  }

  exportLogs(lines: string[], label: string) {
    exportLogsInternal(lines, label);
  }

  resetToolStream() {
    resetToolStreamInternal(this as unknown as Parameters<typeof resetToolStreamInternal>[0]);
  }

  resetChatScroll() {
    resetChatScrollInternal(this as unknown as Parameters<typeof resetChatScrollInternal>[0]);
  }

  scrollToBottom(opts?: { smooth?: boolean }) {
    resetChatScrollInternal(this as unknown as Parameters<typeof resetChatScrollInternal>[0]);
    scheduleChatScrollInternal(
      this as unknown as Parameters<typeof scheduleChatScrollInternal>[0],
      true,
      Boolean(opts?.smooth),
    );
  }

  async loadAssistantIdentity() {
    await loadAssistantIdentityInternal(this);
  }

  applySettings(next: UiSettings) {
    applySettingsInternal(this as unknown as Parameters<typeof applySettingsInternal>[0], next);
  }

  setTab(next: Tab) {
    setTabInternal(this as unknown as Parameters<typeof setTabInternal>[0], next);
  }

  setTheme(next: ThemeMode, context?: Parameters<typeof setThemeInternal>[2]) {
    setThemeInternal(this as unknown as Parameters<typeof setThemeInternal>[0], next, context);
  }

  async loadOverview() {
    await loadOverviewInternal(this as unknown as Parameters<typeof loadOverviewInternal>[0]);
  }

  async loadCron() {
    await loadCronInternal(this as unknown as Parameters<typeof loadCronInternal>[0]);
  }

  async handleAbortChat() {
    await handleAbortChatInternal(this as unknown as Parameters<typeof handleAbortChatInternal>[0]);
  }

  removeQueuedMessage(id: string) {
    removeQueuedMessageInternal(
      this as unknown as Parameters<typeof removeQueuedMessageInternal>[0],
      id,
    );
  }

  async handleSendChat(
    messageOverride?: string,
    opts?: Parameters<typeof handleSendChatInternal>[2],
  ) {
    await handleSendChatInternal(
      this as unknown as Parameters<typeof handleSendChatInternal>[0],
      messageOverride,
      opts,
    );
  }

  async handleWhatsAppStart(force: boolean) {
    await handleWhatsAppStartInternal(this, force);
  }

  async handleWhatsAppWait() {
    await handleWhatsAppWaitInternal(this);
  }

  async handleWhatsAppLogout() {
    await handleWhatsAppLogoutInternal(this);
  }

  async handleChannelConfigSave() {
    await handleChannelConfigSaveInternal(this);
  }

  async handleChannelConfigReload() {
    await handleChannelConfigReloadInternal(this);
  }

  handleNostrProfileEdit(accountId: string, profile: NostrProfile | null) {
    handleNostrProfileEditInternal(this, accountId, profile);
  }

  handleNostrProfileCancel() {
    handleNostrProfileCancelInternal(this);
  }

  handleNostrProfileFieldChange(field: keyof NostrProfile, value: string) {
    handleNostrProfileFieldChangeInternal(this, field, value);
  }

  async handleNostrProfileSave() {
    await handleNostrProfileSaveInternal(this);
  }

  async handleNostrProfileImport() {
    await handleNostrProfileImportInternal(this);
  }

  handleNostrProfileToggleAdvanced() {
    handleNostrProfileToggleAdvancedInternal(this);
  }

  async handleExecApprovalDecision(decision: "allow-once" | "allow-always" | "deny") {
    const active = this.execApprovalQueue[0];
    if (!active || !this.client || this.execApprovalBusy) {
      return;
    }
    this.execApprovalBusy = true;
    this.execApprovalError = null;
    try {
      await this.client.request("exec.approval.resolve", {
        id: active.id,
        decision,
      });
      this.execApprovalQueue = this.execApprovalQueue.filter((entry) => entry.id !== active.id);
    } catch (err) {
      this.execApprovalError = `Exec approval failed: ${String(err)}`;
    } finally {
      this.execApprovalBusy = false;
    }
  }

  handleGatewayUrlConfirm() {
    const nextGatewayUrl = this.pendingGatewayUrl;
    if (!nextGatewayUrl) {
      return;
    }
    this.pendingGatewayUrl = null;
    applySettingsInternal(this as unknown as Parameters<typeof applySettingsInternal>[0], {
      ...this.settings,
      gatewayUrl: nextGatewayUrl,
    });
    this.connect();
  }

  handleGatewayUrlCancel() {
    this.pendingGatewayUrl = null;
  }

  // Sidebar handlers for tool output viewing
  handleOpenSidebar(content: string) {
    if (this.sidebarCloseTimer != null) {
      window.clearTimeout(this.sidebarCloseTimer);
      this.sidebarCloseTimer = null;
    }
    this.sidebarContent = content;
    this.sidebarError = null;
    this.sidebarOpen = true;
  }

  handleCloseSidebar() {
    this.sidebarOpen = false;
    // Clear content after transition
    if (this.sidebarCloseTimer != null) {
      window.clearTimeout(this.sidebarCloseTimer);
    }
    this.sidebarCloseTimer = window.setTimeout(() => {
      if (this.sidebarOpen) {
        return;
      }
      this.sidebarContent = null;
      this.sidebarError = null;
      this.sidebarCloseTimer = null;
    }, 200);
  }

  handleSplitRatioChange(ratio: number) {
    const newRatio = Math.max(0.4, Math.min(0.7, ratio));
    this.splitRatio = newRatio;
    this.applySettings({ ...this.settings, splitRatio: newRatio });
  }

  render() {
    return renderApp(this as unknown as AppViewState);
  }
}
