import type { StarlightRouteData } from '@astrojs/starlight/route-data'
import type { HookParameters } from '@astrojs/starlight/types'

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('update-context-root-segment-label')

// The first segment represents the entire sidebar and the second one the autogenerated group root segment so when
// accessing a segment, we need to offset indices by 2.
const sidebarUpdateContextSegmentOffset = 2

export async function updatePageSidebar(
  items: StarlightRouteData['sidebar'],
  metadata: ProjectMetadata,
  locale: Locale,
  context: StarlightAutoSidebarContext,
) {
  return updateSidebarItems(items, metadata, locale, context)
}

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

  for (const [index, itemConfig] of context.sidebar.entries()) {
    const item = items[index]

    if (!item) {
      continue
    } else if (isSidebarSlugItemConfig(itemConfig) || isSidebarLinkItemConfig(itemConfig)) {
      result.sidebar.push(item)
    } else if (isSidebarManualGroupConfig(itemConfig) && isSidebarGroup(item)) {
      const {
        sidebar: entries,
        prev,
        next,
      } = await updateSidebarItems(item.entries, metadata, locale, { ...context, sidebar: itemConfig.items })
      if (prev !== undefined) result.prev = prev
      if (next !== undefined) result.next = next
      result.sidebar.push({ ...item, entries })
    } else if (isSidebarAutogeneratedGroupConfig(itemConfig) && isSidebarGroup(item)) {
      const updateContext: SidebarUpdateContext = {
        ...context,
        config: itemConfig,
        locale,
        metadata,
        segments: [createUpdateContextRootSegment(items), { group: item, index }],
      }

      // Sorting and filtering needs to be done before computing the prev/next links.
      await sortAutogeneratedGroupItems(item, getAutogeneratedGroupDirMetadata(updateContext), updateContext)

      const { sidebar: entries, prev, next } = await updateAutogeneratedGroup(item, updateContext)
      if (prev !== undefined) result.prev = prev
      if (next !== undefined) result.next = next
      result.sidebar.push({ ...item, entries })
    }
  }

  return result
}

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

  result.sidebar = await Promise.all(
    group.entries.map(async (item, index) => {
      if (isSidebarGroup(item)) {
        const groupContext = createUpdateContextForGroup(item, index, context)
        const metadata = getAutogeneratedGroupDirMetadata(groupContext)
        const label = metadata?.label ?? item.label
        const collapsed = metadata?.collapsed ?? groupContext.cascade?.collapsed ?? item.collapsed
        const badge = metadata?.badge ?? item.badge

        const { sidebar: entries, prev, next } = await updateAutogeneratedGroup(item, groupContext)
        if (prev !== undefined) result.prev = prev
        if (next !== undefined) result.next = next
        return { ...item, label, entries, collapsed, badge }
      }

      if (item.isCurrent && context.pagination !== false) {
        const { prev, next } = await getEntryPrevNextLinks(getSidebarLinkId(item), context.locale)

        result.prev = getPrevNextLink('prev', index, context) ?? null
        updateResultWithEntryPrevNextLink(result, 'prev', prev)

        result.next = getPrevNextLink('next', index, context) ?? null
        updateResultWithEntryPrevNextLink(result, 'next', next)
      }

      return item
    }),
  )

  return result
}

async function sortAutogeneratedGroupItems(
  group: SidebarGroup,
  metadata: Metadata | undefined,
  context: SidebarUpdateContext,
): Promise<SidebarGroup> {
  const orders = await getAutogeneratedGroupItemsOrder(group.entries, context)
  const collator = new Intl.Collator(getDefaultLang())

  const sort = metadata?.sort === 'reverse-slug' || context.cascade?.sort === 'reverse-slug'

  orders.sort(({ order: aOrder, path: aPath }, { order: bOrder, path: bPath }) => {
    if (aOrder !== bOrder) return aOrder < bOrder ? -1 : 1
    return collator.compare(aPath, bPath) * (sort ? -1 : 1)
  })

  const entries: SidebarItem[] = []

  for (const { hidden, index } of orders) {
    if (hidden) continue
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    entries.push(group.entries[index]!)
  }

  group.entries = entries

  return group
}

function getAutogeneratedGroupItemsOrder(
  items: SidebarItem[],
  context: SidebarUpdateContext,
): Promise<AutogeneratedGroupItemsOrder> {
  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 }
      }

      const itemContext = createUpdateContextForGroup(item, index, context)
      const metadata = getAutogeneratedGroupDirMetadata(itemContext)

      await sortAutogeneratedGroupItems(item, metadata, itemContext)

      const order: AutogeneratedGroupItemsOrder[number] = {
        index,
        order: metadata?.order ?? Number.MAX_VALUE,
        path: getAutoGeneratedGroupPath(item, context),
      }

      if (
        metadata?.hidden ||
        (context.limit && context.segments.length - sidebarUpdateContextSegmentOffset >= context.limit)
      )
        order.hidden = true

      return order
    }),
  )
}

function getPrevNextLink(
  type: 'prev' | 'next',
  index: number,
  context: SidebarUpdateContext,
  segmentCursor = 0,
): SidebarLink | undefined {
  const segment = context.segments.at(segmentCursor - 1)
  if (!segment) return

  const { group, index: groupIndex } = segment
  let item = group.entries[index + 1 * (type === 'next' ? 1 : -1)]

  if (!item) return getPrevNextLink(type, groupIndex, context, segmentCursor - 1)

  if (isSidebarLink(item)) return item
  if (isSidebarGroup(item)) while (isSidebarGroup(item)) item = item.entries.at(type === 'next' ? 0 : -1)

  return item
}

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 getAutogeneratedGroupDirMetadata(context: SidebarUpdateContext) {
  const { config, metadata, segments } = context

  const id = isAutogeneratedGroupRootSegment(context)
    ? config.autogenerate.directory
    : `${config.autogenerate.directory}/${getUpdateContextTrail(context)}`

  const dirMetadata =
    !context.isMultilingual || !context.locale
      ? metadata[id]
      : (metadata[`${context.locale}/${id}`] ?? metadata[DefaultLocale ? `${DefaultLocale}/${id}` : id])

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

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

  return dirMetadata
}

function isSidebarSlugItemConfig(itemConfig: SidebarItemConfig): itemConfig is SidebarSlugItemConfig {
  return typeof itemConfig === 'string' || 'slug' in itemConfig
}

function isSidebarLinkItemConfig(itemConfig: SidebarItemConfig): itemConfig is SidebarLinkItemConfig {
  return typeof itemConfig === 'object' && 'link' in itemConfig
}

export function isSidebarManualGroupConfig(itemConfig: SidebarItemConfig): itemConfig is SidebarManualGroupConfig {
  return typeof itemConfig === 'object' && 'items' in itemConfig
}

export function isSidebarAutogeneratedGroupConfig(
  itemConfig: SidebarItemConfig,
): itemConfig is SidebarAutogeneratedGroupConfig {
  return typeof itemConfig === 'object' && 'autogenerate' in itemConfig
}

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

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

function getAutoGeneratedGroupPath(group: SidebarGroup, context: SidebarUpdateContext) {
  const trail = isAutogeneratedGroupRootSegment(context)
    ? context.config.autogenerate.directory
    : `${context.config.autogenerate.directory}/${getUpdateContextTrail(context)}`

  return `${trail}/${group.label}`
}

function isAutogeneratedGroupRootSegment(context: SidebarUpdateContext) {
  return context.segments.length === sidebarUpdateContextSegmentOffset
}

function createUpdateContextForGroup(
  group: SidebarGroup,
  index: number,
  context: SidebarUpdateContext,
): SidebarUpdateContext {
  return { ...context, segments: [...context.segments, { group, index }] }
}

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

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

function getUpdateContextTrail(context: SidebarUpdateContext) {
  const segments = context.segments.slice(sidebarUpdateContextSegmentOffset)
  return segments.map((segment) => segment.group.label).join('/')
}

export type SidebarUserConfig = NonNullable<HookParameters<'config:setup'>['config']['sidebar']>

export type SidebarItemConfig = SidebarUserConfig[number]
export type SidebarSlugItemConfig = Extract<SidebarItemConfig, string | { slug: string }>
export type SidebarLinkItemConfig = Extract<SidebarItemConfig, { link: string }>
export type SidebarManualGroupConfig = Extract<SidebarItemConfig, { items: SidebarItemConfig[] }>
export type SidebarAutogeneratedGroupConfig = Extract<SidebarItemConfig, { autogenerate: { directory: string } }>

type SidebarItem = StarlightRouteData['sidebar'][number]
export type SidebarLink = Extract<SidebarItem, { type: 'link' }>
type SidebarGroup = Extract<SidebarItem, { type: 'group' }>

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

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

interface SidebarUpdateContext extends StarlightAutoSidebarContext {
  // Contains cascaded metadata that should be applied and not if cascading is enabled.
  cascade?: {
    collapsed?: Metadata['collapsed']
    sort?: Metadata['sort']
  }
  config: SidebarAutogeneratedGroupConfig
  limit?: number
  locale: Locale
  metadata: ProjectMetadata
  segments: { group: SidebarGroup; index: number }[]
}

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