import {
  projectFilePath,
  type ProjectFilePath,
  type ProjectStorageProvider,
} from '@learnitive/project-storage';

export type GitWorkingTree = ReadonlyMap<ProjectFilePath, Uint8Array>;

function isGitInternal(path: string): boolean {
  return path === '.git' || path.startsWith('.git/');
}

function copyBytes(bytes: Uint8Array): Uint8Array {
  return bytes.slice();
}

/** Reads the complete user working tree without interpreting file contents. */
export async function readProjectWorkingTree(
  storage: ProjectStorageProvider,
  signal?: AbortSignal,
): Promise<Map<ProjectFilePath, Uint8Array>> {
  signal?.throwIfAborted();
  const files = new Map<ProjectFilePath, Uint8Array>();
  for (const entry of await storage.listEntries(signal)) {
    signal?.throwIfAborted();
    if (entry.kind !== 'file' || isGitInternal(entry.path)) {
      continue;
    }
    const bytes = await storage.readFile(entry.path, signal);
    if (bytes === undefined) {
      throw new Error(
        `Project file disappeared while reading Git tree: ${entry.path}`,
      );
    }
    files.set(entry.path, copyBytes(bytes));
  }
  return files;
}

/**
 * Replaces the user working tree with bytes produced by a Git checkout/clone
 * while preserving `.git/*`. Existing user files are retained in memory until
 * every incoming write succeeds; any failure rolls storage back to its exact
 * pre-operation bytes rather than leaving a partial checkout.
 */
export async function applyGitWorkingTree(
  storage: ProjectStorageProvider,
  workingTree: ReadonlyMap<string, Uint8Array>,
  signal?: AbortSignal,
): Promise<ProjectFilePath[]> {
  signal?.throwIfAborted();

  const incoming = new Map<ProjectFilePath, Uint8Array>();
  for (const [rawPath, bytes] of workingTree) {
    signal?.throwIfAborted();
    const path = projectFilePath(rawPath);
    if (isGitInternal(path)) {
      continue;
    }
    if (incoming.has(path)) {
      throw new Error(`Duplicate Git working-tree path: ${path}`);
    }
    incoming.set(path, copyBytes(bytes));
  }

  const before = await readProjectWorkingTree(storage, signal);
  const written = new Set<ProjectFilePath>();

  try {
    for (const [path, bytes] of incoming) {
      signal?.throwIfAborted();
      await storage.writeFile(path, bytes, signal);
      written.add(path);
    }

    for (const path of before.keys()) {
      signal?.throwIfAborted();
      if (!incoming.has(path)) {
        await storage.deleteFile(path, signal);
      }
    }
  } catch (error) {
    // Restore all original bytes first.
    for (const [path, bytes] of before) {
      await storage.writeFile(path, bytes).catch(() => undefined);
    }
    // Remove files that did not exist before the failed operation.
    for (const path of written) {
      if (!before.has(path)) {
        await storage.deleteFile(path).catch(() => undefined);
      }
    }
    throw error;
  }

  return [...incoming.keys()].sort((a, b) => a.localeCompare(b));
}
