
import type {} from '@tiptap/extension-bold'
import type {} from '@tiptap/extension-bullet-list'
import type {} from '@tiptap/extension-heading'
import type {} from '@tiptap/extension-italic'
import {
  RichTextEditor,
  createDefaultExtensions,
  type EditorExtensionConfig,
  type RichTextEditorToolbarState,
} from '@platform-modules/ui-editor'
import {
  useCallback,
  useId,
  useMemo,
  useRef,
  useState,
  type KeyboardEvent,
  type MouseEvent,
  type PointerEvent,
} from 'react'
import { cn } from '../cva.js'
import { createEditorWidgetError } from './errors.js'
import { buildPreviewDocument } from './preview.js'
import type {
  EditorWidgetError,
  EditorWidgetMediaItem,
  EditorWidgetProps,
} from './types.js'

const FORMAT_BUTTON_CLASSES =
  'EditorWidget-formatButton inline-flex min-h-[var(--mod-touch-min)] min-w-[var(--mod-touch-min)] flex-none items-center justify-center border border-[var(--mod-color-border)] bg-[var(--mod-color-surface)] px-3 text-sm text-[var(--mod-color-fg)] hover:bg-[var(--mod-color-surface-raised)] active:border-[var(--mod-color-border-strong)] active:bg-[var(--mod-color-surface-raised)] aria-[pressed=true]:border-[var(--mod-color-border-strong)] aria-[pressed=true]:bg-[var(--mod-color-surface-raised)] aria-[pressed=true]:outline aria-[pressed=true]:outline-current focus-visible:z-10 focus-visible:outline-[var(--mod-focus-ring-width)] focus-visible:outline-[var(--mod-focus-ring-color)] focus-visible:outline-offset-[var(--mod-focus-ring-offset)] disabled:cursor-not-allowed disabled:opacity-[var(--mod-opacity-disabled)]'

const TAB_CLASSES =
  'EditorWidget-tab inline-flex min-h-[var(--mod-touch-min)] items-center justify-center rounded-md border border-transparent px-3 text-sm text-[var(--mod-color-fg)] data-[state=inactive]:bg-[var(--mod-color-surface)] data-[state=inactive]:hover:bg-[var(--mod-color-surface-raised)] data-[state=active]:bg-[var(--mod-color-accent)] data-[state=active]:text-[var(--mod-color-accent-fg)] forced-colors:data-[state=active]:border-current forced-colors:data-[state=active]:outline forced-colors:data-[state=active]:outline-2 focus-visible:outline-[var(--mod-focus-ring-width)] focus-visible:outline-[var(--mod-focus-ring-color)] focus-visible:outline-offset-[var(--mod-focus-ring-offset)]'

const EDITOR_SURFACE_CLASSES =
  'EditorWidget-surface min-w-0 rounded-md border border-[var(--mod-color-border)] bg-[var(--mod-color-surface)] text-[var(--mod-color-fg)] focus-within:outline-[var(--mod-focus-ring-width)] focus-within:outline-[var(--mod-focus-ring-color)] focus-within:outline-offset-[var(--mod-focus-ring-offset)] [&_[role=textbox]]:min-h-[calc(var(--mod-touch-min)*6)] [&_[role=textbox]]:[overflow-wrap:anywhere] [&_[role=textbox]]:p-4 [&_[role=textbox]]:outline-none [&_[role=textbox]>*:first-child]:mt-0 [&_[role=textbox]>*:last-child]:mb-0 [&_[role=textbox]_h2]:my-4 [&_[role=textbox]_h2]:text-xl [&_[role=textbox]_h2]:font-semibold [&_[role=textbox]_h2]:leading-tight [&_[role=textbox]_p]:mb-3 [&_[role=textbox]_ul]:mb-3 [&_[role=textbox]_ul]:list-disc [&_[role=textbox]_ul]:ps-6 [&_[role=textbox]_li+li]:mt-1 [&_[role=textbox]_img]:h-auto [&_[role=textbox]_img]:max-w-full'

const FORMATTING_ITEMS = [
  { label: 'Bold', text: 'B', shortcut: 'Control+B Meta+B' },
  { label: 'Italic', text: 'I', shortcut: 'Control+I Meta+I' },
  { label: 'Heading 2', text: 'H2' },
  { label: 'Bullet list', text: 'List' },
] as const

type FormattingItem = {
  label: string
  text: string
  shortcut?: string
  pressed: boolean
  run(): void
}

function positiveFinite(value: number | undefined): boolean {
  return value === undefined || (Number.isFinite(value) && value > 0)
}

function safeMediaSource(value: unknown): value is string {
  if (typeof value !== 'string' || value.trim() !== value || value.length === 0) return false
  try {
    const url = new URL(value)
    return url.protocol === 'https:' && url.username === '' && url.password === ''
  } catch {
    return false
  }
}

function validMediaItem(value: unknown): value is EditorWidgetMediaItem {
  if (typeof value !== 'object' || value === null) return false
  const item = value as Partial<EditorWidgetMediaItem>
  return safeMediaSource(item.src)
    && typeof item.alt === 'string'
    && positiveFinite(item.width)
    && positiveFinite(item.height)
}

function FormattingToolbar({
  state,
  ariaLabel,
  dir,
  media,
  reportError,
  reportStatus,
}: {
  state: RichTextEditorToolbarState
  ariaLabel: string
  dir: 'ltr' | 'rtl'
  media: EditorWidgetProps['media']
  reportError(error: EditorWidgetError): void
  reportStatus(message: string): void
}) {
  const buttons = useRef<Array<HTMLButtonElement | null>>([])
  const [tabStop, setTabStop] = useState(0)
  const editor = state.editor

  const items: FormattingItem[] = [
    {
      ...FORMATTING_ITEMS[0],
      pressed: state.isBold,
      run: () => {
        if (editor?.can().toggleBold()) editor.chain().focus().toggleBold().run()
      },
    },
    {
      ...FORMATTING_ITEMS[1],
      pressed: state.isItalic,
      run: () => {
        if (editor?.can().toggleItalic()) editor.chain().focus().toggleItalic().run()
      },
    },
    {
      ...FORMATTING_ITEMS[2],
      pressed: state.isH2,
      run: () => {
        if (editor?.can().toggleHeading({ level: 2 })) {
          editor.chain().focus().toggleHeading({ level: 2 }).run()
        }
      },
    },
    {
      ...FORMATTING_ITEMS[3],
      pressed: state.isBullet,
      run: () => {
        if (editor?.can().toggleBulletList()) editor.chain().focus().toggleBulletList().run()
      },
    },
  ]

  function moveFocus(event: KeyboardEvent<HTMLDivElement>): void {
    const forward = dir === 'rtl' ? -1 : 1
    let next = tabStop
    if (event.key === 'ArrowRight') next = (tabStop + forward + items.length) % items.length
    else if (event.key === 'ArrowLeft') next = (tabStop - forward + items.length) % items.length
    else if (event.key === 'Home') next = 0
    else if (event.key === 'End') next = items.length - 1
    else return

    event.preventDefault()
    setTabStop(next)
    buttons.current[next]?.focus()
  }

  function preserveEditorFocus(event: MouseEvent<HTMLButtonElement> | PointerEvent<HTMLButtonElement>): void {
    event.preventDefault()
  }

  function activateWithKeyboard(event: KeyboardEvent<HTMLButtonElement>, run: () => void): void {
    if (event.key !== 'Enter' && event.key !== ' ') return
    event.preventDefault()
    run()
  }

  function insertMedia(item: EditorWidgetMediaItem): boolean {
    if (!validMediaItem(item)) {
      reportError(createEditorWidgetError('invalid_media', 'The selected image is invalid.'))
      return false
    }

    if (!editor || editor.schema.nodes.image === undefined) {
      reportError(createEditorWidgetError('media_insert_failed', 'The selected image could not be inserted.'))
      return false
    }

    try {
      const inserted = editor
        .chain()
        .insertContent({
          type: 'image',
          attrs: {
            src: item.src,
            alt: item.alt,
            ...(item.width === undefined ? {} : { width: item.width }),
            ...(item.height === undefined ? {} : { height: item.height }),
          },
        })
        .run()

      if (!inserted) {
        reportError(createEditorWidgetError('media_insert_failed', 'The selected image could not be inserted.'))
        return false
      }

      reportStatus('Image inserted.')
      return true
    } catch {
      reportError(createEditorWidgetError('media_insert_failed', 'The selected image could not be inserted.'))
      return false
    }
  }

  const mediaDisabled = editor === null || editor.schema.nodes.image === undefined

  return (
    <div
      className="EditorWidget-toolbarRow flex min-w-0 flex-col gap-2 @md:flex-row @md:items-start @md:justify-between"
      data-editor-widget-part="toolbar-row"
      data-editor-ready={editor === null ? 'false' : 'true'}
    >
      <div
        className="EditorWidget-toolbar flex max-w-full overflow-x-auto rounded-md"
        data-editor-widget-part="formatting-toolbar"
        role="toolbar"
        aria-label={`${ariaLabel} formatting`}
        aria-orientation="horizontal"
        onKeyDown={moveFocus}
      >
        {items.map((item, index) => (
          <button
            key={item.label}
            ref={(node) => {
              buttons.current[index] = node
            }}
            className={FORMAT_BUTTON_CLASSES}
            data-editor-widget-part="format-button"
            type="button"
            aria-label={item.label}
            aria-keyshortcuts={item.shortcut}
            aria-pressed={item.pressed}
            disabled={editor === null}
            tabIndex={tabStop === index ? 0 : -1}
            onFocus={() => setTabStop(index)}
            onMouseDown={preserveEditorFocus}
            onPointerDown={preserveEditorFocus}
            onKeyDown={(event) => activateWithKeyboard(event, item.run)}
            onClick={item.run}
          >
            {item.text}
          </button>
        ))}
      </div>
      {media ? (
        <div className="EditorWidget-media min-w-0" data-editor-widget-part="media-picker">
          {media.renderPicker({ disabled: mediaDisabled, onSelect: insertMedia })}
        </div>
      ) : null}
    </div>
  )
}

function sameOptionValue(left: unknown, right: unknown, seen = new WeakMap<object, object>()): boolean {
  if (Object.is(left, right)) return true
  if (typeof left !== 'object' || left === null || typeof right !== 'object' || right === null) return false
  if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right)) return false
  if (seen.get(left) === right) return true
  seen.set(left, right)
  const leftKeys = Object.keys(left as Record<string, unknown>)
  const rightKeys = Object.keys(right as Record<string, unknown>)
  if (leftKeys.length !== rightKeys.length) return false
  return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key)
    && sameOptionValue(
      (left as Record<string, unknown>)[key],
      (right as Record<string, unknown>)[key],
      seen,
    ))
}

function sameExtensionImplementation(
  left: object,
  right: object,
): boolean {
  const leftKeys = Object.keys(left)
  const rightKeys = Object.keys(right)
  if (leftKeys.length !== rightKeys.length) return false

  return leftKeys.every((key) => {
    if (!Object.prototype.hasOwnProperty.call(right, key)) return false
    const leftValue = Reflect.get(left, key) as unknown
    const rightValue = Reflect.get(right, key) as unknown
    if (typeof leftValue === 'function' || typeof rightValue === 'function') {
      return typeof leftValue === 'function'
        && typeof rightValue === 'function'
        && Function.prototype.toString.call(leftValue) === Function.prototype.toString.call(rightValue)
    }
    return sameOptionValue(leftValue, rightValue)
  })
}

function sameExtensionConfig(
  left: EditorExtensionConfig | undefined,
  right: EditorExtensionConfig | undefined,
): boolean {
  if (left === right) return true
  if (!left || !right || left.extensions.length !== right.extensions.length) return false
  return left.extensions.every((extension, index) => {
    const candidate = right.extensions[index]
    return candidate !== undefined
      && extension.name === candidate.name
      && sameOptionValue(extension.options, candidate.options)
      && sameExtensionImplementation(extension.config, candidate.config)
  })
}

function useStableExtensionConfig(config: EditorExtensionConfig | undefined): EditorExtensionConfig | undefined {
  const stable = useRef(config)
  if (!sameExtensionConfig(stable.current, config)) stable.current = config
  return stable.current
}

function editorExtensions(mediaExtensions: EditorExtensionConfig | undefined): EditorExtensionConfig {
  const extensionsByName = new Map(
    createDefaultExtensions().map((extension) => [extension.name, extension]),
  )
  for (const extension of mediaExtensions?.extensions ?? []) {
    extensionsByName.set(extension.name, extension)
  }
  return { extensions: [...extensionsByName.values()] }
}

export function EditorWidget({
  value,
  onChange,
  dir = 'ltr',
  readOnly = false,
  ariaLabel,
  ariaDescribedBy,
  media,
  onError,
  className,
}: EditorWidgetProps) {
  const instanceId = useId()
  const labelId = `${instanceId}-label`
  const writeTabId = `${instanceId}-write-tab`
  const previewTabId = `${instanceId}-preview-tab`
  const writePanelId = `${instanceId}-write-panel`
  const previewPanelId = `${instanceId}-preview-panel`
  const writeTab = useRef<HTMLButtonElement>(null)
  const previewTab = useRef<HTMLButtonElement>(null)
  const [activeTab, setActiveTab] = useState<'write' | 'preview'>('write')
  const announcementId = useRef(0)
  const [announcement, setAnnouncement] = useState<{ id: number; message: string } | null>(null)
  const mediaExtensions = useStableExtensionConfig(media?.extensions)
  const extensions = useMemo(() => editorExtensions(mediaExtensions), [mediaExtensions])
  const previewSource = useMemo(
    () => (activeTab === 'preview' ? buildPreviewDocument(value, dir) : undefined),
    [activeTab, value, dir],
  )

  const announce = useCallback((message: string) => {
    announcementId.current += 1
    setAnnouncement({ id: announcementId.current, message })
  }, [])

  const reportError = useCallback((error: EditorWidgetError) => {
    announce(error.message)
    onError?.(error)
  }, [announce, onError])

  function onTabKeyDown(event: KeyboardEvent<HTMLButtonElement>): void {
    if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return
    event.preventDefault()
    const order = dir === 'rtl'
      ? (['preview', 'write'] as const)
      : (['write', 'preview'] as const)
    const current = order.indexOf(activeTab)
    const next = (event.key === 'Home'
      ? 'write'
      : event.key === 'End'
        ? 'preview'
        : event.key === 'ArrowRight'
          ? order[(current + 1) % order.length]
          : order[(current - 1 + order.length) % order.length]) ?? activeTab
    setActiveTab(next)
    ;(next === 'write' ? writeTab : previewTab).current?.focus()
  }

  const writeInactive = activeTab !== 'write'
  const previewInactive = activeTab !== 'preview'

  return (
    <section
      className={cn('EditorWidget @container grid min-w-0 gap-2 text-[var(--mod-color-fg)]', className)}
      data-editor-widget=""
      data-read-only={readOnly ? 'true' : 'false'}
      dir={dir}
      aria-labelledby={labelId}
    >
      <span id={labelId} className="sr-only">
        {ariaLabel}
      </span>

      {!readOnly ? (
        <p className="EditorWidget-hint m-0 text-sm text-[var(--mod-color-fg-muted)]">
          Shortcuts: Control/Command+B and Control/Command+I.
        </p>
      ) : null}

      <div className="EditorWidget-tabs grid min-w-0 gap-2" data-editor-widget-part="tabs">
        <div
          className="EditorWidget-tabList inline-flex w-fit max-w-full gap-1 rounded-md bg-[var(--mod-color-surface)] p-1"
          role="tablist"
          aria-label={`${ariaLabel} view`}
        >
          <button
            ref={writeTab}
            id={writeTabId}
            className={TAB_CLASSES}
            data-editor-widget-part="tab"
            data-editor-widget-view="write"
            data-state={activeTab === 'write' ? 'active' : 'inactive'}
            type="button"
            role="tab"
            aria-selected={activeTab === 'write'}
            aria-controls={writePanelId}
            tabIndex={activeTab === 'write' ? 0 : -1}
            onClick={() => setActiveTab('write')}
            onKeyDown={onTabKeyDown}
          >
            Write
          </button>
          <button
            ref={previewTab}
            id={previewTabId}
            className={TAB_CLASSES}
            data-editor-widget-part="tab"
            data-editor-widget-view="preview"
            data-state={activeTab === 'preview' ? 'active' : 'inactive'}
            type="button"
            role="tab"
            aria-selected={activeTab === 'preview'}
            aria-controls={previewPanelId}
            tabIndex={activeTab === 'preview' ? 0 : -1}
            onClick={() => setActiveTab('preview')}
            onKeyDown={onTabKeyDown}
          >
            Preview
          </button>
        </div>

        <div
          ref={(node) => {
            node?.toggleAttribute('inert', writeInactive)
          }}
          id={writePanelId}
          className="EditorWidget-panel EditorWidget-panel--write min-w-0"
          data-editor-widget-part="write-panel"
          role="tabpanel"
          aria-labelledby={writeTabId}
          aria-hidden={writeInactive || undefined}
          hidden={writeInactive}
        >
          <RichTextEditor
            value={value}
            onChange={onChange}
            dir={dir}
            aria-label={ariaLabel}
            aria-describedby={ariaDescribedBy}
            readOnly={readOnly}
            className={EDITOR_SURFACE_CLASSES}
            extensions={extensions}
            renderToolbar={readOnly
              ? () => null
              : (state) => (
                  <FormattingToolbar
                    state={state}
                    ariaLabel={ariaLabel}
                    dir={dir}
                    media={media}
                    reportError={reportError}
                    reportStatus={announce}
                  />
                )}
          />
        </div>

        <div
          ref={(node) => {
            node?.toggleAttribute('inert', previewInactive)
          }}
          id={previewPanelId}
          className="EditorWidget-panel EditorWidget-panel--preview min-w-0"
          data-editor-widget-part="preview-panel"
          role="tabpanel"
          aria-labelledby={previewTabId}
          aria-hidden={previewInactive || undefined}
          hidden={previewInactive}
        >
          {previewSource !== undefined ? (
            <iframe
              className="EditorWidget-preview block min-h-[calc(var(--mod-touch-min)*8)] w-full rounded-md border border-[var(--mod-color-border)] bg-[var(--mod-color-surface)] focus-visible:outline-[var(--mod-focus-ring-width)] focus-visible:outline-[var(--mod-focus-ring-color)] focus-visible:outline-offset-[var(--mod-focus-ring-offset)]"
              data-editor-widget-part="preview"
              title={`${ariaLabel} preview`}
              sandbox=""
              referrerPolicy="no-referrer"
              tabIndex={0}
              srcDoc={previewSource}
            />
          ) : null}
        </div>
      </div>

      <p className="sr-only" role="status" aria-live="polite" aria-atomic="true">
        {announcement ? <span key={announcement.id}>{announcement.message}</span> : null}
      </p>
    </section>
  )
}
