import {
  type AppBskyFeedDefs,
  type AppBskyGraphDefs,
  type AtpAgent,
  type ComAtprotoRepoStrongRef,
} from '@atproto/api'
import {AtUri} from '@atproto/api'

import {DM_SERVICE_HEADERS, IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants'
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
import {resolveShortLink} from '#/lib/link-meta/resolve-short-link'
import {downloadAndResize} from '#/lib/media/manip'
import {
  createStarterPackUri,
  parseStarterPackUri,
} from '#/lib/strings/starter-pack'
import {
  convertBskyAppUrlIfNeeded,
  getChatInviteCodeFromUrl,
  isBskyCustomFeedUrl,
  isBskyListUrl,
  isBskyPostUrl,
  isBskyStarterPackUrl,
  isBskyStartUrl,
  isShortLink,
  makeRecordUri,
} from '#/lib/strings/url-helpers'
import {type ComposerImage} from '#/state/gallery'
import {createComposerImage} from '#/state/gallery'
import {type ChatInvitePreview} from '#/state/queries/join-links'
import {type Gif} from '#/features/gifPicker/types'
import {createGIFDescription} from '../gif-alt-text'

type ResolvedExternalLink = {
  type: 'external'
  uri: string
  title: string
  description: string
  thumb: ComposerImage | undefined
  /**
   * The AT-URI of the Atmosphere record representing this external content, if
   * it exists. Example: a site.standard.document record.
   */
  associatedRefs?: LinkMeta['associatedRefs']
  view?: LinkMeta['view']
}

type ResolvedPostRecord = {
  type: 'record'
  record: ComAtprotoRepoStrongRef.Main
  kind: 'post'
  view: AppBskyFeedDefs.PostView
}

type ResolvedFeedRecord = {
  type: 'record'
  record: ComAtprotoRepoStrongRef.Main
  kind: 'feed'
  view: AppBskyFeedDefs.GeneratorView
}

type ResolvedListRecord = {
  type: 'record'
  record: ComAtprotoRepoStrongRef.Main
  kind: 'list'
  view: AppBskyGraphDefs.ListView
}

type ResolvedStarterPackRecord = {
  type: 'record'
  record: ComAtprotoRepoStrongRef.Main
  kind: 'starter-pack'
  view: AppBskyGraphDefs.StarterPackView
}

type ResolvedChatInvite = {
  type: 'chat-invite'
  uri: string
  code: string
  view?: ChatInvitePreview
}

export type ResolvedLink =
  | ResolvedExternalLink
  | ResolvedPostRecord
  | ResolvedFeedRecord
  | ResolvedListRecord
  | ResolvedStarterPackRecord
  | ResolvedChatInvite

export class EmbeddingDisabledError extends Error {
  constructor() {
    super('Embedding is disabled for this record')
  }
}

export async function resolveLink(
  agent: AtpAgent,
  uri: string,
): Promise<ResolvedLink> {
  let resolvedUri = uri
  if (isShortLink(resolvedUri)) {
    resolvedUri = await resolveShortLink(resolvedUri)
  }
  if (isBskyPostUrl(uri)) {
    uri = convertBskyAppUrlIfNeeded(uri)
    const [_0, user, _1, rkey] = uri.split('/').filter(Boolean)
    const recordUri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
    const post = await getPost({uri: recordUri})
    if (post.viewer?.embeddingDisabled) {
      throw new EmbeddingDisabledError()
    }
    return {
      type: 'record',
      record: {
        cid: post.cid,
        uri: post.uri,
      },
      kind: 'post',
      view: post,
    }
  }
  if (isBskyCustomFeedUrl(resolvedUri)) {
    resolvedUri = convertBskyAppUrlIfNeeded(resolvedUri)
    const [_0, handleOrDid, _1, rkey] = resolvedUri.split('/').filter(Boolean)
    const did = await fetchDid(handleOrDid)
    const feed = makeRecordUri(did, 'app.bsky.feed.generator', rkey)
    const res = await agent.app.bsky.feed.getFeedGenerator({feed})
    return {
      type: 'record',
      record: {
        uri: res.data.view.uri,
        cid: res.data.view.cid,
      },
      kind: 'feed',
      view: res.data.view,
    }
  }
  if (isBskyListUrl(resolvedUri)) {
    resolvedUri = convertBskyAppUrlIfNeeded(resolvedUri)
    const [_0, handleOrDid, _1, rkey] = resolvedUri.split('/').filter(Boolean)
    const did = await fetchDid(handleOrDid)
    const list = makeRecordUri(did, 'app.bsky.graph.list', rkey)
    const res = await agent.app.bsky.graph.getList({list})
    return {
      type: 'record',
      record: {
        uri: res.data.list.uri,
        cid: res.data.list.cid,
      },
      kind: 'list',
      view: res.data.list,
    }
  }
  const chatInviteCode = getChatInviteCodeFromUrl(uri)
  if (chatInviteCode) {
    const res = await agent.chat.bsky.group.getJoinLinkPreviews(
      {codes: [chatInviteCode]},
      {headers: DM_SERVICE_HEADERS},
    )
    return {
      type: 'chat-invite',
      uri,
      code: chatInviteCode,
      view: res.data.joinLinkPreviews[0],
    }
  }
  if (isBskyStartUrl(resolvedUri) || isBskyStarterPackUrl(resolvedUri)) {
    const parsed = parseStarterPackUri(resolvedUri)
    if (!parsed) {
      throw new Error(
        'Unexpectedly called getStarterPackAsEmbed with a non-starterpack url',
      )
    }
    const did = await fetchDid(parsed.name)
    const starterPack = createStarterPackUri({did, rkey: parsed.rkey})
    const res = await agent.app.bsky.graph.getStarterPack({starterPack})
    return {
      type: 'record',
      record: {
        uri: res.data.starterPack.uri,
        cid: res.data.starterPack.cid,
      },
      kind: 'starter-pack',
      view: res.data.starterPack,
    }
  }
  return resolveExternal(agent, resolvedUri)

  // Forked from useGetPost. TODO: move into RQ.
  async function getPost({uri}: {uri: string}) {
    const urip = new AtUri(uri)
    if (!urip.host.startsWith('did:')) {
      const res = await agent.resolveHandle({
        handle: urip.host,
      })
      // @ts-expect-error TODO new-sdk-migration
      urip.host = res.data.did
    }
    const res = await agent.getPosts({
      uris: [urip.toString()],
    })
    if (res.success && res.data.posts[0]) {
      return res.data.posts[0]
    }
    throw new Error('getPost: post not found')
  }

  // Forked from useFetchDid. TODO: move into RQ.
  async function fetchDid(handleOrDid: string) {
    let identifier = handleOrDid
    if (!identifier.startsWith('did:')) {
      const res = await agent.resolveHandle({handle: identifier})
      identifier = res.data.did
    }
    return identifier
  }
}

export async function resolveGif(
  agent: AtpAgent,
  gif: Gif,
): Promise<ResolvedExternalLink> {
  const gifUrl = gif.media_formats.gif.url
  const params = new URLSearchParams()
  params.set('hh', String(gif.media_formats.gif.dims[1]))
  params.set('ww', String(gif.media_formats.gif.dims[0]))

  // For Klipy GIFs, embed video format slugs so parseKlipyGif can
  // swap to the right format per platform at render time. Klipy uses
  // different filename slugs per format (unlike Tenor where format is
  // encoded in the URL ID), so this info must travel with the URL.
  try {
    const url = new URL(gifUrl)
    if (url.hostname === 'static.klipy.com') {
      const mp4Slug = getFileSlug(gif.media_formats.mp4?.url)
      const webmSlug = getFileSlug(gif.media_formats.webm?.url)
      if (mp4Slug) params.set('mp4', mp4Slug)
      if (webmSlug) params.set('webm', webmSlug)
    }
  } catch {}

  const uri = `${gifUrl}?${params.toString()}`
  const altText = gif.content_description || gif.title
  return {
    type: 'external',
    uri,
    title: altText,
    description: createGIFDescription(altText),
    thumb: await imageToThumb(gif.media_formats.preview.url),
  }
}

function getFileSlug(url: string | undefined): string | undefined {
  if (!url) return undefined
  const filename = url.split('/').pop()
  if (!filename) return undefined
  const dotIndex = filename.lastIndexOf('.')
  return dotIndex > 0 ? filename.slice(0, dotIndex) : undefined
}

async function resolveExternal(
  agent: AtpAgent,
  uri: string,
): Promise<ResolvedExternalLink> {
  const result = await getLinkMeta(agent, uri)
  return {
    type: 'external',
    uri: result.url,
    title: result.title ?? '',
    description: result.description ?? '',
    thumb: result.image ? await imageToThumb(result.image) : undefined,
    /*
     * New fields from Standard Site integration. Other fields are derived from
     * opengraph/oembed as before.
     */
    associatedRefs: result.associatedRefs,
    view: result.view,
  }
}

export async function imageToThumb(
  imageUri: string,
): Promise<ComposerImage | undefined> {
  try {
    const img = await downloadAndResize({
      uri: imageUri,
      ...IMAGE_SIZE_CONFIG_2K_1MB,
      timeout: 15e3,
    })
    if (img) {
      return await createComposerImage(img)
    }
  } catch {}
}
