// ABOUTME: Main application component with project-centric thread-based layout.
// ABOUTME: Initializes auth, settings, wallet, and renders AppShell with global modals.

import {
  createEffect,
  ErrorBoundary,
  onCleanup,
  onMount,
  Show,
  untrack,
} from "solid-js";
import { AboutDialog } from "@/components/common/AboutDialog";
import { LowBalanceModal } from "@/components/common/LowBalanceWarning";
import { OrganizationOtpModal } from "@/components/common/OrganizationOtpModal";
import { GatewayToolApproval } from "@/components/gateway/GatewayToolApproval";
import { AppShell } from "@/components/layout/AppShell";
import { X402PaymentApproval } from "@/components/mcp/X402PaymentApproval";
import { ShellApproval } from "@/components/shell/ShellApproval";
import { DailyClaimPopup } from "@/components/wallet/DailyClaimPopup";
import {
  connectLocalProviderRuntime,
  disconnectLocalProviderRuntime,
  listenForRuntimeRestart,
} from "@/lib/browser-local-runtime";
import { getRuntimeConfig } from "@/lib/runtime";
import { shortcuts } from "@/lib/shortcuts";
import { captureUnknownError } from "@/lib/support/hook";
import { Phase3Playground } from "@/playground/Phase3Playground";
import { initAutoTopUp } from "@/services/autoTopUp";
import { syncMemories } from "@/services/memory";
import { resetUserSessionState } from "@/services/session-state";
import { telemetry } from "@/services/telemetry";
import { agentStore } from "@/stores/agent.store";
import {
  authStore,
  checkAuth,
  initAuthRuntimeBindings,
  logout,
  setAuthenticated,
} from "@/stores/auth.store";
import { autocompleteStore } from "@/stores/autocomplete.store";
import { chatStore } from "@/stores/chat.store";
import { fileTreeState, initDefaultRootIfNeeded } from "@/stores/fileTree";
import { providerStore } from "@/stores/provider.store";
import { loadAllSettings, settingsState } from "@/stores/settings.store";
import { skillsStore } from "@/stores/skills.store";
import { threadStore } from "@/stores/thread.store";
import {
  checkDailyClaim,
  startAutoRefresh,
  startDailyClaimPolling,
  stopAutoRefresh,
} from "@/stores/wallet.store";
import "./styles.css";

// Initialize telemetry early to capture startup errors
telemetry.init();

function App() {
  if (shouldRenderPhase3Playground()) {
    return <Phase3Playground />;
  }

  const runtime = getRuntimeConfig();

  onMount(async () => {
    await initAuthRuntimeBindings();
    await checkAuth();

    if (
      runtime.capabilities.agents &&
      !authStore.privateChatPolicy?.disable_local_agents &&
      (runtime.mode === "browser-local" || runtime.mode === "desktop-native")
    ) {
      try {
        await connectLocalProviderRuntime();
        void listenForRuntimeRestart();
      } catch (error) {
        console.error(
          "[App] Failed to connect to local provider runtime:",
          error,
        );
      }
    }

    if (runtime.capabilities.updater) {
      const { updaterStore } = await import("@/stores/updater.store");
      void updaterStore.initUpdater();
    }

    // Load all settings including app settings (chatDefaultModel, etc.) and MCP settings
    await loadAllSettings();

    // Load provider settings - this restores the last used model from previous session
    await providerStore.loadSettings();

    // Sync chatStore with the active model from provider store
    chatStore.setModel(providerStore.activeModel);

    // Initialize keyboard shortcuts
    shortcuts.init();

    // Set default project root if none is open
    if (runtime.capabilities.localFiles) {
      await initDefaultRootIfNeeded();
    }

    // Detect available agents (Claude, Codex)
    if (runtime.capabilities.agents) {
      if (!authStore.privateChatPolicy?.disable_local_agents) {
        await agentStore.initialize();
      }
    }

    // Load skills and threads after auth check completes
    await skillsStore.refresh();
    await threadStore.refresh();

    // Claude Code auto-memory interceptor — does NOT run at boot. The
    // reactive createEffect below waits until the user is authenticated
    // AND has an active SerenDB project, then attempts to start the
    // watcher. Not-logged-in at boot is a normal state, not an error, and
    // must not raise a dialog. Actual failures (cloud errors, migration
    // failures) do raise a dialog because they mean something broke.
  });

  // Periodically refresh available skills so newly published skills appear without restart.
  // Base interval: 5 min + up to 30s jitter.
  // Skip the refresh when any agent session is actively prompting — the 4+ network
  // requests and large setState batch compound the already-saturated event stream
  // from high-velocity tool-call chains, making the UI feel unresponsive. #1531.
  const SKILLS_REFRESH_BASE = 5 * 60 * 1000;
  const skillsRefreshTimer = setInterval(
    () => {
      const session = agentStore.activeSession;
      if (session?.info.status === "prompting") return;
      void skillsStore.refresh();
    },
    SKILLS_REFRESH_BASE + Math.random() * 30 * 1000,
  );

  onCleanup(() => {
    clearInterval(skillsRefreshTimer);
    shortcuts.destroy();
    if (
      runtime.capabilities.agents &&
      !authStore.privateChatPolicy?.disable_local_agents &&
      (runtime.mode === "browser-local" || runtime.mode === "desktop-native")
    ) {
      disconnectLocalProviderRuntime();
    }
  });

  // Reload installed skill inventory when project root changes.
  createEffect((prevRoot: string | null | undefined) => {
    const root = fileTreeState.rootPath;
    if (root === prevRoot) return root;
    void skillsStore.refreshInstalled();
    void skillsStore.loadProjectConfig(root);
    return root;
  }, fileTreeState.rootPath);

  // Keep thread-level skill override cache in sync with selected thread.
  createEffect(() => {
    void skillsStore.ensureContextLoaded(
      fileTreeState.rootPath,
      threadStore.activeThreadId,
    );
  });

  // Store cleanup function for auto top-up
  let cleanupAutoTopUp: (() => void) | null = null;

  // Initialize wallet and AI features when authenticated
  createEffect((prev) => {
    const isAuth = authStore.isAuthenticated;

    // Only run if auth state actually changed
    if (isAuth === prev) return isAuth;

    if (isAuth) {
      console.log("[App] User authenticated, starting services...");

      // Use untrack to prevent reactive dependencies
      untrack(() => {
        startAutoRefresh();
        autocompleteStore.enable();
        // Store cleanup to prevent effect accumulation
        cleanupAutoTopUp = initAutoTopUp();
        checkDailyClaim();
        startDailyClaimPolling();
        // Push any locally-cached memories that failed to reach cloud (e.g. cold start)
        void syncMemories();
        void threadStore.refresh();
        // Re-initialize the agent runtime side-channel listeners after a
        // re-login: `resetUserSessionState()` disposes them on logout so
        // stale handlers can't fire against the previous session, but
        // the listeners are machine-local (provider-runtime ready/
        // restarted, CLI scan rejection, synthetic schema drift) and need
        // to be re-installed so post-login events are picked up without
        // an app reload. `initialize()` is idempotent and skipped on the
        // initial mount (handled in the await block on first auth).
        if (
          prev === false &&
          runtime.capabilities.agents &&
          !authStore.privateChatPolicy?.disable_local_agents
        ) {
          void agentStore.initialize();
        }
      });
    } else {
      console.log("[App] User logged out, stopping services...");
      untrack(() => {
        // Clean up auto top-up effect
        if (cleanupAutoTopUp) {
          cleanupAutoTopUp();
          cleanupAutoTopUp = null;
        }
        stopAutoRefresh();
        resetUserSessionState();
      });
    }

    return isAuth;
  }, authStore.isAuthenticated);

  let historySyncStartedForAuth: string | null = null;
  createEffect(() => {
    const isAuth = authStore.isAuthenticated;
    const settingsLoaded = !settingsState.isLoading;
    const enabled = settingsState.app.historySyncEnabled;

    untrack(async () => {
      const { startHistorySync, stopHistorySync } = await import(
        "@/services/historySync"
      );
      if (!settingsLoaded || !isAuth || !enabled) {
        historySyncStartedForAuth = null;
        stopHistorySync();
        return;
      }
      const key = "auth-session";
      if (historySyncStartedForAuth === key) return;
      historySyncStartedForAuth = key;
      startHistorySync();
    });
  });

  // Claude Code auto-memory interceptor — start only after the user is
  // authenticated. The interceptor auto-provisions its own SerenDB project
  // (`claude-agent-prefs`) and database (`claude_agent_prefs`) on first
  // run via `ensureClaudeMemoryProvisioned()`, so we do NOT need
  // `projectStore.activeProject` to be set. Not-logged-in at boot is a
  // normal state, not an error, so we do NOT raise a dialog until a real
  // failure happens (cloud error, provisioning failure, migration failure).
  // A guard variable ensures we only start once per authenticated session.
  let claudeMemoryStartedForAuth: string | null = null;
  createEffect(() => {
    const isAuth = authStore.isAuthenticated;
    // Track the dependency explicitly so createEffect re-runs when auth changes.
    void isAuth;

    untrack(async () => {
      try {
        const { settingsStore } = await import("@/stores/settings.store");
        if (!settingsStore.get("claudeMemoryInterceptEnabled")) {
          claudeMemoryStartedForAuth = null;
          return;
        }
        if (!isAuth) {
          // Normal "not authenticated yet" state — no dialog, just debug log.
          console.debug(
            "[ClaudeMemory] waiting for SerenDB login before starting interceptor",
          );
          claudeMemoryStartedForAuth = null;
          return;
        }
        // Only start once per authenticated session.
        const key = "auth-session";
        if (claudeMemoryStartedForAuth === key) {
          return;
        }
        claudeMemoryStartedForAuth = key;

        const { startClaudeMemoryInterceptor, migrateExistingClaudeMemory } =
          await import("@/services/claudeMemory");
        const { message: showMessageDialog } = await import(
          "@tauri-apps/plugin-dialog"
        );
        // Informational notice, not a panic. The interceptor runs in the
        // background and the user can always fix the underlying issue from
        // Settings — there is no need for a red error icon here.
        const notifyUser = async (msg: string) => {
          console.info(`[ClaudeMemory] ${msg}`);
          try {
            await showMessageDialog(msg, {
              title: "Claude Memory Interceptor",
              kind: "info",
            });
          } catch {
            // Dialog plugin unavailable in browser runtime — console is the fallback.
          }
        };
        try {
          await startClaudeMemoryInterceptor();
          if (settingsStore.get("claudeMemoryMigrateOnStartup")) {
            const report = await migrateExistingClaudeMemory();
            console.info(
              `[ClaudeMemory] startup migration: persisted=${report.persisted} failures=${report.failures}`,
            );
            if (report.failures > 0) {
              await notifyUser(
                `${report.failures} Claude memory file${
                  report.failures === 1 ? "" : "s"
                } could not be pushed to SerenDB yet and ${
                  report.failures === 1 ? "was" : "were"
                } left on disk. The interceptor will retry automatically on the next write, or you can retry now from Settings → Code Indexing → Claude Code Auto-Memory → Migrate Existing Files.`,
              );
            }
          }
        } catch (err) {
          // Reset so a later project/auth change can retry.
          claudeMemoryStartedForAuth = null;
          // Emit telemetry so persistent failures (e.g. cold-start exhaustion,
          // gateway outages) open a serenorg/seren-core ticket instead of dying
          // silently in the dialog. #1845 — pre-fix, this entire failure class
          // produced zero PostHog logs and zero error-tracking issues.
          const errMessage = err instanceof Error ? err.message : String(err);
          const errStack =
            err instanceof Error && err.stack ? err.stack.split("\n") : [];
          const httpStatusMatch = /returned HTTP (\d{3})/i.exec(errMessage);
          const httpStatusCode = httpStatusMatch?.[1]
            ? Number.parseInt(httpStatusMatch[1], 10)
            : undefined;
          try {
            const { captureSupportError } = await import("@/lib/support/hook");
            void captureSupportError({
              kind: "claude_memory.start_failure",
              message: errMessage,
              stack: errStack,
              http: {
                method: "POST",
                url: "https://api.serendb.com/publishers/seren-db/query",
                status: httpStatusCode,
                body: errMessage,
              },
            });
          } catch (captureErr) {
            // Telemetry must never block the user-facing dialog.
            console.warn(
              `[ClaudeMemory] captureSupportError unavailable: ${captureErr}`,
            );
          }
          await notifyUser(
            `The Claude memory interceptor could not start: ${err}. You can try again by toggling it off and on in Settings → Code Indexing → Claude Code Auto-Memory.`,
          );
        }
      } catch (error) {
        console.error(`[ClaudeMemory] reactive start crashed: ${error}`);
      }
    });
  });

  const handleLoginSuccess = async () => {
    await setAuthenticated({ id: "", email: "", name: "" });
  };

  const handleLogout = async () => {
    await logout();
  };

  return (
    <>
      <Show
        when={!authStore.isLoading}
        fallback={
          <div class="flex flex-col items-center justify-center h-screen gap-4 text-muted-foreground">
            <div class="loading-spinner" />
            <p>Loading...</p>
          </div>
        }
      >
        <ErrorBoundary
          fallback={(error) => {
            captureUnknownError("solid_error_boundary", error);
            return (
              <div class="flex h-screen items-center justify-center text-sm text-destructive">
                Something went wrong. Seren is recovering.
              </div>
            );
          }}
        >
          <AppShell
            onLoginSuccess={handleLoginSuccess}
            onLogout={handleLogout}
          />
        </ErrorBoundary>
        <LowBalanceModal />
        <DailyClaimPopup />
        <X402PaymentApproval />
        <Show when={runtime.capabilities.localMcp}>
          <GatewayToolApproval />
        </Show>
        <Show when={runtime.capabilities.terminal}>
          <ShellApproval />
        </Show>
        <Show when={runtime.mode === "desktop-native"}>
          <AboutDialog />
        </Show>
      </Show>
      <OrganizationOtpModal />
    </>
  );
}

export default App;

function shouldRenderPhase3Playground(): boolean {
  if (!import.meta.env.DEV) return false;
  if (typeof window === "undefined") return false;
  const params = new URLSearchParams(window.location.search);
  return params.get("test") === "phase3";
}
