import { spawn } from 'node:child_process';
import { PassThrough, Transform } from 'node:stream';
import { resticProcessEnv } from './config.js';

export class CommandError extends Error {
  constructor(message, result) {
    super(message);
    this.name = 'CommandError';
    this.result = result;
  }
}

export function spawnCommand(command, args = [], options = {}) {
  const {
    env = process.env,
    input = null,
    log = null,
    ignoreExitCode = false,
    cwd = process.cwd(),
  } = options;

  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      cwd,
      env,
      shell: false,
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    let stdout = '';
    let stderr = '';

    child.stdout.on('data', (chunk) => {
      const text = chunk.toString();
      stdout += text;
      log?.(text.trimEnd());
    });

    child.stderr.on('data', (chunk) => {
      const text = chunk.toString();
      stderr += text;
      log?.(text.trimEnd());
    });

    child.on('error', (error) => {
      reject(error);
    });

    child.on('close', (exitCode) => {
      const result = { stdout, stderr, exitCode };
      if (exitCode !== 0 && !ignoreExitCode) {
        reject(new CommandError(stderr.trim() || `${command} exited with ${exitCode}`, result));
        return;
      }
      resolve(result);
    });

    if (input) {
      input.on('error', (error) => {
        child.kill('SIGTERM');
        reject(error);
      });
      input.pipe(child.stdin);
    } else {
      child.stdin.end();
    }
  });
}

export function runRestic(args, options = {}) {
  return spawnCommand('restic', args, {
    ...options,
    env: resticProcessEnv(),
  });
}

export async function resticSnapshotFileSize(snapshotId, filename) {
  const result = await runRestic(['ls', '--json', snapshotId]);
  const wanted = `/${filename}`;
  for (const line of result.stdout.split('\n')) {
    if (!line.trim()) continue;
    const entry = JSON.parse(line);
    if (entry.type === 'file' && (entry.path === filename || entry.path === wanted || entry.path?.endsWith(wanted))) {
      return Number(entry.size);
    }
  }
  const error = new Error(`Could not verify Restic file size for ${filename} in snapshot ${snapshotId}.`);
  error.status = 500;
  throw error;
}

export function createProgressStream(totalBytes, onProgress) {
  let currentBytes = 0;
  return new Transform({
    transform(chunk, _encoding, callback) {
      currentBytes += chunk.length;
      onProgress?.({
        currentBytes,
        totalBytes,
        percent: totalBytes ? (currentBytes / totalBytes) * 100 : 0,
      });
      callback(null, chunk);
    },
  });
}

export function streamResticDumpToDd(snapshotId, filename, outputPath, log, progress = null) {
  return new Promise((resolve, reject) => {
    const restic = spawn('restic', ['dump', snapshotId, filename], {
      env: resticProcessEnv(),
      shell: false,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    const dd = spawn('dd', [`of=${outputPath}`, 'bs=4M', 'conv=sparse', 'status=none'], {
      env: process.env,
      shell: false,
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    const errors = [];
    let resticClosed = false;
    let ddClosed = false;
    const pipe = new PassThrough();
    let restoreStream = restic.stdout.pipe(pipe);
    if (progress) {
      restoreStream = restoreStream.pipe(createProgressStream(progress.totalBytes, progress.onProgress));
    }
    restoreStream.pipe(dd.stdin);

    restic.stderr.on('data', (chunk) => log?.(chunk.toString().trimEnd()));
    dd.stderr.on('data', (chunk) => log?.(chunk.toString().trimEnd()));
    restic.on('error', reject);
    dd.on('error', reject);

    restic.on('close', (code) => {
      resticClosed = true;
      if (code !== 0) {
        errors.push(`restic dump exited with ${code}`);
        dd.stdin.destroy();
      }
      maybeFinish();
    });

    dd.on('close', (code) => {
      ddClosed = true;
      if (code !== 0) {
        errors.push(`dd exited with ${code}`);
      }
      maybeFinish();
    });

    function maybeFinish() {
      if (!resticClosed || !ddClosed) return;
      if (errors.length) {
        reject(new Error(errors.join('; ')));
        return;
      }
      resolve();
    }
  });
}
