import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { spawn } from 'child_process';
import { randomUUID } from 'crypto';
import { mkdtemp, rm, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { extname, join } from 'path';

type CommandProbeResult = {
  path: string;
  available: boolean;
  version: string;
  error?: string;
};

@Injectable()
export class MediaProbeService {
  private readonly logger = new Logger(MediaProbeService.name);
  private readonly commandProbeCache = new Map<string, CommandProbeResult>();

  constructor(private readonly configService: ConfigService) {}

  getFfmpegPath(): string {
    return (
      this.configService.get<string>('videoProcessing.ffmpegPath', { infer: true }) ?? 'ffmpeg'
    ).trim();
  }

  getFfprobePath(): string {
    const configured = (
      this.configService.get<string>('videoProcessing.ffprobePath', { infer: true }) ?? ''
    ).trim();
    if (configured) {
      return configured;
    }

    const ffmpegPath = this.getFfmpegPath();
    if (/ffmpeg(?:\.exe)?$/i.test(ffmpegPath)) {
      return ffmpegPath.replace(/ffmpeg(?:\.exe)?$/i, (match) =>
        match.toLowerCase().endsWith('.exe') ? 'ffprobe.exe' : 'ffprobe',
      );
    }

    return 'ffprobe';
  }

  async checkFfmpeg(): Promise<CommandProbeResult> {
    return this.checkCommand(this.getFfmpegPath());
  }

  async checkFfprobe(): Promise<CommandProbeResult> {
    return this.checkCommand(this.getFfprobePath());
  }

  async extractDurationSecondsFromBuffer(
    buffer: Buffer,
    options: { originalname?: string; mimetype?: string } = {},
  ): Promise<number | null> {
    if (!buffer.length) {
      return null;
    }

    const workingDir = await mkdtemp(join(tmpdir(), 'oudelaa-media-probe-'));
    const inputPath = join(
      workingDir,
      `input-${randomUUID()}${this.resolveInputExtension(options)}`,
    );

    try {
      await writeFile(inputPath, buffer);
      return await this.extractDurationSeconds(inputPath);
    } catch (error) {
      this.logger.warn(
        `Media duration extraction failed for "${options.originalname ?? 'upload'}": ${
          error instanceof Error ? error.message : 'unknown error'
        }`,
      );
      return null;
    } finally {
      await rm(workingDir, { recursive: true, force: true });
    }
  }

  async extractDurationSeconds(filePath: string): Promise<number | null> {
    const ffprobe = await this.checkFfprobe();
    if (!ffprobe.available) {
      this.logger.warn(
        `ffprobe is unavailable at "${ffprobe.path}"; media duration extraction skipped`,
      );
      return null;
    }

    try {
      const stdout = await this.runCommand(
        ffprobe.path,
        [
          '-v',
          'error',
          '-show_entries',
          'format=duration',
          '-of',
          'default=noprint_wrappers=1:nokey=1',
          filePath,
        ],
        5000,
      );
      const seconds = Number.parseFloat(stdout.trim());
      if (!Number.isFinite(seconds) || seconds <= 0) {
        return null;
      }
      return Math.round(seconds);
    } catch (error) {
      this.logger.warn(
        `ffprobe duration check failed: ${error instanceof Error ? error.message : 'unknown error'}`,
      );
      return null;
    }
  }

  private async checkCommand(command: string): Promise<CommandProbeResult> {
    const cached = this.commandProbeCache.get(command);
    if (cached) {
      return cached;
    }

    try {
      const stdout = await this.runCommand(command, ['-version'], 3000);
      const version = stdout.split(/\r?\n/)[0]?.trim() ?? '';
      const result = { path: command, available: true, version };
      this.commandProbeCache.set(command, result);
      return result;
    } catch (error) {
      const result = {
        path: command,
        available: false,
        version: '',
        error: error instanceof Error ? error.message : 'unknown command error',
      };
      this.commandProbeCache.set(command, result);
      return result;
    }
  }

  private resolveInputExtension(options: { originalname?: string; mimetype?: string }): string {
    const extension = extname(options.originalname ?? '').toLowerCase();
    if (extension) {
      return extension;
    }

    switch (options.mimetype) {
      case 'video/mp4':
        return '.mp4';
      case 'video/quicktime':
        return '.mov';
      case 'video/webm':
        return '.webm';
      case 'audio/mpeg':
        return '.mp3';
      case 'audio/mp4':
      case 'audio/x-m4a':
        return '.m4a';
      case 'audio/wav':
      case 'audio/x-wav':
        return '.wav';
      case 'audio/aac':
        return '.aac';
      case 'audio/ogg':
        return '.ogg';
      default:
        return '.media';
    }
  }

  private async runCommand(command: string, args: string[], timeoutMs: number): Promise<string> {
    return new Promise<string>((resolve, reject) => {
      const child = spawn(command, args, { windowsHide: true });
      let stdout = '';
      let stderr = '';
      const timeout = setTimeout(() => {
        child.kill('SIGKILL');
        reject(new Error(`${command} timed out after ${timeoutMs}ms`));
      }, timeoutMs);

      child.stdout.on('data', (chunk) => {
        stdout += chunk.toString();
      });

      child.stderr.on('data', (chunk) => {
        stderr += chunk.toString();
      });

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

      child.on('close', (code) => {
        clearTimeout(timeout);
        if (code === 0) {
          resolve(stdout);
          return;
        }

        reject(new Error(stderr.trim() || `${command} exited with code ${code ?? 'unknown'}`));
      });
    });
  }
}
