import { 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";

interface BundleManifestRecord {
  projectId: string;
  sourceUrl: string;
  remoteEtag?: string;
  sharedSkillEntries?: string[];
  sharedCronEntries?: string[];
}

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

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;
  }
}

function sortStrings(values: Iterable<string>): string[] {
  return [...values].sort((left, right) => left.localeCompare(right, "en"));
}

function buildRemoteSkillAsset(baseUrl: string, key: string): RemoteSkillAsset {
  return {
    bindingId: `bundle-churn-binding-${key}`,
    skillId: `bundle-churn-skill-${key}`,
    name: `Bundle Churn ${key.toUpperCase()}`,
    description: `Remote bundle churn fixture ${key}.`,
    category: "project",
    fileName: `${key}.zip`,
    downloadUrl: `${baseUrl}/${key}.zip`
  };
}

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-churn-smoke", "result.json"));
  const alphaZipPath = path.resolve(process.argv[3] ?? path.join(path.dirname(resultPath), "alpha.zip"));
  const betaZipPathV1 = path.resolve(process.argv[4] ?? path.join(path.dirname(resultPath), "beta-v1.zip"));
  const betaZipPathV2 = path.resolve(process.argv[5] ?? path.join(path.dirname(resultPath), "beta-v2.zip"));
  const gammaZipPath = path.resolve(process.argv[6] ?? path.join(path.dirname(resultPath), "gamma.zip"));
  const deltaZipPath = path.resolve(process.argv[7] ?? path.join(path.dirname(resultPath), "delta.zip"));
  const tempRoot = path.dirname(resultPath);
  const userDataPath = path.join(tempRoot, "user-data");
  let betaVariant: "v1" | "v2" = "v1";

  for (const [label, filePath] of [
    ["alpha", alphaZipPath],
    ["beta-v1", betaZipPathV1],
    ["beta-v2", betaZipPathV2],
    ["gamma", gammaZipPath],
    ["delta", deltaZipPath]
  ] as const) {
    assert(await pathExists(filePath), `Bundle zip fixture ${label} was not found: ${filePath}`);
  }

  await rm(userDataPath, { recursive: true, force: true });
  await mkdir(userDataPath, { recursive: true });

  const server = http.createServer(async (request, response) => {
    try {
      const route = request.url ?? "/";
      const current = route === "/alpha.zip"
        ? {
            zipPath: alphaZipPath,
            etag: "\"bundle-churn-alpha-v1\"",
            lastModified: "Tue, 31 Mar 2026 15:00:00 GMT"
          }
        : route === "/beta.zip"
          ? betaVariant === "v1"
            ? {
                zipPath: betaZipPathV1,
                etag: "\"bundle-churn-beta-v1\"",
                lastModified: "Tue, 31 Mar 2026 15:05:00 GMT"
              }
            : {
                zipPath: betaZipPathV2,
                etag: "\"bundle-churn-beta-v2\"",
                lastModified: "Tue, 31 Mar 2026 15:10:00 GMT"
              }
          : route === "/gamma.zip"
            ? {
                zipPath: gammaZipPath,
                etag: "\"bundle-churn-gamma-v1\"",
                lastModified: "Tue, 31 Mar 2026 15:15:00 GMT"
              }
            : route === "/delta.zip"
              ? {
                  zipPath: deltaZipPath,
                  etag: "\"bundle-churn-delta-v1\"",
                  lastModified: "Tue, 31 Mar 2026 15:20:00 GMT"
                }
              : null;

      if (!current) {
        response.statusCode = 404;
        response.end("not found");
        return;
      }

      const payload = await readFile(current.zipPath);
      const headers = {
        ETag: current.etag,
        "Last-Modified": current.lastModified,
        "Content-Length": String(payload.length),
        "Cache-Control": "no-store"
      };

      if (request.method === "HEAD") {
        response.writeHead(200, headers);
        response.end();
        return;
      }

      if (request.method === "GET") {
        response.writeHead(200, {
          ...headers,
          "Content-Type": "application/zip"
        });
        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 churn smoke server.");
  const baseUrl = `http://127.0.0.1:${address.port}`;

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

    const projectStore = new ProjectStoreService(configService);
    await projectStore.initialize();
    const projectBundleService = new ProjectBundleService(configService, projectStore);

    const alpha = buildRemoteSkillAsset(baseUrl, "alpha");
    const beta = buildRemoteSkillAsset(baseUrl, "beta");
    const gamma = buildRemoteSkillAsset(baseUrl, "gamma");
    const delta = buildRemoteSkillAsset(baseUrl, "delta");

    const alphaProjectId = "bundle-churn-alpha";
    const betaProjectId = "bundle-churn-beta";
    const gammaProjectId = "bundle-churn-gamma";
    const deltaProjectId = "bundle-churn-delta";
    const alphaSkillEntry = "bundle-churn-alpha-skill";
    const betaSkillEntryV1 = "bundle-churn-beta-skill-v1";
    const betaSkillEntryV2 = "bundle-churn-beta-skill-v2";
    const gammaSkillEntry = "bundle-churn-gamma-skill";
    const deltaSkillEntry = "bundle-churn-delta-skill";
    const alphaCronEntry = "bundle-churn-alpha-task.txt";
    const betaCronEntryV1 = "bundle-churn-beta-task-v1.txt";
    const betaCronEntryV2 = "bundle-churn-beta-task-v2.txt";
    const gammaCronEntry = "bundle-churn-gamma-task.txt";
    const deltaCronEntry = "bundle-churn-delta-task.txt";

    await projectBundleService.syncRemoteBundles([alpha, beta, gamma], "bundle-churn-config-v1", "sync");

    const workspaceRoot = await projectStore.getWorkspaceRoot();
    const manifestPath = path.join(workspaceRoot, "manifests", "project-bundles.json");
    const alphaProjectPath = path.join(workspaceRoot, "projects", alphaProjectId);
    const betaProjectPath = path.join(workspaceRoot, "projects", betaProjectId);
    const gammaProjectPath = path.join(workspaceRoot, "projects", gammaProjectId);
    const deltaProjectPath = path.join(workspaceRoot, "projects", deltaProjectId);
    const betaReadmePath = path.join(betaProjectPath, "README.md");

    assert(await pathExists(alphaProjectPath), "Alpha project was not materialized on initial sync.");
    assert(await pathExists(betaProjectPath), "Beta project was not materialized on initial sync.");
    assert(await pathExists(gammaProjectPath), "Gamma project was not materialized on initial sync.");
    assert(await pathExists(path.join(workspaceRoot, "skills", alphaSkillEntry)), "Alpha skill entry was not materialized on initial sync.");
    assert(await pathExists(path.join(workspaceRoot, "skills", betaSkillEntryV1)), "Beta v1 skill entry was not materialized on initial sync.");
    assert(await pathExists(path.join(workspaceRoot, "skills", gammaSkillEntry)), "Gamma skill entry was not materialized on initial sync.");
    assert(await pathExists(path.join(workspaceRoot, "cron", alphaCronEntry)), "Alpha cron entry was not materialized on initial sync.");
    assert(await pathExists(path.join(workspaceRoot, "cron", betaCronEntryV1)), "Beta v1 cron entry was not materialized on initial sync.");
    assert(await pathExists(path.join(workspaceRoot, "cron", gammaCronEntry)), "Gamma cron entry was not materialized on initial sync.");

    const projectsAfterInitialSync = sortStrings((await projectStore.listProjects()).map((project) => project.id));
    assert(JSON.stringify(projectsAfterInitialSync) === JSON.stringify(sortStrings([alphaProjectId, betaProjectId, gammaProjectId])), "Initial project inventory did not match alpha/beta/gamma.");

    await projectStore.setActiveProject(alphaProjectId);
    const alphaPinnedSession = await projectStore.createSession("Alpha Pinned Session", alphaProjectId);

    betaVariant = "v2";
    await projectBundleService.syncRemoteBundles([alpha, beta, delta], "bundle-churn-config-v2", "sync");

    const projectsAfterChurn = sortStrings((await projectStore.listProjects()).map((project) => project.id));
    assert(JSON.stringify(projectsAfterChurn) === JSON.stringify(sortStrings([alphaProjectId, betaProjectId, deltaProjectId])), "Churn sync did not leave the expected alpha/beta/delta inventory.");

    const activeAfterChurn = await projectStore.getActiveProject();
    assert(activeAfterChurn.id === alphaProjectId, "Active project should remain alpha when it survives churn sync.");

    const alphaSessionsAfterChurn = await projectStore.listSessions(alphaProjectId);
    assert(alphaSessionsAfterChurn.some((session) => session.id === alphaPinnedSession.id), "Alpha session did not survive unrelated churn sync.");

    assert(!(await pathExists(gammaProjectPath)), "Removed gamma project directory still exists after churn sync.");
    assert(!(await pathExists(path.join(workspaceRoot, "skills", gammaSkillEntry))), "Removed gamma skill entry still exists after churn sync.");
    assert(!(await pathExists(path.join(workspaceRoot, "cron", gammaCronEntry))), "Removed gamma cron entry still exists after churn sync.");

    assert(await pathExists(deltaProjectPath), "Added delta project was not materialized during churn sync.");
    assert(await pathExists(path.join(workspaceRoot, "skills", deltaSkillEntry)), "Added delta skill entry was not materialized during churn sync.");
    assert(await pathExists(path.join(workspaceRoot, "cron", deltaCronEntry)), "Added delta cron entry was not materialized during churn sync.");

    const betaReadmeAfterReplacement = await readFile(betaReadmePath, "utf8");
    assert(betaReadmeAfterReplacement.includes("Beta variant V2"), "Beta replacement did not materialize the v2 README.");
    assert(await pathExists(path.join(workspaceRoot, "skills", betaSkillEntryV2)), "Beta v2 skill entry was not materialized during churn sync.");
    assert(await pathExists(path.join(workspaceRoot, "cron", betaCronEntryV2)), "Beta v2 cron entry was not materialized during churn sync.");
    assert(!(await pathExists(path.join(workspaceRoot, "skills", betaSkillEntryV1))), "Beta v1 skill entry still exists after replacement to v2.");
    assert(!(await pathExists(path.join(workspaceRoot, "cron", betaCronEntryV1))), "Beta v1 cron entry still exists after replacement to v2.");

    const manifestAfterChurn = (await readJsonFile<Record<string, BundleManifestRecord>>(manifestPath)) ?? {};
    assert(JSON.stringify(sortStrings(Object.keys(manifestAfterChurn))) === JSON.stringify(sortStrings([alphaProjectId, betaProjectId, deltaProjectId])), "Manifest did not match alpha/beta/delta after churn sync.");
    assert(manifestAfterChurn[betaProjectId]?.remoteEtag === "\"bundle-churn-beta-v2\"", "Manifest beta record did not update to the v2 freshness metadata.");
    assert(manifestAfterChurn[betaProjectId]?.sharedSkillEntries?.includes(betaSkillEntryV2), "Manifest beta record did not track beta v2 skill ownership.");
    assert(!manifestAfterChurn[betaProjectId]?.sharedSkillEntries?.includes(betaSkillEntryV1), "Manifest beta record still tracked beta v1 skill ownership after replacement.");
    assert(manifestAfterChurn[deltaProjectId]?.sharedSkillEntries?.includes(deltaSkillEntry), "Manifest delta record did not track delta skill ownership.");

    await projectStore.setActiveProject(deltaProjectId);
    const removedDeltaSession = await projectStore.createSession("Delta Removed Session", deltaProjectId);

    await projectBundleService.syncRemoteBundles([alpha, beta], "bundle-churn-config-v3", "sync");

    const projectsAfterRemoval = sortStrings((await projectStore.listProjects()).map((project) => project.id));
    assert(JSON.stringify(projectsAfterRemoval) === JSON.stringify(sortStrings([alphaProjectId, betaProjectId])), "Final churn sync did not leave the expected alpha/beta inventory.");

    const activeAfterRemovingDelta = await projectStore.getActiveProject();
    assert(activeAfterRemovingDelta.id !== deltaProjectId, "Active project did not switch away from removed delta.");
    assert([alphaProjectId, betaProjectId].includes(activeAfterRemovingDelta.id), "Active project did not fall back to a remaining valid project.");

    assert(!(await pathExists(deltaProjectPath)), "Removed delta project directory still exists after final churn sync.");
    assert(!(await pathExists(path.join(workspaceRoot, "skills", deltaSkillEntry))), "Removed delta skill entry still exists after final churn sync.");
    assert(!(await pathExists(path.join(workspaceRoot, "cron", deltaCronEntry))), "Removed delta cron entry still exists after final churn sync.");

    const removedDeltaSessionLookup = await projectStore.getSessionState(removedDeltaSession.id)
      .then(() => null)
      .catch((error) => error instanceof Error ? error.message : String(error));
    assert(removedDeltaSessionLookup === `Session ${removedDeltaSession.id} was not found.`, "Removed delta session still resolved after deleting the backing project.");

    const alphaSessionsAfterFinalRemoval = await projectStore.listSessions(alphaProjectId);
    assert(alphaSessionsAfterFinalRemoval.some((session) => session.id === alphaPinnedSession.id), "Alpha session did not survive unrelated project removal.");

    const manifestAfterFinalRemoval = (await readJsonFile<Record<string, BundleManifestRecord>>(manifestPath)) ?? {};
    assert(JSON.stringify(sortStrings(Object.keys(manifestAfterFinalRemoval))) === JSON.stringify(sortStrings([alphaProjectId, betaProjectId])), "Manifest did not match alpha/beta after removing delta.");

    const summary = {
      ok: true,
      userDataPath,
      workspaceRoot,
      projectsAfterInitialSync,
      projectsAfterChurn,
      projectsAfterRemoval,
      activeAfterChurn: activeAfterChurn.id,
      activeAfterRemovingDelta: activeAfterRemovingDelta.id,
      alphaPinnedSessionId: alphaPinnedSession.id,
      removedDeltaSessionId: removedDeltaSession.id,
      removedDeltaSessionLookup,
      betaReadmePath,
      betaRemoteEtagAfterChurn: manifestAfterChurn[betaProjectId]?.remoteEtag ?? null,
      manifestKeysAfterFinalRemoval: sortStrings(Object.keys(manifestAfterFinalRemoval))
    };

    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-churn-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;
});
