import type { Editor } from "@tiptap/react"
import type { DocParagraphStyleDefinition } from "@/lib/drive/docs-paragraph-styles"
import {
  inferParagraphStyleIdFromEditorState,
  normalizeParagraphStyleId,
  resolveParagraphStyleDefinition,
} from "@/lib/drive/docs-paragraph-styles"
import { getActiveParagraphStyleCatalog } from "@/lib/drive/docs-paragraph-style-bridge"
import { fontFamilyStackForName } from "@/lib/drive/docs-font-family"

type BlockNodeType = "paragraph" | "heading"

function blockNodeType(def: DocParagraphStyleDefinition): BlockNodeType {
  return def.blockType === "heading" && def.level ? "heading" : "paragraph"
}

function applyRunMarksToRange(
  editor: Editor,
  from: number,
  to: number,
  def: DocParagraphStyleDefinition
) {
  if (from >= to) return

  let chain = editor.chain().setTextSelection({ from, to })

  const stack = def.fontFamily
    ? def.fontFamily.includes(",")
      ? def.fontFamily
      : fontFamilyStackForName(def.fontFamily)
    : null

  if (stack) chain = chain.setFontFamily(stack)
  else chain = chain.unsetFontFamily()

  if (def.fontSizePx != null) chain = chain.setFontSize(`${def.fontSizePx}px`)
  else chain = chain.unsetFontSize()

  if (def.bold) chain = chain.setMark("bold")
  else chain = chain.unsetMark("bold")

  if (def.italic) chain = chain.setMark("italic")
  else chain = chain.unsetMark("italic")

  if (def.underline) chain = chain.setMark("underline")
  else chain = chain.unsetMark("underline")

  if (def.color) chain = chain.setColor(def.color)
  else chain = chain.unsetColor()

  chain.run()
}

function forEachSelectedBlock(
  editor: Editor,
  fn: (from: number, to: number, nodeType: BlockNodeType) => void
) {
  const { state } = editor
  const { from, to } = state.selection

  state.doc.nodesBetween(from, to, (node, pos) => {
    if (node.type.name !== "paragraph" && node.type.name !== "heading") return
    const start = pos
    const end = pos + node.content.size
    fn(start + 1, end + 1, node.type.name as BlockNodeType)
    return false
  })
}

export function applyParagraphStyleDefinition(
  editor: Editor,
  def: DocParagraphStyleDefinition
) {
  const nodeType = blockNodeType(def)
  const chain = editor.chain().focus() as ReturnType<Editor["chain"]> & {
    setParagraph: () => ReturnType<Editor["chain"]>
    setHeading: (attrs: { level: number }) => ReturnType<Editor["chain"]>
    updateAttributes: (
      type: string,
      attrs: Record<string, unknown>
    ) => ReturnType<Editor["chain"]>
    setTextAlign: (align: string) => ReturnType<Editor["chain"]>
  }

  if (nodeType === "heading" && def.level) {
    chain.setHeading({ level: def.level })
  } else {
    chain.setParagraph()
  }

  chain.updateAttributes(nodeType, {
    styleId: def.id,
  })

  if (def.textAlign) chain.setTextAlign(def.textAlign)
  chain.run()

  forEachSelectedBlock(editor, (blockFrom, blockTo) => {
    applyRunMarksToRange(editor, blockFrom, blockTo, def)
  })

  if (editor.state.selection.empty) {
    const { $from } = editor.state.selection
    if ($from.parent.type.name === "paragraph" || $from.parent.type.name === "heading") {
      applyRunMarksToRange(editor, $from.start(), $from.end(), def)
    }
  }
}

export function applyDocsParagraphStyleById(editor: Editor, styleId: string) {
  const catalog = getActiveParagraphStyleCatalog()
  const def = resolveParagraphStyleDefinition(catalog, styleId)
  if (!def) return false
  applyParagraphStyleDefinition(editor, def)
  return true
}

export function extractParagraphStyleFromSelection(
  editor: Editor,
  styleId: string
): DocParagraphStyleDefinition | null {
  const catalog = getActiveParagraphStyleCatalog()
  const existing =
    resolveParagraphStyleDefinition(catalog, styleId) ??
    resolveParagraphStyleDefinition(catalog, "normal")
  if (!existing) return null

  const { $from } = editor.state.selection
  const node = $from.parent
  if (node.type.name !== "paragraph" && node.type.name !== "heading") return null

  const attrs = node.attrs as { styleId?: string; textAlign?: string }
  const marks = editor.state.storedMarks ?? $from.marks()

  const textStyle = marks.find((m) => m.type.name === "textStyle")
  const fontFamily = (textStyle?.attrs.fontFamily as string | undefined) ?? existing.fontFamily
  const fontSizeRaw = textStyle?.attrs.fontSize as string | undefined
  const fontSizePx = fontSizeRaw ? Number.parseFloat(fontSizeRaw) : existing.fontSizePx

  return {
    ...existing,
    id: normalizeParagraphStyleId(styleId),
    name: existing.name,
    scope: existing.scope,
    blockType: node.type.name === "heading" ? "heading" : "paragraph",
    level: node.type.name === "heading" ? (node.attrs.level as number) : undefined,
    fontFamily,
    fontSizePx: Number.isFinite(fontSizePx) ? fontSizePx : existing.fontSizePx,
    bold: marks.some((m) => m.type.name === "bold"),
    italic: marks.some((m) => m.type.name === "italic"),
    underline: marks.some((m) => m.type.name === "underline"),
    color: (marks.find((m) => m.type.name === "textStyle")?.attrs.color as string) ?? existing.color,
    textAlign:
      (attrs.textAlign as DocParagraphStyleDefinition["textAlign"]) ?? existing.textAlign,
  }
}

export function readActiveParagraphStyleId(editor: Editor): string {
  const headingAttrs = editor.getAttributes("heading")
  if (headingAttrs.styleId) return normalizeParagraphStyleId(String(headingAttrs.styleId))

  const paragraphAttrs = editor.getAttributes("paragraph")
  if (paragraphAttrs.styleId) return normalizeParagraphStyleId(String(paragraphAttrs.styleId))

  return inferParagraphStyleIdFromEditorState({
    isHeading: editor.isActive("heading"),
    headingLevel: headingAttrs.level as number | undefined,
  })
}
