use http::header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderValue};
use reqx::prelude::RetryPolicy;
use serde::Serialize;
use serde::de::DeserializeOwned;

use crate::client::RequestDefaults;
use crate::transport::{
    PreparedTelegramCall, TransportRequestConfig, TransportRetryMode, build_multipart_payload,
    build_multipart_payload_many, build_transport_client, multipart_header_values,
};
use crate::types::upload::{UploadFile, UploadPart};
use crate::{Error, Result};

pub(crate) struct BlockingTransport {
    client: reqx::blocking::Client,
}

impl BlockingTransport {
    pub(crate) fn new(
        base_url: String,
        defaults: &RequestDefaults,
        default_headers: &[(String, String)],
    ) -> Result<Self> {
        let client = build_transport_client(
            reqx::blocking::Client::builder(base_url),
            defaults,
            default_headers,
        )?;
        Ok(Self { client })
    }

    pub(crate) fn execute_json<P, R>(
        &self,
        method: &str,
        token: &str,
        payload: &P,
        config: TransportRequestConfig<'_>,
    ) -> Result<R>
    where
        P: Serialize + ?Sized,
        R: DeserializeOwned,
    {
        let call = PreparedTelegramCall::new(method, token)?;
        let body =
            serde_json::to_vec(payload).map_err(|source| Error::SerializeRequest { source })?;

        let request = self.configure_request(self.client.post(call.path()), config);
        let response = request
            .header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
            .body(body)
            .send()
            .map_err(|source| call.map_transport_error(source))?;

        call.parse_response(response, config.defaults())
    }

    pub(crate) fn execute_multipart<R>(
        &self,
        method: &str,
        token: &str,
        fields: &[(String, String)],
        file_field_name: &str,
        file: &UploadFile,
        config: TransportRequestConfig<'_>,
    ) -> Result<R>
    where
        R: DeserializeOwned,
    {
        let call = PreparedTelegramCall::new(method, token)?;
        let payload = build_multipart_payload(fields, file_field_name, file)?;
        let (content_type, content_length) = multipart_header_values(&payload)?;

        let request = self.configure_request(self.client.post(call.path()), config);
        let response = request
            .header(CONTENT_TYPE, content_type)
            .header(CONTENT_LENGTH, content_length)
            .body_reader(payload.into_reader())
            .send()
            .map_err(|source| call.map_transport_error(source))?;

        call.parse_response(response, config.defaults())
    }

    pub(crate) fn execute_multipart_files<R>(
        &self,
        method: &str,
        token: &str,
        fields: &[(String, String)],
        files: &[UploadPart],
        config: TransportRequestConfig<'_>,
    ) -> Result<R>
    where
        R: DeserializeOwned,
    {
        let call = PreparedTelegramCall::new(method, token)?;
        let files = files
            .iter()
            .map(|part| (part.field_name(), part.file()))
            .collect::<Vec<_>>();
        let payload = build_multipart_payload_many(fields, &files)?;
        let (content_type, content_length) = multipart_header_values(&payload)?;

        let request = self.configure_request(self.client.post(call.path()), config);
        let response = request
            .header(CONTENT_TYPE, content_type)
            .header(CONTENT_LENGTH, content_length)
            .body_reader(payload.into_reader())
            .send()
            .map_err(|source| call.map_transport_error(source))?;

        call.parse_response(response, config.defaults())
    }

    pub(crate) fn execute_empty<R>(
        &self,
        method: &str,
        token: &str,
        config: TransportRequestConfig<'_>,
    ) -> Result<R>
    where
        R: DeserializeOwned,
    {
        let call = PreparedTelegramCall::new(method, token)?;

        let response = self
            .configure_request(self.client.post(call.path()), config)
            .send()
            .map_err(|source| call.map_transport_error(source))?;

        call.parse_response(response, config.defaults())
    }

    fn configure_request<'a>(
        &self,
        mut request: reqx::blocking::RequestBuilder<'a>,
        config: TransportRequestConfig<'_>,
    ) -> reqx::blocking::RequestBuilder<'a> {
        let defaults = config.defaults();
        request = request
            .timeout(defaults.request_timeout)
            .max_response_body_bytes(defaults.max_response_body_bytes);

        if let Some(total_timeout) = defaults.total_timeout {
            request = request.total_timeout(total_timeout);
        }
        if config.retry_mode() == TransportRetryMode::Disabled {
            request = request.retry_policy(RetryPolicy::disabled());
        }

        request
    }
}
