import type { TipTapJSON } from "@/lib/drive/richtext-import"
import type { DocsGraphicFloatSide, DocsGraphicPlacement, DocsGraphicPositionMode, DocsGraphicWrap } from "./docs-graphic-types.ts"

type DocxArchive = Record<string, Uint8Array>

const EMU_PER_PX = 914400 / 96

export type DocxGraphicPosition = {
  width: number
  height: number
  x: number
  y: number
  pageX: number
  pageY: number
  pageIndex: number
  placement: DocsGraphicPlacement
  wrap: DocsGraphicWrap
  floatSide: DocsGraphicFloatSide
  rotationDeg: number
  zIndex: number
  behindDoc: boolean
  positionMode: DocsGraphicPositionMode
  wrapMarginMm: number
  cropX?: number
  cropY?: number
  cropWidth?: number
  cropHeight?: number
}

function decodeXml(bytes: Uint8Array | undefined): string {
  if (!bytes) return ""
  return new TextDecoder().decode(bytes)
}

function emuToPx(emu: number): number {
  return Math.round(emu / EMU_PER_PX)
}

function readIntAttr(fragment: string, name: string): number | null {
  const match = fragment.match(new RegExp(`\\b${name}="(-?\\d+)"`, "i"))
  if (!match) return null
  const value = Number.parseInt(match[1] ?? "", 10)
  return Number.isFinite(value) ? value : null
}

function parseWrapType(anchorXml: string): DocsGraphicWrap {
  if (/<wp:wrapNone\b/i.test(anchorXml)) return "top-bottom"
  if (/<wp:wrapTopAndBottom\b/i.test(anchorXml)) return "top-bottom"
  if (/<wp:wrapTight\b/i.test(anchorXml)) return "tight"
  if (/<wp:wrapThrough\b/i.test(anchorXml)) return "through"
  if (/<wp:wrapSquare\b/i.test(anchorXml)) return "square"
  if (/<wp:wrapNone\b/i.test(anchorXml)) return "square"
  return "square"
}

function parseFloatSide(anchorXml: string): DocsGraphicFloatSide {
  const align = anchorXml.match(/<wp:positionH\b[^>]*>[\s\S]*?<wp:align>(\w+)<\/wp:align>/i)?.[1]
  if (align === "right") return "right"
  if (align === "center") return "center"
  return "left"
}

function parseRotation(drawingXml: string): number {
  const rot = drawingXml.match(/\brot="(-?\d+)"/i)?.[1]
  if (!rot) return 0
  // OOXML rotation is in 60000ths of a degree
  return Math.round(Number(rot) / 60000)
}

function parseCropFromBlip(blipXml: string): Pick<
  DocxGraphicPosition,
  "cropX" | "cropY" | "cropWidth" | "cropHeight"
> | null {
  const srcRect = blipXml.match(/<a:srcRect\b[^>]*\/?>/i)?.[0]
  if (!srcRect) return null
  const l = readIntAttr(srcRect, "l") ?? 0
  const t = readIntAttr(srcRect, "t") ?? 0
  const r = readIntAttr(srcRect, "r") ?? 0
  const b = readIntAttr(srcRect, "b") ?? 0
  if (l === 0 && t === 0 && r === 0 && b === 0) return null
  const cropX = l / 100000
  const cropY = t / 100000
  const cropWidth = 1 - (l + r) / 100000
  const cropHeight = 1 - (t + b) / 100000
  return { cropX, cropY, cropWidth, cropHeight }
}

function parseWrapMarginMm(block: string): number {
  const wrapSquare = block.match(/<wp:wrapSquare\b[^>]*\/?>/i)?.[0]
  if (!wrapSquare) return 3
  const distL = readIntAttr(wrapSquare, "distL") ?? readIntAttr(wrapSquare, "distT")
  if (distL == null) return 3
  const px = emuToPx(distL)
  return Math.max(0, Math.round((px / 96) * 25.4))
}

function parsePositionMode(block: string, inline: boolean): DocsGraphicPositionMode {
  if (inline) return "move-with-text"
  const posH = block.match(/<wp:positionH\b[^>]*relativeFrom="(\w+)"/i)?.[1]
  const posV = block.match(/<wp:positionV\b[^>]*relativeFrom="(\w+)"/i)?.[1]
  if (
    posH === "page" ||
    posH === "margin" ||
    posH === "column" ||
    posV === "page" ||
    posV === "margin" ||
    posV === "paragraph"
  ) {
    if (posH === "page" || posH === "margin" || posV === "page" || posV === "margin") {
      return "fixed-on-page"
    }
  }
  return "move-with-text"
}

function parseDrawingBlock(block: string, inline: boolean): DocxGraphicPosition | null {
  const extent = block.match(/<wp:extent\b[^>]*\/?>/i)?.[0]
  if (!extent) return null
  const cx = readIntAttr(extent, "cx")
  const cy = readIntAttr(extent, "cy")
  if (cx == null || cy == null) return null

  const behindDoc = /<wp:anchor\b[^>]*\bbehindDoc="1"/i.test(block)
  const inFront = /<wp:anchor\b[^>]*\blayoutInCell="0"/i.test(block) && !behindDoc

  let x = 0
  let y = 0
  if (!inline) {
    const posH = block.match(/<wp:positionH\b[^>]*>[\s\S]*?<\/wp:positionH>/i)?.[0]
    const posV = block.match(/<wp:positionV\b[^>]*>[\s\S]*?<\/wp:positionV>/i)?.[0]
    const posOffsetH = posH?.match(/<wp:posOffset>(-?\d+)<\/wp:posOffset>/i)?.[1]
    const posOffsetV = posV?.match(/<wp:posOffset>(-?\d+)<\/wp:posOffset>/i)?.[1]
    if (posOffsetH) x = emuToPx(Number(posOffsetH))
    if (posOffsetV) y = emuToPx(Number(posOffsetV))
  }

  const wrap = inline ? "inline" : behindDoc ? "behind" : inFront ? "in-front" : parseWrapType(block)
  const placement: DocsGraphicPlacement = inline ? "inline" : "absolute"

  const blip = block.match(/<a:blip\b[^>]*\/?>/i)?.[0] ?? ""
  const crop = parseCropFromBlip(blip)

  return {
    width: Math.max(24, emuToPx(cx)),
    height: Math.max(24, emuToPx(cy)),
    x,
    y,
    pageX: x,
    pageY: y,
    pageIndex: 0,
    placement,
    wrap,
    floatSide: inline ? "left" : parseFloatSide(block),
    rotationDeg: parseRotation(block),
    zIndex: behindDoc ? 0 : inFront ? 20 : 1,
    behindDoc,
    positionMode: parsePositionMode(block, inline),
    wrapMarginMm: parseWrapMarginMm(block),
    ...crop,
  }
}

/** Extract ordered graphic positions from word/document.xml. */
export function extractGraphicPositionsFromDocx(archive: DocxArchive): DocxGraphicPosition[] {
  const documentXml = decodeXml(archive["word/document.xml"])
  if (!documentXml) return []

  const positions: DocxGraphicPosition[] = []
  const drawingPattern = /<w:drawing\b[^>]*>[\s\S]*?<\/w:drawing>/gi
  for (const match of documentXml.matchAll(drawingPattern)) {
    const block = match[0]
    const inline = /<wp:inline\b/i.test(block)
    const anchor = /<wp:anchor\b/i.test(block)
    if (!inline && !anchor) continue
    const parsed = parseDrawingBlock(block, inline)
    if (parsed) positions.push(parsed)
  }
  return positions
}

function isGraphicNode(node: TipTapJSON): boolean {
  return (
    node.type === "image" ||
    node.type === "docsGraphic" ||
    node.type === "docsInlineGraphic"
  )
}

function applyPositionToNode(node: TipTapJSON, pos: DocxGraphicPosition): TipTapJSON {
  const attrs = (node.attrs as Record<string, unknown>) ?? {}
  return {
    ...node,
    attrs: {
      ...attrs,
      width: pos.width,
      height: pos.height,
      x: pos.x,
      y: pos.y,
      pageX: pos.pageX,
      pageY: pos.pageY,
      pageIndex: pos.pageIndex,
      placement: pos.placement,
      wrap: pos.wrap,
      floatSide: pos.floatSide,
      rotationDeg: pos.rotationDeg,
      zIndex: pos.zIndex,
      positionMode: pos.positionMode,
      wrapMarginMm: pos.wrapMarginMm,
      ...(pos.cropX != null ? { cropX: pos.cropX } : {}),
      ...(pos.cropY != null ? { cropY: pos.cropY } : {}),
      ...(pos.cropWidth != null ? { cropWidth: pos.cropWidth } : {}),
      ...(pos.cropHeight != null ? { cropHeight: pos.cropHeight } : {}),
    },
  }
}

function collectGraphicNodes(content: TipTapJSON): { path: number[]; node: TipTapJSON }[] {
  const results: { path: number[]; node: TipTapJSON }[] = []

  const walk = (node: TipTapJSON, path: number[]) => {
    if (isGraphicNode(node)) {
      results.push({ path, node })
    }
    if (Array.isArray(node.content)) {
      node.content.forEach((child, index) => {
        if (child && typeof child === "object") walk(child as TipTapJSON, [...path, index])
      })
    }
  }

  walk(content, [])
  return results
}

function setNodeAtPath(root: TipTapJSON, path: number[], node: TipTapJSON): TipTapJSON {
  if (path.length === 0) return node
  const [head, ...rest] = path
  const content = Array.isArray(root.content) ? [...root.content] : []
  content[head!] = setNodeAtPath(content[head!] as TipTapJSON, rest, node)
  return { ...root, content }
}

/** Enrich imported TipTap content with OOXML anchor/inline positioning. */
export async function enrichGraphicsFromDocxPositions(
  buffer: ArrayBuffer,
  content: TipTapJSON
): Promise<TipTapJSON> {
  try {
    const { unzipSync } = await import("fflate")
    const archive = unzipSync(new Uint8Array(buffer)) as DocxArchive
    const positions = extractGraphicPositionsFromDocx(archive)
    if (positions.length === 0) return content

    const graphics = collectGraphicNodes(content)
    let result = content
    const count = Math.min(graphics.length, positions.length)
    for (let i = 0; i < count; i++) {
      const { path, node } = graphics[i]!
      const pos = positions[i]!
      result = setNodeAtPath(result, path, applyPositionToNode(node, pos))
    }
    return result
  } catch {
    return content
  }
}
