import { app, BrowserWindow, ipcMain, nativeImage, nativeTheme, session, dialog } from "electron";
import * as path from "path";
import { readFileSync } from "fs";
import { join } from "path";
import { ConfigManager } from "./config/manager";
import { createSecretStore } from "./config/secrets";
import { ModelManager } from "./models/manager";
import { AudioRecorder } from "./audio/recorder";
import { transcribe } from "./audio/whisper";
import { createLlmProvider } from "./llm/factory";
import { Pipeline } from "./pipeline";
import { ShortcutManager } from "./shortcuts/manager";
import { setupTray, setTrayModelState, updateTrayConfig, updateTrayMenu, getTrayState, destroyTray } from "./tray";
import { getResourcePath } from "./resources";
import { initAutoUpdater } from "./updater";
import { isUpdating } from "./update-state";
import { openHome, setAppMenuCallbacks, refreshAppMenu, suppressBlur, setHideOnBlur } from "./windows/home";
import { registerIpcHandlers } from "./ipc";
import { paster, permissions, applyCase, stripTrailingPeriod } from "./platform";
import { SetupChecker } from "./setup/checker";
import { HistoryManager } from "./history/manager";
import { type AudioCueType } from "../shared/config";
import { generateCueSamples, isWavCue, getWavFilename, parseWavSamples } from "../shared/audio-cue";
import { t, setLanguage, resolveSystemLanguage } from "../shared/i18n";
import { getLlmModelName } from "../shared/llm-utils";
import { AnalyticsService } from "./analytics/service";
import { setupAnalyticsErrorCapture } from "./logger";
import log from "./logger";
import { saveAudioFile, cleanupOrphanedAudioFiles, getAudioFilePath } from "./audio/persistence";

log.initialize();
const slog = log.scope("Vox");

const configDir = path.join(app.getPath("userData"));
const modelsDir = path.join(configDir, "models");
const configManager = new ConfigManager(configDir, createSecretStore());
const modelManager = new ModelManager(modelsDir);
const historyManager = new HistoryManager();
const analytics = new AnalyticsService();

let pipeline: Pipeline | null = null;
let shortcutManager: ShortcutManager | null = null;
let devLlmOverride: { enabled?: boolean; tested?: boolean } | null = null;

function setupPipeline(): void {
  const config = configManager.load();
  if (devLlmOverride) {
    if (devLlmOverride.enabled !== undefined) config.enableLlmEnhancement = devLlmOverride.enabled;
    if (devLlmOverride.tested !== undefined) {
      config.llmConnectionTested = devLlmOverride.tested;
      if (!devLlmOverride.tested) config.llmConfigHash = "";
    }
  }
  const modelPath = config.whisper.model
    ? modelManager.getModelPath(config.whisper.model)
    : "";
  const llmProvider = createLlmProvider(config);

  const recorder = new AudioRecorder();
  recorder.onAudioLevels = (levels) => {
    shortcutManager?.sendAudioLevels(levels);
  };

  pipeline = new Pipeline({
    recorder,
    transcribe,
    llmProvider,
    modelPath,
    dictionary: config.dictionary ?? [],
    speechLanguages: config.speechLanguages ?? [],
    hasCustomPrompt: Boolean(config.customPrompt),
    llmModelName: config.enableLlmEnhancement ? getLlmModelName(config.llm) : undefined,
    analytics,
    onStage: (stage) => {
      shortcutManager?.getHud().setState(stage);
      if (stage === "enhancing") {
        shortcutManager?.getHud().startEnhancingEffect();
      }
    },
    onTranscriptionComplete: (rawText) => {
      shortcutManager?.getHud().updateTextPanel(rawText);
    },
    onLlmFailed: () => {
      const cfg = configManager.load();
      cfg.llmConnectionTested = false;
      cfg.llmConfigHash = "";
      configManager.save(cfg);
      for (const win of BrowserWindow.getAllWindows()) {
        win.webContents.send("config:changed");
      }
      // Rebuild the pipeline so the next recording uses NoopProvider
      // (setImmediate lets the current run finish before we swap it out)
      setImmediate(() => setupPipeline());
    },
    onComplete: (result) => {
      // Defer all side effects so the pipeline can return finalText immediately
      // and the paste happens without waiting for history/IPC/disk operations.
      setImmediate(() => {
        try {
          const latestConfig = configManager.load();
          const stripped = latestConfig.finishWithPeriod ? result.text : stripTrailingPeriod(result.text);
          const finalText = applyCase(stripped, latestConfig.lowercaseStart);

        shortcutManager?.getHud().stopEnhancingEffect();

        const status = result.llmFailed ? "llm_failed" as const : "success" as const;

          const entry = historyManager.add({
            text: finalText,
            originalText: result.originalText,
            audioDurationMs: result.audioDurationMs,
            wordCount: finalText.split(/\s+/).filter(Boolean).length,
            whisperModel: config.whisper.model || "unknown",
            llmEnhanced: config.enableLlmEnhancement,
            llmProvider: config.enableLlmEnhancement ? config.llm.provider : undefined,
            llmModel: config.enableLlmEnhancement ? getLlmModelName(config.llm) : undefined,
            status,
            errorMessage: result.errorMessage,
            failedStep: result.llmFailed ? "llm" : undefined,
          });

          if (latestConfig.audioRetentionCount > 0) {
            const audioFilePath = getAudioFilePath(entry.id);
            historyManager.updateEntry(entry.id, { audioFilePath });
            saveAudioFile(result.recording.audioBuffer, result.recording.sampleRate, entry.id).catch((err) =>
              slog.warn("Failed to save audio file", err),
            );
          }

          historyManager.enforceAudioRetention(latestConfig.audioRetentionCount);

          for (const win of BrowserWindow.getAllWindows()) {
            win.webContents.send("history:entry-added");
            win.webContents.send("pipeline:result", finalText);
          }
        } catch (err) {
          slog.error("Failed to save transcription to history", err);
        }
      });
    },
    onFailure: (result) => {
      setImmediate(() => {
        try {
          const latestConfig = configManager.load();

          const entry = historyManager.add({
            text: "",
            originalText: "",
            wordCount: 0,
            audioDurationMs: result.audioDurationMs,
            whisperModel: config.whisper.model || "unknown",
            llmEnhanced: false,
            status: "whisper_failed",
            errorMessage: result.error.message,
            failedStep: "whisper",
          });

          if (latestConfig.audioRetentionCount > 0) {
            const audioFilePath = getAudioFilePath(entry.id);
            historyManager.updateEntry(entry.id, { audioFilePath });
            saveAudioFile(result.recording.audioBuffer, result.recording.sampleRate, entry.id).catch((err) =>
              slog.warn("Failed to save audio file", err),
            );
          }

          historyManager.enforceAudioRetention(latestConfig.audioRetentionCount);

          for (const win of BrowserWindow.getAllWindows()) {
            win.webContents.send("history:entry-added");
          }
        } catch (err) {
          slog.error("Failed to save failed transcription to history", err);
        }
      });
    },
  });
}

function reloadConfig(): void {
  const config = configManager.load();

  const lang = config.language === "system"
    ? resolveSystemLanguage(app.getLocale())
    : config.language;
  setLanguage(lang);

  const busy = shortcutManager?.isRecording() ?? false;

  if (!busy) {
    setupPipeline();
  }

  shortcutManager?.registerShortcutKeys();
  shortcutManager?.updateHud();
  updateTrayConfig(config);
  refreshAppMenu();
  analytics.setEnabled(config.analyticsEnabled);
  setHideOnBlur(config.hideOnBlur);

  suppressBlur();

  if (config.showInDock) {
    app.dock?.show();
  } else {
    app.dock?.hide();
  }

  if (config.showInTray) {
    if (!getTrayState().trayActive) {
      setupTray({
        onOpenHome: () => openHome(reloadConfig),
        onOpenHistory: () => openHome(reloadConfig, "transcriptions"),
        onStartListening: () => shortcutManager?.triggerToggle(),
        onStopListening: () => shortcutManager?.stopAndProcess(),
        onCancelListening: () => shortcutManager?.cancelRecording(),
        onToggleHud: () => {
          const cfg = configManager.load();
          cfg.showHud = !cfg.showHud;
          if (!cfg.showHud) cfg.hudShowOnHover = false;
          configManager.save(cfg);
          shortcutManager?.updateHud();
          updateTrayConfig(cfg);
          refreshAppMenu();
          for (const win of BrowserWindow.getAllWindows()) {
            win.webContents.send("config:changed");
          }
        },
      });
    }
    updateTrayConfig(config);
    const setupChecker = new SetupChecker(modelManager);
    setTrayModelState(setupChecker.hasAnyModel());
  } else {
    destroyTray();
  }
}

app.whenReady().then(async () => {
  session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback) => {
    callback(permission === "media");
  });
  session.defaultSession.setPermissionCheckHandler((_webContents, permission) => {
    return permission === "media";
  });

  const initialConfig = configManager.load();
  nativeTheme.themeSource = initialConfig.theme;

  setHideOnBlur(initialConfig.hideOnBlur);

  const systemLocale = app.getLocale();
  const lang = initialConfig.language === "system"
    ? resolveSystemLanguage(systemLocale)
    : initialConfig.language;
  setLanguage(lang);

  analytics.init({
    enabled: initialConfig.analyticsEnabled,
    locale: lang,
  });
  analytics.identify();
  setupAnalyticsErrorCapture(analytics);
  analytics.track("app_launched", {
    launch_at_login: initialConfig.launchAtLogin,
  });

  registerIpcHandlers(configManager, modelManager, historyManager, reloadConfig, analytics);

  ipcMain.handle("audio:preview-cue", async (_event, cueType: string) => {
    if (cueType === "none" || !pipeline) return;
    const cue = cueType as AudioCueType;
    if (isWavCue(cue)) {
      const filename = getWavFilename(cue);
      if (!filename) return;
      const audioDir = app.isPackaged
        ? join(process.resourcesPath, "resources", "audio")
        : join(app.getAppPath(), "resources", "audio");
      const wavData = readFileSync(join(audioDir, filename));
      const parsed = parseWavSamples(wavData);
      if (parsed.samples.length > 0) await pipeline.playAudioCue(parsed.samples, parsed.sampleRate);
    } else {
      const samples = generateCueSamples(cue, 44100);
      if (samples.length > 0) await pipeline.playAudioCue(samples);
    }
  });

  setupPipeline();

  historyManager.cleanup();

  // Clean up orphaned audio files
  const validIds = new Set(
    historyManager.getAllEntryIds().filter((id) => {
      const entry = historyManager.getEntry(id);
      return !!entry?.audioFilePath;
    }),
  );
  await cleanupOrphanedAudioFiles(validIds);

  const hasAccessibility = paster.isAccessibilityGranted();
  if (!hasAccessibility && initialConfig.onboardingCompleted) {
    const response = await dialog.showMessageBox({
      type: "warning",
      title: t("dialog.accessibilityTitle"),
      message: t("dialog.accessibilityMessage"),
      detail: t("dialog.accessibilityDetail"),
      buttons: [t("dialog.openSystemSettings"), t("dialog.continueWithoutShortcuts")],
      defaultId: 0,
      cancelId: 1,
    });

    if (response.response === 0) {
      permissions.openAccessibilitySettings();
      slog.info("Opening Accessibility settings");
    } else {
      slog.info("User chose to continue without Accessibility permission");
    }
  }

  shortcutManager = new ShortcutManager({
    configManager,
    getPipeline: () => pipeline!,
    analytics,
    openSettings: (tab) => openHome(reloadConfig, tab),
    hasModel: () => new SetupChecker(modelManager).hasAnyModel(),
  });
  shortcutManager.start();

  ipcMain.handle("dev:get-runtime-state", () => {
    return {
      shortcutState: shortcutManager?.getStateMachineState() ?? "idle",
      isRecording: shortcutManager?.isRecording() ?? false,
      hudVisible: shortcutManager?.getHud().isVisible() ?? false,
      hudState: shortcutManager?.getHud().getState() ?? "idle",
      ...getTrayState(),
    };
  });

  ipcMain.handle("dev:set-analytics-enabled", (_event, enabled: boolean) => {
    analytics.setEnabled(enabled);
    return enabled;
  });

  ipcMain.handle("dev:set-model-override", (_event, hasModel: boolean | null) => {
    shortcutManager?.setDevModelOverride(hasModel);
  });

  ipcMain.handle("dev:set-llm-override", (_event, enabled: boolean | null, tested: boolean | null) => {
    if (enabled === null && tested === null) {
      devLlmOverride = null;
    } else {
      devLlmOverride = {};
      if (enabled !== null) devLlmOverride.enabled = enabled;
      if (tested !== null) devLlmOverride.tested = tested;
    }
    setupPipeline();
  });

  ipcMain.handle("dev:test-error", () => {
    analytics.captureError(new Error("Test error from Dev Panel"), { scope: "devPanel" }, { force: true });
  });

  ipcMain.handle("dev:get-system-info", () => {
    return {
      electronVersion: process.versions.electron,
      nodeVersion: process.versions.node,
      chromeVersion: process.versions.chrome,
      v8Version: process.versions.v8,
      platform: process.platform,
      arch: process.arch,
      isPackaged: app.isPackaged,
      appVersion: app.getVersion(),
      appPath: app.getAppPath(),
      userDataPath: app.getPath("userData"),
      logsPath: app.getPath("logs"),
      logLevelFile: String(log.transports.file.level),
      logLevelConsole: String(log.transports.console.level),
      whisperLib: "whisper.cpp v1.8.3 (vendored)",
    };
  });

  const setupChecker = new SetupChecker(modelManager);
  if (initialConfig.showInTray) {
    setupTray({
      onOpenHome: () => openHome(reloadConfig),
      onOpenHistory: () => openHome(reloadConfig, "transcriptions"),
      onStartListening: () => shortcutManager?.triggerToggle(),
      onStopListening: () => shortcutManager?.stopAndProcess(),
      onCancelListening: () => shortcutManager?.cancelRecording(),
      onToggleHud: () => {
        const cfg = configManager.load();
        cfg.showHud = !cfg.showHud;
        if (!cfg.showHud) cfg.hudShowOnHover = false;
        configManager.save(cfg);
        shortcutManager?.updateHud();
        updateTrayConfig(cfg);
        refreshAppMenu();
        for (const win of BrowserWindow.getAllWindows()) {
          win.webContents.send("config:changed");
        }
      },
    });
    setTrayModelState(setupChecker.hasAnyModel());
    updateTrayConfig(configManager.load());
  }

  initAutoUpdater(() => updateTrayMenu());

  setAppMenuCallbacks({
    onShowVox: () => openHome(reloadConfig),
    onTranscriptions: () => openHome(reloadConfig, "transcriptions"),
    onDictionary: () => openHome(reloadConfig, "dictionary"),
    onSettings: () => openHome(reloadConfig, "general"),
    onToggleHud: () => {
      const cfg = configManager.load();
      cfg.showHud = !cfg.showHud;
      if (!cfg.showHud) cfg.hudShowOnHover = false;
      configManager.save(cfg);
      shortcutManager?.updateHud();
      updateTrayConfig(cfg);
      refreshAppMenu();
      openHome(reloadConfig);
      for (const win of BrowserWindow.getAllWindows()) {
        win.webContents.send("config:changed");
      }
    },
    onCheckForUpdates: () => openHome(reloadConfig, "about"),
    onOnboarding: () => {
      openHome(reloadConfig, "onboarding");
    },
    isHudVisible: () => configManager.load().showHud,
  });

  shortcutManager.updateHud();

  await app.dock?.show();
  if (!app.isPackaged) {
    const devDockIcon = nativeImage.createFromPath(getResourcePath("dockIcon-dev.png"));
    if (!devDockIcon.isEmpty()) {
      app.dock?.setIcon(devDockIcon);
      slog.info("Dev dock icon applied");
    } else {
      slog.warn("Dev dock icon not found at", getResourcePath("dockIcon-dev.png"));
    }
  }
  if (!initialConfig.showInDock) {
    app.dock?.hide();
  }

  openHome(reloadConfig);
}).catch((err) => {
  slog.error("Fatal error during app initialization", err);
});

app.on("activate", () => {
  openHome(reloadConfig);
});

app.on("before-quit", () => {
  shortcutManager?.stop();
  destroyTray();
  for (const win of BrowserWindow.getAllWindows()) {
    if (!win.isDestroyed()) win.destroy();
  }
});

let isQuitting = false;
app.on("will-quit", (event) => {
  // When electron-updater is performing quit-and-install, stay completely out
  // of the way.  Its own will-quit handler must run uninterrupted to install
  // the update and relaunch via Squirrel.Mac.
  if (isUpdating()) return;

  if (!isQuitting) {
    isQuitting = true;
    event.preventDefault();
    analytics.track("app_quit", {
      session_duration_ms: Math.round(performance.now()),
    });
    analytics.shutdown().finally(() => app.quit());
  }
});

app.on("window-all-closed", () => {});

for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
  process.on(sig, () => app.exit(0));
}

// In dev mode, electron-vite spawns Electron as a child process.
// When Ctrl+C kills electron-vite, the Electron child becomes an orphan
// reparented to launchd (PID 1) and never receives SIGINT.
// Detect parent death by polling and exit immediately.
if (!app.isPackaged) {
  const parentPid = process.ppid;
  setInterval(() => {
    try { process.kill(parentPid, 0); } catch { app.exit(0); }
  }, 200).unref();
}
