import { app, BrowserWindow, nativeTheme, net, protocol } from "electron";
import { dirname, extname, normalize, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { access, writeFile } from "node:fs/promises";
import { createEngine } from "../src/engine.js";
import { MemoryResponseCache } from "../src/core/memoryCache.js";
import { PoliteHttp } from "../src/core/politeHttp.js";
import { ElectronBrowserHub } from "./electronBrowserHub.js";
import { registerDesktopIpc, openExternalUrl } from "./ipc.js";
import { JsonPlanStorage } from "./planStorage.js";
import { JsonSearchHistoryStorage } from "./historyStorage.js";
import { isSmokeRun } from "./runtimeFlags.js";
import { startDesktopUpdates } from "./updateManager.js";
import { desktopTitleBarColors, desktopWindowBackground } from "./windowAppearance.js";

protocol.registerSchemesAsPrivileged([
  { scheme: "app", privileges: { standard: true, secure: true, supportFetchAPI: true } },
]);

const hasLock = app.requestSingleInstanceLock();
if (!hasLock) app.quit();

let mainWindow: BrowserWindow | null = null;
let browser: ElectronBrowserHub | null = null;
let disposeIpc: (() => void) | null = null;
let disposeUpdates: (() => void) | null = null;

if (hasLock) {
  app.on("second-instance", () => {
    if (!mainWindow || mainWindow.isDestroyed()) return;
    if (mainWindow.isMinimized()) mainWindow.restore();
    mainWindow.show();
    mainWindow.focus();
  });

  app.whenReady().then(start).catch((error) => {
    console.error(error);
    app.exit(1);
  });
}

async function start(): Promise<void> {
  const rootDir = app.isPackaged
    ? app.getAppPath()
    : resolve(dirname(fileURLToPath(import.meta.url)), "../..");
  const rendererDir = resolve(process.env.DESKTOP_RENDERER_DIR ?? resolve(rootDir, "public"));
  await access(resolve(rendererDir, "index.html"));
  registerAppProtocol(rendererDir);

  mainWindow = createMainWindow(resolve(dirname(fileURLToPath(import.meta.url)), "preload.cjs"));
  mainWindow.on("closed", () => {
    mainWindow = null;
    if (process.platform !== "darwin") app.quit();
  });
  browser = new ElectronBrowserHub({
    forceSolve: parseStoreSet([
      process.env.DESKTOP_FORCE_SOLVE,
      argumentValue("--force-solve"),
    ].filter(Boolean).join(",")),
    holdForcedSolve: process.argv.includes("--exercise-dismiss"),
    onSolveCleared: (storeId) => mainWindow?.webContents.send("solve:cleared", storeId),
    onSolveDismissed: (storeId) => mainWindow?.webContents.send("solve:dismissed", storeId),
  });
  const planStorage = new JsonPlanStorage(resolve(app.getPath("userData"), "purchase-plan.json"));
  const historyStorage = new JsonSearchHistoryStorage(resolve(app.getPath("userData"), "search-history.json"));
  const engine = createEngine({
    http: new PoliteHttp(),
    browser,
    cache: new MemoryResponseCache(),
  });
  disposeIpc = registerDesktopIpc({ engine, browser, planStorage, historyStorage });

  installNavigationPolicy(mainWindow);
  await mainWindow.loadURL("app://app/");
  mainWindow.show();
  disposeUpdates = await startDesktopUpdates(mainWindow);

  if (isSmokeRun()) {
    await runSmoke(engine, planStorage);
  }
}

function createMainWindow(preload: string): BrowserWindow {
  const overlay = desktopTitleBarColors(nativeTheme.shouldUseDarkColors);
  const window = new BrowserWindow({
    show: false,
    width: 1280,
    height: 860,
    minWidth: 780,
    minHeight: 600,
    autoHideMenuBar: true,
    title: "Auto detaļu salīdzinātājs",
    titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "hidden",
    ...(process.platform === "darwin" ? {} : {
      titleBarOverlay: { ...overlay, height: 48 },
    }),
    backgroundColor: desktopWindowBackground(nativeTheme.shouldUseDarkColors),
    webPreferences: {
      preload,
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: true,
    },
  });
  if (process.platform !== "darwin") {
    const syncTitleBar = () => {
      if (window.isDestroyed()) return;
      window.setTitleBarOverlay({ ...desktopTitleBarColors(nativeTheme.shouldUseDarkColors), height: 48 });
      window.setBackgroundColor(desktopWindowBackground(nativeTheme.shouldUseDarkColors));
    };
    nativeTheme.on("updated", syncTitleBar);
    window.once("closed", () => nativeTheme.removeListener("updated", syncTitleBar));
  }
  window.webContents.session.setPermissionRequestHandler((_contents, _permission, callback) => callback(false));
  return window;
}

function registerAppProtocol(rendererDir: string): void {
  protocol.handle("app", async (request) => {
    const url = new URL(request.url);
    if (url.host !== "app") return new Response("Not found", { status: 404 });
    const requested = decodeURIComponent(url.pathname).replace(/^\/+/, "") || "index.html";
    const candidate = resolve(rendererDir, normalize(requested));
    const rootPrefix = rendererDir.endsWith(sep) ? rendererDir : `${rendererDir}${sep}`;
    if (candidate !== rendererDir && !candidate.startsWith(rootPrefix)) return new Response("Not found", { status: 404 });
    const asset = await existingAsset(candidate) ?? resolve(rendererDir, "index.html");
    return net.fetch(pathToFileURL(asset).toString());
  });
}

async function existingAsset(path: string): Promise<string | null> {
  if (!extname(path)) return null;
  try { await access(path); return path; } catch { return null; }
}

function installNavigationPolicy(window: BrowserWindow): void {
  window.webContents.setWindowOpenHandler(({ url }) => {
    if (isExternal(url)) void openExternalUrl(url).catch(console.error);
    return { action: "deny" };
  });
  window.webContents.on("will-navigate", (event, url) => {
    if (url.startsWith("app://app/")) return;
    event.preventDefault();
    if (isExternal(url)) void openExternalUrl(url).catch(console.error);
  });
}

function isExternal(url: string): boolean {
  return url.startsWith("https://") || url.startsWith("http://");
}

function parseStoreSet(value: string | undefined): ReadonlySet<string> {
  return new Set((value ?? "").split(",").map((item) => item.trim()).filter(Boolean));
}

async function runSmoke(
  engine: ReturnType<typeof createEngine>,
  storage: JsonPlanStorage,
): Promise<void> {
  void engine;
  void storage;
  const smoke = await mainWindow!.webContents.executeJavaScript(
    `(async () => {
      const bridge = window.comparatorDesktop;
      const plan = { schemaVersion: 1, storeSort: "lines", requirements: [] };
      const history = { schemaVersion: 1, entries: [{ query: "W 712/52", canonical: "W71252", searchedAt: 1234 }] };
      const root = document.querySelector("app-root");
      const header = document.querySelector(".app-shell-header");
      const shellBar = document.querySelector(".app-shell-bar");
      const contentScroll = document.querySelector(".app-content-scroll");
      const originalLanguage = document.documentElement.lang || "lv";
      const originalStoredLanguage = localStorage.getItem("car-parts-comparator.language");
      const chooseLanguage = async value => {
        if (!document.querySelector(".settings-sheet")) {
          document.querySelector(".app-settings-action")?.click();
          await new Promise(resolve => setTimeout(resolve, 240));
        }
        document.querySelector("#settings-language-select")?.click();
        await new Promise(resolve => setTimeout(resolve, 100));
        const names = { lv: "Latviešu", ru: "Русский", en: "English" };
        const item = [...document.querySelectorAll("hlm-select-item")]
          .find(candidate => candidate.textContent?.trim() === names[value]);
        item?.click();
        await new Promise(resolve => setTimeout(resolve, 0));
      };
      await chooseLanguage("en");
      const settingsHeader = document.querySelector(".settings-header");
      const settingsBackButton = settingsHeader?.querySelector("button");
      const languageLabel = document.querySelector("#settings-language-select")?.textContent?.trim() ?? "";
      const settingsHeaderTop = Math.round(settingsHeader?.getBoundingClientRect().top ?? 0);
      const settingsBackAction = settingsBackButton?.getAttribute("aria-label") ?? "";
      const settingsSafe = settingsHeaderTop >= 49 && settingsBackAction === "Back";
      settingsBackButton?.click();
      await new Promise(resolve => setTimeout(resolve, 0));
      const shell = {
        host: root?.getAttribute("data-host"),
        os: root?.getAttribute("data-desktop-os"),
        height: Math.round(shellBar?.getBoundingClientRect().height ?? 0),
        title: document.querySelector(".app-shell-title")?.textContent?.trim(),
        tagline: Boolean(header?.querySelector("p")),
        headerBackground: header ? getComputedStyle(header).backgroundColor : "",
        headerBackdropFilter: header ? getComputedStyle(header).backdropFilter : "",
        headerBottom: Math.round(header?.getBoundingClientRect().bottom ?? 0),
        contentTop: Math.round(contentScroll?.getBoundingClientRect().top ?? 0),
        contentBottom: Math.round(contentScroll?.getBoundingClientRect().bottom ?? 0),
        viewportHeight: window.innerHeight,
        contentOverflowY: contentScroll ? getComputedStyle(contentScroll).overflowY : "",
        contentPaddingTop: Math.round(parseFloat(contentScroll ? getComputedStyle(contentScroll).paddingTop : "0")),
        scrollbarTrackMarginTop: Math.round(parseFloat(contentScroll ? getComputedStyle(contentScroll, "::-webkit-scrollbar-track").marginTop : "0")),
        planSafe: false,
        planHeaderTop: 0,
        backAction: "",
        settingsSafe,
        settingsHeaderTop,
        settingsBackAction,
        languageLabel,
        htmlLang: document.documentElement.lang,
        searchLabel: document.querySelector('button[type="submit"]')?.textContent?.trim(),
      };
      if (
        shell.host !== "desktop" || shell.height !== 48 || shell.tagline || !shell.title ||
        !shell.settingsSafe || shell.languageLabel !== "English" || shell.htmlLang !== "en" || shell.searchLabel !== "Search" || document.title !== "Car parts price comparator" ||
        !shell.headerBackground.includes(" / 0.72") || shell.headerBackdropFilter === "none" ||
        shell.contentTop !== 0 || shell.contentBottom !== shell.viewportHeight ||
        shell.contentPaddingTop !== shell.headerBottom || shell.scrollbarTrackMarginTop !== shell.headerBottom ||
        shell.contentOverflowY !== "auto"
      ) {
        throw new Error("Desktop shell smoke failed: " + JSON.stringify(shell));
      }
      document.querySelector(".app-plan-action")?.click();
      await new Promise(resolve => setTimeout(resolve, 380));
      const planHeader = document.querySelector(".plan-sheet > header:first-child");
      const backButton = planHeader?.querySelector("button");
      shell.planHeaderTop = Math.round(planHeader?.getBoundingClientRect().top ?? 0);
      shell.backAction = backButton?.getAttribute("aria-label") ?? "";
      shell.planSafe = shell.planHeaderTop >= 49 && shell.backAction === "Back";
      if (!shell.planSafe) throw new Error("Purchase-plan header overlaps native window controls: " + JSON.stringify(shell));
      if (!${JSON.stringify(process.argv.includes("--capture-plan"))}) backButton?.click();
      const before = await bridge.loadPlan();
      const saved = await bridge.savePlan(plan);
      const after = await bridge.loadPlan();
      const historyBefore = await bridge.loadSearchHistory();
      const historySaved = await bridge.saveSearchHistory(history);
      const historyAfter = await bridge.loadSearchHistory();
      await chooseLanguage(originalLanguage);
      if (originalStoredLanguage === null) localStorage.removeItem("car-parts-comparator.language");
      else localStorage.setItem("car-parts-comparator.language", originalStoredLanguage);
      return { renderer: { title: document.title, bridge: bridge.platform, node: typeof process, shell }, stores: (await bridge.stores()).stores.length, before, saved, after, historyBefore, historySaved, historyAfter };
    })()`,
    true,
  );
  let solve: unknown = undefined;
  if (process.argv.includes("--exercise-solve")) {
    solve = await mainWindow!.webContents.executeJavaScript(
      `(async () => {
        const bridge = window.comparatorDesktop;
        const result = await bridge.search("trodo", "0986452041");
        if (!result.needsSolve) return { needsSolve: false, cleared: false };
        const cleared = new Promise(resolve => {
          const unsubscribe = bridge.onSolveCleared(storeId => { unsubscribe(); resolve(storeId); });
        });
        await bridge.showSolve("trodo");
        return { needsSolve: true, cleared: await cleared };
      })()`,
      true,
    );
  }
  let solveDismissed: unknown = undefined;
  if (process.argv.includes("--exercise-dismiss")) {
    const dismissed = mainWindow!.webContents.executeJavaScript(
      `(async () => {
        const bridge = window.comparatorDesktop;
        const result = await bridge.search("trodo", "0986452041");
        if (!result.needsSolve) return { needsSolve: false, dismissed: false };
        const event = new Promise(resolve => {
          const unsubscribe = bridge.onSolveDismissed(storeId => { unsubscribe(); resolve(storeId); });
        });
        await bridge.showSolve("trodo");
        await bridge.dismissSolve("trodo");
        return { needsSolve: true, dismissed: await event };
      })()`,
      true,
    );
    solveDismissed = await dismissed;
  }
  let searches: unknown = undefined;
  const searchAllQuery = argumentValue("--search-all");
  if (searchAllQuery) {
    searches = await mainWindow!.webContents.executeJavaScript(
      `(async () => {
        const bridge = window.comparatorDesktop;
        const stores = (await bridge.stores()).stores;
        return Promise.all(stores.map(async store => {
          try {
            const result = await bridge.search(store.id, ${JSON.stringify(searchAllQuery)});
            return {
              store: store.id,
              ok: result.ok,
              offers: result.offers.length,
              errorCode: result.errorCode ?? null,
              needsSolve: result.needsSolve ?? false,
              browserEvents: result.diagnostic?.browserEvents.map(event => event.event) ?? [],
            };
          } catch (error) {
            return { store: store.id, ok: false, offers: 0, error: String(error) };
          }
        }));
      })()`,
      true,
    );
  }
  console.log(JSON.stringify({ phase: "desktop-smoke", ...smoke, solve, solveDismissed, searches }));
  const capturePath = argumentValue("--capture");
  if (capturePath) {
    const image = await mainWindow!.webContents.capturePage();
    await writeFile(capturePath, image.toPNG());
  }
  if (process.argv.includes("--hold")) await new Promise((resolve) => setTimeout(resolve, 15_000));
  if (process.argv.includes("--close-window")) mainWindow?.close();
  else app.quit();
}

function argumentValue(name: string): string | undefined {
  const prefix = `${name}=`;
  return process.argv.find((argument) => argument.startsWith(prefix))?.slice(prefix.length);
}

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
});

app.on("activate", () => {
  if (mainWindow && !mainWindow.isDestroyed()) mainWindow.show();
});

app.on("before-quit", () => {
  disposeUpdates?.();
  disposeUpdates = null;
  disposeIpc?.();
  disposeIpc = null;
  void browser?.close();
  browser = null;
  mainWindow = null;
});
