/*
    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::{HashKey, cache_memory};
use jellycommon::{Appearance, CreditCategory, IdentifierType, Node, NodeID, NodeKind, RatingType};
use log::info;
use reqwest::{
    Client, ClientBuilder,
    header::{HeaderMap, HeaderName, HeaderValue},
};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fmt::Display, sync::Arc};
use tokio::runtime::Handle;

pub struct Trakt {
    client: Client,
}

impl Trakt {
    pub fn new(api_key: &str) -> Self {
        let client = ClientBuilder::new()
            .default_headers(HeaderMap::from_iter([
                (
                    HeaderName::from_static("trakt-api-key"),
                    HeaderValue::from_str(api_key).unwrap(),
                ),
                (
                    HeaderName::from_static("trakt-api-version"),
                    HeaderValue::from_static("2"),
                ),
                (
                    HeaderName::from_static("content-type"),
                    HeaderValue::from_static("application/json"),
                ),
                (
                    HeaderName::from_static("user-agent"),
                    HeaderValue::from_static(USER_AGENT),
                ),
            ]))
            .build()
            .unwrap();
        Self { client }
    }

    pub fn search(
        &self,
        kinds: &[TraktKind],
        query: &str,
        rt: &Handle,
    ) -> Result<Arc<Vec<TraktSearchResult>>> {
        cache_memory(
            &format!("ext/trakt/search/{}.json", HashKey(query)),
            move || {
                rt.block_on(async {
                    let url = format!(
                        "https://api.trakt.tv/search/{}?query={}&extended=full",
                        kinds
                            .iter()
                            .map(|t| t.singular())
                            .collect::<Vec<_>>()
                            .join(","),
                        urlencoding::encode(query),
                    );
                    let res = self.client.get(url).send().await?.error_for_status()?;
                    Ok(res.json().await?)
                })
            },
        )
        .context("trakt search")
    }

    pub fn lookup(&self, kind: TraktKind, id: u64, rt: &Handle) -> Result<Arc<TraktMediaObject>> {
        cache_memory(&format!("ext/trakt/lookup/{kind}-{id}.json"), move || {
            rt.block_on(async {
                info!("trakt lookup {kind:?}:{id:?}");
                let url = format!("https://api.trakt.tv/{}/{id}?extended=full", kind.plural());
                let res = self.client.get(url).send().await?.error_for_status()?;
                Ok(res.json().await?)
            })
        })
        .context("trakt lookup")
    }

    pub fn people(&self, kind: TraktKind, id: u64, rt: &Handle) -> Result<Arc<TraktPeople>> {
        cache_memory(&format!("ext/trakt/people/{kind}-{id}.json"), move || {
            rt.block_on(async {
                info!("trakt people {kind:?}:{id:?}");
                let url = format!(
                    "https://api.trakt.tv/{}/{id}/people?extended=full",
                    kind.plural()
                );
                let res = self.client.get(url).send().await?.error_for_status()?;
                Ok(res.json().await?)
            })
        })
        .context("trakt people")
    }

    pub fn show_seasons(&self, id: u64, rt: &Handle) -> Result<Arc<Vec<TraktSeason>>> {
        cache_memory(&format!("ext/trakt/seasons/{id}.json"), move || {
            rt.block_on(async {
                info!("trakt seasons {id:?}");
                let url = format!("https://api.trakt.tv/shows/{id}/seasons?extended=full");
                let res = self.client.get(url).send().await?.error_for_status()?;
                Ok(res.json().await?)
            })
        })
        .context("trakt show seasons")
    }

    pub fn show_season_episodes(
        &self,
        id: u64,
        season: usize,
        rt: &Handle,
    ) -> Result<Arc<Vec<TraktEpisode>>> {
        cache_memory(
            &format!("ext/trakt/episodes/{id}-S{season}.json"),
            move || {
                rt.block_on(async {
                    info!("trakt episodes {id:?} season={season}");
                    let url =
                        format!("https://api.trakt.tv/shows/{id}/seasons/{season}?extended=full");
                    let res = self.client.get(url).send().await?.error_for_status()?;
                    Ok(res.json().await?)
                })
            },
        )
        .context("trakt show season episodes")
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct TraktSeason {
    pub number: usize,
    pub ids: TraktIds,
    pub rating: f64,
    pub votes: usize,
    pub episode_count: usize,
    pub aired_count: Option<usize>,
    pub title: String,
    pub overview: Option<String>,
    pub network: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct TraktEpisode {
    pub season: Option<usize>,
    pub number: usize,
    pub number_abs: Option<usize>,
    pub ids: TraktIds,
    pub rating: Option<f64>,
    pub votes: usize,
    pub title: String,
    pub runtime: Option<f64>,
    pub overview: Option<String>,
    pub available_translations: Vec<String>,
    pub first_aired: Option<String>,
    pub episode_type: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct TraktPeople {
    #[serde(default)]
    pub cast: Vec<TraktAppearance>,
    #[serde(default)]
    pub crew: BTreeMap<TraktPeopleGroup, Vec<TraktAppearance>>,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct TraktAppearance {
    #[serde(default)]
    pub jobs: Vec<String>,
    #[serde(default)]
    pub characters: Vec<String>,
    pub person: TraktPerson,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct TraktPerson {
    pub name: String,
    pub ids: TraktIds,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TraktSearchResult {
    pub r#type: TraktKind,
    pub score: f64,
    #[serde(flatten)]
    pub inner: TraktKindObject,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TraktKindObject {
    Movie(TraktMediaObject),
    Show(TraktMediaObject),
    Season(TraktMediaObject),
    Episode(TraktMediaObject),
    Person(TraktMediaObject),
    User(TraktMediaObject),
}

impl TraktKindObject {
    pub fn inner(&self) -> &TraktMediaObject {
        match self {
            TraktKindObject::Movie(x)
            | TraktKindObject::Show(x)
            | TraktKindObject::Season(x)
            | TraktKindObject::Episode(x)
            | TraktKindObject::Person(x)
            | TraktKindObject::User(x) => x,
        }
    }
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum TraktPeopleGroup {
    #[serde(rename = "production")]
    Production,
    #[serde(rename = "art")]
    Art,
    #[serde(rename = "crew")]
    Crew,
    #[serde(rename = "costume & make-up")] //? they really use that in as a key?!
    CostumeMakeup,
    #[serde(rename = "directing")]
    Directing,
    #[serde(rename = "writing")]
    Writing,
    #[serde(rename = "sound")]
    Sound,
    #[serde(rename = "camera")]
    Camera,
    #[serde(rename = "visual effects")]
    VisualEffects,
    #[serde(rename = "lighting")]
    Lighting,
    #[serde(rename = "editing")]
    Editing,
    #[serde(rename = "created by")]
    CreatedBy,
}
impl TraktPeopleGroup {
    pub fn as_credit_category(self) -> CreditCategory {
        match self {
            TraktPeopleGroup::Production => CreditCategory::Production,
            TraktPeopleGroup::Art => CreditCategory::Art,
            TraktPeopleGroup::Crew => CreditCategory::Crew,
            TraktPeopleGroup::CostumeMakeup => CreditCategory::CostumeMakeup,
            TraktPeopleGroup::Directing => CreditCategory::Directing,
            TraktPeopleGroup::Writing => CreditCategory::Writing,
            TraktPeopleGroup::Sound => CreditCategory::Sound,
            TraktPeopleGroup::Camera => CreditCategory::Camera,
            TraktPeopleGroup::VisualEffects => CreditCategory::Vfx,
            TraktPeopleGroup::Lighting => CreditCategory::Lighting,
            TraktPeopleGroup::Editing => CreditCategory::Editing,
            TraktPeopleGroup::CreatedBy => CreditCategory::CreatedBy,
        }
    }
}
impl TraktAppearance {
    pub fn a(&self) -> Appearance {
        Appearance {
            jobs: self.jobs.to_owned(),
            characters: self.characters.to_owned(),
            node: NodeID([0; 32]), // person: Person {
                                   //     name: self.person.name.to_owned(),
                                   //     headshot: None,
                                   //     ids: self.person.ids.to_owned(),
                                   // },
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TraktMediaObject {
    pub title: String,
    pub year: Option<u32>,
    pub ids: TraktIds,

    pub tagline: Option<String>,
    pub overview: Option<String>,
    pub released: Option<String>,
    pub runtime: Option<usize>,
    pub country: Option<String>,
    pub trailer: Option<String>,
    pub homepage: Option<String>,
    pub status: Option<String>,
    pub rating: Option<f64>,
    pub votes: Option<usize>,
    pub comment_count: Option<usize>,
    pub language: Option<String>,
    pub available_translations: Option<Vec<String>>,
    pub genres: Option<Vec<String>>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct TraktIds {
    pub trakt: Option<u64>,
    pub slug: Option<String>,
    pub tvdb: Option<u64>,
    pub imdb: Option<String>,
    pub tmdb: Option<u64>,
}

impl Display for TraktSearchResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!(
            "{} ({}) \x1b[2m{} {}\x1b[0m",
            self.inner.inner().title,
            self.inner.inner().year.unwrap_or(0),
            self.r#type,
            self.inner.inner().ids.trakt.unwrap_or_default()
        ))
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, Copy, Hash, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TraktKind {
    Movie,
    Show,
    Season,
    Episode,
    Person,
    User,
}

impl TraktKind {
    pub fn as_node_kind(self) -> NodeKind {
        match self {
            TraktKind::Movie => NodeKind::Movie,
            TraktKind::Show => NodeKind::Show,
            TraktKind::Season => NodeKind::Season,
            TraktKind::Episode => NodeKind::Episode,
            TraktKind::Person => NodeKind::Channel,
            TraktKind::User => NodeKind::Channel,
        }
    }
}

impl TraktKind {
    pub fn singular(self) -> &'static str {
        match self {
            TraktKind::Movie => "movie",
            TraktKind::Show => "show",
            TraktKind::Season => "season",
            TraktKind::Episode => "episode",
            TraktKind::Person => "person",
            TraktKind::User => "user",
        }
    }
    pub fn plural(self) -> &'static str {
        match self {
            TraktKind::Movie => "movies",
            TraktKind::Show => "shows",
            TraktKind::Season => "seasons",
            TraktKind::Episode => "episodes",
            TraktKind::Person => "people",
            TraktKind::User => "users", // not used in API
        }
    }
}
impl Display for TraktKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            TraktKind::Movie => "Movie",
            TraktKind::Show => "Show",
            TraktKind::Season => "Season",
            TraktKind::Episode => "Episode",
            TraktKind::Person => "Person",
            TraktKind::User => "User",
        })
    }
}

impl ImportPlugin for Trakt {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "trakt",
            handle_instruction: true,
            handle_process: true,
            ..Default::default()
        }
    }
    fn instruction(&self, ct: &ImportContext, node: NodeID, line: &str) -> Result<()> {
        if let Some(value) = line.strip_prefix("trakt-").or(line.strip_prefix("trakt=")) {
            let (ty, id) = value.split_once(":").unwrap_or(("movie", value));
            let ty = match ty {
                "movie" => IdentifierType::TraktMovie,
                "show" => IdentifierType::TraktShow,
                "season" => IdentifierType::TraktSeason,
                "episode" => IdentifierType::TraktEpisode,
                _ => bail!("unknown trakt kind"),
            };
            ct.db.update_node_init(node, |node| {
                node.identifiers.insert(ty, id.to_owned());
            })?;
        }
        Ok(())
    }
    fn process(&self, ct: &ImportContext, node: NodeID, data: &Node) -> Result<()> {
        self.process_primary(ct, node, data)?;
        self.process_episode(ct, node, data)?;
        Ok(())
    }
}

impl Trakt {
    fn process_primary(&self, ct: &ImportContext, node: NodeID, data: &Node) -> Result<()> {
        let (trakt_kind, trakt_id): (_, u64) =
            if let Some(id) = data.identifiers.get(&IdentifierType::TraktShow) {
                (TraktKind::Show, id.parse()?)
            } else if let Some(id) = data.identifiers.get(&IdentifierType::TraktMovie) {
                (TraktKind::Movie, id.parse()?)
            } else {
                return Ok(());
            };

        let details = self.lookup(trakt_kind, trakt_id, ct.rt)?;
        let people = self.people(trakt_kind, trakt_id, ct.rt)?;

        let mut people_map = BTreeMap::<CreditCategory, Vec<Appearance>>::new();
        for p in people.cast.iter() {
            people_map
                .entry(CreditCategory::Cast)
                .or_default()
                .push(p.a())
        }
        for (group, people) in people.crew.iter() {
            for p in people {
                people_map
                    .entry(group.as_credit_category())
                    .or_default()
                    .push(p.a())
            }
        }

        // for p in people_map.values_mut().flatten() {
        //     if let Some(id) = p.person.ids.tmdb {
        //         let k = rthandle.block_on(tmdb.person_image(id))?;
        //         if let Some(prof) = k.profiles.first() {
        //             let im = rthandle.block_on(tmdb.image(&prof.file_path))?;
        //             p.person.headshot = Some(AssetInner::Cache(im).ser());
        //         }
        //     }
        // }

        ct.db.update_node_init(node, |node| {
            node.kind = trakt_kind.as_node_kind();
            node.title = Some(details.title.clone());
            if let Some(overview) = &details.overview {
                node.description = Some(overview.clone())
            }
            if let Some(tagline) = &details.tagline {
                node.tagline = Some(tagline.clone())
            }
            node.credits.extend(people_map);
            if let Some(x) = details.ids.imdb.clone() {
                node.identifiers.insert(IdentifierType::Imdb, x);
            }
            if let Some(x) = details.ids.tvdb.clone() {
                node.identifiers.insert(IdentifierType::Tvdb, x.to_string());
            }
            if let Some(x) = details.ids.tmdb.clone() {
                match trakt_kind {
                    TraktKind::Movie => node
                        .identifiers
                        .insert(IdentifierType::TmdbMovie, x.to_string()),
                    TraktKind::Show => node
                        .identifiers
                        .insert(IdentifierType::TmdbSeries, x.to_string()),
                    _ => None,
                };
            }
            if let Some(rating) = &details.rating {
                node.ratings.insert(RatingType::Trakt, *rating);
            }
        })?;
        Ok(())
    }
    fn process_episode(&self, ct: &ImportContext, node: NodeID, node_data: &Node) -> Result<()> {
        let (Some(episode), Some(season)) = (node_data.index, node_data.season_index) else {
            return Ok(());
        };
        let mut show_id = None;
        for &parent in &node_data.parents {
            let parent_data = ct.db.get_node(parent)?.ok_or(anyhow!("parent missing"))?;
            if let Some(id) = parent_data.identifiers.get(&IdentifierType::TraktShow) {
                show_id = Some(id.parse::<u64>()?);
                break;
            }
        }
        let Some(show_id) = show_id else {
            return Ok(());
        };

        let seasons = self.show_seasons(show_id, ct.rt)?;
        if seasons.iter().any(|x| x.number == season) {
            let episodes = self.show_season_episodes(show_id, season, ct.rt)?;
            if let Some(episode) = episodes.get(episode.saturating_sub(1)) {
                ct.db.update_node_init(node, |node| {
                    node.kind = NodeKind::Episode;
                    node.index = Some(episode.number);
                    node.title = Some(episode.title.clone());
                    node.description = episode.overview.clone().or(node.description.clone());
                    if let Some(r) = episode.rating {
                        node.ratings.insert(RatingType::Trakt, r);
                    }
                })?;
            }
        }
        Ok(())
    }
}
