import * as uint8arrays from 'uint8arrays';
import type FileSystem from '@oddjs/odd/fs/index';
import { sha256 } from '@oddjs/odd/components/crypto/implementation/browser';
import { publicKeyToDid } from '@oddjs/odd/did/transformers';
import type { Crypto } from '@oddjs/odd';
import { getRecoil, setRecoil } from 'recoil-nexus';

import { filesystemStore, sessionStore } from '../../stores';
import { AREAS } from '../../routes/playlists/stores';
import { COLLECTION_DIRS } from '../../routes/playlists/lib/playlists';
import { ACCOUNT_SETTINGS_DIR } from '../account-settings';
import { asyncDebounce } from '../utils';
import { getBackupStatus } from './backup';

export const USERNAME_STORAGE_KEY = 'fullUsername';

export enum RECOVERY_STATES {
  Ready = 0,
  Processing = 1,
  Error = 2,
  Done = 3,
}

export const isUsernameValid = async (username: string): Promise<boolean> => {
  const session = getRecoil(sessionStore);
  return session.authStrategy.isUsernameValid(username);
};

export const isUsernameAvailable = async (
  username: string
): Promise<boolean> => {
  const session = getRecoil(sessionStore);
  return session.authStrategy.isUsernameAvailable(username);
};

export const debouncedIsUsernameAvailable = asyncDebounce(
  isUsernameAvailable,
  300
);

/**
 * Create additional directories and files needed by the app
 *
 * @param fs FileSystem
 */
const initializeFilesystem = async (fs: FileSystem): Promise<void> => {
  await fs.mkdir(COLLECTION_DIRS[AREAS.PUBLIC]);
  await fs.mkdir(COLLECTION_DIRS[AREAS.PRIVATE]);
  await fs.mkdir(ACCOUNT_SETTINGS_DIR);
};

export const createDID = async (
  crypto: Crypto.Implementation
): Promise<string> => {
  const pubKey = await crypto.keystore.publicExchangeKey();
  const ksAlg = await crypto.keystore.getAlgorithm();

  return publicKeyToDid(crypto, pubKey, ksAlg);
};

export const prepareUsername = async (username: string): Promise<string> => {
  const normalizedUsername = username.normalize('NFD');
  const hashedUsername = await sha256(
    new TextEncoder().encode(normalizedUsername)
  );

  return uint8arrays.toString(hashedUsername, 'base32').slice(0, 32);
};

export const register = async (hashedUsername: string): Promise<boolean> => {
  const originalSession = getRecoil(sessionStore);
  const {
    authStrategy,
    program: {
      components: { storage },
    },
  } = originalSession;
  const { success } = await authStrategy.register({ username: hashedUsername });

  if (!success) return success;

  const session = await authStrategy.session();
  setRecoil(filesystemStore, session.fs);

  // TODO Remove if only public and private directories are needed
  await initializeFilesystem(session.fs);

  const fullUsername = (await storage.getItem(USERNAME_STORAGE_KEY)) as string;

  setRecoil(sessionStore, {
    ...originalSession,
    username: {
      full: fullUsername,
      hashed: hashedUsername,
      trimmed: fullUsername.split('#')[0],
    },
    session,
  });

  return success;
};

export const loadAccount = async (
  hashedUsername: string,
  fullUsername: string
): Promise<void> => {
  const originalSession = getRecoil(sessionStore);
  const {
    authStrategy,
    program: {
      components: { storage },
    },
  } = originalSession;
  const session = await authStrategy.session();

  setRecoil(filesystemStore, session.fs);

  const backupStatus = await getBackupStatus(session.fs);

  await storage.setItem(USERNAME_STORAGE_KEY, fullUsername);

  setRecoil(sessionStore, {
    ...originalSession,
    username: {
      full: fullUsername,
      hashed: hashedUsername,
      trimmed: fullUsername.split('#')[0],
    },
    session,
    backupCreated: !!backupStatus?.created,
  });
};