import { mkdir } from 'node:fs/promises';
import path from 'node:path';
import { execa } from 'execa';

export type RunOptions = {
  cwd: string;
  env?: Record<string, string>;
  redact: (value: string) => string;
  timeoutMs: number;
};

export const run = async (
  command: string,
  args: string[],
  options: RunOptions,
) => {
  const result = await execa(command, args, {
    cwd: options.cwd,
    env: options.env,
    all: true,
    reject: false,
    timeout: options.timeoutMs,
  });
  return {
    exitCode: result.exitCode ?? 0,
    output: options.redact(result.all),
  };
};

export const cloneRepository = async (args: {
  workdir: string;
  token: string;
  owner: string;
  repo: string;
  baseBranch: string;
  workBranch: string;
  redact: (value: string) => string;
  timeoutMs: number;
  // Directory name to clone into under `workdir` (default "repo"). Used to lay
  // out checkouts as ~/Code/{spoon}/{branch}.
  dirName?: string;
}) => {
  await mkdir(args.workdir, { recursive: true });
  const dirName = args.dirName ?? 'repo';
  const repoUrl = `https://x-access-token:${args.token}@github.com/${args.owner}/${args.repo}.git`;
  const clone = await run(
    'git',
    ['clone', '--branch', args.baseBranch, '--single-branch', repoUrl, dirName],
    {
      cwd: args.workdir,
      redact: args.redact,
      timeoutMs: args.timeoutMs,
    },
  );
  if (clone.exitCode !== 0) {
    throw new Error(`git clone failed:\n${clone.output}`);
  }
  const repoDir = path.join(args.workdir, dirName);
  const checkout = await run('git', ['checkout', '-b', args.workBranch], {
    cwd: repoDir,
    redact: args.redact,
    timeoutMs: args.timeoutMs,
  });
  if (checkout.exitCode !== 0) {
    throw new Error(`git checkout failed:\n${checkout.output}`);
  }
  return repoDir;
};

export const commitAndPush = async (args: {
  repoDir: string;
  workBranch: string;
  message: string;
  redact: (value: string) => string;
  timeoutMs: number;
}) => {
  await run('git', ['config', 'user.name', 'Spoon Agent'], {
    cwd: args.repoDir,
    redact: args.redact,
    timeoutMs: args.timeoutMs,
  });
  await run(
    'git',
    ['config', 'user.email', 'spoon-agent@users.noreply.github.com'],
    {
      cwd: args.repoDir,
      redact: args.redact,
      timeoutMs: args.timeoutMs,
    },
  );
  await run('git', ['add', '-A'], {
    cwd: args.repoDir,
    redact: args.redact,
    timeoutMs: args.timeoutMs,
  });
  const commit = await run('git', ['commit', '-m', args.message], {
    cwd: args.repoDir,
    redact: args.redact,
    timeoutMs: args.timeoutMs,
  });
  if (commit.exitCode !== 0) {
    throw new Error(`git commit failed:\n${commit.output}`);
  }
  const sha = await run('git', ['rev-parse', 'HEAD'], {
    cwd: args.repoDir,
    redact: args.redact,
    timeoutMs: args.timeoutMs,
  });
  const push = await run('git', ['push', 'origin', args.workBranch], {
    cwd: args.repoDir,
    redact: args.redact,
    timeoutMs: args.timeoutMs,
  });
  if (push.exitCode !== 0) {
    throw new Error(`git push failed:\n${push.output}`);
  }
  return sha.output.trim();
};

export const getDiff = async (
  repoDir: string,
  redact: (value: string) => string,
) =>
  await run('git', ['diff', '--cached', '--', '.'], {
    cwd: repoDir,
    redact,
    timeoutMs: 60_000,
  });

export const getWorktreeDiff = async (
  repoDir: string,
  redact: (value: string) => string,
) => {
  const trackedDiff = await run('git', ['diff', '--', '.'], {
    cwd: repoDir,
    redact,
    timeoutMs: 60_000,
  });
  const untracked = await run(
    'git',
    ['ls-files', '--others', '--exclude-standard'],
    {
      cwd: repoDir,
      redact,
      timeoutMs: 60_000,
    },
  );
  const untrackedDiffs: string[] = [];
  for (const filePath of untracked.output.split('\n').filter(Boolean)) {
    const diff = await run(
      'git',
      ['diff', '--no-index', '--', '/dev/null', filePath],
      {
        cwd: repoDir,
        redact,
        timeoutMs: 60_000,
      },
    );
    if (diff.output.trim()) untrackedDiffs.push(diff.output);
  }
  return {
    exitCode: trackedDiff.exitCode === 0 && untracked.exitCode === 0 ? 0 : 1,
    output: [trackedDiff.output, ...untrackedDiffs]
      .filter((part) => part.trim())
      .join('\n'),
  };
};

export const getStatus = async (
  repoDir: string,
  redact: (value: string) => string,
) =>
  await run('git', ['status', '--short'], {
    cwd: repoDir,
    redact,
    timeoutMs: 60_000,
  });
