import { createHash } from "node:crypto";
import http from "node:http";
import https from "node:https";
import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import type { RuntimeCloudFetchAction } from "@qjclaw/shared-types";
import { createRequire } from "node:module";
import type { AppConfigService } from "./app-config.js";
import type { ProjectStoreService } from "./project-store.js";
import type { RemoteSkillAsset } from "./skill-store.js";
import type { StartupLogger } from "./startup-logger.js";

interface BundleManifestRecord {
  sourceUrl: string;
  fileName: string;
  configVersion?: string;
  updatedAt: string;
  projectId: string;
  checksum?: string;
  sourceSkillId?: string;
  sharedSkillEntries?: string[];
  sharedCronEntries?: string[];
  remoteFileSize?: number;
  remoteContentLength?: number;
  remoteEtag?: string;
  remoteLastModified?: string;
}

interface BundleProjectMetadata {
  projectId: string;
  projectName: string;
  version?: string;
  description?: string;
  declaredBoundSkillIds: string[];
  projectSourceRoot: string;
  sharedSourceRoot: string;
}

interface ProjectJsonShape {
  id?: string;
  name?: string;
  version?: string;
  description?: string;
  [key: string]: unknown;
}

interface BundleReplacementOperation {
  stagedPath: string;
  targetPath: string;
}

interface CommittedBundleReplacementOperation extends BundleReplacementOperation {
  backupPath?: string;
  backupMode?: "rename" | "copy";
  targetExisted: boolean;
  applied: boolean;
}

interface RetryableFsOperationLogContext {
  action: string;
  path: string;
  backupPath?: string;
  projectId?: string;
}

interface RemoveReplacementPathOptions {
  requireMissing?: boolean;
}

interface MaterializedBundleTransaction {
  skillIds: string[];
  sharedSkillEntries: string[];
  sharedCronEntries: string[];
  finalize(): Promise<void>;
  rollback(): Promise<void>;
}

interface StagedSharedEntries {
  entryNames: string[];
  directoryIds: string[];
  operations: BundleReplacementOperation[];
}

interface RemoteBundleProbeResult {
  etag?: string;
  lastModified?: string;
  contentLength?: number;
}

export interface ProjectBundleSyncStatus {
  state: "idle" | "syncing" | "ready" | "error";
  lastError?: string;
  lastSyncedAt?: string;
}

const MANIFEST_FILE = "project-bundles.json";
const MANIFESTS_DIR = "manifests";
const TEMP_DIR = ".bundle-tmp";
const REDIRECT_STATUS_CODES = new Set([301, 302, 307, 308]);
const HEAD_UNSUPPORTED_STATUS_CODES = new Set([403, 404, 405, 501]);
const MAX_REDIRECTS = 5;
const BUNDLE_REQUEST_IDLE_TIMEOUT_MS = 30_000;
const WINDOWS_RETRYABLE_FS_ERROR_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
const WINDOWS_FS_RETRY_DELAYS_MS = [50, 120, 250, 500, 1000] as const;
const requireFromHere = createRequire(
  typeof __filename === "string"
    ? __filename
    : path.join(process.cwd(), "apps", "desktop", "src", "main", "services", "project-bundle.ts")
);

type ExtractZipFn = (zipPath: string, options: { dir: string }) => Promise<void>;

let extractZipLoaderPromise: Promise<ExtractZipFn> | null = null;

function nowIso(): string {
  return new Date().toISOString();
}

function normalizeBundleName(fileName: string): string {
  return fileName.replace(/\.zip$/i, "").trim() || "project-bundle";
}

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 writeJsonFile(filePath: string, payload: unknown): Promise<void> {
  await mkdir(path.dirname(filePath), { recursive: true });
  const tempPath = `${filePath}.tmp-${Date.now()}`;
  await writeFile(tempPath, JSON.stringify(payload, null, 2), "utf8");
  await rm(filePath, { force: true }).catch(() => undefined);
  await rename(tempPath, filePath);
}

function resolveDirectChildPath(rootPath: string, entryName: string): string {
  const trimmed = entryName.trim();
  if (!trimmed || path.basename(trimmed) !== trimmed) {
    throw new Error(`Invalid managed entry name: ${entryName}`);
  }

  const resolvedRoot = path.resolve(rootPath);
  const targetPath = path.resolve(resolvedRoot, trimmed);
  const rootPrefix = resolvedRoot.endsWith(path.sep) ? resolvedRoot : `${resolvedRoot}${path.sep}`;
  if (targetPath !== resolvedRoot && !targetPath.startsWith(rootPrefix)) {
    throw new Error(`Managed entry path escaped root: ${entryName}`);
  }
  return targetPath;
}

function uniqueStrings(values: Iterable<string>): string[] {
  return [...new Set([...values].map((value) => value.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right, "en"));
}

function normalizeStringArray(value: unknown): string[] {
  if (!Array.isArray(value)) {
    return [];
  }
  return uniqueStrings(value.map((entry) => typeof entry === "string" ? entry : ""));
}

function normalizeExtractZipModule(moduleValue: unknown): ExtractZipFn | null {
  if (typeof moduleValue === "function") {
    return moduleValue as ExtractZipFn;
  }
  if (moduleValue && typeof moduleValue === "object" && typeof (moduleValue as { default?: unknown }).default === "function") {
    return (moduleValue as { default: ExtractZipFn }).default;
  }
  return null;
}

async function loadExtractZip(): Promise<ExtractZipFn> {
  if (extractZipLoaderPromise) {
    return extractZipLoaderPromise;
  }

  extractZipLoaderPromise = Promise.resolve().then(() => {
    const fallbackCandidates = [
      "extract-zip",
      path.resolve(process.cwd(), "apps", "desktop", "node_modules", "extract-zip"),
      path.resolve(process.cwd(), "node_modules", "extract-zip")
    ];

    for (const candidate of fallbackCandidates) {
      try {
        const loaded = normalizeExtractZipModule(requireFromHere(candidate));
        if (loaded) {
          return loaded;
        }
      } catch {
        // Keep trying fallback resolution candidates.
      }
    }

    throw new Error("Unable to resolve extract-zip for project bundle extraction.");
  });

  return extractZipLoaderPromise;
}

function normalizeHeaderValue(value: string | string[] | undefined): string | undefined {
  if (Array.isArray(value)) {
    return value[0]?.trim() || undefined;
  }
  return value?.trim() || undefined;
}

function normalizeContentLength(value: string | undefined): number | undefined {
  if (!value) {
    return undefined;
  }
  const parsed = Number.parseInt(value, 10);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
}

function sanitizeUrl(url: URL): string {
  return `${url.protocol}//${url.host}${url.pathname}`;
}

function logBundle(event: string, details: Record<string, unknown>): void {
  console.info("[bundle]", event, { ...details, ts: new Date().toISOString() });
}

function getFsErrorCode(error: unknown): string | undefined {
  if (!error || typeof error !== "object") {
    return undefined;
  }
  const code = (error as { code?: unknown }).code;
  return typeof code === "string" ? code : undefined;
}

function isRetryableWindowsFsError(error: unknown): boolean {
  return process.platform === "win32" && WINDOWS_RETRYABLE_FS_ERROR_CODES.has(getFsErrorCode(error) ?? "");
}

function isRecoverableRemoteBundleAssetError(error: unknown): boolean {
  if (!(error instanceof Error)) {
    return false;
  }

  const message = error.message;
  return message.startsWith("Project bundle freshness probe failed")
    || message.startsWith("Project bundle freshness probe timed out")
    || message.startsWith("Project bundle download failed")
    || message.startsWith("Project bundle download timed out")
    || message === "Project bundle probe redirected too many times."
    || message === "Project bundle download redirected too many times.";
}

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export class ProjectBundleService {
  private readonly configService: AppConfigService;
  private readonly projectStore: ProjectStoreService;
  private readonly startupLogger?: StartupLogger;
  private syncStatus: ProjectBundleSyncStatus = { state: "idle" };
  private syncTail: Promise<void> = Promise.resolve();

  constructor(configService: AppConfigService, projectStore: ProjectStoreService, startupLogger?: StartupLogger) {
    this.configService = configService;
    this.projectStore = projectStore;
    this.startupLogger = startupLogger;
  }

  getSyncStatus(): ProjectBundleSyncStatus {
    return { ...this.syncStatus };
  }

  setSyncError(message: string): void {
    this.syncStatus = {
      ...this.syncStatus,
      state: "error",
      lastError: message
    };
  }

  async syncRemoteBundles(remoteSkills: RemoteSkillAsset[], configVersion?: string, _action?: RuntimeCloudFetchAction): Promise<void> {
    const runSync = async (): Promise<void> => {
      const startedAt = Date.now();
      this.syncStatus = {
        ...this.syncStatus,
        state: "syncing",
        lastError: undefined
      };
      const bundleAssets = remoteSkills.filter((asset) => asset.downloadUrl && asset.fileName && /\.zip$/i.test(asset.fileName));
      logBundle("bundle.sync.start", {
        action: _action ?? "unknown",
        configVersion,
        remoteSkillCount: remoteSkills.length,
        bundleAssetCount: bundleAssets.length
      });
      const workspaceRoot = await this.projectStore.getWorkspaceRoot();
      await this.startupLogger?.info("project-bundle", "sync.start", "Project bundle sync started.", {
        action: _action ?? "unknown",
        configVersion,
        workspaceRoot,
        remoteSkillCount: remoteSkills.length,
        bundleAssetCount: bundleAssets.length
      });
      const manifestPath = path.join(workspaceRoot, MANIFESTS_DIR, MANIFEST_FILE);
      const currentManifest = (await readJsonFile<Record<string, BundleManifestRecord>>(manifestPath)) ?? {};
      const nextManifest: Record<string, BundleManifestRecord> = {};
      const seenBundleKeys = new Set<string>();
      let skippedAssetCount = 0;
      logBundle("bundle.asset_filter.result", {
        remoteSkillCount: remoteSkills.length,
        bundleAssetCount: bundleAssets.length
      });

      for (const asset of bundleAssets) {
        if (!asset.downloadUrl || !asset.fileName) {
          continue;
        }

        const bundleKey = this.getBundleAssetKey(asset);
        if (seenBundleKeys.has(bundleKey)) {
          logBundle("bundle.duplicate.detected", {
            skillId: asset.skillId,
            bundleKey
          });
          continue;
        }
        seenBundleKeys.add(bundleKey);

        const currentRecord = this.findManifestRecordForAsset(currentManifest, asset);
        logBundle("bundle.reuse.check", {
          skillId: asset.skillId,
          bundleKey,
          source: sanitizeUrl(new URL(asset.downloadUrl))
        });
        const nextRecord = await this.resolveNextManifestRecordOrFallback(
          workspaceRoot,
          asset,
          configVersion,
          currentRecord
        );
        if (!nextRecord) {
          skippedAssetCount += 1;
          continue;
        }

        if (nextManifest[nextRecord.projectId]) {
          throw new Error(`Project bundle sync resolved duplicate projectId ${nextRecord.projectId}.`);
        }
        nextManifest[nextRecord.projectId] = nextRecord;
      }

      await this.cleanupRemovedBundleState(workspaceRoot, currentManifest, nextManifest);
      await writeJsonFile(manifestPath, nextManifest);
      this.syncStatus = {
        state: "ready",
        lastError: undefined,
        lastSyncedAt: nowIso()
      };
      logBundle("bundle.sync.done", {
        action: _action ?? "unknown",
        configVersion,
        projectCount: Object.keys(nextManifest).length,
        skippedAssetCount,
        elapsedMs: Date.now() - startedAt
      });
    };

    const nextRun = this.syncTail.then(runSync);
    this.syncTail = nextRun.catch(() => undefined);
    await nextRun;
  }

  private getBundleAssetKey(asset: Pick<RemoteSkillAsset, "skillId" | "downloadUrl">): string {
    return `${asset.skillId.trim().toLowerCase()}::${asset.downloadUrl?.trim() ?? ""}`;
  }

  private getBundleRecordKey(record: BundleManifestRecord): string {
    return `${record.sourceSkillId?.trim().toLowerCase() ?? ""}::${record.sourceUrl.trim()}`;
  }

  private findManifestRecordForAsset(
    manifest: Record<string, BundleManifestRecord>,
    asset: Pick<RemoteSkillAsset, "skillId" | "downloadUrl">
  ): BundleManifestRecord | undefined {
    const expectedKey = this.getBundleAssetKey(asset);
    const records = Object.values(manifest);
    return records.find((record) => this.getBundleRecordKey(record) === expectedKey)
      ?? records.find((record) => record.sourceUrl === asset.downloadUrl);
  }

  private async resolveNextManifestRecord(
    workspaceRoot: string,
    asset: RemoteSkillAsset,
    configVersion: string | undefined,
    currentRecord: BundleManifestRecord | undefined
  ): Promise<BundleManifestRecord> {
    if (!this.canReuseManifestRecord(currentRecord, asset, configVersion)) {
      logBundle("bundle.reuse.check", {
        skillId: asset.skillId,
        decision: "redownload",
        reason: "manifest-not-reusable"
      });
      try {
        return await this.downloadAndInstallBundle(workspaceRoot, asset, configVersion);
      } catch (error) {
        const hasLocalProjectCache = await this.hasUsableLocalProjectCache(workspaceRoot, currentRecord);
        if (!hasLocalProjectCache || !currentRecord) {
          throw error;
        }
        logBundle("bundle.reuse.check", {
          skillId: asset.skillId,
          decision: "reuse",
          reason: "redownload-failed-using-local-cache",
          error: error instanceof Error ? error.message : String(error)
        });
        return this.updateManifestRecordFromProbe(currentRecord, asset, configVersion, null);
      }
    }

    let freshnessProbe: RemoteBundleProbeResult | null = null;
    try {
      freshnessProbe = await this.probeRemoteBundle(new URL(asset.downloadUrl!));
    } catch (error) {
      const hasLocalProjectCache = await this.hasUsableLocalProjectCache(workspaceRoot, currentRecord);
      if (!hasLocalProjectCache || !currentRecord) {
        throw error;
      }
      logBundle("bundle.reuse.check", {
        skillId: asset.skillId,
        decision: "reuse",
        reason: "freshness-probe-failed-using-local-cache",
        error: error instanceof Error ? error.message : String(error)
      });
      return this.updateManifestRecordFromProbe(currentRecord, asset, configVersion, null);
    }
    if (this.shouldRedownloadBundle(currentRecord, asset, freshnessProbe)) {
      logBundle("bundle.reuse.check", {
        skillId: asset.skillId,
        decision: "redownload",
        reason: "freshness-mismatch"
      });
      return this.downloadAndInstallBundle(workspaceRoot, asset, configVersion, freshnessProbe);
    }

    logBundle("bundle.reuse.check", {
      skillId: asset.skillId,
      decision: "reuse",
      reason: "freshness-match"
    });
    return this.updateManifestRecordFromProbe(currentRecord, asset, configVersion, freshnessProbe);
  }

  private async resolveNextManifestRecordOrFallback(
    workspaceRoot: string,
    asset: RemoteSkillAsset,
    configVersion: string | undefined,
    currentRecord: BundleManifestRecord | undefined
  ): Promise<BundleManifestRecord | null> {
    try {
      return await this.resolveNextManifestRecord(workspaceRoot, asset, configVersion, currentRecord);
    } catch (error) {
      if (!isRecoverableRemoteBundleAssetError(error)) {
        throw error;
      }

      const hasLocalProjectCache = await this.hasUsableLocalProjectCache(workspaceRoot, currentRecord);
      const decision = hasLocalProjectCache && currentRecord ? "reuse-local-cache" : "skip-asset";
      const errorMessage = error instanceof Error ? error.message : String(error);
      logBundle("bundle.asset.recoverable_error", {
        skillId: asset.skillId,
        source: sanitizeUrl(new URL(asset.downloadUrl!)),
        decision,
        projectId: currentRecord?.projectId,
        error: errorMessage
      });
      await this.startupLogger?.warn(
        "project-bundle",
        "asset.recoverable_error",
        "Recoverable remote project bundle error encountered; continuing startup.",
        {
          skillId: asset.skillId,
          source: sanitizeUrl(new URL(asset.downloadUrl!)),
          decision,
          projectId: currentRecord?.projectId,
          error: errorMessage
        }
      );

      if (hasLocalProjectCache && currentRecord) {
        return {
          ...currentRecord,
          updatedAt: nowIso()
        };
      }
      return null;
    }
  }

  private async hasUsableLocalProjectCache(
    workspaceRoot: string,
    record: BundleManifestRecord | undefined
  ): Promise<boolean> {
    if (!record?.projectId) {
      return false;
    }

    const projectJsonPath = path.join(workspaceRoot, "projects", record.projectId, "project.json");
    return pathExists(projectJsonPath);
  }

  private canReuseManifestRecord(
    record: BundleManifestRecord | undefined,
    asset: RemoteSkillAsset,
    configVersion?: string
  ): record is BundleManifestRecord {
    return Boolean(
      record
      && record.sourceUrl === asset.downloadUrl
      && record.fileName === asset.fileName
      && record.configVersion === configVersion
      && record.checksum
      && Array.isArray(record.sharedSkillEntries)
      && Array.isArray(record.sharedCronEntries)
    );
  }

  private shouldRedownloadBundle(
    record: BundleManifestRecord,
    asset: RemoteSkillAsset,
    freshnessProbe: RemoteBundleProbeResult | null
  ): boolean {
    if (typeof asset.fileSize === "number" && typeof record.remoteFileSize === "number" && asset.fileSize !== record.remoteFileSize) {
      return true;
    }

    const probeHasFreshnessIdentity = Boolean(
      freshnessProbe?.etag
      || freshnessProbe?.lastModified
      || typeof freshnessProbe?.contentLength === "number"
    );
    const recordHasFreshnessIdentity = Boolean(
      record.remoteEtag
      || record.remoteLastModified
      || typeof record.remoteContentLength === "number"
    );
    if (probeHasFreshnessIdentity && !recordHasFreshnessIdentity) {
      return true;
    }

    if (freshnessProbe?.etag && record.remoteEtag && freshnessProbe.etag !== record.remoteEtag) {
      return true;
    }
    if (freshnessProbe?.lastModified && record.remoteLastModified && freshnessProbe.lastModified !== record.remoteLastModified) {
      return true;
    }
    if (typeof freshnessProbe?.contentLength === "number" && typeof record.remoteContentLength === "number" && freshnessProbe.contentLength !== record.remoteContentLength) {
      return true;
    }
    return false;
  }

  private updateManifestRecordFromProbe(
    record: BundleManifestRecord,
    asset: RemoteSkillAsset,
    configVersion: string | undefined,
    freshnessProbe: RemoteBundleProbeResult | null
  ): BundleManifestRecord {
    return {
      ...record,
      sourceUrl: asset.downloadUrl!,
      fileName: asset.fileName!,
      sourceSkillId: asset.skillId,
      configVersion,
      updatedAt: nowIso(),
      remoteFileSize: asset.fileSize ?? record.remoteFileSize,
      remoteContentLength: freshnessProbe?.contentLength ?? record.remoteContentLength,
      remoteEtag: freshnessProbe?.etag ?? record.remoteEtag,
      remoteLastModified: freshnessProbe?.lastModified ?? record.remoteLastModified
    };
  }

  private async cleanupRemovedBundleState(
    workspaceRoot: string,
    currentManifest: Record<string, BundleManifestRecord>,
    nextManifest: Record<string, BundleManifestRecord>
  ): Promise<void> {
    const currentRecords = Object.values(currentManifest);
    if (currentRecords.length === 0) {
      return;
    }

    const nextRecords = Object.values(nextManifest);
    logBundle("bundle.cleanup.removed_state", {
      removedProjectCount: currentRecords.length - nextRecords.length,
      currentProjectCount: currentRecords.length,
      nextProjectCount: nextRecords.length
    });
    const expectedProjectIds = new Set(nextRecords.map((record) => record.projectId));
    const expectedSkillEntries = new Set(nextRecords.flatMap((record) => record.sharedSkillEntries ?? []));
    const expectedCronEntries = new Set(nextRecords.flatMap((record) => record.sharedCronEntries ?? []));

    for (const projectId of uniqueStrings(currentRecords.map((record) => record.projectId))) {
      if (!expectedProjectIds.has(projectId)) {
        await this.projectStore.removeProject(projectId);
      }
    }

    await this.cleanupManagedEntries(
      path.join(workspaceRoot, "skills"),
      uniqueStrings(currentRecords.flatMap((record) => record.sharedSkillEntries ?? [])),
      expectedSkillEntries
    );
    await this.cleanupManagedEntries(
      path.join(workspaceRoot, "cron"),
      uniqueStrings(currentRecords.flatMap((record) => record.sharedCronEntries ?? [])),
      expectedCronEntries
    );
  }

  private async cleanupManagedEntries(rootPath: string, currentEntries: string[], expectedEntries: Set<string>): Promise<void> {
    for (const entryName of currentEntries) {
      if (expectedEntries.has(entryName)) {
        continue;
      }
      const targetPath = resolveDirectChildPath(rootPath, entryName);
      await rm(targetPath, { recursive: true, force: true }).catch(() => undefined);
    }
  }

  private async downloadAndInstallBundle(
    workspaceRoot: string,
    asset: RemoteSkillAsset,
    configVersion?: string,
    freshnessProbe?: RemoteBundleProbeResult | null
  ): Promise<BundleManifestRecord> {
    const startedAt = Date.now();
    const tempRoot = path.join(workspaceRoot, TEMP_DIR, `${Date.now()}-${asset.skillId}`);
    const zipPath = path.join(tempRoot, asset.fileName ?? `${asset.skillId}.zip`);
    const extractPath = path.join(tempRoot, "unzipped");
    await mkdir(extractPath, { recursive: true });
    logBundle("bundle.download.start", {
      skillId: asset.skillId,
      source: sanitizeUrl(new URL(asset.downloadUrl!)),
      fileName: asset.fileName,
      configVersion
    });

    try {
      const resolvedFreshnessProbe = freshnessProbe === undefined
        ? await this.probeRemoteBundle(new URL(asset.downloadUrl!))
        : freshnessProbe;
      const payload = await this.downloadToBuffer(new URL(asset.downloadUrl!));
      await mkdir(path.dirname(zipPath), { recursive: true });
      await writeFile(zipPath, payload);
      await this.extractZip(zipPath, extractPath);
      const contentRoot = await this.resolveArchiveContentRoot(extractPath);
      const metadata = await this.resolveBundleMetadata(contentRoot, asset, configVersion);
      logBundle("bundle.install.start", {
        skillId: asset.skillId,
        projectId: metadata.projectId,
        projectName: metadata.projectName
      });
      const materialized = await this.materializeBundle(workspaceRoot, tempRoot, metadata);
      try {
        await this.projectStore.syncBundleProject({
          projectId: metadata.projectId,
          projectName: metadata.projectName,
          version: metadata.version ?? configVersion,
          boundSkillIds: materialized.skillIds,
          declaredBoundSkillIds: metadata.declaredBoundSkillIds,
          description: metadata.description ?? asset.description,
          ready: true
        });
      } catch (error) {
        await materialized.rollback().catch(() => undefined);
        throw error;
      }
      await materialized.finalize().catch(() => undefined);
      logBundle("bundle.install.success", {
        skillId: asset.skillId,
        projectId: metadata.projectId,
        elapsedMs: Date.now() - startedAt
      });
      return {
        sourceUrl: asset.downloadUrl!,
        fileName: asset.fileName ?? `${asset.skillId}.zip`,
        configVersion,
        updatedAt: nowIso(),
        projectId: metadata.projectId,
        checksum: createHash("sha1").update(payload).digest("hex"),
        sourceSkillId: asset.skillId,
        sharedSkillEntries: materialized.sharedSkillEntries,
        sharedCronEntries: materialized.sharedCronEntries,
        remoteFileSize: asset.fileSize ?? payload.length,
        remoteContentLength: resolvedFreshnessProbe?.contentLength ?? payload.length,
        remoteEtag: resolvedFreshnessProbe?.etag,
        remoteLastModified: resolvedFreshnessProbe?.lastModified
      };
    } catch (error) {
      logBundle("bundle.sync.error", {
        skillId: asset.skillId,
        source: sanitizeUrl(new URL(asset.downloadUrl!)),
        error: error instanceof Error ? error.message : String(error),
        elapsedMs: Date.now() - startedAt
      });
      throw error;
    } finally {
      await rm(tempRoot, { recursive: true, force: true }).catch(() => undefined);
    }
  }

  private async resolveBundleMetadata(sourceRoot: string, asset: RemoteSkillAsset, configVersion?: string): Promise<BundleProjectMetadata> {
    const sourceProjectsDir = path.join(sourceRoot, "projects");
    const bundleName = normalizeBundleName(asset.fileName ?? asset.name);
    const bundleLabel = asset.fileName ?? asset.skillId;
    if (await pathExists(sourceProjectsDir)) {
      const projectEntries = (await readdir(sourceProjectsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory());
      if (projectEntries.length !== 1) {
        throw new Error(`Project bundle must contain exactly one project directory under projects/, found ${projectEntries.length}.`);
      }
      const projectEntry = projectEntries[0];
      const projectSourceRoot = path.join(sourceProjectsDir, projectEntry.name);
      const projectJson = await this.readRequiredProjectJson(path.join(projectSourceRoot, "project.json"), bundleLabel);
      const projectId = projectJson.id?.trim();
      if (!projectId) {
        throw new Error(`Project bundle ${bundleLabel} is missing required project id in project.json.`);
      }
      const projectName = projectJson.name?.trim();
      if (!projectName) {
        throw new Error(`Project bundle ${bundleLabel} is missing required project name in project.json.`);
      }
      return {
        projectId,
        projectName,
        version: projectJson?.version?.trim() || configVersion,
        description: projectJson?.description?.trim() || asset.description,
        declaredBoundSkillIds: normalizeStringArray(projectJson.boundSkillIds),
        projectSourceRoot,
        sharedSourceRoot: sourceRoot
      };
    }

    const rootProjectJson = await this.readRequiredProjectJson(path.join(sourceRoot, "project.json"), bundleLabel);
    const projectId = rootProjectJson.id?.trim();
    if (!projectId) {
      throw new Error(`Project bundle ${bundleLabel} is missing required project id in project.json.`);
    }
    const projectName = rootProjectJson.name?.trim();
    if (!projectName) {
      throw new Error(`Project bundle ${bundleLabel} is missing required project name in project.json.`);
    }
    return {
      projectId,
      projectName,
      version: rootProjectJson?.version?.trim() || configVersion,
      description: rootProjectJson?.description?.trim() || asset.description,
      declaredBoundSkillIds: normalizeStringArray(rootProjectJson.boundSkillIds),
      projectSourceRoot: sourceRoot,
      sharedSourceRoot: sourceRoot
    };
  }

  private async withWindowsFsRetry<T>(
    context: RetryableFsOperationLogContext,
    operation: () => Promise<T>
  ): Promise<T> {
    let attempt = 0;
    while (true) {
      try {
        return await operation();
      } catch (error) {
        if (!isRetryableWindowsFsError(error) || attempt >= WINDOWS_FS_RETRY_DELAYS_MS.length) {
          throw error;
        }

        const delayMs = WINDOWS_FS_RETRY_DELAYS_MS[attempt];
        const errorCode = getFsErrorCode(error);
        attempt += 1;
        logBundle("bundle.fs.retry", {
          ...context,
          attempt,
          delayMs,
          errorCode,
          error: error instanceof Error ? error.message : String(error)
        });
        await this.startupLogger?.warn("project-bundle", "fs.retry", "Retrying Windows bundle filesystem operation.", {
          ...context,
          attempt,
          delayMs,
          errorCode,
          error: error instanceof Error ? error.message : String(error)
        });
        await delay(delayMs);
      }
    }
  }

  private async removeReplacementPath(
    targetPath: string,
    projectId?: string,
    options?: RemoveReplacementPathOptions
  ): Promise<void> {
    await this.withWindowsFsRetry(
      {
        action: "remove-path",
        path: targetPath,
        projectId
      },
      async () => {
        await rm(targetPath, { recursive: true, force: true });
      }
    ).catch(() => undefined);

    if (options?.requireMissing && await pathExists(targetPath)) {
      throw new Error(`Project bundle replacement target could not be cleared: ${targetPath}`);
    }
  }

  private async moveReplacementPath(sourcePath: string, targetPath: string, action: string, projectId?: string): Promise<void> {
    try {
      await this.withWindowsFsRetry(
        {
          action,
          path: sourcePath,
          backupPath: targetPath,
          projectId
        },
        async () => {
          await rename(sourcePath, targetPath);
        }
      );
    } catch (error) {
      if (action === "backup-rename" || !isRetryableWindowsFsError(error)) {
        throw error;
      }

      const errorCode = getFsErrorCode(error);
      logBundle("bundle.fs.apply_fallback", {
        action,
        path: sourcePath,
        backupPath: targetPath,
        projectId,
        errorCode,
        error: error instanceof Error ? error.message : String(error)
      });
      await this.startupLogger?.warn("project-bundle", "apply.fallback", "Falling back to copy-based project bundle replacement.", {
        action,
        path: sourcePath,
        backupPath: targetPath,
        projectId,
        errorCode,
        error: error instanceof Error ? error.message : String(error)
      });
      await this.removeReplacementPath(targetPath, projectId, { requireMissing: true });
      await cp(sourcePath, targetPath, { recursive: true, force: true });
      await this.removeReplacementPath(sourcePath, projectId, { requireMissing: true });
    }
  }

  private async backupReplacementTarget(
    operation: CommittedBundleReplacementOperation,
    projectId: string
  ): Promise<void> {
    if (!operation.backupPath) {
      return;
    }

    await mkdir(path.dirname(operation.backupPath), { recursive: true });
    await this.removeReplacementPath(operation.backupPath, projectId);

    try {
      await this.moveReplacementPath(operation.targetPath, operation.backupPath, "backup-rename", projectId);
      operation.backupMode = "rename";
      return;
    } catch (error) {
      if (!isRetryableWindowsFsError(error)) {
        throw error;
      }

      const errorCode = getFsErrorCode(error);
      logBundle("bundle.fs.backup_fallback", {
        action: "backup-copy",
        path: operation.targetPath,
        backupPath: operation.backupPath,
        projectId,
        errorCode,
        error: error instanceof Error ? error.message : String(error)
      });
      await this.startupLogger?.warn("project-bundle", "backup.fallback", "Falling back to copy-based project bundle backup.", {
        action: "backup-copy",
        path: operation.targetPath,
        backupPath: operation.backupPath,
        projectId,
        errorCode,
        error: error instanceof Error ? error.message : String(error)
      });
      await cp(operation.targetPath, operation.backupPath, { recursive: true, force: true });
      await this.removeReplacementPath(operation.targetPath, projectId, { requireMissing: true });
      operation.backupMode = "copy";
    }
  }

  private async restoreReplacementBackup(operation: CommittedBundleReplacementOperation, projectId?: string): Promise<void> {
    if (!operation.backupPath || !(await pathExists(operation.backupPath))) {
      return;
    }

    await mkdir(path.dirname(operation.targetPath), { recursive: true });
    await this.removeReplacementPath(operation.targetPath, projectId);
    if (operation.backupMode === "copy") {
      logBundle("bundle.fs.rollback_restore", {
        action: "rollback-copy-restore",
        path: operation.targetPath,
        backupPath: operation.backupPath,
        projectId
      });
      await this.startupLogger?.info("project-bundle", "rollback.copy_restore", "Restoring project bundle backup from copied directory.", {
        action: "rollback-copy-restore",
        path: operation.targetPath,
        backupPath: operation.backupPath,
        projectId
      });
      await cp(operation.backupPath, operation.targetPath, { recursive: true, force: true });
      return;
    }

    await this.moveReplacementPath(operation.backupPath, operation.targetPath, "rollback-restore", projectId);
  }

  private async materializeBundle(
    workspaceRoot: string,
    tempRoot: string,
    metadata: BundleProjectMetadata
  ): Promise<MaterializedBundleTransaction> {
    const projectsRoot = path.join(workspaceRoot, "projects");
    const skillsRoot = path.join(workspaceRoot, "skills");
    const cronRoot = path.join(workspaceRoot, "cron");
    await Promise.all([
      mkdir(projectsRoot, { recursive: true }),
      mkdir(skillsRoot, { recursive: true }),
      mkdir(cronRoot, { recursive: true })
    ]);

    const stagingRoot = path.join(tempRoot, "materialized");
    const stagedProjectRoot = path.join(stagingRoot, "projects", metadata.projectId);
    const existingProjectRoot = path.join(projectsRoot, metadata.projectId);
    await this.stageProjectDirectory(metadata, stagedProjectRoot, existingProjectRoot);

    const stagedSkills = await this.stageSharedDirectories(
      this.resolveSharedSubdir(metadata, "skills"),
      path.join(stagingRoot, "skills"),
      skillsRoot
    );
    const stagedCron = await this.stageSharedDirectories(
      this.resolveSharedSubdir(metadata, "cron"),
      path.join(stagingRoot, "cron"),
      cronRoot
    );

    const replacementOperations: BundleReplacementOperation[] = [
      ...stagedSkills.operations,
      ...stagedCron.operations,
      this.buildReplacementOperation(stagedProjectRoot, path.join(projectsRoot, metadata.projectId))
    ];
    const backupRoot = path.join(tempRoot, "replacement-backups");
    const committedOperations: CommittedBundleReplacementOperation[] = [];
    let settled = false;

    try {
      for (const operation of replacementOperations) {
        const committedOperation: CommittedBundleReplacementOperation = {
          ...operation,
          targetExisted: await pathExists(operation.targetPath),
          backupPath: undefined,
          backupMode: undefined,
          applied: false
        };

        if (committedOperation.targetExisted) {
          committedOperation.backupPath = this.buildReplacementBackupPath(backupRoot, operation.targetPath);
          await this.backupReplacementTarget(committedOperation, metadata.projectId);
        }

        committedOperations.push(committedOperation);
        await mkdir(path.dirname(operation.targetPath), { recursive: true });
        await this.moveReplacementPath(operation.stagedPath, operation.targetPath, "apply-replacement", metadata.projectId);
        committedOperation.applied = true;
      }
    } catch (error) {
      await this.rollbackReplacementOperations(committedOperations, metadata.projectId).catch(() => undefined);
      throw error;
    }

    return {
      skillIds: stagedSkills.directoryIds,
      sharedSkillEntries: stagedSkills.entryNames,
      sharedCronEntries: stagedCron.entryNames,
      finalize: async () => {
        if (settled) {
          return;
        }
        settled = true;
        await this.cleanupReplacementBackups(committedOperations, metadata.projectId);
      },
      rollback: async () => {
        if (settled) {
          return;
        }
        settled = true;
        await this.rollbackReplacementOperations(committedOperations, metadata.projectId);
      }
    };
  }

  private resolveSharedSubdir(metadata: BundleProjectMetadata, dirname: "skills" | "cron"): string | null {
    const rootDir = path.join(metadata.sharedSourceRoot, dirname);
    const projectDir = path.join(metadata.projectSourceRoot, dirname);
    if (metadata.sharedSourceRoot !== metadata.projectSourceRoot && rootDir !== projectDir) {
      return rootDir;
    }
    return projectDir;
  }

  private async stageProjectDirectory(
    metadata: BundleProjectMetadata,
    stagedProjectRoot: string,
    existingProjectRoot?: string
  ): Promise<void> {
    await rm(stagedProjectRoot, { recursive: true, force: true }).catch(() => undefined);
    await mkdir(path.dirname(stagedProjectRoot), { recursive: true });
    await cp(metadata.projectSourceRoot, stagedProjectRoot, { recursive: true, force: true });

    const projectJsonPath = path.join(stagedProjectRoot, "project.json");
    const existing = await this.readProjectJson(projectJsonPath);
    await writeJsonFile(projectJsonPath, {
      ...(existing ?? {}),
      id: metadata.projectId,
      name: metadata.projectName,
      version: metadata.version ?? existing?.version,
      description: metadata.description ?? existing?.description,
      updatedAt: nowIso(),
      ready: true,
      boundSkillIds: metadata.declaredBoundSkillIds
    });
    await mkdir(path.join(stagedProjectRoot, "memory"), { recursive: true });

    if (existingProjectRoot && await pathExists(existingProjectRoot)) {
      await this.preserveLocalProjectState(existingProjectRoot, stagedProjectRoot);
    }
  }

  private async preserveLocalProjectState(existingProjectRoot: string, stagedProjectRoot: string): Promise<void> {
    for (const entryName of ["sessions.json", "session-messages", "memory"] as const) {
      const sourcePath = path.join(existingProjectRoot, entryName);
      if (!(await pathExists(sourcePath))) {
        continue;
      }

      const targetPath = path.join(stagedProjectRoot, entryName);
      await rm(targetPath, { recursive: true, force: true }).catch(() => undefined);
      await mkdir(path.dirname(targetPath), { recursive: true });
      await cp(sourcePath, targetPath, { recursive: true, force: true });
    }
  }

  private async stageSharedDirectories(sourceDir: string | null, stagingDir: string, targetRoot: string): Promise<StagedSharedEntries> {
    if (!sourceDir || !(await pathExists(sourceDir))) {
      return {
        entryNames: [],
        directoryIds: [],
        operations: []
      };
    }

    const entries = await readdir(sourceDir, { withFileTypes: true });
    const entryNames: string[] = [];
    const directoryIds: string[] = [];
    const operations: BundleReplacementOperation[] = [];
    for (const entry of entries) {
      if (!entry.isDirectory() && !entry.isFile()) {
        continue;
      }
      const stagedEntryPath = path.join(stagingDir, entry.name);
      await mkdir(path.dirname(stagedEntryPath), { recursive: true });
      await rm(stagedEntryPath, { recursive: true, force: true }).catch(() => undefined);
      await cp(path.join(sourceDir, entry.name), stagedEntryPath, { recursive: true, force: true });
      operations.push(this.buildReplacementOperation(stagedEntryPath, path.join(targetRoot, entry.name)));
      entryNames.push(entry.name);
      if (entry.isDirectory()) {
        directoryIds.push(entry.name);
      }
    }

    return {
      entryNames: uniqueStrings(entryNames),
      directoryIds: uniqueStrings(directoryIds),
      operations
    };
  }

  private buildReplacementOperation(stagedPath: string, targetPath: string): BundleReplacementOperation {
    return {
      stagedPath,
      targetPath
    };
  }

  private buildReplacementBackupPath(backupRoot: string, targetPath: string): string {
    const hashedTarget = createHash("sha1").update(targetPath).digest("hex");
    return path.join(backupRoot, `${path.basename(targetPath)}-${hashedTarget}`);
  }

  private async rollbackReplacementOperations(
    operations: CommittedBundleReplacementOperation[],
    projectId?: string
  ): Promise<void> {
    for (const operation of [...operations].reverse()) {
      if (operation.applied) {
        await this.removeReplacementPath(operation.targetPath, projectId);
      }
      await this.restoreReplacementBackup(operation, projectId);
    }
  }

  private async cleanupReplacementBackups(
    operations: CommittedBundleReplacementOperation[],
    projectId?: string
  ): Promise<void> {
    const backupPaths = uniqueStrings(operations.map((operation) => operation.backupPath ?? ""));
    for (const backupPath of backupPaths) {
      await this.removeReplacementPath(backupPath, projectId);
    }
  }

  private async readProjectJson(filePath: string): Promise<ProjectJsonShape | null> {
    return readJsonFile<ProjectJsonShape>(filePath);
  }

  private async readRequiredProjectJson(filePath: string, bundleLabel: string): Promise<ProjectJsonShape> {
    let raw: string;
    try {
      raw = await readFile(filePath, "utf8");
    } catch {
      throw new Error(`Project bundle ${bundleLabel} is missing required project.json at ${filePath}.`);
    }

    try {
      const parsed = JSON.parse(raw.replace(/^\uFEFF/, "")) as unknown;
      if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
        throw new Error("project.json must contain a JSON object.");
      }
      return parsed as ProjectJsonShape;
    } catch (error) {
      throw new Error(`Project bundle ${bundleLabel} has invalid project.json at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private async resolveArchiveContentRoot(extractPath: string): Promise<string> {
    const entries = await readdir(extractPath, { withFileTypes: true });
    const directories = entries.filter((entry) => entry.isDirectory());
    const files = entries.filter((entry) => entry.isFile());
    if (directories.length === 1 && files.length === 0) {
      const singleDirectory = directories[0].name;
      if (singleDirectory === "projects" || singleDirectory === "skills" || singleDirectory === "cron") {
        return extractPath;
      }
      return path.join(extractPath, singleDirectory);
    }
    return extractPath;
  }

  private async extractZip(zipPath: string, destinationPath: string): Promise<void> {
    await mkdir(destinationPath, { recursive: true });
    const extractZip = await loadExtractZip();
    await extractZip(zipPath, { dir: destinationPath });
  }

  private async probeRemoteBundle(url: URL, redirectCount = 0): Promise<RemoteBundleProbeResult | null> {
    const startedAt = Date.now();
    logBundle("bundle.probe.start", {
      source: sanitizeUrl(url),
      redirectCount
    });
    const client = url.protocol === "https:" ? https : http;
    return new Promise<RemoteBundleProbeResult | null>((resolve, reject) => {
      let settled = false;
      const finishReject = (error: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        logBundle("bundle.probe.result", {
          source: sanitizeUrl(url),
          redirectCount,
          outcome: "error",
          error: error.message,
          elapsedMs: Date.now() - startedAt
        });
        reject(error);
      };
      const finishResolve = (result: RemoteBundleProbeResult | null) => {
        if (settled) {
          return;
        }
        settled = true;
        logBundle("bundle.probe.result", {
          source: sanitizeUrl(url),
          redirectCount,
          outcome: result ? "success" : "unsupported-head",
          contentLength: result?.contentLength,
          elapsedMs: Date.now() - startedAt
        });
        resolve(result);
      };

      const request = client.request(url, { method: "HEAD" }, (response) => {
        const status = response.statusCode ?? 500;
        const location = response.headers.location;
        if (location && REDIRECT_STATUS_CODES.has(status)) {
          if (redirectCount >= MAX_REDIRECTS) {
            finishReject(new Error("Project bundle probe redirected too many times."));
            response.resume();
            return;
          }
          const redirectUrl = new URL(location, url);
          response.resume();
          void this.probeRemoteBundle(redirectUrl, redirectCount + 1).then(finishResolve, finishReject);
          return;
        }
        if (HEAD_UNSUPPORTED_STATUS_CODES.has(status)) {
          response.resume();
          finishResolve(null);
          return;
        }
        if (status < 200 || status >= 300) {
          finishReject(new Error(`Project bundle freshness probe failed with HTTP status ${status}.`));
          response.resume();
          return;
        }

        const etag = normalizeHeaderValue(response.headers.etag);
        const lastModified = normalizeHeaderValue(response.headers["last-modified"]);
        const contentLength = normalizeContentLength(normalizeHeaderValue(response.headers["content-length"]));
        response.resume();
        finishResolve({
          etag,
          lastModified,
          contentLength
        });
      });
      request.setTimeout(BUNDLE_REQUEST_IDLE_TIMEOUT_MS, () => {
        request.destroy(new Error(`Project bundle freshness probe timed out after ${Math.round(BUNDLE_REQUEST_IDLE_TIMEOUT_MS / 1000)}s.`));
      });
      request.on("error", (error) => finishReject(new Error(`Project bundle freshness probe failed: ${error.message}`)));
      request.end();
    });
  }

  private async downloadToBuffer(url: URL, redirectCount = 0): Promise<Buffer> {
    const startedAt = Date.now();
    logBundle("bundle.download.start", {
      source: sanitizeUrl(url),
      redirectCount
    });
    const client = url.protocol === "https:" ? https : http;
    return new Promise<Buffer>((resolve, reject) => {
      let settled = false;
      const finishReject = (error: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        logBundle("bundle.download.error", {
          source: sanitizeUrl(url),
          redirectCount,
          error: error.message,
          elapsedMs: Date.now() - startedAt
        });
        reject(error);
      };
      const finishResolve = (payload: Buffer) => {
        if (settled) {
          return;
        }
        settled = true;
        logBundle("bundle.download.success", {
          source: sanitizeUrl(url),
          redirectCount,
          byteLength: payload.length,
          elapsedMs: Date.now() - startedAt
        });
        resolve(payload);
      };

      const request = client.get(url, (response) => {
        const status = response.statusCode ?? 500;
        const location = response.headers.location;
        if (location && REDIRECT_STATUS_CODES.has(status)) {
          if (redirectCount >= MAX_REDIRECTS) {
            finishReject(new Error("Project bundle download redirected too many times."));
            response.resume();
            return;
          }
          const redirectUrl = new URL(location, url);
          response.resume();
          void this.downloadToBuffer(redirectUrl, redirectCount + 1).then(finishResolve, finishReject);
          return;
        }
        if (status < 200 || status >= 300) {
          finishReject(new Error(`Project bundle download failed with HTTP status ${status}.`));
          response.resume();
          return;
        }
        const chunks: Buffer[] = [];
        response.on("data", (chunk) => {
          chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
        });
        response.on("end", () => finishResolve(Buffer.concat(chunks)));
      });
      request.setTimeout(BUNDLE_REQUEST_IDLE_TIMEOUT_MS, () => {
        request.destroy(new Error(`Project bundle download timed out after ${Math.round(BUNDLE_REQUEST_IDLE_TIMEOUT_MS / 1000)}s.`));
      });
      request.on("error", (error) => finishReject(new Error(`Project bundle download failed: ${error.message}`)));
    });
  }
}








