// Utilities for building Beat Saber playlists server-side

export interface Difficulty {
  name: string;
  characteristic: string;
}

export interface Song {
  hash: string;
  difficulties: Difficulty[];
  key?: string;
  levelId?: string;
  songName?: string;
}

export interface CustomData {
  syncURL?: string | null;
  owner?: string | null;
  id?: string | null;
  hash?: string | null;
  shared?: boolean | null;
}

export interface Playlist {
  playlistTitle: string;
  songs: Song[];
  playlistAuthor?: string;
  image?: string | null;
  coverImage?: string | null;
  description?: string;
  allowDuplicates?: boolean;
  customData?: CustomData | null;
}

type StandardizedSongInput = {
  hash: string;
  difficulties?: Difficulty[];
  key?: string;
  levelId?: string;
  songName?: string;
};

interface PlaylistBuilderOptions {
  coversDir?: string;
  historyFile?: string;
  outputDir?: string;
}

export class PlaylistBuilder {
  private readonly coversDir: string;
  private readonly historyFile: string;
  private readonly outputDir: string;
  private history: { cover_history: string[] };

  constructor(options: PlaylistBuilderOptions = {}) {
    const cwd = this.processModule().cwd();
    this.coversDir = options.coversDir ?? this.pathModule().join(cwd, 'covers');
    this.historyFile = options.historyFile ?? this.pathModule().join(cwd, 'playlist_history.json');
    this.outputDir = options.outputDir ?? cwd;

    this.ensureDirectory(this.coversDir);
    this.ensureDirectory(this.outputDir);
    this.history = this.loadHistory();
    this.saveHistory();
  }

  async createPlaylist(
    playlistData: StandardizedSongInput[],
    playlistTitle: string = 'playlist',
    playlistAuthor: string = 'SaberList Tool'
  ): Promise<string> {
    const songs: Song[] = (playlistData ?? []).map((song) => ({
      hash: song.hash,
      difficulties: (song.difficulties ?? []).map((d) => ({ name: d.name, characteristic: d.characteristic })),
      key: song.key,
      levelId: song.levelId,
      songName: song.songName
    }));

    const coverPath = this.getRandomUnusedCover();
    const imageBase64 = coverPath ? await this.encodeImage(coverPath) : null;
    const imageDataUri = imageBase64 ? `data:image/png;base64,${imageBase64}` : null;

    const playlist: Playlist = {
      playlistTitle,
      playlistAuthor,
      songs,
      image: imageDataUri,
      coverImage: coverPath ?? null,
      description: `Playlist created by SaberList Tool on ${new Date().toISOString()}`,
      allowDuplicates: false,
      customData: {}
    };

    // Remove undefined fields recursively before writing
    const cleaned = this.removeUndefined(playlist);
    const filename = this.pathModule().join(
      this.outputDir,
      `${playlistTitle.replace(/\s+/g, '_')}.bplist`
    );
    await this.writeJsonFile(filename, cleaned);
    this.consoleModule().log(`Playlist created: ${filename}`);
    return filename;
  }

  async splitPlaylist(inputPlaylistPath: string, songsPerPlaylist: number = 50): Promise<string[]> {
    const fs = this.fsPromises();
    try {
      await fs.access(inputPlaylistPath);
    } catch {
      this.consoleModule().error(`Input playlist file not found: ${inputPlaylistPath}`);
      return [];
    }

    const raw = await fs.readFile(inputPlaylistPath, 'utf-8');
    const data = JSON.parse(raw) as Playlist;

    const playlistTitle = data.playlistTitle ?? 'Split Playlist';
    const playlistAuthor = data.playlistAuthor ?? 'SaberList Tool';
    const playlistDescription = data.description ?? '';
    const playlistImage = data.image ?? null;
    const playlistCoverImage = data.coverImage ?? null;
    const playlistCustomData = data.customData ?? null;

    const songs = (data.songs ?? []) as Song[];
    const totalSongs = songs.length;
    if (totalSongs === 0) {
      this.consoleModule().warn('No songs found in the input playlist.');
      return [];
    }

    const numPlaylists = Math.floor((totalSongs + songsPerPlaylist - 1) / songsPerPlaylist);
    const created: string[] = [];

    for (let i = 0; i < numPlaylists; i++) {
      const startIdx = i * songsPerPlaylist;
      const endIdx = Math.min((i + 1) * songsPerPlaylist, totalSongs);
      const subsetSongs = songs.slice(startIdx, endIdx);

      const subset: Playlist = {
        playlistTitle: `${playlistTitle} (${i + 1}/${numPlaylists})`,
        playlistAuthor,
        songs: subsetSongs,
        image: playlistImage,
        coverImage: playlistCoverImage,
        description: playlistDescription,
        allowDuplicates: false,
        customData: playlistCustomData
      };

      const cleaned = this.removeUndefined(subset);
      const filename = this.pathModule().join(
        this.outputDir,
        `${playlistTitle.replace(/\s+/g, '_')}_${i + 1}_${numPlaylists}.bplist`
      );
      await this.writeJsonFile(filename, cleaned);
      this.consoleModule().log(
        `Created split playlist: ${filename} with ${subsetSongs.length} songs`
      );
      created.push(filename);
    }

    return created;
  }

  // Internals
  private ensureDirectory(dir: string): void {
    const fs = this.fsModule();
    if (!fs.existsSync(dir)) {
      try {
        fs.mkdirSync(dir, { recursive: true });
        this.consoleModule().log(`Created directory: ${dir}`);
      } catch {
        // ignore
      }
    }
  }

  private loadHistory(): { cover_history: string[] } {
    const fs = this.fsModule();
    try {
      if (fs.existsSync(this.historyFile)) {
        const raw = fs.readFileSync(this.historyFile, 'utf-8');
        const parsed = JSON.parse(raw) as { cover_history?: string[] };
        return { cover_history: parsed.cover_history ?? [] };
      }
    } catch {
      // ignore
    }
    return { cover_history: [] };
  }

  private saveHistory(): void {
    const fs = this.fsModule();
    try {
      fs.writeFileSync(this.historyFile, JSON.stringify(this.history));
    } catch {
      // ignore
    }
  }

  private getRandomUnusedCover(): string | null {
    const fs = this.fsModule();
    const path = this.pathModule();
    let available: string[] = [];
    try {
      available = (fs.readdirSync(this.coversDir) as string[]).filter(
        (f) => f.endsWith('.jpg') && !this.history.cover_history.includes(f)
      );
    } catch {
      // ignore
    }

    if (!available || available.length === 0) {
      this.consoleModule().warn('No unused cover images available. Using no cover.');
      return null;
    }

    const selected = available[Math.floor(Math.random() * available.length)];
    this.history.cover_history.push(selected);
    this.saveHistory();
    return path.join(this.coversDir, selected);
  }

  private async encodeImage(imagePath: string): Promise<string> {
    const fs = this.fsPromises();
    const buf = await fs.readFile(imagePath);
    return buf.toString('base64');
  }

  private async writeJsonFile(pathname: string, data: unknown): Promise<void> {
    const fs = this.fsPromises();
    await fs.writeFile(pathname, JSON.stringify(data, null, 2));
  }

  private removeUndefined<T>(obj: T): T {
    if (obj === null || obj === undefined) return obj;
    if (Array.isArray(obj)) {
      return obj.map((v) => this.removeUndefined(v)) as unknown as T;
    }
    if (typeof obj === 'object') {
      const out: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
        if (v === undefined) continue;
        out[k] = this.removeUndefined(v as never);
      }
      return out as T;
    }
    return obj;
  }

  // Lazy Node built-ins for SSR friendliness
  private fsPromises() {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const fs = require('fs');
    return fs.promises as typeof import('fs').promises;
  }
  private fsModule() {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    return require('fs') as typeof import('fs');
  }
  private pathModule() {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    return require('path') as typeof import('path');
  }
  private processModule() {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    return require('process') as typeof import('process');
  }
  private consoleModule() {
    return console;
  }
}


