import { ChildProcess, spawn } from "child_process";
import { ffmpegPath } from "./utils";

/**
 * FFmpegProcessManager - Handles the creation, management and termination of FFmpeg processes
 */
export class FFmpegProcessManager {
  private processes: Map<string, ChildProcess> = new Map();

  /**
   * Start a new FFmpeg process
   *
   * @param processName Unique identifier for the process
   * @param args FFmpeg command line arguments
   * @returns Promise that resolves when the process has started successfully
   */
  public startProcess(processName: string, args: string[]): Promise<void> {
    // Don't allow duplicate process names
    if (this.processes.has(processName)) {
      return Promise.reject(new Error(`Process ${processName} already exists`));
    }

    // Start the process
    const process = spawn(ffmpegPath, args);
    this.processes.set(processName, process);

    // Set up error handling for process startup
    return new Promise<void>((resolve, reject) => {
      // Handle immediate errors
      process.on("error", (error: Error) => {
        console.error(`Error starting ${processName}:`, error);
        this.processes.delete(processName);
        reject(error);
      });

      // Install early exit handler
      let earlyExitHandlerActive = true;
      process.once("exit", (code: number | null) => {
        if (earlyExitHandlerActive && code !== 0 && code !== null) {
          const errorMsg = `${processName} exited early with code ${code}`;
          console.error(errorMsg);
          this.processes.delete(processName);
          reject(new Error(errorMsg));
        }
      });

      // After a short delay, assume process started successfully
      setTimeout(() => {
        // Disable the early exit handler by setting the flag instead of removing
        earlyExitHandlerActive = false;

        // Set up regular exit handler
        process.on("exit", (code: number | null) => {
          if (code !== 0 && code !== null) {
            console.error(`${processName} exited with code ${code}`);
          }
          this.processes.delete(processName);
        });

        resolve();
      }, 500);

      // Set up stderr handling to check for startup errors
      let stderrData = "";
      process.stderr.on("data", (data: Buffer) => {
        stderrData += data.toString();
        // Check for common ffmpeg errors
        if (
          stderrData.includes("Cannot open") ||
          stderrData.includes("Error") ||
          stderrData.includes("Invalid")
        ) {
          console.error(`FFmpeg error: ${stderrData}`);
        }
      });
    });
  }

  /**
   * Stop a specific process
   *
   * @param processName Identifier of the process to stop
   * @returns Promise that resolves when the process has been stopped
   */
  public stopProcess(processName: string): Promise<void> {
    return new Promise((resolve) => {
      const process = this.processes.get(processName);

      if (!process) {
        resolve();
        return;
      }

      // Define the cleanup function
      const cleanup = () => {
        process.removeAllListeners();
        this.processes.delete(processName);
        resolve();
      };

      // Handle normal exit
      process.on("exit", cleanup);

      // Gracefully terminate the process with SIGTERM
      if (!process.killed) {
        process.kill("SIGTERM");

        // Set a timeout to force kill if needed
        setTimeout(() => {
          if (!process.killed) {
            try {
              process.kill("SIGKILL");
            } catch (e) {
              console.error(`Error killing ${processName}:`, e);
            }
          }
          cleanup();
        }, 2000);
      } else {
        cleanup();
      }
    });
  }

  /**
   * Stop all processes
   *
   * @returns Promise that resolves when all processes have been stopped
   */
  public stopAllProcesses(): Promise<void[]> {
    const processes = Array.from(this.processes.keys());
    const stopPromises = processes.map((processName) =>
      this.stopProcess(processName)
    );
    return Promise.all(stopPromises);
  }

  /**
   * Check if a process exists
   *
   * @param processName Name of the process to check
   * @returns true if the process exists
   */
  public hasProcess(processName: string): boolean {
    return this.processes.has(processName);
  }

  /**
   * Get a process by name
   *
   * @param processName Name of the process to get
   * @returns The ChildProcess or undefined if not found
   */
  public getProcess(processName: string): ChildProcess | undefined {
    return this.processes.get(processName);
  }

  /**
   * Get the count of running processes
   *
   * @returns Number of running processes
   */
  public getProcessCount(): number {
    return this.processes.size;
  }
}
