import { Song } from '../models/Song.js';
import { ApiError } from '../utils/ApiError.js';
import { asyncHandler } from '../utils/asyncHandler.js';
import {
  extractSpotifyTrackId,
  extractSpotifyAlbumId,
  detectNonTrackSpotifyLinkType,
  fetchSpotifyTrack,
  fetchSpotifyAlbum,
} from '../services/spotifyService.js';
import { findAppleMusicMatch } from '../services/appleMusicService.js';
import { generateUniqueSlug, slugifyText } from '../services/slugService.js';

/**
 * Resolves a Spotify URL into full metadata (Spotify + best-effort Apple
 * Music match). Shared by both the lookup preview and the create/update
 * flows so the server is always the source of truth for song data — the
 * admin never types title, artist, artwork, or the Apple Music link.
 */
async function resolveMetadata(spotifyUrlOrId) {
  const trackId = extractSpotifyTrackId(spotifyUrlOrId);

  if (!trackId) {
    // Tell apart "that's a Spotify link, just not to a track" from "not a
    // Spotify link at all" — otherwise an album/playlist link silently
    // falls through as a bogus track ID and comes back as a confusing
    // "track not found" once it hits the Spotify API below.
    const linkType = detectNonTrackSpotifyLinkType(spotifyUrlOrId);
    if (linkType) {
      throw new ApiError(
        400,
        `That's a Spotify ${linkType} link — paste a link to a single track instead.`
      );
    }
  }

  // Falls back to the raw input so a bare track ID (pasted without its
  // https://open.spotify.com/track/ wrapper) still works.
  const finalTrackId = trackId || spotifyUrlOrId;
  if (!finalTrackId) {
    throw new ApiError(400, 'Enter a valid Spotify track URL');
  }

  const spotify = await fetchSpotifyTrack(finalTrackId);
  const apple = await findAppleMusicMatch(spotify.title, spotify.artist);

  return {
    ...spotify,
    apple_music_url: apple?.apple_music_url ?? null,
    apple_match_score: apple?.apple_match_score ?? null,
  };
}

// Songs are visible to every admin, but only manageable by whoever
// published them. A song with created_by = null (its creator's account
// was deleted) is treated as manageable by anyone, since otherwise it
// would become permanently stuck — every admin is equal in this app, so
// there's no "owner" role that could otherwise reclaim it.
function assertCanManage(song, adminId) {
  if (song.created_by !== null && song.created_by !== adminId) {
    throw new ApiError(403, 'You can only manage releases you published');
  }
}

// POST /api/songs/lookup — preview metadata before saving, used by the
// "paste a Spotify URL" step in the admin form. An album link gets a track
// list back instead of an error, so the admin can pick which single track
// becomes this fanlink — re-submitting with that track's URL then resolves
// exactly like pasting a track link directly.
export const lookupSpotifyTrack = asyncHandler(async (req, res) => {
  const { spotify_url } = req.body;
  if (!spotify_url) throw new ApiError(400, 'spotify_url is required');

  const albumId = extractSpotifyAlbumId(spotify_url);
  if (albumId) {
    const album = await fetchSpotifyAlbum(albumId);
    return res.json({ success: true, type: 'album', album });
  }

  const metadata = await resolveMetadata(spotify_url);
  const suggestedSlug = slugifyText(`${metadata.artist} ${metadata.title}`);

  res.json({
    success: true,
    type: 'track',
    metadata,
    suggested_slug: suggestedSlug,
    apple_music_matched: Boolean(metadata.apple_music_url),
  });
});

export const listSongs = asyncHandler(async (req, res) => {
  const { status } = req.query;
  const songs = await Song.findAll({ status });
  res.json({ success: true, songs });
});

export const getSong = asyncHandler(async (req, res) => {
  const song = await Song.findById(req.params.id);
  if (!song) throw new ApiError(404, 'Song not found');
  res.json({ success: true, song });
});

export const createSong = asyncHandler(async (req, res) => {
  const {
    spotify_url,
    slug,
    release_date,
    status,
    meta_pixel_id,
    tiktok_pixel_id,
    custom_head_script,
    audiomack_url,
    boomplay_url,
    apple_music_url,
  } = req.body;
  if (!spotify_url) throw new ApiError(400, 'spotify_url is required');

  const existing = await Song.findBySpotifyId(extractSpotifyTrackId(spotify_url) || '');
  if (existing) {
    throw new ApiError(409, 'A landing page for this track already exists', {
      existing_slug: existing.slug,
    });
  }

  const metadata = await resolveMetadata(spotify_url);
  const finalSlug = await generateUniqueSlug({
    customSlug: slug,
    artist: metadata.artist,
    title: metadata.title,
  });

  const song = await Song.create({
    ...metadata,
    // A confident auto-match always wins. Only when the automatic Apple
    // Music lookup came up empty do we accept the admin's manually pasted
    // link as a fallback (see the "Manual link" field on the song form).
    apple_music_url: metadata.apple_music_url || apple_music_url || null,
    slug: finalSlug,
    status: status === 'published' ? 'published' : 'draft',
    release_date: release_date || null,
    meta_pixel_id: meta_pixel_id || null,
    tiktok_pixel_id: tiktok_pixel_id || null,
    custom_head_script: custom_head_script || null,
    audiomack_url: audiomack_url || null,
    boomplay_url: boomplay_url || null,
    created_by: req.admin.id,
  });

  res.status(201).json({ success: true, song });
});

export const updateSong = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const existing = await Song.findById(id);
  if (!existing) throw new ApiError(404, 'Song not found');
  assertCanManage(existing, req.admin.id);

  const {
    spotify_url,
    slug,
    release_date,
    status,
    meta_pixel_id,
    tiktok_pixel_id,
    custom_head_script,
    audiomack_url,
    boomplay_url,
    apple_music_url,
  } = req.body;

  // Re-fetch metadata only if the Spotify link changed, so routine edits
  // (slug, status, release date) don't spend an extra API round trip.
  let metadata = {};
  const refetched = spotify_url && extractSpotifyTrackId(spotify_url) !== existing.spotify_track_id;
  if (refetched) {
    metadata = await resolveMetadata(spotify_url);
  }

  let finalSlug = existing.slug;
  if (slug && slugifyText(slug) !== existing.slug) {
    finalSlug = await generateUniqueSlug({
      customSlug: slug,
      artist: metadata.artist ?? existing.artist,
      title: metadata.title ?? existing.title,
      excludeId: id,
    });
  }

  // A confident auto-match (fresh or already on the record) always wins and
  // is never overridable by the client. apple_match_score is only ever set
  // by a real match, so it's what tells a locked auto-match apart from a
  // manually entered link that should stay freely editable.
  const isAutoMatched = refetched
    ? Boolean(metadata.apple_match_score)
    : Boolean(existing.apple_match_score);
  const currentAppleUrl = refetched ? metadata.apple_music_url : existing.apple_music_url;
  const finalAppleUrl = isAutoMatched
    ? currentAppleUrl
    : apple_music_url !== undefined
      ? apple_music_url || null
      : currentAppleUrl;

  const song = await Song.update(id, {
    ...metadata,
    apple_music_url: finalAppleUrl,
    slug: finalSlug,
    status,
    release_date,
    meta_pixel_id: meta_pixel_id !== undefined ? meta_pixel_id || null : undefined,
    tiktok_pixel_id: tiktok_pixel_id !== undefined ? tiktok_pixel_id || null : undefined,
    custom_head_script: custom_head_script !== undefined ? custom_head_script || null : undefined,
    audiomack_url: audiomack_url !== undefined ? audiomack_url || null : undefined,
    boomplay_url: boomplay_url !== undefined ? boomplay_url || null : undefined,
  });

  res.json({ success: true, song });
});

export const deleteSong = asyncHandler(async (req, res) => {
  const existing = await Song.findById(req.params.id);
  if (!existing) throw new ApiError(404, 'Song not found');
  assertCanManage(existing, req.admin.id);

  await Song.delete(req.params.id);
  res.json({ success: true });
});
