import { execFileSync, spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { once } from "node:events";
import {
  access,
  chmod,
  copyFile,
  mkdir,
  rm,
  writeFile,
} from "node:fs/promises";
import path from "node:path";

import type { PiHomeFiles } from "@code/primitives";

interface PrepareRepositoryInput {
  attemptId: string;
  piHomeFiles: PiHomeFiles;
}

interface PreparedRepository {
  baseRevision: string;
  checkoutPath: string;
  created: boolean;
  piHomePath: string;
  sourceRepositoryPath: string;
  toolsPath: string;
}

interface CollectRepositoryInput {
  attemptId: string;
  baseRevision: string;
  checkoutPath: string;
}

interface CollectedRepository {
  candidateRevision: string;
  changedFiles: string[];
  diff: string;
}

interface ProcessResult {
  exitCode: number;
  stderr: string;
  stdout: string;
}

const requireSuccess = (result: ProcessResult, operation: string): string => {
  if (result.exitCode !== 0) {
    throw new Error(`${operation} failed: ${result.stderr || result.stdout}`);
  }
  return result.stdout.trim();
};

const changedFilePath = (line: string): string => {
  const filePath = line.slice(3).trim();
  const renameSeparator = " -> ";
  const renameIndex = filePath.lastIndexOf(renameSeparator);
  return renameIndex === -1
    ? filePath
    : filePath.slice(renameIndex + renameSeparator.length);
};

const runProcess = async (
  command: string,
  args: readonly string[],
  cwd: string,
  env: Record<string, string> = {}
): Promise<ProcessResult> => {
  const environment = Object.fromEntries(
    Object.entries(process.env).filter(
      (entry): entry is [string, string] => entry[1] !== undefined
    )
  );
  const child = spawn(command, args, {
    cwd,
    env: { ...environment, ...env },
  });
  const stderr: Uint8Array[] = [];
  const stdout: Uint8Array[] = [];
  child.stderr.on("data", (chunk: Uint8Array) => stderr.push(chunk));
  child.stdout.on("data", (chunk: Uint8Array) => stdout.push(chunk));
  const [exitCode] = await once(child, "close");
  return {
    exitCode: typeof exitCode === "number" ? exitCode : 1,
    stderr: Buffer.concat(stderr).toString(),
    stdout: Buffer.concat(stdout).toString(),
  };
};

const runGit = (cwd: string, args: readonly string[]) =>
  runProcess("git", args, cwd);

const pathExists = async (target: string): Promise<boolean> => {
  try {
    await access(target);
    return true;
  } catch {
    return false;
  }
};

export class HostRepositoryWorkspace {
  readonly #root: string;
  readonly #sourceRepositoryPath: string;
  readonly #installDependencies: boolean;

  constructor(
    root = process.env.AGENT_WORKSPACE_ROOT ?? "/var/lib/zopu/workspaces",
    sourceRepositoryPath = process.env.ZOPU_SOURCE_REPOSITORY ??
      "/opt/zopu-source",
    installDependencies = true
  ) {
    this.#root = root;
    this.#sourceRepositoryPath = sourceRepositoryPath;
    this.#installDependencies = installDependencies;
  }

  async prepare(input: PrepareRepositoryInput): Promise<PreparedRepository> {
    if (!(await pathExists(path.join(this.#sourceRepositoryPath, ".git")))) {
      throw new Error(
        `Fixed Zopu source repository is unavailable at ${this.#sourceRepositoryPath}`
      );
    }

    const identity = createHash("sha256")
      .update(input.attemptId)
      .digest("hex")
      .slice(0, 24);
    const workspacePath = path.join(this.#root, identity);
    const checkoutPath = path.join(workspacePath, "repository");
    const toolsPath = path.join(workspacePath, "tools");
    const piHomePath = path.join(workspacePath, "home");
    const branch = `zopu/attempt-${identity}`;
    const created = !(await pathExists(path.join(checkoutPath, ".git")));

    await mkdir(workspacePath, { recursive: true });
    if (!created) {
      requireSuccess(
        await runGit(this.#sourceRepositoryPath, [
          "worktree",
          "remove",
          "--force",
          checkoutPath,
        ]),
        "Existing worktree removal"
      );
    }
    await rm(checkoutPath, { force: true, recursive: true });
    requireSuccess(
      await runGit(this.#sourceRepositoryPath, [
        "worktree",
        "add",
        "-B",
        branch,
        checkoutPath,
        "HEAD",
      ]),
      "Zopu worktree creation"
    );

    const bunExecutable =
      process.env.BUN_EXECUTABLE ??
      execFileSync("which", ["bun"], { encoding: "utf-8" }).trim();

    const sourceEnvPath = path.join(this.#sourceRepositoryPath, ".env");
    if (await pathExists(sourceEnvPath)) {
      await copyFile(sourceEnvPath, path.join(checkoutPath, ".env"));
    }
    if (this.#installDependencies) {
      requireSuccess(
        await runProcess(
          bunExecutable,
          ["install", "--frozen-lockfile"],
          checkoutPath,
          { CI: "1" }
        ),
        "Workspace dependency installation"
      );
    }

    const toolsBinPath = path.join(toolsPath, "bin");
    await mkdir(toolsBinPath, { recursive: true });
    const bunPath = path.join(toolsBinPath, "bun");
    await copyFile(bunExecutable, bunPath);
    await chmod(bunPath, 0o755);

    const piAgentPath = path.join(piHomePath, ".pi", "agent");
    await mkdir(piAgentPath, { recursive: true });
    await writeFile(
      path.join(piAgentPath, "models.json"),
      input.piHomeFiles.models
    );
    await writeFile(
      path.join(piAgentPath, "settings.json"),
      input.piHomeFiles.settings
    );

    return {
      baseRevision: requireSuccess(
        await runGit(checkoutPath, ["rev-parse", "HEAD"]),
        "Base revision lookup"
      ),
      checkoutPath,
      created,
      piHomePath,
      sourceRepositoryPath: this.#sourceRepositoryPath,
      toolsPath,
    };
  }

  static async collect(
    input: CollectRepositoryInput
  ): Promise<CollectedRepository> {
    const status = requireSuccess(
      await runGit(input.checkoutPath, ["status", "--porcelain"]),
      "Changed file lookup"
    );
    let diff = "";
    const changedFiles = status
      .split("\n")
      .filter(Boolean)
      .map(changedFilePath);
    let candidateRevision = input.baseRevision;

    if (changedFiles.length > 0) {
      requireSuccess(
        await runGit(input.checkoutPath, ["add", "-A"]),
        "Candidate staging"
      );
      diff = requireSuccess(
        await runGit(input.checkoutPath, [
          "diff",
          "--binary",
          "--cached",
          "--no-ext-diff",
          input.baseRevision,
        ]),
        "Diff collection"
      );
      const tree = requireSuccess(
        await runGit(input.checkoutPath, ["write-tree"]),
        "Candidate tree creation"
      );
      candidateRevision = requireSuccess(
        await runProcess(
          "git",
          [
            "commit-tree",
            tree,
            "-p",
            input.baseRevision,
            "-m",
            `Zopu candidate for ${input.attemptId}`,
          ],
          input.checkoutPath,
          {
            GIT_AUTHOR_EMAIL: "agent@zopu.dev",
            GIT_AUTHOR_NAME: "Zopu Agent",
            GIT_COMMITTER_EMAIL: "agent@zopu.dev",
            GIT_COMMITTER_NAME: "Zopu Agent",
          }
        ),
        "Candidate revision creation"
      );
      requireSuccess(
        await runGit(input.checkoutPath, ["reset"]),
        "Candidate index reset"
      );
    }

    return { candidateRevision, changedFiles, diff };
  }
}
