use serde::Deserialize;
use std::{fmt, time::Duration};
use url::Url;

const API_HOST: &str = "https://api.themoviedb.org/3";

#[derive(Debug)]
pub enum TmdbError {
    Request(ureq::Error),
    Url(url::ParseError),
}

impl std::error::Error for TmdbError {}

impl fmt::Display for TmdbError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TmdbError::Request(e) => write!(f, "request error: {e}"),
            TmdbError::Url(e) => write!(f, "failed to create url: {e}"),
        }
    }
}

impl From<ureq::Error> for TmdbError {
    fn from(err: ureq::Error) -> Self {
        TmdbError::Request(err)
    }
}

impl From<url::ParseError> for TmdbError {
    fn from(err: url::ParseError) -> Self {
        TmdbError::Url(err)
    }
}

pub struct TmdbClient {
    api_token: String,
    http_client: ureq::Agent,
    image_config: Option<ImageConfiguration>,
}

impl TmdbClient {
    pub fn new(api_token: String) -> TmdbClient {
        TmdbClient {
            api_token,
            http_client: ureq::Agent::new_with_defaults(),
            image_config: None,
        }
    }

    pub fn search_movie(
        &self,
        query: &str,
        year: Option<i32>,
    ) -> Result<Vec<MovieSearchResult>, TmdbError> {
        let year_param = year.map_or(String::new(), |y| format!("&year={}", y));
        let url = Url::parse(&format!(
            "{}/search/movie?query={}{}",
            API_HOST, query, year_param
        ))?;

        let resp: MovieSearchResponse = self
            .http_client
            .get(url.as_str())
            .header("Authorization", &self.api_token)
            .config()
            .timeout_global(Some(Duration::from_secs(5)))
            .build()
            .call()?
            .body_mut()
            .read_json()?;

        Ok(resp.results)
    }

    pub fn search_tv(
        &self,
        query: &str,
        year: Option<i32>,
    ) -> Result<Vec<TvSearchResult>, TmdbError> {
        let year_param = year.map_or(String::new(), |y| format!("&first_air_date_year={}", y));
        let url = Url::parse(&format!(
            "{}/search/tv?query={}{}",
            API_HOST, query, year_param
        ))?;

        let resp: TvSearchResponse = self
            .http_client
            .get(url.as_str())
            .header("Authorization", &self.api_token)
            .config()
            .timeout_global(Some(Duration::from_secs(5)))
            .build()
            .call()?
            .body_mut()
            .read_json()?;

        Ok(resp.results)
    }

    pub fn movie_details(&self, movie_id: u32) -> Result<MovieSearchResult, TmdbError> {
        let url = Url::parse(&format!("{}/movie/{}", API_HOST, movie_id))?;

        let details: MovieSearchResult = self
            .http_client
            .get(url.as_str())
            .header("Authorization", &self.api_token)
            .config()
            .timeout_global(Some(Duration::from_secs(5)))
            .build()
            .call()?
            .body_mut()
            .read_json()?;

        Ok(details)
    }

    pub fn tv_details(&self, series_id: u32) -> Result<TvSeriesDetails, TmdbError> {
        let url = Url::parse(&format!("{}/tv/{}", API_HOST, series_id))?;

        let details: TvSeriesDetails = self
            .http_client
            .get(url.as_str())
            .header("Authorization", &self.api_token)
            .config()
            .timeout_global(Some(Duration::from_secs(5)))
            .build()
            .call()?
            .body_mut()
            .read_json()?;

        Ok(details)
    }

    pub fn season_details(
        &self,
        series_id: u32,
        season_number: u32,
    ) -> Result<TvSeasonDetails, TmdbError> {
        let url = Url::parse(&format!(
            "{}/tv/{}/season/{}",
            API_HOST, series_id, season_number
        ))?;

        let details: TvSeasonDetails = self
            .http_client
            .get(url.as_str())
            .header("Authorization", &self.api_token)
            .config()
            .timeout_global(Some(Duration::from_secs(5)))
            .build()
            .call()?
            .body_mut()
            .read_json()?;

        Ok(details)
    }

    pub fn image_configuration(&mut self) -> Result<ImageConfiguration, TmdbError> {
        if self.image_config.is_none() {
            let url = format!("{}/configuration", API_HOST);
            let config: Configuration = self
                .http_client
                .get(&url)
                .header("Authorization", &self.api_token)
                .config()
                .timeout_global(Some(Duration::from_secs(5)))
                .build()
                .call()?
                .body_mut()
                .read_json()?;

            self.image_config = Some(config.images);
        }

        Ok(self.image_config.clone().unwrap())
    }

    pub fn download_image(&self, img_url: &str) -> Result<Vec<u8>, TmdbError> {
        let image = self
            .http_client
            .get(img_url)
            .config()
            .timeout_global(Some(Duration::from_secs(10)))
            .build()
            .call()?
            .body_mut()
            .read_to_vec()?;

        Ok(image)
    }
}

// --- Shared types ---

#[derive(Debug, Clone, Deserialize)]
pub struct ImageConfiguration {
    pub secure_base_url: String,
    #[allow(dead_code)]
    poster_sizes: Vec<String>,
}

impl ImageConfiguration {
    pub fn poster_w342(&self) -> &str {
        "w342"
    }
}

#[derive(Debug, Deserialize)]
struct Configuration {
    images: ImageConfiguration,
}

// --- Movie types ---

#[derive(Debug, Deserialize)]
struct MovieSearchResponse {
    results: Vec<MovieSearchResult>,
}

#[derive(Debug, Deserialize)]
pub struct MovieSearchResult {
    pub id: u32,
    pub title: String,
    pub original_title: String,
    pub release_date: Option<String>,
    pub overview: Option<String>,
    pub vote_average: Option<f64>,
    pub poster_path: Option<String>,
    pub backdrop_path: Option<String>,
}

impl MovieSearchResult {
    #[allow(dead_code)]
    pub fn release_year(&self) -> Option<i32> {
        parse_year(self.release_date.as_deref())
    }

    pub fn get_poster_url(&self, config: &ImageConfiguration) -> Option<String> {
        self.poster_path
            .as_ref()
            .map(|p| format!("{}/{}/{}", config.secure_base_url, config.poster_w342(), p))
    }

    pub fn get_backdrop_url(&self, config: &ImageConfiguration) -> Option<String> {
        self.backdrop_path
            .as_ref()
            .map(|p| format!("{}/w1280/{}", config.secure_base_url, p))
    }
}

// --- TV types ---

#[derive(Debug, Deserialize)]
struct TvSearchResponse {
    results: Vec<TvSearchResult>,
}

#[derive(Debug, Deserialize)]
pub struct TvSearchResult {
    pub id: u32,
    pub name: String,
    pub original_name: String,
    pub first_air_date: Option<String>,
    pub poster_path: Option<String>,
}

impl TvSearchResult {
    #[allow(dead_code)]
    pub fn release_year(&self) -> Option<i32> {
        parse_year(self.first_air_date.as_deref())
    }

    pub fn get_poster_url(&self, config: &ImageConfiguration) -> Option<String> {
        self.poster_path
            .as_ref()
            .map(|p| format!("{}/{}/{}", config.secure_base_url, config.poster_w342(), p))
    }
}

#[derive(Debug, Deserialize)]
pub struct TvSeriesDetails {
    pub id: u32,
    pub name: String,
    pub original_name: String,
    pub first_air_date: Option<String>,
    pub overview: Option<String>,
    pub vote_average: Option<f64>,
    pub poster_path: Option<String>,
    pub backdrop_path: Option<String>,
    #[allow(dead_code)]
    pub number_of_seasons: u32,
    pub seasons: Vec<TvSeriesSeason>,
}

impl TvSeriesDetails {
    pub fn release_year(&self) -> Option<i32> {
        parse_year(self.first_air_date.as_deref())
    }

    pub fn get_poster_url(&self, config: &ImageConfiguration) -> Option<String> {
        self.poster_path
            .as_ref()
            .map(|p| format!("{}/{}/{}", config.secure_base_url, config.poster_w342(), p))
    }

    pub fn get_backdrop_url(&self, config: &ImageConfiguration) -> Option<String> {
        self.backdrop_path
            .as_ref()
            .map(|p| format!("{}/w1280/{}", config.secure_base_url, p))
    }
}

#[derive(Debug, Deserialize)]
pub struct TvSeriesSeason {
    pub id: u32,
    pub season_number: u32,
    pub name: String,
    pub overview: Option<String>,
    pub vote_average: Option<f64>,
    pub poster_path: Option<String>,
    pub air_date: Option<String>,
    #[allow(dead_code)]
    pub episode_count: u32,
}

impl TvSeriesSeason {
    pub fn release_year(&self) -> Option<i32> {
        parse_year(self.air_date.as_deref())
    }

    pub fn get_poster_url(&self, config: &ImageConfiguration) -> Option<String> {
        self.poster_path
            .as_ref()
            .map(|p| format!("{}/{}/{}", config.secure_base_url, config.poster_w342(), p))
    }
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct TvSeasonDetails {
    pub id: u32,
    pub name: String,
    pub overview: Option<String>,
    pub vote_average: Option<f64>,
    pub season_number: u32,
    pub poster_path: Option<String>,
    pub episodes: Vec<TvEpisode>,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct TvEpisode {
    pub id: u32,
    pub episode_number: u32,
    pub name: String,
    pub overview: Option<String>,
    pub vote_average: Option<f64>,
    pub air_date: Option<String>,
    pub still_path: Option<String>,
    pub runtime: Option<u32>,
}

fn parse_year(date: Option<&str>) -> Option<i32> {
    date.and_then(|d| d.get(..4)).and_then(|y| y.parse().ok())
}
