const DEFAULT_RUNTIME_MANIFEST_PATH = "__w3kits/webcontainer-runtime.json";
const DEFAULT_SERVICE_WORKER_URL = "__w3kits/daemon-proxy-sw.js";
const DEFAULT_SERVICE_WORKER_SCOPE = "/";
const DEFAULT_DAEMON_PORT = 7456;
const DEFAULT_OD_DATA_DIR = "/home/agent/.config/opendesign";
const DEFAULT_OD_DISK_ROOT = "/home/agent/.config/opendesign";
const W3KITS_DEFAULT_MODEL = "gpt-5.4-mini";
const W3KITS_DEFAULT_IMAGE_MODEL = "gpt-image-2";
const W3KITS_PLUGIN_API_KEY = "w3kits-plugin-user";

export const w3kitsOpenDesignDaemon = {
  pluginId: "opendesign",
  mode: "upstream-daemon-webcontainer",
  runtimeManifest: DEFAULT_RUNTIME_MANIFEST_PATH,
  serviceWorker: DEFAULT_SERVICE_WORKER_URL,
  daemonEntry: "__w3kits/webcontainer-runtime/apps/daemon/dist/cli.js",
  startCommand: ["node", "__w3kits/webcontainer-runtime/apps/daemon/dist/cli.js", "--host", "0.0.0.0", "--port", "7456", "--no-open"],
  healthPath: "/api/health",
  proxiedPaths: ["/api/*", "/artifacts/*"],
  unsupportedErrorCode: "unsupported_in_w3kits_webcontainer_v1",
};

function assertCrossOriginIsolated() {
  if (typeof globalThis.crossOriginIsolated !== "undefined" && !globalThis.crossOriginIsolated) {
    throw new Error("w3kits_webcontainer_requires_cross_origin_isolation");
  }
}

function packageAssetUrl(relativePath, options = {}) {
  if (options.assetUrl) return options.assetUrl(relativePath);
  if (options.runtimeBaseUrl) return new URL(relativePath, options.runtimeBaseUrl).toString();
  return relativePath;
}

async function loadRuntimeManifest(manifestPath = DEFAULT_RUNTIME_MANIFEST_PATH, options = {}) {
  const response = await fetch(packageAssetUrl(manifestPath, options), { cache: "force-cache" });
  if (!response.ok) throw new Error("w3kits_webcontainer_manifest_unavailable");
  return response.json();
}

function absoluteWebContainerPath(path) {
  const normalized = String(path || "").replace(/^/+|/+$|\/g, "");
  return normalized ? "/" + normalized : "/";
}

async function installRuntimeDependencies(webcontainer, runtimeRoot, options = {}) {
  const markers = [
    "node_modules/express/package.json",
    "node_modules/undici/package.json",
    "node_modules/@open-design/platform/package.json",
  ];
  const packaged = await Promise.all(markers.map(async (marker) => {
    try {
      await webcontainer.fs.readFile(runtimeRoot.replace(/\/+$/, "") + "/" + marker);
      return true;
    } catch {
      return false;
    }
  }));
  if (packaged.every(Boolean)) {
    options.onLog?.("Using packaged runtime dependencies in " + runtimeRoot);
    return;
  }

  const installProcess = await webcontainer.spawn("npm", ["install", "--ignore-scripts", "--no-package-lock", "--no-fund", "--no-audit"], {
    cwd: absoluteWebContainerPath(runtimeRoot),
    env: { W3KITS_WEBCONTAINER: "1" },
  });
  installProcess.output?.pipeTo?.(new WritableStream({
    write(chunk) {
      options.onLog?.(String(chunk));
    },
  })).catch((error) => options.onError?.(error));
  const exitCode = await installProcess.exit;
  if (exitCode !== 0) {
    throw new Error("w3kits_opendesign_runtime_install_failed_" + exitCode);
  }
}

async function waitForServerReady(webcontainer, expectedPort, timeoutMs) {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => reject(new Error("w3kits_opendesign_daemon_start_timeout")), timeoutMs);
    const dispose = webcontainer.on("server-ready", (port, url) => {
      if (port !== expectedPort) return;
      clearTimeout(timeout);
      dispose?.();
      resolve(url);
    });
  });
}

function mergeEnv(runtime, inputEnv) {
  const hostOrigin = globalThis.location?.origin || "https://w3kits.com";
  return {
    ...inputEnv,
    OD_BIND_HOST: "0.0.0.0",
    OD_PORT: String(runtime.daemon.port || DEFAULT_DAEMON_PORT),
    OD_ALLOWED_ORIGINS: hostOrigin,
    OD_DATA_DIR: runtime.persistence?.dataDir || DEFAULT_OD_DATA_DIR,
    OD_RESOURCE_ROOT: runtime.resources?.root || "__w3kits/webcontainer-runtime/resources",
    W3KITS_WEBCONTAINER: "1",
    W3KITS_UNSUPPORTED_ERROR_CODE: runtime.unsupportedLocalOnlyFeatures?.error?.code || "unsupported_in_w3kits_webcontainer_v1",
    W3KITS_OPENAI_BASE_URL: runtime.ai?.openaiBaseUrl || "https://w3kits.com/api/ai/openai/v1",
    W3KITS_RUNTIME_SESSION: inputEnv.W3KITS_RUNTIME_SESSION || runtime.ai?.runtimeSession || "",
    W3KITS_PLUGIN_ID: runtime.pluginId || inputEnv.W3KITS_PLUGIN_ID || "",
    W3KITS_PLUGIN_VERSION: runtime.version || inputEnv.W3KITS_PLUGIN_VERSION || "",
    W3KITS_PLUGIN_PACKAGE: runtime.packageName || inputEnv.W3KITS_PLUGIN_PACKAGE || "",
    W3KITS_PLUGIN_INTEGRITY: runtime.packageIntegrity || inputEnv.W3KITS_PLUGIN_INTEGRITY || "",
  };
}

function pathJoin(...parts) {
  return parts.join("/").replace(/\\/g, "/").replace(/\/+/g, "/");
}

function parentPath(filePath) {
  const normalized = filePath.replace(/\\/g, "/");
  const slash = normalized.lastIndexOf("/");
  return slash <= 0 ? "/" : normalized.slice(0, slash);
}

function relativePath(root, filePath) {
  const normalizedRoot = root.replace(/\/+$/, "");
  return filePath === normalizedRoot ? "" : filePath.slice(normalizedRoot.length + 1);
}

function bytesSignature(bytes) {
  let hash = 2166136261;
  for (const byte of bytes) {
    hash ^= byte;
    hash = Math.imul(hash, 16777619);
  }
  return bytes.byteLength + ":" + (hash >>> 0).toString(16);
}

async function listFiles(webcontainer, root) {
  const files = [];
  async function walk(dir) {
    let entries;
    try {
      entries = await webcontainer.fs.readdir(dir, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      const name = typeof entry.name === "string" ? entry.name : new TextDecoder().decode(entry.name);
      const next = pathJoin(dir, name);
      if (entry.isDirectory?.()) {
        await walk(next);
      } else if (entry.isFile?.()) {
        files.push(next);
      }
    }
  }
  await walk(root);
  return files;
}

async function ensureDataDir(webcontainer, runtime, options = {}) {
  const dataDir = runtime.persistence?.dataDir || DEFAULT_OD_DATA_DIR;
  try {
    await webcontainer.fs.mkdir(dataDir, { recursive: true });
  } catch (error) {
    options.onError?.(error);
  }
}

async function readJsonFile(webcontainer, filePath) {
  try {
    const body = await webcontainer.fs.readFile(filePath, "utf8");
    const parsed = JSON.parse(body);
    return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
  } catch {
    return {};
  }
}

async function writeJsonFile(webcontainer, filePath, value) {
  await webcontainer.fs.mkdir(parentPath(filePath), { recursive: true });
  await webcontainer.fs.writeFile(filePath, JSON.stringify(value, null, 2));
}

async function seedW3KitsDefaults(webcontainer, runtime, env, options = {}) {
  const dataDir = runtime.persistence?.dataDir || DEFAULT_OD_DATA_DIR;
  const openaiBaseUrl = env.W3KITS_OPENAI_BASE_URL || runtime.ai?.openaiBaseUrl || "https://w3kits.com/api/ai/openai/v1";
  const appConfigPath = pathJoin(dataDir, "app-config.json");
  const mediaConfigPath = pathJoin(dataDir, "media-config.json");

  try {
    const appConfig = await readJsonFile(webcontainer, appConfigPath);
    const codexEnv = {
      ...(appConfig.agentCliEnv?.codex || {}),
      OPENAI_BASE_URL: appConfig.agentCliEnv?.codex?.OPENAI_BASE_URL || openaiBaseUrl,
      OPENAI_API_KEY: appConfig.agentCliEnv?.codex?.OPENAI_API_KEY || W3KITS_PLUGIN_API_KEY,
    };
    const nextAppConfig = {
      ...appConfig,
      onboardingCompleted: appConfig.onboardingCompleted === false ? false : true,
      agentId: appConfig.agentId || "codex",
      agentModels: {
        ...(appConfig.agentModels || {}),
        codex: {
          ...(appConfig.agentModels?.codex || {}),
          model: appConfig.agentModels?.codex?.model || W3KITS_DEFAULT_MODEL,
        },
      },
      agentCliEnv: {
        ...(appConfig.agentCliEnv || {}),
        codex: codexEnv,
      },
    };
    await writeJsonFile(webcontainer, appConfigPath, nextAppConfig);

    const mediaConfig = await readJsonFile(webcontainer, mediaConfigPath);
    const nextMediaConfig = {
      ...mediaConfig,
      providers: {
        ...(mediaConfig.providers || {}),
        openai: {
          ...(mediaConfig.providers?.openai || {}),
          apiKey: mediaConfig.providers?.openai?.apiKey || W3KITS_PLUGIN_API_KEY,
          baseUrl: mediaConfig.providers?.openai?.baseUrl || openaiBaseUrl,
          model: mediaConfig.providers?.openai?.model || W3KITS_DEFAULT_IMAGE_MODEL,
        },
      },
    };
    await writeJsonFile(webcontainer, mediaConfigPath, nextMediaConfig);
    options.onLog?.("[w3kits defaults] seeded OpenDesign AI and media config");
  } catch (error) {
    options.onError?.(error);
  }
}

function shouldPersistFile(runtime, dataDir, filePath) {
  const rel = relativePath(dataDir, filePath);
  if (!rel || rel.startsWith("node_modules/") || rel.includes("/node_modules/")) return false;
  const includes = runtime.persistence?.include || [];
  if (!includes.length) return true;
  return includes.some((pattern) => {
    if (pattern.endsWith("/**")) return rel === pattern.slice(0, -3) || rel.startsWith(pattern.slice(0, -2));
    return rel === pattern;
  });
}

function diskFilesEndpoint(runtime, options) {
  if (options.diskFilesEndpoint) return options.diskFilesEndpoint;
  const pluginId = options.pluginId || runtime.pluginId || "opendesign";
  return "/api/plugins/" + encodeURIComponent(pluginId) + "/webcontainer/disk/files";
}

function startWebContainerAutosave(webcontainer, runtime, options = {}) {
  const token = options.r2DiskSession?.token || options.runtimeSession;
  if (!token || typeof fetch !== "function") return { stop() {} };
  const dataDir = runtime.persistence?.dataDir || DEFAULT_OD_DATA_DIR;
  const diskRoot = runtime.persistence?.diskRoot || DEFAULT_OD_DISK_ROOT;
  const workspaceId = options.r2DiskSession?.workspaceId || options.workspaceId || "default";
  const endpoint = diskFilesEndpoint(runtime, options);
  const intervalMs = runtime.persistence?.flushPolicy?.intervalMs || 30000;
  const seen = new Map();
  let stopped = false;
  let flushing = false;

  async function upload(filePath, bytes) {
    const relativeFilePath = relativePath(dataDir, filePath);
    const url = new URL(endpoint, globalThis.location?.origin || "https://w3kits.com");
    url.searchParams.set("workspaceId", workspaceId);
    url.searchParams.set("path", pathJoin(diskRoot, relativeFilePath));
    const response = await fetch(url.toString(), {
      method: "PUT",
      credentials: "same-origin",
      headers: {
        "content-type": "application/octet-stream",
        "x-w3kits-runtime-session": token,
      },
      body: bytes,
    });
    if (!response.ok) throw new Error("w3kits_disk_autosave_upload_failed:" + response.status + ":" + filePath);
  }

  async function flush(reason = "interval") {
    if (stopped || flushing) return;
    flushing = true;
    try {
      const files = await listFiles(webcontainer, dataDir);
      for (const filePath of files) {
        if (!shouldPersistFile(runtime, dataDir, filePath)) continue;
        let bytes;
        try {
          bytes = await webcontainer.fs.readFile(filePath);
        } catch {
          continue;
        }
        const signature = bytesSignature(bytes);
        if (seen.get(filePath) === signature) continue;
        await upload(filePath, bytes);
        seen.set(filePath, signature);
      }
      options.onLog?.("[w3kits autosave] flushed " + files.length + " files (" + reason + ")");
    } catch (error) {
      options.onError?.(error);
    } finally {
      flushing = false;
    }
  }

  const timer = globalThis.setInterval?.(() => void flush("interval"), intervalMs);
  const lifecycleFlush = () => void flush("lifecycle");
  globalThis.addEventListener?.("visibilitychange", lifecycleFlush);
  globalThis.addEventListener?.("pagehide", lifecycleFlush);
  void flush("startup");
  return {
    flush,
    stop() {
      stopped = true;
      if (timer) globalThis.clearInterval?.(timer);
      globalThis.removeEventListener?.("visibilitychange", lifecycleFlush);
      globalThis.removeEventListener?.("pagehide", lifecycleFlush);
    },
  };
}

export async function bootW3KitsOpenDesignWebContainer(options = {}) {
  assertCrossOriginIsolated();
  const runtime = options.runtimeManifest || await loadRuntimeManifest(options.runtimeManifestPath || DEFAULT_RUNTIME_MANIFEST_PATH, options);
  const WebContainer = options.WebContainer || globalThis.WebContainer;
  if (!WebContainer?.boot) throw new Error("w3kits_webcontainer_api_unavailable");

  const webcontainer = options.webcontainer || await WebContainer.boot(options.bootOptions || {});
  if (options.mountTree) await webcontainer.mount(options.mountTree);
  if (options.mounts) {
    for (const mount of options.mounts) await webcontainer.mount(mount.tree, mount.options);
  }

  const runtimeRoot = runtime.runtimeRoot || "__w3kits/webcontainer-runtime";
  options.onLog?.("Installing runtime dependencies in " + runtimeRoot);
  await installRuntimeDependencies(webcontainer, runtimeRoot, options);

  const env = mergeEnv(runtime, options.env || {});
  const command = options.command || runtime.daemon.startCommand;
  await ensureDataDir(webcontainer, runtime, options);
  await seedW3KitsDefaults(webcontainer, runtime, env, options);
  const process = await webcontainer.spawn(command[0], command.slice(1), { cwd: "/", env });
  process.output?.pipeTo?.(new WritableStream({
    write(chunk) {
      options.onLog?.(String(chunk));
    },
  })).catch((error) => options.onError?.(error));

  const daemonUrl = await waitForServerReady(webcontainer, runtime.daemon.port || DEFAULT_DAEMON_PORT, options.startTimeoutMs || 30000);
  const autosave = startWebContainerAutosave(webcontainer, runtime, options);
  return { webcontainer, process, daemonUrl, runtime, autosave };
}
