import * as fs from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { spawnSync } from 'node:child_process';

const DEFAULT_EXCLUDES = [
  'node_modules',
  '.git',
  'tests',
  '.forked-from',
  '.env',
  '.env.*',
  '*.spec.ts',
  '*.test.ts',
];

function runTar(args: string[]): void {
  const result = spawnSync('tar', args, {
    encoding: 'utf8',
    windowsHide: true,
  });
  if (result.status !== 0) {
    throw new Error(`tar failed: ${result.stderr || result.stdout || `exit ${result.status ?? 'unknown'}`}`);
  }
}

export function createTarball(sourceDir: string, tarballPath: string, excludes: string[] = DEFAULT_EXCLUDES): void {
  fs.mkdirSync(path.dirname(tarballPath), { recursive: true });
  const excludeArgs = excludes.flatMap((pattern) => ['--exclude', pattern]);
  runTar([
    '-czf',
    tarballPath,
    ...excludeArgs,
    '-C',
    sourceDir,
    '.',
  ]);
}

export function extractTarball(tarballPath: string, outputDir: string): void {
  fs.mkdirSync(outputDir, { recursive: true });
  runTar([
    '-xzf',
    tarballPath,
    '-C',
    outputDir,
  ]);
}

export function computeDirectorySha256(rootDir: string, excludes: Set<string> = new Set(['node_modules', '.git'])): string {
  const hash = crypto.createHash('sha256');

  function walk(dir: string): void {
    const entries = fs.readdirSync(dir, { withFileTypes: true })
      .filter((entry) => !excludes.has(entry.name))
      .sort((a, b) => a.name.localeCompare(b.name));

    for (const entry of entries) {
      const absolute = path.join(dir, entry.name);
      const relative = path.relative(rootDir, absolute).split(path.sep).join('/');
      if (entry.isDirectory()) {
        hash.update(`dir:${relative}\n`);
        walk(absolute);
      } else if (entry.isFile()) {
        hash.update(`file:${relative}\n`);
        hash.update(fs.readFileSync(absolute));
      }
    }
  }

  walk(rootDir);
  return hash.digest('hex');
}
