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 "../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"
}

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
}

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
}

export function createLineCommentAnnotationRenderer<T>(props: {
  renderComment: (comment: T) => CommentProps
  renderDraft: (range: SelectedLineRange) => DraftProps
}) {
  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 (
          <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>
        )
      }

      const view = createMemo(() => {
        const next = current()
        if (next.kind !== "draft") return props.renderDraft(active.range)
        return props.renderDraft(next.range)
      })
      return (
        <LineCommentEditor
          inline
          value={view().value}
          selection={view().selection}
          onInput={view().onInput}
          onCancel={view().onCancel}
          onSubmit={view().onSubmit}
          onPopoverFocusOut={view().onPopoverFocusOut}
          mention={view().mention}
        />
      )
    }, 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 }
}

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)
  }

  const finishSelection = (range: SelectedLineRange) => {
    closeComment()
    setSelected(range)
    cancelDraft()
  }

  createEffect(() => {
    props.commenting()
    setDraft("")
  })

  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,
    finishSelection,
    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>>["renderAnnotation"]
  renderHoverUtility: ReturnType<typeof createLineCommentHoverRenderer>
  onLineSelected: (range: SelectedLineRange | null) => void
  onLineSelectionEnd: (range: SelectedLineRange | null) => void
  onLineNumberSelectionEnd: (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>>["renderAnnotation"]
  renderHoverUtility: ReturnType<typeof createLineCommentHoverRenderer>
  onLineSelected: (range: SelectedLineRange | null) => void
  onLineSelectionEnd: (range: SelectedLineRange | null) => void
  onLineNumberSelectionEnd: (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>({
    annotations,
    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,
      onSubmit: (comment) => {
        props.onSubmit({ comment, selection: cloneSelectedLineRange(range) })
        note.cancelDraft()
      },
      onPopoverFocusOut: props.onDraftPopoverFocusOut,
    }),
  })

  const renderHoverUtility = createLineCommentHoverRenderer({
    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.finishSelection(range)
  }

  const onLineNumberSelectionEnd = (range: SelectedLineRange | null) => {
    if (!range) return
    note.openDraft(range)
  }

  return {
    note,
    annotations,
    renderAnnotation,
    renderHoverUtility,
    onLineSelected,
    onLineSelectionEnd,
    onLineNumberSelectionEnd,
  }
}

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>,
        },
      ]
    })
  }

  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]
  })
}

export function createManagedLineCommentAnnotationRenderer<T>(props: {
  annotations: Accessor<LineCommentAnnotation<T>[]>
  renderComment: (comment: T) => CommentProps
  renderDraft: (range: SelectedLineRange) => DraftProps
}) {
  const renderer = createLineCommentAnnotationRenderer<T>({
    renderComment: props.renderComment,
    renderDraft: props.renderDraft,
  })

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

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

  return {
    renderAnnotation: renderer.render,
  }
}

export function createLineCommentHoverRenderer(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)
      },
    })
}
