<?php
namespace EasyChat\Library\Attachment;

use EasyChat\Library\SafeRuntime;

 
class AudioInspector {
    const MAX_DURATION_SECONDS = 900;

    public function inspect(string $path, int $reported_duration = 0): array {
        if (!is_file($path)) {
            return ['success' => false, 'error' => 'audio_file_missing'];
        }

        $detected = $this->containerDuration($path);
        $duration = $detected > 0 ? $detected : max(0, $reported_duration);
        if ($duration <= 0) {
            return ['success' => false, 'error' => 'audio_duration_unknown'];
        }
        if ($duration > self::MAX_DURATION_SECONDS) {
            return ['success' => false, 'error' => 'audio_duration_exceeded', 'duration' => $duration];
        }

        return ['success' => true, 'duration' => $duration];
    }

    private function containerDuration(string $path): int {
        $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        if (in_array($extension, ['ogg', 'oga', 'opus'], true)) {
            $duration = $this->oggDuration($path);
            if ($duration > 0) {
                return $duration;
            }
        } elseif ($extension === 'wav') {
            $duration = $this->wavDuration($path);
            if ($duration > 0) {
                return $duration;
            }
        } elseif ($extension === 'm4a') {
            $duration = $this->m4aDuration($path);
            if ($duration > 0) {
                return $duration;
            }
        }

        return $this->ffprobeDuration($path);
    }

    private function oggDuration(string $path): int {
        $size = (int)filesize($path);
        $handle = @fopen($path, 'rb');
        if ($handle === false || $size < 32) {
            if (is_resource($handle)) fclose($handle);
            return 0;
        }
        $read = min($size, 128 * 1024);
        fseek($handle, $size - $read);
        $tail = (string)fread($handle, $read);
        fclose($handle);
        $position = strrpos($tail, 'OggS');
        if ($position === false || strlen($tail) < $position + 14) {
            return 0;
        }
        $parts = unpack('Vlow/Vhigh', substr($tail, $position + 6, 8));
        if (!is_array($parts)) {
            return 0;
        }
        $granule = (float)$parts['low'] + ((float)$parts['high'] * 4294967296.0);
        return $granule > 0 ? (int)ceil($granule / 48000.0) : 0;
    }

    private function wavDuration(string $path): int {
        $handle = @fopen($path, 'rb');
        if ($handle === false) {
            return 0;
        }
        $head = (string)fread($handle, 44);
        fclose($handle);
        if (strlen($head) < 44 || substr($head, 0, 4) !== 'RIFF' || substr($head, 8, 4) !== 'WAVE') {
            return 0;
        }
        $byte_rate = unpack('Vrate', substr($head, 28, 4));
        $rate = (int)($byte_rate['rate'] ?? 0);
        return $rate > 0 ? (int)ceil(max(0, filesize($path) - 44) / $rate) : 0;
    }

    



    private function m4aDuration(string $path): int {
        $handle = @fopen($path, 'rb');
        $file_size = (int)SafeRuntime::call(function () use ($path) {
            return filesize($path);
        }, 0);
        if ($handle === false || $file_size < 24) {
            if (is_resource($handle)) fclose($handle);
            return 0;
        }

        $moov = $this->findIsoBox($handle, 0, $file_size, 'moov');
        if (!$moov) {
            fclose($handle);
            return 0;
        }
        $mvhd = $this->findIsoBox($handle, $moov['payload_start'], $moov['end'], 'mvhd');
        if (!$mvhd || fseek($handle, $mvhd['payload_start']) !== 0) {
            fclose($handle);
            return 0;
        }

        $head = (string)fread($handle, min(40, $mvhd['end'] - $mvhd['payload_start']));
        fclose($handle);
        if (strlen($head) < 20) return 0;

        $version = ord($head[0]);
        if ($version === 1) {
            if (strlen($head) < 32) return 0;
            $timescale = $this->uint32be(substr($head, 20, 4));
            $duration = $this->uint64be(substr($head, 24, 8));
        } else {
            $timescale = $this->uint32be(substr($head, 12, 4));
            $duration = $this->uint32be(substr($head, 16, 4));
        }
        return $timescale > 0 && $duration > 0 ? (int)ceil($duration / $timescale) : 0;
    }

     
    private function findIsoBox($handle, int $start, int $end, string $wanted): array {
        $offset = $start;
        while ($offset + 8 <= $end) {
            if (fseek($handle, $offset) !== 0) return [];
            $header = (string)fread($handle, 16);
            if (strlen($header) < 8) return [];
            $size = $this->uint32be(substr($header, 0, 4));
            $type = substr($header, 4, 4);
            $header_size = 8;
            if ($size === 1) {
                if (strlen($header) < 16) return [];
                $size = $this->uint64be(substr($header, 8, 8));
                $header_size = 16;
            } elseif ($size === 0) {
                $size = $end - $offset;
            }
            if ($size < $header_size || $offset + $size > $end) return [];
            if ($type === $wanted) {
                return ['payload_start' => $offset + $header_size, 'end' => $offset + $size];
            }
            $offset += $size;
        }
        return [];
    }

    private function uint32be(string $bytes): int {
        $parts = strlen($bytes) === 4 ? unpack('Nvalue', $bytes) : false;
        return is_array($parts) ? (int)$parts['value'] : 0;
    }

    private function uint64be(string $bytes): float {
        if (strlen($bytes) !== 8) return 0.0;
        $parts = unpack('Nhigh/Nlow', $bytes);
        return is_array($parts)
            ? ((float)$parts['high'] * 4294967296.0) + (float)$parts['low']
            : 0.0;
    }

    private function ffprobeDuration(string $path): int {
        if (!function_exists('proc_open')) {
            return 0;
        }
        $disabled = array_map('trim', explode(',', (string)ini_get('disable_functions')));
        if (in_array('proc_open', $disabled, true)) {
            return 0;
        }

        $command = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', $path];
        $pipes = [];
        $process = @proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
        if (!is_resource($process)) {
            return 0;
        }
        $stdout = isset($pipes[1]) ? (string)stream_get_contents($pipes[1]) : '';
        $stderr = isset($pipes[2]) ? (string)stream_get_contents($pipes[2]) : '';
        foreach ($pipes as $pipe) {
            if (is_resource($pipe)) fclose($pipe);
        }
        $exit = proc_close($process);
        if ($exit !== 0 || $stderr !== '' || !is_numeric(trim($stdout))) {
            return 0;
        }
        return (int)ceil((float)trim($stdout));
    }
}
