import { Accordion } from "./accordion"
import { Button } from "./button"
import { HoverCard } from "./hover-card"
import { Popover } from "./popover"
import { RadioGroup } from "./radio-group"
import { DiffChanges } from "./diff-changes"
import { FileIcon } from "./file-icon"
import { Icon } from "./icon"
import { StickyAccordionHeader } from "./sticky-accordion-header"
import { useDiffComponent } from "../context/diff"
import { useI18n } from "../context/i18n"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createEffect, createMemo, createSignal, For, Match, Show, Switch, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { type FileContent, type FileDiff } from "@opencode-ai/sdk/v2"
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { type SelectedLineRange } from "@pierre/diffs"
import { Dynamic } from "solid-js/web"

export type SessionReviewDiffStyle = "unified" | "split"

export type SessionReviewComment = {
  id: string
  file: string
  selection: SelectedLineRange
  comment: string
}

export type SessionReviewLineComment = {
  file: string
  selection: SelectedLineRange
  comment: string
  preview?: string
}

export type SessionReviewFocus = { file: string; id: string }

export interface SessionReviewProps {
  split?: boolean
  diffStyle?: SessionReviewDiffStyle
  onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void
  onDiffRendered?: () => void
  onLineComment?: (comment: SessionReviewLineComment) => void
  comments?: SessionReviewComment[]
  focusedComment?: SessionReviewFocus | null
  onFocusedCommentChange?: (focus: SessionReviewFocus | null) => void
  open?: string[]
  onOpenChange?: (open: string[]) => void
  scrollRef?: (el: HTMLDivElement) => void
  onScroll?: JSX.EventHandlerUnion<HTMLDivElement, Event>
  class?: string
  classList?: Record<string, boolean | undefined>
  classes?: { root?: string; header?: string; container?: string }
  actions?: JSX.Element
  diffs: (FileDiff & { preloaded?: PreloadMultiFileDiffResult<any> })[]
  onViewFile?: (file: string) => void
  readFile?: (path: string) => Promise<FileContent | undefined>
}

const imageExtensions = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif", "bmp", "ico", "tif", "tiff", "heic"])
const audioExtensions = new Set(["mp3", "wav", "ogg", "m4a", "aac", "flac", "opus"])

function normalizeMimeType(type: string | undefined): string | undefined {
  if (!type) return

  const mime = type.split(";", 1)[0]?.trim().toLowerCase()
  if (!mime) return

  if (mime === "audio/x-aac") return "audio/aac"
  if (mime === "audio/x-m4a") return "audio/mp4"

  return mime
}

function getExtension(file: string): string {
  const idx = file.lastIndexOf(".")
  if (idx === -1) return ""
  return file.slice(idx + 1).toLowerCase()
}

function isImageFile(file: string): boolean {
  return imageExtensions.has(getExtension(file))
}

function isAudioFile(file: string): boolean {
  return audioExtensions.has(getExtension(file))
}

function dataUrl(content: FileContent | undefined): string | undefined {
  if (!content) return
  if (content.encoding !== "base64") return
  const mime = normalizeMimeType(content.mimeType)
  if (!mime) return
  if (!mime.startsWith("image/") && !mime.startsWith("audio/")) return
  return `data:${mime};base64,${content.content}`
}

function dataUrlFromValue(value: unknown): string | undefined {
  if (typeof value === "string") {
    if (value.startsWith("data:image/")) return value
    if (value.startsWith("data:audio/x-aac;")) return value.replace("data:audio/x-aac;", "data:audio/aac;")
    if (value.startsWith("data:audio/x-m4a;")) return value.replace("data:audio/x-m4a;", "data:audio/mp4;")
    if (value.startsWith("data:audio/")) return value
    return
  }
  if (!value || typeof value !== "object") return

  const content = (value as { content?: unknown }).content
  const encoding = (value as { encoding?: unknown }).encoding
  const mimeType = (value as { mimeType?: unknown }).mimeType

  if (typeof content !== "string") return
  if (encoding !== "base64") return
  if (typeof mimeType !== "string") return
  const mime = normalizeMimeType(mimeType)
  if (!mime) return
  if (!mime.startsWith("image/") && !mime.startsWith("audio/")) return

  return `data:${mime};base64,${content}`
}

type SessionReviewSelection = {
  file: string
  range: SelectedLineRange
}

function findSide(element: HTMLElement): "additions" | "deletions" {
  const code = element.closest("[data-code]")
  if (!(code instanceof HTMLElement)) return "additions"
  if (code.hasAttribute("data-deletions")) return "deletions"
  return "additions"
}

function findMarker(root: ShadowRoot, range: SelectedLineRange) {
  const line = Math.max(range.start, range.end)
  const side = range.endSide ?? range.side
  const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
    (node): node is HTMLElement => node instanceof HTMLElement,
  )
  if (nodes.length === 0) return
  if (!side) return nodes[0]

  const match = nodes.find((node) => findSide(node) === side)
  return match ?? nodes[0]
}

function markerTop(wrapper: HTMLElement, marker: HTMLElement) {
  const wrapperRect = wrapper.getBoundingClientRect()
  const rect = marker.getBoundingClientRect()
  return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2)
}

export const SessionReview = (props: SessionReviewProps) => {
  const i18n = useI18n()
  const diffComponent = useDiffComponent()
  const anchors = new Map<string, HTMLElement>()
  const [store, setStore] = createStore({
    open: props.diffs.length > 10 ? [] : props.diffs.map((d) => d.file),
  })

  const [selection, setSelection] = createSignal<SessionReviewSelection | null>(null)
  const [commenting, setCommenting] = createSignal<SessionReviewSelection | null>(null)
  const [opened, setOpened] = createSignal<SessionReviewFocus | null>(null)

  const open = () => props.open ?? store.open
  const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")

  const handleChange = (open: string[]) => {
    props.onOpenChange?.(open)
    if (props.open !== undefined) return
    setStore("open", open)
  }

  const handleExpandOrCollapseAll = () => {
    const next = open().length > 0 ? [] : props.diffs.map((d) => d.file)
    handleChange(next)
  }

  const selectionLabel = (range: SelectedLineRange) => {
    const start = Math.min(range.start, range.end)
    const end = Math.max(range.start, range.end)
    if (start === end) return `line ${start}`
    return `lines ${start}-${end}`
  }

  const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions"

  const selectionPreview = (diff: FileDiff, range: SelectedLineRange) => {
    const side = selectionSide(range)
    const contents = side === "deletions" ? diff.before : diff.after
    if (typeof contents !== "string" || contents.length === 0) return undefined

    const start = Math.max(1, Math.min(range.start, range.end))
    const end = Math.max(range.start, range.end)
    const lines = contents.split("\n").slice(start - 1, end)
    if (lines.length === 0) return undefined
    return lines.slice(0, 2).join("\n")
  }

  createEffect(() => {
    const focus = props.focusedComment
    if (!focus) return

    setOpened(focus)

    const comment = (props.comments ?? []).find((c) => c.file === focus.file && c.id === focus.id)
    if (comment) setSelection({ file: comment.file, range: comment.selection })

    const current = open()
    if (!current.includes(focus.file)) {
      handleChange([...current, focus.file])
    }

    requestAnimationFrame(() => {
      anchors.get(focus.file)?.scrollIntoView({ block: "center" })
    })

    requestAnimationFrame(() => props.onFocusedCommentChange?.(null))
  })

  return (
    <div
      data-component="session-review"
      ref={props.scrollRef}
      onScroll={props.onScroll}
      classList={{
        ...(props.classList ?? {}),
        [props.classes?.root ?? ""]: !!props.classes?.root,
        [props.class ?? ""]: !!props.class,
      }}
    >
      <div
        data-slot="session-review-header"
        classList={{
          [props.classes?.header ?? ""]: !!props.classes?.header,
        }}
      >
        <div data-slot="session-review-title">{i18n.t("ui.sessionReview.title")}</div>
        <div data-slot="session-review-actions">
          <Show when={props.onDiffStyleChange}>
            <RadioGroup
              options={["unified", "split"] as const}
              current={diffStyle()}
              value={(style) => style}
              label={(style) =>
                i18n.t(style === "unified" ? "ui.sessionReview.diffStyle.unified" : "ui.sessionReview.diffStyle.split")
              }
              onSelect={(style) => style && props.onDiffStyleChange?.(style)}
            />
          </Show>
          <Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
            <Switch>
              <Match when={open().length > 0}>{i18n.t("ui.sessionReview.collapseAll")}</Match>
              <Match when={true}>{i18n.t("ui.sessionReview.expandAll")}</Match>
            </Switch>
          </Button>
          {props.actions}
        </div>
      </div>
      <div
        data-slot="session-review-container"
        classList={{
          [props.classes?.container ?? ""]: !!props.classes?.container,
        }}
      >
        <Accordion multiple value={open()} onChange={handleChange}>
          <For each={props.diffs}>
            {(diff) => {
              let wrapper: HTMLDivElement | undefined
              let textarea: HTMLTextAreaElement | undefined

              const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
              const commentedLines = createMemo(() => comments().map((c) => c.selection))

              const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
              const afterText = () => (typeof diff.after === "string" ? diff.after : "")

              const isAdded = () => beforeText().length === 0 && afterText().length > 0
              const isDeleted = () => afterText().length === 0 && beforeText().length > 0
              const isImage = () => isImageFile(diff.file)
              const isAudio = () => isAudioFile(diff.file)

              const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
              const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
              const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")

              const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
              const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
              const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
              const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)

              const selectedLines = createMemo(() => {
                const current = selection()
                if (!current || current.file !== diff.file) return null
                return current.range
              })

              const draftRange = createMemo(() => {
                const current = commenting()
                if (!current || current.file !== diff.file) return null
                return current.range
              })

              const [draft, setDraft] = createSignal("")
              const [positions, setPositions] = createSignal<Record<string, number>>({})
              const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)

              const getRoot = () => {
                const el = wrapper
                if (!el) return

                const host = el.querySelector("diffs-container")
                if (!(host instanceof HTMLElement)) return
                return host.shadowRoot ?? undefined
              }

              const updateAnchors = () => {
                const el = wrapper
                if (!el) return

                const root = getRoot()
                if (!root) return

                const next: Record<string, number> = {}
                for (const item of comments()) {
                  const marker = findMarker(root, item.selection)
                  if (!marker) continue
                  next[item.id] = markerTop(el, marker)
                }
                setPositions(next)

                const range = draftRange()
                if (!range) {
                  setDraftTop(undefined)
                  return
                }

                const marker = findMarker(root, range)
                if (!marker) {
                  setDraftTop(undefined)
                  return
                }

                setDraftTop(markerTop(el, marker))
              }

              const scheduleAnchors = () => {
                requestAnimationFrame(updateAnchors)
              }

              createEffect(() => {
                comments()
                scheduleAnchors()
              })

              createEffect(() => {
                const range = draftRange()
                if (!range) return
                setDraft("")
                scheduleAnchors()
                requestAnimationFrame(() => textarea?.focus())
              })

              createEffect(() => {
                if (!open().includes(diff.file)) return
                if (!isImage()) return
                if (imageSrc()) return
                if (imageStatus() !== "idle") return

                const reader = props.readFile
                if (!reader) return

                setImageStatus("loading")
                reader(diff.file)
                  .then((result) => {
                    const src = dataUrl(result)
                    if (!src) {
                      setImageStatus("error")
                      return
                    }
                    setImageSrc(src)
                    setImageStatus("idle")
                  })
                  .catch(() => {
                    setImageStatus("error")
                  })
              })

              createEffect(() => {
                if (!open().includes(diff.file)) return
                if (!isAudio()) return
                if (audioSrc()) return
                if (audioStatus() !== "idle") return

                const reader = props.readFile
                if (!reader) return

                setAudioStatus("loading")
                reader(diff.file)
                  .then((result) => {
                    const src = dataUrl(result)
                    if (!src) {
                      setAudioStatus("error")
                      return
                    }
                    setAudioMime(normalizeMimeType(result?.mimeType))
                    setAudioSrc(src)
                    setAudioStatus("idle")
                  })
                  .catch(() => {
                    setAudioStatus("error")
                  })
              })

              const handleLineSelected = (range: SelectedLineRange | null) => {
                if (!props.onLineComment) return

                if (!range) {
                  setSelection(null)
                  return
                }

                setSelection({ file: diff.file, range })
              }

              const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
                if (!props.onLineComment) return

                if (!range) {
                  setCommenting(null)
                  return
                }

                setSelection({ file: diff.file, range })
                setCommenting({ file: diff.file, range })
              }

              const openComment = (comment: SessionReviewComment) => {
                setOpened({ file: comment.file, id: comment.id })
                setSelection({ file: comment.file, range: comment.selection })
              }

              const isCommentOpen = (comment: SessionReviewComment) => {
                const current = opened()
                if (!current) return false
                return current.file === comment.file && current.id === comment.id
              }

              return (
                <Accordion.Item value={diff.file} data-slot="session-review-accordion-item">
                  <StickyAccordionHeader>
                    <Accordion.Trigger>
                      <div data-slot="session-review-trigger-content">
                        <div data-slot="session-review-file-info">
                          <FileIcon node={{ path: diff.file, type: "file" }} />
                          <div data-slot="session-review-file-name-container">
                            <Show when={diff.file.includes("/")}>
                              <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
                            </Show>
                            <span data-slot="session-review-filename">{getFilename(diff.file)}</span>
                            <Show when={props.onViewFile}>
                              <button
                                data-slot="session-review-view-button"
                                type="button"
                                onClick={(e) => {
                                  e.stopPropagation()
                                  props.onViewFile?.(diff.file)
                                }}
                              >
                                <Icon name="eye" size="small" />
                              </button>
                            </Show>
                          </div>
                        </div>
                        <div data-slot="session-review-trigger-actions">
                          <Switch>
                            <Match when={isAdded()}>
                              <span data-slot="session-review-change" data-type="added">
                                Added
                              </span>
                            </Match>
                            <Match when={isDeleted()}>
                              <span data-slot="session-review-change" data-type="removed">
                                Removed
                              </span>
                            </Match>
                            <Match when={true}>
                              <DiffChanges changes={diff} />
                            </Match>
                          </Switch>
                          <Icon name="chevron-grabber-vertical" size="small" />
                        </div>
                      </div>
                    </Accordion.Trigger>
                  </StickyAccordionHeader>
                  <Accordion.Content data-slot="session-review-accordion-content">
                    <Switch>
                      <Match when={isImage()}>
                        <div data-slot="session-review-image-container">
                          <Show
                            when={imageSrc()}
                            fallback={
                              <div data-slot="session-review-image-placeholder">
                                <Switch>
                                  <Match when={imageStatus() === "loading"}>Loading image...</Match>
                                  <Match when={true}>Image preview unavailable</Match>
                                </Switch>
                              </div>
                            }
                          >
                            <img data-slot="session-review-image" src={imageSrc()!} alt={getFilename(diff.file)} />
                          </Show>
                        </div>
                      </Match>
                      <Match when={isAudio()}>
                        <div data-slot="session-review-audio-container">
                          <Show
                            when={audioSrc() && audioStatus() !== "error"}
                            fallback={
                              <div data-slot="session-review-audio-placeholder">
                                <Switch>
                                  <Match when={audioStatus() === "loading"}>Loading audio...</Match>
                                  <Match when={true}>Audio preview unavailable</Match>
                                </Switch>
                              </div>
                            }
                          >
                            <audio
                              data-slot="session-review-audio"
                              controls
                              preload="metadata"
                              onError={() => {
                                setAudioStatus("error")
                              }}
                            >
                              <source src={audioSrc()!} type={audioMime()} />
                            </audio>
                          </Show>
                        </div>
                      </Match>
                      <Match when={true}>
                        <div
                          data-slot="session-review-diff-wrapper"
                          ref={(el) => {
                            wrapper = el
                            anchors.set(diff.file, el)
                            scheduleAnchors()
                          }}
                        >
                          <Dynamic
                            component={diffComponent}
                            preloadedDiff={diff.preloaded}
                            diffStyle={diffStyle()}
                            onRendered={() => {
                              props.onDiffRendered?.()
                              scheduleAnchors()
                            }}
                            enableLineSelection={props.onLineComment != null}
                            onLineSelected={handleLineSelected}
                            onLineSelectionEnd={handleLineSelectionEnd}
                            selectedLines={selectedLines()}
                            commentedLines={commentedLines()}
                            before={{
                              name: diff.file!,
                              contents: beforeText(),
                            }}
                            after={{
                              name: diff.file!,
                              contents: afterText(),
                            }}
                          />

                          <For each={comments()}>
                            {(comment) => (
                              <div
                                data-slot="session-review-comment-anchor"
                                style={{
                                  top: `${positions()[comment.id] ?? 0}px`,
                                  opacity: positions()[comment.id] === undefined ? 0 : 1,
                                  "pointer-events": positions()[comment.id] === undefined ? "none" : "auto",
                                }}
                              >
                                <Popover
                                  open={isCommentOpen(comment)}
                                  onOpenChange={(open) => {
                                    if (open) {
                                      openComment(comment)
                                      return
                                    }
                                    if (!isCommentOpen(comment)) return
                                    setOpened(null)
                                  }}
                                  trigger={
                                    <HoverCard
                                      trigger={
                                        <button
                                          type="button"
                                          data-slot="session-review-comment-button"
                                          onMouseEnter={() =>
                                            setSelection({ file: comment.file, range: comment.selection })
                                          }
                                        >
                                          <Icon name="speech-bubble" size="small" />
                                        </button>
                                      }
                                    >
                                      <div data-slot="session-review-comment-hover">
                                        <div data-slot="session-review-comment-hover-label">
                                          {getFilename(comment.file)}:{selectionLabel(comment.selection)}
                                        </div>
                                        <div data-slot="session-review-comment-hover-text">{comment.comment}</div>
                                      </div>
                                    </HoverCard>
                                  }
                                >
                                  <div data-slot="session-review-comment-popover">
                                    <div data-slot="session-review-comment-popover-label">
                                      {getFilename(comment.file)}:{selectionLabel(comment.selection)}
                                    </div>
                                    <div data-slot="session-review-comment-popover-text">{comment.comment}</div>
                                    <Show when={selectionPreview(diff, comment.selection)}>
                                      {(preview) => <pre data-slot="session-review-comment-preview">{preview()}</pre>}
                                    </Show>
                                  </div>
                                </Popover>
                              </div>
                            )}
                          </For>

                          <Show when={draftRange()}>
                            {(range) => (
                              <Show when={draftTop() !== undefined}>
                                <div data-slot="session-review-comment-anchor" style={{ top: `${draftTop() ?? 0}px` }}>
                                  <Popover
                                    open={true}
                                    onOpenChange={(open) => {
                                      if (open) return
                                      setCommenting(null)
                                    }}
                                    trigger={
                                      <button type="button" data-slot="session-review-comment-button">
                                        <Icon name="speech-bubble" size="small" />
                                      </button>
                                    }
                                  >
                                    <div data-slot="session-review-comment-popover">
                                      <div data-slot="session-review-comment-popover-label">
                                        Commenting on {getFilename(diff.file)}:{selectionLabel(range())}
                                      </div>
                                      <textarea
                                        ref={textarea}
                                        data-slot="session-review-comment-textarea"
                                        rows={3}
                                        placeholder="Add a comment"
                                        value={draft()}
                                        onInput={(e) => setDraft(e.currentTarget.value)}
                                        onKeyDown={(e) => {
                                          if (e.key !== "Enter") return
                                          if (e.shiftKey) return
                                          e.preventDefault()
                                          const value = draft().trim()
                                          if (!value) return
                                          props.onLineComment?.({
                                            file: diff.file,
                                            selection: range(),
                                            comment: value,
                                            preview: selectionPreview(diff, range()),
                                          })
                                          setCommenting(null)
                                        }}
                                      />
                                      <div data-slot="session-review-comment-actions">
                                        <Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
                                          Cancel
                                        </Button>
                                        <Button
                                          size="small"
                                          variant="secondary"
                                          disabled={draft().trim().length === 0}
                                          onClick={() => {
                                            const value = draft().trim()
                                            if (!value) return
                                            props.onLineComment?.({
                                              file: diff.file,
                                              selection: range(),
                                              comment: value,
                                              preview: selectionPreview(diff, range()),
                                            })
                                            setCommenting(null)
                                          }}
                                        >
                                          Comment
                                        </Button>
                                      </div>
                                    </div>
                                  </Popover>
                                </div>
                              </Show>
                            )}
                          </Show>
                        </div>
                      </Match>
                    </Switch>
                  </Accordion.Content>
                </Accordion.Item>
              )
            }}
          </For>
        </Accordion>
      </div>
    </div>
  )
}
