Documentation
use chrono::Utc;
use reqwest::{
    multipart::{Form, Part},
    Body, Client, Error as ReqwestError,
};
use serde::{Deserialize, Serialize};
use serde_json::Error as SerdeJsonError;
use url::{ParseError as UrlParseError, Url};

//
pub const URL: &str = "https://open-api.tiktok.com/share/video/upload/";

//
pub async fn video_upload<T>(
    client: &Client,
    open_id: impl AsRef<str>,
    access_token: impl AsRef<str>,
    stream: T,
    stream_length: Option<u64>,
    file_name: Option<String>,
) -> Result<VideoUploadResponseBody, VideoUploadError>
where
    T: Into<Body>,
{
    let open_id = open_id.as_ref();
    let access_token = access_token.as_ref();

    //
    let mut req_url = Url::parse(URL).map_err(VideoUploadError::MakeRequestUrlFailed)?;

    req_url
        .query_pairs_mut()
        .append_pair("open_id", open_id)
        .append_pair("access_token", access_token);

    //
    let part = if let Some(stream_length) = stream_length {
        Part::stream_with_length(stream, stream_length)
    } else {
        Part::stream(stream)
    };

    let part = if let Some(file_name) = file_name {
        part.file_name(file_name)
    } else {
        part.file_name(format!("{}.mp4", Utc::now().timestamp_millis()))
    };

    let form = Form::new().part("video", part).percent_encode_noop();

    //
    let resp = client
        .post(req_url)
        .multipart(form)
        .send()
        .await
        .map_err(VideoUploadError::RespondFailed)?;

    //
    let resp_body = resp
        .bytes()
        .await
        .map_err(VideoUploadError::ReadResponseBodyFailed)?;
    let resp_body = resp_body.as_ref();

    serde_json::from_slice::<VideoUploadResponseBody>(resp_body)
        .map_err(VideoUploadError::DeResponseBodyFailed)
}

#[cfg(feature = "with_tokio")]
pub async fn video_upload_from_reader_stream<S>(
    client: &Client,
    open_id: impl AsRef<str>,
    access_token: impl AsRef<str>,
    stream: S,
    stream_length: Option<u64>,
    file_name: Option<String>,
) -> Result<VideoUploadResponseBody, VideoUploadError>
where
    S: tokio::io::AsyncRead + Send + Sync + 'static,
{
    use tokio_util::io::ReaderStream;

    video_upload(
        client,
        open_id,
        access_token,
        Body::wrap_stream(ReaderStream::new(stream)),
        stream_length,
        file_name,
    )
    .await
}

#[cfg(feature = "with_tokio_fs")]
pub async fn video_upload_from_file(
    client: &Client,
    open_id: impl AsRef<str>,
    access_token: impl AsRef<str>,
    file_path: &std::path::PathBuf,
) -> Result<VideoUploadResponseBody, VideoUploadError> {
    use tokio::fs::File;

    let crate::tokio_fs_util::Info {
        file_size,
        file_name,
    } = crate::tokio_fs_util::info(file_path)
        .await
        .map_err(VideoUploadError::GetFileInfoFailed)?;

    let file = File::open(&file_path)
        .await
        .map_err(VideoUploadError::OpenFileFailed)?;

    video_upload_from_reader_stream(
        client,
        open_id,
        access_token,
        file,
        Some(file_size),
        file_name,
    )
    .await
}

//
//
//
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct VideoUploadResponseBody {
    pub data: VideoUploadResponseBodyData,
    pub extra: VideoUploadResponseBodyExtra,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct VideoUploadResponseBodyData {
    pub err_code: i64,
    pub error_code: i64,
    pub share_id: Option<String>,
    pub error_msg: Option<String>,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct VideoUploadResponseBodyExtra {
    pub error_detail: String,
    pub logid: String,
}

//
//
//
#[derive(Debug)]
pub enum VideoUploadError {
    MakeRequestUrlFailed(UrlParseError),
    RespondFailed(ReqwestError),
    ReadResponseBodyFailed(ReqwestError),
    DeResponseBodyFailed(SerdeJsonError),
    #[cfg(feature = "with_tokio_fs")]
    GetFileInfoFailed(std::io::Error),
    #[cfg(feature = "with_tokio_fs")]
    OpenFileFailed(std::io::Error),
}
impl core::fmt::Display for VideoUploadError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{self:?}")
    }
}
impl std::error::Error for VideoUploadError {}

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

    #[test]
    fn test_de_response_body() {
        match serde_json::from_str::<VideoUploadResponseBody>(include_str!(
            "../../../tests/response_body_files/share/video_upload.json"
        )) {
            Ok(ok_json) => {
                assert_eq!(ok_json.data.err_code, 0);
                assert_eq!(ok_json.extra.error_detail, "");
            }
            x => panic!("{x:?}"),
        }

        match serde_json::from_str::<VideoUploadResponseBody>(include_str!(
            "../../../tests/response_body_files/share/video_upload__err.json"
        )) {
            Ok(ok_json) => {
                assert_eq!(ok_json.data.err_code, 20000);
                assert_eq!(
                    ok_json.extra.error_detail,
                    "access_token not found in the request query param"
                );
            }
            x => panic!("{x:?}"),
        }
    }
}