<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Audio utility helpers for Niwa AI Admin Autonomy.
 *
 * Handles downloading, format detection, and validation of audio files
 * for OpenAI Whisper transcription.
 */
class Niwa_AI_Admin_Autonomy_Audio_Utils {

	const WHISPER_MAX_FILE_SIZE = 25 * 1024 * 1024; // 25 MB

	const WHISPER_SUPPORTED_MIMES = array(
		'audio/ogg',
		'audio/mpeg',
		'audio/mp3',
		'audio/mp4',
		'audio/wav',
		'audio/webm',
		'audio/x-m4a',
		'audio/m4a',
		'audio/mpga',
		'audio/flac',
		'video/mp4',   // video_note — Whisper extracts audio track
		'video/webm',
	);

	public static function is_audio_media_ref( $ref ) {
		$ref  = is_array( $ref ) ? $ref : array();
		$kind = (string) ( $ref['kind'] ?? '' );
		if ( in_array( $kind, array( 'voice', 'audio' ), true ) ) {
			return true;
		}
		if ( 'document' !== $kind ) {
			return false;
		}

		$mime      = strtolower( trim( (string) ( $ref['mime_hint'] ?? '' ) ) );
		$extension = strtolower( pathinfo( (string) ( $ref['file_name'] ?? '' ), PATHINFO_EXTENSION ) );
		if ( 0 === strpos( $mime, 'audio/' ) || in_array( $mime, array( 'application/ogg', 'application/x-ogg' ), true ) ) {
			return true;
		}
		if ( 'application/mp4' === $mime ) {
			return 'm4a' === $extension;
		}
		if ( '' !== $mime && 'application/octet-stream' !== $mime ) {
			return false;
		}

		return in_array( $extension, array( 'flac', 'm4a', 'mp3', 'mpeg', 'mpga', 'oga', 'ogg', 'opus', 'wav' ), true );
	}

	/**
	 * Download through the shared WordPress HTTP trust boundary with a hard
	 * timeout, redirect validation, TLS verification, and Whisper byte cap.
	 *
	 * @param string $url           Audio URL (must already pass ssrf_check).
	 * @param int    $idle_seconds  Seconds the transfer may stay below 1 byte/sec.
	 * @param int    $max_seconds   Absolute hard cap for the entire transfer.
	 * @return string|WP_Error      Temp file path on success, WP_Error on failure.
	 */
	public static function fetch_with_idle_timeout( $url, $idle_seconds = 30, $max_seconds = 300 ) {
		$tmp = Niwa_AI_Admin_Autonomy_Temp_Files::create( 'niwa_audio_' );
		if ( ! $tmp ) {
			return new WP_Error( 'niwa_audio_tmpfile', 'Cannot create temp file.' );
		}
		$response = Niwa_AI_Safe_HTTP::download(
			$url,
			$tmp,
			array(
				'timeout'     => max( intval( $idle_seconds ) + 1, intval( $max_seconds ) ),
				'redirection' => 5,
			),
			self::WHISPER_MAX_FILE_SIZE
		);
		if ( is_wp_error( $response ) ) {
			@unlink( $tmp );
			return $response;
		}
		$status = intval( wp_remote_retrieve_response_code( $response ) );
		if ( $status < 200 || $status >= 300 ) {
			@unlink( $tmp );
			return new WP_Error( 'niwa_audio_http_error', 'HTTP ' . $status . ' downloading audio.' );
		}

		if ( ! is_file( $tmp ) || 0 === filesize( $tmp ) ) {
			@unlink( $tmp );
			return new WP_Error( 'niwa_audio_empty_body', 'Downloaded audio body is empty.' );
		}

		return $tmp;
	}

	/**
	 * Download an audio file from any URL (Telegram, public) to a local temp file.
	 *
	 * Pipeline:
	 *   1. SSRF guard (Image_Utils::ssrf_check shared logic).
	 *   2. Bounded WordPress HTTP streaming with per-hop redirect validation.
	 *
	 * @param string $url Audio file URL.
	 * @return string|WP_Error Temp file path on success, WP_Error on failure.
	 */
	public static function download_to_temp( $url ) {
		$url = esc_url_raw( trim( (string) $url ) );
		if ( '' === $url ) {
			return new WP_Error( 'niwa_audio_empty_url', 'Audio URL is empty.' );
		}

		// SSRF guard first — shares the Image_Utils trust list / filters.
		if ( class_exists( 'Niwa_AI_Admin_Autonomy_Image_Utils' ) ) {
			$ssrf = Niwa_AI_Admin_Autonomy_Image_Utils::ssrf_check( $url );
			if ( is_wp_error( $ssrf ) ) {
				return $ssrf;
			}
		}

		$idle = (int) apply_filters( 'niwa_ai_audio_download_idle_seconds', 30 );
		$max  = (int) apply_filters( 'niwa_ai_audio_download_max_seconds', 300 );
		return self::fetch_with_idle_timeout( $url, $idle, $max );
	}

	/**
	 * Phase 7 — retry-aware wrapper around download_to_temp().
	 *
	 * Mirrors Image_Utils::download_to_temp_with_retry with the same
	 * exponential-backoff schedule (1s/2s/4s capped) and the shared
	 * `niwa_ai_telegram_retry_sleep_us` filter so tests can disable the
	 * sleep without per-class plumbing.
	 *
	 * @param string $url      Audio URL (Telegram or public).
	 * @param int    $attempts Maximum attempts (default 3, floor 1).
	 * @return string|WP_Error Temp file path on success, last WP_Error otherwise.
	 */
	public static function download_to_temp_with_retry( $url, $attempts = 3 ) {
		$attempts   = max( 1, intval( $attempts ) );
		$last_error = null;

		for ( $i = 0; $i < $attempts; $i++ ) {
			$result = self::download_to_temp( $url );
			if ( ! is_wp_error( $result ) ) {
				return $result;
			}
			$last_error = $result;

			// SSRF errors are deterministic — retrying never helps.
			if ( 'niwa_ssrf' === $result->get_error_code() ) {
				return $result;
			}

			if ( $i + 1 < $attempts ) {
				$base   = 1000 * pow( 2, $i );
				$base   = (float) min( 4000, $base );
				$jitter = ( mt_rand( -20, 20 ) / 100.0 ) * $base;
				$delay  = max( 100, intval( $base + $jitter ) );

				$delay_us = (int) apply_filters( 'niwa_ai_telegram_retry_sleep_us', $delay * 1000, $delay );
				if ( $delay_us > 0 && function_exists( 'usleep' ) ) {
					usleep( $delay_us );
				}
			}
		}

		return $last_error;
	}

	/**
	 * Detect audio MIME type from file magic bytes.
	 *
	 * @param string $file_path Path to the file.
	 * @return string MIME type (e.g. 'audio/ogg') or '' if unknown.
	 */
	public static function detect_audio_mime_type( $file_path ) {
		if ( ! is_file( $file_path ) || ! is_readable( $file_path ) ) {
			return '';
		}

		// Try WP native first.
		$check = wp_check_filetype_and_ext( $file_path, basename( $file_path ) );
		if ( ! empty( $check['type'] ) ) {
			return (string) $check['type'];
		}

		// Try PHP built-in.
		if ( function_exists( 'mime_content_type' ) ) {
			$detected = mime_content_type( $file_path );
			if ( is_string( $detected ) && '' !== $detected ) {
				return $detected;
			}
		}

		// Fallback: magic bytes.
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		$fh = fopen( $file_path, 'rb' );
		if ( ! $fh ) {
			return '';
		}
		$header = fread( $fh, 16 );
		fclose( $fh );

		if ( false === $header || strlen( $header ) < 4 ) {
			return '';
		}

		// OGG: "OggS"
		if ( 'OggS' === substr( $header, 0, 4 ) ) {
			return 'audio/ogg';
		}
		if ( 'fLaC' === substr( $header, 0, 4 ) ) {
			return 'audio/flac';
		}
		// MP3 with ID3 tag: "ID3"
		if ( 'ID3' === substr( $header, 0, 3 ) ) {
			return 'audio/mpeg';
		}
		// MP3 sync word: FF FB, FF F3, FF F2
		if ( "\xFF" === $header[0] && ( "\xFB" === $header[1] || "\xF3" === $header[1] || "\xF2" === $header[1] ) ) {
			return 'audio/mpeg';
		}
		// WAV: "RIFF" ... "WAVE"
		if ( 'RIFF' === substr( $header, 0, 4 ) && strlen( $header ) >= 12 && 'WAVE' === substr( $header, 8, 4 ) ) {
			return 'audio/wav';
		}
		// MP4/M4A: bytes 4-8 = "ftyp"
		if ( strlen( $header ) >= 8 && 'ftyp' === substr( $header, 4, 4 ) ) {
			return 'audio/mp4';
		}
		// WebM: starts with EBML header 1A 45 DF A3
		if ( "\x1A\x45\xDF\xA3" === substr( $header, 0, 4 ) ) {
			return 'audio/webm';
		}

		return '';
	}

	/**
	 * Validate that the file is suitable for Whisper transcription.
	 *
	 * Checks: file exists, size <= 25MB, MIME is supported.
	 *
	 * @param string $file_path Local file path.
	 * @return true|WP_Error True if valid, WP_Error explaining the issue.
	 */
	public static function validate_for_transcription( $file_path ) {
		if ( ! is_file( $file_path ) || ! is_readable( $file_path ) ) {
			return new WP_Error( 'niwa_audio_not_found', 'Audio file not found or unreadable.' );
		}

		$size = filesize( $file_path );
		if ( false === $size || $size > self::WHISPER_MAX_FILE_SIZE ) {
			$mb = false !== $size ? round( $size / ( 1024 * 1024 ), 1 ) : '?';
			return new WP_Error(
				'niwa_audio_too_large',
				'Audio fajl je prevelik (' . $mb . ' MB). Maksimum za transkripciju je 25 MB.'
			);
		}

		if ( 0 === $size ) {
			return new WP_Error( 'niwa_audio_empty', 'Audio fajl je prazan (0 bajtova).' );
		}

		$mime = self::detect_audio_mime_type( $file_path );
		if ( '' !== $mime && ! in_array( $mime, self::WHISPER_SUPPORTED_MIMES, true ) ) {
			return new WP_Error(
				'niwa_audio_unsupported_format',
				'Nepodrzani audio format: ' . $mime . '. Podrzani: OGG, MP3, MP4, WAV, WebM, M4A, FLAC.'
			);
		}

		return true;
	}

	/**
	 * Map a MIME type to a file extension Whisper recognizes.
	 *
	 * @param string $mime MIME type.
	 * @return string Extension without dot (e.g. 'ogg', 'mp3').
	 */
	public static function extension_for_mime( $mime ) {
		$map = array(
			'audio/ogg'    => 'ogg',
			'audio/mpeg'   => 'mp3',
			'audio/mp3'    => 'mp3',
			'audio/mp4'    => 'mp4',
			'audio/m4a'    => 'm4a',
			'audio/x-m4a'  => 'm4a',
			'audio/mpga'   => 'mpga',
			'audio/flac'   => 'flac',
			'audio/wav'    => 'wav',
			'audio/webm'   => 'webm',
			'video/mp4'    => 'mp4',
			'video/webm'   => 'webm',
		);

		$mime = strtolower( trim( (string) $mime ) );
		return isset( $map[ $mime ] ) ? $map[ $mime ] : 'ogg';
	}
}
