import { useTrakt, getTraktClientId, getTraktClientSecret } from '../stores/trakt-store'
import { jellyfinClient, getItemsApi } from '../api/jellyfin'
import type { BaseItemDto } from '../api/types'

/**
 * Trakt.tv REST helpers: device-code OAuth + scrobble. The device flow
 * lets us authorise without redirecting through a browser - we show the
 * user a short code, they enter it on trakt.tv/activate, and we poll
 * for the resulting token.
 *
 * Scrobble endpoints expect tmdb / imdb / tvdb ids on the media object
 * plus a progress percentage. We pull those off the Jellyfin item's
 * ProviderIds so no extra lookups are required.
 */

const BASE = 'https://api.trakt.tv'

export interface DeviceCode {
  device_code: string
  user_code: string
  verification_url: string
  expires_in: number
  interval: number
}

function headers(): Record<string, string> {
  const clientId = getTraktClientId()
  return {
    'Content-Type': 'application/json',
    'trakt-api-version': '2',
    'trakt-api-key': clientId,
  }
}

function authHeaders(token: string): Record<string, string> {
  return {
    ...headers(),
    Authorization: `Bearer ${token}`,
  }
}

export async function requestDeviceCode(): Promise<DeviceCode> {
  const clientId = getTraktClientId()
  if (!clientId) throw new Error('Trakt client id not configured')
  const res = await fetch(`${BASE}/oauth/device/code`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ client_id: clientId }),
  })
  if (!res.ok) throw new Error(`device/code failed: ${res.status}`)
  return res.json()
}

export async function pollDeviceToken(deviceCode: string): Promise<{ access_token: string; refresh_token: string; expires_in: number } | null> {
  const clientId = getTraktClientId()
  const clientSecret = getTraktClientSecret()
  if (!clientId || !clientSecret) throw new Error('Trakt credentials not configured')
  const res = await fetch(`${BASE}/oauth/device/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      code: deviceCode,
      client_id: clientId,
      client_secret: clientSecret,
    }),
  })
  if (res.status === 200) return res.json()
  // 400 = pending, 404 = expired, 409 = already used, 410 = denied, 418 = polling too fast, 429 = rate limit
  if (res.status === 400 || res.status === 429) return null
  throw new Error(`token poll failed: ${res.status}`)
}

export async function refreshToken(): Promise<boolean> {
  const tokens = useTrakt.getState().tokens
  if (!tokens?.refreshToken) return false
  const clientId = getTraktClientId()
  const clientSecret = getTraktClientSecret()
  if (!clientId || !clientSecret) return false
  try {
    const res = await fetch(`${BASE}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        refresh_token: tokens.refreshToken,
        client_id: clientId,
        client_secret: clientSecret,
        grant_type: 'refresh_token',
      }),
    })
    if (!res.ok) return false
    const data = await res.json()
    useTrakt.getState().setTokens({
      accessToken: data.access_token,
      refreshToken: data.refresh_token,
      expiresAt: new Date(Date.now() + data.expires_in * 1000).toISOString(),
    })
    return true
  } catch {
    return false
  }
}

async function ensureFreshToken(): Promise<string | null> {
  const tokens = useTrakt.getState().tokens
  if (!tokens) return null
  if (Date.parse(tokens.expiresAt) - Date.now() < 60_000) {
    const ok = await refreshToken()
    if (!ok) return null
    return useTrakt.getState().tokens?.accessToken ?? null
  }
  return tokens.accessToken
}

// Cache series ProviderIds keyed by seriesId so episode scrobbles don't
// re-fetch the series item on every start/pause/stop. Cleared when the
// app reloads - matches the typical Jellyfin Items API freshness window.
const seriesIdsCache = new Map<string, Record<string, string>>()

async function fetchSeriesProviderIds(seriesId: string): Promise<Record<string, string>> {
  const cached = seriesIdsCache.get(seriesId)
  if (cached) return cached
  const api = jellyfinClient.getApi()
  if (!api) return {}
  try {
    const res = await getItemsApi(api).getItems({
      userId: jellyfinClient.getAuthState()!.userId,
      ids: [seriesId],
      fields: ['ProviderIds'],
    } as any)
    const series = res.data.Items?.[0]
    const ids = series?.ProviderIds || {}
    const result: Record<string, string> = {}
    if (ids.Tmdb) result.tmdb = String(ids.Tmdb)
    if (ids.Imdb) result.imdb = String(ids.Imdb)
    if (ids.Tvdb) result.tvdb = String(ids.Tvdb)
    seriesIdsCache.set(seriesId, result)
    return result
  } catch {
    return {}
  }
}

/**
 * Build the media payload for a scrobble call. For episodes we resolve
 * the parent series' ProviderIds via the Items API (cached) so Trakt
 * matches on tmdb / imdb / tvdb id rather than the much fuzzier title.
 * Returns null when there's no usable identifier at all.
 */
async function buildScrobbleBody(item: BaseItemDto, progressPct: number) {
  const progress = Math.max(0, Math.min(100, progressPct))

  if (item.Type === 'Movie') {
    const ids: Record<string, string | number> = {}
    const tmdb = item.ProviderIds?.Tmdb
    const imdb = item.ProviderIds?.Imdb
    const tvdb = item.ProviderIds?.Tvdb
    if (tmdb) ids.tmdb = Number(tmdb) || tmdb
    if (imdb) ids.imdb = imdb
    if (tvdb) ids.tvdb = Number(tvdb) || tvdb
    if (Object.keys(ids).length === 0) return null
    return { movie: { ids }, progress }
  }

  if (item.Type === 'Episode') {
    const showIds: Record<string, string | number> = {}
    if (item.SeriesId) {
      const fetched = await fetchSeriesProviderIds(item.SeriesId)
      if (fetched.tmdb) showIds.tmdb = Number(fetched.tmdb) || fetched.tmdb
      if (fetched.imdb) showIds.imdb = fetched.imdb
      if (fetched.tvdb) showIds.tvdb = Number(fetched.tvdb) || fetched.tvdb
    }
    const show = Object.keys(showIds).length
      ? { ids: showIds }
      : { title: item.SeriesName || '' }
    return {
      show,
      episode: {
        season: item.ParentIndexNumber ?? 1,
        number: item.IndexNumber ?? 1,
      },
      progress,
    }
  }
  return null
}

async function postScrobble(action: 'start' | 'pause' | 'stop', body: object) {
  const token = await ensureFreshToken()
  if (!token) return false
  try {
    const res = await fetch(`${BASE}/scrobble/${action}`, {
      method: 'POST',
      headers: authHeaders(token),
      body: JSON.stringify(body),
    })
    return res.ok
  } catch {
    return false
  }
}

export async function scrobbleStart(item: BaseItemDto, progressPct: number): Promise<boolean> {
  if (!useTrakt.getState().enabled) return false
  const body = await buildScrobbleBody(item, progressPct)
  if (!body) return false
  return postScrobble('start', body)
}

export async function scrobblePause(item: BaseItemDto, progressPct: number): Promise<boolean> {
  if (!useTrakt.getState().enabled) return false
  const body = await buildScrobbleBody(item, progressPct)
  if (!body) return false
  return postScrobble('pause', body)
}

export async function scrobbleStop(item: BaseItemDto, progressPct: number): Promise<boolean> {
  if (!useTrakt.getState().enabled) return false
  const body = await buildScrobbleBody(item, progressPct)
  if (!body) return false
  return postScrobble('stop', body)
}

/** Fetch the user's Trakt watchlist (movies + shows). */
export async function fetchTraktWatchlist(): Promise<Array<{ type: 'movie' | 'show'; ids: Record<string, string | number> }>> {
  const token = await ensureFreshToken()
  if (!token) return []
  try {
    const res = await fetch(`${BASE}/sync/watchlist`, {
      headers: authHeaders(token),
    })
    if (!res.ok) return []
    const data = await res.json()
    const out: Array<{ type: 'movie' | 'show'; ids: Record<string, string | number> }> = []
    for (const entry of data || []) {
      if (entry.type === 'movie' && entry.movie?.ids) {
        out.push({ type: 'movie', ids: entry.movie.ids })
      } else if (entry.type === 'show' && entry.show?.ids) {
        out.push({ type: 'show', ids: entry.show.ids })
      }
    }
    return out
  } catch {
    return []
  }
}

/** Add a movie or show to the Trakt watchlist. */
export async function addToTraktWatchlist(item: BaseItemDto): Promise<boolean> {
  const token = await ensureFreshToken()
  if (!token) return false
  const body = await buildScrobbleBody(item, 0)
  if (!body) return false
  const payload = item.Type === 'Movie'
    ? { movies: [{ ids: (body as { movie: { ids: Record<string, string | number> } }).movie.ids }] }
    : { shows: [{ ids: (body as { show: { ids: Record<string, string | number> } }).show.ids }] }
  try {
    const res = await fetch(`${BASE}/sync/watchlist`, {
      method: 'POST',
      headers: authHeaders(token),
      body: JSON.stringify(payload),
    })
    return res.ok
  } catch {
    return false
  }
}

/** Remove a movie or show from the Trakt watchlist. */
export async function removeFromTraktWatchlist(item: BaseItemDto): Promise<boolean> {
  const token = await ensureFreshToken()
  if (!token) return false
  const body = await buildScrobbleBody(item, 0)
  if (!body) return false
  const payload = item.Type === 'Movie'
    ? { movies: [{ ids: (body as { movie: { ids: Record<string, string | number> } }).movie.ids }] }
    : { shows: [{ ids: (body as { show: { ids: Record<string, string | number> } }).show.ids }] }
  try {
    const res = await fetch(`${BASE}/sync/watchlist/remove`, {
      method: 'POST',
      headers: authHeaders(token),
      body: JSON.stringify(payload),
    })
    return res.ok
  } catch {
    return false
  }
}
