import { type DiffLineAnnotation, type SelectedLineRange } from "@pierre/diffs"
import { createEffect, createMemo, createSignal, onCleanup, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { render as renderSolid } from "solid-js/web"
import { useI18n } from "@opencode-ai/ui/context/i18n"
import { createHoverCommentUtility } from "../pierre/comment-hover"
import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge"
import { LineComment, LineCommentEditor, type LineCommentEditorProps } from "./line-comment"

export type LineCommentAnnotationMeta<T> =
  | { kind: "comment"; key: string; comment: T }
  | { kind: "draft"; key: string; range: SelectedLineRange }

export type LineCommentAnnotation<T> = {
  lineNumber: number
  side?: "additions" | "deletions"
  metadata: LineCommentAnnotationMeta<T>
}

type LineCommentAnnotationsProps<T> = {
  comments: Accessor<T[]>
  getCommentId: (comment: T) => string
  getCommentSelection: (comment: T) => SelectedLineRange
  draftRange: Accessor<SelectedLineRange | null>
  draftKey: Accessor<string>
}

type LineCommentAnnotationsWithSideProps<T> = LineCommentAnnotationsProps<T> & {
  getSide: (range: SelectedLineRange) => "additions" | "deletions"
}

type HoverCommentLine = {
  lineNumber: number
  side?: "additions" | "deletions"
}

export type LineCommentStateProps<T> = {
  opened: Accessor<T | null>
  setOpened: (id: T | null) => void
  selected: Accessor<SelectedLineRange | null>
  setSelected: (range: SelectedLineRange | null) => void
  commenting: Accessor<SelectedLineRange | null>
  setCommenting: (range: SelectedLineRange | null) => void
  syncSelected?: (range: SelectedLineRange | null) => void
  hoverSelected?: (range: SelectedLineRange) => void
}

export type LineCommentShape = {
  id: string
  selection: SelectedLineRange
  comment: string
}

type LineCommentControllerProps<T extends LineCommentShape> = {
  comments: Accessor<T[]>
  draftKey: Accessor<string>
  label: string
  mention?: LineCommentEditorProps["mention"]
  state: LineCommentStateProps<string>
  onSubmit: (input: { comment: string; selection: SelectedLineRange }) => void
  onUpdate?: (input: { id: string; comment: string; selection: SelectedLineRange }) => void
  onDelete?: (comment: T) => void
  renderCommentActions?: (comment: T, controls: { edit: VoidFunction; remove: VoidFunction }) => JSX.Element
  editSubmitLabel?: string
  onDraftPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent>
  getHoverSelectedRange?: Accessor<SelectedLineRange | null>
  cancelDraftOnCommentToggle?: boolean
  clearSelectionOnSelectionEndNull?: boolean
}

type LineCommentControllerWithSideProps<T extends LineCommentShape> = LineCommentControllerProps<T> & {
  getSide: (range: SelectedLineRange) => "additions" | "deletions"
}

type CommentProps = {
  id?: string
  open: boolean
  comment: JSX.Element
  selection: JSX.Element
  actions?: JSX.Element
  editor?: DraftProps
  onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
  onMouseEnter?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
}

type DraftProps = {
  value: string
  selection: JSX.Element
  mention?: LineCommentEditorProps["mention"]
  onInput: (value: string) => void
  onCancel: VoidFunction
  onSubmit: (value: string) => void
  onPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent>
  cancelLabel?: string
  submitLabel?: string
}

// Generic host machinery shared by the v1 and v2 annotation renderers: each
// annotation key gets a detached DOM host with its own Solid root, updated in
// place through a signal so Pierre can reparent the host without re-rendering.
export function createLineCommentAnnotationRenderer<T, C, D>(props: {
  renderComment: (comment: T) => C
  renderDraft: (range: SelectedLineRange) => D
  commentElement: (view: Accessor<C>) => JSX.Element
  draftElement: (view: Accessor<D>) => JSX.Element
}) {
  const nodes = new Map<
    string,
    {
      host: HTMLDivElement
      dispose: VoidFunction
      setMeta: (meta: LineCommentAnnotationMeta<T>) => void
    }
  >()

  const mount = (meta: LineCommentAnnotationMeta<T>) => {
    if (typeof document === "undefined") return

    const host = document.createElement("div")
    host.setAttribute("data-prevent-autofocus", "")
    const [current, setCurrent] = createSignal(meta)

    const dispose = renderSolid(() => {
      const active = current()
      if (active.kind === "comment") {
        const view = createMemo(() => {
          const next = current()
          if (next.kind !== "comment") return props.renderComment(active.comment)
          return props.renderComment(next.comment)
        })
        return props.commentElement(view)
      }

      const view = createMemo(() => {
        const next = current()
        if (next.kind !== "draft") return props.renderDraft(active.range)
        return props.renderDraft(next.range)
      })
      return props.draftElement(view)
    }, host)

    const node = { host, dispose, setMeta: setCurrent }
    nodes.set(meta.key, node)
    return node
  }

  const render = <A extends { metadata: LineCommentAnnotationMeta<T> }>(annotation: A) => {
    const meta = annotation.metadata
    const node = nodes.get(meta.key) ?? mount(meta)
    if (!node) return
    node.setMeta(meta)
    return node.host
  }

  const reconcile = <A extends { metadata: LineCommentAnnotationMeta<T> }>(annotations: A[]) => {
    const next = new Set(annotations.map((annotation) => annotation.metadata.key))
    for (const [key, node] of nodes) {
      if (next.has(key)) continue
      node.dispose()
      nodes.delete(key)
    }
  }

  const cleanup = () => {
    for (const [, node] of nodes) node.dispose()
    nodes.clear()
  }

  return { render, reconcile, cleanup }
}

function lineCommentElement(view: Accessor<CommentProps>) {
  return (
    <Show
      when={view().editor}
      fallback={
        <LineComment
          inline
          id={view().id}
          open={view().open}
          comment={view().comment}
          selection={view().selection}
          actions={view().actions}
          onClick={view().onClick}
          onMouseEnter={view().onMouseEnter}
        />
      }
    >
      <LineCommentEditor
        inline
        id={view().id}
        value={view().editor!.value}
        selection={view().editor!.selection}
        onInput={view().editor!.onInput}
        onCancel={view().editor!.onCancel}
        onSubmit={view().editor!.onSubmit}
        onPopoverFocusOut={view().editor!.onPopoverFocusOut}
        cancelLabel={view().editor!.cancelLabel}
        submitLabel={view().editor!.submitLabel}
        mention={view().editor!.mention}
      />
    </Show>
  )
}

function lineCommentDraftElement(view: Accessor<DraftProps>) {
  return (
    <LineCommentEditor
      inline
      value={view().value}
      selection={view().selection}
      onInput={view().onInput}
      onCancel={view().onCancel}
      onSubmit={view().onSubmit}
      onPopoverFocusOut={view().onPopoverFocusOut}
      mention={view().mention}
    />
  )
}

export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
  const [state, setState] = createStore({
    draft: "",
    editing: null as T | null,
  })
  const draft = () => state.draft
  const setDraft = (value: string) => setState("draft", value)
  const editing = () => state.editing
  const setEditing = (value: T | null) => setState("editing", typeof value === "function" ? () => value : value)

  const toRange = (range: SelectedLineRange | null) => (range ? cloneSelectedLineRange(range) : null)
  const setSelected = (range: SelectedLineRange | null) => {
    const next = toRange(range)
    props.setSelected(next)
    props.syncSelected?.(toRange(next))
    return next
  }

  const setCommenting = (range: SelectedLineRange | null) => {
    const next = toRange(range)
    props.setCommenting(next)
    return next
  }

  const closeComment = () => {
    props.setOpened(null)
  }

  const cancelDraft = () => {
    setDraft("")
    setEditing(null)
    setCommenting(null)
  }

  const reset = () => {
    setDraft("")
    setEditing(null)
    props.setOpened(null)
    props.setSelected(null)
    props.setCommenting(null)
  }

  const openComment = (id: T, range: SelectedLineRange, options?: { cancelDraft?: boolean }) => {
    if (options?.cancelDraft) cancelDraft()
    props.setOpened(id)
    setSelected(range)
  }

  const toggleComment = (id: T, range: SelectedLineRange, options?: { cancelDraft?: boolean }) => {
    if (options?.cancelDraft) cancelDraft()
    const next = props.opened() === id ? null : id
    props.setOpened(next)
    setSelected(range)
  }

  const openDraft = (range: SelectedLineRange) => {
    const next = toRange(range)
    setDraft("")
    setEditing(null)
    closeComment()
    setSelected(next)
    setCommenting(next)
  }

  const openEditor = (id: T, range: SelectedLineRange, value: string) => {
    closeComment()
    setSelected(range)
    props.setCommenting(null)
    setEditing(id)
    setDraft(value)
  }

  const hoverComment = (range: SelectedLineRange) => {
    const next = toRange(range)
    if (!next) return
    if (props.hoverSelected) {
      props.hoverSelected(next)
      return
    }

    setSelected(next)
  }

  return {
    draft,
    setDraft,
    editing,
    opened: props.opened,
    selected: props.selected,
    commenting: props.commenting,
    isOpen: (id: T) => props.opened() === id,
    isEditing: (id: T) => editing() === id,
    closeComment,
    openComment,
    toggleComment,
    openDraft,
    openEditor,
    hoverComment,
    cancelDraft,
    select: setSelected,
    reset,
  }
}

export function createLineCommentController<T extends LineCommentShape>(
  props: LineCommentControllerWithSideProps<T>,
): {
  note: ReturnType<typeof createLineCommentState<string>>
  annotations: Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>
  renderAnnotation: ReturnType<
    typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
  >["renderAnnotation"]
  renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
  onLineSelected: (range: SelectedLineRange | null) => void
  onLineSelectionEnd: (range: SelectedLineRange | null) => void
}
export function createLineCommentController<T extends LineCommentShape>(
  props: LineCommentControllerProps<T>,
): {
  note: ReturnType<typeof createLineCommentState<string>>
  annotations: Accessor<LineCommentAnnotation<T>[]>
  renderAnnotation: ReturnType<
    typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
  >["renderAnnotation"]
  renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
  onLineSelected: (range: SelectedLineRange | null) => void
  onLineSelectionEnd: (range: SelectedLineRange | null) => void
}
export function createLineCommentController<T extends LineCommentShape>(
  props: LineCommentControllerProps<T> | LineCommentControllerWithSideProps<T>,
) {
  const i18n = useI18n()
  const note = createLineCommentState<string>(props.state)

  const annotations =
    "getSide" in props
      ? createLineCommentAnnotations({
          comments: props.comments,
          getCommentId: (comment) => comment.id,
          getCommentSelection: (comment) => comment.selection,
          draftRange: note.commenting,
          draftKey: props.draftKey,
          getSide: props.getSide,
        })
      : createLineCommentAnnotations({
          comments: props.comments,
          getCommentId: (comment) => comment.id,
          getCommentSelection: (comment) => comment.selection,
          draftRange: note.commenting,
          draftKey: props.draftKey,
        })

  const { renderAnnotation } = createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>({
    annotations,
    commentElement: lineCommentElement,
    draftElement: lineCommentDraftElement,
    renderComment: (comment) => {
      const edit = () => note.openEditor(comment.id, comment.selection, comment.comment)
      const remove = () => {
        note.reset()
        props.onDelete?.(comment)
      }

      return {
        id: comment.id,
        get open() {
          return note.isOpen(comment.id) || note.isEditing(comment.id)
        },
        comment: comment.comment,
        selection: formatSelectedLineLabel(comment.selection, i18n.t),
        get actions() {
          return props.renderCommentActions?.(comment, { edit, remove })
        },
        get editor() {
          return note.isEditing(comment.id)
            ? {
                get value() {
                  return note.draft()
                },
                selection: formatSelectedLineLabel(comment.selection, i18n.t),
                mention: props.mention,
                onInput: note.setDraft,
                onCancel: note.cancelDraft,
                onSubmit: (value: string) => {
                  props.onUpdate?.({
                    id: comment.id,
                    comment: value,
                    selection: cloneSelectedLineRange(comment.selection),
                  })
                  note.cancelDraft()
                },
                submitLabel: props.editSubmitLabel,
              }
            : undefined
        },
        onMouseEnter: () => note.hoverComment(comment.selection),
        onClick: () => {
          if (note.isEditing(comment.id)) return
          note.toggleComment(comment.id, comment.selection, { cancelDraft: props.cancelDraftOnCommentToggle })
        },
      }
    },
    renderDraft: (range) => ({
      get value() {
        return note.draft()
      },
      selection: formatSelectedLineLabel(range, i18n.t),
      mention: props.mention,
      onInput: note.setDraft,
      onCancel: () => {
        note.cancelDraft()
        note.select(null)
      },
      onSubmit: (comment) => {
        props.onSubmit({ comment, selection: cloneSelectedLineRange(range) })
        note.cancelDraft()
      },
      onPopoverFocusOut: props.onDraftPopoverFocusOut,
    }),
  })

  const renderGutterUtility = createLineCommentGutterRenderer({
    label: props.label,
    getSelectedRange: () => {
      if (note.opened()) return null
      return props.getHoverSelectedRange?.() ?? note.selected()
    },
    onOpenDraft: note.openDraft,
  })

  const onLineSelected = (range: SelectedLineRange | null) => {
    if (!range) {
      note.select(null)
      note.cancelDraft()
      return
    }

    note.select(range)
  }

  const onLineSelectionEnd = (range: SelectedLineRange | null) => {
    if (!range) {
      if (props.clearSelectionOnSelectionEndNull) note.select(null)
      note.cancelDraft()
      return
    }

    note.openDraft(range)
  }

  return {
    note,
    annotations,
    renderAnnotation,
    renderGutterUtility,
    onLineSelected,
    onLineSelectionEnd,
  }
}

export function createLineCommentAnnotations<T>(
  props: LineCommentAnnotationsWithSideProps<T>,
): Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>
export function createLineCommentAnnotations<T>(
  props: LineCommentAnnotationsProps<T>,
): Accessor<LineCommentAnnotation<T>[]>
export function createLineCommentAnnotations<T>(
  props: LineCommentAnnotationsProps<T> | LineCommentAnnotationsWithSideProps<T>,
) {
  const line = (range: SelectedLineRange) => Math.max(range.start, range.end)

  if ("getSide" in props) {
    return createMemo<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>(
      () => {
        const list = props.comments().map((comment) => {
          const range = props.getCommentSelection(comment)
          return {
            side: props.getSide(range),
            lineNumber: line(range),
            metadata: {
              kind: "comment",
              key: `comment:${props.getCommentId(comment)}`,
              comment,
            } satisfies LineCommentAnnotationMeta<T>,
          }
        })

        const range = props.draftRange()
        if (!range) return list

        return [
          ...list,
          {
            side: props.getSide(range),
            lineNumber: line(range),
            metadata: {
              kind: "draft",
              key: `draft:${props.draftKey()}`,
              range,
            } satisfies LineCommentAnnotationMeta<T>,
          },
        ]
      },
      [],
      // Stable identity for unchanged annotations avoids no-op diff rerenders downstream.
      { equals: sameAnnotationLists },
    )
  }

  return createMemo<LineCommentAnnotation<T>[]>(
    () => {
      const list = props.comments().map((comment) => {
        const range = props.getCommentSelection(comment)
        const entry: LineCommentAnnotation<T> = {
          lineNumber: line(range),
          metadata: {
            kind: "comment",
            key: `comment:${props.getCommentId(comment)}`,
            comment,
          },
        }

        return entry
      })

      const range = props.draftRange()
      if (!range) return list

      const draft: LineCommentAnnotation<T> = {
        lineNumber: line(range),
        metadata: {
          kind: "draft",
          key: `draft:${props.draftKey()}`,
          range,
        },
      }

      return [...list, draft]
    },
    [],
    { equals: sameAnnotationLists },
  )
}

type AnnotationListItem = {
  lineNumber: number
  side?: unknown
  metadata: { kind: string; key: string; comment?: unknown; range?: unknown }
}

function sameAnnotationLists(previous: AnnotationListItem[], next: AnnotationListItem[]) {
  if (previous.length !== next.length) return false
  return previous.every((item, index) => {
    const other = next[index]!
    return (
      item.lineNumber === other.lineNumber &&
      item.side === other.side &&
      item.metadata.kind === other.metadata.kind &&
      item.metadata.key === other.metadata.key &&
      item.metadata.comment === other.metadata.comment &&
      item.metadata.range === other.metadata.range
    )
  })
}

export function createManagedLineCommentAnnotationRenderer<T, C, D>(props: {
  annotations: Accessor<LineCommentAnnotation<T>[]>
  renderComment: (comment: T) => C
  renderDraft: (range: SelectedLineRange) => D
  commentElement: (view: Accessor<C>) => JSX.Element
  draftElement: (view: Accessor<D>) => JSX.Element
}) {
  const renderer = createLineCommentAnnotationRenderer<T, C, D>({
    renderComment: props.renderComment,
    renderDraft: props.renderDraft,
    commentElement: props.commentElement,
    draftElement: props.draftElement,
  })

  createEffect(() => {
    renderer.reconcile(props.annotations())
  })

  onCleanup(() => {
    renderer.cleanup()
  })

  return {
    renderAnnotation: renderer.render,
  }
}

export function createLineCommentGutterRenderer(props: {
  label: string
  getSelectedRange: Accessor<SelectedLineRange | null>
  onOpenDraft: (range: SelectedLineRange) => void
}) {
  return (getHoveredLine: () => HoverCommentLine | undefined) =>
    createHoverCommentUtility({
      label: props.label,
      getHoveredLine,
      onSelect: (hovered) => {
        const current = props.getSelectedRange()
        if (current && lineInSelectedRange(current, hovered.lineNumber, hovered.side)) {
          props.onOpenDraft(cloneSelectedLineRange(current))
          return
        }

        const range: SelectedLineRange = {
          start: hovered.lineNumber,
          end: hovered.lineNumber,
        }
        if (hovered.side) range.side = hovered.side
        props.onOpenDraft(range)
      },
    })
}
