import type { StarlightRouteData } from '@astrojs/starlight/route-data'
import { slug } from 'github-slugger'

import { stripBase } from './astro'
import type { Metadata, ProjectMetadata } from './metadata'
import { stripLeadingAndTrailingSlash } from './path'
import {
  DefaultLocale,
  getDefaultLang,
  getEntryOrder,
  getEntryPrevNextLinks,
  type EntryData,
  type Locale,
} from './starlight'
import type { StarlightAutoSidebarContext } from './vite'

const updateContextRootSegmentLabel = Symbol.for('starlight-auto-sidebar:update-context-root-segment-label')

export async function updatePageSidebar(
  items: StarlightRouteData['sidebar'],
  metadata: ProjectMetadata,
  locale: Locale,
  context: StarlightAutoSidebarContext,
) {
  const result = await updateSidebarItems(items, metadata, locale, context, [createUpdateContextRootSegment(items)])

  await updateResultWithPrevNextLinks(result, result.sidebar, locale, context)

  return result
}

async function updateSidebarItems(
  items: SidebarItem[],
  metadata: ProjectMetadata,
  locale: Locale,
  context: StarlightAutoSidebarContext,
  segments: SidebarUpdateContextSegment[],
): Promise<SidebarUpdateResult> {
  const result: SidebarUpdateResult = { sidebar: [] }

  for (let index = 0; index < items.length; index++) {
    const item = items[index]
    if (!item) continue

    if (isSidebarAutoItem(item)) {
      const startIndex = index
      const { entries, directory } = getAutogeneratedEntrySlice(items, startIndex)

      const group = createAutogeneratedEntriesGroup(entries)
      const autoSegments = [...segments, { group }]

      const updateContext: SidebarUpdateContext = {
        ...context,
        autogeneratedRootDepth: autoSegments.length,
        currentDirectory: directory,
        locale,
        metadata,
        segments: autoSegments,
      }

      // Sorting and filtering needs to be done before computing the prev/next links.
      const { metadata: groupMetadata, context: nextContext } = getAutogeneratedGroupDirState(updateContext)
      await sortGroupWithAutogeneratedEntries(group, groupMetadata, nextContext)

      const { sidebar } = await updateGroupWithAutogeneratedEntries(group, nextContext)

      items.splice(startIndex, entries.length, ...sidebar)
      index = startIndex + sidebar.length - 1

      result.sidebar.push(...sidebar)
    } else if (isSidebarGroup(item)) {
      const { sidebar } = await updateSidebarItems(item.entries, metadata, locale, context, [
        ...segments,
        { group: item },
      ])

      item.entries = sidebar

      result.sidebar.push({ ...item, entries: sidebar })
    } else {
      result.sidebar.push(item)
    }
  }

  return result
}

async function updateGroupWithAutogeneratedEntries(
  group: SidebarGroup,
  context: SidebarUpdateContext,
): Promise<SidebarUpdateResult> {
  const result: SidebarUpdateResult = { sidebar: [] }

  result.sidebar = await Promise.all(
    group.entries.map(async (item) => {
      if (isSidebarAutoGroup(item)) {
        const groupContext = createUpdateContextForGroup(item, context)
        const { metadata: groupMetadata, context: nextContext } = getAutogeneratedGroupDirState(groupContext)
        const label = groupMetadata?.label ?? item.label
        const collapsed = groupMetadata?.collapsed ?? nextContext.cascade?.collapsed ?? item.collapsed
        const badge = groupMetadata?.badge ?? item.badge

        const { sidebar: entries } = await updateGroupWithAutogeneratedEntries(item, nextContext)
        return { ...item, label, entries, collapsed, badge }
      }

      return item
    }),
  )

  return result
}

async function sortGroupWithAutogeneratedEntries(
  group: SidebarGroup,
  metadata: Metadata | undefined,
  context: SidebarUpdateContext,
): Promise<SidebarGroup> {
  const sort = getAutogeneratedGroupSort(metadata, context)
  const sortEntries = await getAutogeneratedGroupSortEntries(group.entries, context, sort)
  const collator = new Intl.Collator(getDefaultLang())
  const isReverse = sort === 'reverse-slug' || sort === 'reverse-label'

  sortEntries.sort(
    ({ order: aOrder, path: aPath, sortValue: aSortValue }, { order: bOrder, path: bPath, sortValue: bSortValue }) => {
      if (aOrder !== bOrder) return aOrder < bOrder ? -1 : 1

      const comparison = collator.compare(aSortValue, bSortValue)
      if (comparison !== 0) return comparison * (isReverse ? -1 : 1)

      return collator.compare(aPath, bPath)
    },
  )

  const entries: SidebarItem[] = []

  for (const { hidden, index } of sortEntries) {
    if (hidden) continue
    const entry = group.entries[index]
    if (entry) entries.push(entry)
  }

  group.entries = entries

  return group
}

function getAutogeneratedGroupSortEntries(
  items: SidebarItem[],
  context: SidebarUpdateContext,
  sort: AutogeneratedGroupSort,
): Promise<AutogeneratedGroupItemSortEntries> {
  const sortByLabel = sort === 'label' || sort === 'reverse-label'

  return Promise.all(
    items.map(async (item, index) => {
      if (isSidebarLink(item)) {
        const id = getSidebarLinkId(item)
        return {
          index,
          order: await getEntryOrder(id, context.locale),
          path: id,
          sortValue: sortByLabel ? item.label : id,
        }
      }

      const itemContext = createUpdateContextForGroup(item, context)
      const { metadata: groupMetadata, context: nextContext } = getAutogeneratedGroupDirState(itemContext)

      await sortGroupWithAutogeneratedEntries(item, groupMetadata, nextContext)

      const path = itemContext.currentDirectory
      const sortEntry: AutogeneratedGroupItemSortEntries[number] = {
        index,
        order: groupMetadata?.order ?? Number.MAX_VALUE,
        path,
        sortValue: sortByLabel ? (groupMetadata?.label ?? item.label) : path,
      }

      if (
        groupMetadata?.hidden ||
        (context.limit && context.segments.length - context.autogeneratedRootDepth >= context.limit)
      ) {
        sortEntry.hidden = true
      }

      return sortEntry
    }),
  )
}

async function updateResultWithPrevNextLinks(
  result: SidebarUpdateResult,
  sidebar: SidebarItem[],
  locale: Locale,
  context: StarlightAutoSidebarContext,
) {
  if (context.pagination === false) return

  const links = getSidebarLinks(sidebar)
  const current = getCurrentSidebarLink(links)
  if (!current) return

  const { prev, next } = await getEntryPrevNextLinks(getSidebarLinkId(current.link), locale)

  result.prev = links[current.index - 1] ?? null
  updateResultWithEntryPrevNextLink(result, 'prev', prev)

  result.next = links[current.index + 1] ?? null
  updateResultWithEntryPrevNextLink(result, 'next', next)
}

function updateResultWithEntryPrevNextLink(
  result: SidebarUpdateResult,
  type: 'prev' | 'next',
  entryPrevNextLink: EntryData['prev'],
) {
  if (entryPrevNextLink === undefined) return

  if (entryPrevNextLink === false) {
    result[type] = null
  } else if (typeof entryPrevNextLink === 'string' && result[type]) {
    result[type] = { ...result[type], label: entryPrevNextLink }
  } else if (typeof entryPrevNextLink === 'object' && result[type]) {
    result[type] = {
      ...result[type],
      label: entryPrevNextLink.label ?? result[type].label,
      href: entryPrevNextLink.link ?? result[type].href,
    }
  }
}

function getSidebarLinks(items: SidebarItem[]): SidebarLink[] {
  return items.flatMap((item) => (isSidebarLink(item) ? item : getSidebarLinks(item.entries)))
}

function getCurrentSidebarLink(links: SidebarLink[]) {
  for (const [index, link] of links.entries()) {
    if (link.isCurrent) return { index, link }
  }

  return
}

function getAutogeneratedGroupDirState(context: SidebarUpdateContext) {
  const nextContext: SidebarUpdateContext = {
    ...context,
    ...(context.cascade ? { cascade: { ...context.cascade } } : {}),
  }

  const { currentDirectory, metadata, segments } = nextContext

  const dirMetadata =
    !nextContext.isMultilingual || !nextContext.locale
      ? metadata[currentDirectory]
      : (metadata[getLocalizedMetadataDirectory(nextContext.locale, currentDirectory)] ??
        metadata[getLocalizedMetadataDirectory(DefaultLocale, currentDirectory)])

  if (dirMetadata?.depth !== undefined) {
    nextContext.limit = dirMetadata.depth + segments.length - nextContext.autogeneratedRootDepth
  }

  if (dirMetadata?.cascade !== undefined) {
    nextContext.cascade ??= {}
    if (dirMetadata.cascade.includes('collapsed')) nextContext.cascade.collapsed = dirMetadata.collapsed
    if (dirMetadata.cascade.includes('sort')) {
      nextContext.cascade.sort = getAutogeneratedGroupSort(dirMetadata, nextContext)
    }
  }

  return { metadata: dirMetadata, context: nextContext }
}

function getAutogeneratedGroupSort(
  metadata: Metadata | undefined,
  context: SidebarUpdateContext,
): AutogeneratedGroupSort {
  return metadata?.sort ?? context.cascade?.sort ?? 'slug'
}

function isSidebarLink(item: SidebarItem | undefined): item is SidebarLink {
  return item?.type === 'link'
}

function isSidebarAutoLink(item: SidebarItem | undefined): item is SidebarAutoLink {
  return item?.type === 'link' && 'autogenerate' in item
}

function isSidebarGroup(item: SidebarItem | undefined): item is SidebarGroup {
  return item?.type === 'group'
}

function isSidebarAutoGroup(item: SidebarItem | undefined): item is SidebarAutoGroup {
  return item?.type === 'group' && 'autogenerate' in item
}

function isSidebarAutoItem(item: SidebarItem | undefined): item is SidebarAutoLink | SidebarAutoGroup {
  return isSidebarAutoLink(item) || isSidebarAutoGroup(item)
}

function createUpdateContextForGroup(group: SidebarGroup, context: SidebarUpdateContext): SidebarUpdateContext {
  return {
    ...context,
    currentDirectory: getAutogeneratedChildDirectory(context.currentDirectory, group.label),
    segments: [...context.segments, { group }],
  }
}

function createUpdateContextRootSegment(entries: SidebarItem[]): SidebarUpdateContext['segments'][number] {
  return {
    group: { type: 'group', entries, label: String(updateContextRootSegmentLabel), collapsed: false, badge: undefined },
  }
}

function getAutogeneratedChildDirectory(directory: string, label: string) {
  const segment = slug(label)
  return directory ? `${directory}/${segment}` : segment
}

function getLocalizedMetadataDirectory(locale: Locale, directory: string) {
  return locale ? (directory ? `${locale}/${directory}` : locale) : directory
}

function getAutogeneratedEntrySlice(items: SidebarItem[], startIndex: number) {
  const firstItem = items[startIndex]

  if (!isSidebarAutoItem(firstItem)) throw new Error('Expected an autogenerated sidebar item.')

  const directory = stripLeadingAndTrailingSlash(firstItem.autogenerate.directory)
  const entries: SidebarItem[] = []

  for (let index = startIndex; index < items.length; index++) {
    const item = items[index]

    if (!isSidebarAutoItem(item)) break
    if (stripLeadingAndTrailingSlash(item.autogenerate.directory) !== directory) break

    entries.push(item)
  }

  return { directory, entries }
}

// Starlight expands an autogenerated config item to multiple sibling entries. Such entries are wrapped in a fake group
// so that the existing group sorting and prev/next logic can process them similarly to other groups.
function createAutogeneratedEntriesGroup(entries: SidebarItem[]): SidebarGroup {
  return {
    type: 'group',
    entries,
    label: String(updateContextRootSegmentLabel),
    collapsed: false,
    badge: undefined,
  }
}

function getSidebarLinkId(link: SidebarLink) {
  return stripBase(stripLeadingAndTrailingSlash(link.href))
}

type SidebarItem = StarlightRouteData['sidebar'][number]
type SidebarLink = Extract<SidebarItem, { type: 'link' }>
type SidebarAutoLink = Extract<SidebarLink, { autogenerate: { directory: string } }>
type SidebarGroup = Extract<SidebarItem, { type: 'group' }>
type SidebarAutoGroup = Extract<SidebarGroup, { autogenerate: { directory: string } }>

type PrevNextLink = StarlightRouteData['pagination']['next']

interface SidebarUpdateResult {
  sidebar: SidebarItem[]
  prev?: PrevNextLink | null
  next?: PrevNextLink | null
}

interface SidebarUpdateContextSegment {
  group: SidebarGroup
}

interface SidebarUpdateContext extends StarlightAutoSidebarContext {
  autogeneratedRootDepth: number
  // Contains cascaded metadata that should be applied or not if cascading is enabled.
  cascade?: {
    collapsed?: Metadata['collapsed']
    sort?: AutogeneratedGroupSort
  }
  currentDirectory: string
  limit?: number
  locale: Locale
  metadata: ProjectMetadata
  segments: SidebarUpdateContextSegment[]
}

type AutogeneratedGroupItemSortEntries = {
  hidden?: true
  index: number
  order: number
  path: string
  sortValue: string
}[]

type AutogeneratedGroupSort = NonNullable<Metadata['sort']>
