import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import { once } from "node:events";
import { fileURLToPath } from "node:url";
import { AppConfigService } from "../../apps/desktop/src/main/services/app-config.js";
import { ProjectBundleService } from "../../apps/desktop/src/main/services/project-bundle.js";
import { ProjectStoreService } from "../../apps/desktop/src/main/services/project-store.js";
import type { RemoteSkillAsset } from "../../apps/desktop/src/main/services/skill-store.js";

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

function createRetryableWindowsError(message: string, code: "EBUSY" | "EPERM" | "ENOTEMPTY" = "EBUSY"): NodeJS.ErrnoException {
  const error = new Error(message) as NodeJS.ErrnoException;
  error.code = code;
  return error;
}

async function pathExists(targetPath: string): Promise<boolean> {
  try {
    await stat(targetPath);
    return true;
  } catch {
    return false;
  }
}

async function readJsonFile<T>(filePath: string): Promise<T | null> {
  try {
    const raw = await readFile(filePath, "utf8");
    return JSON.parse(raw) as T;
  } catch {
    return null;
  }
}

async function main(): Promise<void> {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);
  const repoRoot = path.resolve(__dirname, "..", "..");
  const resultPath = path.resolve(process.argv[2] ?? path.join(repoRoot, ".tmp", "project-bundle-replacement-smoke", "result.json"));
  const bundleZipPathA = path.resolve(process.argv[3] ?? path.join(path.dirname(resultPath), "bundle-a.zip"));
  const bundleZipPathB = path.resolve(process.argv[4] ?? path.join(path.dirname(resultPath), "bundle-b.zip"));
  const bundleZipPathC = path.resolve(process.argv[5] ?? path.join(path.dirname(resultPath), "bundle-c.zip"));
  const bundleSourceRootA = path.resolve(process.argv[6] ?? path.join(path.dirname(resultPath), "bundle-src-a"));
  const bundleSourceRootB = path.resolve(process.argv[7] ?? path.join(path.dirname(resultPath), "bundle-src-b"));
  const bundleSourceRootC = path.resolve(process.argv[8] ?? path.join(path.dirname(resultPath), "bundle-src-c"));
  const tempRoot = path.dirname(resultPath);
  const userDataPath = path.join(tempRoot, "user-data");
  const projectId = "bundle-replacement-smoke";
  const configVersion = "bundle-replacement-config-v1";
  let activeVariant: "a" | "b" | "c" = "a";

  assert(await pathExists(bundleZipPathA), `Bundle zip fixture A was not found: ${bundleZipPathA}`);
  assert(await pathExists(bundleZipPathB), `Bundle zip fixture B was not found: ${bundleZipPathB}`);
  assert(await pathExists(bundleZipPathC), `Bundle zip fixture C was not found: ${bundleZipPathC}`);
  assert(await pathExists(bundleSourceRootA), `Bundle source fixture A was not found: ${bundleSourceRootA}`);
  assert(await pathExists(bundleSourceRootB), `Bundle source fixture B was not found: ${bundleSourceRootB}`);
  assert(await pathExists(bundleSourceRootC), `Bundle source fixture C was not found: ${bundleSourceRootC}`);
  await rm(userDataPath, { recursive: true, force: true });
  await mkdir(userDataPath, { recursive: true });

  const variantSkillEntry = {
    a: "replacement-skill-a",
    b: "replacement-skill-b",
    c: "replacement-skill-c"
  } as const;
  const variantCronEntry = {
    a: "replacement-task-a.txt",
    b: "replacement-task-b.txt",
    c: "replacement-task-c.txt"
  } as const;

  const server = http.createServer(async (request, response) => {
    try {
      if (request.url !== "/bundle.zip") {
        response.statusCode = 404;
        response.end("not found");
        return;
      }

      const variantPath = activeVariant === "a" ? bundleZipPathA : activeVariant === "b" ? bundleZipPathB : bundleZipPathC;
      const payload = await readFile(variantPath);
      const etag = activeVariant === "a"
        ? '"bundle-replacement-a"'
        : activeVariant === "b"
          ? '"bundle-replacement-b"'
          : '"bundle-replacement-c"';
      const lastModified = activeVariant === "a"
        ? "Tue, 31 Mar 2026 14:00:00 GMT"
        : activeVariant === "b"
          ? "Tue, 31 Mar 2026 14:05:00 GMT"
          : "Tue, 31 Mar 2026 14:10:00 GMT";

      if (request.method === "HEAD") {
        response.writeHead(200, {
          ETag: etag,
          "Last-Modified": lastModified,
          "Content-Length": "4096",
          "Cache-Control": "no-store"
        });
        response.end();
        return;
      }

      if (request.method === "GET") {
        response.writeHead(200, {
          "Content-Type": "application/zip",
          "Cache-Control": "no-store"
        });
        response.end(payload);
        return;
      }

      response.statusCode = 405;
      response.end("method not allowed");
    } catch (error) {
      response.statusCode = 500;
      response.end(error instanceof Error ? error.message : String(error));
    }
  });

  server.listen(0, "127.0.0.1");
  await once(server, "listening");
  const address = server.address();
  assert(address && typeof address === "object", "Failed to start local replacement smoke server.");
  const downloadUrl = `http://127.0.0.1:${address.port}/bundle.zip`;

  try {
    const configService = new AppConfigService(userDataPath);
    await configService.load();

    const projectStore = new ProjectStoreService(configService);
    await projectStore.initialize();
    const projectBundleService = new ProjectBundleService(configService, projectStore);
    const projectBundleServiceWithTestHooks = projectBundleService as unknown as {
      extractZip(zipPath: string, destinationPath: string): Promise<void>;
      withWindowsFsRetry?<T>(
        context: { action: string; path: string; backupPath?: string; projectId?: string },
        operation: () => Promise<T>
      ): Promise<T>;
      moveReplacementPath?(sourcePath: string, targetPath: string, action: string, projectId?: string): Promise<void>;
    };
    projectBundleServiceWithTestHooks.extractZip = async (_zipPath: string, destinationPath: string) => {
      const sourceRoot = activeVariant === "a"
        ? bundleSourceRootA
        : activeVariant === "b"
          ? bundleSourceRootB
          : bundleSourceRootC;
      await rm(destinationPath, { recursive: true, force: true }).catch(() => undefined);
      await mkdir(destinationPath, { recursive: true });
      await cp(sourceRoot, destinationPath, { recursive: true, force: true });
    };

    const remoteAsset: RemoteSkillAsset = {
      bindingId: "bundle-replacement-binding",
      skillId: "bundle-replacement-skill-asset",
      name: "Bundle Replacement Smoke Asset",
      description: "Remote bundle smoke asset for replacement and rollback validation.",
      category: "project",
      fileName: "bundle.zip",
      downloadUrl
    };

    const workspaceRoot = await projectStore.getWorkspaceRoot();
    const projectPath = path.join(workspaceRoot, "projects", projectId);
    const readmePath = path.join(projectPath, "README.md");
    const manifestPath = path.join(workspaceRoot, "manifests", "project-bundles.json");

    await projectBundleService.syncRemoteBundles([remoteAsset], configVersion, "sync");
    assert((await readFile(readmePath, "utf8")).includes("Replacement variant A"), "Variant A was not materialized into the project README.");
    assert(await pathExists(path.join(workspaceRoot, "skills", variantSkillEntry.a)), "Variant A skill entry was not materialized.");
    assert(await pathExists(path.join(workspaceRoot, "cron", variantCronEntry.a)), "Variant A cron entry was not materialized.");

    const originalWithWindowsFsRetry = projectBundleServiceWithTestHooks.withWindowsFsRetry?.bind(projectBundleService);
    assert(originalWithWindowsFsRetry, "withWindowsFsRetry hook was not available for smoke test.");
    let injectedRetryCount = 0;
    projectBundleServiceWithTestHooks.withWindowsFsRetry = async <T>(context, operation) => {
      if (context.action === "apply-replacement" && injectedRetryCount === 0) {
        let firstAttempt = true;
        return originalWithWindowsFsRetry(context, async () => {
          if (firstAttempt) {
            firstAttempt = false;
            injectedRetryCount += 1;
            throw createRetryableWindowsError("Injected retryable apply-replacement failure.");
          }
          return operation();
        });
      }
      return originalWithWindowsFsRetry(context, operation);
    };

    activeVariant = "b";
    await projectBundleService.syncRemoteBundles([remoteAsset], configVersion, "sync");
    assert(injectedRetryCount === 1, `Expected one injected retry before variant B replacement, received ${injectedRetryCount}.`);

    const manifestAfterB = (await readJsonFile<Record<string, {
      checksum?: string;
      remoteEtag?: string;
      remoteLastModified?: string;
      sharedSkillEntries?: string[];
      sharedCronEntries?: string[];
    }>>(manifestPath)) ?? {};
    const recordAfterB = manifestAfterB[projectId];
    assert(recordAfterB, "Variant B sync did not persist a manifest record.");
    assert(recordAfterB.remoteEtag === '"bundle-replacement-b"', `Unexpected variant B remoteEtag: ${String(recordAfterB.remoteEtag || "")}`);
    assert((await readFile(readmePath, "utf8")).includes("Replacement variant B"), "Variant B was not materialized into the project README.");
    assert(await pathExists(path.join(workspaceRoot, "skills", variantSkillEntry.b)), "Variant B skill entry was not materialized.");
    assert(await pathExists(path.join(workspaceRoot, "cron", variantCronEntry.b)), "Variant B cron entry was not materialized.");
    assert(!(await pathExists(path.join(workspaceRoot, "skills", variantSkillEntry.a))), "Variant A skill entry was not cleaned up after replacement with variant B.");
    assert(!(await pathExists(path.join(workspaceRoot, "cron", variantCronEntry.a))), "Variant A cron entry was not cleaned up after replacement with variant B.");

    const originalMoveReplacementPath = projectBundleServiceWithTestHooks.moveReplacementPath?.bind(projectBundleService);
    assert(originalMoveReplacementPath, "moveReplacementPath hook was not available for smoke test.");
    let injectedFallbackCount = 0;
    projectBundleServiceWithTestHooks.moveReplacementPath = async (sourcePath, targetPath, action, hookProjectId) => {
      if (action === "backup-rename" && hookProjectId === projectId && injectedFallbackCount === 0) {
        injectedFallbackCount += 1;
        throw createRetryableWindowsError("Injected retryable backup rename failure.");
      }
      return originalMoveReplacementPath(sourcePath, targetPath, action, hookProjectId);
    };

    const originalSyncBundleProject = projectStore.syncBundleProject.bind(projectStore);
    let injectedFailureCount = 0;
    (projectStore as ProjectStoreService & {
      syncBundleProject: typeof originalSyncBundleProject;
    }).syncBundleProject = async (...args: Parameters<typeof originalSyncBundleProject>) => {
      if (injectedFailureCount === 0) {
        injectedFailureCount += 1;
        throw new Error("Simulated syncBundleProject failure after bundle replacement commit.");
      }
      return originalSyncBundleProject(...args);
    };

    activeVariant = "c";
    const replacementFailure = await projectBundleService.syncRemoteBundles([remoteAsset], configVersion, "sync")
      .then(() => null)
      .catch((error) => error instanceof Error ? error.message : String(error));
    (projectStore as ProjectStoreService & {
      syncBundleProject: typeof originalSyncBundleProject;
    }).syncBundleProject = originalSyncBundleProject;
    projectBundleServiceWithTestHooks.moveReplacementPath = originalMoveReplacementPath;
    projectBundleServiceWithTestHooks.withWindowsFsRetry = originalWithWindowsFsRetry;

    assert(injectedFallbackCount === 1, `Expected one injected fallback backup failure, received ${injectedFallbackCount}.`);
    assert(replacementFailure === "Simulated syncBundleProject failure after bundle replacement commit.", "Injected replacement failure did not surface the expected error.");

    const readmeAfterFailure = await readFile(readmePath, "utf8");
    assert(readmeAfterFailure.includes("Replacement variant B"), "Rollback did not restore variant B README after injected failure.");
    assert(!readmeAfterFailure.includes("Replacement variant C"), "Rollback left variant C README content in place after injected failure.");
    assert(await pathExists(path.join(workspaceRoot, "skills", variantSkillEntry.b)), "Rollback did not restore variant B skill entry after injected failure.");
    assert(!(await pathExists(path.join(workspaceRoot, "skills", variantSkillEntry.c))), "Rollback left variant C skill entry in place after injected failure.");
    assert(await pathExists(path.join(workspaceRoot, "cron", variantCronEntry.b)), "Rollback did not restore variant B cron entry after injected failure.");
    assert(!(await pathExists(path.join(workspaceRoot, "cron", variantCronEntry.c))), "Rollback left variant C cron entry in place after injected failure.");

    const manifestAfterFailure = (await readJsonFile<Record<string, {
      checksum?: string;
      remoteEtag?: string;
      remoteLastModified?: string;
      sharedSkillEntries?: string[];
      sharedCronEntries?: string[];
    }>>(manifestPath)) ?? {};
    const recordAfterFailure = manifestAfterFailure[projectId];
    assert(recordAfterFailure?.checksum === recordAfterB.checksum, "Manifest checksum changed despite rollback after injected replacement failure.");
    assert(recordAfterFailure?.remoteEtag === recordAfterB.remoteEtag, "Manifest remoteEtag changed despite rollback after injected replacement failure.");

    await projectBundleService.syncRemoteBundles([remoteAsset], configVersion, "sync");

    const readmeAfterRecovery = await readFile(readmePath, "utf8");
    assert(readmeAfterRecovery.includes("Replacement variant C"), "Recovery sync did not materialize variant C after rollback.");
    assert(await pathExists(path.join(workspaceRoot, "skills", variantSkillEntry.c)), "Recovery sync did not materialize variant C skill entry.");
    assert(await pathExists(path.join(workspaceRoot, "cron", variantCronEntry.c)), "Recovery sync did not materialize variant C cron entry.");
    assert(!(await pathExists(path.join(workspaceRoot, "skills", variantSkillEntry.b))), "Recovery sync did not clean up variant B skill entry.");
    assert(!(await pathExists(path.join(workspaceRoot, "cron", variantCronEntry.b))), "Recovery sync did not clean up variant B cron entry.");

    const manifestAfterRecovery = (await readJsonFile<Record<string, {
      checksum?: string;
      remoteEtag?: string;
      remoteLastModified?: string;
      sharedSkillEntries?: string[];
      sharedCronEntries?: string[];
    }>>(manifestPath)) ?? {};
    const recordAfterRecovery = manifestAfterRecovery[projectId];
    assert(recordAfterRecovery?.remoteEtag === '"bundle-replacement-c"', `Unexpected variant C remoteEtag: ${String(recordAfterRecovery?.remoteEtag || "")}`);

    const summary = {
      ok: true,
      userDataPath,
      workspaceRoot,
      bundleZipPathA,
      bundleZipPathB,
      bundleZipPathC,
      bundleSourceRootA,
      bundleSourceRootB,
      bundleSourceRootC,
      downloadUrl,
      configVersion,
      replacementFailure,
      retryInjectionCount: injectedRetryCount,
      fallbackInjectionCount: injectedFallbackCount,
      manifestRemoteEtagAfterB: recordAfterB.remoteEtag ?? null,
      manifestRemoteEtagAfterFailure: recordAfterFailure?.remoteEtag ?? null,
      manifestRemoteEtagAfterRecovery: recordAfterRecovery?.remoteEtag ?? null,
      checksumAfterB: recordAfterB.checksum ?? null,
      checksumAfterFailure: recordAfterFailure?.checksum ?? null,
      checksumAfterRecovery: recordAfterRecovery?.checksum ?? null,
      readmePath,
      projectId
    };

    await mkdir(path.dirname(resultPath), { recursive: true });
    await writeFile(resultPath, JSON.stringify(summary, null, 2), "utf8");
    console.log(JSON.stringify(summary, null, 2));
  } finally {
    await new Promise<void>((resolve) => {
      server.close(() => resolve());
    });
  }
}

main().catch(async (error) => {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);
  const repoRoot = path.resolve(__dirname, "..", "..");
  const resultPath = path.resolve(process.argv[2] ?? path.join(repoRoot, ".tmp", "project-bundle-replacement-smoke", "result.json"));
  const failure = {
    ok: false,
    error: error instanceof Error ? error.stack ?? error.message : String(error)
  };

  await mkdir(path.dirname(resultPath), { recursive: true });
  await writeFile(resultPath, JSON.stringify(failure, null, 2), "utf8");
  console.error(failure.error);
  process.exitCode = 1;
});


