import "server-only";

import { spawn } from "node:child_process";
import path from "node:path";

import { getMediaProcessingConfig } from "@/lib/server/media-config";
import { MediaProcessingError } from "@/lib/server/media-errors";

interface RunBinaryCommandOptions {
  timeoutMs?: number;
  cwd?: string;
}

export interface CommandExecutionResult {
  binaryPath: string;
  args: string[];
  stdout: string;
  stderr: string;
  durationMs: number;
}

function joinTail(output: string, maxLength = 4000) {
  if (output.length <= maxLength) {
    return output;
  }

  return output.slice(output.length - maxLength);
}

function getMissingBinaryMessage(binaryPath: string) {
  const resolvedConfig = getMediaProcessingConfig();
  const normalized = path.basename(binaryPath).toLowerCase();
  const isFfprobe = normalized.includes("ffprobe");
  const envVariable = isFfprobe ? "FFPROBE_PATH" : "FFMPEG_PATH";

  return `Unable to find '${binaryPath}'. Install FFmpeg (includes ffprobe) or set ${envVariable} to an absolute binary path. Current resolution: ffmpeg='${resolvedConfig.ffmpegPath}' (${resolvedConfig.ffmpegSource}), ffprobe='${resolvedConfig.ffprobePath}' (${resolvedConfig.ffprobeSource}).`;
}

export async function runBinaryCommand(
  binaryPath: string,
  args: string[],
  options: RunBinaryCommandOptions = {}
): Promise<CommandExecutionResult> {
  const startedAt = Date.now();
  const config = getMediaProcessingConfig();
  const timeoutMs = options.timeoutMs ?? config.maxCommandDurationMs;

  return await new Promise((resolve, reject) => {
    const childProcess = spawn(binaryPath, args, {
      windowsHide: true,
      cwd: options.cwd,
      stdio: ["ignore", "pipe", "pipe"]
    });

    let stdout = "";
    let stderr = "";
    let didTimeout = false;

    const timer = setTimeout(() => {
      didTimeout = true;
      childProcess.kill("SIGKILL");
    }, timeoutMs);

    childProcess.stdout.on("data", (chunk: Buffer | string) => {
      stdout += chunk.toString();
    });

    childProcess.stderr.on("data", (chunk: Buffer | string) => {
      stderr += chunk.toString();
    });

    childProcess.on("error", (error) => {
      clearTimeout(timer);
      const isMissingBinaryError = error.message.includes("ENOENT");
      reject(
        new MediaProcessingError({
          message: isMissingBinaryError
            ? getMissingBinaryMessage(binaryPath)
            : `Failed to start command '${binaryPath}'. ${error.message}`,
          code: isMissingBinaryError ? "missing_binary" : "ffmpeg_failure",
          statusCode: isMissingBinaryError ? 503 : 500
        })
      );
    });

    childProcess.on("close", (exitCode) => {
      clearTimeout(timer);
      const durationMs = Date.now() - startedAt;

      if (didTimeout) {
        reject(
          new MediaProcessingError({
            message: `Command timed out after ${timeoutMs}ms.`,
            code: "timeout",
            statusCode: 504,
            details: {
              binaryPath,
              args,
              stderr: joinTail(stderr)
            }
          })
        );
        return;
      }

      if (exitCode !== 0) {
        reject(
          new MediaProcessingError({
            message: `Command '${binaryPath}' exited with code ${String(exitCode)}.`,
            code: binaryPath.toLowerCase().includes("ffprobe") ? "ffprobe_failure" : "ffmpeg_failure",
            statusCode: 500,
            details: {
              binaryPath,
              args,
              stderr: joinTail(stderr),
              stdout: joinTail(stdout)
            }
          })
        );
        return;
      }

      resolve({
        binaryPath,
        args,
        stdout,
        stderr,
        durationMs
      });
    });
  });
}
