import { useCallback, useEffect, useRef, useState } from 'react'
import { api, type DocSummary, type Document, type Suggestion, type Tag, type TagColor } from './api/client'
import { useAutoSave } from './hooks/useAutoSave'
import { useCheckpoint } from './hooks/useCheckpoint'
import { useSpellChecker } from './hooks/useSpellChecker'
import { useTags } from './hooks/useTags'
import { DocList } from './components/DocList/DocList'
import { EditorCore, type EditorChange } from './components/Editor/EditorCore'
import { ToneSelect } from './components/Editor/ToneSelect'
import { ExportMenu } from './components/Export/ExportMenu'
import { HistoryPanel } from './components/History/HistoryPanel'
import { GardenPanel } from './components/Garden/GardenPanel'
import { StatusBar } from './components/StatusBar/StatusBar'
import { PetalCompanion } from './components/Companion/PetalCompanion'
import { UpdateBanner } from './components/UpdateBanner/UpdateBanner'
import { useVersionWatch } from './hooks/useVersionWatch'
import { PetalFall } from './effects/PetalFall'
import { useNightMode } from './hooks/useNightMode'
import { playSuggestionSound } from './audio/sounds'

export default function App() {
  const updateAvailable = useVersionWatch()
  // Late-night calm mode: dark theme + falling stars instead of petals. The hook
  // toggles the `petal-night` class on <html>; we pass the flag to the ambient
  // layer so the petals become stars.
  const night = useNightMode()
  const [docs, setDocs] = useState<DocSummary[]>([])
  const [currentDoc, setCurrentDoc] = useState<Document | null>(null)
  const [title, setTitle] = useState('')
  const [wordCount, setWordCount] = useState(0)
  // The current document's target tone (steers checkpoint advice) and its live
  // plain text (drives the expanded writing-stats panel in the StatusBar).
  const [tone, setTone] = useState('general')
  const [docText, setDocText] = useState('')
  const [ready, setReady] = useState(false)
  // Distraction-free mode: entered on editor focus, collapses the doc-list
  // sidebar. Escape or a click outside the editor canvas restores it.
  const [focusMode, setFocusMode] = useState(false)
  // Mobile drawer: below the tablet breakpoint the sidebar is an overlay toggled
  // by the header hamburger. Ignored on wide screens (sidebar is always in-flow).
  const [drawerOpen, setDrawerOpen] = useState(false)
  const canvasRef = useRef<HTMLDivElement>(null)
  // Monotonic counters the companion watches to react to writing + accepts.
  const [editTick, setEditTick] = useState(0)
  const [acceptTick, setAcceptTick] = useState(0)
  // History drawer visibility, and an epoch bumped on restore to force the
  // editor to remount with the restored content (its initialContent is read
  // only on mount).
  const [historyOpen, setHistoryOpen] = useState(false)
  const [gardenOpen, setGardenOpen] = useState(false)
  const [editorEpoch, setEditorEpoch] = useState(0)

  // Live mirrors of the current doc's editable fields so the "discard blank
  // drafts on navigation" logic can read the latest values without rebuilding
  // callbacks on every keystroke.
  const currentDocRef = useRef(currentDoc)
  const titleRef = useRef(title)
  const wordCountRef = useRef(wordCount)
  currentDocRef.current = currentDoc
  titleRef.current = title
  wordCountRef.current = wordCount

  // A throwaway blank draft: no words and still the default/empty title. We
  // delete these on navigation rather than leave orphan "Untitled" docs behind.
  const isBlankDraft = useCallback(() => {
    const t = titleRef.current.trim()
    return wordCountRef.current === 0 && (t === '' || t === 'Untitled')
  }, [])

  const { status, schedule, saveNow } = useAutoSave(currentDoc?.id ?? null)
  const {
    suggestions,
    checking,
    voicing,
    collocating,
    llmDown,
    schedule: scheduleCheckpoint,
    runVoice,
    runCollocation,
    removeSuggestion,
  } = useCheckpoint(currentDoc?.id ?? null)
  // Browser-side spell checker — loads the en-US dictionary once per session.
  const { checker: spellChecker, addWord } = useSpellChecker()
  // The tag roster (with counts). Assignments live on the doc summaries below.
  const { tags: tagRoster, refresh: refreshTags, createTag } = useTags()

  // Patch a summary in the sidebar list (optimistic title / word-count updates).
  const patchSummary = useCallback((id: string, patch: Partial<DocSummary>) => {
    setDocs((prev) => prev.map((d) => (d.id === id ? { ...d, ...patch } : d)))
  }, [])

  // Attach or detach a tag on a document, updating the sidebar optimistically and
  // refreshing the roster so its counts stay current. Tags are kept sorted by
  // name to match the server's ordering.
  const setDocTag = useCallback(
    async (docId: string, tag: Tag, attach: boolean) => {
      setDocs((prev) =>
        prev.map((d) => {
          if (d.id !== docId) return d
          const without = d.tags.filter((t) => t.id !== tag.id)
          const next = attach ? [...without, tag] : without
          next.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }))
          return { ...d, tags: next }
        }),
      )
      try {
        if (attach) await api.assignTag(docId, tag.id)
        else await api.unassignTag(docId, tag.id)
        void refreshTags()
      } catch (err) {
        console.error('tag assignment failed', err)
        void refreshTags()
      }
    },
    [refreshTags],
  )

  const handleToggleTag = useCallback(
    (docId: string, tag: Tag) => {
      const doc = docs.find((d) => d.id === docId)
      const has = doc?.tags.some((t) => t.id === tag.id) ?? false
      void setDocTag(docId, tag, !has)
    },
    [docs, setDocTag],
  )

  const handleCreateTag = useCallback(
    async (docId: string, name: string, color: TagColor) => {
      const tag = await createTag(name, color)
      if (tag) void setDocTag(docId, tag, true)
    },
    [createTag, setDocTag],
  )

  const openDoc = useCallback(
    async (id: string) => {
      setDrawerOpen(false) // close the mobile drawer when a doc is chosen
      // Decide the leaving doc's fate before any await mutates state.
      const leaving = currentDocRef.current
      const leavingBlank = isBlankDraft()
      await saveNow() // flush any pending edits to the doc we're leaving
      const doc = await api.getDoc(id)
      setCurrentDoc(doc)
      setTitle(doc.title)
      setWordCount(doc.word_count)
      setTone(doc.tone || 'general')
      setDocText(doc.content_text)
      // Leaving an untouched blank draft for a different doc? Drop it silently
      // (best-effort cleanup — a 404 just means it's already gone).
      if (leaving && leaving.id !== id && leavingBlank) {
        api.deleteDoc(leaving.id).catch(() => {})
        setDocs((prev) => prev.filter((d) => d.id !== leaving.id))
      }
    },
    [saveNow, isBlankDraft],
  )

  // Initial load: fetch the list, opening the first doc (or creating one).
  const bootedRef = useRef(false)
  useEffect(() => {
    if (bootedRef.current) return
    bootedRef.current = true
    ;(async () => {
      try {
        let list = await api.listDocs()
        if (list.length === 0) {
          const fresh = await api.createDoc()
          list = [{ id: fresh.id, title: fresh.title, word_count: 0, updated_at: fresh.updated_at, tags: [] }]
          setDocs(list)
          setCurrentDoc(fresh)
          setTitle(fresh.title)
          setWordCount(0)
          setTone(fresh.tone || 'general')
          setDocText('')
        } else {
          setDocs(list)
          await openDoc(list[0].id)
        }
      } catch (err) {
        console.error('failed to load documents', err)
      } finally {
        setReady(true)
      }
    })()
  }, [openDoc])

  const handleCreate = useCallback(async () => {
    // Already sitting on a fresh blank draft? Reuse it instead of stacking
    // another empty "Untitled" on top.
    if (currentDocRef.current && isBlankDraft()) return
    await saveNow()
    const fresh = await api.createDoc()
    setDocs((prev) => [
      { id: fresh.id, title: fresh.title, word_count: 0, updated_at: fresh.updated_at, tags: [] },
      ...prev,
    ])
    setCurrentDoc(fresh)
    setTitle(fresh.title)
    setWordCount(0)
    setTone(fresh.tone || 'general')
    setDocText('')
  }, [saveNow, isBlankDraft])

  // Duplicate a document: copy its body/tone into a fresh doc titled "… (副本)",
  // then open the copy. Tags aren't carried over (a fresh start for the copy).
  const handleDuplicate = useCallback(
    async (id: string) => {
      try {
        await saveNow() // flush in case we're duplicating the open doc
        const src = await api.getDoc(id)
        const fresh = await api.createDoc()
        const dupTitle = `${src.title?.trim() || 'Untitled'} (副本)`
        const updated = await api.updateDoc(fresh.id, {
          title: dupTitle,
          content: src.content,
          content_text: src.content_text,
          tone: src.tone,
          word_count: src.word_count,
        })
        setDocs((prev) => [
          { id: updated.id, title: updated.title, word_count: updated.word_count, updated_at: updated.updated_at, tags: [] },
          ...prev,
        ])
        setCurrentDoc(updated)
        setTitle(updated.title)
        setWordCount(updated.word_count)
        setTone(updated.tone || 'general')
        setDocText(updated.content_text)
      } catch (err) {
        console.error('duplicate failed', err)
      }
    },
    [saveNow],
  )

  const handleDelete = useCallback(
    async (id: string) => {
      await api.deleteDoc(id)
      setDocs((prev) => {
        const remaining = prev.filter((d) => d.id !== id)
        if (currentDoc?.id === id) {
          if (remaining.length > 0) {
            void openDoc(remaining[0].id)
          } else {
            setCurrentDoc(null)
            setTitle('')
            setWordCount(0)
            setTone('general')
            setDocText('')
          }
        }
        return remaining
      })
    },
    [currentDoc, openDoc],
  )

  const handleTitleChange = useCallback(
    (value: string) => {
      setTitle(value)
      if (currentDoc) {
        patchSummary(currentDoc.id, { title: value })
        schedule({ title: value })
      }
    },
    [currentDoc, patchSummary, schedule],
  )

  const handleEditorChange = useCallback(
    (change: EditorChange) => {
      setWordCount(change.word_count)
      setDocText(change.content_text)
      setEditTick((n) => n + 1)
      if (currentDoc) {
        patchSummary(currentDoc.id, { word_count: change.word_count })
        schedule(change)
        scheduleCheckpoint()
      }
    },
    [currentDoc, patchSummary, schedule, scheduleCheckpoint],
  )

  // Changing the tone persists it and re-runs the checkpoint so Petal's advice
  // re-tunes to the new register. The auto-save (1.5s) lands before the
  // checkpoint debounce (4s), so the server reads the updated tone.
  const handleToneChange = useCallback(
    (value: string) => {
      setTone(value)
      if (currentDoc) {
        schedule({ tone: value })
        scheduleCheckpoint()
      }
    },
    [currentDoc, schedule, scheduleCheckpoint],
  )

  // Accept applies the replacement in the editor (handled in EditorCore) and
  // marks the suggestion accepted; dismiss just rejects it. Both drop it locally.
  const handleAccept = useCallback(
    async (s: Suggestion) => {
      removeSuggestion(s.id)
      setAcceptTick((n) => n + 1)
      try {
        await api.acceptSuggestion(s.id)
      } catch (err) {
        console.error('accept failed', err)
      }
    },
    [removeSuggestion],
  )

  // After restoring a version, swap the restored doc into the editor. Bumping
  // editorEpoch remounts EditorCore so it picks up the restored content.
  const handleRestored = useCallback(
    (doc: Document) => {
      setCurrentDoc(doc)
      setTitle(doc.title)
      setWordCount(doc.word_count)
      setTone(doc.tone || 'general')
      setDocText(doc.content_text)
      patchSummary(doc.id, { title: doc.title, word_count: doc.word_count })
      setEditorEpoch((n) => n + 1)
    },
    [patchSummary],
  )

  // Escape always restores the sidebar while in distraction-free mode.
  useEffect(() => {
    if (!focusMode) return
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setFocusMode(false)
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [focusMode])

  // A pointer-down outside the centered editor canvas (the cream gutters, the
  // header, the status bar) also restores the sidebar.
  const handleChromeDown = useCallback((e: React.MouseEvent) => {
    if (!canvasRef.current?.contains(e.target as Node)) setFocusMode(false)
  }, [])

  const handleDismiss = useCallback(
    async (s: Suggestion) => {
      removeSuggestion(s.id)
      try {
        await api.dismissSuggestion(s.id)
      } catch (err) {
        console.error('dismiss failed', err)
      }
    },
    [removeSuggestion],
  )

  // Play a soft sound when freshly-checked suggestions arrive — one per distinct
  // new type, lightly staggered so a batch reads as a little melody rather than
  // a pile-up. We track which ids we've already chimed for, and only chime for
  // recently-created suggestions so opening a doc with old pending advice stays
  // silent (the existing set was created in a past session).
  const chimedRef = useRef<Set<string>>(new Set())
  useEffect(() => {
    const fresh = suggestions.filter((s) => !chimedRef.current.has(s.id))
    fresh.forEach((s) => chimedRef.current.add(s.id))
    const justMade = fresh.filter(
      (s) => Date.now() - new Date(s.created_at).getTime() < 12_000,
    )
    if (justMade.length === 0) return
    const types = [...new Set(justMade.map((s) => s.type))].slice(0, 3)
    const timers = types.map((t, i) =>
      window.setTimeout(() => playSuggestionSound(t), i * 150),
    )
    return () => timers.forEach((id) => window.clearTimeout(id))
  }, [suggestions])

  // Forget chimed ids when switching documents so the set can't grow unbounded.
  useEffect(() => {
    chimedRef.current = new Set()
  }, [currentDoc?.id])

  return (
    <div className="flex h-full flex-col">
      <PetalFall night={night} />
      <header
        onMouseDown={handleChromeDown}
        className="petal-no-print flex h-12 shrink-0 items-center gap-2 px-5"
        style={{ borderBottom: '1px solid var(--color-border)' }}
      >
        <button
          type="button"
          aria-label="Toggle document list"
          onClick={() => {
            setFocusMode(false)
            setDrawerOpen((v) => !v)
          }}
          className="petal-sidebar-toggle petal-tap-sm -ml-2 mr-1 items-center justify-center text-xl"
          style={{ borderRadius: 'var(--radius-pill)', width: 36, height: 36, color: 'var(--color-plum)' }}
        >
          ☰
        </button>
        <span className="text-xl">🌸</span>
        <span className="text-lg font-extrabold text-plum">Petal</span>
        <button
          type="button"
          onClick={() => setGardenOpen(true)}
          aria-label="Vocabulary garden"
          title="Words you've looked up, blooming for review"
          className="petal-tap-sm ml-auto inline-flex h-9 items-center gap-1.5 whitespace-nowrap px-3 text-sm font-bold"
          style={{
            borderRadius: 'var(--radius-pill)',
            background: 'var(--color-surface)',
            color: 'var(--color-plum)',
            boxShadow: 'var(--shadow-soft)',
          }}
        >
          <span aria-hidden>🌷</span>
          <span>词汇花园</span>
          <span style={{ color: 'var(--color-muted)' }}>· Garden</span>
        </button>
      </header>

      <div className="relative flex min-h-0 flex-1">
        <div
          className={`petal-no-print petal-sidebar shrink-0${focusMode ? ' petal-sidebar-hidden' : ''}${drawerOpen ? ' petal-drawer-open' : ''}`}
        >
          <DocList
            docs={docs}
            roster={tagRoster}
            selectedId={currentDoc?.id ?? null}
            onSelect={openDoc}
            onCreate={handleCreate}
            onDelete={handleDelete}
            onDuplicate={handleDuplicate}
            onToggleTag={handleToggleTag}
            onCreateTag={handleCreateTag}
          />
        </div>

        <div
          className={`petal-no-print petal-scrim${drawerOpen ? ' petal-scrim-show' : ''}`}
          onClick={() => setDrawerOpen(false)}
          aria-hidden
        />

        <main className="flex min-w-0 flex-1 flex-col">
          {currentDoc ? (
            <>
              <div
                onMouseDown={handleChromeDown}
                className="flex flex-1 flex-col overflow-y-auto px-6 py-8"
              >
                <div ref={canvasRef} className="mx-auto flex w-full max-w-[720px] flex-1 flex-col">
                  <div className="mb-5 flex items-center gap-3">
                    <input
                      value={title}
                      onChange={(e) => handleTitleChange(e.target.value)}
                      placeholder="Untitled"
                      aria-label="Document title"
                      className="min-w-0 flex-1 bg-transparent text-3xl font-extrabold text-plum focus:outline-none"
                      style={{ fontFamily: 'var(--font-ui)' }}
                    />
                    <div className="petal-no-print shrink-0">
                      <ToneSelect value={tone} onChange={handleToneChange} />
                    </div>
                    <button
                      type="button"
                      onClick={() => setHistoryOpen(true)}
                      aria-label="Version history"
                      title="Browse and restore earlier versions"
                      className="petal-no-print inline-flex h-9 shrink-0 items-center gap-1.5 whitespace-nowrap px-3 text-sm font-bold"
                      style={{
                        borderRadius: 'var(--radius-pill)',
                        background: 'var(--color-surface)',
                        color: 'var(--color-plum)',
                        boxShadow: 'var(--shadow-soft)',
                      }}
                    >
                      <span aria-hidden>🕘</span>
                      <span>历史</span>
                      <span style={{ color: 'var(--color-muted)' }}>· History</span>
                    </button>
                    <div className="petal-no-print">
                      <ExportMenu docId={currentDoc.id} />
                    </div>
                  </div>
                  <EditorCore
                    key={`${currentDoc.id}:${editorEpoch}`}
                    docId={currentDoc.id}
                    initialContent={currentDoc.content}
                    onChange={handleEditorChange}
                    suggestions={suggestions}
                    onAccept={handleAccept}
                    onDismiss={handleDismiss}
                    onVoiceCheck={runVoice}
                    voicing={voicing}
                    onCollocationCheck={runCollocation}
                    collocating={collocating}
                    onFocusMode={() => setFocusMode(true)}
                    spellChecker={spellChecker}
                    onAddWord={addWord}
                  />
                </div>
              </div>
              <div className="petal-no-print" onMouseDown={handleChromeDown}>
                <StatusBar
                  wordCount={wordCount}
                  text={docText}
                  saveStatus={status}
                  checking={checking}
                  voicing={voicing}
                  collocating={collocating}
                  llmDown={llmDown}
                />
              </div>
            </>
          ) : (
            <div
              className="flex flex-1 items-center justify-center text-sm"
              style={{ color: 'var(--color-muted)' }}
            >
              {ready ? 'Create a document to start writing.' : 'Loading…'}
            </div>
          )}
        </main>
      </div>

      {historyOpen && currentDoc && (
        <HistoryPanel
          docId={currentDoc.id}
          onClose={() => setHistoryOpen(false)}
          onRestored={handleRestored}
        />
      )}

      {gardenOpen && (
        <GardenPanel
          onClose={() => setGardenOpen(false)}
          onOpenDoc={(id) => {
            setGardenOpen(false)
            void openDoc(id)
          }}
        />
      )}

      {updateAvailable && <UpdateBanner />}

      <div className="petal-no-print">
        <PetalCompanion
          wordCount={wordCount}
          saveStatus={status}
          llmDown={llmDown}
          editTick={editTick}
          acceptTick={acceptTick}
          text={docText}
        />
      </div>
    </div>
  )
}
