<?php

/**
 * TelegramService - Handles all Telegram Bot API interactions
 *
 * Features:
 * - Send text messages with optional inline keyboard
 * - Send photos with caption
 * - Proper timeout and error handling
 */

declare(strict_types=1);

namespace Notifier\Services;

class TelegramService
{
    private string $botToken;
    private string $chatId;
    private int $connectTimeout;
    private int $timeout;

    private const API_BASE = 'https://api.telegram.org/bot';

    /**
     * @param string $botToken Telegram bot token
     * @param string $chatId Default chat ID for sends
     * @param int $timeout HTTP timeout in seconds (default 10)
     */
    public function __construct(string $botToken, string $chatId, int $timeout = 10)
    {
        $this->botToken = $botToken;
        $this->chatId = $chatId;
        $this->timeout = $timeout;
        $this->connectTimeout = 5; // Fixed 5s connect timeout
    }

    /**
     * Create instance from config array.
     *
     * @param array $config Config with 'bot_token', 'chat_id', optional 'timeout'
     */
    public static function fromConfig(array $config): self
    {
        return new self(
            $config['bot_token'],
            $config['chat_id'],
            $config['timeout'] ?? 10
        );
    }

    /**
     * Send a text message.
     *
     * @param string $text Message text (plain or MarkdownV2)
     * @param string|null $parseMode 'MarkdownV2', 'HTML', or null for plain text
     * @param array|null $inlineKeyboard Inline keyboard array, e.g. [[['text' => 'Open', 'url' => 'https://example.com']]]
     * @param string|null $chatId Override default chat ID
     * @return array ['success' => bool, 'message_id' => int|null, 'error' => string|null]
     */
    public function sendMessage(
        string $text,
        ?string $parseMode = null,
        ?array $inlineKeyboard = null,
        ?string $chatId = null
    ): array {
        $params = [
            'chat_id' => $chatId ?? $this->chatId,
            'text' => $text,
        ];

        if ($parseMode !== null) {
            $params['parse_mode'] = $parseMode;
        }

        if ($inlineKeyboard !== null) {
            $params['reply_markup'] = \json_encode([
                'inline_keyboard' => $inlineKeyboard,
            ]);
        }

        return $this->apiCall('sendMessage', $params);
    }

    /**
     * Send a photo with optional caption.
     *
     * @param string $photoPath Absolute path to the photo file
     * @param string|null $caption Caption text (plain or MarkdownV2)
     * @param string|null $parseMode 'MarkdownV2', 'HTML', or null
     * @param array|null $inlineKeyboard Inline keyboard array
     * @param string|null $chatId Override default chat ID
     * @return array ['success' => bool, 'message_id' => int|null, 'error' => string|null]
     */
    public function sendPhoto(
        string $photoPath,
        ?string $caption = null,
        ?string $parseMode = null,
        ?array $inlineKeyboard = null,
        ?string $chatId = null
    ): array {
        if (!\file_exists($photoPath)) {
            return [
                'success' => false,
                'message_id' => null,
                'error' => "Photo file not found: {$photoPath}",
            ];
        }

        $params = [
            'chat_id' => $chatId ?? $this->chatId,
            'photo' => new \CURLFile($photoPath),
        ];

        if ($caption !== null) {
            $params['caption'] = $caption;
        }

        if ($parseMode !== null) {
            $params['parse_mode'] = $parseMode;
        }

        if ($inlineKeyboard !== null) {
            $params['reply_markup'] = \json_encode([
                'inline_keyboard' => $inlineKeyboard,
            ]);
        }

        return $this->apiCall('sendPhoto', $params, true);
    }

    /**
     * Get bot info (useful for verification).
     *
     * @return array ['success' => bool, 'bot' => array|null, 'error' => string|null]
     */
    public function getMe(): array
    {
        $result = $this->apiCall('getMe', []);

        if ($result['success']) {
            return [
                'success' => true,
                'bot' => $result['data']['result'] ?? null,
                'error' => null,
            ];
        }

        return [
            'success' => false,
            'bot' => null,
            'error' => $result['error'],
        ];
    }

    /**
     * Escape text for MarkdownV2 format (escapes ALL special characters).
     *
     * Characters that need escaping: _ * [ ] ( ) ~ ` > # + - = | { } . !
     *
     * @param string $text Plain text to escape
     * @return string Escaped text safe for MarkdownV2
     */
    public static function escapeMarkdownV2(string $text): string
    {
        // Characters that need escaping in MarkdownV2
        $chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!', '\\'];

        // Escape backslash first to avoid double-escaping
        $text = \str_replace('\\', '\\\\', $text);

        // Escape all other special characters
        foreach ($chars as $char) {
            if ($char !== '\\') {
                $text = \str_replace($char, '\\' . $char, $text);
            }
        }

        return $text;
    }

    /**
     * Format text for MarkdownV2 while PRESERVING intentional formatting.
     *
     * Preserves: *bold*, _italic_, ~strikethrough~, `code`, ```code blocks```
     * Escapes: all other MarkdownV2 special characters
     *
     * @param string $text Text with optional markdown formatting
     * @return string Formatted text safe for MarkdownV2
     */
    public static function formatMarkdownV2(string $text): string
    {
        // Characters that need escaping (excluding formatting chars we want to preserve)
        $escapeChars = ['[', ']', '(', ')', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];

        // First, escape backslashes
        $text = \str_replace('\\', '\\\\', $text);

        // Escape characters that are not used for formatting
        foreach ($escapeChars as $char) {
            $text = \str_replace($char, '\\' . $char, $text);
        }

        // Now handle formatting characters carefully
        // We need to escape * _ ~ ` that are NOT part of valid formatting pairs

        // Process the text to preserve valid formatting while escaping orphan characters
        $result = self::processFormattingChars($text);

        return $result;
    }

    /**
     * Process formatting characters, preserving valid pairs and escaping orphans.
     *
     * @param string $text Text to process
     * @return string Processed text
     */
    private static function processFormattingChars(string $text): string
    {
        // Handle code blocks first (```...```) - they should not be processed inside
        $text = \preg_replace_callback('/```(.*?)```/s', function ($m) {
            // Inside code blocks, escape the content but preserve the backticks
            $inner = \str_replace(['*', '_', '~'], ['\\*', '\\_', '\\~'], $m[1]);
            return '```' . $inner . '```';
        }, $text);

        // Handle inline code (`...`) - escape content inside
        $text = \preg_replace_callback('/`([^`]+)`/', function ($m) {
            $inner = \str_replace(['*', '_', '~'], ['\\*', '\\_', '\\~'], $m[1]);
            return '`' . $inner . '`';
        }, $text);

        // For *, _, ~ we need to:
        // 1. Find valid pairs (non-greedy match with content between)
        // 2. Escape any unpaired/orphan occurrences

        // Bold: *text* (but not ** which would be empty)
        $text = self::preserveOrEscape($text, '*', '/\*([^\*\n]+)\*/');

        // Italic: _text_ (underscore)
        $text = self::preserveOrEscape($text, '_', '/_([^_\n]+)_/');

        // Strikethrough: ~text~
        $text = self::preserveOrEscape($text, '~', '/~([^~\n]+)~/');

        return $text;
    }

    /**
     * Preserve valid formatting pairs and escape orphan formatting characters.
     *
     * @param string $text Text to process
     * @param string $char The formatting character
     * @param string $pattern Regex pattern for valid pairs
     * @return string Processed text
     */
    private static function preserveOrEscape(string $text, string $char, string $pattern): string
    {
        // Use a placeholder for valid pairs
        $placeholder = "\x00PAIRPLACEHOLDER\x00";
        $pairs = [];

        // Extract valid pairs
        $text = \preg_replace_callback($pattern, function ($m) use (&$pairs, $placeholder, $char) {
            $idx = \count($pairs);
            // Store the full match including formatting chars
            $pairs[$idx] = $char . $m[1] . $char;
            return $placeholder . $idx . $placeholder;
        }, $text);

        // Escape remaining occurrences of the character
        $text = \str_replace($char, '\\' . $char, $text);

        // Restore valid pairs
        foreach ($pairs as $idx => $pair) {
            $text = \str_replace($placeholder . $idx . $placeholder, $pair, $text);
        }

        return $text;
    }

    /**
     * Create a confirmation link button.
     *
     * @param string $url Public confirmation URL
     * @param string $label Button label
     * @return array Inline keyboard array
     */
    public static function createConfirmLinkButton(string $url, string $label = '✅ Complete task'): array
    {
        return [[
            ['text' => $label, 'url' => $url],
        ]];
    }

    /**
     * Make an API call to Telegram.
     *
     * @param string $method API method name
     * @param array $params Parameters
     * @param bool $isMultipart Use multipart/form-data (for file uploads)
     * @param int|null $customTimeout Override default timeout
     * @return array ['success' => bool, 'data' => array|null, 'message_id' => int|null, 'error' => string|null]
     */
    private function apiCall(
        string $method,
        array $params,
        bool $isMultipart = false,
        ?int $customTimeout = null
    ): array {
        $url = self::API_BASE . $this->botToken . '/' . $method;

        $ch = \curl_init($url);

        $curlOptions = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
            CURLOPT_TIMEOUT => $customTimeout ?? $this->timeout,
        ];

        if (!empty($params)) {
            $curlOptions[CURLOPT_POST] = true;

            if ($isMultipart) {
                // For file uploads, pass array directly (curl handles multipart)
                $curlOptions[CURLOPT_POSTFIELDS] = $params;
            } else {
                // For regular requests, use URL-encoded
                $curlOptions[CURLOPT_POSTFIELDS] = \http_build_query($params);
            }
        }

        \curl_setopt_array($ch, $curlOptions);

        $response = \curl_exec($ch);
        $httpCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = \curl_error($ch);
        \curl_close($ch);

        // Handle curl errors
        if ($response === false) {
            return [
                'success' => false,
                'data' => null,
                'message_id' => null,
                'error' => "cURL error: {$curlError}",
            ];
        }

        // Parse JSON response
        $data = \json_decode($response, true);

        if ($data === null) {
            return [
                'success' => false,
                'data' => null,
                'message_id' => null,
                'error' => "Invalid JSON response: " . \substr($response, 0, 200),
            ];
        }

        // Check Telegram API response
        if (!($data['ok'] ?? false)) {
            $errorCode = $data['error_code'] ?? 'unknown';
            $description = $data['description'] ?? 'Unknown error';
            return [
                'success' => false,
                'data' => $data,
                'message_id' => null,
                'error' => "Telegram API error [{$errorCode}]: {$description}",
            ];
        }

        // Extract message_id if present (for sendMessage/sendPhoto)
        $messageId = $data['result']['message_id'] ?? null;

        return [
            'success' => true,
            'data' => $data,
            'message_id' => $messageId,
            'error' => null,
        ];
    }

    /**
     * Get the current chat ID.
     */
    public function getChatId(): string
    {
        return $this->chatId;
    }

    /**
     * Get the bot token (masked for logging).
     */
    public function getMaskedToken(): string
    {
        return \substr($this->botToken, 0, 10) . '...' . \substr($this->botToken, -4);
    }
}
