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

import type { Track } from "@applets/core/types.d.ts";
import type { Server } from "./types.d.ts";
import { SCHEME } from "./constants.ts";
import { loadServers, serverId } from "./common.ts";
import { expose } from "../../../scripts/common.ts";

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

export type Actions = typeof actions;

// Actions

async function consult(fileUriOrScheme: string) {
  // TODO: Check if server is available + CORS works?
  return { supported: true };
}

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

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

  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 server = parseURI(uri);
  if (!server) return undefined;

  const client = createClient(server);
  const parsedURI = URI.parse(uri);
  const qs = QS.parse(parsedURI.query || "");

  const songId = typeof qs.songId === "string" ? qs.songId : undefined;
  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 };
}

////////////////////////////////////////////
// 🛠️
////////////////////////////////////////////
function autoTypeToTrackKind(type: Child["type"]): Track["kind"] {
  switch (type?.toLowerCase()) {
    case "audiobook":
      return "audiobook";

    case "music":
      return "music";

    case "podcast":
      return "podcast";

    default:
      return "miscellaneous";
  }
}

function buildURI(server: Server, args: { songId: string; path?: string }) {
  return URI.serialize({
    scheme: SCHEME,
    userinfo: server.apiKey
      ? URI.escapeComponent(server.apiKey)
      : `${URI.escapeComponent(server.username || "")}:${URI.escapeComponent(server.password || "")}`,
    host: server.host.replace(/^https?:\/\//, ""),
    path: args.path,
    query: QS.stringify({
      songId: args.songId,
      tls: server.tls ? "t" : "f",
    }),
  });
}

function createClient(server: Server) {
  return new SubsonicAPI({
    url: `http${server.tls ? "s" : ""}://${server.host}`,
    auth: server.apiKey
      ? { apiKey: URI.unescapeComponent(server.apiKey) }
      : {
          username: URI.unescapeComponent(server.username || ""),
          password: URI.unescapeComponent(server.password || ""),
        },
  });
}

function parseURI(uriString: string): Server | undefined {
  const uri = URI.parse(uriString);
  if (uri.scheme !== SCHEME) return undefined;
  if (!uri.host) return undefined;

  let apiKey: string | undefined = undefined;
  let username: string | undefined = undefined;
  let password: string | undefined = undefined;

  if (uri.userinfo?.includes(":")) {
    // Username + Password
    const [u, p] = uri.userinfo.split(":");
    username = u;
    password = p;
    if (!username || !password) return undefined;
  } else {
    // API key
    apiKey = uri.userinfo;
    if (!apiKey) return undefined;
  }

  const qs = QS.parse(uri.query || "");

  return {
    apiKey,
    host: uri.port ? `${uri.host}:${uri.port}` : uri.host,
    password,
    tls: qs.tls === "f" ? false : true,
    username,
  };
}

function serversFromTracks(tracks: Track[]) {
  return tracks.reduce((acc: Record<string, Server>, track: Track) => {
    const server = parseURI(track.uri);
    if (!server) return acc;

    const id = serverId(server);
    if (acc[id]) return acc;

    return { ...acc, [id]: server };
  }, {});
}
