/*
    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::{ImportPlugin, PluginInfo},
};
use anyhow::{Context, Result};
use jellycache::cache_memory;
use log::info;
use reqwest::{
    Client, ClientBuilder,
    header::{HeaderMap, HeaderName, HeaderValue},
};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, sync::Arc, time::Duration};
use tokio::{
    runtime::Handle,
    sync::Semaphore,
    time::{Instant, sleep_until},
};

pub mod reltypes {
    pub const MUSIC_VIDEO: &str = "ce3de655-7451-44d1-9224-87eb948c205d";
    pub const INSTRUMENTAL: &str = "9fc01a58-7801-4bd2-b07d-61cc7ffacf90";
    pub const VOCAL: &str = "0fdbe3c6-7700-4a31-ae54-b53f06ae1cfa";
    pub const RECORDING: &str = "a01ee869-80a8-45ef-9447-c59e91aa7926";
    pub const PROGRAMMING: &str = "36c50022-44e0-488d-994b-33f11d20301e";
    pub const PRODUCER: &str = "5c0ceac3-feb4-41f0-868d-dc06f6e27fc0";
    pub const ARTIST: &str = "5c0ceac3-feb4-41f0-868d-dc06f6e27fc0";
    pub const PHONOGRAPHIC_COPYRIGHT: &str = "7fd5fbc0-fbf4-4d04-be23-417d50a4dc30";
    pub const MIX: &str = "3e3102e1-1896-4f50-b5b2-dd9824e46efe";
    pub const INSTRUMENT: &str = "59054b12-01ac-43ee-a618-285fd397e461";
    pub const WIKIDATA: &str = "689870a4-a1e4-4912-b17f-7b2664215698";
    pub const VGMDB: &str = "0af15ab3-c615-46d6-b95b-a5fcd2a92ed9";
}

pub struct MusicBrainz {
    client: Client,
    rate_limit: Arc<Semaphore>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbRecordingRel {
    pub id: String,
    pub first_release_date: Option<String>,
    pub title: String,
    pub isrcs: Vec<String>,
    pub video: bool,
    pub disambiguation: String,
    pub length: Option<u32>,
    pub relations: Vec<MbRelation>,
    pub artist_credit: Vec<MbArtistCredit>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbArtistRel {
    pub id: String,
    pub isnis: Vec<String>,
    pub ipis: Vec<String>,
    pub name: String,
    pub disambiguation: String,
    pub country: Option<String>,
    pub sort_name: String,
    pub gender_id: Option<String>,
    pub area: Option<MbArea>,
    pub begin_area: Option<MbArea>,
    pub end_area: Option<MbArea>,
    pub life_span: MbTimespan,
    pub relations: Vec<MbRelation>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbArtistCredit {
    pub name: String,
    pub artist: MbArtist,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbRelation {
    pub direction: String,
    pub r#type: String,
    pub type_id: String,
    pub begin: Option<String>,
    pub end: Option<String>,
    pub ended: bool,
    pub target_type: String,
    pub target_credit: String,
    pub source_credit: String,
    pub attributes: Vec<String>,
    pub attribute_ids: BTreeMap<String, String>,
    pub attribute_values: BTreeMap<String, String>,

    pub work: Option<MbWork>,
    pub artist: Option<MbArtist>,
    pub url: Option<MbUrl>,
    pub recording: Option<MbRecording>,
    pub series: Option<MbSeries>,
    pub event: Option<MbEvent>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbSeries {
    pub id: String,
    pub r#type: Option<String>,
    pub type_id: Option<String>,
    pub name: String,
    pub disambiguation: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbRecording {
    pub id: String,
    pub title: String,
    #[serde(default)]
    pub isrcs: Vec<String>,
    pub video: bool,
    pub disambiguation: String,
    pub length: Option<u32>,
    #[serde(default)]
    pub artist_credit: Vec<MbArtistCredit>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbWork {
    pub id: String,
    pub r#type: Option<String>,
    pub type_id: Option<String>,
    pub languages: Vec<String>,
    pub iswcs: Vec<String>,
    pub language: Option<String>,
    pub title: String,
    pub attributes: Vec<String>,
    pub disambiguation: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbEvent {
    pub id: String,
    pub r#type: Option<String>,
    pub type_id: Option<String>,
    pub name: String,
    pub time: String,
    pub cancelled: bool,
    pub setlist: String,
    pub life_span: MbTimespan,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbArtist {
    pub id: String,
    pub r#type: Option<String>,
    pub type_id: Option<String>,
    pub name: String,
    pub disambiguation: String,
    pub country: Option<String>,
    pub sort_name: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbTimespan {
    pub begin: Option<String>,
    pub end: Option<String>,
    pub ended: bool,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbArea {
    pub name: String,
    pub sort_name: String,
    #[serde(default)]
    pub iso_3166_1_codes: Vec<String>,
    pub id: String,
    pub disambiguation: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MbUrl {
    pub id: String,
    pub resource: String,
}

impl Default for MusicBrainz {
    fn default() -> Self {
        Self::new()
    }
}

impl MusicBrainz {
    const MAX_PAR_REQ: usize = 4;
    pub fn new() -> 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,
            // send at most 1 req/s according to musicbrainz docs, each lock is held for 10s
            // this implementation also never sends more than MAX_PAR_REQ requests in-flight.
            rate_limit: Arc::new(Semaphore::new(Self::MAX_PAR_REQ)),
        }
    }

    pub fn lookup_recording(&self, id: String, rt: &Handle) -> Result<Arc<MbRecordingRel>> {
        cache_memory(&format!("ext/musicbrainz/recording/{id}.json"), move || {
            rt.block_on(async {
                let _permit = self.rate_limit.clone().acquire_owned().await?;
                let permit_drop_ts = Instant::now() + Duration::from_secs(Self::MAX_PAR_REQ as u64);
                info!("recording lookup: {id}");

                let inc = [
                    "isrcs",
                    "artists",
                    "area-rels",
                    "artist-rels",
                    "event-rels",
                    "genre-rels",
                    "instrument-rels",
                    "label-rels",
                    "place-rels",
                    "recording-rels",
                    "release-rels",
                    "release-group-rels",
                    "series-rels",
                    "url-rels",
                    "work-rels",
                ]
                .join("+");

                let resp = self
                    .client
                    .get(format!(
                        "https://musicbrainz.org/ws/2/recording/{id}?inc={inc}"
                    ))
                    .send()
                    .await?
                    .error_for_status()?
                    .json::<MbRecordingRel>()
                    .await?;

                tokio::task::spawn(async move {
                    sleep_until(permit_drop_ts).await;
                    drop(_permit);
                });

                Ok(resp)
            })
        })
        .context("musicbrainz recording lookup")
    }

    pub fn lookup_artist(&self, id: String, rt: &Handle) -> Result<Arc<MbArtistRel>> {
        cache_memory(&format!("ext/musicbrainz/artist/{id}.json"), move || {
            rt.block_on(async {
                let _permit = self.rate_limit.clone().acquire_owned().await?;
                let permit_drop_ts = Instant::now() + Duration::from_secs(Self::MAX_PAR_REQ as u64);
                info!("artist lookup: {id}");

                let inc = [
                    "area-rels",
                    "artist-rels",
                    "event-rels",
                    "genre-rels",
                    "instrument-rels",
                    "label-rels",
                    "place-rels",
                    "recording-rels",
                    "release-rels",
                    "release-group-rels",
                    "series-rels",
                    "url-rels",
                    "work-rels",
                ]
                .join("+");

                let resp = self
                    .client
                    .get(format!(
                        "https://musicbrainz.org/ws/2/artist/{id}?inc={inc}"
                    ))
                    .send()
                    .await?
                    .error_for_status()?
                    .json::<MbArtistRel>()
                    .await?;

                tokio::task::spawn(async move {
                    sleep_until(permit_drop_ts).await;
                    drop(_permit);
                });

                Ok(resp)
            })
        })
        .context("musicbrainz artist lookup")
    }
}

impl ImportPlugin for MusicBrainz {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "musicbrainz",
            ..Default::default()
        }
    }
}
