/*
    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 std::sync::Arc;

use anyhow::{Context, Result, anyhow};
use jellycache::Cache;
use jellycommon::{
    IDENT_IMDB, MSOURCE_OMDB, NO_DESCRIPTION, NO_IDENTIFIERS, NO_RATINGS, NO_TITLE, RTYP_IMDB,
    RTYP_METACRITIC, RTYP_ROTTEN_TOMATOES,
};
use jellydb::RowNum;
use log::info;
use reqwest::{
    Client, ClientBuilder,
    header::{HeaderMap, HeaderName, HeaderValue},
};
use serde::{Deserialize, Serialize};
use tokio::runtime::Handle;

use crate::{
    USER_AGENT,
    plugins::{ImportPlugin, PluginContext, PluginInfo},
    source_rank::ObjectImportSourceExt,
};

pub struct Omdb {
    client: Client,
    key: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct OmdbMovie {
    title: String,
    year: String,
    rated: String,
    released: String,
    runtime: String,
    director: String,
    writer: String,
    actors: String,
    plot: String,
    language: String,
    country: String,
    awards: String,
    poster: String,
    ratings: Vec<OmdbRating>,
    metascore: String,
    #[serde(rename = "imdbRating")]
    imdb_rating: String,
    #[serde(rename = "imdbVotes")]
    imdb_votes: String,
    #[serde(rename = "imdbID")]
    imdb_id: String,
    r#type: String,
    #[serde(rename = "DVD")]
    dvd: Option<String>,
    box_office: Option<String>,
    production: Option<String>,
    website: Option<String>,
    response: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct OmdbRating {
    source: String,
    value: String,
}

impl Omdb {
    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();
        Self {
            client,
            key: api_key.to_owned(),
        }
    }
    fn lookup(&self, cache: &Cache, id: &str, rt: &Handle) -> Result<Arc<OmdbMovie>> {
        cache
            .cache_memory(&format!("ext/omdb/movie/{id}.json"), move || {
                rt.block_on(async {
                    info!("lookup {id}");
                    let res = self
                        .client
                        .get(format!(
                            "http://www.omdbapi.com/?apikey={}&i={id}",
                            self.key
                        ))
                        .send()
                        .await?
                        .error_for_status()?;
                    Ok(res.json().await?)
                })
            })
            .context("omdb lookup")
    }
}

impl ImportPlugin for Omdb {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "omdb",
            tag: MSOURCE_OMDB,
            handle_process: true,
            ..Default::default()
        }
    }
    fn process(&self, ct: &PluginContext, node: RowNum) -> Result<()> {
        let data = ct.ic.get_node(node)?.unwrap();
        let data = data.as_object();

        let Some(id) = data.get(NO_IDENTIFIERS).unwrap_or_default().get(IDENT_IMDB) else {
            return Ok(());
        };

        let entry = self.lookup(&ct.ic.cache, id, ct.rt)?;

        let imdb = match entry.imdb_rating.as_str() {
            "N/A" => None,
            v => Some(v.parse::<f64>().context("parse imdb rating")?),
        };
        let metascore = match entry.metascore.as_str() {
            "N/A" => None,
            v => Some(v.parse::<f64>().context("parse metascore rating")?),
        };
        let rotten_tomatoes = entry
            .ratings
            .iter()
            .find(|r| r.source == "Rotten Tomatoes")
            .map(|r| {
                r.value
                    .strip_suffix("%")
                    .ok_or(anyhow!("% missing"))?
                    .parse::<f64>()
                    .context("parse rotten tomatoes rating")
            })
            .transpose()?;

        ct.ic.update_node(node, |mut node| {
            node = node.as_object().insert_s(ct.is, NO_TITLE, &entry.title);
            node = node
                .as_object()
                .insert_s(ct.is, NO_DESCRIPTION, &entry.plot);

            for (typ, val) in [
                (RTYP_METACRITIC, metascore),
                (RTYP_IMDB, imdb),
                (RTYP_ROTTEN_TOMATOES, rotten_tomatoes),
            ] {
                if let Some(x) = val {
                    node = node
                        .as_object()
                        .update(NO_RATINGS, |rts| rts.insert_s(ct.is, typ, x));
                }
            }
            node
        })?;

        Ok(())
    }
}
