import type { DocParagraphStyleDefinition, DocParagraphStylesCatalog } from "@/lib/drive/docs-paragraph-styles"
import { resolveParagraphStyleDefinition } from "@/lib/drive/docs-paragraph-styles"
import { prepareTablesForDocxExport } from "@/lib/drive/docs-table-export"
import { parseGraphicAttrs, type DocsGraphicAttrs } from "@/lib/drive/docs-graphic-types"
import type { TipTapJSON } from "@/lib/drive/richtext-import"

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value)
}

function blobToDataUrl(blob: Blob): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => resolve(String(reader.result))
    reader.onerror = reject
    reader.readAsDataURL(blob)
  })
}

async function resolveGraphicSrc(src: string | null): Promise<string | null> {
  if (!src) return null
  if (src.startsWith("data:")) return src
  try {
    const res = await fetch(src, { credentials: "include" })
    if (!res.ok) return null
    return blobToDataUrl(await res.blob())
  } catch {
    return null
  }
}

function textMarksFromStyle(def: DocParagraphStyleDefinition): TipTapJSON[] {
  const marks: TipTapJSON[] = []
  if (def.bold) marks.push({ type: "bold" })
  if (def.italic) marks.push({ type: "italic" })
  if (def.underline) marks.push({ type: "underline" })

  const textStyleAttrs: Record<string, unknown> = {}
  if (def.fontFamily) textStyleAttrs.fontFamily = def.fontFamily
  if (def.fontSizePx != null) textStyleAttrs.fontSize = `${def.fontSizePx}px`
  if (def.color) textStyleAttrs.color = def.color
  if (Object.keys(textStyleAttrs).length > 0) {
    marks.push({ type: "textStyle", attrs: textStyleAttrs })
  }
  return marks
}

function applyMarksToTextNodes(node: TipTapJSON, marks: TipTapJSON[]): TipTapJSON {
  if (node.type === "text") {
    const existing = Array.isArray(node.marks) ? (node.marks as TipTapJSON[]) : []
    return { ...node, marks: [...existing, ...marks] }
  }
  if (!Array.isArray(node.content)) return node
  return {
    ...node,
    content: node.content.map((child) =>
      applyMarksToTextNodes(child as TipTapJSON, marks)
    ),
  }
}

function nodeChildren(node: TipTapJSON): TipTapJSON[] {
  return Array.isArray(node.content) ? (node.content as TipTapJSON[]) : []
}

export function applyParagraphStylesForDocxExport(
  content: TipTapJSON,
  catalog: DocParagraphStylesCatalog
): TipTapJSON {
  const walk = (node: TipTapJSON): TipTapJSON => {
    const type = node.type as string | undefined
    const attrs = isRecord(node.attrs) ? { ...node.attrs } : undefined

    if ((type === "paragraph" || type === "heading") && attrs) {
      const styleId = typeof attrs.styleId === "string" ? attrs.styleId : null
      if (styleId) {
        const def = resolveParagraphStyleDefinition(catalog, styleId)
        if (def) {
          if (def.blockType === "heading" && def.level) {
            return walk({
              type: "heading",
              attrs: {
                level: def.level,
                textAlign: def.textAlign ?? attrs.textAlign,
              },
              content: nodeChildren(node).map((child) =>
                applyMarksToTextNodes(child, textMarksFromStyle(def))
              ),
            })
          }

          const nextAttrs: Record<string, unknown> = { ...attrs }
          delete nextAttrs.styleId
          if (def.textAlign) nextAttrs.textAlign = def.textAlign

          let next: TipTapJSON = {
            ...node,
            attrs: nextAttrs,
            content: nodeChildren(node).map((child) =>
              applyMarksToTextNodes(child, textMarksFromStyle(def))
            ),
          }
          if (Array.isArray(next.content)) {
            next = {
              ...next,
              content: next.content.map((child) => walk(child as TipTapJSON)),
            }
          }
          return next
        }
      }
    }

    if (Array.isArray(node.content)) {
      return {
        ...node,
        content: node.content.map((child) => walk(child as TipTapJSON)),
      }
    }
    return node
  }

  return walk(content)
}

export type PreparedGraphicMeta = {
  nodeIndex: number
  attrs: DocsGraphicAttrs
}

async function graphicToImageNode(
  attrs: Record<string, unknown>,
  asInline: boolean
): Promise<TipTapJSON | null> {
  const graphic = parseGraphicAttrs(attrs)
  if (graphic.graphicType !== "image" || !graphic.src) {
    if (graphic.graphicType === "shape" || graphic.graphicType === "gradient") {
      return null
    }
    return null
  }

  const src = await resolveGraphicSrc(graphic.src)
  if (!src) return null

  const imageAttrs: Record<string, unknown> = {
    src,
    alt: graphic.alt || graphic.altTitle || "",
    width: graphic.width,
    height: graphic.height,
  }

  if (graphic.placement === "absolute" || graphic.positionMode === "fixed-on-page") {
    imageAttrs.placement = "absolute"
    imageAttrs.x = graphic.pageX || graphic.x
    imageAttrs.y = graphic.pageY || graphic.y
    imageAttrs.wrap = graphic.wrap
    imageAttrs.floatSide = graphic.floatSide
    imageAttrs.rotationDeg = graphic.rotationDeg
  }

  return { type: "image", attrs: imageAttrs }
}

export async function prepareGraphicsForDocxExport(
  content: TipTapJSON
): Promise<{ content: TipTapJSON; anchoredGraphics: PreparedGraphicMeta[] }> {
  const anchoredGraphics: PreparedGraphicMeta[] = []
  let nodeIndex = 0

  const walk = async (node: TipTapJSON): Promise<TipTapJSON | null> => {
    const type = node.type as string | undefined

    if (type === "docsGraphic" || type === "docsInlineGraphic") {
      const attrs = isRecord(node.attrs) ? node.attrs : {}
      const graphic = parseGraphicAttrs(attrs)
      const usesPageLayer =
        graphic.placement === "absolute" ||
        graphic.positionMode === "fixed-on-page" ||
        graphic.wrap === "behind" ||
        graphic.wrap === "in-front"

      if (usesPageLayer && graphic.src) {
        const resolvedSrc = await resolveGraphicSrc(graphic.src)
        anchoredGraphics.push({
          nodeIndex,
          attrs: resolvedSrc ? { ...graphic, src: resolvedSrc } : graphic,
        })
        nodeIndex += 1
        return null
      }

      const imageNode = await graphicToImageNode(attrs, type === "docsInlineGraphic")
      nodeIndex += 1
      return imageNode
    }

    if (type === "image") {
      const attrs = isRecord(node.attrs) ? { ...node.attrs } : {}
      if (typeof attrs.src === "string") {
        const resolved = await resolveGraphicSrc(attrs.src)
        if (resolved) attrs.src = resolved
      }
      nodeIndex += 1
      return { ...node, attrs }
    }

    if (Array.isArray(node.content)) {
      const nextContent: TipTapJSON[] = []
      for (const child of node.content) {
        const next = await walk(child as TipTapJSON)
        if (next) nextContent.push(next)
      }
      return { ...node, content: nextContent }
    }

    nodeIndex += 1
    return node
  }

  const prepared = await walk(content)
  return {
    content: prepared ?? { type: "doc", content: [] },
    anchoredGraphics,
  }
}

export function stripEditorDecorations(content: TipTapJSON): TipTapJSON {
  const walk = (node: TipTapJSON): TipTapJSON | null => {
    if (node.type === "docsPageFlowSpacer") return null
    if (!Array.isArray(node.content)) return node
    const nextContent = node.content
      .map((child) => walk(child as TipTapJSON))
      .filter((child): child is TipTapJSON => child != null)
    return { ...node, content: nextContent }
  }

  const result = walk(content)
  return result ?? { type: "doc", content: [] }
}

export function prepareListsForDocxExport(content: TipTapJSON): TipTapJSON {
  return content
}

export async function prepareContentForDocxExport(
  content: TipTapJSON,
  catalog: DocParagraphStylesCatalog
): Promise<{ content: TipTapJSON; anchoredGraphics: PreparedGraphicMeta[] }> {
  let prepared = stripEditorDecorations(content)
  prepared = applyParagraphStylesForDocxExport(prepared, catalog)
  prepared = prepareListsForDocxExport(prepared)
  prepared = prepareTablesForDocxExport(prepared)
  return prepareGraphicsForDocxExport(prepared)
}
