import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Link } from 'react-router-dom'

// ── Types ────────────────────────────────────────────────────────────────────

interface EmailDeliveryState {
  status: string
  lastEventAt: string | null
}

interface ContactSubmission {
  id: string
  submittedAt: string
  name: string
  email: string
  message: string
  messageType: string
  subscribe: boolean
  archived?: boolean
  starred?: boolean
  source?: string
  inboundTo?: string
  notes?: string
  tags?: string[]
  emailStatus?: {
    welcome: EmailDeliveryState
    adminNotification: EmailDeliveryState
    adminReply: EmailDeliveryState
  }
}

interface ReplyHistoryItem {
  id: string
  submissionId: string
  toEmail: string
  toName: string
  fromEmail: string
  subject: string
  preview: string
  sentAt: string
  scheduledAt?: string | null
}

interface Contact {
  key: string
  email: string
  name: string
  source: string | undefined
  subscribe: boolean
  archived: boolean
  firstContactAt: string   // oldest submission date
  latestAt: string         // newest submission date
  message: string
  notes: string
  tags: string[]
  lastContactedAt: string | null
  submissionCount: number
  mainId: string
  allIds: string[]
  allSubmissions: ContactSubmission[]
  bestDeliveryStatus: string | null   // opened/clicked/delivered/sent/null
  unreplied: boolean                  // has inbound messages, no reply sent
  engagementScore: number
}

type ConversationItem =
  | { kind: 'inbound'; date: string; name: string; message: string; source?: string; id: string }
  | { kind: 'outbound'; date: string; subject: string; preview: string; toEmail: string }

interface ChecklistEpisode {
  id: string
  series: string
  episodeNumber: number | null
  title: string
  datePublished: string
}

// ── Helpers ───────────────────────────────────────────────────────────────────

const TAG_PALETTE = [
  '#1a3d2b', '#2b1a3d', '#3d1a1a', '#1a2b3d', '#3d2b1a',
  '#1a3d3d', '#3d1a3d', '#2b3d1a', '#1a1a3d', '#3d3d1a',
]

function tagBg(tag: string): string {
  let h = 0
  for (const c of tag) h = (h * 31 + c.charCodeAt(0)) & 0x7fffffff
  return TAG_PALETTE[h % TAG_PALETTE.length]
}

function fmtDate(iso: string | null | undefined) {
  if (!iso) return '—'
  try { return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) }
  catch { return '—' }
}

function fmtShort(iso: string | null | undefined) {
  if (!iso) return ''
  try {
    const d = new Date(iso)
    const now = new Date()
    if (d.toDateString() === now.toDateString()) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
    if (d.getFullYear() === now.getFullYear()) return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
    return d.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' })
  } catch { return '' }
}

// ── CSV helpers ───────────────────────────────────────────────────────────────

function csvField(v: string): string {
  const s = String(v ?? '')
  return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s
}

function exportContactsCSV(contacts: Contact[]) {
  const headers = ['name', 'email', 'notes', 'tags', 'source', 'subscribed', 'first_contact', 'last_contact', 'message_count']
  const rows = contacts.map(c => [
    c.name, c.email, c.notes,
    c.tags.join(';'),
    c.source ?? '',
    c.subscribe ? 'yes' : 'no',
    c.firstContactAt.slice(0, 10),
    c.latestAt.slice(0, 10),
    String(c.submissionCount),
  ])
  const csv = [headers, ...rows].map(r => r.map(csvField).join(',')).join('\r\n')
  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url; a.download = `contacts-${new Date().toISOString().slice(0, 10)}.csv`; a.click()
  setTimeout(() => URL.revokeObjectURL(url), 5000)
}

function parseCSVRow(line: string): string[] {
  const result: string[] = []
  let cur = '', inQ = false
  for (let i = 0; i < line.length; i++) {
    const ch = line[i]
    if (ch === '"') {
      if (inQ && line[i + 1] === '"') { cur += '"'; i++ }
      else inQ = !inQ
    } else if (ch === ',' && !inQ) { result.push(cur); cur = '' }
    else cur += ch
  }
  result.push(cur)
  return result
}

function parseCSV(text: string): Record<string, string>[] {
  const lines = text.split(/\r?\n/).filter(l => l.trim())
  if (lines.length < 2) return []
  const headers = parseCSVRow(lines[0]).map(h => h.toLowerCase().trim().replace(/\s+/g, '_'))
  return lines.slice(1)
    .map(line => {
      const vals = parseCSVRow(line)
      return Object.fromEntries(headers.map((h, i) => [h, (vals[i] ?? '').trim()]))
    })
    .filter(r => Object.values(r).some(v => v))
}

// ── Auth Shell ────────────────────────────────────────────────────────────────

export default function ContactsShell() {
  const [authState, setAuthState] = useState<'checking' | 'needs-password' | 'needs-totp' | 'ok'>('checking')
  const [password, setPassword] = useState('')
  const [totp, setTotp] = useState('')
  const [authError, setAuthError] = useState('')
  const [authBusy, setAuthBusy] = useState(false)
  const [pendingToken, setPendingToken] = useState('')

  useEffect(() => {
    fetch('/api/admin-auth/status', { credentials: 'include' })
      .then(r => r.json())
      .then((d: { authenticated?: boolean }) => setAuthState(d.authenticated ? 'ok' : 'needs-password'))
      .catch(() => setAuthState('needs-password'))
  }, [])

  async function handleLogin(e: React.FormEvent) {
    e.preventDefault(); setAuthBusy(true); setAuthError('')
    try {
      const res = await fetch('/api/admin-auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }), credentials: 'include' })
      const data = await res.json() as { ok?: boolean; totpRequired?: boolean; pendingToken?: string; message?: string }
      if (!res.ok) { setAuthError(data.message ?? 'Invalid password.'); setAuthBusy(false); return }
      if (data.totpRequired && data.pendingToken) { setPendingToken(data.pendingToken); setAuthState('needs-totp'); setAuthBusy(false); return }
      setAuthState('ok')
    } catch { setAuthError('Login failed.') }
    setAuthBusy(false)
  }

  async function handleTotp(e: React.FormEvent) {
    e.preventDefault(); setAuthBusy(true); setAuthError('')
    try {
      const res = await fetch('/api/admin-auth/totp-verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pendingToken, code: totp }), credentials: 'include' })
      const data = await res.json() as { ok?: boolean; message?: string }
      if (!res.ok) { setAuthError(data.message ?? 'Invalid code.'); setAuthBusy(false); return }
      setAuthState('ok')
    } catch { setAuthError('Verification failed.') }
    setAuthBusy(false)
  }

  if (authState === 'checking') return <div className="em-auth-loading">Loading…</div>

  if (authState === 'needs-password') return (
    <div className="em-auth-wrap">
      <form className="em-auth-form" onSubmit={handleLogin}>
        <h1 className="em-auth-title">Contacts</h1>
        <label className="em-auth-label">Admin password<input type="password" className="em-auth-input" value={password} onChange={e => setPassword(e.target.value)} autoFocus /></label>
        {authError && <p className="em-auth-error">{authError}</p>}
        <button type="submit" className="em-btn em-btn--primary" disabled={authBusy}>{authBusy ? 'Signing in…' : 'Sign in'}</button>
      </form>
    </div>
  )

  if (authState === 'needs-totp') return (
    <div className="em-auth-wrap">
      <form className="em-auth-form" onSubmit={handleTotp}>
        <h1 className="em-auth-title">Two-factor code</h1>
        <label className="em-auth-label">Authenticator code<input type="text" className="em-auth-input" inputMode="numeric" pattern="[0-9]*" maxLength={6} value={totp} onChange={e => setTotp(e.target.value)} autoFocus /></label>
        {authError && <p className="em-auth-error">{authError}</p>}
        <button type="submit" className="em-btn em-btn--primary" disabled={authBusy}>{authBusy ? 'Verifying…' : 'Verify'}</button>
      </form>
    </div>
  )

  return <ContactsClient />
}

// ── Contacts Client ───────────────────────────────────────────────────────────

function ContactsClient() {
  const [submissions, setSubmissions] = useState<ContactSubmission[]>([])
  const [replyHistory, setReplyHistory] = useState<ReplyHistoryItem[]>([])
  const [checklistEpisodes, setChecklistEpisodes] = useState<ChecklistEpisode[]>([])
  const [loading, setLoading] = useState(true)
  const [search, setSearch] = useState('')
  const [tagFilter, setTagFilter] = useState('')
  const [showArchived, setShowArchived] = useState(false)

  // Edit state
  const [editingKey, setEditingKey] = useState<string | null>(null)
  const [editName, setEditName] = useState('')
  const [editNotes, setEditNotes] = useState('')
  const [editTags, setEditTags] = useState<string[]>([])
  const [editTagInput, setEditTagInput] = useState('')
  const [editSaving, setEditSaving] = useState(false)

  // Merge state
  const [mergePickerKey, setMergePickerKey] = useState<string | null>(null)
  const [mergeSearch, setMergeSearch] = useState('')
  const [mergeBusy, setMergeBusy] = useState(false)
  const [mergeMsg, setMergeMsg] = useState('')
  // Drip trigger
  const [dripBusyKey, setDripBusyKey] = useState<string | null>(null)
  const [dripMsgKey, setDripMsgKey] = useState<string | null>(null)
  const [dripMsgText, setDripMsgText] = useState('')

  // History state
  const [expandedHistoryKey, setExpandedHistoryKey] = useState<string | null>(null)

  // Add contact
  const [addOpen, setAddOpen] = useState(false)
  const [addName, setAddName] = useState('')
  const [addEmail, setAddEmail] = useState('')
  const [addNotes, setAddNotes] = useState('')
  const [addTags, setAddTags] = useState('')
  const [addBusy, setAddBusy] = useState(false)
  const [addError, setAddError] = useState('')

  // CSV import
  const [importBusy, setImportBusy] = useState(false)
  const [importMsg, setImportMsg] = useState('')
  const [importPreview, setImportPreview] = useState<{ rows: Record<string, string>[]; filename: string } | null>(null)
  const importFileRef = useRef<HTMLInputElement>(null)

  // Flash
  const [flashMsg, setFlashMsg] = useState('')

  const reload = useCallback(async () => {
    try {
      const [subRes, histRes, clRes] = await Promise.all([
        fetch('/api/admin-contact-submissions', { credentials: 'include' }),
        fetch('/api/admin-contact-reply-history', { credentials: 'include' }),
        fetch('/api/admin-podcast-checklist', { credentials: 'include' }),
      ])
      if (subRes.ok) {
        const d = await subRes.json() as { submissions: ContactSubmission[] }
        setSubmissions(d.submissions ?? [])
      }
      if (histRes.ok) {
        const d = await histRes.json() as { items: ReplyHistoryItem[] }
        setReplyHistory(d.items ?? [])
      }
      if (clRes.ok) {
        const d = await clRes.json() as { checklist?: { episodes?: ChecklistEpisode[] } }
        setChecklistEpisodes(d.checklist?.episodes ?? [])
      }
    } catch { /* silent */ }
    setLoading(false)
  }, [])

  useEffect(() => { reload() }, [reload])

  // ── Derived contacts ──

  const contacts: Contact[] = useMemo(() => {
    // Group by email (lowercased), falling back to id
    const grouped = new Map<string, ContactSubmission[]>()
    for (const s of submissions) {
      const key = s.email?.trim().toLowerCase() || s.id
      const arr = grouped.get(key) ?? []
      arr.push(s)
      grouped.set(key, arr)
    }

    // Build last-contacted index from reply history
    const lastContactedMap = new Map<string, string>()
    for (const item of replyHistory) {
      const email = item.toEmail?.trim().toLowerCase()
      if (!email) continue
      const existing = lastContactedMap.get(email)
      if (!existing || item.sentAt > existing) lastContactedMap.set(email, item.sentAt)
    }

    return Array.from(grouped.values())
      .map(entries => {
        const sorted = [...entries].sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime())
        const latest = sorted[0]
        const oldest = sorted[sorted.length - 1]
        const emailKey = latest.email?.trim().toLowerCase() || latest.id
        // Tags: prefer entry that has tags, falling back to mainId submission
        const withTags = sorted.find(s => s.tags && s.tags.length > 0)
        // Best delivery status: clicked > opened > delivered > sent
        const STATUS_RANK: Record<string, number> = { clicked: 4, opened: 3, delivered: 2, sent: 1 }
        let bestDeliveryStatus: string | null = null
        let bestRank = -1
        for (const s of sorted) {
          const st = s.emailStatus?.adminReply?.status
          if (st && STATUS_RANK[st] !== undefined && STATUS_RANK[st] > bestRank) {
            bestRank = STATUS_RANK[st]
            bestDeliveryStatus = st
          }
        }

        const repliesForContact = replyHistory.filter(r => r.toEmail?.trim().toLowerCase() === emailKey)
        const unreplied = sorted.some(s => !s.archived) && repliesForContact.length === 0

        const engagementScore =
          repliesForContact.length * 3 +
          sorted.filter(s => s.emailStatus?.adminReply?.status === 'clicked').length * 2 +
          sorted.filter(s => s.emailStatus?.adminReply?.status === 'opened').length * 1 +
          (sorted.some(s => s.source === 'download') ? 2 : 0)

        return {
          key: emailKey,
          email: latest.email ?? '',
          name: latest.name ?? '',
          source: latest.source,
          subscribe: sorted.some(s => s.subscribe),
          archived: sorted.every(s => s.archived === true),
          firstContactAt: oldest.submittedAt,
          latestAt: latest.submittedAt,
          message: latest.message || sorted.find(s => s.message)?.message || '',
          notes: sorted.find(s => s.notes)?.notes ?? '',
          tags: withTags?.tags ?? latest.tags ?? [],
          lastContactedAt: lastContactedMap.get(emailKey) ?? null,
          submissionCount: sorted.length,
          mainId: latest.id,
          allIds: sorted.map(s => s.id),
          allSubmissions: sorted,
          bestDeliveryStatus,
          unreplied,
          engagementScore,
        }
      })
      .sort((a, b) => new Date(b.latestAt).getTime() - new Date(a.latestAt).getTime())
  }, [submissions, replyHistory])

  // ── All tags (for filter dropdown + autocomplete) ──

  const allTags = useMemo(() => {
    const set = new Set<string>()
    for (const c of contacts) for (const t of c.tags) set.add(t)
    return [...set].sort()
  }, [contacts])

  // ── Filtered list ──

  const filtered = useMemo(() => {
    return contacts.filter(c => {
      if (!showArchived && c.archived) return false
      if (tagFilter && !c.tags.includes(tagFilter)) return false
      if (!search.trim()) return true
      const q = search.toLowerCase()
      return (
        c.name.toLowerCase().includes(q) ||
        c.email.toLowerCase().includes(q) ||
        c.message.toLowerCase().includes(q) ||
        c.notes.toLowerCase().includes(q) ||
        c.tags.some(t => t.toLowerCase().includes(q))
      )
    })
  }, [contacts, showArchived, tagFilter, search])

  // ── Edit helpers ──

  function startEdit(c: Contact) {
    setEditingKey(c.key)
    setEditName(c.name)
    setEditNotes(c.notes)
    setEditTags([...c.tags])
    setEditTagInput('')
    setMergePickerKey(null)
    setMergeMsg('')
  }

  function cancelEdit() {
    setEditingKey(null)
    setMergePickerKey(null)
    setMergeMsg('')
  }

  async function saveEdit(c: Contact) {
    setEditSaving(true)
    try {
      await fetch(`/api/admin-contact-submissions/${encodeURIComponent(c.mainId)}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ name: editName, notes: editNotes, tags: editTags }),
      })
      setSubmissions(prev => prev.map(s =>
        s.id === c.mainId
          ? { ...s, name: editName, notes: editNotes, tags: editTags }
          : (s.email?.trim().toLowerCase() === c.key ? { ...s, name: editName } : s)
      ))
      setEditingKey(null)
    } catch { /* silent */ }
    setEditSaving(false)
  }

  function addEditTag(tag: string) {
    const t = tag.trim().slice(0, 50)
    if (!t || editTags.includes(t)) return
    setEditTags(prev => [...prev, t])
    setEditTagInput('')
  }

  function removeEditTag(tag: string) {
    setEditTags(prev => prev.filter(t => t !== tag))
  }

  // ── Delete ──

  async function deleteContact(c: Contact) {
    const label = c.name || c.email || 'this contact'
    const plural = c.submissionCount > 1 ? `all ${c.submissionCount} submissions` : 'submission'
    if (!confirm(`Delete ${plural} from ${label}?`)) return
    await Promise.all(
      c.allIds.map(id =>
        fetch(`/api/admin-contact-submissions/${encodeURIComponent(id)}`, { method: 'DELETE', credentials: 'include' })
      )
    )
    await reload()
  }

  // ── Merge ──

  async function triggerDrip(c: Contact) {
    setDripBusyKey(c.key); setDripMsgKey(c.key); setDripMsgText('')
    try {
      const res = await fetch('/api/admin-contacts/trigger-drip', {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: c.email }),
      })
      const d = await res.json() as { ok?: boolean; note?: string; message?: string }
      setDripMsgText(res.ok ? (d.note ?? 'Drip triggered.') : (d.message ?? 'Failed.'))
    } catch { setDripMsgText('Network error.') }
    setDripBusyKey(null)
  }

  async function doMerge(keepContact: Contact, mergeContact: Contact) {
    if (!confirm(`Merge "${mergeContact.name || mergeContact.email}" into "${keepContact.name || keepContact.email}"? All messages from ${mergeContact.email} will be reassigned to ${keepContact.email}.`)) return
    setMergeBusy(true)
    try {
      const res = await fetch('/api/admin-contacts/merge', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ keepEmail: keepContact.email, mergeEmail: mergeContact.email }),
      })
      if (res.ok) {
        setMergeMsg('')
        setMergePickerKey(null)
        setEditingKey(null)
        flash(`Merged ${mergeContact.email} into ${keepContact.email}.`)
        await reload()
      } else {
        const d = await res.json() as { message?: string }
        setMergeMsg(d.message ?? 'Merge failed.')
      }
    } catch { setMergeMsg('Network error.') }
    setMergeBusy(false)
  }

  // ── Add contact ──

  async function handleAdd(e: React.FormEvent) {
    e.preventDefault(); setAddBusy(true); setAddError('')
    try {
      const tags = addTags.split(',').map(t => t.trim()).filter(Boolean)
      const res = await fetch('/api/admin-contact-submissions/add', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ name: addName, email: addEmail, notes: addNotes, tags }),
      })
      const d = await res.json() as { ok?: boolean; message?: string }
      if (!res.ok) { setAddError(d.message ?? 'Failed to add.'); setAddBusy(false); return }
      setAddOpen(false); setAddName(''); setAddEmail(''); setAddNotes(''); setAddTags('')
      await reload()
    } catch { setAddError('Network error.') }
    setAddBusy(false)
  }

  // ── CSV import ──

  function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0]
    if (!file) return
    const reader = new FileReader()
    reader.onload = evt => {
      const text = evt.target?.result as string
      const rows = parseCSV(text)
      setImportPreview({ rows, filename: file.name })
    }
    reader.readAsText(file)
    if (importFileRef.current) importFileRef.current.value = ''
  }

  async function doImport() {
    if (!importPreview) return
    setImportBusy(true); setImportMsg('')
    try {
      const res = await fetch('/api/admin-contacts/import', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ rows: importPreview.rows }),
      })
      const d = await res.json() as { ok?: boolean; created?: number; skipped?: number; message?: string }
      if (!res.ok) { setImportMsg(d.message ?? 'Import failed.'); setImportBusy(false); return }
      setImportMsg(`Imported ${d.created} contact${d.created !== 1 ? 's' : ''}${d.skipped ? `, skipped ${d.skipped}` : ''}.`)
      setImportPreview(null)
      await reload()
    } catch { setImportMsg('Network error.') }
    setImportBusy(false)
  }

  // ── Flash ──

  function flash(msg: string) {
    setFlashMsg(msg)
    setTimeout(() => setFlashMsg(''), 3000)
  }

  // ── Conversation history builder ──

  function buildHistory(c: Contact): ConversationItem[] {
    const inbound: ConversationItem[] = c.allSubmissions.map(s => ({
      kind: 'inbound',
      date: s.submittedAt,
      name: s.name,
      message: s.source === 'inbound-email'
        ? s.message.replace(/^Subject:\s*.+\n+/m, '').trim().slice(0, 400)
        : s.message.slice(0, 400),
      source: s.source,
      id: s.id,
    }))
    const outbound: ConversationItem[] = replyHistory
      .filter(r => r.toEmail?.trim().toLowerCase() === c.key)
      .map(r => ({
        kind: 'outbound',
        date: r.scheduledAt ?? r.sentAt,
        subject: r.subject,
        preview: r.preview,
        toEmail: r.toEmail,
      }))
    return [...inbound, ...outbound].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
  }

  // ── Source badge ──

  function sourceBadge(source: string | undefined) {
    if (source === 'manual') return <span className="ct-badge ct-badge--manual">manual</span>
    if (source === 'inbound-email') return <span className="ct-badge ct-badge--email">email</span>
    if (source === 'download') return <span className="ct-badge ct-badge--download">download</span>
    return <span className="ct-badge ct-badge--form">form</span>
  }

  // ── Render ──

  return (
    <div className="ct-app">
      {/* Header */}
      <header className="ct-header">
        <div className="ct-header-brand">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
            <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" />
            <path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" />
          </svg>
          Contacts
          <span className="ct-header-count">{contacts.length}</span>
        </div>
        <div className="ct-header-actions">
          <button type="button" className="em-btn em-btn--secondary em-btn--sm" onClick={() => { setAddOpen(o => !o); setAddError('') }}>
            {addOpen ? 'Cancel' : '+ Add Contact'}
          </button>
          <button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => exportContactsCSV(filtered)} title="Export visible contacts to CSV">
            ↓ Export CSV
          </button>
          <label className="em-btn em-btn--ghost em-btn--sm" style={{ cursor: 'pointer' }}>
            ↑ Import CSV
            <input ref={importFileRef} type="file" accept=".csv,text/csv" style={{ display: 'none' }} onChange={handleFileChange} />
          </label>
          <Link to="/email" className="em-btn em-btn--ghost em-btn--sm">✉ Email</Link>
          <Link to="/calendar" className="em-btn em-btn--ghost em-btn--sm">Calendar</Link>
          <Link to="/admin" className="em-btn em-btn--ghost em-btn--sm">← Admin</Link>
        </div>
      </header>

      {/* Import preview */}
      {importPreview && (
        <div className="ct-import-banner">
          <div className="ct-import-info">
            <strong>{importPreview.filename}</strong> — {importPreview.rows.length} row{importPreview.rows.length !== 1 ? 's' : ''} found
            {importPreview.rows.length > 0 && (
              <span className="ct-import-cols"> · columns: {Object.keys(importPreview.rows[0]).join(', ')}</span>
            )}
          </div>
          <div className="ct-import-actions">
            <button type="button" className="em-btn em-btn--primary em-btn--sm" onClick={doImport} disabled={importBusy || importPreview.rows.length === 0}>
              {importBusy ? 'Importing…' : `Import ${importPreview.rows.length} contacts`}
            </button>
            <button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => { setImportPreview(null); setImportMsg('') }}>Cancel</button>
          </div>
          {importMsg && <p className="ct-import-msg">{importMsg}</p>}
        </div>
      )}
      {importMsg && !importPreview && <div className="ct-flash">{importMsg}</div>}
      {flashMsg && <div className="ct-flash">{flashMsg}</div>}

      {/* Add contact form */}
      {addOpen && (
        <div className="ct-add-banner">
          <form className="ct-add-form" onSubmit={handleAdd}>
            <h3 className="ct-add-title">Add Contact</h3>
            <div className="ct-add-row">
              <label className="ct-add-label">Name<input className="ct-input" type="text" placeholder="Full name" value={addName} onChange={e => setAddName(e.target.value)} autoFocus /></label>
              <label className="ct-add-label">Email<input className="ct-input" type="email" placeholder="email@example.com" value={addEmail} onChange={e => setAddEmail(e.target.value)} /></label>
              <label className="ct-add-label">Tags (comma-separated)<input className="ct-input" type="text" placeholder="listener, partner" value={addTags} onChange={e => setAddTags(e.target.value)} /></label>
            </div>
            <label className="ct-add-label ct-add-label--full">Notes<textarea className="ct-input ct-notes-input" placeholder="Notes (optional)" value={addNotes} onChange={e => setAddNotes(e.target.value)} rows={2} /></label>
            {addError && <p className="ct-error">{addError}</p>}
            <div className="ct-add-actions">
              <button type="submit" className="em-btn em-btn--primary em-btn--sm" disabled={addBusy}>{addBusy ? 'Adding…' : 'Add Contact'}</button>
            </div>
          </form>
        </div>
      )}

      {/* Toolbar */}
      <div className="ct-toolbar">
        <input type="search" className="ct-search" placeholder="Search name, email, notes, tags…" value={search} onChange={e => setSearch(e.target.value)} />
        <select className="ct-tag-filter" value={tagFilter} onChange={e => setTagFilter(e.target.value)}>
          <option value="">All tags</option>
          {allTags.map(t => <option key={t} value={t}>{t}</option>)}
        </select>
        <label className="ct-archived-toggle">
          <input type="checkbox" checked={showArchived} onChange={e => setShowArchived(e.target.checked)} />
          Show archived
        </label>
        <span className="ct-count-label">{filtered.length} contact{filtered.length !== 1 ? 's' : ''}</span>
      </div>

      {/* Contact list */}
      <div className="ct-list">
        {loading && <p className="ct-empty">Loading contacts…</p>}
        {!loading && filtered.length === 0 && (
          <p className="ct-empty">{search || tagFilter ? 'No contacts match.' : 'No contacts yet.'}</p>
        )}

        {filtered.map(c => {
          const isEditing = editingKey === c.key
          const historyOpen = expandedHistoryKey === c.key
          const history = historyOpen ? buildHistory(c) : []
          const isMergeTarget = mergePickerKey === c.key

          return (
            <div key={c.mainId} className={`ct-card${c.archived ? ' ct-card--archived' : ''}`}>
              <div className="ct-avatar" aria-hidden="true">
                {(c.name || c.email || '?').charAt(0).toUpperCase()}
              </div>

              <div className="ct-card-body">
                {isEditing ? (
                  /* ── Edit mode ── */
                  <div className="ct-edit-form">
                    <div className="ct-edit-row">
                      <label className="ct-edit-label">Name<input className="ct-input" type="text" value={editName} onChange={e => setEditName(e.target.value)} autoFocus /></label>
                    </div>
                    <label className="ct-edit-label">
                      Tags
                      <div className="ct-tag-editor">
                        {editTags.map(t => (
                          <span key={t} className="ct-tag" style={{ background: tagBg(t) }}>
                            {t}
                            <button type="button" className="ct-tag-remove" onClick={() => removeEditTag(t)} aria-label={`Remove ${t}`}>×</button>
                          </span>
                        ))}
                        <input
                          className="ct-tag-input"
                          type="text"
                          placeholder="Add tag…"
                          value={editTagInput}
                          list="ct-tag-suggestions"
                          onChange={e => setEditTagInput(e.target.value)}
                          onKeyDown={e => {
                            if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); addEditTag(editTagInput) }
                            else if (e.key === 'Backspace' && !editTagInput && editTags.length) removeEditTag(editTags[editTags.length - 1])
                          }}
                        />
                        <datalist id="ct-tag-suggestions">
                          {allTags.filter(t => !editTags.includes(t)).map(t => <option key={t} value={t} />)}
                        </datalist>
                      </div>
                    </label>
                    <label className="ct-edit-label">Notes<textarea className="ct-input ct-notes-input" rows={3} value={editNotes} onChange={e => setEditNotes(e.target.value)} placeholder="Add a note…" /></label>

                    {/* Merge picker */}
                    {isMergeTarget && (
                      <div className="ct-merge-picker">
                        <p className="ct-merge-label">Merge another contact into <strong>{c.name || c.email}</strong>:</p>
                        <input className="ct-input ct-merge-search" type="search" placeholder="Search contacts to merge…" value={mergeSearch} autoFocus onChange={e => setMergeSearch(e.target.value)} />
                        <div className="ct-merge-list">
                          {contacts
                            .filter(other =>
                              other.key !== c.key &&
                              (!mergeSearch || other.name.toLowerCase().includes(mergeSearch.toLowerCase()) || other.email.toLowerCase().includes(mergeSearch.toLowerCase()))
                            )
                            .slice(0, 20)
                            .map(other => (
                              <button key={other.key} type="button" className="ct-merge-option" onClick={() => doMerge(c, other)} disabled={mergeBusy}>
                                <span className="ct-merge-name">{other.name || <em>No name</em>}</span>
                                <span className="ct-merge-email">{other.email}</span>
                                <span className="ct-merge-count">{other.submissionCount} msg{other.submissionCount !== 1 ? 's' : ''}</span>
                              </button>
                            ))
                          }
                        </div>
                        {mergeMsg && <p className="ct-error">{mergeMsg}</p>}
                      </div>
                    )}

                    <div className="ct-edit-actions">
                      <button type="button" className="em-btn em-btn--primary em-btn--sm" onClick={() => saveEdit(c)} disabled={editSaving}>{editSaving ? 'Saving…' : 'Save'}</button>
                      <button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => { setMergePickerKey(prev => prev === c.key ? null : c.key); setMergeSearch(''); setMergeMsg('') }}>
                        {isMergeTarget ? 'Cancel merge' : 'Merge with…'}
                      </button>
                      {c.email && (
                        <button type="button" className="em-btn em-btn--ghost em-btn--sm" title="Sync to Resend audience and trigger drip automation" onClick={() => triggerDrip(c)} disabled={dripBusyKey === c.key}>
                          {dripBusyKey === c.key ? 'Triggering…' : '▶ Drip'}
                        </button>
                      )}
                      <button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={cancelEdit}>Cancel</button>
                    </div>
                    {dripMsgKey === c.key && dripMsgText && (
                      <p className="ct-drip-msg">{dripMsgText}</p>
                    )}
                  </div>
                ) : (
                  /* ── Display mode ── */
                  <>
                    <div className="ct-card-top">
                      <span className="ct-card-name">{c.name || <em>No name</em>}</span>
                      {c.email && <a className="ct-card-email" href={`mailto:${c.email}`}>{c.email}</a>}
                      {sourceBadge(c.source)}
                      {c.subscribe && <span className="ct-badge ct-badge--sub">subscriber</span>}
                      {c.unreplied && <span className="ct-badge ct-badge--unreplied" title="No reply sent yet">needs reply</span>}
                      {c.bestDeliveryStatus && (
                        <span className={`ct-delivery-pill ct-delivery-pill--${c.bestDeliveryStatus}`}>
                          {c.bestDeliveryStatus === 'clicked' ? '🔗 clicked' : c.bestDeliveryStatus === 'opened' ? '👁 opened' : c.bestDeliveryStatus === 'delivered' ? '✓ delivered' : '→ sent'}
                        </span>
                      )}
                      {c.engagementScore >= 3 && <span className="ct-engage-badge" title={`Engagement score: ${c.engagementScore}`}>{'★'.repeat(Math.min(3, Math.floor(c.engagementScore / 3)))}</span>}
                      {c.submissionCount > 1 && <span className="ct-count-badge">{c.submissionCount}</span>}
                    </div>

                    {/* Tags row */}
                    {c.tags.length > 0 && (
                      <div className="ct-tags-row">
                        {c.tags.map(t => (
                          <span key={t} className="ct-tag" style={{ background: tagBg(t) }}>{t}</span>
                        ))}
                      </div>
                    )}

                    <div className="ct-card-meta-row">
                      <span className="ct-card-date">
                        First: {fmtDate(c.firstContactAt)}
                        {c.lastContactedAt && <> · <span className="ct-last-contacted">Last replied: {fmtShort(c.lastContactedAt)}</span></>}
                      </span>
                    </div>

                    {c.notes && <p className="ct-card-notes">{c.notes}</p>}

                    {c.message && (
                      <p className="ct-card-preview">
                        {c.message.length > 100 ? c.message.slice(0, 100) + '…' : c.message}
                      </p>
                    )}
                  </>
                )}
              </div>

              {/* Actions */}
              {!isEditing && (
                <div className="ct-card-actions">
                  <button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => startEdit(c)}>Edit</button>
                  <button
                    type="button"
                    className={`em-btn em-btn--sm ${historyOpen ? 'em-btn--secondary' : 'em-btn--ghost'}`}
                    onClick={() => setExpandedHistoryKey(prev => prev === c.key ? null : c.key)}
                  >
                    History{c.submissionCount > 1 || replyHistory.some(r => r.toEmail?.trim().toLowerCase() === c.key) ? ` (${c.submissionCount})` : ''}
                  </button>
                  <button type="button" className="em-btn em-btn--danger em-btn--sm" onClick={() => deleteContact(c)}>Delete</button>
                </div>
              )}

              {/* Conversation history panel */}
              {historyOpen && !isEditing && (() => {
                const relatedEps = checklistEpisodes.filter(ep => {
                  if (!ep.datePublished) return false
                  const epMs = new Date(ep.datePublished + 'T12:00:00').getTime()
                  const refMs = new Date(c.latestAt).getTime()
                  return Math.abs(epMs - refMs) <= 30 * 24 * 60 * 60 * 1000
                })
                return (
                  <div className="ct-history-panel">
                    {history.length === 0 && relatedEps.length === 0 && <p className="ct-history-empty">No conversation history.</p>}
                    {history.map((item, i) => (
                      item.kind === 'inbound' ? (
                        <div key={item.id || i} className="ct-history-item ct-history-item--in">
                          <div className="ct-history-meta">
                            <span className="ct-history-who">{item.name}</span>
                            <span className="ct-history-date">{fmtShort(item.date)}</span>
                          </div>
                          <p className="ct-history-body">{item.message || '(no message body)'}</p>
                        </div>
                      ) : (
                        <div key={i} className="ct-history-item ct-history-item--out">
                          <div className="ct-history-meta">
                            <span className="ct-history-who">You → {item.toEmail}</span>
                            <span className="ct-history-date">{fmtShort(item.date)}</span>
                          </div>
                          <div className="ct-history-subject">{item.subject}</div>
                          <p className="ct-history-body">{item.preview}</p>
                        </div>
                      )
                    ))}
                    {relatedEps.length > 0 && (
                      <div className="ct-history-ep-section">
                        <p className="ct-history-ep-label">📅 Episodes near this contact</p>
                        {relatedEps.map(ep => {
                          const parts = [ep.series, ep.episodeNumber != null ? `Ep. ${ep.episodeNumber}` : null, ep.title].filter(Boolean)
                          return (
                            <div key={ep.id} className="ct-history-item ct-history-item--ep">
                              <div className="ct-history-meta">
                                <span className="ct-history-who">{parts.join(' – ') || 'Untitled episode'}</span>
                                <span className="ct-history-date">{ep.datePublished}</span>
                              </div>
                            </div>
                          )
                        })}
                      </div>
                    )}
                  </div>
                )
              })()}
            </div>
          )
        })}
      </div>
    </div>
  )
}
