'use strict'

const spotifyURI = require('spotify-uri')
const { parse } = require('himalaya')

const TYPE = {
  ALBUM: 'album',
  ARTIST: 'artist',
  EPISODE: 'episode',
  PLAYLIST: 'playlist',
  TRACK: 'track'
}

const ERROR = {
  REPORT:
    'Please report the problem at https://github.com/microlinkhq/spotify-url-info/issues.',
  NOT_DATA: "Couldn't find any data in embed page that we know how to parse.",
  NOT_SCRIPTS: "Couldn't find scripts to get the data."
}

const SUPPORTED_TYPES = Object.values(TYPE)

const throwError = (message, html) => {
  const error = new TypeError(`${message}\n${ERROR.REPORT}`)
  error.html = html
  throw error
}

const parseData = html => {
  const embed = parse(html)

  let scripts = embed.find(el => el.tagName === 'html')
  if (scripts === undefined) return throwError(ERROR.NOT_SCRIPTS, html)

  scripts = scripts.children
    .find(el => el.tagName === 'body')
    .children.filter(({ tagName }) => tagName === 'script')

  let script = scripts.find(script =>
    script.attributes.some(({ value }) => value === 'resource')
  )

  if (script !== undefined) {
    return normalizeData({
      data: JSON.parse(Buffer.from(script.children[0].content, 'base64'))
    })
  }

  script = scripts.find(script =>
    script.attributes.some(({ value }) => value === 'initial-state')
  )

  if (script !== undefined) {
    const data = JSON.parse(Buffer.from(script.children[0].content, 'base64'))
      .data.entity
    return normalizeData({ data })
  }

  script = scripts.find(script =>
    script.attributes.some(({ value }) => value === '__NEXT_DATA__')
  )

  if (script !== undefined) {
    const string = Buffer.from(script.children[0].content)
    const data = JSON.parse(string).props.pageProps.state?.data.entity
    if (data !== undefined) return normalizeData({ data })
  }

  return throwError(ERROR.NOT_DATA, html)
}

const createGetData = fetch => async (url, opts) => {
  const parsedUrl = getParsedUrl(url)
  const embedURL = spotifyURI.formatEmbedURL(parsedUrl)
  const response = await fetch(embedURL, opts)
  const text = await response.text()
  return parseData(text)
}

function getParsedUrl (url) {
  try {
    const parsedURL = spotifyURI.parse(url)
    if (!parsedURL.type) throw new TypeError()
    return spotifyURI.formatEmbedURL(parsedURL)
  } catch (_) {
    throw new TypeError(`Couldn't parse '${url}' as valid URL`)
  }
}

const getImages = data =>
  data.coverArt?.sources || data.images || data.visualIdentity.image

const getDate = data => data.releaseDate?.isoString || data.release_date

const getLink = data => spotifyURI.formatOpenURL(data.uri)

function getArtistTrack (track) {
  return track.show
    ? track.show.publisher
    : []
        .concat(track.artists)
        .filter(Boolean)
        .map(a => a.name)
        .reduce(
          (acc, name, index, array) =>
            index === 0
              ? name
              : acc + (array.length - 1 === index ? ' & ' : ', ') + name,
          ''
        )
}

const getTracks = data =>
  data.trackList ? data.trackList.map(toTrack) : [toTrack(data)]

function getPreview (data) {
  const [track] = getTracks(data)
  const date = getDate(data)

  return {
    date: date ? new Date(date).toISOString() : date,
    title: data.name,
    type: data.type,
    track: track.name,
    description: data.description || data.subtitle || track.description,
    artist: track.artist,
    image: getImages(data)?.reduce((a, b) => (a.width > b.width ? a : b))?.url,
    audio: track.previewUrl,
    link: getLink(data),
    embed: `https://embed.spotify.com/?uri=${data.uri}`
  }
}

const toTrack = track => ({
  artist: getArtistTrack(track) || track.subtitle,
  duration: track.duration,
  name: track.title,
  previewUrl: track.isPlayable ? track.audioPreview.url : undefined,
  uri: track.uri
})

const normalizeData = ({ data }) => {
  if (!data || !data.type || !data.name) {
    throw new Error("Data doesn't seem to be of the right shape to parse")
  }

  if (!SUPPORTED_TYPES.includes(data.type)) {
    throw new Error(
      `Not an ${SUPPORTED_TYPES.join(', ')}. Only these types can be parsed`
    )
  }

  data.type = data.uri.split(':')[1]

  return data
}

module.exports = fetch => {
  const getData = createGetData(fetch)
  return {
    getLink,
    getData,
    getPreview: (url, opts) => getData(url, opts).then(getPreview),
    getTracks: (url, opts) => getData(url, opts).then(getTracks),
    getDetails: (url, opts) =>
      getData(url, opts).then(data => ({
        preview: getPreview(data),
        tracks: getTracks(data)
      }))
  }
}

module.exports.parseData = parseData
module.exports.throwError = throwError
