/*
    This file is part of jellything (https://codeberg.org/metamuffin/jellything)
    which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
    Copyright (C) 2026 metamuffin <metamuffin.org>
*/
use crate::{
    USER_AGENT,
    plugins::{ImportContext, ImportPlugin, PluginInfo},
};
use anyhow::{Context, Result, anyhow, bail};
use jellycache::{EscapeKey, HashKey, cache_memory, cache_store};
use jellycommon::{
    Asset, IdentifierType, Node, NodeID, PictureSlot, RatingType,
    chrono::{Utc, format::Parsed},
};
use log::info;
use reqwest::{
    Client, ClientBuilder,
    header::{HeaderMap, HeaderName, HeaderValue},
};
use serde::{Deserialize, Serialize};
use std::{fmt::Display, sync::Arc};
use tokio::runtime::Handle;

pub struct Tmdb {
    client: Client,
    image_client: Client,
    key: String,
}

impl Tmdb {
    pub fn new(api_key: &str) -> Self {
        let client = ClientBuilder::new()
            .default_headers(HeaderMap::from_iter([
                (
                    HeaderName::from_static("accept"),
                    HeaderValue::from_static("application/json"),
                ),
                (
                    HeaderName::from_static("user-agent"),
                    HeaderValue::from_static(USER_AGENT),
                ),
            ]))
            .build()
            .unwrap();
        let image_client = ClientBuilder::new().build().unwrap();
        Self {
            client,
            image_client,
            key: api_key.to_owned(),
        }
    }
    pub fn search(&self, kind: TmdbKind, query: &str, rt: &Handle) -> Result<Arc<TmdbQuery>> {
        cache_memory(
            &format!("ext/tmdb/search/{kind}-{}.json", HashKey(query)),
            move || {
                rt.block_on(async {
                    info!("searching tmdb: {query:?}");
                    Ok(self
                        .client
                        .get(format!(
                            "https://api.themoviedb.org/3/search/{kind}?query={}?api_key={}",
                            query.replace(" ", "+"),
                            self.key
                        ))
                        .send()
                        .await?
                        .error_for_status()?
                        .json::<TmdbQuery>()
                        .await?)
                })
            },
        )
        .context("tmdb search")
    }
    pub fn details(&self, kind: TmdbKind, id: u64, rt: &Handle) -> Result<Arc<TmdbDetails>> {
        cache_memory(&format!("ext/tmdb/details/{kind}-{id}.json"), move || {
            rt.block_on(async {
                info!("fetching details: {id:?}");
                Ok(self
                    .client
                    .get(format!(
                        "https://api.themoviedb.org/3/{kind}/{id}?api_key={}",
                        self.key,
                    ))
                    .send()
                    .await?
                    .error_for_status()?
                    .json()
                    .await?)
            })
        })
        .context("tmdb details")
    }
    pub fn person_image(&self, id: u64, rt: &Handle) -> Result<Arc<TmdbPersonImage>> {
        cache_memory(&format!("ext/tmdb/person/images/{id}.json"), move || {
            rt.block_on(async {
                Ok(self
                    .client
                    .get(format!(
                        "https://api.themoviedb.org/3/person/{id}/images?api_key={}",
                        self.key,
                    ))
                    .send()
                    .await?
                    .error_for_status()?
                    .json()
                    .await?)
            })
        })
        .context("tmdb person images")
    }
    pub fn image(&self, path: &str, rt: &Handle) -> Result<Asset> {
        cache_store(
            format!("ext/tmdb/image/{}.image", EscapeKey(path)),
            move || {
                rt.block_on(async {
                    info!("downloading image {path:?}");
                    Ok(self
                        .image_client
                        .get(format!("https://image.tmdb.org/t/p/original{path}"))
                        .send()
                        .await?
                        .error_for_status()?
                        .bytes()
                        .await?
                        .to_vec())
                })
            },
        )
        .context("tmdb image download")
        .map(Asset)
    }

    pub fn episode_details(
        &self,
        series_id: u64,
        season: usize,
        episode: usize,
        rt: &Handle,
    ) -> Result<Arc<TmdbEpisode>> {
        cache_memory(&format!("ext/tmdb/episode-details/{series_id}-S{season}-E{episode}.json"), move || {
            rt.block_on(async {
                info!("tmdb episode details {series_id} S={season} E={episode}");
                Ok(self
                    .image_client
                    .get(format!("https://api.themoviedb.org/3/tv/{series_id}/season/{season}/episode/{episode}?api_key={}", self.key))
                    .send()
                    .await?
                    .error_for_status()?
                    .json()
                    .await?)
            })
        })
        .context("tmdb episode details")
    }
}

impl ImportPlugin for Tmdb {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "tmdb",
            handle_process: true,
            ..Default::default()
        }
    }
    fn process(&self, ct: &ImportContext, node: NodeID, data: &Node) -> Result<()> {
        self.process_primary(ct, node, data)?;
        self.process_episode(ct, node, data)?;
        Ok(())
    }
}
impl Tmdb {
    fn process_primary(&self, ct: &ImportContext, node: NodeID, data: &Node) -> Result<()> {
        let (tmdb_kind, tmdb_id): (_, u64) =
            if let Some(id) = data.identifiers.get(&IdentifierType::TmdbSeries) {
                (TmdbKind::Tv, id.parse()?)
            } else if let Some(id) = data.identifiers.get(&IdentifierType::TmdbMovie) {
                (TmdbKind::Movie, id.parse()?)
            } else {
                return Ok(());
            };

        let details = self.details(tmdb_kind, tmdb_id, ct.rt)?;
        let mut images = Vec::new();
        if let Some(path) = &details.backdrop_path {
            images.push((
                PictureSlot::Backdrop,
                self.image(path, ct.rt).context("backdrop image")?,
            ));
        }
        if let Some(path) = &details.poster_path {
            images.push((
                PictureSlot::Cover,
                self.image(path, ct.rt).context("poster image")?,
            ));
        }

        let release_date = details
            .release_date
            .as_ref()
            .map(|s| parse_release_date(s))
            .transpose()?
            .flatten();

        ct.db.update_node_init(node, |node| {
            node.title = details.title.clone().or(node.title.clone());
            node.tagline = details.tagline.clone().or(node.tagline.clone());
            node.description = Some(details.overview.clone());
            node.ratings.insert(RatingType::Tmdb, details.vote_average);
            node.pictures.extend(images);
            node.release_date = release_date.or(node.release_date);
        })?;
        Ok(())
    }
    fn process_episode(&self, ct: &ImportContext, node: NodeID, data: &Node) -> Result<()> {
        let (Some(episode), Some(season)) = (data.index, data.season_index) else {
            return Ok(());
        };
        let mut series_id = None;
        for &parent in &data.parents {
            let parent_data = ct.db.get_node(parent)?.ok_or(anyhow!("parent missing"))?;
            if let Some(id) = parent_data.identifiers.get(&IdentifierType::TmdbSeries) {
                series_id = Some(id.parse::<u64>()?);
                break;
            }
        }
        let Some(series_id) = series_id else {
            return Ok(());
        };
        let details = self.episode_details(series_id, season, episode, ct.rt)?;
        let mut images = Vec::new();
        if let Some(path) = &details.still_path {
            images.push((PictureSlot::Cover, self.image(path, ct.rt)?))
        }

        let release_date = parse_release_date(&details.air_date)?;
        ct.db.update_node_init(node, |node| {
            node.title = Some(details.name.clone());
            node.description = Some(details.overview.clone());
            node.release_date = release_date.or(node.release_date);
            node.ratings.insert(RatingType::Tmdb, details.vote_average);
            node.pictures.extend(images);
        })?;
        Ok(())
    }
}

pub fn parse_release_date(d: &str) -> Result<Option<i64>> {
    if d.is_empty() {
        return Ok(None);
    } else if d.len() < 10 {
        bail!(anyhow!("date string too short"))
    }
    let (year, month, day) = (&d[0..4], &d[5..7], &d[8..10]);
    let (year, month, day) = (
        year.parse().context("parsing year")?,
        month.parse().context("parsing month")?,
        day.parse().context("parsing day")?,
    );

    let mut p = Parsed::new();
    p.year = Some(year);
    p.month = Some(month);
    p.day = Some(day);
    p.hour_div_12 = Some(0);
    p.hour_mod_12 = Some(0);
    p.minute = Some(0);
    p.second = Some(0);
    Ok(Some(p.to_datetime_with_timezone(&Utc)?.timestamp_millis()))
}

impl Display for TmdbKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            TmdbKind::Tv => "tv",
            TmdbKind::Movie => "movie",
        })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbEpisode {
    pub air_date: String,
    pub overview: String,
    pub name: String,
    pub id: u64,
    pub runtime: f64,
    pub still_path: Option<String>,
    pub vote_average: f64,
    pub vote_count: usize,
}

#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize)]
pub enum TmdbKind {
    Tv,
    Movie,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbPersonImage {
    pub id: u64,
    pub profiles: Vec<TmdbPersonImageProfile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbPersonImageProfile {
    pub aspect_ratio: f64,
    pub height: u32,
    pub width: u32,
    pub file_path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbQuery {
    pub page: usize,
    pub results: Vec<TmdbQueryResult>,
    pub total_pages: usize,
    pub total_results: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbQueryResult {
    pub adult: bool,
    pub backdrop_path: Option<String>,
    pub genre_ids: Vec<u64>,
    pub id: u64,
    pub original_language: Option<String>,
    pub original_title: Option<String>,
    pub overview: String,
    pub popularity: f64,
    pub poster_path: Option<String>,
    pub release_date: Option<String>,
    pub title: Option<String>,
    pub name: Option<String>,
    pub vote_average: f64,
    pub vote_count: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbDetails {
    pub adult: bool,
    pub backdrop_path: Option<String>,
    pub genres: Vec<TmdbGenre>,
    pub id: u64,
    pub original_language: Option<String>,
    pub original_title: Option<String>,
    pub overview: String,
    pub popularity: f64,
    pub poster_path: Option<String>,
    pub release_date: Option<String>,
    pub title: Option<String>,
    pub name: Option<String>,
    pub vote_average: f64,
    pub vote_count: usize,
    pub budget: Option<usize>,
    pub homepage: Option<String>,
    pub imdb_id: Option<String>,
    pub production_companies: Vec<TmdbProductionCompany>,
    pub revenue: Option<usize>,
    pub tagline: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbGenre {
    pub id: u64,
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbProductionCompany {
    pub id: u64,
    pub name: String,
    pub logo_path: Option<String>,
}
