function detectBrowser() {
  const userAgent = navigator.userAgent
  if (userAgent.indexOf('Edg') > -1) {
    return 'Microsoft Edge'
  } else if (userAgent.indexOf('Chrome') > -1) {
    return 'Chrome'
  } else if (userAgent.indexOf('Firefox') > -1) {
    return 'Firefox'
  } else if (userAgent.indexOf('Safari') > -1) {
    return 'Safari'
  } else if (userAgent.indexOf('Opera') > -1) {
    return 'Opera'
  } else if (userAgent.indexOf('Trident') > -1 || userAgent.indexOf('MSIE') > -1) {
    return 'Internet Explorer'
  }

  return 'Unknown'
}

type Endpoint = {
  resource: string
  method: string
}

type View = {
  Id: string
  Name: string
  CollectionType: string
  ParentId: string
}

type Item = {
  Name: string
  OriginalTitle: string
  Id: string
  ParentId?: string
  Type: string
  OfficialRating: string
  ImageTags: ImageTags
  CommunityRating: number
  ProductionYear: number
  IndexNumber?: number
  IsFolder: boolean
  ParentIndexNumber?: number
  SeriesName?: string
  SeriesId?: string
  Overview: string
  RunTimeTicks: number
  BackdropImageTags: string[]
  ParentLogoItemId?: string
  ParentLogoImageTag?: string
  ParentThumbItemId?: string
  ParentThumbImageTag?: string
  ParentBackdropItemId?: string
  ParentBackdropImageTags?: string[]
  SeriesPrimaryImageTag?: string
  UserData?: UserData
  MediaStreams?: MediaStream[]
  Taglines: string[]
  Genres: string[]
  Studios: Studio[]
  People: Person[]
}

type Person = {
  Name: string
  Id: string
  Role: string
  Type: string
  PrimaryImageTag?: string
}

type Studio = {
  Name: string
  Id: string
}

type MediaStream = {
  Language: string
  Type: string
  DisplayTitle: string
  IsDefault: boolean
  IsExternal: boolean
  IsForced: boolean
}

type MediaSegment = {
  EndTicks: number
  Id: string
  ItemId: string
  StartTicks: number
  Type: string // Intro | Outro
}

type UserData = {
  UnplayedItemCount: number
}

type ImageTags = {
  Primary?: string
  Thumb?: string
  Logo?: string
}

type User = {
  Name: string
  Id: string
  Views?: View[]
}

type Records<T> = {
  Items: T[]
  StartIndex: number
  TotalRecordCount: number
}

export type { User, View, Item, Records, MediaStream, MediaSegment }

const generateId = () => {
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  let result = ' '
  const charactersLength = characters.length
  for (let i = 0; i < 64; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength))
  }

  return result
}

class JFApi {
  buildAuthHeader(): string {
    let deviceId = localStorage.getItem('jf_deviceId')
    if (deviceId === null) {
      deviceId = generateId()
      localStorage.setItem('jf_deviceId', deviceId)
    }
    return `MediaBrowser Client="Featherfin", Device="${detectBrowser()}", DeviceId="${deviceId}", Version="v0.0.1", Token="${localStorage.getItem('jfauth_token')}"`
  }
  buildRequest(endpoint: Endpoint): Request {
    const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
    let deviceId = localStorage.getItem('jf_deviceId')
    if (deviceId === null) {
      deviceId = generateId()
      localStorage.setItem('jf_deviceId', deviceId)
    }
    return new Request(`${host}${endpoint.resource}`, {
      method: endpoint?.method,
      headers: {
        Authorization: this.buildAuthHeader(),
      },
    })
  }
  request<T>(endpoint: Endpoint): Promise<T | null> {
    const host = localStorage.getItem('jf_host')
    if (host === '' || host === null) {
      window.location.href = '/login'
    }
    let deviceId = localStorage.getItem('jf_deviceId')
    if (deviceId === null) {
      deviceId = generateId()
      localStorage.setItem('jf_deviceId', deviceId)
    }
    return new Promise((resolve) => {
      fetch(`${host}${endpoint.resource}`, {
        method: endpoint?.method,
        headers: {
          Authorization: this.buildAuthHeader(),
        },
      })
        .then(async (response) => {
          const data = await response.json()
          resolve(data as T)
        })
        .catch((error) => {
          console.log(error)
          resolve(null)
        })
    })
  }
  LoadViews(): Promise<Records<View> | null> {
    const userId = localStorage.getItem('jf_userid') || ''
    return this.request({
      resource: `/UserViews?userId=${userId}`,
      method: 'GET',
    })
      .then((data) => {
        const views = data as Records<View>
        const response: Records<View> = {
          Items: [],
          StartIndex: 0,
          TotalRecordCount: views.TotalRecordCount,
        }
        for (const view of views.Items) {
          if (view.CollectionType !== 'movies' && view.CollectionType !== 'tvshows') {
            response.TotalRecordCount--
            continue
          }
          response.Items.push(view)
        }
        return response
      })
      .catch(() => {
        return null
      })
  }
  GetLatest(view: View): Promise<Item[] | null> {
    const userId = localStorage.getItem('jf_userid') || ''
    return this.request({
      resource: `/Users/${userId}/Items/Latest?Limit=16&Fields=PrimaryImageAspectRatio%2COverview%2CPath%2COriginalTitle&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&ParentId=${view.Id}`,
      method: 'GET',
    })
  }
  GetNextUp(): Promise<Records<Item> | null> {
    const userId = localStorage.getItem('jf_userid') || ''
    return this.request({
      resource: `/Shows/NextUp?Limit=24&Fields=PrimaryImageAspectRatio%2CDateCreated%2CPath%2CMediaSourceCount%2COverview%2COriginalTitle&UserId=${userId}&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CBanner%2CThumb&EnableTotalRecordCount=false&DisableFirstEpisode=false&NextUpDateCutoff=2023-12-03T21%3A55%3A33.690Z&EnableResumable=false&EnableRewatching=false`,
      method: 'GET',
    })
  }
  GetNextUpInSeries(seriesId: string): Promise<Records<Item> | null> {
    const userId = localStorage.getItem('jf_userid') || ''
    return this.request({
      resource: `/Shows/NextUp?SeriesId=${seriesId}&UserId=${userId}&Fields=MediaSourceCount%2COverview%2COriginalTitle`,
      method: 'GET',
    })
  }
  GetSeasons(seriesId: string): Promise<Records<Item> | null> {
    const userId = localStorage.getItem('jf_userid') || ''
    return this.request({
      resource: `/Shows/${seriesId}/Seasons?userId=${userId}&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CMediaSourceCount`,
      method: 'GET',
    })
  }
  GetEpisodes(seriesId: string, seasonId: string): Promise<Records<Item> | null> {
    const userId = localStorage.getItem('jf_userid') || ''
    return this.request({
      resource: `/Shows/${seriesId}/Episodes?seasonId=${seasonId}&userId=${userId}&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CMediaSourceCount%2COverview%2COriginalTitle`,
      method: 'GET',
    })
  }
  GetFirstEpisode(seriesId: string): Promise<Records<Item> | null> {
    const userId = localStorage.getItem('jf_userid') || ''
    return this.request({
      resource: `/Shows/${seriesId}/Episodes?userId=${userId}&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CMediaSourceCount%2COverview%2COriginalTitle&limit=1`,
      method: 'GET',
    })
  }
  GetItem(itemId: string): Promise<Item | null> {
    const userid = localStorage.getItem('jf_userid') || ''
    const req = this.buildRequest({
      resource: `/Items/${itemId}?userId=${userid}`,
      method: 'GET',
    })
    return new Promise((resolve) => {
      caches.open('jfapi_item_cache').then((cache) => {
        cache.match(req).then((response) => {
          if (response) {
            resolve(response.json() as Promise<Item>)
          } else {
            fetch(req)
              .then(async (response) => {
                if (response.status === 401) {
                  window.location.href = '/login'
                }
                await cache.put(req, response.clone())
                resolve(response.json() as Promise<Item | null>)
              })
              .catch((error) => {
                console.log(error)
                resolve(null)
              })
          }
        })
      })
    })
  }
  GetItemsInView(viewId: string, type: string, startIndex: number) {
    const userId = localStorage.getItem('jf_userid') || ''
    return this.request<Records<Item>>({
      resource: `/Users/${userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=${type}&Recursive=true&Fields=PrimaryImageAspectRatio%2COverview%2COriginalTitle&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CBanner%2CThumb&StartIndex=${startIndex}&Limit=100&ParentId=${viewId}`,
      method: 'GET',
    })
  }
  SearchItems(query: string) {
    const userId = localStorage.getItem('jf_userid') || ''
    return this.request<Records<Item>>({
      resource: `/Items?userId=${userId}&limit=100&recursive=true&searchTerm=${query}&fields=PrimaryImageAspectRatio&fields=MediaSourceCount&imageTypeLimit=1&enableTotalRecordCount=false`, // &includeItemTypes=Movie
      method: 'GET',
    })
  }
  GetIntroOutroSegments(itemId: string): Promise<Records<MediaSegment> | null> {
    return this.request({
      resource: `/MediaSegments/${itemId}?includeSegmentTypes=Outro&includeSegmentTypes=Intro`,
      method: 'GET',
    })
  }
  PostProgress(itemId: string, sessionId: string, ticks: number) {
    const host = localStorage.getItem('jf_host') || ''
    fetch(`${host}/Sessions/Playing/Progress`, {
      body: JSON.stringify({
        CanSeek: true,
        ItemId: itemId,
        SessionId: sessionId, // TODO
        MediaSourceId: itemId,
        AudioStreamIndex: 1, // TODO
        SubtitleStreamIndex: 3, // TODO
        IsPaused: false,
        IsMuted: false,
        PositionTicks: ticks,
        VolumeLevel: 100,
        PlayMethod: 'Transcode',
        PlaySessionId: sessionId, // same as SessionId
        RepeatMode: 'RepeatAll',
      }),
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: this.buildAuthHeader(),
      },
    })
  }
  ThumbImageUrl(itemId: string, imgTag: string) {
    const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
    return `${host}/Items/${itemId}/Images/Thumb?fillHeight=267&fillWidth=474&quality=96&tag=${imgTag}`
  }
  BackdropImageUrl(itemId: string, imgTag: string) {
    const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
    return `${host}/Items/${itemId}/Images/Backdrop?fillHeight=267&fillWidth=474&quality=96&tag=${imgTag}`
  }
  FullBackdropImageUrl(itemId: string, imgTag: string, imgIndex: number) {
    const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
    return `${host}/Items/${itemId}/Images/Backdrop/${imgIndex}?tag=${imgTag}`
  }
  PrimaryImageUrl(itemId: string, imgTag: string) {
    const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
    return `${host}/Items/${itemId}/Images/Primary?fillHeight=446&fillWidth=297&quality=96&tag=${imgTag}`
  }
  LogoImageUrl(itemId: string, imgTag: string) {
    const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
    return `${host}/Items/${itemId}/Images/Logo?tag=${imgTag}`
  }
  Login(host: string, username: string, password: string): Promise<User | null> {
    let deviceId = localStorage.getItem('jf_deviceId')
    if (deviceId === null) {
      deviceId = generateId()
      localStorage.setItem('jf_deviceId', deviceId)
    }
    return new Promise((resolve) => {
      fetch(`${host}/Users/authenticatebyname`, {
        method: 'POST',
        body: JSON.stringify({ Username: username, Pw: password }),
        headers: {
          'Content-Type': 'application/json',
          Authorization: this.buildAuthHeader(),
        },
      })
        .then((r) => r.json().then((data) => ({ status: r.status, body: data })))
        .then((data) => {
          console.log(data)
          if (data.status !== 200) {
            // bad
            resolve(null)
          } else {
            localStorage.setItem('jfauth_token', data.body.AccessToken)
            resolve({
              Name: data.body.User.Name,
              Id: data.body.User.Id,
            })
          }
        })
    })
  }
  Logout() {
    return this.request({
      resource: '/Sessions/Logout',
      method: 'POST',
    })
      .then((data) => {
        localStorage.removeItem('jfauth_token')
        return data
      })
      .catch((err) => {
        return err
      })
  }
}

export default new JFApi()

/*
- /UserViews?userId=XYZ - returns [Library]
- /Latest?...&ParentId=Library.Id - returns latest in library
- /Resume?...&MediaTypes=Video - returns unfinished playback
- /NextUp?... - returns [Episode]
- /Users/authenticatebyname body:{Username: "xyz", Pw: "xyz123"} returns: AccessToken, User, SessionInfo
Authentication is done by appending 'Token="MyTokenXYZ123"' to Authorization header
- /Users/User.Id - returns: User
- /Users/User.Id/Items?SortBy=DateCreated%2CSortName%2CProductionYear&SortOrder=Descending&IncludeItemTypes=Movie&Recursive=true&Fields=PrimaryImageAspectRatio%2CMediaSourceCount&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CBanner%2CThumb&StartIndex=0&ParentId=f137a2dd21bbc1b99aa5c0f6bf02a805&Limit=100 - returns [Item<Movie>]
- /Users/User.Id/Items/Item.Id - FullItem
- /Items/Item.Id/Similar?userId=User.Id&limit=12&fields=PrimaryImageAspectRatio - returns similar items
- /Items/Item.Id/Images/Primary?fillHeight=713&fillWidth=500&quality=96&tag=Item.ImageTags.Primary - returns Primary Image
- /.../Images/Thumb?...&tag=Episode.ParentThumbImageTag
- /Users/User.Id/PlayedItems/Item.Id - DELETE: remove played status; POST w/ DatePlayed=2024-12-04T20%3A18%3A28.936Z: add played status (GMT time zone)
- /Users/User.Id/FavoriteItems/Item.Id - DELETE: remove favorite status; POST: add favorite status
- /Sessions/Logout - logout

Play video flow:
- POST /PlayBackInfo
    Body (among other things): AudioStreamIndex, SubtitleStreamIndex, StartTimeTicks...
    Response (among other things): Bitrate, TranscodingUrl, RunTimeTicks, PlaySessionId, ETag
    When not transcoding, GET /Videos/Item.Id/stream.mp4/Static=true&mediaSourceId=Item.Id&deviceId=Device.Id&api_key={}&Tag=ETag
- POST /Sessions/Playing
    Body (among other things): AudioStreamIndex, SubtitleStreamIndex, PlayMethod
- GET /System/Configuration/encoding: seems to just return server transcoding capabilities
*/
