import path from "node:path";
import { promises as fs } from "node:fs";
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";
import {
  app,
  autoUpdater,
  dialog,
  Menu,
  shell,
  Tray,
  nativeImage
} from "electron";
import { formatCompact, scanUsage } from "@tokenmaxx/usage";
import { buildIngestPayload, defaultDevice, postIngest } from "./sync.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REFRESH_MS = 60_000;
const UPDATE_CHECK_MS = 6 * 60 * 60 * 1000;
const PROD_API_URL = "https://api.tokenmaxx.dev";
const PROD_WEB_URL = "https://tokenmaxx.dev";
const LOCAL_API_URL = "http://localhost:8788";
const LOCAL_WEB_URL = "http://localhost:3000";
const DESKTOP_PROTOCOL = "tokenmaxx";
const DESKTOP_AUTH_CALLBACK_URL = `${DESKTOP_PROTOCOL}://auth/callback`;
const LOCAL_AUTH_TIMEOUT_MS = 5 * 60 * 1000;

app.setName("Tokenmaxx");

let tray;
let latestSummary;
let latestSync;
let latestError;
let refreshTimer;
let refreshPromise;
let updateCheckTimer;
let updateCheckInFlight = false;
let updateCheckTriggeredByUser = false;
let availableUpdate;
let downloadedUpdate;
let updateCheckError;
let updateStatusMessage;
let cachedAppIcon;
let lastSeenToken = "";
let authenticated = false;
let localAuthServer;
let localAuthTimeout;
const shouldShowOnLaunch = process.argv.includes("--show");
const pendingProtocolUrls = [];

autoUpdater.on("checking-for-update", () => {
  updateCheckInFlight = true;
  updateCheckError = null;
  updateStatusMessage = "";
  updateTrayMenu().catch(() => {});
});

autoUpdater.on("update-available", () => {
  availableUpdate = {
    version: "the latest version",
    status: "downloading",
    notes: "",
    downloadUrl: ""
  };
  downloadedUpdate = null;
  updateCheckError = null;
  updateStatusMessage = "";
  updateTrayMenu().catch(() => {});
});

autoUpdater.on("update-not-available", () => {
  const wasUserTriggered = updateCheckTriggeredByUser;
  updateCheckInFlight = false;
  updateCheckTriggeredByUser = false;
  availableUpdate = null;
  downloadedUpdate = null;
  updateCheckError = null;
  updateStatusMessage = wasUserTriggered ? `Tokenmaxx ${app.getVersion()} is up to date.` : "";
  updateTrayMenu().catch(() => {});
});

autoUpdater.on("update-downloaded", (_event, releaseNotes, releaseName, releaseDate, updateURL) => {
  const shouldInstall = updateCheckTriggeredByUser;
  const update = {
    version: releaseName || "the latest version",
    status: "downloaded",
    notes: releaseNotes || "",
    publishedAt: releaseDate instanceof Date ? releaseDate.toISOString() : "",
    downloadUrl: updateURL || ""
  };
  updateCheckInFlight = false;
  updateCheckTriggeredByUser = false;
  availableUpdate = update;
  downloadedUpdate = update;
  updateCheckError = null;
  updateStatusMessage = shouldInstall ? "Installing update..." : `Tokenmaxx ${update.version} is ready to install.`;
  updateTrayMenu().catch(() => {});
  if (shouldInstall) installDownloadedUpdate();
});

autoUpdater.on("error", (error) => {
  const wasUserTriggered = updateCheckTriggeredByUser;
  updateCheckInFlight = false;
  updateCheckTriggeredByUser = false;
  availableUpdate = null;
  downloadedUpdate = null;
  updateCheckError = wasUserTriggered ? error.message : null;
  updateStatusMessage = "";
  updateTrayMenu().catch(() => {});
});

const singleInstanceLock = app.requestSingleInstanceLock();
if (!singleInstanceLock) {
  app.quit();
} else {
  app.on("second-instance", (_event, argv) => {
    const protocolUrl = argv.find(isDesktopAuthUrl);
    if (protocolUrl) {
      handleProtocolUrl(protocolUrl).catch((error) => {
        latestError = error.message;
        console.error("Failed to handle auth callback from second instance", error);
      });
      return;
    }

    if (argv.includes("--show")) {
      openDashboard().catch((error) => {
        latestError = error.message;
        console.error("Failed to open dashboard from second instance", error);
      });
    }
  });
}

app.on("open-url", (event, url) => {
  event.preventDefault();
  handleProtocolUrl(url).catch((error) => {
    latestError = error.message;
    console.error("Failed to handle auth callback", error);
  });
});

const displayModes = {
  week_tokens: "7-day tokens",
  today_tokens: "Today tokens",
  week_cost: "7-day cost",
  today_cost: "Today cost"
};

function money(value) {
  const number = Number(value || 0);
  if (Math.abs(number) >= 10_000) return `$${formatCompact(number)}`;
  return new Intl.NumberFormat(undefined, {
    style: "currency",
    currency: "USD",
    maximumFractionDigits: number >= 100 ? 0 : 2
  }).format(number);
}

function defaultConfig() {
  return {
    ...backendConfig(),
    displayMode: "week_tokens",
    deviceId: randomUUID(),
    sessionToken: ""
  };
}

function backendConfig() {
  return app.isPackaged
    ? { apiUrl: PROD_API_URL, webUrl: PROD_WEB_URL }
    : { apiUrl: LOCAL_API_URL, webUrl: LOCAL_WEB_URL };
}

function updateFeedUrl(config) {
  return `${config.apiUrl.replace(/\/$/, "")}/desktop/updates/${process.platform}/${process.arch}/${app.getVersion()}`;
}

function normalizeConfig(stored = {}) {
  const source = stored && typeof stored === "object" ? stored : {};
  const defaults = defaultConfig();
  return {
    ...defaults,
    displayMode: displayModes[source.displayMode] ? source.displayMode : defaults.displayMode,
    deviceId: source.deviceId || defaults.deviceId,
    sessionToken: source.sessionToken || defaults.sessionToken
  };
}

async function configPath() {
  const dir = app.getPath("userData");
  await fs.mkdir(dir, { recursive: true });
  return path.join(dir, "config.json");
}

async function readConfig() {
  try {
    const raw = await fs.readFile(await configPath(), "utf8");
    return normalizeConfig(JSON.parse(raw));
  } catch {
    const migrated = await readLegacyConfig();
    if (migrated) {
      const config = normalizeConfig(migrated);
      await writeConfig(config);
      return config;
    }
    const config = defaultConfig();
    await writeConfig(config);
    return config;
  }
}

async function readLegacyConfig() {
  const legacyPath = path.join(app.getPath("appData"), "@tokenmaxx", "desktop", "config.json");
  try {
    return JSON.parse(await fs.readFile(legacyPath, "utf8"));
  } catch {
    return null;
  }
}

async function writeConfig(nextConfig) {
  const config = normalizeConfig(nextConfig);
  await fs.writeFile(
    await configPath(),
    JSON.stringify(
      {
        displayMode: config.displayMode,
        deviceId: config.deviceId,
        sessionToken: config.sessionToken
      },
      null,
      2
    )
  );
}

function titleFor(summary, mode) {
  if (!authenticated) return "login";
  if (!summary) return "sync";
  if (mode === "today_tokens") return formatCompact(summary.windows.today.totalTokens);
  if (mode === "week_cost") return money(summary.windows.week.costUsd);
  if (mode === "today_cost") return money(summary.windows.today.costUsd);
  return formatCompact(summary.windows.week.totalTokens);
}

function tooltipFor(summary, mode) {
  if (!authenticated) return "Tokenmaxx - sign in with GitHub";
  const label = displayModes[mode] || displayModes.week_tokens;
  if (!summary) return `Tokenmaxx - ${label}`;
  const lines = [
    `Tokenmaxx - ${label}`,
    `Today: ${formatCompact(summary.windows.today.totalTokens)} tokens / ${money(summary.windows.today.costUsd)}`,
    `7 days: ${formatCompact(summary.windows.week.totalTokens)} tokens / ${money(summary.windows.week.costUsd)}`,
    `All time: ${formatCompact(summary.totals.totalTokens)} tokens / ${money(summary.totals.costUsd)}`
  ];
  if (latestSync) lines.push(`Synced: ${new Date(latestSync).toLocaleTimeString()}`);
  if (latestError) lines.push(`Last error: ${latestError}`);
  return lines.join("\n");
}

function trayIcon() {
  const iconPath = path.join(__dirname, "assets", "tray-iconTemplate.png");
  const image = nativeImage.createFromPath(iconPath);
  if (image.isEmpty()) {
    console.error(`Failed to load tray icon at ${iconPath}`);
  }
  image.setTemplateImage(true);
  return image;
}

function appIcon() {
  if (cachedAppIcon) return cachedAppIcon;

  const iconPath = app.isPackaged
    ? path.join(process.resourcesPath, "Tokenmaxx.icns")
    : path.join(__dirname, "..", "..", "web", "public", "logo512.png");
  const image = nativeImage.createFromPath(iconPath);
  if (image.isEmpty()) {
    console.error(`Failed to load app icon at ${iconPath}`);
    return undefined;
  }

  cachedAppIcon = image;
  return cachedAppIcon;
}

function dialogOptions(options) {
  const icon = appIcon();
  return icon ? { ...options, icon } : options;
}

async function authToken() {
  const config = await readConfig();
  const token = config.sessionToken || "";
  authenticated = Boolean(token);
  return token;
}

async function updateTrayTitle() {
  if (!tray) return;
  const config = await readConfig();
  tray.setTitle(titleFor(latestSummary, config.displayMode));
  tray.setToolTip(tooltipFor(latestSummary, config.displayMode));
}

async function updateTrayMenu() {
  await updateTrayTitle();
  if (tray) tray.setContextMenu(await buildMenu());
}

function htmlEscape(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function authCompleteHtml(ok, detail = "") {
  const title = ok ? "Tokenmaxx is signed in" : "Tokenmaxx sign-in failed";
  const message = ok
    ? "You can close this tab. The menu bar app will open the dashboard."
    : detail || "The menu bar app did not receive a session token.";

  return `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>${htmlEscape(title)}</title>
    <style>
      body {
        margin: 0;
        min-height: 100vh;
        display: grid;
        place-items: center;
        background: #080806;
        color: #f4f1e8;
        font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      }

      main {
        width: min(520px, calc(100vw - 40px));
        border: 1px solid rgba(244, 241, 232, 0.18);
        padding: 28px;
        background: rgba(244, 241, 232, 0.04);
      }

      h1 {
        margin: 0 0 10px;
        font-size: 24px;
      }

      p {
        margin: 0;
        color: rgba(244, 241, 232, 0.68);
        line-height: 1.5;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>${htmlEscape(title)}</h1>
      <p>${htmlEscape(message)}</p>
    </main>
  </body>
</html>`;
}

function closeLocalAuthServer() {
  if (localAuthTimeout) {
    clearTimeout(localAuthTimeout);
    localAuthTimeout = null;
  }

  if (localAuthServer) {
    localAuthServer.close();
    localAuthServer = null;
  }
}

async function startLocalAuthCallbackServer() {
  closeLocalAuthServer();

  return new Promise((resolve, reject) => {
    const server = createServer((request, response) => {
      const requestUrl = new URL(request.url || "/", `http://${request.headers.host || "127.0.0.1"}`);
      if (requestUrl.pathname !== "/auth/callback") {
        response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
        response.end("Not found");
        return;
      }

      const token = requestUrl.searchParams.get("token") || "";
      const error = requestUrl.searchParams.get("error") || "";
      response.writeHead(token ? 200 : 400, { "content-type": "text/html; charset=utf-8" });
      response.end(authCompleteHtml(Boolean(token), error));
      closeLocalAuthServer();

      const callbackUrl = new URL(DESKTOP_AUTH_CALLBACK_URL);
      for (const [key, value] of requestUrl.searchParams) {
        callbackUrl.searchParams.set(key, value);
      }

      handleAuthCallback(callbackUrl.toString()).catch((authError) => {
        latestError = authError.message;
        console.error("Failed to complete local auth callback", authError);
        updateTrayMenu().catch(() => {});
      });
    });

    server.once("error", reject);
    server.listen(0, "127.0.0.1", () => {
      const address = server.address();
      if (!address || typeof address === "string") {
        server.close();
        reject(new Error("Failed to start local auth callback server"));
        return;
      }

      localAuthServer = server;
      localAuthTimeout = setTimeout(() => {
        latestError = "GitHub login timed out";
        closeLocalAuthServer();
        updateTrayMenu().catch(() => {});
      }, LOCAL_AUTH_TIMEOUT_MS);

      resolve(`http://127.0.0.1:${address.port}/auth/callback`);
    });
  });
}

async function desktopCallbackUrl() {
  if (app.isPackaged) return DESKTOP_AUTH_CALLBACK_URL;
  return startLocalAuthCallbackServer();
}

function dashboardUrlFromReturnTo(returnTo, fallback) {
  if (!returnTo) return fallback;

  try {
    const candidate = new URL(returnTo);
    const expected = new URL(fallback);
    return candidate.origin === expected.origin ? candidate.toString() : fallback;
  } catch {
    return fallback;
  }
}

function menuLabel(value, maxLength = 86) {
  const text = String(value || "");
  return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
}

async function openDashboard(returnTo = "") {
  const config = await readConfig();
  await shell.openExternal(dashboardUrlFromReturnTo(returnTo, config.webUrl));
}

async function openLogin() {
  const config = await readConfig();
  const authUrl = new URL("/auth/github/start", config.apiUrl);
  authUrl.searchParams.set("return_to", config.webUrl);
  authUrl.searchParams.set("desktop_callback", await desktopCallbackUrl());
  await shell.openExternal(authUrl.toString());
}

async function buildMenu() {
  const config = await readConfig();
  const token = await authToken();
  return Menu.buildFromTemplate([
    {
      label: token ? "Open Dashboard" : "Sign in with GitHub",
      click: () => (token ? openDashboard() : openLogin())
    },
    { type: "separator" },
    {
      label: "Menu Bar Display",
      enabled: Boolean(token),
      submenu: Object.entries(displayModes).map(([mode, label]) => ({
        label,
        type: "radio",
        checked: config.displayMode === mode,
        click: async () => {
          await writeConfig({ ...config, displayMode: mode });
          await updateTrayTitle();
          tray.setContextMenu(await buildMenu());
        }
      }))
    },
    { type: "separator" },
    {
      label: latestSummary
        ? `Today: ${formatCompact(latestSummary.windows.today.totalTokens)} / ${money(latestSummary.windows.today.costUsd)}`
        : token
          ? "Today: scanning..."
          : "Today: sign in required",
      enabled: false
    },
    {
      label: latestSummary
        ? `7 days: ${formatCompact(latestSummary.windows.week.totalTokens)} / ${money(latestSummary.windows.week.costUsd)}`
        : token
          ? "7 days: scanning..."
          : "7 days: sign in required",
      enabled: false
    },
    {
      label: latestSync ? `Last sync: ${new Date(latestSync).toLocaleTimeString()}` : "Last sync: never",
      enabled: false
    },
    ...(latestError
      ? [
          {
            label: `Last error: ${menuLabel(latestError)}`,
            enabled: false
          }
        ]
      : []),
    { type: "separator" },
    {
      label: "Sync Now",
      enabled: Boolean(token),
      click: () => refreshSummary(true)
    },
    { type: "separator" },
    availableUpdate
      ? {
          label:
            availableUpdate.status === "downloaded"
              ? `Restart and Install Tokenmaxx ${availableUpdate.version}`
              : "Downloading Update...",
          enabled: availableUpdate.status === "downloaded",
          click: () => installDownloadedUpdate()
        }
      : {
          label: updateCheckInFlight ? "Checking for Updates..." : "Check for Updates",
          enabled: !updateCheckInFlight,
          click: () => checkForUpdates({ installWhenDownloaded: true })
        },
    ...(updateCheckError
      ? [
          {
            label: `Update check failed: ${updateCheckError}`,
            enabled: false
          }
        ]
      : []),
    ...(updateStatusMessage
      ? [
          {
            label: updateStatusMessage,
            enabled: false
          }
        ]
      : []),
    {
      label: "Quit Tokenmaxx",
      click: () => app.quit()
    }
  ]);
}

function installDownloadedUpdate() {
  if (!downloadedUpdate) return;
  updateStatusMessage = "Installing update...";
  updateTrayMenu().catch(() => {});
  autoUpdater.quitAndInstall();
}

async function checkForUpdates({ installWhenDownloaded = false } = {}) {
  if (process.platform !== "darwin") return null;
  if (!app.isPackaged) return null;
  if (downloadedUpdate) {
    if (installWhenDownloaded) installDownloadedUpdate();
    return downloadedUpdate;
  }
  if (updateCheckInFlight) return null;

  try {
    const config = await readConfig();
    autoUpdater.setFeedURL({
      url: updateFeedUrl(config),
      serverType: "json"
    });
    updateCheckTriggeredByUser = installWhenDownloaded;
    updateCheckInFlight = true;
    updateCheckError = null;
    updateStatusMessage = "";
    await updateTrayMenu();
    autoUpdater.checkForUpdates();
  } catch (error) {
    updateCheckInFlight = false;
    updateCheckTriggeredByUser = false;
    updateCheckError = installWhenDownloaded ? error.message : null;
    updateStatusMessage = "";
    await updateTrayMenu();
  }

  return null;
}

async function refreshSummaryInner(force = false) {
  const token = await authToken();
  if (!token) {
    latestSummary = null;
    latestError = null;
    lastSeenToken = "";
    await updateTrayMenu();
    return false;
  }

  try {
    const config = await readConfig();
    lastSeenToken = token;
    authenticated = true;
    latestError = null;
    await updateTrayMenu();
    latestSummary = await scanUsage();
    const payload = buildIngestPayload(latestSummary, {
      id: config.deviceId,
      ...defaultDevice()
    });
    await postIngest(config.apiUrl, token, payload);
    latestSync = Date.now();
    latestError = null;
    await updateTrayMenu();
    return true;
  } catch (error) {
    const legalAcceptanceRequired =
      error.status === 403 && String(error.message || "").includes("legal_acceptance_required");
    latestError = legalAcceptanceRequired
      ? "Open dashboard to accept updated Terms and Privacy Policy"
      : error.message;
    if (error.status === 401) {
      const config = await readConfig();
      await writeConfig({ ...config, sessionToken: "" });
      authenticated = false;
    }
    if (force) console.error(error);
    await updateTrayMenu();
    return false;
  }
}

async function refreshSummary(force = false) {
  if (refreshPromise) return refreshPromise;
  refreshPromise = refreshSummaryInner(force).finally(() => {
    refreshPromise = null;
  });
  return refreshPromise;
}

function isDesktopAuthUrl(value = "") {
  try {
    const url = new URL(value);
    return url.protocol === `${DESKTOP_PROTOCOL}:` && url.hostname === "auth" && url.pathname === "/callback";
  } catch {
    return false;
  }
}

function registerProtocolClient() {
  try {
    const registered =
      process.defaultApp && process.argv[1]
        ? app.setAsDefaultProtocolClient(DESKTOP_PROTOCOL, process.execPath, [path.resolve(process.argv[1])])
        : app.setAsDefaultProtocolClient(DESKTOP_PROTOCOL);
    if (!registered && app.isPackaged) {
      console.error(`Failed to register ${DESKTOP_PROTOCOL}:// protocol handler`);
    }
  } catch (error) {
    latestError = error.message;
    console.error("Failed to register desktop auth protocol", error);
  }
}

function protocolParamsFromUrl(rawUrl) {
  const url = new URL(rawUrl);
  const params = new URLSearchParams(url.searchParams);
  if (url.hash.startsWith("#")) {
    for (const [key, value] of new URLSearchParams(url.hash.slice(1))) {
      params.set(key, value);
    }
  }
  return params;
}

async function handleAuthCallback(rawUrl) {
  if (!isDesktopAuthUrl(rawUrl)) return;

  const params = protocolParamsFromUrl(rawUrl);
  const token = params.get("token") || "";
  const error = params.get("error") || "";
  if (!token) {
    latestError = error || "GitHub login did not return a desktop session token";
    await updateTrayMenu();
    await dialog.showMessageBox(dialogOptions({
      type: "warning",
      title: "Tokenmaxx sign-in failed",
      message: "Tokenmaxx could not finish GitHub sign-in.",
      detail: latestError
    }));
    return;
  }

  const config = await readConfig();
  await writeConfig({ ...config, sessionToken: token });
  authenticated = true;
  latestError = null;
  lastSeenToken = "";
  await updateTrayMenu();
  await refreshSummary(true);
  await openDashboard(params.get("return_to") || "");
}

async function handleProtocolUrl(rawUrl) {
  if (!isDesktopAuthUrl(rawUrl)) return;
  if (!app.isReady()) {
    pendingProtocolUrls.push(rawUrl);
    return;
  }
  await handleAuthCallback(rawUrl);
}

async function drainPendingProtocolUrls() {
  while (pendingProtocolUrls.length) {
    await handleAuthCallback(pendingProtocolUrls.shift());
  }
}

app.on("before-quit", () => {
  if (refreshTimer) clearInterval(refreshTimer);
  if (updateCheckTimer) clearInterval(updateCheckTimer);
  closeLocalAuthServer();
});

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

if (singleInstanceLock) {
  app.whenReady().then(async () => {
    if (app.dock) app.dock.hide();
    registerProtocolClient();

    tray = new Tray(trayIcon());
    tray.setTitle("");
    tray.setToolTip("Tokenmaxx");
    tray.on("right-click", async () => {
      tray.popUpContextMenu(await buildMenu());
    });

    await drainPendingProtocolUrls();
    await refreshSummary(true);
    tray.setContextMenu(await buildMenu());
    await updateTrayTitle();

    refreshTimer = setInterval(() => {
      refreshSummary().catch((error) => {
        latestError = error.message;
        console.error("Failed to refresh token summary", error);
      });
    }, REFRESH_MS);

    if (app.isPackaged) {
      checkForUpdates().catch((error) => {
        console.error("Failed to check for updates", error);
      });
      updateCheckTimer = setInterval(() => {
        checkForUpdates().catch((error) => {
          console.error("Failed to check for updates", error);
        });
      }, UPDATE_CHECK_MS);
    }

    if (shouldShowOnLaunch) {
      openDashboard().catch((error) => {
        latestError = error.message;
        console.error("Failed to open dashboard on launch", error);
      });
    }
  });
}
