const axios = require('axios')
const crypto = require('crypto')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')

dayjs.extend(utc)
dayjs.extend(customParseFormat)

const MANIFEST_URL = 'https://prod.dcm.telekom-dienste.de/v1/settings/web-mtv/manifest'
const DEVICE_MODEL = 'WEB2_FTV'
const PORTAL = 'release'
const SUBSCRIBER_TYPE = 'FTV_FREEMIUM_DT'
const CHANNEL_PAGE_SIZE = 100
const CHANNEL_PAGE_LIMIT = 1000

const FALLBACK_MPX = Object.freeze({
  accountPid: 'mdeprod',
  locationIdUri: 'http://data.entertainment.tv.theplatform.eu/entertainment/data/Location/245991976396',
  feeds: {
    allChannelSchedulesFeed:
      'https://feed.entertainment.tv.theplatform.eu/f/{MpxAccountPid}/{MpxAccountPid}-all-channel-schedules',
    allChannelStationsFeed:
      'https://feed.entertainment.tv.theplatform.eu/f/{MpxAccountPid}/{MpxAccountPid}-channel-stations-main'
  }
})

let session
let manifestPromise

module.exports = {
  site: 'www.magenta.tv',
  days: 2,
  request: {
    cache: {
      ttl: 60 * 60 * 1000 // 1 hour
    }
  },
  async url({ channel, date }) {
    const mpx = await getMpxConfig()
    const currentSession = getSession()
    const window = getUtcWindow(date)

    return buildScheduleUrl({
      mpx,
      session: currentSession,
      siteIds: [channel.site_id],
      window
    })
  },
  async parser({ content, channel }) {
    return parseScheduleResponse(content, channel)
  },
  async channels() {
    const mpx = await getMpxConfig()
    const currentSession = getSession()
    const channels = []

    for (let start = 1; start <= CHANNEL_PAGE_LIMIT; start += CHANNEL_PAGE_SIZE) {
      const entries = await getChannelEntries({
        mpx,
        session: currentSession,
        start,
        end: start + CHANNEL_PAGE_SIZE - 1
      })

      if (!entries.length) break

      entries.forEach(entry => {
        const channel = parseChannel(entry)
        if (channel) channels.push(channel)
      })

      if (entries.length < CHANNEL_PAGE_SIZE) break
    }

    return channels
  }
}

function createSession() {
  return {
    deviceId: crypto.randomUUID(),
    sessionId: crypto.randomUUID()
  }
}

function getSession() {
  if (!session) {
    session = createSession()
  }

  return session
}

function buildCid(currentSession) {
  return `${currentSession.sessionId}::${crypto.randomUUID()}`
}

function getManifestRequestConfig(currentSession) {
  return {
    headers: {
      'X-DT-Call-ID': crypto.randomUUID(),
      'X-DT-Session-ID': currentSession.sessionId
    },
    params: {
      deviceId: currentSession.deviceId,
      deviceModel: DEVICE_MODEL,
      portal: PORTAL,
      subscriberType: SUBSCRIBER_TYPE,
      $redirect: false,
      sid: currentSession.sessionId
    }
  }
}

async function getManifest() {
  if (!manifestPromise) {
    manifestPromise = axios
      .get(MANIFEST_URL, getManifestRequestConfig(getSession()))
      .then(r => r.data)
      .catch(() => null)
  }

  return manifestPromise
}

async function getMpxConfig() {
  const manifest = await getManifest()
  const manifestMpx = manifest && manifest.mpx ? manifest.mpx : {}

  return {
    ...FALLBACK_MPX,
    ...manifestMpx,
    feeds: {
      ...FALLBACK_MPX.feeds,
      ...(manifestMpx.feeds || {})
    }
  }
}

async function getChannelEntries({ mpx, session, start, end }) {
  const url = buildChannelFeedUrl({ mpx, session, start, end })
  const data = await axios
    .get(url)
    .then(r => r.data)
    .catch(() => null)

  return Array.isArray(data && data.entries) ? data.entries : []
}

function buildChannelFeedUrl({ mpx, session, start, end }) {
  const url = new URL(resolveFeedTemplate(mpx.feeds.allChannelStationsFeed, mpx.accountPid))

  url.searchParams.set('lang', 'short-de')
  url.searchParams.set('sort', 'dt$displayChannelNumber')
  url.searchParams.set('range', `${start}-${end}`)
  url.searchParams.set('cid', buildCid(session))

  return url.toString()
}

function buildScheduleUrl({ mpx, session, siteIds, window }) {
  const url = new URL(resolveFeedTemplate(mpx.feeds.allChannelSchedulesFeed, mpx.accountPid))

  url.searchParams.set('byId', siteIds.join('|'))
  url.searchParams.set('byListingTime', window)
  url.searchParams.set('byLocationId', mpx.locationIdUri)
  url.searchParams.set('cid', buildCid(session))

  return url.toString()
}

function resolveFeedTemplate(template, accountPid) {
  return template.replaceAll('{MpxAccountPid}', accountPid)
}

function getUtcWindow(date) {
  const start = date.utc().startOf('day')
  const end = start.add(1, 'day')

  return `${start.toISOString()}~${end.toISOString()}`
}

function parseScheduleResponse(content, channel) {
  const data = parseJson(content)
  const entries = Array.isArray(data && data.entries) ? data.entries : []
  const targetSiteId = channel && channel.site_id ? String(channel.site_id) : null
  const programs = []

  entries.forEach(entry => {
    if (targetSiteId && extractNumericId(entry.id) !== targetSiteId) return
    if (!Array.isArray(entry.listings)) return

    entry.listings.forEach(listing => {
      const program = parseProgramme(entry, listing)
      if (program) programs.push(program)
    })
  })

  return programs
}

function parseChannel(entry) {
  if (!entry || entry['dt$isRadio']) return null

  const station = getFirstStation(entry)
  const siteId = extractNumericId(entry.id)
  const name = station && station.title ? station.title : entry.title

  if (!station || !siteId || !name) return null

  return {
    lang: 'de',
    site_id: siteId,
    name
  }
}

function parseProgramme(entry, listing) {
  if (
    !listing ||
    !listing.program ||
    listing.startTime === null ||
    listing.startTime === undefined ||
    listing.endTime === null ||
    listing.endTime === undefined
  ) {
    return null
  }

  const program = listing.program
  const programInfo = parseProgramInfo(listing['dt$programInfo'])
  const title = program.title

  if (!title) return null

  const parsed = {
    title,
    description: program.description || null,
    category: parseCategories(program.tags),
    sub_title: parseSubTitle(program, listing),
    rating: parseRating(program.ratings),
    season: normalizeNumber(program.tvSeasonNumber ?? programInfo.tvSeasonNumber),
    episode: normalizeNumber(
      program.tvSeasonEpisodeNumber ??
        programInfo.tvSeasonEpisodeNumber ??
        programInfo.seriesEpisodeNumber
    ),
    image: parseProgramImage(program),
    icon: parseProgramImage(program),
    start: dayjs(Number(listing.startTime)),
    stop: dayjs(Number(listing.endTime)),
    country: parseCountry(program['dt$countries']),
    date: program.year ? String(program.year) : null
  }

  if (parsed.image === null) delete parsed.image
  if (parsed.icon === null) delete parsed.icon
  if (parsed.category === null) delete parsed.category
  if (parsed.sub_title === null) delete parsed.sub_title
  if (parsed.rating === null) delete parsed.rating
  if (parsed.season === null) delete parsed.season
  if (parsed.episode === null) delete parsed.episode
  if (parsed.country === null) delete parsed.country
  if (parsed.date === null) delete parsed.date

  return parsed
}

function parseSubTitle(program, listing) {
  if (program.secondaryTitle && program.secondaryTitle !== program.title) {
    return program.secondaryTitle
  }

  if (listing['dt$seriesTitle'] && listing['dt$seriesTitle'] !== program.title) {
    return listing['dt$seriesTitle']
  }

  return null
}

function parseCategories(tags) {
  if (!Array.isArray(tags)) return null

  const categories = tags
    .filter(tag => ['category', 'genre-primary', 'genre-secondary'].includes(tag.scheme) && tag.title)
    .map(tag => tag.title)

  return categories.length ? [...new Set(categories)] : null
}

function parseRating(ratings) {
  if (!Array.isArray(ratings)) return null

  const rating = ratings.find(item => item && item.rating && item.rating !== 'UNKNOWN')
  if (!rating) return null

  return {
    system: rating.scheme || 'magenta',
    value: rating.rating
  }
}

function parseProgramInfo(value) {
  if (!value || typeof value !== 'string') return {}

  try {
    return JSON.parse(value.replaceAll("'", '"'))
  } catch {
    return {}
  }
}

function parseProgramImage(program) {
  if (!program || !program.thumbnails) return null

  const thumbnails = Object.values(program.thumbnails)
    .filter(thumbnail => thumbnail && thumbnail.url)
    .sort((a, b) => (b.width || 0) * (b.height || 0) - (a.width || 0) * (a.height || 0))

  return thumbnails[0] ? thumbnails[0].url : null
}

function parseCountry(value) {
  if (!value || typeof value !== 'string') return null
  return value.toUpperCase()
}

function getFirstStation(entry) {
  if (!entry || !entry.stations || typeof entry.stations !== 'object') return null
  return Object.values(entry.stations)[0] || null
}

function extractNumericId(uri) {
  if (!uri || typeof uri !== 'string') return null
  const match = uri.match(/(\d+)(?!.*\d)/)
  return match ? match[1] : null
}

function normalizeNumber(value) {
  return value === null || value === undefined || value === '' ? null : value
}

function parseJson(content) {
  if (!content) return {}
  if (typeof content !== 'string') return content

  try {
    return JSON.parse(content)
  } catch {
    return {}
  }
}
