/* eslint-disable no-await-in-loop */
import { readFile, unlink } from 'fs/promises';
import { z } from 'zod';
import { addTrackIdsToUser, getCloseTrackId, storeFirstListenedAtIfLess } from '../../database';
import { setImporterStateCurrent } from '../../database/queries/importer';
import { RecentlyPlayedTrack, SpotifyTrack } from '../../database/schemas/track';
import { User } from '../../database/schemas/user';
import { saveMusics } from '../../spotify/dbTools';
import { logger } from '../logger';
import { beforeParenthesis, minOfArray, removeDiacritics, retryPromise } from '../misc';
import { squeue } from '../queue';
import { Unpack } from '../types';
import { getFromCache, setToCache } from './cache';
import { HistoryImporter, ImporterStateTypes, PrivacyImporterState } from './types';

const privacyFileSchema = z.array(
  z.object({
    endTime: z.string(),
    artistName: z.string(),
    trackName: z.string(),
    msPlayed: z.number(),
  }),
);

export type PrivacyItem = Unpack<z.infer<typeof privacyFileSchema>>;

export class PrivacyImporter implements HistoryImporter<ImporterStateTypes.privacy> {
  private id: string;

  private userId: string;

  private elements: PrivacyItem[] | null;

  private currentItem: number;

  constructor(user: User) {
    this.id = '';
    this.userId = user._id.toString();
    this.elements = null;
    this.currentItem = 0;
  }

  static search = async (userId: string, track: string, artist: string) => {
    const res = await retryPromise(
      () =>
        squeue.queue(
          (client) =>
            client.get(
              `/search?q=track:${encodeURIComponent(track)}+artist:${encodeURIComponent(
                artist,
              )}&type=track&limit=10`,
            ),
          userId,
        ),
      10,
      30,
    );
    return res.data.tracks.items[0] as SpotifyTrack;
  };

  storeItems = async (userId: string, items: RecentlyPlayedTrack[]) => {
    await saveMusics(
      userId,
      items.map((it) => it.track),
    );
    const finalInfos: { played_at: Date; id: string }[] = [];
    for (let i = 0; i < items.length; i += 1) {
      const item = items[i];
      const date = new Date(`${item.played_at}Z`);
      const duplicate = await getCloseTrackId(this.userId.toString(), item.track.id, date, 60);
      if (duplicate.length > 0) {
        logger.info(`${item.track.name} - ${item.track.artists[0].name} was duplicate`);
        continue;
      }
      finalInfos.push({
        played_at: date,
        id: item.track.id,
      });
    }
    await setImporterStateCurrent(this.id, this.currentItem + 1);
    await addTrackIdsToUser(this.userId.toString(), finalInfos);
    const min = minOfArray(finalInfos, (info) => info.played_at.getTime());
    if (min) {
      await storeFirstListenedAtIfLess(this.userId, finalInfos[min.minIndex].played_at);
    }
  };

  initWithJSONContent = async (content: any[]) => {
    try {
      const validations = privacyFileSchema.parse(content);
      this.elements = validations;
      return content;
    } catch (e) {
      logger.error(e);
    }
    return null;
  };

  initWithFiles = async (filePaths: string[]) => {
    const files = await Promise.all(filePaths.map((f) => readFile(f)));
    const filesContent = files.map((f) => JSON.parse(f.toString()));

    const totalContent = filesContent.reduce<PrivacyItem[]>((acc, curr) => {
      acc.push(...curr);
      return acc;
    }, []);

    if (!this.initWithJSONContent(totalContent)) {
      return false;
    }

    return true;
  };

  init = async (existingState: PrivacyImporterState | null, filePaths: string[]) => {
    try {
      this.currentItem = existingState?.current ?? 0;
      const success = await this.initWithFiles(filePaths);
      if (success) {
        return { total: this.elements!.length };
      }
    } catch (e) {
      logger.error(e);
    }
    return null;
  };

  run = async (id: string) => {
    this.id = id;
    let items: RecentlyPlayedTrack[] = [];
    if (!this.elements) {
      return false;
    }
    for (let i = this.currentItem; i < this.elements.length; i += 1) {
      this.currentItem = i;
      const content = this.elements[i];
      if (content.msPlayed < 30 * 1000) {
        // If track was played for less than 30 seconds
        logger.info(
          `Track ${content.trackName} - ${
            content.artistName
          } was passed, only listened for ${Math.floor(content.msPlayed / 1000)} seconds`,
        );
        continue;
      }
      let item = getFromCache(this.userId.toString(), content.trackName, content.artistName);
      if (!item) {
        item = await PrivacyImporter.search(
          this.userId,
          removeDiacritics(content.trackName),
          removeDiacritics(content.artistName),
        );
      }
      if (!item) {
        item = await PrivacyImporter.search(
          this.userId,
          removeDiacritics(beforeParenthesis(content.trackName)),
          removeDiacritics(beforeParenthesis(content.artistName)),
        );
      }
      if (!item) {
        logger.warn(`${content.trackName} by ${content.artistName} was not found by search`);
        continue;
      }
      setToCache(this.userId.toString(), content.trackName, content.artistName, item);
      logger.info(
        `Adding ${item.name} - ${item.artists[0].name} from data (${i}/${this.elements.length})`,
      );
      items.push({ track: item, played_at: content.endTime });
      if (items.length >= 20) {
        await this.storeItems(this.userId, items);
        items = [];
      }
    }
    if (items.length > 0) {
      await this.storeItems(this.userId, items);
      items = [];
    }
    return true;
  };

  // eslint-disable-next-line class-methods-use-this
  cleanup = async (filePaths: string[]) => {
    await Promise.all(filePaths.map((f) => unlink(f)));
  };
}