/**
 * Suno API Client
 *
 * Simple client for interacting with Suno's internal APIs:
 * - /api/generate/v2-web: Generate songs
 * - /api/feed/v3: Check song status
 */

export interface SongMetadata {
  id: string;
  title: string;
  artist?: string;
  audioUrl?: string;
  videoUrl?: string;
  albumArtUrl?: string;
  duration?: number;
  status: "pending" | "processing" | "streaming" | "completed" | "failed";
  createdAt: string;
  errorMessage?: string;
}

export interface GenerateSongRequest {
  prompt: string; // Lyrics/creative prompt for the song
  tags?: string; // Comma-separated genre/style tags (e.g., "lofi, chill, jazz")
  makeInstrumental?: boolean;
  waitAudio?: boolean;
  coverClipId?: string; // Optional: Suno clip ID to create a cover/remix of
  task?: "cover"; // Optional: Set to "cover" when creating a cover version
}

export interface GenerateSongResponse {
  clips: Array<{
    id: string;
    status: string;
    created_at: string;
  }>;
}

export interface FeedResponse {
  clips: Array<{
    id: string;
    title: string;
    display_name?: string;
    audio_url?: string;
    video_url?: string;
    image_url?: string;
    duration?: number;
    status: string;
    created_at: string;
    error_message?: string;
  }>;
}

export class SunoClient {
  private baseUrl: string;
  private sessionToken: string;

  constructor(config: { baseUrl: string; sessionToken: string }) {
    this.baseUrl = config.baseUrl;
    this.sessionToken = config.sessionToken;
  }

  /**
   * Generate songs using v2-web endpoint
   * Returns 2 song IDs that can be polled for status
   *
   * Important:
   * - 'prompt' should contain lyrics only
   * - 'tags' should contain genre/style descriptors
   * - 'mv' is set to 'chirp-crow' (the model version)
   * - 'coverClipId' if provided, creates a cover/remix in a different style
   */
  async generateSongs(request: GenerateSongRequest): Promise<string[]> {
    const body: any = {
      prompt: request.prompt,
      tags: request.tags,
      mv: "chirp-crow",
      make_instrumental: request.makeInstrumental ?? false,
      wait_audio: request.waitAudio ?? false,
    };

    // Add cover_clip_id if provided
    if (request.coverClipId) {
      body.cover_clip_id = request.coverClipId;
    }

    // Add task field if provided (e.g., "cover")
    if (request.task) {
      body.task = request.task;
    }

    const response = await fetch(`${this.baseUrl}/api/generate/v2-web/`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${this.sessionToken}`,
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(
        `Failed to generate songs: ${response.status} ${errorText}`
      );
    }

    const data: GenerateSongResponse = await response.json();
    return data.clips.map((clip) => clip.id);
  }

  /**
   * Check status of songs using feed v3 endpoint
   * Supports multiple IDs via ?ids=id1,id2,id3 query parameter
   */
  async getSongStatus(songIds: string[]): Promise<SongMetadata[]> {
    const idsParam = songIds.join(",");
    const response = await fetch(
      `${this.baseUrl}/api/feed/v2/?ids=${idsParam}`,
      {
        method: "GET",
        headers: {
          Authorization: `Bearer ${this.sessionToken}`,
        },
      }
    );

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(
        `Failed to fetch song status: ${response.status} ${errorText}`
      );
    }

    const data: FeedResponse = await response.json();

    return data.clips.map((clip) => this.mapClipToMetadata(clip));
  }

  /**
   * Poll for song completion
   * Returns when all songs are completed or failed
   */
  async waitForCompletion(
    songIds: string[],
    options: { maxAttempts?: number; pollIntervalMs?: number } = {}
  ): Promise<SongMetadata[]> {
    const maxAttempts = options.maxAttempts ?? 150; // 5 minutes with 2s intervals
    const pollIntervalMs = options.pollIntervalMs ?? 2000;

    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      const songs = await this.getSongStatus(songIds);

      const allComplete = songs.every(
        (song) => song.status === "completed" || song.status === "failed"
      );

      if (allComplete) {
        return songs;
      }

      await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
    }

    throw new Error("Song generation timed out");
  }

  /**
   * Increment play count for a song
   * Uses /api/gen/{gen_id}/increment_play_count/v2 endpoint
   */
  async incrementPlayCount(
    genId: string,
    sampleFactor: number = 1
  ): Promise<void> {
    const response = await fetch(
      `${this.baseUrl}/api/gen/${genId}/increment_play_count/v2`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${this.sessionToken}`,
        },
        body: JSON.stringify({
          sample_factor: sampleFactor,
        }),
      }
    );

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(
        `Failed to increment play count: ${response.status} ${errorText}`
      );
    }
  }

  /**
   * Update reaction type (like/dislike) for a song
   * Uses /api/gen/{gen_id}/update_reaction_type endpoint
   */
  async updateReactionType(
    genId: string,
    reaction: "LIKE" | "DISLIKE" | null,
    recommendationMetadata?: {
      context_type?: string;
      hook_id?: string;
      recommendation_item_id?: string;
    }
  ): Promise<void> {
    const response = await fetch(
      `${this.baseUrl}/api/gen/${genId}/update_reaction_type`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${this.sessionToken}`,
        },
        body: JSON.stringify({
          reaction,
          recommendation_metadata: recommendationMetadata ?? {},
        }),
      }
    );

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(
        `Failed to update reaction type: ${response.status} ${errorText}`
      );
    }
  }

  /**
   * Generate stems for a song
   * Uses the "twelve" stem task to generate all 12 stems at once
   */
  async generateStems(clipId: string): Promise<string[]> {
    const response = await fetch(`${this.baseUrl}/api/generate/v2-web/`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${this.sessionToken}`,
      },
      body: JSON.stringify({
        task: "gen_stem",
        mv: "chirp-v3-0", // Important: different from regular songs
        make_instrumental: true,
        continue_clip_id: clipId,
        stem_type_id: 91,
        stem_type_group_name: "Twelve",
        stem_task: "twelve",
        prompt: "",
        tags: "",
      }),
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(
        `Failed to generate stems: ${response.status} ${errorText}`
      );
    }

    const data: GenerateSongResponse = await response.json();
    return data.clips.map((clip) => clip.id);
  }

  /**
   * Poll MIDI generation status
   * Returns the MIDI file URL when ready
   */
  async getMidiStatus(clipId: string): Promise<{
    status: "pending" | "processing" | "completed" | "failed";
    midiUrl?: string;
    midiData?: any;
  }> {
    const response = await fetch(`${this.baseUrl}/api/gen/${clipId}/midi`, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${this.sessionToken}`,
      },
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(
        `Failed to fetch MIDI status: ${response.status} ${errorText}`
      );
    }

    const data = await response.json();

    // Map Suno's state/status to our simplified status
    // Note: API returns "state" not "status" for MIDI
    const apiStatus = data.state || data.status;
    let status: "pending" | "processing" | "completed" | "failed" =
      "processing";
    if (apiStatus === "complete" || apiStatus === "completed") {
      status = "completed";
    } else if (apiStatus === "error" || apiStatus === "failed") {
      status = "failed";
    } else if (
      apiStatus === "submitted" ||
      apiStatus === "queued" ||
      apiStatus === "pending"
    ) {
      status = "pending";
    }

    return {
      status,
      midiUrl: data.midi_url || data.midiUrl,
      midiData: data.instruments ? data : undefined, // Return full MIDI data if present
    };
  }

  private mapClipToMetadata(clip: FeedResponse["clips"][0]): SongMetadata {
    // Map Suno's status strings to our simplified status enum
    let status: SongMetadata["status"] = "processing";
    if (clip.status === "complete") {
      status = "completed";
    } else if (clip.status === "streaming") {
      status = "streaming";
    } else if (clip.status === "error" || clip.status === "failed") {
      status = "failed";
    } else if (clip.status === "submitted" || clip.status === "queued") {
      status = "pending";
    }

    // Log duration info for debugging
    if (status === "completed" || status === "streaming") {
      console.log(`[SunoClient] Song ${clip.id} status=${status}, duration=${clip.duration}, metadata.duration=${(clip as any).metadata?.duration}`);
    }

    return {
      id: clip.id,
      title: clip.title || "Untitled",
      artist: clip.display_name,
      audioUrl: clip.audio_url,
      videoUrl: clip.video_url,
      albumArtUrl: clip.image_url,
      duration: clip.duration || (clip as any).metadata?.duration,
      status,
      createdAt: clip.created_at,
      errorMessage: clip.error_message,
    };
  }
}

/**
 * Create a Suno client instance from environment variables
 * Optionally accepts a user-specific token to override the default
 */
export function createSunoClient(userToken?: string | null): SunoClient {
  const baseUrl = process.env.SUNO_BASE_URL;
  const defaultToken = process.env.SUNO_SESSION_TOKEN;

  // Use user token if provided, otherwise fall back to default
  const sessionToken = userToken || defaultToken;

  if (!baseUrl || !sessionToken) {
    throw new Error(
      "Missing required Suno environment variables: SUNO_BASE_URL, SUNO_SESSION_TOKEN"
    );
  }

  return new SunoClient({ baseUrl, sessionToken });
}
