use serde::Serialize;
use std::path::PathBuf;
use thiserror::Error;

/// Источник файлового вложения: локальный файл или уже полученный токен.
///
/// Используется в вариантах перечисления [`Attachment`], связанных с файлами
/// (изображения, видео, аудио, документы). Позволяет библиотеке автоматически
/// загрузить локальный файл при отправке сообщения или сразу использовать
/// существующий токен.
#[derive(Debug, Clone)]
pub enum AttachmentSource {
    /// Путь к локальному файлу, который будет прочитан и загружен в MAX.
    LocalFile(PathBuf),
    /// Готовый токен вложения, полученный ранее (например, при загрузке файла).
    Token(String),
}

/// Данные контакта для вложения `contact`.
///
/// Используется при отправке контакта через [`Attachment::Contact`].
/// Все поля необязательные, поля с `None` не сериализуются в JSON.
#[derive(Debug, Clone, Serialize)]
pub struct ContactData {
    /// Имя контакта.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Идентификатор контакта в MAX.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub contact_id: Option<i64>,
    /// VCF‑информация о контакте.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vcf_info: Option<String>,
    /// Телефон контакта в формате VCF.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vcf_phone: Option<String>,
}

/// Кнопка inline‑клавиатуры.
///
/// Может быть различных типов, определяемых полем `type`. Подробнее о типах кнопок
/// смотрите в документации API MAX.
#[derive(Debug, Clone, Serialize)]
pub struct InlineKeyboardButton {
    /// Тип кнопки: `"callback"`, `"link"`, `"request_geo_location"`, `"open_app"`, `"message"`,
    /// `"clipboard"`.
    pub r#type: String,
    /// Текст, отображаемый на кнопке.
    pub text: String,
    /// Полезная нагрузка для callback‑кнопок.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub payload: Option<String>,
    /// URL для кнопок‑ссылок.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    /// Флаг быстрой кнопки (обычно `true` для запросов контакта или геолокации).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub quick: Option<bool>,
    /// URL веб‑приложения для кнопок типа `"open_app"`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub web_app: Option<String>,
    /// Идентификатор контакта для кнопок `"open_app"`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub contact_id: Option<i64>,
    /// Полезная нагрузка для кнопок `"open_app"`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub app_payload: Option<String>,
    /// Текст сообщения, которое будет отправлено при нажатии на кнопку типа `"message"`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message_text: Option<String>,
}

/// Inline‑клавиатура (двумерный массив кнопок).
///
/// Каждый вложенный вектор представляет собой строку кнопок.
/// Используйте [`InlineKeyboardBuilder`] для удобного построения клавиатуры.
#[derive(Debug, Clone, Serialize)]
pub struct InlineKeyboard {
    /// Строки кнопок.
    pub buttons: Vec<Vec<InlineKeyboardButton>>,
}

/// Данные для вложения `share` (отсылка/предпросмотр ссылки).
///
/// Может содержать либо URL для генерации предпросмотра, либо готовый токен вложения.
#[derive(Debug, Clone, Serialize)]
pub struct ShareData {
    /// URL, на который нужно сделать отсылку.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    /// Токен уже подготовленного вложения‑ссылки.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token: Option<String>,
}

/// Тип вложения.
///
/// Представляет все возможные вложения, которые можно отправить с сообщением
/// в MAX. Используется при формировании запроса [`SendMessageParams`][crate::SendMessageParams].
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", content = "payload")]
pub enum Attachment {
    /// Изображение: локальный файл, токен или URL.
    #[serde(rename = "image")]
    Image {
        /// Источник изображения: локальный файл или готовый токен.
        /// Не сериализуется, используется только для загрузки локальных файлов.
        #[serde(skip)]
        source: AttachmentSource,
        /// Токен ранее загруженного изображения (если есть).
        token: Option<String>,
        /// Прямой URL изображения (если есть).
        url: Option<String>,
    },
    /// Видео: локальный файл, токен или URL.
    #[serde(rename = "video")]
    Video {
        /// Источник видео.
        #[serde(skip)]
        source: AttachmentSource,
        /// Токен вложения (заполняется автоматически после загрузки).
        token: Option<String>,
        /// Адрес готового видео.
        url: Option<String>,
        /// Желаемое имя файла. Используется при скачивании по URL для правильного определения MIME-типа.
        filename: Option<String>,
    },
    /// Аудио: локальный файл, токен или URL.
    #[serde(rename = "audio")]
    Audio {
        #[serde(skip)]
        source: AttachmentSource,
        /// Токен вложения (заполняется автоматически после загрузки).
        token: Option<String>,
        /// Адрес готового аудиофайла.
        url: Option<String>,
        /// Желаемое имя файла. Используется при скачивании по URL для правильного определения MIME-типа.
        filename: Option<String>,
    },
    /// Файл: локальный файл, токен или URL.
    #[serde(rename = "file")]
    File {
        #[serde(skip)]
        source: AttachmentSource,
        /// Токен вложения (заполняется автоматически после загрузки).
        token: Option<String>,
        /// Адрес готового файла.
        url: Option<String>,
        /// Желаемое имя файла. Используется при скачивании по URL для правильного определения MIME-типа.
        filename: Option<String>,
    },
    /// Стикер.
    #[serde(rename = "sticker")]
    Sticker {
        /// Код стикера (уникальный идентификатор в системе MAX).
        code: String,
    },
    /// Контакт.
    #[serde(rename = "contact")]
    Contact(ContactData),
    /// Inline‑клавиатура.
    #[serde(rename = "inline_keyboard")]
    InlineKeyboard(InlineKeyboard),
    /// Геолокация.
    #[serde(rename = "location")]
    Location {
        /// Широта.
        latitude: f64,
        /// Долгота.
        longitude: f64,
    },
    /// Поделиться ссылкой.
    #[serde(rename = "share")]
    Share(ShareData),
}

impl Attachment {
    /// Возвращает строковый тип вложения, как того требует API MAX.
    ///
    /// # Пример
    /// ```
    /// use maxbot::Attachment;
    ///
    /// let att = Attachment::sticker("sticker_code");
    /// assert_eq!(att.get_type(), "sticker");
    /// ```
    pub fn get_type(&self) -> &'static str {
        match self {
            Attachment::Image { .. }      => "image",
            Attachment::Video { .. }      => "video",
            Attachment::Audio { .. }      => "audio",
            Attachment::File { .. }       => "file",
            Attachment::Sticker { .. }    => "sticker",
            Attachment::Contact(_)        => "contact",
            Attachment::InlineKeyboard(_) => "inline_keyboard",
            Attachment::Location { .. }   => "location",
            Attachment::Share(_)          => "share",
        }
    }

    // ----- Конструкторы для файловых вложений -----

    /// Создаёт вложение‑изображение из локального файла.
    ///
    /// Файл будет загружен на сервер MAX при отправке сообщения.
    pub fn image_local(path: impl Into<PathBuf>) -> Self {
        Attachment::Image {
            source: AttachmentSource::LocalFile(path.into()),
            token: None,
            url: None,
        }
    }

    /// Создаёт вложение‑изображение по существующему токену.
    pub fn image_token(token: impl Into<String>) -> Self {
        Attachment::Image {
            source: AttachmentSource::Token(token.into()),
            token: None,
            url: None,
        }
    }

    /// Создаёт вложение‑изображение по URL.
    pub fn image_url(url: impl Into<String>) -> Self {
        Attachment::Image {
            source: AttachmentSource::Token(String::new()), // фиктивное значение, не используется
            token: None,
            url: Some(url.into()),
        }
    }

    /// Создаёт вложение‑видео из локального файла.
    pub fn video_local(path: impl Into<PathBuf>) -> Self {
        Attachment::Video {
            source: AttachmentSource::LocalFile(path.into()),
            token: None,
            url: None,
            filename: None,
        }
    }

    /// Создаёт вложение‑видео по существующему токену.
    pub fn video_token(token: impl Into<String>) -> Self {
        Attachment::Video {
            source: AttachmentSource::Token(token.into()),
            token: None,
            url: None,
            filename: None,
        }
    }

    /// Создаёт вложение‑видео по URL.
    pub fn video_url(url: impl Into<String>, filename: impl Into<String>) -> Self {
        Attachment::Video {
            source: AttachmentSource::Token(String::new()),
            token: None,
            url: Some(url.into()),
            filename: Some(filename.into()),
        }
    }

    /// Создаёт вложение‑аудио из локального файла.
    pub fn audio_local(path: impl Into<PathBuf>) -> Self {
        Attachment::Audio {
            source: AttachmentSource::LocalFile(path.into()),
            token: None,
            url: None,
            filename: None,
        }
    }

    /// Создаёт вложение‑аудио по существующему токену.
    pub fn audio_token(token: impl Into<String>) -> Self {
        Attachment::Audio {
            source: AttachmentSource::Token(token.into()),
            token: None,
            url: None,
            filename: None,
        }
    }

    /// Создаёт вложение‑аудио по URL.
    pub fn audio_url(url: impl Into<String>, filename: impl Into<String>) -> Self {
        Attachment::Audio {
            source: AttachmentSource::Token(String::new()),
            token: None,
            url: Some(url.into()),
            filename: Some(filename.into()),
        }
    }

    /// Создаёт вложение‑файл (документ) из локального файла.
    pub fn file_local(path: impl Into<PathBuf>) -> Self {
        Attachment::File {
            source: AttachmentSource::LocalFile(path.into()),
            token: None,
            url: None,
            filename: None,
        }
    }

    /// Создаёт вложение‑файл по существующему токену.
    pub fn file_token(token: impl Into<String>) -> Self {
        Attachment::File {
            source: AttachmentSource::Token(token.into()),
            token: None,
            url: None,
            filename: None,
        }
    }

    /// Создаёт вложение‑файл по прямому URL.
    pub fn file_url(url: impl Into<String>, filename: impl Into<String>) -> Self {
        Attachment::File {
            source: AttachmentSource::Token(String::new()),
            token: None,
            url: Some(url.into()),
            filename: Some(filename.into()),
        }
    }

    // ----- Конструкторы для других типов вложений -----

    /// Создаёт вложение‑стикер по его коду.
    pub fn sticker(code: impl Into<String>) -> Self {
        Attachment::Sticker { code: code.into() }
    }

    /// Создаёт вложение‑контакт с указанными данными.
    pub fn contact(data: ContactData) -> Self {
        Attachment::Contact(data)
    }

    /// Создаёт вложение‑inline‑клавиатуру.
    pub fn inline_keyboard(keyboard: InlineKeyboard) -> Self {
        Attachment::InlineKeyboard(keyboard)
    }

    /// Создаёт вложение‑геолокацию.
    pub fn location(latitude: f64, longitude: f64) -> Self {
        Attachment::Location { latitude, longitude }
    }

    /// Создаёт вложение, чтобы поделиться ссылкой.
    pub fn share(data: ShareData) -> Self {
        Attachment::Share(data)
    }
}

/// Ошибка проверки inline-клавиатуры.
///
/// Возвращается методом [`InlineKeyboardBuilder::build`], если клавиатура
/// не соответствует ограничениям MAX API.
#[derive(Debug, Error, PartialEq)]
pub enum KeyboardValidationError {
    /// Превышено максимальное количество строк (максимум 30).
    #[error("Too many rows: maximum allowed is 30, got {0}")]
    TooManyRows(usize),

    /// Превышено максимальное количество кнопок в строке.
    /// Для обычных кнопок (callback/message) максимум 7,
    /// для специальных (link/open_app/request_contact/request_geo_location) максимум 3.
    #[error("Too many buttons in row: maximum allowed is 7 (or 3 if row contains any special button type), got {0} in row {1}")]
    TooManyButtonsInRow(usize, usize),

    /// Превышено общее количество кнопок (максимум 210).
    #[error("Total number of buttons exceeds maximum allowed: maximum is 210, got {0}")]
    TotalButtonsExceeded(usize),
}

/// Построитель inline‑клавиатуры.
///
/// Позволяет удобно формировать двумерный массив кнопок.
///
/// # Пример
/// ```
/// use maxbot::{InlineKeyboardBuilder, InlineKeyboardButton};
///
/// let keyboard = InlineKeyboardBuilder::new()
///     .row(vec![
///         InlineKeyboardButton::callback("Кнопка 1", "payload1"),
///         InlineKeyboardButton::link("Сайт", "https://example.com"),
///     ])
///     .new_row()
///     .button(InlineKeyboardButton::request_contact("Отправить контакт"))
///     .build()
///     .unwrap();
/// ```
pub struct InlineKeyboardBuilder {
    rows: Vec<Vec<InlineKeyboardButton>>,
}

impl InlineKeyboardBuilder {
    /// Создаёт новый пустой построитель.
    pub fn new() -> Self {
        Self { rows: vec![] }
    }

    /// Добавляет строку кнопок.
    /// Если последняя существующая строка пуста, она заменяется новой.
    pub fn row(mut self, buttons: Vec<InlineKeyboardButton>) -> Self {
        if let Some(last) = self.rows.last() {
            if last.is_empty() {
                *self.rows.last_mut().unwrap() = buttons;
                return self;
            }
        }
        self.rows.push(buttons);
        self
    }

    /// Добавляет одну кнопку в последнюю строку.
    /// Если строк ещё нет, создаёт новую строку.
    pub fn button(mut self, btn: InlineKeyboardButton) -> Self {
        if self.rows.is_empty() {
            self.rows.push(vec![]);
        }
        self.rows.last_mut().unwrap().push(btn);
        self
    }

    /// Добавляет несколько кнопок в последнюю строку.
    pub fn buttons(mut self, btns: Vec<InlineKeyboardButton>) -> Self {
        if self.rows.is_empty() {
            self.rows.push(vec![]);
        }
        self.rows.last_mut().unwrap().extend(btns);
        self
    }

    /// Добавляет новую пустую строку, только если последняя строка не пуста (или строк ещё нет).
    /// Повторные вызовы без добавления кнопок не создают несколько пустых строк подряд.
    pub fn new_row(mut self) -> Self {
        if self.rows.is_empty() {
            self.rows.push(vec![]);
        } else {
            let last_row = self.rows.last().unwrap();
            if !last_row.is_empty() {
                self.rows.push(vec![]);
            }
        }
        self
    }

    /// Проверяет клавиатуру на ограничения MAX.
    fn validate(&self) -> Result<(), KeyboardValidationError> {
        let total_rows = self.rows.len();
        if total_rows > 30 {
            return Err(KeyboardValidationError::TooManyRows(total_rows));
        }

        let mut total_buttons = 0;

        for (row_idx, row) in self.rows.iter().enumerate() {
            let row_len = row.len();
            total_buttons += row_len;

            // Определяем, есть ли в строке хотя бы одна специальная кнопка
            let has_special = row.iter().any(|btn| {
                matches!(
                    btn.r#type.as_str(),
                    "link" | "open_app" | "request_contact" | "request_geo_location"
                )
            });

            let max_in_row = if has_special { 3 } else { 7 };
            if row_len > max_in_row {
                return Err(KeyboardValidationError::TooManyButtonsInRow(row_len, row_idx));
            }
        }

        if total_buttons > 210 {
            return Err(KeyboardValidationError::TotalButtonsExceeded(total_buttons));
        }

        Ok(())
    }

    /// Строит клавиатуру с проверкой ограничений.
    ///
    /// # Errors
    ///
    /// Возвращает [`KeyboardValidationError`], если клавиатура не соответствует
    /// ограничениям MAX (максимум 30 строк, 210 кнопок, до 7 обычных или 3 спец‑кнопок в строке.
    pub fn build(self) -> Result<InlineKeyboard, KeyboardValidationError> {
        self.validate()?;
        Ok(InlineKeyboard { buttons: self.rows })
    }

    /// Определяет максимальное количество кнопок в строке в зависимости от наличия специальных кнопок.
    fn max_buttons_in_row(row: &[InlineKeyboardButton]) -> usize {
        let has_special = row.iter().any(|btn| {
            matches!(
                btn.r#type.as_str(),
                "link" | "open_app" | "request_contact" | "request_geo_location"
            )
        });
        if has_special { 3 } else { 7 }
    }

    /// Определяет, является ли кнопка специальной (link, open_app, request_contact, request_geo_location).
    fn is_special_button(btn: &InlineKeyboardButton) -> bool {
        matches!(
            btn.r#type.as_str(),
            "link" | "open_app" | "request_contact" | "request_geo_location"
        )
    }

    /// Добавляет одну кнопку, автоматически создавая новую строку при необходимости.
    ///
    /// Кнопка добавляется в последнюю строку, если она ещё не заполнена:
    /// - Если в последней строке есть хотя бы одна специальная кнопка, максимальное количество — 3.
    /// - Иначе максимальное количество — 7.
    /// Если добавляемая кнопка является специальной, она добавляется только если в последней строке
    /// не более 2 кнопок (иначе создаётся новая строка).
    pub fn push_button(mut self, btn: InlineKeyboardButton) -> Self {
        let is_special = Self::is_special_button(&btn);
        
        // Если строк нет, создаём первую пустую строку
        if self.rows.is_empty() {
            self.rows.push(vec![]);
        }
        
        let last_row_idx = self.rows.len() - 1;
        let current_row = &self.rows[last_row_idx];
        let max_allowed = Self::max_buttons_in_row(current_row);
        
        // Проверяем, можно ли добавить кнопку в текущую строку
        let can_add = if is_special {
            // Специальную кнопку можно добавить только если в строке нет других специальных кнопок
            // (т.к. они все считаются специальными, и лимит 3) и текущее количество < 3
            current_row.len() < 3
        } else {
            // Обычную кнопку можно добавить, если не превышено ограничение 7
            current_row.len() < max_allowed
        };
        
        if can_add {
            // Добавляем в последнюю строку
            self.rows.last_mut().unwrap().push(btn);
        } else {
            // Создаём новую строку и добавляем туда
            self.rows.push(vec![btn]);
        }
        
        self
    }

    /// Добавляет несколько кнопок, автоматически распределяя их по строкам.
    pub fn push_buttons(mut self, btns: Vec<InlineKeyboardButton>) -> Self {
        for btn in btns {
            self = self.push_button(btn);
        }
        self
    }
}

impl Default for InlineKeyboardBuilder {
    fn default() -> Self {
        Self::new()
    }
}

// ----- Конструкторы кнопок -----

impl InlineKeyboardButton {
    /// Создаёт кнопку типа `"callback"`.
    ///
    /// При нажатии бот получит `callback_query` с указанным `payload`.
    pub fn callback(text: impl Into<String>, payload: impl Into<String>) -> Self {
        Self {
            r#type: "callback".to_string(),
            text: text.into(),
            payload: Some(payload.into()),
            url: None,
            quick: None,
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: None,
        }
    }

    /// Создаёт кнопку‑ссылку типа `"link"`.
    pub fn link(text: impl Into<String>, url: impl Into<String>) -> Self {
        let url = url.into();
        if url.len() > 2048 {
            #[cfg(debug_assertions)]
            eprintln!("Warning: link URL exceeds 2048 characters, may be rejected by MAX");
        }
        Self {
            r#type: "link".to_string(),
            text: text.into(),
            payload: None,
            url: Some(url),
            quick: None,
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: None,
        }
    }

    /// Создаёт кнопку запроса контакта у пользователя.
    pub fn request_contact(text: impl Into<String>) -> Self {
        Self {
            r#type: "request_contact".to_string(),
            text: text.into(),
            payload: None,
            url: None,
            quick: Some(true),
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: None,
        }
    }

    /// Создаёт кнопку запроса геолокации у пользователя.
    pub fn request_location(text: impl Into<String>) -> Self {
        Self {
            r#type: "request_geo_location".to_string(),
            text: text.into(),
            payload: None,
            url: None,
            quick: Some(true),
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: None,
        }
    }

    /// Создаёт кнопку открытия веб‑приложения (тип `"open_app"`).
    pub fn open_app(
        text: impl Into<String>,
        web_app_url: impl Into<String>,
        contact_id: i64,
        payload: Option<String>,
    ) -> Self {
        Self {
            r#type: "open_app".to_string(),
            text: text.into(),
            payload,
            url: None,
            quick: None,
            web_app: Some(web_app_url.into()),
            contact_id: Some(contact_id),
            app_payload: None,
            message_text: None,
        }
    }

    /// Создаёт кнопку отправки сообщения от имени пользователя (тип `"message"`).
    pub fn message(text: impl Into<String>, message_text: impl Into<String>) -> Self {
        Self {
            r#type: "message".to_string(),
            text: text.into(),
            payload: None,
            url: None,
            quick: None,
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: Some(message_text.into()),
        }
    }

    /// Создаёт кнопку типа `"clipboard"`.
    ///
    /// При нажатии текст `payload` копируется в буфер обмена пользователя.
    /// Эта кнопка не отправляет callback-запрос боту.
    pub fn clipboard(text: impl Into<String>, payload: impl Into<String>) -> Self {
        Self {
            r#type: "clipboard".to_string(),
            text: text.into(),
            payload: Some(payload.into()),
            url: None,
            quick: None,
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: None,
        }
    }
}
