import { SubsonicAPI, type Child } from "subsonic-api";
import * as URI from "uri-js";
import QS from "query-string";

import type { Consult, ConsultGrouping, GroupConsult, Track } from "@applets/core/types.d.ts";
import { SCHEME } from "./constants.ts";
import {
  autoTypeToTrackKind,
  buildURI,
  consultServer,
  createClient,
  groupTracksByServer,
  loadServers,
  parseURI,
  serverId,
  serversFromTracks,
} from "./common.ts";
import { expose } from "../../../scripts/common.ts";

////////////////////////////////////////////
// ACTIONS
////////////////////////////////////////////
const actions = expose({
  consult,
  contextualize,
  groupConsult,
  list,
  resolve,
});

export type Actions = typeof actions;

// Actions

async function consult(fileUriOrScheme: string): Promise<Consult> {
  if (!fileUriOrScheme.includes(":")) return { supported: true, consult: "undetermined" };

  const parsed = parseURI(fileUriOrScheme);
  if (!parsed) return { supported: true, consult: "undetermined" };

  const consult = await consultServer(parsed.server);
  return { supported: true, consult };
}

async function contextualize(tracks: Track[]) {
  return serversFromTracks(tracks);
}

async function groupConsult(tracks: Track[]): Promise<GroupConsult> {
  const groups = groupTracksByServer(tracks);

  const promises = Object.entries(groups).map(async ([serverId, { server, tracks }]) => {
    const available = await consultServer(server);
    const grouping: ConsultGrouping = available
      ? { available, tracks }
      : { available, reason: "Server ping failed" };

    return {
      key: `${SCHEME}:${serverId}`,
      grouping,
    };
  });

  const entries = (await Promise.all(promises)).map((entry) => [entry.key, entry.grouping]);
  return Object.fromEntries(entries);
}

async function list(cachedTracks: Track[] = []) {
  const cache = cachedTracks.reduce((acc: Record<string, Record<string, Track>>, t: Track) => {
    const parsed = parseURI(t.uri);
    if (!parsed || !parsed.path) return acc;

    const bid = serverId(parsed?.server);
    const trk = { [parsed.path]: t };

    return { ...acc, [bid]: acc[bid] ? { ...acc[bid], ...trk } : trk };
  }, {});

  // TODO

  async function search(client: SubsonicAPI, offset = 0): Promise<Child[]> {
    const result = await client.search3({
      query: "",
      artistCount: 0,
      albumCount: 0,
      songCount: 1000,
      songOffset: offset,
    });

    const songs = result.searchResult3.song || [];

    if (songs.length === 1000) {
      const moreSongs = await search(client, offset + 1000);
      return [...songs, ...moreSongs];
    }

    return songs;
  }

  const servers = await loadServers();
  const promises = Object.values(servers).map(async (server) => {
    const client = createClient(server);
    const list = await search(client, 0);

    return list
      .filter((song) => !song.isVideo)
      .map((song) => {
        if (song.path && cache[song.path]) return cache[song.path];

        const track: Track = {
          id: crypto.randomUUID(),
          kind: autoTypeToTrackKind(song.type),
          uri: buildURI(server, { songId: song.id, path: song.path }),

          stats: {
            bitrate: song.bitRate,
            duration: song.duration,
          },
          tags: {
            album: song.album,
            artist: song.artist,
            disc: { no: song.discNumber || 1 },
            genre: song.genre,
            title: song.title,
            track: { no: song.track || 1 },
            year: song.year,
          },
        };

        return track;
      });
  });

  return (await Promise.all(promises)).flat(1);
}

async function resolve({ uri }: { method: string; uri: string }) {
  const parsed = parseURI(uri);
  if (!parsed) return undefined;

  const client = createClient(parsed.server);
  const songId = parsed.songId;
  if (!songId) return undefined;

  // TODO:
  // const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days
  // const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds;

  const url = await client
    .download({
      id: songId,
      format: "raw",
    })
    .then((a) => a.blob())
    .then((blob) => URL.createObjectURL(blob));

  // NOTE:
  // First idea was to get the URL for the download and use that instead.
  // Problem is, more often than not, servers don't allow for CORS Range requests,
  // so it's basically useless.

  return { expiresAt: Infinity, url };
}
