<?php
/**
 * Niwa AI — Telegram Media Fetcher (Phase 3 / feature/telegram-and-authonomy).
 *
 * Wraps Telegram Bot API's getFile + binary download with retry, file-too-big
 * detection, idle-aware HTTP timeouts, and magic-bytes classification so the
 * rest of the agent never has to think about transient mrežne greške ili
 * format detection. Modeled on Openclaw's resolve-media pipeline but ported
 * to plain PHP — no grammY, no streaming primitives that PHP doesn't have.
 *
 * Telegram Bot API caps file downloads at 20 MB; that limit surfaces as a
 * permanent 400-class error and must NOT be retried — retrying just burns the
 * rate limit while never succeeding. Network glitches, on the other hand,
 * routinely fix themselves on the second attempt; before this class, a
 * single failed request meant the user's image was lost forever.
 *
 * @package Niwa_AI
 */

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

class Niwa_AI_Admin_Autonomy_Telegram_Media_Fetcher {

    const RETRY_ATTEMPTS    = 3;
    const RETRY_BASE_MS     = 1000;
    const RETRY_MAX_MS      = 4000;
    const RETRY_JITTER_PCT  = 20;
    const REQUEST_TIMEOUT   = 20;
    const MAX_BOT_API_BYTES = 20 * 1024 * 1024;

    /**
     * Decrypted Telegram bot token. Pass through `Telegram::get_settings()` so
     * the auto-decryption pipeline runs once and the fetcher never sees the
     * encrypted form.
     *
     * @var string
     */
    private $bot_token;

    public function __construct( $bot_token ) {
        $this->bot_token = trim( (string) $bot_token );
    }

    /**
     * Resolve a Telegram file_id into a downloadable URL, retrying transient
     * failures with exponential backoff + jitter. Permanent errors (file too
     * big) short-circuit on the first response.
     *
     * @param string $file_id Telegram file identifier.
     * @return array {
     *     success:    bool,
     *     file_url?:  string  Absolute URL on api.telegram.org/file/bot<TOKEN>.
     *     file_path?: string  Telegram-relative path (useful for cache keys).
     *     error?:     string  Human-readable error.
     *     permanent?: bool    True when the error will not change on retry.
     * }
     */
    public function resolve_file_path_with_retry( $file_id ) {
        $file_id = trim( (string) $file_id );
        if ( '' === $this->bot_token ) {
            return array(
                'success'   => false,
                'error'     => 'Bot token nedostaje za Telegram file resolve.',
                'permanent' => true,
            );
        }
        if ( '' === $file_id ) {
            return array(
                'success'   => false,
                'error'     => 'file_id nedostaje za Telegram file resolve.',
                'permanent' => true,
            );
        }

        $last_error = '';
        for ( $i = 0; $i < self::RETRY_ATTEMPTS; $i++ ) {
            $result = $this->do_get_file( $file_id );
            if ( ! empty( $result['success'] ) ) {
                return $result;
            }

            $last_error = isset( $result['error'] ) ? (string) $result['error'] : '';

            // Permanent: don't retry. Telegram surfaces "file is too big" as a
            // 400 with that exact phrase; future calls return the same.
            if ( $this->is_file_too_big_error( $last_error ) ) {
                $result['permanent'] = true;
                return $result;
            }

            // Last attempt: bail out without sleeping.
            if ( $i + 1 < self::RETRY_ATTEMPTS ) {
                $this->sleep_with_backoff( $i );
            }
        }

        return array(
            'success' => false,
            'error'   => 'Telegram getFile failed after ' . self::RETRY_ATTEMPTS . ' attempts: ' . $last_error,
        );
    }

    /**
     * Full inbound-media ingestion: getFile (with retry) → binary download
     * (with retry) → magic-bytes content-type classification.
     *
     * Phase 5 will call this from build_message_context() in place of the
     * current direct resolve_file_url + ingest_from_url chain.
     *
     * @param array $ref MediaRef array as produced by Telegram::collect_inbound_media_refs().
     * @return array {
     *     success:       bool,
     *     path?:         string  Local temp file path (caller is responsible for cleanup).
     *     content_type?: string  Magic-bytes-detected MIME, falls back to ref.mime_hint.
     *     kind?:         string  Coarse routing kind: image|video|audio|document.
     *     error?:        string,
     *     permanent?:    bool
     * }
     */
    public function download_inbound_media( $ref ) {
        $ref = is_array( $ref ) ? $ref : array();

        $file_id = isset( $ref['file_id'] ) ? trim( (string) $ref['file_id'] ) : '';
        if ( '' === $file_id ) {
            return array(
                'success'   => false,
                'error'     => 'MediaRef nema file_id.',
                'permanent' => true,
            );
        }

        $resolved = $this->resolve_file_path_with_retry( $file_id );
        if ( empty( $resolved['success'] ) || empty( $resolved['file_url'] ) ) {
            return array(
                'success'   => false,
                'error'     => isset( $resolved['error'] ) ? (string) $resolved['error'] : 'getFile resolve failed.',
                'permanent' => ! empty( $resolved['permanent'] ),
            );
        }

        if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Image_Utils' ) ) {
            return array(
                'success' => false,
                'error'   => 'Image_Utils klasa nije ucitana.',
            );
        }

        $tmp = Niwa_AI_Admin_Autonomy_Image_Utils::download_to_temp_with_retry( $resolved['file_url'] );
        if ( is_wp_error( $tmp ) ) {
            return array(
                'success' => false,
                'error'   => 'download failed: ' . $tmp->get_error_message(),
            );
        }

        $detected_mime = (string) Niwa_AI_Admin_Autonomy_Image_Utils::detect_mime_type( $tmp );
        $kind          = $this->classify_kind( $detected_mime, $ref );

        return array(
            'success'      => true,
            'path'         => $tmp,
            'content_type' => '' !== $detected_mime ? $detected_mime : ( isset( $ref['mime_hint'] ) ? (string) $ref['mime_hint'] : '' ),
            'kind'         => $kind,
        );
    }

    /**
     * Single-shot Telegram getFile call. Returns the resolved file URL on
     * success or a structured error on failure. Public ONLY for tests; the
     * production path is resolve_file_path_with_retry().
     *
     * @param string $file_id
     * @return array
     */
    public function do_get_file( $file_id ) {
        $url = 'https://api.telegram.org/bot' . rawurlencode( $this->bot_token ) . '/getFile';

        $response = wp_remote_post(
            $url,
            array(
                'timeout' => self::REQUEST_TIMEOUT,
                'body'    => array(
                    'file_id' => (string) $file_id,
                ),
            )
        );

        if ( is_wp_error( $response ) ) {
            return array(
                'success' => false,
                'error'   => $response->get_error_message(),
            );
        }

        $status = intval( wp_remote_retrieve_response_code( $response ) );
        $body   = (string) wp_remote_retrieve_body( $response );
        $data   = json_decode( $body, true );

        if ( ! is_array( $data ) ) {
            return array(
                'success' => false,
                'error'   => 'Telegram getFile returned invalid JSON (HTTP ' . $status . ').',
            );
        }

        if ( empty( $data['ok'] ) || empty( $data['result']['file_path'] ) ) {
            return array(
                'success' => false,
                'error'   => isset( $data['description'] ) ? (string) $data['description'] : 'Telegram getFile failed.',
            );
        }

        $file_path = (string) $data['result']['file_path'];
        return array(
            'success'   => true,
            'file_url'  => 'https://api.telegram.org/file/bot' . $this->bot_token . '/' . ltrim( $file_path, '/' ),
            'file_path' => $file_path,
        );
    }

    public function is_file_too_big_error( $message ) {
        $needle = 'file is too big';
        return false !== stripos( (string) $message, $needle );
    }

    /**
     * Backoff sleep between retry attempts. Default schedule: ~1s, ~2s, ~4s
     * (capped at RETRY_MAX_MS) with ±RETRY_JITTER_PCT jitter so concurrent
     * pollers don't synchronize their retries.
     *
     * Tests neutralize the actual sleep by hooking the
     * `niwa_ai_telegram_retry_sleep_us` filter to return 0.
     *
     * @param int $attempt Zero-based attempt index that just failed.
     */
    private function sleep_with_backoff( $attempt ) {
        $base   = self::RETRY_BASE_MS * pow( 2, max( 0, intval( $attempt ) ) );
        $base   = (float) min( self::RETRY_MAX_MS, $base );
        $jitter = ( mt_rand( -self::RETRY_JITTER_PCT, self::RETRY_JITTER_PCT ) / 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 );
        }
    }

    /**
     * Coarse classification used by Phase 5 routing. Magic-bytes MIME wins;
     * if magic-bytes detection failed (rare edge case for very small files),
     * we fall back to the ref's mime_hint, then to the ref's already-set kind.
     */
    private function classify_kind( $detected_mime, $ref ) {
        $mime = strtolower( (string) $detected_mime );
        if ( '' === $mime ) {
            $mime = isset( $ref['mime_hint'] ) ? strtolower( (string) $ref['mime_hint'] ) : '';
        }

        if ( 0 === strpos( $mime, 'image/' ) ) {
            return 'image';
        }
        if ( 0 === strpos( $mime, 'video/' ) ) {
            return 'video';
        }
        if ( 0 === strpos( $mime, 'audio/' ) ) {
            return 'audio';
        }
        if ( in_array( $mime, array( 'application/pdf', 'application/zip', 'application/octet-stream' ), true ) ) {
            return 'document';
        }
        if ( 0 === strpos( $mime, 'application/' ) || 0 === strpos( $mime, 'text/' ) ) {
            return 'document';
        }

        return isset( $ref['kind'] ) ? (string) $ref['kind'] : 'document';
    }
}
