import electron from "electron";
import esbuild from "esbuild";
import { createHash } from "node:crypto";
import { spawn } from "node:child_process";
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
import {
  buildStyles,
  cleanDist,
  browserRendererHtmlInput,
  cliSourceRoot,
  coreSourceRoot,
  copyAppAssets,
  copyBrowserRendererHtml,
  copyCliRuntimeToElectronDist,
  copyMarketplacePlugins,
  copyModelCatalog,
  copyRendererHtml,
  copyTrayRendererHtml,
  createBrowserRendererBuildOptions,
  createCliBuildOptions,
  createMainBuildOptions,
  createRendererBuildOptions,
  createTrayRendererBuildOptions,
  createWebClientBridgeBuildOptions,
  appAssetsInput,
  modelCatalogInput,
  projectRoot,
  rendererRoot,
  rendererHtmlInput,
  syncUiRendererToRuntimeDists,
  trayRendererHtmlInput,
  watchPlugin
} from "./esbuild.config.mjs";

let electronProcess = null;
let restartTimer = null;
let restartInFlight = false;
let restartQueued = false;
let pendingRestartReasons = [];
const watchSignatures = new Map();
let shuttingDown = false;
const restartDelayMs = 160;
const styleBuildDelayMs = 160;
const stylePollIntervalMs = 1000;
const ignoredSignatureEntries = new Set([".DS_Store"]);
let styleBuildTimer = null;
let styleBuildInFlight = false;
let queuedStyleBuildReason = null;
const ready = {
  browser: false,
  cli: false,
  main: false,
  renderer: false,
  tray: false,
  webBridge: false
};
const devTarget = parseDevTarget(process.argv.slice(2));
const enabled = {
  cli: devTarget === "cli" || devTarget === "electron",
  electron: devTarget === "electron",
  ui: true
};
const coreSharedSourceRoot = path.join(coreSourceRoot, "shared");
const styleWatchRoots = [rendererRoot, coreSharedSourceRoot].filter((watchRoot) => existsSync(watchRoot));
const activeReadyNames = new Set([
  ...(enabled.ui ? ["browser", "renderer", "tray", "webBridge"] : []),
  ...(enabled.cli ? ["cli"] : []),
  ...(enabled.electron ? ["main"] : [])
]);

function parseDevTarget(args) {
  const target = args[0] ?? "electron";
  if (target === "--help" || target === "-h") {
    console.log("Usage: node build/dev.mjs [ui|cli|electron]");
    process.exit(0);
  }
  if (target === "ui" || target === "cli" || target === "electron") {
    return target;
  }
  console.error(`Unknown dev target "${target}". Expected ui, cli, or electron.`);
  process.exit(2);
}

function logDev(message) {
  console.log(`[dev] ${new Date().toISOString()} ${message}`);
}

function relativePath(file) {
  return path.relative(projectRoot, file) || ".";
}

function readyState() {
  return Object.entries(ready)
    .filter(([name]) => activeReadyNames.has(name))
    .map(([name, value]) => `${name}:${value ? "ready" : "pending"}`)
    .join(" ");
}

function contentSignature(targetPath) {
  try {
    return readContentSignature(targetPath);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return {
      key: `error:${message}`,
      summary: `signature-error=${message}`
    };
  }
}

function readContentSignature(targetPath) {
  if (!existsSync(targetPath)) {
    return {
      key: "missing",
      summary: "missing"
    };
  }

  const stats = statSync(targetPath);
  if (stats.isDirectory()) {
    return directorySignature(targetPath);
  }

  const content = readFileSync(targetPath);
  const hash = createHash("sha1").update(content).digest("hex").slice(0, 12);
  return {
    key: `file:${hash}`,
    summary: `size=${stats.size} mtime=${stats.mtime.toISOString()} ctime=${stats.ctime.toISOString()} sha1=${hash}`
  };
}

function directorySignature(targetPath) {
  const files = listDirectoryFiles(targetPath);
  const hash = createHash("sha1");
  let newestMtimeMs = 0;

  for (const file of files) {
    const absolutePath = path.join(targetPath, file);
    const stats = statSync(absolutePath);
    newestMtimeMs = Math.max(newestMtimeMs, stats.mtimeMs);
    hash.update(file);
    hash.update("\0");
    hash.update(readFileSync(absolutePath));
    hash.update("\0");
  }

  const digest = hash.digest("hex").slice(0, 12);
  const newestMtime = newestMtimeMs > 0 ? new Date(newestMtimeMs).toISOString() : "none";
  return {
    key: `dir:${digest}`,
    summary: `files=${files.length} newestMtime=${newestMtime} sha1=${digest}`
  };
}

function listDirectoryFiles(targetPath, basePath = targetPath) {
  const entries = readdirSync(targetPath, { withFileTypes: true })
    .filter((entry) => !ignoredSignatureEntries.has(entry.name))
    .sort((a, b) => a.name.localeCompare(b.name));
  const files = [];

  for (const entry of entries) {
    const absolutePath = path.join(targetPath, entry.name);
    const relative = path.relative(basePath, absolutePath);
    if (entry.isDirectory()) {
      files.push(...listDirectoryFiles(absolutePath, basePath));
    } else if (entry.isFile()) {
      files.push(relative);
    }
  }

  return files;
}

function rememberWatchSignature(label, targetPath, options = {}) {
  const signature = options.metadataOnly
    ? metadataSignature(targetPath)
    : contentSignature(targetPath);
  watchSignatures.set(label, signature.key);
  logDev(`watch baseline: ${label} ${relativePath(targetPath)}; ${signature.summary}`);
}

function scheduleStyleBuild(reason) {
  queuedStyleBuildReason = reason;
  if (styleBuildTimer) {
    clearTimeout(styleBuildTimer);
  }
  styleBuildTimer = setTimeout(() => {
    styleBuildTimer = null;
    void rebuildStyles(queuedStyleBuildReason ?? reason);
  }, styleBuildDelayMs);
}

async function rebuildStyles(reason) {
  if (styleBuildInFlight) {
    queuedStyleBuildReason = reason;
    return;
  }

  styleBuildInFlight = true;
  queuedStyleBuildReason = null;
  try {
    logDev(`rebuilding styles: ${reason}`);
    await buildStyles({ minify: false });
    syncUiRendererToRuntimeDists();
    if (enabled.electron) {
      scheduleRestart(`styles rebuilt: ${reason}`);
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logDev(`style rebuild failed: ${message}`);
  } finally {
    styleBuildInFlight = false;
    if (queuedStyleBuildReason) {
      const queuedReason = queuedStyleBuildReason;
      queuedStyleBuildReason = null;
      scheduleStyleBuild(queuedReason);
    }
  }
}

function pollStyleWatchRoots() {
  for (const styleWatchRoot of styleWatchRoots) {
    const label = `styles ${relativePath(styleWatchRoot)}`;
    const signature = contentSignature(styleWatchRoot);
    const previousSignature = watchSignatures.get(label);
    if (previousSignature === signature.key) {
      continue;
    }

    watchSignatures.set(label, signature.key);
    logDev(`watch event: ${label}; ${signature.summary}; content=changed`);
    scheduleStyleBuild(label);
  }
}

function pollWatchedInput(label, targetPath, onChange, options = {}) {
  const signature = options.metadataOnly
    ? metadataSignature(targetPath)
    : contentSignature(targetPath);
  const previousSignature = watchSignatures.get(label);
  if (previousSignature === signature.key) {
    return;
  }

  watchSignatures.set(label, signature.key);
  logDev(`watch event: ${label} ${relativePath(targetPath)}; ${signature.summary}; content=changed`);
  try {
    onChange();
    if (enabled.electron && options.restart !== false) {
      scheduleRestart(label);
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logDev(`watch action failed: ${label}; ${message}`);
  }
}

function metadataSignature(targetPath) {
  if (!existsSync(targetPath)) {
    return {
      key: "missing",
      summary: "missing"
    };
  }

  const stats = statSync(targetPath);
  return {
    key: `metadata:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`,
    summary: `size=${stats.size} mtime=${stats.mtime.toISOString()} ctime=${stats.ctime.toISOString()}`
  };
}

function pollSourceWatchTargets() {
  pollWatchedInput("home html", rendererHtmlInput, () => {
    copyRendererHtml();
    syncUiRendererToRuntimeDists();
  });
  pollWatchedInput("browser html", browserRendererHtmlInput, () => {
    copyBrowserRendererHtml();
    syncUiRendererToRuntimeDists();
  });
  pollWatchedInput("tray html", trayRendererHtmlInput, () => {
    copyTrayRendererHtml();
    syncUiRendererToRuntimeDists();
  });
  if (enabled.electron) {
    pollWatchedInput("app assets", appAssetsInput, copyAppAssets);
  }
  if ((enabled.cli || enabled.electron) && existsSync(modelCatalogInput)) {
    pollWatchedInput("model catalog", modelCatalogInput, copyModelCatalog, { metadataOnly: true });
  }
}

function markReady(name, reason = `${name} esbuild completed`) {
  if (name === "browser" || name === "cli" || name === "main" || name === "renderer" || name === "tray" || name === "webBridge") {
    ready[name] = true;
  }
  logDev(`build ready: ${reason}; ${readyState()}`);
  if (enabled.electron && Array.from(activeReadyNames).every((readyName) => ready[readyName])) {
    scheduleRestart(reason);
  }
}

function scheduleRestart(reason = "unknown trigger") {
  if (shuttingDown) {
    logDev(`restart ignored during shutdown: ${reason}`);
    return;
  }
  pendingRestartReasons.push(reason);
  if (restartTimer) {
    clearTimeout(restartTimer);
    logDev(`restart rescheduled in ${restartDelayMs}ms: ${reason}`);
  } else {
    logDev(`restart scheduled in ${restartDelayMs}ms: ${reason}`);
  }
  restartTimer = setTimeout(restartElectron, restartDelayMs);
}

async function restartElectron() {
  if (restartInFlight) {
    restartQueued = true;
    return;
  }
  restartInFlight = true;
  const reasons = Array.from(new Set(pendingRestartReasons));
  pendingRestartReasons = [];
  restartTimer = null;

  try {
    if (electronProcess) {
      await stopElectron(electronProcess);
    }

    if (shuttingDown) {
      return;
    }
    logDev(`starting Electron; reasons=${reasons.join(" | ") || "initial start"}`);
    const child = spawn(electron, ["."], {
      cwd: projectRoot,
      env: {
        ...process.env,
        NODE_ENV: "development"
      },
      stdio: "inherit"
    });
    electronProcess = child;
    logDev(`Electron started pid=${child.pid ?? "unknown"}`);
    child.on("exit", (code, signal) => {
      logDev(`Electron exited pid=${child.pid ?? "unknown"} code=${code ?? "null"} signal=${signal ?? "null"}`);
      if (electronProcess === child) {
        electronProcess = null;
      }
    });
  } finally {
    restartInFlight = false;
    if (restartQueued || pendingRestartReasons.length > 0) {
      restartQueued = false;
      scheduleRestart("changes queued during Electron restart");
    }
  }
}

async function stopElectron(child) {
  logDev(`stopping Electron pid=${child.pid ?? "unknown"}`);
  if (child.exitCode !== null || child.signalCode !== null) {
    if (electronProcess === child) {
      electronProcess = null;
    }
    return;
  }

  await new Promise((resolve) => {
    let forceTimer = null;
    let giveUpTimer = null;
    const finish = () => {
      if (forceTimer) clearTimeout(forceTimer);
      if (giveUpTimer) clearTimeout(giveUpTimer);
      child.off("exit", finish);
      resolve();
    };
    child.once("exit", finish);
    child.kill();
    forceTimer = setTimeout(() => {
      if (child.exitCode === null && child.signalCode === null) {
        logDev(`force stopping Electron pid=${child.pid ?? "unknown"}`);
        child.kill("SIGKILL");
      }
    }, 2_000);
    giveUpTimer = setTimeout(finish, 5_000);
  });
  if (electronProcess === child) {
    electronProcess = null;
  }
}

logDev(`starting dev build target=${devTarget} ui=${enabled.ui ? "on" : "off"} cli=${enabled.cli ? "on" : "off"} electron=${enabled.electron ? "on" : "off"}`);
cleanDist();
if (enabled.electron) {
  copyAppAssets();
}
if (enabled.cli || enabled.electron) {
  copyMarketplacePlugins();
  copyModelCatalog();
}
copyBrowserRendererHtml();
copyRendererHtml();
copyTrayRendererHtml();
await buildStyles({ minify: false });
syncUiRendererToRuntimeDists();

rememberWatchSignature("home html", rendererHtmlInput);
rememberWatchSignature("browser html", browserRendererHtmlInput);
rememberWatchSignature("tray html", trayRendererHtmlInput);
for (const styleWatchRoot of styleWatchRoots) {
  rememberWatchSignature(`styles ${relativePath(styleWatchRoot)}`, styleWatchRoot);
}
if (enabled.electron) {
  rememberWatchSignature("app assets", appAssetsInput);
}
if ((enabled.cli || enabled.electron) && existsSync(modelCatalogInput)) {
  rememberWatchSignature("model catalog", modelCatalogInput, { metadataOnly: true });
}

const sourcePoller = setInterval(() => {
  pollStyleWatchRoots();
  pollSourceWatchTargets();
}, stylePollIntervalMs);

const contexts = [];

if (enabled.electron) {
  contexts.push(
    await esbuild.context(
      createMainBuildOptions({
        mode: "development",
        plugins: [watchPlugin("main", (name) => markReady(name))]
      })
    )
  );
}

if (enabled.cli) {
  contexts.push(
    await esbuild.context(
      createCliBuildOptions({
        mode: "development",
        plugins: [
          watchPlugin("cli", (name) => {
            if (enabled.electron) {
              copyCliRuntimeToElectronDist();
            }
            markReady(name);
          })
        ]
      })
    )
  );
}

if (enabled.ui) {
  contexts.push(
    await esbuild.context(
      createRendererBuildOptions({
        mode: "development",
        plugins: [
          watchPlugin("renderer", (name) => {
            copyRendererHtml();
            syncUiRendererToRuntimeDists();
            markReady(name);
          })
        ]
      })
    ),
    await esbuild.context(
      createTrayRendererBuildOptions({
        mode: "development",
        plugins: [
          watchPlugin("tray", (name) => {
            copyTrayRendererHtml();
            syncUiRendererToRuntimeDists();
            markReady(name);
          })
        ]
      })
    ),
    await esbuild.context(
      createBrowserRendererBuildOptions({
        mode: "development",
        plugins: [
          watchPlugin("browser", (name) => {
            copyBrowserRendererHtml();
            syncUiRendererToRuntimeDists();
            markReady(name);
          })
        ]
      })
    ),
    await esbuild.context(
      createWebClientBridgeBuildOptions({
        mode: "development",
        plugins: [
          watchPlugin("webBridge", (name) => {
            syncUiRendererToRuntimeDists();
            markReady(name);
          })
        ]
      })
    )
  );
}

await Promise.all(contexts.map((context) => context.watch()));
logDev("watchers are active");

async function shutdown() {
  logDev("shutting down dev build");
  shuttingDown = true;
  if (restartTimer) {
    clearTimeout(restartTimer);
  }
  if (electronProcess) {
    await stopElectron(electronProcess);
  }
  if (styleBuildTimer) {
    clearTimeout(styleBuildTimer);
  }
  clearInterval(sourcePoller);
  await Promise.all(contexts.map((context) => context.dispose()));
  process.exit(0);
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
