import type { Editor } from "@tiptap/react"
import type { Node as ProseMirrorNode } from "@tiptap/pm/model"
import {
  type DocsBulletStyleId,
  type DocsChecklistStyleId,
  DOCS_DEFAULT_BULLET_STYLE,
  DOCS_DEFAULT_ORDERED_STYLE,
  normalizeBulletStyleId,
  normalizeOrderedStyleId,
  orderedPresetById,
  type DocsOrderedStyleId,
} from "@/lib/drive/docs-list-styles"

type ListKind = "bulletList" | "orderedList" | "taskList"

export type DocsListState = {
  isBulletList: boolean
  isOrderedList: boolean
  isTaskList: boolean
  bulletStyleId: DocsBulletStyleId | null | "mixed"
  orderedStyleId: DocsOrderedStyleId | null | "mixed"
  checklistStyleId: DocsChecklistStyleId | null | "mixed"
  orderedStart: number | "mixed"
}

function mergeMixed<T>(values: T[]): T | "mixed" {
  if (values.length === 0) return values[0] as T
  const first = values[0]
  return values.every((value) => value === first) ? first : "mixed"
}

function findEnclosingLists(
  doc: ProseMirrorNode,
  from: number,
  to: number
): Array<{ pos: number; node: ProseMirrorNode; kind: ListKind }> {
  const results: Array<{ pos: number; node: ProseMirrorNode; kind: ListKind }> = []

  const visit = (pos: number, node: ProseMirrorNode) => {
    if (node.type.name === "bulletList" || node.type.name === "orderedList" || node.type.name === "taskList") {
      results.push({ pos, node, kind: node.type.name as ListKind })
    }
  }

  if (from === to) {
    const $pos = doc.resolve(from)
    for (let depth = $pos.depth; depth > 0; depth -= 1) {
      const node = $pos.node(depth)
      if (node.type.name === "bulletList" || node.type.name === "orderedList" || node.type.name === "taskList") {
        visit($pos.before(depth), node)
        break
      }
    }
    return results
  }

  doc.nodesBetween(from, to, (node, pos) => {
    if (node.type.name === "bulletList" || node.type.name === "orderedList" || node.type.name === "taskList") {
      visit(pos, node)
    }
  })

  return results
}

function updateListAttrs(
  editor: Editor,
  kind: ListKind,
  attrs: Record<string, unknown>
): boolean {
  const { state } = editor
  const lists = findEnclosingLists(state.doc, state.selection.from, state.selection.to).filter(
    (entry) => entry.kind === kind
  )
  if (lists.length === 0) return false

  let tr = state.tr
  let changed = false
  for (const { pos, node } of lists) {
    tr = tr.setNodeMarkup(pos, undefined, { ...node.attrs, ...attrs })
    changed = true
  }
  if (!changed) return false
  editor.view.dispatch(tr.scrollIntoView())
  return true
}

function exitOtherLists(editor: Editor, target: ListKind): boolean {
  let chain = editor.chain().focus()
  let changed = false
  if (target !== "taskList" && editor.isActive("taskList")) {
    chain = chain.toggleTaskList()
    changed = true
  }
  if (target !== "orderedList" && editor.isActive("orderedList")) {
    chain = chain.toggleOrderedList()
    changed = true
  }
  if (target !== "bulletList" && editor.isActive("bulletList")) {
    chain = chain.toggleBulletList()
    changed = true
  }
  if (changed) chain.run()
  return changed
}

export function applyDocsBulletStyle(editor: Editor | null, styleId: DocsBulletStyleId): boolean {
  if (!editor || editor.isDestroyed) return false
  exitOtherLists(editor, "bulletList")
  if (!editor.isActive("bulletList")) {
    editor.chain().focus().toggleBulletList().run()
  }
  return updateListAttrs(editor, "bulletList", {
    bulletStyle: styleId,
  })
}

export function applyDocsOrderedStyle(editor: Editor | null, styleId: DocsOrderedStyleId): boolean {
  if (!editor || editor.isDestroyed) return false
  const wasOrdered = editor.isActive("orderedList")
  exitOtherLists(editor, "orderedList")
  if (!editor.isActive("orderedList")) {
    editor.chain().focus().toggleOrderedList().run()
  }
  const preset = orderedPresetById(styleId)
  const attrs: Record<string, unknown> = {
    orderedStyle: styleId,
    type: preset.olType ?? null,
  }
  if (!wasOrdered) attrs.start = 1
  return updateListAttrs(editor, "orderedList", attrs)
}

export function applyDocsChecklistStyle(
  editor: Editor | null,
  styleId: DocsChecklistStyleId
): boolean {
  if (!editor || editor.isDestroyed) return false
  exitOtherLists(editor, "taskList")
  if (!editor.isActive("taskList")) {
    editor.chain().focus().toggleTaskList().run()
  }
  return updateListAttrs(editor, "taskList", {
    checklistStyle: styleId,
  })
}

export function restartDocsOrderedList(editor: Editor | null): boolean {
  if (!editor || editor.isDestroyed || !editor.isActive("orderedList")) return false
  return updateListAttrs(editor, "orderedList", { start: 1 })
}

export function setDocsOrderedListStart(editor: Editor | null, start: number): boolean {
  if (!editor || editor.isDestroyed || !editor.isActive("orderedList")) return false
  const value = Math.max(1, Math.round(start))
  return updateListAttrs(editor, "orderedList", { start: value })
}

export function continueDocsOrderedList(editor: Editor | null): boolean {
  if (!editor || editor.isDestroyed || !editor.isActive("orderedList")) return false

  const { state } = editor
  const lists = findEnclosingLists(state.doc, state.selection.from, state.selection.to).filter(
    (entry) => entry.kind === "orderedList"
  )
  if (lists.length === 0) return false

  const listPos = lists[0].pos
  const $list = state.doc.resolve(listPos)
  const parent = $list.parent
  const indexInParent = $list.index()

  let nextStart = 1
  for (let i = indexInParent - 1; i >= 0; i -= 1) {
    const sibling = parent.child(i)
    if (sibling.type.name === "orderedList") {
      const start = (sibling.attrs.start as number | undefined) ?? 1
      nextStart = start + sibling.childCount
      break
    }
  }

  return updateListAttrs(editor, "orderedList", { start: nextStart })
}

export function readDocsListState(editor: Editor | null): DocsListState {
  if (!editor || editor.isDestroyed) {
    return {
      isBulletList: false,
      isOrderedList: false,
      isTaskList: false,
      bulletStyleId: null,
      orderedStyleId: null,
      checklistStyleId: null,
      orderedStart: 1,
    }
  }

  const lists = findEnclosingLists(
    editor.state.doc,
    editor.state.selection.from,
    editor.state.selection.to
  )

  const bulletLists = lists.filter((entry) => entry.kind === "bulletList")
  const orderedLists = lists.filter((entry) => entry.kind === "orderedList")
  const taskLists = lists.filter((entry) => entry.kind === "taskList")

  return {
    isBulletList: editor.isActive("bulletList"),
    isOrderedList: editor.isActive("orderedList"),
    isTaskList: editor.isActive("taskList"),
    bulletStyleId:
      bulletLists.length === 0
        ? null
        : mergeMixed(
            bulletLists.map((entry) =>
              normalizeBulletStyleId((entry.node.attrs.bulletStyle as string | undefined) ?? DOCS_DEFAULT_BULLET_STYLE)
            )
          ),
    orderedStyleId:
      orderedLists.length === 0
        ? null
        : mergeMixed(
            orderedLists.map((entry) =>
              normalizeOrderedStyleId(
                (entry.node.attrs.orderedStyle as string | undefined) ?? DOCS_DEFAULT_ORDERED_STYLE
              )
            )
          ),
    checklistStyleId:
      taskLists.length === 0
        ? null
        : mergeMixed(
            taskLists.map(
              (entry) =>
                ((entry.node.attrs.checklistStyle as DocsChecklistStyleId | undefined) ?? "simple") as DocsChecklistStyleId
            )
          ),
    orderedStart:
      orderedLists.length === 0
        ? 1
        : mergeMixed(orderedLists.map((entry) => (entry.node.attrs.start as number | undefined) ?? 1)),
  }
}

export function readDocsOrderedListStartDraft(editor: Editor | null): number {
  const state = readDocsListState(editor)
  if (state.orderedStart === "mixed") return 1
  return state.orderedStart
}
