import React from 'react'
import { createRoot } from 'react-dom/client'
import browser from 'webextension-polyfill'
import { getSettings, saveSettings, type ExtensionSettings } from '../lib/settings'
import { normalizePageUrl, type ClipItem } from '../lib/clipTypes'
import './styles.css'

type MediaCandidate = {
  id: string
  url: string
  kind: 'mp4' | 'm3u8' | 'm3u' | 'hls' | 'unknown'
  referer: string
  pageTitle?: string
  suggestedOut?: string
  cookie?: string
  userAgent?: string
  detectedAt: number
}

function clipLog(...args: unknown[]): void {
  console.log('[gomdown-helper][clip][popup]', ...args)
}

function safeFileName(raw: string): string {
  const title = String(raw || '')
    .replace(/\s*[|\-–—]\s*(qiita|medium|youtube|x|twitter|tistory|velog|github)\s*$/i, '')
    .replace(/\s*[-–—|]\s*(edge|chrome|firefox)\s*$/i, '')
    .replace(/\[[^\]]{1,30}\]\s*$/g, '')
  const next = title
    .trim()
    .replace(/[\\/:*?"<>|]/g, '_')
    .replace(/[(){}[\]]/g, '')
    .replace(/\s+/g, ' ')
    .replace(/[._-]{2,}/g, '-')
    .replace(/^[._\-\s]+|[._\-\s]+$/g, '')
    .slice(0, 80)
  return next || 'clips'
}

function htmlToMarkdown(html: string, baseUrl = ''): string {
  if (!html.trim()) return ''
  const parser = new DOMParser()
  const doc = parser.parseFromString(`<div>${html}</div>`, 'text/html')
  const root = doc.body.firstElementChild as HTMLElement | null
  if (!root) return ''

  const resolveUrl = (raw: string): string => {
    const src = String(raw || '').trim()
    if (!src) return ''
    if (!baseUrl) return src
    try {
      return new URL(src, baseUrl).toString()
    } catch {
      return src
    }
  }

  const cleanInlineText = (raw: string): string => {
    return String(raw || '').replace(/\s+/g, ' ').trim()
  }

  const resolveImageUrl = (node: HTMLElement): string => {
    const candidates = [
      node.getAttribute('src'),
      node.getAttribute('data-src'),
      node.getAttribute('data-original'),
      node.getAttribute('data-url'),
      (node as HTMLImageElement).currentSrc,
    ]
    for (const candidate of candidates) {
      const resolved = resolveUrl(String(candidate || '').trim())
      if (resolved) return resolved
    }
    return ''
  }

  const escapeCell = (raw: string): string => {
    return raw.replace(/\|/g, '\\|').replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim()
  }

  const tableToMarkdown = (table: HTMLElement): string => {
    const rows = Array.from(table.querySelectorAll('tr'))
    if (rows.length === 0) return ''

    const matrix = rows
      .map((tr) => {
        return Array.from(tr.querySelectorAll('th,td')).map((cell) => {
          const cellText = Array.from(cell.childNodes).map((child) => {
            if (child instanceof HTMLElement && ['pre', 'ul', 'ol', 'table'].includes(child.tagName.toLowerCase())) {
              return cleanInlineText(block(child))
            }
            return inline(child)
          }).join('')
          return escapeCell(cellText)
        })
      })
      .filter((row) => row.length > 0)
    if (matrix.length === 0) return ''

    const colCount = matrix.reduce((max, row) => Math.max(max, row.length), 0)
    const normalized = matrix.map((row) => {
      const next = [...row]
      while (next.length < colCount) next.push('')
      return next
    })

    const firstTr = rows[0]
    const hasHeader = firstTr ? Array.from(firstTr.querySelectorAll('th')).length > 0 : false
    const header = hasHeader ? normalized[0] : normalized[0].map((_, idx) => `col${idx + 1}`)
    const body = hasHeader ? normalized.slice(1) : normalized

    const headerLine = `| ${header.join(' | ')} |`
    const sepLine = `| ${header.map(() => '---').join(' | ')} |`
    const bodyLines = body.map((row) => `| ${row.join(' | ')} |`)
    return [headerLine, sepLine, ...bodyLines].join('\n')
  }

  const inline = (node: Node): string => {
    if (node.nodeType === Node.TEXT_NODE) return (node.nodeValue || '').replace(/\s+/g, ' ')
    if (!(node instanceof HTMLElement)) return ''
    const tag = node.tagName.toLowerCase()

    if (tag === 'br') return '\n'
    if (tag === 'strong' || tag === 'b') return `**${Array.from(node.childNodes).map(inline).join('')}**`
    if (tag === 'em' || tag === 'i') return `*${Array.from(node.childNodes).map(inline).join('')}*`
    if (tag === 'img') {
      const src = resolveImageUrl(node)
      const alt = String(node.getAttribute('alt') || '').trim()
      const title = cleanInlineText(node.getAttribute('title') || '')
      const titlePart = title ? ` "${title.replace(/"/g, '\\"')}"` : ''
      return src ? `![${alt}](${src}${titlePart})` : ''
    }
    if (tag === 'a') {
      const href = resolveUrl(node.getAttribute('href') || '')
      const text = Array.from(node.childNodes).map(inline).join('').trim() || href
      return href ? `[${text}](${href})` : text
    }
    if (tag === 'code') {
      if (node.parentElement?.tagName.toLowerCase() === 'pre') {
        return node.textContent || ''
      }
      const codeText = Array.from(node.childNodes).map(inline).join('').replace(/\n+/g, ' ').trim()
      return codeText ? `\`${codeText}\`` : ''
    }
    return Array.from(node.childNodes).map(inline).join('')
  }

  const block = (node: Node): string => {
    if (node.nodeType === Node.TEXT_NODE) return (node.nodeValue || '').replace(/\s+/g, ' ').trim()
    if (!(node instanceof HTMLElement)) return ''
    const tag = node.tagName.toLowerCase()

    if (tag === 'pre') {
      const codeNode = node.querySelector('code')
      const rawCode = (codeNode?.textContent || node.textContent || '').replace(/\r\n/g, '\n').replace(/\n+$/, '')
      const className = String(codeNode?.className || '')
      const langMatch = className.match(/language-([a-z0-9_-]+)/i)
      const lang = langMatch?.[1] || ''
      return rawCode ? `\`\`\`${lang}\n${rawCode}\n\`\`\`\n\n` : ''
    }

    if (tag === 'img') {
      const src = resolveImageUrl(node)
      const alt = String(node.getAttribute('alt') || '').trim()
      const title = cleanInlineText(node.getAttribute('title') || '')
      const titlePart = title ? ` "${title.replace(/"/g, '\\"')}"` : ''
      return src ? `![${alt}](${src}${titlePart})\n\n` : ''
    }

    if (tag === 'figure') {
      const image = node.querySelector('img')
      const figcaption = cleanInlineText(node.querySelector('figcaption')?.textContent || '')
      if (image instanceof HTMLElement) {
        const imageMd = block(image).trim()
        if (imageMd && figcaption) return `${imageMd}\n*${figcaption}*\n\n`
        if (imageMd) return `${imageMd}\n\n`
      }
      const fallback = cleanInlineText(Array.from(node.childNodes).map(inline).join(''))
      return fallback ? `${fallback}\n\n` : ''
    }

    if (tag === 'ul' || tag === 'ol') {
      const rows = Array.from(node.children)
        .map((child, idx) => {
          if (!(child instanceof HTMLElement) || child.tagName.toLowerCase() !== 'li') return ''
          const body = Array.from(child.childNodes)
            .map((n) => (n instanceof HTMLElement && (n.tagName.toLowerCase() === 'ul' || n.tagName.toLowerCase() === 'ol') ? '' : inline(n)))
            .join('')
            .trim()
          const prefix = tag === 'ol' ? `${idx + 1}. ` : '- '
          const nested = Array.from(child.children)
            .filter((n) => n.tagName && ['ul', 'ol'].includes(n.tagName.toLowerCase()))
            .map((n) => block(n).trimEnd().split('\n').map((line) => (line ? `  ${line}` : line)).join('\n'))
            .filter(Boolean)
            .join('\n')
          return `${prefix}${body}${nested ? `\n${nested}` : ''}`
        })
        .filter(Boolean)
        .join('\n')
      return rows ? `${rows}\n\n` : ''
    }

    if (tag === 'table') {
      const table = tableToMarkdown(node)
      return table ? `${table}\n\n` : ''
    }

    if (tag === 'p' || tag === 'div' || tag === 'section' || tag === 'article' || tag === 'blockquote') {
      const content = Array.from(node.childNodes)
        .map((child) => {
          if (child instanceof HTMLElement && ['pre', 'ul', 'ol', 'img', 'table', 'figure'].includes(child.tagName.toLowerCase())) {
            return `\n${block(child)}`
          }
          return inline(child)
        })
        .join('')
        .trim()
      if (!content) return ''
      if (tag === 'blockquote') {
        return `${content.split('\n').map((line) => (line ? `> ${line}` : '>')).join('\n')}\n\n`
      }
      return `${content}\n\n`
    }

    if (tag === 'code') return ''
    return `${Array.from(node.childNodes).map(block).join('')}`
  }

  return block(root)
    .replace(/\n{3,}/g, '\n\n')
    .trim()
}

function buildPageMarkdown(pageClips: ClipItem[], activePageUrl: string): string {
  if (pageClips.length === 0) return ''
  const pageTitle = pageClips[0]?.pageTitle || activePageUrl || 'Untitled'
  const lines: string[] = [
    `# ${pageTitle}`,
    `- source: ${activePageUrl}`,
    `- exportedAt: ${new Date().toISOString()}`,
    `- clips: ${pageClips.length}`,
    '',
    '---',
    '',
  ]
  for (let i = 0; i < pageClips.length; i += 1) {
    lines.push(clipToMarkdown(pageClips[i], i))
    lines.push('')
  }
  return lines.join('\n')
}

function clipToMarkdown(item: ClipItem, index: number): string {
  const quoteByHtml = htmlToMarkdown(String(item.quoteHtml || ''), item.pageUrl || '')
  const quoteRaw = (quoteByHtml || item.quote || item.anchor.exact || '').replace(/\r\n/g, '\n').trim()
  const quote = quoteRaw.length > 4000 ? `${quoteRaw.slice(0, 4000)}\n...(truncated)` : quoteRaw
  const quoteLines = quote
    .split('\n')
    .map((line) => line.trimEnd())
    .filter((line, idx, arr) => !(line === '' && arr[idx - 1] === ''))
  const hasStructuredMarkdown = /(^|\n)\|.*\|\n\|[-:| ]+\||(^|\n)```|!\[[^\]]*\]\(|(^|\n)(- |\d+\. )/.test(quote)
  const body = hasStructuredMarkdown
    ? quote
    : (quoteLines.length === 0 ? '> ' : quoteLines.map((line) => `> ${line}`).join('\n'))
  return [
    `### ${index + 1}. Clip`,
    body,
    '',
    `- created: ${item.createdAt}`,
    `- status: ${item.resolveStatus || 'ok'}`,
  ].join('\n')
}

function ActionIcon({ kind }: { kind: 'save' | 'settings' | 'history' | 'send' | 'clip' }): JSX.Element {
  if (kind === 'save') {
    return (
      <svg viewBox="0 0 24 24" aria-hidden="true">
        <path d="M4 4h13l3 3v13H4z" fill="none" stroke="currentColor" strokeWidth="1.8" />
        <path d="M8 4h8v5H8zM8 14h8v6H8z" fill="none" stroke="currentColor" strokeWidth="1.8" />
      </svg>
    )
  }
  if (kind === 'settings') {
    return (
      <svg viewBox="0 0 24 24" aria-hidden="true">
        <path d="M12 3l2 1 2-1 2 3-1 2 1 2 2 1v2l-2 1-1 2 1 2-2 3-2-1-2 1-2-1-2 1-2-3 1-2-1-2-2-1v-2l2-1 1-2-1-2 2-3 2 1z" fill="none" stroke="currentColor" strokeWidth="1.4" />
        <circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" strokeWidth="1.8" />
      </svg>
    )
  }
  if (kind === 'history') {
    return (
      <svg viewBox="0 0 24 24" aria-hidden="true">
        <path d="M5 12a7 7 0 1 0 2-5" fill="none" stroke="currentColor" strokeWidth="1.8" />
        <path d="M5 4v4h4M12 8v5l3 2" fill="none" stroke="currentColor" strokeWidth="1.8" />
      </svg>
    )
  }
  if (kind === 'send') {
    return (
      <svg viewBox="0 0 24 24" aria-hidden="true">
        <path d="M3 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" strokeWidth="1.8" />
      </svg>
    )
  }
  return (
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path d="M4 5h16v12H8l-4 3z" fill="none" stroke="currentColor" strokeWidth="1.8" />
      <path d="M8 9h8M8 12h6" fill="none" stroke="currentColor" strokeWidth="1.8" />
    </svg>
  )
}

function App(): JSX.Element {
  const [settings, setSettings] = React.useState<ExtensionSettings | null>(null)
  const [status, setStatus] = React.useState('')
  const [mediaItems, setMediaItems] = React.useState<MediaCandidate[]>([])
  const [activeTabId, setActiveTabId] = React.useState<number | null>(null)
  const [activePageUrl, setActivePageUrl] = React.useState('')
  const [clips, setClips] = React.useState<ClipItem[]>([])
  const importInputRef = React.useRef<HTMLInputElement | null>(null)

  React.useEffect(() => {
    void getSettings().then(setSettings)
  }, [])

  React.useEffect(() => {
    let timer: number | null = null
    const loadMedia = async () => {
      const response = (await browser.runtime.sendMessage({ type: 'media:list' })) as {
        ok?: boolean
        items?: MediaCandidate[]
      }
      if (response?.ok && Array.isArray(response.items)) {
        setMediaItems(response.items.slice(0, 10))
      }
    }
    void loadMedia()
    timer = window.setInterval(() => {
      void loadMedia()
    }, 2000)

    return () => {
      if (timer !== null) window.clearInterval(timer)
    }
  }, [])

  React.useEffect(() => {
    const loadTabAndClips = async () => {
      const tabs = await browser.tabs.query({ active: true, currentWindow: true })
      const tab = tabs[0]
      const tabUrl = normalizePageUrl(String(tab?.url || ''))
      setActiveTabId(Number.isInteger(tab?.id) ? (tab?.id as number) : null)
      setActivePageUrl(tabUrl)
      if (!tabUrl) {
        setClips([])
        return
      }
      const response = (await browser.runtime.sendMessage({
        type: 'clip:list',
        pageUrl: tabUrl,
      })) as { ok?: boolean; items?: ClipItem[] }
      if (response?.ok && Array.isArray(response.items)) {
        setClips(response.items)
      }
    }
    void loadTabAndClips()
    const timer = window.setInterval(() => {
      void loadTabAndClips()
    }, 2500)
    return () => {
      window.clearInterval(timer)
    }
  }, [])

  const update = <K extends keyof ExtensionSettings>(key: K, value: ExtensionSettings[K]) => {
    setSettings((prev) => (prev ? { ...prev, [key]: value } : prev))
  }

  const onToggleExtension = async (enabled: boolean) => {
    if (!settings) return
    const next = { ...settings, extensionStatus: enabled }
    setSettings(next)
    await saveSettings({ extensionStatus: enabled })
    setStatus(enabled ? 'Extension ON' : 'Extension OFF')
    window.setTimeout(() => setStatus(''), 1200)
  }

  const onSave = async () => {
    if (!settings) return
    await saveSettings(settings)
    setStatus('Saved')
    window.setTimeout(() => setStatus(''), 1200)
  }

  const onOpenSettings = async () => {
    const url = browser.runtime.getURL('src/config/index.html')
    await browser.tabs.create({ url })
  }

  const onOpenHistory = async () => {
    const url = browser.runtime.getURL('src/history/index.html')
    await browser.tabs.create({ url })
  }

  const onSendActiveTabByYtDlp = async () => {
    const result = (await browser.runtime.sendMessage({
      type: 'page:enqueue-ytdlp',
    })) as { ok?: boolean; error?: string }
    setStatus(result?.ok ? 'Active tab sent to gdown (yt-dlp)' : `Send failed: ${result?.error || 'unknown error'}`)
    window.setTimeout(() => setStatus(''), 1800)
  }

  const onSendMedia = async (item: MediaCandidate) => {
    const result = (await browser.runtime.sendMessage({
      type: 'media:enqueue',
      url: item.url,
      referer: item.referer || '',
      kind: item.kind,
      suggestedOut: item.suggestedOut || '',
      cookie: item.cookie || '',
      userAgent: item.userAgent || '',
    })) as { ok?: boolean; error?: string }
    setStatus(result?.ok ? 'Media sent to gdown' : `Send failed: ${result?.error || 'unknown error'}`)
    window.setTimeout(() => setStatus(''), 1600)
  }

  const onClearMedia = async () => {
    await browser.runtime.sendMessage({ type: 'media:clear' })
    setMediaItems([])
    setStatus('Captured media cleared')
    window.setTimeout(() => setStatus(''), 1200)
  }

  const onCreateClipFromSelection = async () => {
    clipLog('onCreateClipFromSelection:clicked', {
      activeTabId,
      activePageUrl,
    })
    const result = (await browser.runtime.sendMessage({
      type: 'clip:create-active-tab',
    })) as { ok?: boolean; error?: string } | undefined
    clipLog('onCreateClipFromSelection:result', result)
    if (!result?.ok) {
      setStatus(`클립 생성 실패: ${result?.error || 'active tab unavailable'}`)
      window.setTimeout(() => setStatus(''), 2000)
      return
    }
    setStatus('클립 저장 완료')
    window.setTimeout(() => setStatus(''), 1400)
    const refreshed = (await browser.runtime.sendMessage({
      type: 'clip:list',
      pageUrl: activePageUrl,
    })) as { ok?: boolean; items?: ClipItem[] }
    if (refreshed?.ok && Array.isArray(refreshed.items)) {
      setClips(refreshed.items)
    }
  }

  const onRevealClip = async (id: string) => {
    const result = (await browser.runtime.sendMessage({
      type: 'clip:reveal',
      id,
    })) as { ok?: boolean; error?: string; opened?: boolean }
    setStatus(result?.ok ? (result.opened ? '클립 페이지를 열었습니다' : '클립 위치로 이동') : `이동 실패: ${result?.error || 'unknown error'}`)
    window.setTimeout(() => setStatus(''), 1700)
  }

  const onDeleteClip = async (id: string) => {
    const result = (await browser.runtime.sendMessage({
      type: 'clip:delete',
      id,
    })) as { ok?: boolean; error?: string }
    if (!result?.ok) {
      setStatus(`삭제 실패: ${result?.error || 'unknown error'}`)
      window.setTimeout(() => setStatus(''), 1600)
      return
    }
    setClips((prev) => prev.filter((item) => item.id !== id))
    setStatus('클립 삭제 완료')
    window.setTimeout(() => setStatus(''), 1200)
  }

  const onExportClips = async () => {
    const response = (await browser.runtime.sendMessage({
      type: 'clip:export',
    })) as { ok?: boolean; items?: ClipItem[]; error?: string }
    if (!response?.ok || !Array.isArray(response.items)) {
      setStatus(`내보내기 실패: ${response?.error || 'unknown error'}`)
      window.setTimeout(() => setStatus(''), 1800)
      return
    }
    const payload = {
      exportedAt: new Date().toISOString(),
      version: 1,
      count: response.items.length,
      clips: response.items,
    }
    const stamp = new Date().toISOString().replace(/[:.]/g, '-')
    const fileName = `gomdown-clips-${stamp}.json`
    const result = (await browser.runtime.sendMessage({
      type: 'file:download-text',
      filename: fileName,
      mime: 'application/json;charset=utf-8',
      content: JSON.stringify(payload, null, 2),
    })) as { ok?: boolean; error?: string }
    if (!result?.ok) {
      setStatus(`내보내기 실패: ${result?.error || 'download error'}`)
      window.setTimeout(() => setStatus(''), 1800)
      return
    }
    setStatus(`클립 ${response.items.length}개 내보내기 완료`)
    window.setTimeout(() => setStatus(''), 1500)
  }

  const onClickImport = () => {
    importInputRef.current?.click()
  }

  const onExportMarkdown = async () => {
    const pageClips = clips.filter((item) => normalizePageUrl(item.pageUrl) === normalizePageUrl(activePageUrl))
    if (pageClips.length === 0) {
      setStatus('내보낼 클립이 없습니다.')
      window.setTimeout(() => setStatus(''), 1500)
      return
    }
    const pageTitle = pageClips[0]?.pageTitle || activePageUrl || 'Untitled'
    const markdown = buildPageMarkdown(pageClips, activePageUrl)
    const result = (await browser.runtime.sendMessage({
      type: 'file:download-text',
      filename: `${safeFileName(pageTitle)}-clips.md`,
      mime: 'text/markdown;charset=utf-8',
      content: markdown,
    })) as { ok?: boolean; error?: string }
    if (!result?.ok) {
      setStatus(`Markdown 내보내기 실패: ${result?.error || 'download error'}`)
      window.setTimeout(() => setStatus(''), 1900)
      return
    }
    setStatus(`Markdown 내보내기 완료 (${pageClips.length}개)`)
    window.setTimeout(() => setStatus(''), 1700)
  }

  const onCopyMarkdown = async () => {
    const pageClips = clips.filter((item) => normalizePageUrl(item.pageUrl) === normalizePageUrl(activePageUrl))
    if (pageClips.length === 0) {
      setStatus('복사할 클립이 없습니다.')
      window.setTimeout(() => setStatus(''), 1500)
      return
    }
    const markdown = buildPageMarkdown(pageClips, activePageUrl)
    try {
      await navigator.clipboard.writeText(markdown)
      setStatus(`Markdown 복사 완료 (${pageClips.length}개)`)
      window.setTimeout(() => setStatus(''), 1700)
    } catch (error) {
      setStatus(`복사 실패: ${String(error)}`)
      window.setTimeout(() => setStatus(''), 1700)
    }
  }

  const onSendToObsidian = async () => {
    const pageClips = clips.filter((item) => normalizePageUrl(item.pageUrl) === normalizePageUrl(activePageUrl))
    if (pageClips.length === 0) {
      setStatus('보낼 클립이 없습니다.')
      window.setTimeout(() => setStatus(''), 1500)
      return
    }
    const pageTitle = pageClips[0]?.pageTitle || activePageUrl || 'Untitled'
    try {
      const result = (await browser.runtime.sendMessage({
        type: 'clip:send-obsidian-current-page',
        pageUrl: activePageUrl,
        pageTitle,
      })) as { ok?: boolean; error?: string; uri?: string }
      if (!result?.ok || !result?.uri) {
        setStatus(`Obsidian 전송 실패: ${result?.error || 'unknown error'}`)
        window.setTimeout(() => setStatus(''), 2200)
        return
      }
      window.open(String(result.uri), '_blank')
      setStatus('Obsidian 새 노트 전송 시도 완료')
    } catch (error) {
      setStatus(`Obsidian 전송 실패: ${String(error)}`)
      try {
        const result = (await browser.runtime.sendMessage({
          type: 'clip:send-obsidian-current-page',
          pageUrl: activePageUrl,
          pageTitle,
        })) as { ok?: boolean; uri?: string }
        if (result?.ok && result.uri) {
          window.location.href = String(result.uri)
          setStatus('Obsidian 새 노트 전송 시도 완료')
        }
      } catch {
        // ignored
      }
    } finally {
      window.setTimeout(() => setStatus(''), 1900)
    }
  }

  const onImportClips = async (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0]
    event.target.value = ''
    if (!file) return
    try {
      const text = await file.text()
      const parsed = JSON.parse(text) as { clips?: unknown[] } | unknown[]
      const clips = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.clips) ? parsed.clips : []
      if (!Array.isArray(clips)) {
        setStatus('가져오기 실패: JSON 형식이 올바르지 않습니다.')
        window.setTimeout(() => setStatus(''), 1900)
        return
      }
      const result = (await browser.runtime.sendMessage({
        type: 'clip:import',
        items: clips,
      })) as { ok?: boolean; imported?: number; total?: number; error?: string }
      if (!result?.ok) {
        setStatus(`가져오기 실패: ${result?.error || 'unknown error'}`)
        window.setTimeout(() => setStatus(''), 1900)
        return
      }
      setStatus(`가져오기 완료: ${result.imported || 0}/${result.total || clips.length}`)
      window.setTimeout(() => setStatus(''), 1900)
      const refreshed = (await browser.runtime.sendMessage({
        type: 'clip:list',
        pageUrl: activePageUrl,
      })) as { ok?: boolean; items?: ClipItem[] }
      if (refreshed?.ok && Array.isArray(refreshed.items)) {
        setClips(refreshed.items)
      }
    } catch (error) {
      setStatus(`가져오기 실패: ${String(error)}`)
      window.setTimeout(() => setStatus(''), 1900)
    }
  }

  if (!settings) {
    return <div className="container">Loading...</div>
  }

  return (
    <div className="container">
      <div className="top-row">
        <h1>Gomdown Helper</h1>
        <button
          className={`power-toggle ${settings.extensionStatus ? 'on' : 'off'}`}
          onClick={() => {
            void onToggleExtension(!settings.extensionStatus)
          }}
        >
          <span className="power-dot" />
          {settings.extensionStatus ? 'ON' : 'OFF'}
        </button>
      </div>

      <div className="field">
        <label>RPC Secret</label>
        <input
          type="text"
          value={settings.motrixAPIkey}
          onChange={(e) => update('motrixAPIkey', e.target.value)}
          placeholder="aria2 rpc secret"
        />
      </div>

      <div className="field">
        <label>RPC Port</label>
        <input
          type="number"
          value={settings.motrixPort}
          onChange={(e) => update('motrixPort', Number(e.target.value || 16800))}
        />
      </div>

      <label className="toggle">
        Use Native Host
        <input
          type="checkbox"
          checked={settings.useNativeHost}
          onChange={(e) => update('useNativeHost', e.target.checked)}
        />
      </label>

      <label className="toggle">
        Activate gdown App
        <input
          type="checkbox"
          checked={settings.activateAppOnDownload}
          onChange={(e) => update('activateAppOnDownload', e.target.checked)}
        />
      </label>

      <div className="media-panel">
        <div className="media-head">
          <strong>Captured Media</strong>
          <button className="mini ghost" onClick={onClearMedia}>Clear</button>
        </div>
        {mediaItems.length === 0 ? (
          <div className="empty">No media captured yet</div>
        ) : (
          <div className="media-list">
            {mediaItems.map((item) => (
              <div key={item.id} className="media-item">
                <div className="media-meta">
                  <span className="kind">{item.kind.toUpperCase()}</span>
                  {item.pageTitle ? <span className="url">{item.pageTitle}</span> : null}
                  <span className="url">{item.url}</span>
                </div>
                <button className="mini" onClick={() => void onSendMedia(item)}>Add</button>
              </div>
            ))}
          </div>
        )}
      </div>
      <div className="media-panel">
        <div className="page-clips-head">
          <div className="page-clips-title-row">
            <strong>Page Clips</strong>
            <span className="hint">Alt+Shift+C</span>
          </div>
          <div className="page-clips-actions">
            <button className="mini ghost page-action-btn" onClick={() => void onSendToObsidian()}>Obsidian</button>
            <button className="mini ghost page-action-btn" onClick={() => void onCopyMarkdown()}>Copy MD</button>
            <button className="mini ghost page-action-btn" onClick={() => void onExportMarkdown()}>MD</button>
            <button className="mini ghost page-action-btn" onClick={() => void onExportClips()}>Export</button>
            <button className="mini ghost page-action-btn page-action-btn-wide" onClick={onClickImport}>Import</button>
          </div>
        </div>
        <input ref={importInputRef} type="file" accept="application/json,.json" style={{ display: 'none' }} onChange={onImportClips} />
        {clips.length === 0 ? (
          <div className="empty">No clips for this page</div>
        ) : (
          <div className="media-list">
            {clips.map((item) => (
              <div key={item.id} className="clip-item">
                <div className="media-meta">
                  {item.resolveStatus === 'broken' ? <span className="broken-badge">BROKEN</span> : null}
                  <span className="url">{item.quote || item.anchor.exact}</span>
                </div>
                <div className="clip-actions">
                  <button className="mini" onClick={() => void onRevealClip(item.id)}>Go</button>
                  <button className="mini ghost" onClick={() => void onDeleteClip(item.id)}>Del</button>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
      <div className="status">{status}</div>
      <div className="action-dock">
        <button className="action-btn" onClick={onSave} title="Save">
          <ActionIcon kind="save" />
          <span>Save</span>
        </button>
        <button className="action-btn" onClick={onOpenSettings} title="Settings">
          <ActionIcon kind="settings" />
          <span>Settings</span>
        </button>
        <button className="action-btn" onClick={onOpenHistory} title="History">
          <ActionIcon kind="history" />
          <span>History</span>
        </button>
        <button className="action-btn" onClick={() => void onSendActiveTabByYtDlp()} title="Send Active">
          <ActionIcon kind="send" />
          <span>Send</span>
        </button>
        <button className="action-btn" onClick={() => void onCreateClipFromSelection()} title="Capture Selection">
          <ActionIcon kind="clip" />
          <span>Capture</span>
        </button>
      </div>
    </div>
  )
}

createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)
