<?php
namespace EasyChat\Library\Voice;

use EasyChat\Library\Ai\AiReplyEligibilityGuard;
use EasyChat\Library\Ai\AiResponseTimingPolicy;
use EasyChat\Library\AndroidPushNotifier;
use EasyChat\Library\Attachment\AudioInspector;
use EasyChat\Library\AttachmentPolicy;
use EasyChat\Library\AttachmentStorage;
use EasyChat\Library\Messenger\ChannelCatalog;
use EasyChat\Library\Messenger\ProviderRegistry;
use EasyChat\Library\Messenger\SecretBox;
use EasyChat\Library\Queue\QueueManager;
use EasyChat\Library\SafeRuntime;
use EasyChat\Model\Message;
use EasyChat\Model\Messenger;
use EasyChat\Model\Session;
use EasyChat\Model\Setting;

 
class VoiceMediaService {
    const MAX_BYTES = 20 * 1024 * 1024;
    const MAX_MB = 20;

    private $registry;
    private $db;
    private $prefix;
    private $settings;

    public function __construct($registry) {
        $this->registry = $registry;
        $this->db = $registry->get('db');
        $this->prefix = easychat_db_prefix();
        $this->settings = new Setting($registry);
    }

    public function placeholder(string $provider, int $channel_id, array $source): array {
        $duration = max(0, (int)($source['duration'] ?? 0));
        $size = max(0, (int)($source['file_size'] ?? 0));
        $rejection = '';
        if ($size > self::MAX_BYTES) {
            $rejection = 'audio_size_exceeded';
        } elseif ($duration > AudioInspector::MAX_DURATION_SECONDS) {
            $rejection = 'audio_duration_exceeded';
        }

        $safe_source = $this->safeSource($provider, $channel_id, $source);
        $encoded = json_encode($safe_source, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $install_secret = (string)$this->settings->get('chat_install_secret', '');
        $encrypted = $encoded !== false ? SecretBox::encrypt($encoded, $install_secret) : '';
        $display_name = $this->displayName((string)($source['file_name'] ?? ''), $provider);
        $hash = hash('sha256', (string)($source['payload_hash'] ?? '') . '|' . (string)($source['telegram_file_id'] ?? '') . '|' . (string)($source['url'] ?? ''));

        return [
            'success' => true,
            'kind' => 'audio',
            'filename' => 'voice_pending_' . substr($hash, 0, 20) . '.pending',
            'mime' => (string)($source['mime_type'] ?? 'application/octet-stream'),
            'size' => $size,
            'display_name' => $display_name,
            'original_name' => $display_name,
            'voice' => [
                'media_status' => $rejection === '' ? 'queued' : 'rejected',
                'duration' => $duration,
                'waveform' => $this->waveform((string)($source['waveform'] ?? ''), $hash),
                'source' => $encrypted,
                'transcription' => [
                    'status' => $rejection === '' ? 'waiting_media' : 'error',
                    'text' => '',
                    'language' => '',
                    'error_code' => $rejection,
                    'updated_at' => gmdate('c'),
                ],
            ],
            'external_payload_hash' => (string)($source['payload_hash'] ?? ''),
        ];
    }

    public function enqueueFetch(int $attachment_id): int {
        if ($attachment_id <= 0) return 0;
        return (new QueueManager($this->registry))->pushUnique(
            'voice_media_fetch',
            ['attachment_id' => $attachment_id],
            'voice_media_fetch:' . $attachment_id,
            0,
            8
        );
    }

    public function fetch(int $attachment_id): void {
        $attachment = $this->attachment($attachment_id);
        $meta = $this->meta($attachment);
        $voice = is_array($meta['voice'] ?? null) ? $meta['voice'] : [];
        if (!$attachment || ($meta['kind'] ?? '') !== 'audio' || ($voice['media_status'] ?? '') === 'rejected') return;
        if (($voice['media_status'] ?? '') === 'ready' && is_file($this->filePath($attachment))) {
            $this->enqueueTranscription($attachment_id);
            return;
        }

        $source = $this->openSource((string)($voice['source'] ?? ''));
        if (!$source) {
            $this->failState($attachment, $meta, 'media_source_invalid', 'media');
            throw new \RuntimeException('voice_media_source_invalid');
        }
        $channel = (new Messenger($this->registry))->getChannelById((int)($source['channel_id'] ?? 0));
        if (!$channel || (string)($channel['provider'] ?? '') !== (string)($source['provider'] ?? '')) {
            $this->failState($attachment, $meta, 'channel_unavailable', 'media');
            throw new \RuntimeException('voice_channel_unavailable');
        }

        $provider = (string)$source['provider'];
        $credentials = json_decode((string)($channel['credentials_json'] ?? ''), true);
        $credentials = is_array($credentials) ? $credentials : [];
        $token = SecretBox::decrypt((string)($credentials['bot_token'] ?? ''), (string)$this->settings->get('chat_install_secret', ''));
        $url = trim((string)($source['url'] ?? ''));
        if ($provider === 'telegram' && $url === '' && !empty($source['telegram_file_id'])) {
            $adapter = ProviderRegistry::get($provider, $this->registry);
            $url = $adapter && method_exists($adapter, 'getFileUrl')
                ? (string)$adapter->getFileUrl($token, (string)$source['telegram_file_id'])
                : '';
        }
        if ($url === '') {
            $this->failState($attachment, $meta, 'media_not_ready', 'media');
            throw new \RuntimeException('voice_media_not_ready');
        }

        $headers = [];
        $host = strtolower((string)(parse_url($url, PHP_URL_HOST) ?: ''));
        if ($provider === 'max-bot' && $token !== '' && ($host === 'max.ru' || substr($host, -7) === '.max.ru')) {
            $headers[] = 'Authorization: ' . preg_replace('/^Bearer\s+/i', '', trim($token));
        }
        $allowed_hosts = $provider === 'telegram' ? ['api.telegram.org'] : [];
        $storage = new AttachmentStorage($this->registry);
        $result = $storage->saveRemoteAttachment(
            (int)$attachment['chat_id'],
            $url,
            $headers,
            self::MAX_MB,
            $provider === 'telegram' ? 'telegram' : 'max',
            (string)($source['file_name'] ?? ''),
            $allowed_hosts,
            45
        );
        if (empty($result['success']) || ($result['kind'] ?? '') !== 'audio') {
            $code = (string)($result['error'] ?? 'media_download_failed');
            $this->failState($attachment, $meta, $code, 'media');
            throw new \RuntimeException('voice_media_fetch_failed:' . $this->safeCode($code));
        }

        $inspection = (new AudioInspector())->inspect((string)$result['path'], (int)($voice['duration'] ?? 0));
        if (empty($inspection['success'])) {
            SafeRuntime::unlink((string)$result['path']);
            $this->failState($attachment, $meta, (string)($inspection['error'] ?? 'audio_invalid'), 'media');
            return;
        }

        $voice['media_status'] = 'ready';
        $voice['duration'] = (int)$inspection['duration'];
        $voice['source'] = '';
        $voice['transcription'] = [
            'status' => $this->isOperational() ? 'queued' : 'disabled',
            'text' => '',
            'language' => '',
            'error_code' => $this->isOperational() ? '' : 'easytokens_required',
            'updated_at' => gmdate('c'),
        ];
        $meta['voice'] = $voice;
        $this->db->query(
            "UPDATE `{$this->prefix}message_attachments` SET "
            . "`file_name` = '" . $this->db->escape((string)$result['filename']) . "', "
            . "`mime_type` = '" . $this->db->escape((string)$result['mime']) . "', "
            . "`file_size` = " . (int)$result['size'] . ", "
            . "`meta_json` = '" . $this->db->escape(json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . "' "
            . "WHERE `id` = " . $attachment_id
        );
        $this->touch((int)$attachment['message_id'], (int)$attachment['chat_id']);
        if ($this->isOperational()) $this->enqueueTranscription($attachment_id);
    }

    public function enqueueTranscription(int $attachment_id, int $delay = 0): int {
        if ($attachment_id <= 0 || !$this->isOperational()) return 0;
        return (new QueueManager($this->registry))->pushUnique(
            'voice_transcribe',
            ['attachment_id' => $attachment_id],
            'voice_transcribe:' . $attachment_id,
            max(0, $delay),
            6
        );
    }

    public function transcribe(int $attachment_id, ?EasyTokensAudioClient $client = null): void {
        $attachment = $this->attachment($attachment_id);
        $meta = $this->meta($attachment);
        $voice = is_array($meta['voice'] ?? null) ? $meta['voice'] : [];
        if (!$attachment || ($meta['kind'] ?? '') !== 'audio' || ($voice['media_status'] ?? '') !== 'ready') return;
        $current = is_array($voice['transcription'] ?? null) ? $voice['transcription'] : [];
        if (($current['status'] ?? '') === 'ready') return;
        if (!$this->isOperational()) {
            $this->failState($attachment, $meta, 'easytokens_required', 'transcription', 'disabled');
            return;
        }

        $path = $this->filePath($attachment);
        $validation = AttachmentPolicy::validateAudioFile($path, (string)$attachment['file_name'], (int)$attachment['file_size'], self::MAX_MB);
        if (empty($validation['success'])) {
            $this->failState($attachment, $meta, (string)($validation['error'] ?? 'audio_invalid'), 'transcription');
            return;
        }
        $inspection = (new AudioInspector())->inspect($path, (int)($voice['duration'] ?? 0));
        if (empty($inspection['success'])) {
            $this->failState($attachment, $meta, (string)($inspection['error'] ?? 'audio_invalid'), 'transcription');
            return;
        }

        $current['status'] = 'processing';
        $current['error_code'] = '';
        $current['updated_at'] = gmdate('c');
        $current['attempts'] = max(0, (int)($current['attempts'] ?? 0)) + 1;
        $voice['transcription'] = $current;
        $meta['voice'] = $voice;
        $this->saveMeta($attachment, $meta);

        $client = $client ?: new EasyTokensAudioClient($this->registry);
        $result = $client->transcribe(
            $path,
            (string)$validation['mime'],
            (string)($meta['display_name'] ?? $attachment['file_name']),
            (string)$this->settings->get('chat_ai_key', ''),
            'easychat-voice-' . $attachment_id . '-' . hash_file('sha256', $path)
        );
        if (empty($result['success'])) {
            $code = $this->safeCode((string)($result['error'] ?? 'transcription_failed'));
            $this->failState($attachment, $meta, $code, 'transcription');
            if (!empty($result['retryable'])) throw new \RuntimeException($code);
            return;
        }
        $reported_duration = (int)($result['duration'] ?? 0);
        if ($reported_duration > AudioInspector::MAX_DURATION_SECONDS) {
            $this->failState($attachment, $meta, 'audio_duration_exceeded', 'transcription');
            return;
        }

        $text = trim((string)($result['text'] ?? ''));
        $voice['duration'] = max((int)($voice['duration'] ?? 0), $reported_duration);
        $voice['transcription'] = [
            'status' => $text === '' ? 'empty' : 'ready',
            'text' => $text,
            'language' => (string)($result['language'] ?? ''),
            'error_code' => $text === '' ? 'speech_not_recognized' : '',
            'updated_at' => gmdate('c'),
            'attempts' => (int)($current['attempts'] ?? 1),
        ];
        $meta['voice'] = $voice;
        $this->saveMeta($attachment, $meta);
        if ($text !== '') $this->scheduleAi((int)$attachment['chat_id'], (int)$attachment['message_id']);
    }

    public function retry(int $attachment_id, int $chat_id): array {
        $attachment = $this->attachment($attachment_id);
        $meta = $this->meta($attachment);
        if (!$attachment || (int)$attachment['chat_id'] !== $chat_id || ($meta['kind'] ?? '') !== 'audio') {
            return ['success' => false, 'error' => 'voice_attachment_not_found'];
        }
        $voice = is_array($meta['voice'] ?? null) ? $meta['voice'] : [];
        if (($voice['media_status'] ?? '') === 'error' && trim((string)($voice['source'] ?? '')) !== '') {
            $voice['media_status'] = 'queued';
            $voice['transcription'] = array_merge((array)($voice['transcription'] ?? []), [
                'status' => 'waiting_media', 'text' => '', 'error_code' => '', 'updated_at' => gmdate('c'),
            ]);
            $meta['voice'] = $voice;
            $this->saveMeta($attachment, $meta);
            return ['success' => true, 'job_id' => $this->enqueueFetch($attachment_id), 'stage' => 'media'];
        }
        if (($voice['media_status'] ?? '') !== 'ready') {
            return ['success' => false, 'error' => 'voice_media_not_ready'];
        }
        if (!$this->isOperational()) {
            return ['success' => false, 'error' => 'easytokens_required'];
        }
        $transcription_status = (string)($voice['transcription']['status'] ?? 'disabled');
        if ($transcription_status === 'ready') {
             
             
            return ['success' => true, 'job_id' => 0, 'stage' => 'ready'];
        }
        if (in_array($transcription_status, ['queued', 'processing', 'waiting_media'], true)) {
            return ['success' => true, 'job_id' => 0, 'stage' => 'transcription'];
        }
        if (!in_array($transcription_status, ['error', 'empty', 'disabled'], true)) {
            return ['success' => false, 'error' => 'voice_retry_not_allowed'];
        }
        $voice['transcription'] = array_merge((array)($voice['transcription'] ?? []), [
            'status' => 'queued', 'text' => '', 'error_code' => '', 'updated_at' => gmdate('c'),
        ]);
        $meta['voice'] = $voice;
        $this->saveMeta($attachment, $meta);
        return ['success' => true, 'job_id' => $this->enqueueTranscription($attachment_id)];
    }

    public function isOperational(): bool {
        return $this->settings->get('chat_voice_transcription_status', '1') === '1'
            && $this->settings->get('chat_ai_provider', 'easytokens') === 'easytokens'
            && trim((string)$this->settings->get('chat_ai_key', '')) !== '';
    }

    public function transcriptForMessage(int $message_id): string {
        if ($message_id <= 0) return '';
        $query = $this->db->query(
            "SELECT `meta_json` FROM `{$this->prefix}message_attachments` WHERE `message_id` = " . $message_id . " ORDER BY `id` ASC"
        );
        $parts = [];
        foreach ((array)$query->rows as $row) {
            $meta = json_decode((string)($row['meta_json'] ?? ''), true);
            $transcription = is_array($meta['voice']['transcription'] ?? null) ? $meta['voice']['transcription'] : [];
            if (($transcription['status'] ?? '') === 'ready' && trim((string)($transcription['text'] ?? '')) !== '') {
                $parts[] = trim((string)$transcription['text']);
            }
        }
        return implode("\n", $parts);
    }

    private function scheduleAi(int $chat_id, int $message_id): void {
        if ($this->settings->get('chat_ai_status', '0') !== '1') return;
        $message = (new Message($this->registry))->getMessageById($message_id);
        if (!$message || (int)($message['author'] ?? 0) !== 1) return;
        $guard = new AiReplyEligibilityGuard($this->registry);
        if (empty($guard->checkChatId($chat_id)['allowed'])) return;
        $delay = (new AiResponseTimingPolicy($this->registry))->initialQueueDelaySeconds();
        (new QueueManager($this->registry))->pushUnique(
            'messenger_ai_tick',
            ['chat_id' => $chat_id, 'message_id' => $message_id, 'source_provider' => (string)($message['source_provider'] ?? '')],
            'voice_ai_tick:' . $message_id,
            $delay,
            2
        );
    }

    private function failState(array $attachment, array $meta, string $code, string $target, string $status = 'error'): void {
        $voice = is_array($meta['voice'] ?? null) ? $meta['voice'] : [];
        if ($target === 'media') {
            $voice['media_status'] = $status;
        }
        $transcription = is_array($voice['transcription'] ?? null) ? $voice['transcription'] : [];
        $transcription['status'] = $status;
        $transcription['text'] = '';
        $transcription['error_code'] = $this->safeCode($code);
        $transcription['updated_at'] = gmdate('c');
        $voice['transcription'] = $transcription;
        $meta['voice'] = $voice;
        $this->saveMeta($attachment, $meta);
    }

    private function saveMeta(array $attachment, array $meta): void {
        $this->db->query(
            "UPDATE `{$this->prefix}message_attachments` SET `meta_json` = '"
            . $this->db->escape(json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
            . "' WHERE `id` = " . (int)$attachment['id']
        );
        $this->touch((int)$attachment['message_id'], (int)$attachment['chat_id']);
    }

    private function touch(int $message_id, int $chat_id): void {
        $this->db->query("UPDATE `{$this->prefix}messages` SET `date_modified` = NOW() WHERE `message_id` = " . $message_id);
        (new Session($this->registry))->updateModifiedDate($chat_id);
        $message = (new Message($this->registry))->getMessageById($message_id);
        try {
            (new AndroidPushNotifier($this->registry))->notifySyncChat(
                $chat_id,
                (string)($message['source_provider'] ?? ''),
                '',
                ChannelCatalog::label((string)($message['source_provider'] ?? ''))
            );
        } catch (\Throwable $e) {
             
        }
    }

    private function attachment(int $attachment_id): array {
        if ($attachment_id <= 0) return [];
        $query = $this->db->query("SELECT * FROM `{$this->prefix}message_attachments` WHERE `id` = " . $attachment_id . " LIMIT 1");
        return $query->row ?: [];
    }

    private function meta(array $attachment): array {
        $meta = json_decode((string)($attachment['meta_json'] ?? ''), true);
        return is_array($meta) ? $meta : [];
    }

    private function filePath(array $attachment): string {
        return rtrim(DIR_IMAGE, DS) . DS . 'chat_id_' . (int)$attachment['chat_id'] . DS . basename((string)$attachment['file_name']);
    }

    private function openSource(string $encrypted): array {
        $json = SecretBox::decrypt($encrypted, (string)$this->settings->get('chat_install_secret', ''));
        $source = json_decode($json, true);
        return is_array($source) ? $source : [];
    }

    private function safeSource(string $provider, int $channel_id, array $source): array {
        $url = trim((string)($source['url'] ?? ''));
        if (strlen($url) > 4096 || ($url !== '' && strpos($url, 'https://') !== 0)) $url = '';
        return [
            'provider' => preg_replace('/[^a-z0-9_-]/i', '', strtolower($provider)),
            'channel_id' => max(0, $channel_id),
            'url' => $url,
            'telegram_file_id' => substr(preg_replace('/[^A-Za-z0-9_-]/', '', (string)($source['telegram_file_id'] ?? '')), 0, 256),
            'file_name' => $this->displayName((string)($source['file_name'] ?? ''), $provider),
        ];
    }

    private function displayName(string $name, string $provider): string {
        $name = basename(trim($name));
        if ($name === '') $name = ($provider === 'telegram' ? 'telegram-voice.ogg' : 'max-voice.m4a');
        $name = trim((string)preg_replace('/[\x00-\x1F\x7F]+/', '', $name));
        return function_exists('mb_substr') ? mb_substr($name, 0, 120, 'UTF-8') : substr($name, 0, 120);
    }

    private function waveform(string $provider_wave, string $seed): array {
        $bytes = $provider_wave !== '' ? hash('sha256', $provider_wave, true) : hash('sha256', $seed, true);
        $bars = [];
        for ($i = 0; $i < 36; $i++) $bars[] = 22 + (ord($bytes[$i % strlen($bytes)]) % 71);
        return $bars;
    }

    private function safeCode(string $code): string {
        $code = strtolower((string)preg_replace('/[^a-z0-9_-]/i', '_', $code));
        return substr(trim($code, '_'), 0, 64) ?: 'voice_failed';
    }
}
