import type { IPicture } from "music-metadata";
import * as IDB from "idb-keyval";

import type { Actions as MetadataActions } from "../metadata/worker";
import type { Artwork, ArtworkRequest } from "./types";
import { expose } from "@scripts/common";
import { IDB_ARTWORK_PREFIX } from "./constants";
import { createEndpoint } from "@remote-ui/rpc";

// State
let queue: ArtworkRequest[] = [];

// Metadata worker
const metadataWorker = createEndpoint<MetadataActions>(
  new Worker(new URL("../metadata/worker", import.meta.url), { type: "module" }),
);

////////////////////////////////////////////
// ACTIONS
////////////////////////////////////////////
const actions = expose({
  artwork,
  supply,
});

export type Actions = typeof actions;

// Actions

function artwork(request: ArtworkRequest) {
  return processRequest(request);
}

function supply(items: ArtworkRequest[]) {
  const exe = !queue[0];
  queue = [...queue, ...items];
  if (exe) shiftQueue();
}

////////////////////////////////////////////
// 🛠️
////////////////////////////////////////////
async function lastFm(req: ArtworkRequest): Promise<Artwork[]> {
  if (!navigator.onLine) return [];

  const query = req.tags?.artist;

  return await fetch(
    `https://ws.audioscrobbler.com/2.0/?method=album.search&album=${query}&api_key=4f0fe85b67baef8bb7d008a8754a95e5&format=json`,
  )
    .then((r) => r.json())
    .then((r) => lastFmCover(r.results.albummatches.album));
}

function lastFmCover(remainingMatches: any[]): Promise<Artwork[]> {
  const album = remainingMatches[0];
  const url = album ? album.image[album.image.length - 1]["#text"] : null;

  return url && url !== ""
    ? fetch(url)
        .then((r) => r.blob())
        .then(async (b) => [{ bytes: await b.bytes(), mime: b.type }])
        .catch((_) => lastFmCover(remainingMatches.slice(1)))
    : album && lastFmCover(remainingMatches.slice(1));
}

async function musicBrainz(req: ArtworkRequest): Promise<Artwork[]> {
  const artist = req.tags?.artist;
  const album = req.tags?.album;

  if (!navigator.onLine) return [];
  if (!album && !artist) return [];

  // TODO
  const variousArtists = false;

  const query = `release:"${album}"` + (variousArtists ? `` : ` AND artist:"${artist}"`);
  const encodedQuery = encodeURIComponent(query);

  return await fetch(`https://musicbrainz.org/ws/2/release/?query=${encodedQuery}&fmt=json`)
    .then((r) => r.json())
    .then((r) => musicBrainzCover(r.releases));
}

async function musicBrainzCover(remainingReleases: any[]): Promise<Artwork[]> {
  const release = remainingReleases[0];
  if (!release) return [];

  return await fetch(`https://coverartarchive.org/release/${release.id}/front-500`)
    .then((r) => r.blob())
    .then(async (b) => {
      if (b && b.type.startsWith("image/")) {
        return [{ bytes: await b.bytes(), mime: b.type }];
      } else {
        return musicBrainzCover(remainingReleases.slice(1));
      }
    })
    .catch(() => musicBrainzCover(remainingReleases.slice(1)));
}

async function processRequest(req: ArtworkRequest): Promise<Artwork[]> {
  // Check if already processed
  // TODO: Retry if none was found?
  const cache = await IDB.get(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`);
  if (cache) return cache;

  // 🚀
  let art: Artwork[] = [];

  // Get metadata + possible artwork from file metadata
  const meta = await metadataWorker.call.supply({ ...req, includeArtwork: true });
  if (!req.tags) req.tags = meta.tags;

  // Add artwork from metadata
  const fromMeta =
    meta.artwork?.map((a: IPicture) => {
      return { bytes: a.data, mime: a.format };
    }) || [];

  art.push(...fromMeta);

  // If no artwork, try finding it on other sources
  if (art.length === 0) {
    const fromMusicBrainz = await musicBrainz(req);
    art.push(...fromMusicBrainz);
  }

  if (art.length === 0) {
    const fromLastFm = await lastFm(req);
    art.push(...fromLastFm);
  }

  // Save artwork to IDB
  await IDB.set(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`, art);

  // Fin
  return art;
}

async function shiftQueue() {
  const next = queue.shift();
  if (!next) return;

  await processRequest(next);
  await shiftQueue();
}
