/*
    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};
use jellycache::{HashKey, cache_memory};
use jellycommon::{IdentifierType, NodeID};
use jellyremuxer::matroska::Segment;
use log::info;
use reqwest::{
    Client, ClientBuilder,
    header::{HeaderMap, HeaderName, HeaderValue},
};
use serde::{Deserialize, Serialize};
use std::{
    io::Read,
    path::Path,
    process::{Command, Stdio},
    sync::Arc,
    time::Duration,
};
use tokio::{
    runtime::Handle,
    sync::Semaphore,
    time::{Instant, sleep_until},
};

pub(crate) struct AcoustID {
    client: Client,
    key: String,
    rate_limit: Arc<Semaphore>,
}

#[derive(Debug, Hash, Clone, Serialize, Deserialize)]
pub(crate) struct Fingerprint {
    duration: u32,
    fingerprint: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct FpCalcOutput {
    duration: f32,
    fingerprint: String,
}

#[derive(Serialize, Deserialize)]
pub(crate) struct AcoustIDLookupResultRecording {
    id: String,
}
#[derive(Serialize, Deserialize)]
pub(crate) struct AcoustIDLookupResult {
    id: String,
    score: f32,
    #[serde(default)]
    recordings: Vec<AcoustIDLookupResultRecording>,
}
#[derive(Serialize, Deserialize)]
pub(crate) struct AcoustIDLookupResponse {
    status: String,
    results: Vec<AcoustIDLookupResult>,
}

impl AcoustID {
    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,
            // send at most 3 req/s according to acoustid docs, each lock is therefore held for 1s
            // this implementation also never sends more than 3 requests in-flight.
            rate_limit: Arc::new(Semaphore::new(3)),
            key: api_key.to_owned(),
        }
    }

    pub fn get_atid_mbid(&self, fp: &Fingerprint, rt: &Handle) -> Result<Option<(String, String)>> {
        let res = self.lookup(fp.to_owned(), rt)?;
        for r in &res.results {
            if let Some(k) = r.recordings.first() {
                return Ok(Some((r.id.clone(), k.id.clone())));
            }
        }
        Ok(None)
    }

    pub fn lookup(&self, fp: Fingerprint, rt: &Handle) -> Result<Arc<AcoustIDLookupResponse>> {
        cache_memory(&format!("ext/acoustid/{}.json", HashKey(&fp)) , move || rt.block_on(async {
            let _permit = self.rate_limit.clone().acquire_owned().await?;
            let permit_drop_ts = Instant::now() + Duration::SECOND;
            info!("acoustid lookup");

            let duration = fp.duration;
            let fingerprint = fp.fingerprint.replace("=", "%3D");
            let client = &self.key;
            let body = format!("format=json&meta=recordingids&client={client}&duration={duration}&fingerprint={fingerprint}");

            let resp = self
                .client
                .post("https://api.acoustid.org/v2/lookup".to_string())
                .header("Content-Type", "application/x-www-form-urlencoded")
                .body(body)
                .send()
                .await?.error_for_status()?.json::<AcoustIDLookupResponse>().await?;

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

            Ok(resp)
        }))
        .context("acoustid lookup")
    }
}

pub(crate) fn acoustid_fingerprint(path: &Path) -> Result<Arc<Fingerprint>> {
    cache_memory(
        &format!("media/chromaprint/{}.json", HashKey(path)),
        move || {
            let child = Command::new("fpcalc")
                .arg("-json")
                .arg(path)
                .stdout(Stdio::piped())
                .spawn()
                .context("fpcalc")?;

            let mut buf = Vec::new();
            child
                .stdout
                .unwrap()
                .read_to_end(&mut buf)
                .context("read fpcalc output")?;

            let out: FpCalcOutput =
                serde_json::from_slice(&buf).context("parsing fpcalc output")?;
            let out = Fingerprint {
                duration: out.duration as u32,
                fingerprint: out.fingerprint,
            };
            Ok(out)
        },
    )
}

impl ImportPlugin for AcoustID {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "acoustid",
            handle_media: true,
            ..Default::default()
        }
    }
    fn media(&self, ct: &ImportContext, node: NodeID, path: &Path, _seg: &Segment) -> Result<()> {
        if !ct.iflags.use_acoustid {
            return Ok(());
        }
        let fp = acoustid_fingerprint(path)?;
        if let Some((atid, mbid)) = self.get_atid_mbid(&fp, &ct.rt)? {
            ct.db.update_node_init(node, |n| {
                n.identifiers.insert(IdentifierType::AcoustIdTrack, atid);
                n.identifiers
                    .insert(IdentifierType::MusicbrainzRecording, mbid);
            })?;
        };
        Ok(())
    }
}
