use std::ffi::OsStr;
use std::io;
use std::path::Path;
use std::time::Duration;

use reqwest::{StatusCode, multipart};
use serde::{Deserialize, Serialize};
use tokio_util::codec::{BytesCodec, FramedRead};

use crate::AppResult;
use crate::cli::{Level, ParseMode, long_message_caption};
use crate::config::Config;
use crate::temp::write_temp_text_file;

const MAX_MESSAGE_LENGTH: usize = 3500;
const MAX_PROMPT_DELAY_SECS: u64 = 15;
const MAX_FILE_SIZE_BYTES: u64 = 50 * 1024 * 1024;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct NotifyBody {
    message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    level: Option<Level>,
    #[serde(skip_serializing_if = "Option::is_none")]
    parse_mode: Option<ParseMode>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct PromptBody {
    message: String,
    buttons: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    parse_mode: Option<ParseMode>,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PromptResponse {
    prompt_id: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PollResponse {
    status: String,
    response: Option<String>,
}

#[derive(Deserialize)]
struct ErrorResponse {
    error: Option<String>,
}

fn client(config: &Config) -> reqwest::Client {
    if config.debug {
        eprintln!("[DEBUG] Base URL: {}", config.base_url);
    }
    reqwest::Client::new()
}

pub(crate) async fn send_notification(
    config: &Config,
    message: String,
    title: Option<String>,
    level: Option<Level>,
    parse_mode: Option<ParseMode>,
) -> AppResult<()> {
    let url = format!("{}/v1/notify", config.base_url);
    let body = NotifyBody {
        message,
        title,
        level,
        parse_mode,
    };

    if config.debug {
        eprintln!("[DEBUG] URL: {url}");
        eprintln!("[DEBUG] Payload: {}", serde_json::to_string(&body)?);
    }

    let response = client(config)
        .post(url)
        .bearer_auth(&config.token)
        .json(&body)
        .send()
        .await?;

    handle_send_response(response, config.debug).await
}

pub(crate) async fn send_file(
    config: &Config,
    path: &Path,
    caption: Option<String>,
    parse_mode: Option<ParseMode>,
) -> AppResult<()> {
    let url = format!("{}/v1/file", config.base_url);
    let part = build_file_part(path).await?;
    let mut form = multipart::Form::new().part("file", part);

    if let Some(caption) = caption {
        form = form.text("caption", caption);
    }

    if let Some(parse_mode) = parse_mode {
        form = form.text("parseMode", parse_mode.wire_value().to_string());
    }

    if config.debug {
        eprintln!("[DEBUG] URL: {url}");
        eprintln!("[DEBUG] Multipart fields: file, caption?, parseMode?");
    }

    let response = client(config)
        .post(url)
        .bearer_auth(&config.token)
        .multipart(form)
        .send()
        .await?;

    handle_send_response(response, config.debug).await
}

async fn build_file_part(path: &Path) -> AppResult<multipart::Part> {
    let file = tokio::fs::File::open(path).await?;
    let size = file.metadata().await?.len();
    validate_file_size(size)?;

    let name = path.file_name().and_then(OsStr::to_str).unwrap_or("file");
    let stream = FramedRead::new(file, BytesCodec::new());
    let file_body = reqwest::Body::wrap_stream(stream);
    let part = multipart::Part::stream_with_length(file_body, size)
        .file_name(name.to_string())
        .mime_str("application/octet-stream")?;
    Ok(part)
}

fn validate_file_size(size: u64) -> AppResult<()> {
    if size > MAX_FILE_SIZE_BYTES {
        return Err(io::Error::other(format!(
            "File is too large to send ({}); maximum is {}",
            human_size(size),
            human_size(MAX_FILE_SIZE_BYTES)
        ))
        .into());
    }

    Ok(())
}

fn human_size(bytes: u64) -> String {
    const MIB: u64 = 1024 * 1024;

    if bytes >= MIB {
        return format!("{:.1} MiB", bytes as f64 / MIB as f64);
    }

    format!("{bytes} bytes")
}

async fn handle_send_response(response: reqwest::Response, debug: bool) -> AppResult<()> {
    let status = response.status();
    let body = response.text().await.unwrap_or_default();

    if debug {
        eprintln!("[DEBUG] HTTP Status: {status}");
        eprintln!("[DEBUG] Response: {body}");
    }

    if status.is_success() {
        eprintln!("Notification sent");
        return Ok(());
    }

    Err(io::Error::other(format_api_error(status, &body)).into())
}

fn format_api_error(status: StatusCode, body: &str) -> String {
    let api_error = serde_json::from_str::<ErrorResponse>(body)
        .ok()
        .and_then(|response| response.error)
        .filter(|message| !message.is_empty());

    let prefix = match status.as_u16() {
        400 => "Bad request",
        401 => "Invalid or missing token",
        403 => "Token has been revoked or service is disabled",
        429 => "Rate limit exceeded",
        502 => "Telegram API error",
        _ => return format!("Request failed with status {status}: {body}"),
    };

    match api_error {
        Some(message) => format!("{prefix}: {message}"),
        None => prefix.to_string(),
    }
}

pub(crate) async fn send_message_or_file(
    config: &Config,
    message: String,
    title: Option<String>,
    level: Option<Level>,
    parse_mode: Option<ParseMode>,
) -> AppResult<()> {
    if message.len() <= MAX_MESSAGE_LENGTH {
        return send_notification(config, message, title, level, parse_mode).await;
    }

    let temp = write_temp_text_file(&message)?;
    let caption = long_message_caption(title, level);
    send_file(config, temp.path(), caption, parse_mode).await
}

pub(crate) async fn send_prompt(
    config: &Config,
    message: String,
    buttons: Vec<String>,
    parse_mode: Option<ParseMode>,
    timeout: Duration,
) -> AppResult<()> {
    let url = format!("{}/v1/prompt", config.base_url);
    let body = PromptBody {
        message,
        buttons,
        parse_mode,
    };

    if config.debug {
        eprintln!("[DEBUG] URL: {url}");
        eprintln!("[DEBUG] Payload: {}", serde_json::to_string(&body)?);
    }

    let response = client(config)
        .post(url)
        .bearer_auth(&config.token)
        .json(&body)
        .send()
        .await?;

    let status = response.status();
    let text = response.text().await.unwrap_or_default();

    if config.debug {
        eprintln!("[DEBUG] HTTP Status: {status}");
        eprintln!("[DEBUG] Response: {text}");
    }

    if !status.is_success() {
        return Err(io::Error::other(format_api_error(status, &text)).into());
    }

    let prompt = serde_json::from_str::<PromptResponse>(&text)?;
    eprintln!("Waiting for response...");
    poll_prompt(config, &prompt.prompt_id, timeout).await
}

async fn poll_prompt(config: &Config, prompt_id: &str, timeout: Duration) -> AppResult<()> {
    let start = std::time::Instant::now();
    let mut delay = Duration::from_secs(1);

    loop {
        if start.elapsed() >= timeout {
            return Err(io::Error::other("Timeout waiting for response").into());
        }

        let url = format!("{}/v1/prompt/{prompt_id}", config.base_url);
        let response = client(config)
            .get(url)
            .bearer_auth(&config.token)
            .send()
            .await?;

        let status = response.status();
        let text = response.text().await.unwrap_or_default();

        if config.debug {
            eprintln!("[DEBUG] Poll Status: {status}");
            eprintln!("[DEBUG] Poll Response: {text}");
        }

        if !status.is_success() {
            return Err(io::Error::other(format_api_error(status, &text)).into());
        }

        let poll = serde_json::from_str::<PollResponse>(&text)?;
        match poll.status.as_str() {
            "answered" => {
                println!("{}", poll.response.unwrap_or_default());
                return Ok(());
            }
            "expired" => return Err(io::Error::other("Prompt expired").into()),
            "pending" => {
                tokio::time::sleep(delay).await;
                delay = std::cmp::min(delay * 2, Duration::from_secs(MAX_PROMPT_DELAY_SECS));
            }
            other => return Err(io::Error::other(format!("Unknown prompt status: {other}")).into()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn formats_api_errors() {
        assert_eq!(
            format_api_error(
                StatusCode::UNAUTHORIZED,
                r#"{"ok":false,"error":"Invalid token"}"#
            ),
            "Invalid or missing token: Invalid token"
        );
        assert_eq!(
            format_api_error(StatusCode::TOO_MANY_REQUESTS, ""),
            "Rate limit exceeded"
        );
    }

    #[test]
    fn rejects_files_over_upload_limit() {
        let error = validate_file_size(MAX_FILE_SIZE_BYTES + 1)
            .unwrap_err()
            .to_string();
        assert!(error.contains("File is too large"));
        assert!(error.contains("50.0 MiB"));
    }

    #[test]
    fn formats_file_sizes() {
        assert_eq!(human_size(12), "12 bytes");
        assert_eq!(human_size(1024 * 1024), "1.0 MiB");
    }
}
