pub mod jellyfin;

use std::sync::Arc;

use ::tap::*;
use reqwest::{Method, header::InvalidHeaderValue};
use serde::{Deserialize, Serialize};

#[derive(thiserror::Error, Debug)]
pub enum JellyfinApiError {
    #[error("Jellyfin API error: {0}")]
    ReqwestError(#[from] reqwest::Error),
    #[error("Serialization/Deserialization error: {0}")]
    SerdeError(#[from] serde_json::Error),
    #[error("IO error: {0}")]
    IoError(#[from] std::io::Error),
    #[error("Unknown Jellyfin API error")]
    InvalidHeader(#[from] InvalidHeaderValue),
    #[error("Unknown Jellyfin API error")]
    Unknown,
}

type Result<T, E = JellyfinApiError> = std::result::Result<T, E>;

#[derive(Debug, Clone)]
pub struct JellyfinClient {
    client: reqwest::Client,
    access_token: Option<Arc<str>>,
    pub config: Arc<JellyfinConfig>,
}

impl JellyfinClient {
    pub async fn authenticate(
        username: impl AsRef<str>,
        password: impl AsRef<str>,
        config: JellyfinConfig,
    ) -> Result<Self> {
        let url = format!("{}/Users/AuthenticateByName", config.server_url);
        let client = reqwest::Client::new();
        let token = client
            .post(url)
            .json(&jellyfin::AuthenticateUserByName {
                username: Some(username.as_ref().to_string()),
                pw: Some(password.as_ref().to_string()),
            })
            .send()
            .await?
            .error_for_status()?
            .json::<jellyfin::AuthenticationResult>()
            .await?
            .access_token
            .ok_or_else(|| std::io::Error::other("No field access_token in auth response"))?;
        Self::pre_authenticated(token, config)
    }

    pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
        let auth_header = core::iter::once((
            reqwest::header::HeaderName::from_static("x-emby-authorization"),
            reqwest::header::HeaderValue::from_str(&format!(
                "MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"",
                config.client_name, config.device_name, config.device_id, config.version
            ))?,
        ))
        .collect();
        let client = reqwest::Client::builder()
            .default_headers(auth_header)
            .build()?;
        Ok(Self {
            client,
            access_token: Some(token.as_ref().to_string().into()),
            config: Arc::new(config),
        })
    }

    pub fn new_with_config(config: JellyfinConfig) -> Self {
        JellyfinClient {
            client: reqwest::Client::new(),
            access_token: None,
            config: Arc::new(config),
        }
    }

    pub async fn save_token(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
        if let Some(token) = &self.access_token {
            tokio::fs::write(path, &**token).await
        } else {
            Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                "No access token to save",
            ))
        }
    }

    pub async fn load_token(
        &mut self,
        path: impl AsRef<std::path::Path>,
    ) -> std::io::Result<String> {
        let token = tokio::fs::read_to_string(path).await?;
        self.access_token = Some(token.clone().into());
        Ok(token)
    }

    pub fn set_token(&mut self, token: impl AsRef<str>) {
        self.access_token = Some(token.as_ref().into());
    }

    pub fn request_builder(
        &self,
        method: reqwest::Method,
        uri: impl AsRef<str>,
    ) -> reqwest::RequestBuilder {
        let url = format!("{}/{}", self.config.server_url.as_str(), uri.as_ref());
        self.client.request(method, &url)
            .header("X-Emby-Authorization", format!("MediaBrowser Client=\"Jello\", Device=\"Jello\", DeviceId=\"{}\", Version=\"1.0.0\"", self.config.device_id))
            .pipe(|builder| {
                if let Some(token) = &self.access_token {
                    builder.header("X-MediaBrowser-Token", &**token)
                } else {
                    builder
                }
            })
    }

    // pub fn get_builder(&self, uri: impl AsRef<str>) -> reqwest::RequestBuilder {
    //     let url = format!("{}/{}", self.config.server_url.as_str(), uri.as_ref());
    //     self.client.get(&url)
    //         .header("X-Emby-Authorization", format!("MediaBrowser Client=\"Jello\", Device=\"Jello\", DeviceId=\"{}\", Version=\"1.0.0\"", self.config.device_id))
    //         .pipe(|builder| {
    //             if let Some(token) = &self.access_token {
    //                 builder.header("X-MediaBrowser-Token", token)
    //             } else {
    //                 builder
    //             }
    //         })
    // }

    pub async fn post<T: Serialize + ?Sized, U: serde::de::DeserializeOwned>(
        &self,
        uri: impl AsRef<str>,
        body: &T,
    ) -> Result<U> {
        let text = self
            .request_builder(reqwest::Method::POST, uri)
            .json(body)
            .send()
            .await?
            .error_for_status()?
            .text()
            .await?;
        let out: U = serde_json::from_str(&text)?;
        Ok(out)
    }

    pub async fn get<U: serde::de::DeserializeOwned>(&self, uri: impl AsRef<str>) -> Result<U> {
        let text = self
            .request_builder(reqwest::Method::GET, uri)
            .send()
            .await?
            .error_for_status()?
            .text()
            .await?;
        let out: U = serde_json::from_str(&text)?;
        Ok(out)
    }

    pub async fn raw_items(&self) -> Result<jellyfin::BaseItemDtoQueryResult> {
        let text = &self
            .request_builder(Method::GET, "Items")
            .send()
            .await?
            .error_for_status()?
            .text()
            .await?;
        let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
        Ok(out)
    }

    pub async fn items(
        &self,
        root: impl Into<Option<uuid::Uuid>>,
    ) -> Result<Vec<jellyfin::BaseItemDto>> {
        let text = &self
            .request_builder(Method::GET, "Items")
            .query(&[("parentId", root.into())])
            .send()
            .await?
            .error_for_status()?
            .text()
            .await?;
        let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
        Ok(out.items)
    }

    pub async fn search(&self, query: impl AsRef<str>) -> Result<Vec<jellyfin::BaseItemDto>> {
        let text = &self
            .request_builder(Method::GET, "Items/Search")
            .query(&[("searchTerm", query.as_ref()), ("recursive", "true")])
            .send()
            .await?
            .error_for_status()?
            .text()
            .await?;
        let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
        Ok(out.items)
    }

    pub async fn thumbnail(
        &self,
        item: uuid::Uuid,
        image_type: jellyfin::ImageType,
    ) -> Result<bytes::Bytes> {
        let uri = format!(
            "Items/{}/Images/{}",
            item,
            serde_json::to_string(&image_type).expect("Failed to serialize image type")
        );
        let bytes = self
            .request_builder(Method::GET, uri)
            .send()
            .await?
            .error_for_status()?
            .bytes()
            .await?;
        Ok(bytes)
    }

    pub async fn playback_info(&self, item: uuid::Uuid) -> Result<jellyfin::PlaybackInfoDto> {
        let uri = format!("Items/{}/PlaybackInfo", item);
        let text = &self
            .request_builder(Method::GET, uri)
            .send()
            .await?
            .error_for_status()?
            .text()
            .await?;
        let out: jellyfin::PlaybackInfoDto = serde_json::from_str(&text)?;
        Ok(out)
    }

    pub async fn user_data(&self, item: uuid::Uuid) -> Result<jellyfin::UserItemDataDto> {
        let uri = format!("UserItems/{}/UserData", item);
        let text = &self
            .request_builder(Method::GET, uri)
            .send()
            .await?
            .error_for_status()?
            .text()
            .await?;
        let out: jellyfin::UserItemDataDto = serde_json::from_str(&text)?;
        Ok(out)
    }

    pub fn stream_url(&self, item: uuid::Uuid) -> Result<url::Url> {
        let stream_url = format!(
            "{}/Videos/{}/stream?static=true",
            self.config.server_url.as_str(),
            item,
        );
        Ok(url::Url::parse(&stream_url).expect("Failed to parse stream URL"))
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JellyfinConfig {
    pub server_url: iref::IriBuf,
    pub device_id: String,
    pub device_name: String,
    pub client_name: String,
    pub version: String,
}
