import * as odd from '@oddjs/odd';
import { getRecoil, setRecoil } from 'recoil-nexus';
import type PublicFile from '@oddjs/odd/fs/v1/PublicFile';
import type PrivateFile from '@oddjs/odd/fs/v1/PrivateFile';
import { isFile } from '@oddjs/odd/fs/types/check';

import { filesystemStore } from '../../../stores';
import { collectionStore, AREAS } from '../stores';
import { addNotification } from '../../../lib/notifications';
import { fileToUint8Array } from '../../../lib/utils';

export type PlaylistSourceType =
  | 'apple'
  | 'spotify'
  | 'youtube'
  | 'tidal'
  | 'deezer'
  | 'bandcamp'
  | 'soundcloud'
  | 'other';

export type PlaylistSource = {
  type: PlaylistSourceType;
  url: string;
};

export type Playlist = {
  cid: string;
  ctime: number;
  name: string;
  private: boolean;
  size: number;
  src: string;
  cover?: string;
  sources?: PlaylistSource[];
};

export type COLLECTION = {
  publicPlaylists: Playlist[];
  privatePlaylists: Playlist[];
  selectedArea: AREAS;
  loading: boolean;
};

type Link = {
  size: number;
};

export const COLLECTION_DIRS = {
  [AREAS.PUBLIC]: odd.path.directory('public', 'collection'),
  [AREAS.PRIVATE]: odd.path.directory('private', 'collection'),
};

const FILE_SIZE_LIMIT = 20;

/**
 * Get playlists from the user's WNFS and construct the `src` value for the playlists
 */

export const getPlaylistsFromWNFS: () => Promise<void> = async () => {
  const collection = getRecoil(collectionStore);
  const fs = getRecoil(filesystemStore);
  if (!fs) return;

  try {
    // Set loading: true on the collectionStore
    setRecoil(collectionStore, { ...collection, loading: true });

    const { selectedArea } = collection;
    const isPrivate = selectedArea === AREAS.PRIVATE;

    // Set path to either private or public gallery dir
    const path = COLLECTION_DIRS[selectedArea];

    // Get list of links for files in the gallery dir
    const links = await fs.ls(path);

    let playlists = await Promise.all(
      Object.entries(links).map(async ([name]) => {
        const file = await fs.get(
          odd.path.combine(
            COLLECTION_DIRS[selectedArea],
            odd.path.file(`${name}`)
          )
        );

        if (!isFile(file)) return null;

        // The CID for private files is currently located in `file.header.content`,
        // whereas the CID for public files is located in `file.cid`
        const cid = isPrivate
          ? (file as PrivateFile).header.content.toString()
          : (file as PublicFile).cid.toString();

        // Create a blob to use as the playlist `src`
        const blob = new Blob([file.content]);
        const src = URL.createObjectURL(blob);

        const ctime = isPrivate
          ? (file as PrivateFile).header.metadata.unixMeta.ctime
          : (file as PublicFile).header.metadata.unixMeta.ctime;

        return {
          cid,
          ctime,
          name,
          private: isPrivate,
          size: (links[name] as Link).size,
          src,
        };
      })
    );

    // Sort playlists by ctime(created at date)
    // NOTE: this will eventually be controlled via the UI
    playlists = playlists.filter((a) => !!a);
    playlists.sort((a, b) => b.ctime - a.ctime);

    // Push playlists to the collectionStore
    setRecoil(collectionStore, {
      ...collection,
      ...(isPrivate
        ? {
            privatePlaylists: playlists,
          }
        : {
            publicPlaylists: playlists,
          }),
      loading: false,
    });
  } catch (error) {
    setRecoil(collectionStore, {
      ...collection,
      loading: false,
    });
  }
};

/**
 * Upload a playlist to the user's private or public WNFS
 * @param playlist
 */

export const uploadPlaylistToWNFS: (playlist: File) => Promise<void> = async (
  playlist
) => {
  const collection = getRecoil(collectionStore);
  const fs = getRecoil(filesystemStore);
  if (!fs) return;

  try {
    const { selectedArea } = collection;

    // Reject files over 20MB
    const playlistSizeInMB = playlist.size / (1024 * 1024);
    if (playlistSizeInMB > FILE_SIZE_LIMIT) {
      throw new Error('Playlist can be no larger than 20MB');
    }

    // Reject the upload if the playlist already exists in the directory
    const playlistExists = await fs.exists(
      odd.path.combine(
        COLLECTION_DIRS[selectedArea],
        odd.path.file(playlist.name)
      )
    );
    if (playlistExists) {
      throw new Error(`${playlist.name} playlist already exists`);
    }

    // Create a sub directory and add some content
    await fs.write(
      odd.path.combine(
        COLLECTION_DIRS[selectedArea],
        odd.path.file(playlist.name)
      ),
      await fileToUint8Array(playlist)
    );

    // Announce the changes to the server
    await fs.publish();

    addNotification({
      msg: `${playlist.name} playlist has been published`,
      type: 'success',
    });
  } catch (error) {
    addNotification({ msg: (error as Error).message, type: 'error' });
    console.error(error);
  }
};

/**
 * Delete a playlist from the user's private or public WNFS
 * @param name
 */
export const deletePlaylistFromWNFS: (name: string) => Promise<void> = async (
  name
) => {
  const collection = getRecoil(collectionStore);
  const fs = getRecoil(filesystemStore);
  if (!fs) return;

  try {
    const { selectedArea } = collection;

    const playlistExists = await fs.exists(
      odd.path.combine(COLLECTION_DIRS[selectedArea], odd.path.file(name))
    );

    if (playlistExists) {
      // Remove playlists from server
      await fs.rm(
        odd.path.combine(COLLECTION_DIRS[selectedArea], odd.path.file(name))
      );

      // Announce the changes to the server
      await fs.publish();

      addNotification({
        msg: `${name} playlist has been deleted`,
        type: 'success',
      });

      // Refetch playlists and update collectionStore
      await getPlaylistsFromWNFS();
    } else {
      throw new Error(`${name} playlist has already been deleted`);
    }
  } catch (error) {
    addNotification({ msg: (error as Error).message, type: 'error' });
    console.error(error);
  }
};

/**
 * Handle uploads made by interacting with the file input directly
 */
export const handleFileInput: (
  files: FileList | null
) => Promise<void> = async (files) => {
  if (!files) return;

  await Promise.all(
    Array.from(files).map(async (file) => {
      await uploadPlaylistToWNFS(file);
    })
  );

  // Refetch playlists and update collectionStore
  await getPlaylistsFromWNFS();
};