/**
 * FEEDBACK-TICKETS: "Meine Tickets" — verwaltet die eingereichten "Fehler melden"-Berichte.
 *
 * Erreichbar über die Hilfe-Seite (/hilfe → „Meine Tickets") unter /hilfe/tickets.
 * Listet alle Tickets (neueste zuerst, GET /feedback) mit Status-Badge (Offen/Gelöst).
 * Pro Ticket: als gelöst markieren / wieder öffnen, Beschreibung bearbeiten (inline),
 * löschen (mit Bestätigung). Erfolg/Fehler über das bestehende Toast-System.
 *
 * Drei gefilterte Verlinkungen (Filter-Chips, kein Pflicht-Tab): „Rückfragen" (NeedsInfo —
 * wartet auf die Züchterin), „Offen" (Open + Answered — von der KI zu erledigen, beantwortete
 * Rückfragen kommen hierher zurück), „Geschlossen" (Resolved, zeigt den Changelog/fixNote).
 * Der Frage/Antwort-Verlauf (thread) wird angezeigt; das interne `agentContext` NIEMALS.
 */
import { createContext, Fragment, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { de } from '../strings/de'
import { ApiError } from '../api/client'
import {
  listFeedback,
  updateFeedback,
  deleteFeedback,
  restoreFeedback,
  uploadAttachment,
  deleteAttachment,
  attachmentUrl,
  readFileAsUpload,
  type FeedbackTicket,
} from '../api/feedback'
import { getGerbil } from '../api/gerbils'
import { REF_ROUTE, REF_TOKEN_RE, resolveRefs, type RefType } from '../api/refs'
import { useApi } from '../hooks/useApi'
import { useToast } from '../components/toast'
import PushToggle from '../components/PushToggle'
import './tickets.css'

/** Aufgelöster Kurz-Verweis (Shortlink): null = unbekannt/mehrdeutig. */
type ResolvedRef = { id: string; name: string } | null

/**
 * Nachschlage-Kontext für Ticket-Text-Links, damit RichText nicht durch jede Ebene
 * durchgereicht werden muss:
 *  - ticketIds: bekannte Ticket-IDs (nackte GUID → Link auf das Ticket).
 *  - gerbilNames: aufgelöste Tier-Namen je GerbilId (nackte GUID → Name + Link zur Akte).
 *  - refNames: aufgelöste Kurz-Verweise je `typ:code` (die KI kann `tier:3c5b747b` schreiben
 *    → Name + Link). Wert `undefined` = noch nicht geladen, `null` = unbekannt/mehrdeutig.
 */
const TicketRefsContext = createContext<{
  ticketIds: Set<string>
  gerbilNames: Record<string, string | null>
  refNames: Record<string, ResolvedRef>
}>({ ticketIds: new Set(), gerbilNames: {}, refNames: {} })

/** Cache-Schlüssel für einen Kurz-Verweis. */
function refKey(type: string, code: string): string {
  return `${type.toLowerCase()}:${code.replace(/-/g, '').toLowerCase()}`
}

const t = de.feedback.tickets
const ctx = de.feedback.contexts
const nav = de.nav

/** Kontext-Label (Ansicht); fällt für unbekannte Werte auf den Rohwert zurück. */
function contextLabel(context: string): string {
  return (ctx as Record<string, string>)[context] ?? context
}

/** Interne Route zum referenzierten Datensatz (Tier/Wurf/Kontakt) eines Tickets, falls vorhanden. */
function entityHref(ticket: FeedbackTicket): string | null {
  if (ticket.gerbilId) return `/rennmaeuse/${ticket.gerbilId}`
  if (ticket.litterId) return `/wuerfe/${ticket.litterId}`
  if (ticket.contactId) return `/kontakte/${ticket.contactId}`
  return null
}

const GUID_RE = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'

type RichBlock = { type: 'p'; text: string } | { type: 'ul'; items: string[] }

/** Block-Ebene: aufeinanderfolgende Aufzählungszeilen (-, *, •) zu einer Liste gruppieren. */
function parseBlocks(text: string): RichBlock[] {
  const blocks: RichBlock[] = []
  let para: string | null = null
  let list: string[] | null = null
  const flushPara = () => {
    if (para !== null) {
      blocks.push({ type: 'p', text: para })
      para = null
    }
  }
  const flushList = () => {
    if (list) {
      blocks.push({ type: 'ul', items: list })
      list = null
    }
  }
  for (const line of text.split('\n')) {
    const bullet = line.match(/^\s*[-*•]\s+(.*)$/)
    if (bullet) {
      flushPara()
      ;(list ??= []).push(bullet[1])
    } else {
      flushList()
      para = para === null ? line : `${para}\n${line}`
    }
  }
  flushPara()
  flushList()
  return blocks
}

/** Alle nackten GUIDs in einem Text, die KEINE Markdown-Link-Ziele sind (mögliche Tier-IDs). */
function bareGuids(text: string): string[] {
  const stripped = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
  return stripped.match(new RegExp(GUID_RE, 'g')) ?? []
}

/** Plain-Text mit erhaltenen Zeilenumbrüchen. */
function TextWithBreaks({ value }: { value: string }) {
  const lines = value.split('\n')
  return (
    <>
      {lines.map((line, i) => (
        <Fragment key={i}>
          {i > 0 && <br />}
          {line}
        </Fragment>
      ))}
    </>
  )
}

/** Inline-Markdown: **fett**, `code`, [Label](url)-Links, Kurz-Verweise (tier:…), IDs, Umbrüche. */
function InlineMarkdown({ text }: { text: string }) {
  const { ticketIds, gerbilNames, refNames } = useContext(TicketRefsContext)
  // Gruppen: 1/2 = Markdown-Link, 3 = fett, 4 = code, 5/6 = Kurz-Verweis (typ/code), 7 = nackte GUID.
  const re = new RegExp(
    `\\[([^\\]]+)\\]\\(([^)]+)\\)|\\*\\*([^*]+)\\*\\*|\`([^\`]+)\`|` +
      `\\b(tier|kontakt|wurf|gehege):([0-9a-fA-F]{6,}(?:-[0-9a-fA-F-]+)?)|(${GUID_RE})`,
    'g',
  )
  const nodes: ReactNode[] = []
  let last = 0
  let m: RegExpExecArray | null
  let k = 0
  while ((m = re.exec(text)) !== null) {
    if (m.index > last) nodes.push(<TextWithBreaks key={k++} value={text.slice(last, m.index)} />)
    if (m[1] !== undefined && m[2] !== undefined) {
      const href = m[2]
      nodes.push(
        href.startsWith('/') ? (
          <Link key={k++} to={href} className="ticket-link">
            {m[1]}
          </Link>
        ) : (
          <a key={k++} href={href} className="ticket-link" target="_blank" rel="noreferrer">
            {m[1]}
          </a>
        ),
      )
    } else if (m[3] !== undefined) {
      nodes.push(<strong key={k++}>{m[3]}</strong>)
    } else if (m[4] !== undefined) {
      nodes.push(
        <code key={k++} className="ticket-code">
          {m[4]}
        </code>,
      )
    } else if (m[5] !== undefined && m[6] !== undefined) {
      // Kurz-Verweis tier:/kontakt:/wurf:/gehege: → aufgelöster Name + Link, sonst Roh-Text.
      const type = m[5] as RefType
      const resolved = refNames[refKey(type, m[6])]
      if (resolved) {
        nodes.push(
          <Link key={k++} to={`${REF_ROUTE[type]}/${resolved.id}`} className="ticket-link">
            {resolved.name}
          </Link>,
        )
      } else {
        nodes.push(<TextWithBreaks key={k++} value={m[0]} />)
      }
    } else {
      const guid = m[7]
      const gn = gerbilNames[guid]
      if (ticketIds.has(guid)) {
        nodes.push(
          <Link key={k++} to={`/hilfe/tickets?focus=${guid}`} className="ticket-link">
            {t.ticketRefLabel}
          </Link>,
        )
      } else if (gn) {
        nodes.push(
          <Link key={k++} to={`/rennmaeuse/${guid}`} className="ticket-link">
            {gn}
          </Link>,
        )
      } else {
        nodes.push(<TextWithBreaks key={k++} value={guid} />)
      }
    }
    last = re.lastIndex
  }
  if (last < text.length) nodes.push(<TextWithBreaks key={k} value={text.slice(last)} />)
  return <>{nodes}</>
}

/** Rendert Ticket-Text als Markdown-Teilmenge (Absätze, Aufzählungen, Inline-Formatierung). */
function RichText({ text }: { text: string }) {
  const blocks = useMemo(() => parseBlocks(text), [text])
  return (
    <>
      {blocks.map((b, i) =>
        b.type === 'ul' ? (
          <ul key={i} className="ticket-md-list">
            {b.items.map((it, j) => (
              <li key={j}>
                <InlineMarkdown text={it} />
              </li>
            ))}
          </ul>
        ) : (
          <InlineMarkdown key={i} text={b.text} />
        ),
      )}
    </>
  )
}

function formatDate(iso: string | null): string {
  if (!iso) return '—'
  const d = new Date(iso)
  if (Number.isNaN(d.getTime())) return '—'
  return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
}

/** Gefilterte Ansichten (Filter-Chips). */
type TicketView = 'dialog' | 'open' | 'closed' | 'deleted'

/** Auto-Fallback-Reihenfolge, wenn die gewünschte Ansicht leer ist — „Gelöscht" NICHT (Papierkorb). */
const FALLBACK_VIEWS: TicketView[] = ['open', 'dialog', 'closed']

/**
 * Zuordnung Ticket → gefilterte Ansicht.
 *  - „Gelöscht" (deleted): soft-gelöscht (deletedAt gesetzt) — hat Vorrang vor dem Status.
 *  - „Rückfragen" (dialog): NUR NeedsInfo — wartet auf die Antwort der Züchterin.
 *  - „Offen" (open): Open + Answered — von der KI zu erledigen. Sobald die Züchterin
 *    geantwortet hat (Answered), ist SIE fertig → das Ticket wandert zurück nach „Offen"
 *    (mit „Beantwortet"-Badge), statt in ihrer Rückfragen-Liste zu verbleiben.
 *  - „Geschlossen" (closed): Resolved.
 */
function viewOf(ticket: Pick<FeedbackTicket, 'status' | 'deletedAt'>): TicketView {
  if (ticket.deletedAt) return 'deleted'
  if (ticket.status === 'Resolved') return 'closed'
  if (ticket.status === 'NeedsInfo') return 'dialog'
  return 'open'
}

export default function TicketsPage() {
  const toast = useToast()
  const tickets = useApi(() => listFeedback(), [])
  // Default-Ansicht: „Offen" (neue, noch unbearbeitete Tickets) — fällt auf die erste
  // nicht-leere Gruppe zurück, falls „Offen" leer ist. Solange die Nutzerin keinen Filter
  // selbst gewählt hat (userPicked), darf ein ?focus=-Sprung die Ansicht bestimmen.
  // Der zuletzt gewählte Tab wird in der Session gemerkt, damit man nach „Zurück" (Remount)
  // NICHT wieder im falschen Tab („Offen") landet. userPicked startet bewusst false, damit ein
  // frischer ?focus=-Deep-Link weiterhin in den passenden Tab springen darf.
  const [view, setView] = useState<TicketView>(() => {
    try {
      const v = sessionStorage.getItem('tickets-view')
      if (v === 'dialog' || v === 'open' || v === 'closed' || v === 'deleted') return v
    } catch {
      /* ignorieren */
    }
    return 'open'
  })
  const [userPicked, setUserPicked] = useState(false)

  async function handleToggleStatus(ticket: FeedbackTicket) {
    try {
      if (ticket.status === 'Resolved') {
        // Wiederöffnen → zurück in die „Rückfragen" (NeedsInfo). Hatte das Ticket bereits eine
        // offene Rückfrage (z. B. eine manuell gelöste Rückfrage), wird genau diese Frage wieder
        // geöffnet (kein neuer Hinweis, kein Zeitstempel). War es dagegen ein echt gelöstes Ticket
        // ohne Rückfrage, wird um mehr Infos gebeten — und das Backend stempelt „Wieder geöffnet am".
        const hadRueckfrage = !!ticket.question && ticket.question.trim().length > 0
        await updateFeedback(ticket.id, {
          status: 'NeedsInfo',
          question: hadRueckfrage ? ticket.question! : t.reopenRequest,
        })
        tickets.reload()
        toast.success(t.reopenedToast)
      } else {
        await updateFeedback(ticket.id, { status: 'Resolved' })
        tickets.reload()
        toast.success(t.resolvedToast)
      }
    } catch (err) {
      toast.error(err instanceof ApiError ? err.message : t.updateError)
    }
  }

  async function handleSaveMessage(ticket: FeedbackTicket, message: string) {
    await updateFeedback(ticket.id, { message })
    tickets.reload()
    toast.success(t.saved)
  }

  async function handleAnswer(ticket: FeedbackTicket, answer: string) {
    await updateFeedback(ticket.id, { answer })
    tickets.reload()
    toast.success(t.answeredToast)
  }

  async function handleDelete(ticket: FeedbackTicket) {
    if (!window.confirm(t.confirmDelete)) return
    try {
      await deleteFeedback(ticket.id)
      tickets.reload()
      toast.success(t.deletedToast)
    } catch (err) {
      toast.error(err instanceof ApiError ? err.message : t.deleteError)
    }
  }

  async function handleRestore(ticket: FeedbackTicket) {
    try {
      await restoreFeedback(ticket.id)
      tickets.reload()
      toast.success(t.restoredToast)
    } catch (err) {
      toast.error(err instanceof ApiError ? err.message : t.restoreError)
    }
  }

  // 👍/👎 auf einem gelösten Ticket. 👍 = nur Rückmeldung speichern; 👎 = Ticket wieder öffnen
  // (zurück in die Rückfragen) und um die fehlende Info bitten.
  async function handleHelpful(ticket: FeedbackTicket, helpful: boolean) {
    try {
      if (helpful) {
        await updateFeedback(ticket.id, { helpful: true })
        tickets.reload()
        toast.success(t.helpfulThanks)
      } else {
        const hadRueckfrage = !!ticket.question && ticket.question.trim().length > 0
        await updateFeedback(ticket.id, {
          helpful: false,
          status: 'NeedsInfo',
          question: hadRueckfrage ? ticket.question! : t.helpfulReopenNote,
        })
        tickets.reload()
        toast.success(t.reopenedToast)
      }
    } catch (err) {
      toast.error(err instanceof ApiError ? err.message : t.updateError)
    }
  }

  async function handleAddAttachment(ticket: FeedbackTicket, file: File) {
    try {
      await uploadAttachment(ticket.id, await readFileAsUpload(file))
      tickets.reload()
      toast.success(t.attachmentAdded)
    } catch (err) {
      toast.error(err instanceof ApiError ? err.message : t.attachmentError)
    }
  }

  async function handleDeleteAttachment(attachmentId: string) {
    try {
      await deleteAttachment(attachmentId)
      tickets.reload()
      toast.success(t.attachmentRemoved)
    } catch (err) {
      toast.error(err instanceof ApiError ? err.message : t.attachmentError)
    }
  }

  const rows = tickets.data

  // Verlinkung von Ticket zu Ticket: /hilfe/tickets?focus=<id> wechselt in die passende
  // Ansicht und hebt das Ticket hervor. ticketIds = alle bekannten IDs (für nackte-GUID-Links).
  const [searchParams] = useSearchParams()
  const focusId = searchParams.get('focus')
  const ticketIds = useMemo(() => new Set((rows ?? []).map((r) => r.id)), [rows])
  // Aufgelöste Tier-Namen je GerbilId (für nackte Tier-IDs im Text → Name + Link).
  const [gerbilNames, setGerbilNames] = useState<Record<string, string | null>>({})
  // Aufgelöste Kurz-Verweise (tier:/kontakt:/wurf:/gehege:) je `typ:code`.
  const [refNames, setRefNames] = useState<Record<string, ResolvedRef>>({})

  // Gruppierung + Zählung je gefilterter Ansicht (für die Badges an den Filter-Chips).
  const counts = useMemo(() => {
    const c: Record<TicketView, number> = { dialog: 0, open: 0, closed: 0, deleted: 0 }
    for (const ticket of rows ?? []) c[viewOf(ticket)] += 1
    return c
  }, [rows])

  // Ansicht des per ?focus=<id> angesprungenen Tickets (ohne setState im Effekt abgeleitet).
  const focusView = useMemo<TicketView | null>(() => {
    if (!focusId || !rows) return null
    const target = rows.find((r) => r.id === focusId)
    return target ? viewOf(target) : null
  }, [focusId, rows])

  // Sichtbare Ansicht: Fokus-Sprung hat Vorrang (bis die Nutzerin selbst filtert),
  // sonst der gewählte Filter; falls leer, auf die erste nicht-leere Gruppe ausweichen.
  const desiredView: TicketView = focusView && !userPicked ? focusView : view
  const activeView: TicketView =
    counts[desiredView] > 0
      ? desiredView
      : FALLBACK_VIEWS.find((v) => counts[v] > 0) ?? desiredView

  const visible = (rows ?? []).filter((ticket) => viewOf(ticket) === activeView)

  // Suche (Volltext über Nachricht/Frage/Antwort/Name/Kategorie) + Kategorie-Filter, jeweils
  // INNERHALB der aktiven Ansicht.
  const [query, setQuery] = useState('')
  const [categoryFilter, setCategoryFilter] = useState<string>('')
  // Vorhandene Kategorien (über alle nicht-gelöschten Tickets) für das Auswahlmenü.
  const categories = useMemo(() => {
    const set = new Set<string>()
    for (const tk of rows ?? []) if (!tk.deletedAt && tk.category) set.add(tk.category)
    return [...set].sort((a, b) => a.localeCompare(b, 'de'))
  }, [rows])
  const displayed = useMemo(() => {
    const q = query.trim().toLowerCase()
    return visible.filter((tk) => {
      if (categoryFilter && tk.category !== categoryFilter) return false
      if (!q) return true
      const hay = [tk.message, tk.question, tk.answer, tk.fixNote, tk.entityName, tk.category]
        .filter(Boolean)
        .join(' ')
        .toLowerCase()
      return hay.includes(q)
    })
  }, [visible, query, categoryFilter])

  // Nach dem Rendern der sichtbaren Liste zum fokussierten Ticket scrollen + kurz hervorheben.
  useEffect(() => {
    if (!focusId) return
    const el = document.getElementById(`ticket-${focusId}`)
    if (!el) return
    el.scrollIntoView({ behavior: 'smooth', block: 'center' })
    el.classList.add('ticket-card--focus')
    const timer = setTimeout(() => el.classList.remove('ticket-card--focus'), 2500)
    return () => clearTimeout(timer)
  }, [focusId, displayed])

  // (Scroll-Wiederherstellung läuft global über useScrollRestoration im AppShell — gilt für alle
  //  Listen, inkl. Handy-Sperre/Hintergrund. ?focus= unten hat hier Vorrang.)

  // Nackte Tier-IDs in den sichtbaren Tickets sammeln und ihre Namen einmalig auflösen,
  // damit die KI im Text einfach eine Tier-ID hinterlegen kann (→ Name + Link zur Akte).
  const candidateGerbilIds = useMemo(() => {
    const ids = new Set<string>()
    const add = (txt?: string | null) => {
      if (!txt) return
      for (const g of bareGuids(txt)) if (!ticketIds.has(g)) ids.add(g)
    }
    for (const tk of visible) {
      add(tk.message); add(tk.question); add(tk.answer); add(tk.fixNote)
      for (const e of tk.thread) add(e.text)
    }
    return ids
  }, [visible, ticketIds])

  useEffect(() => {
    const missing = [...candidateGerbilIds].filter((id) => !(id in gerbilNames))
    if (missing.length === 0) return
    let cancelled = false
    Promise.all(
      missing.map((id) =>
        getGerbil(id)
          .then((g) => [id, g.name] as const)
          .catch(() => [id, null] as const),
      ),
    ).then((pairs) => {
      if (cancelled) return
      setGerbilNames((prev) => {
        const next = { ...prev }
        for (const [id, name] of pairs) next[id] = name
        return next
      })
    })
    return () => {
      cancelled = true
    }
  }, [candidateGerbilIds, gerbilNames])

  // Kurz-Verweise (tier:/kontakt:/wurf:/gehege:) in den sichtbaren Tickets sammeln und
  // einmalig über POST /refs/resolve auflösen — so kann die KI im Text die Kurz-ID nutzen.
  const candidateRefs = useMemo(() => {
    const map = new Map<string, { type: RefType; code: string }>()
    const add = (txt?: string | null) => {
      if (!txt) return
      const re = new RegExp(REF_TOKEN_RE.source, REF_TOKEN_RE.flags)
      let m: RegExpExecArray | null
      while ((m = re.exec(txt)) !== null) {
        const type = m[1] as RefType
        map.set(refKey(type, m[2]), { type, code: m[2] })
      }
    }
    for (const tk of visible) {
      add(tk.message); add(tk.question); add(tk.answer); add(tk.fixNote)
      for (const e of tk.thread) add(e.text)
    }
    return map
  }, [visible])

  useEffect(() => {
    const missing = [...candidateRefs].filter(([key]) => !(key in refNames))
    if (missing.length === 0) return
    let cancelled = false
    resolveRefs(missing.map(([, r]) => r))
      .then((results) => {
        if (cancelled) return
        setRefNames((prev) => {
          const next = { ...prev }
          for (const r of results) {
            next[refKey(r.type, r.code)] = r.id && r.name ? { id: r.id, name: r.name } : null
          }
          return next
        })
      })
      .catch(() => {
        /* Auflösung fehlgeschlagen — Tokens bleiben Roh-Text. */
      })
    return () => {
      cancelled = true
    }
  }, [candidateRefs, refNames])

  const emptyText: Record<TicketView, string> = {
    dialog: t.emptyDialog,
    open: t.emptyOpen,
    closed: t.emptyClosed,
    deleted: t.emptyDeleted,
  }

  return (
    <section className="page tickets-page">
      <nav className="tickets-breadcrumb" aria-label="Brotkrümel">
        <Link to="/hilfe">{nav.help}</Link>
        <span aria-hidden="true"> › </span>
        <span>{t.title}</span>
      </nav>
      <h2>{t.title}</h2>
      <p className="tickets-subtitle">{t.subtitle}</p>
      <PushToggle />

      {tickets.error && <p className="tickets-error">{t.loadError}</p>}

      {rows === null ? null : rows.length === 0 && !tickets.error ? (
        <p className="tickets-empty">{t.empty}</p>
      ) : (
        <>
          <div className="tickets-filters" role="group" aria-label={t.title}>
            {(['dialog', 'open', 'closed', 'deleted'] as TicketView[]).map((v) => (
              <button
                key={v}
                type="button"
                className={`tickets-filter${v === activeView ? ' tickets-filter--active' : ''}`}
                aria-pressed={v === activeView}
                onClick={() => {
                  setUserPicked(true)
                  setView(v)
                  try {
                    sessionStorage.setItem('tickets-view', v)
                  } catch {
                    /* ignorieren */
                  }
                }}
              >
                {t.filters[v]}
                <span className="tickets-filter__count">{counts[v]}</span>
              </button>
            ))}
          </div>

          <div className="tickets-search">
            <input
              type="search"
              className="tickets-search__input"
              placeholder={t.searchPlaceholder}
              aria-label={t.searchPlaceholder}
              value={query}
              onChange={(e) => setQuery(e.target.value)}
            />
            {categories.length > 0 && (
              <select
                className="tickets-search__category"
                aria-label={t.categoryFilterLabel}
                value={categoryFilter}
                onChange={(e) => setCategoryFilter(e.target.value)}
              >
                <option value="">{t.categoryAll}</option>
                {categories.map((c) => (
                  <option key={c} value={c}>
                    {c}
                  </option>
                ))}
              </select>
            )}
          </div>

          {displayed.length === 0 ? (
            <p className="tickets-empty">{query || categoryFilter ? t.emptySearch : emptyText[activeView]}</p>
          ) : (
            <TicketRefsContext.Provider value={{ ticketIds, gerbilNames, refNames }}>
              <ul className="tickets-list">
                {displayed.map((ticket) => (
                  <TicketCard
                    key={ticket.id}
                    ticket={ticket}
                    onToggleStatus={() => handleToggleStatus(ticket)}
                    onSaveMessage={(message) => handleSaveMessage(ticket, message)}
                    onAnswer={(answer) => handleAnswer(ticket, answer)}
                    onDelete={() => handleDelete(ticket)}
                    onRestore={() => handleRestore(ticket)}
                    onHelpful={(helpful) => handleHelpful(ticket, helpful)}
                    onAddAttachment={(file) => handleAddAttachment(ticket, file)}
                    onDeleteAttachment={handleDeleteAttachment}
                  />
                ))}
              </ul>
            </TicketRefsContext.Provider>
          )}
        </>
      )}
    </section>
  )
}

interface TicketCardProps {
  ticket: FeedbackTicket
  onToggleStatus: () => void
  onSaveMessage: (message: string) => Promise<void>
  onAnswer: (answer: string) => Promise<void>
  onDelete: () => void
  onRestore: () => void
  onHelpful: (helpful: boolean) => void
  onAddAttachment: (file: File) => void
  onDeleteAttachment: (attachmentId: string) => void
}

/** Status-Badge: Beschriftung + Modifier-Klasse je Lebenszyklus-Zustand. */
function statusBadge(status: FeedbackTicket['status']): { label: string; modifier: string } {
  switch (status) {
    case 'Resolved':
      return { label: `✓ ${t.statusResolved}`, modifier: 'ticket-badge--resolved' }
    case 'NeedsInfo':
      return { label: t.statusNeedsInfo, modifier: 'ticket-badge--needsinfo' }
    case 'Answered':
      return { label: t.statusAnswered, modifier: 'ticket-badge--answered' }
    default:
      return { label: t.statusOpen, modifier: 'ticket-badge--open' }
  }
}

/** Tage bis zur endgültigen Löschung (Soft-Delete-Aufbewahrung). */
const TRASH_RETENTION_DAYS = 30

function TicketCard({
  ticket,
  onToggleStatus,
  onSaveMessage,
  onAnswer,
  onDelete,
  onRestore,
  onHelpful,
  onAddAttachment,
  onDeleteAttachment,
}: TicketCardProps) {
  const toast = useToast()
  const [editing, setEditing] = useState(false)
  const [draft, setDraft] = useState(ticket.message)
  const [saving, setSaving] = useState(false)
  // Antwort-Entwurf bleibt beim Verlassen/Zurückkehren erhalten (z. B. nach Klick auf
  // einen Tier-Link im Frage-Text) — pro Ticket in localStorage gepuffert.
  const draftKey = `ticket-answer-draft-${ticket.id}`
  const [answerDraft, setAnswerDraft] = useState(() => {
    try {
      return localStorage.getItem(draftKey) ?? ''
    } catch {
      return ''
    }
  })
  useEffect(() => {
    try {
      if (answerDraft) localStorage.setItem(draftKey, answerDraft)
      else localStorage.removeItem(draftKey)
    } catch {
      /* localStorage nicht verfügbar — Entwurf dann nur im Speicher */
    }
  }, [answerDraft, draftKey])
  const [answering, setAnswering] = useState(false)
  const entityLink = entityHref(ticket)
  const resolved = ticket.status === 'Resolved'
  const deleted = !!ticket.deletedAt
  const badge = statusBadge(ticket.status)
  // Verbleibende Tage bis zur endgültigen Löschung (Soft-Delete-Countdown). `new Date()` (wie
  // anderswo im Code) statt Date.now() — letzteres verstößt gegen die Purity-Lintregel.
  const trashDaysLeft = ticket.deletedAt
    ? Math.max(
        0,
        Math.ceil(
          (new Date(ticket.deletedAt).getTime() +
            TRASH_RETENTION_DAYS * 86_400_000 -
            new Date().getTime()) /
            86_400_000,
        ),
      )
    : null
  const [answerEditing, setAnswerEditing] = useState(false)
  // Erstantwort: Rückfrage offen, noch keine Antwort.
  const canAnswerNew = !ticket.answer && (ticket.status === 'NeedsInfo' || (!!ticket.question && !resolved))
  // Nachträglich ändern: bereits beantwortet (Answered) — die Züchterin darf ihre letzte
  // Info noch korrigieren, BEVOR die KI das Ticket bearbeitet.
  const canAmend = !!ticket.answer && ticket.status === 'Answered'
  // Antwort-Formular zeigen: neue Antwort, aktives Ändern, oder ein gepufferter Änderungs-Entwurf.
  // Im Papierkorb (deleted) ist nichts interaktiv außer „Wiederherstellen".
  const showAnswerForm = !deleted && (canAnswerNew || answerEditing || (canAmend && !!answerDraft))
  // Der ursprüngliche Ticket-Text ist nur bearbeitbar, SOLANGE es noch keine Antwort gibt
  // UND keine Rückfrage offen ist (NeedsInfo). Sobald die Züchterin geantwortet hat (oder die
  // KI nachgefragt hat), bleibt der Erst-Text fix — editierbar ist dann nur noch die neueste
  // Antwort (und auch die nur, bis die KI darauf reagiert hat, s. canAmend).
  const canEditMessage = !deleted && !ticket.answer && ticket.status !== 'NeedsInfo'

  function startAmend() {
    if (!answerDraft) setAnswerDraft(ticket.answer ?? '')
    setAnswerEditing(true)
  }
  function cancelAmend() {
    setAnswerEditing(false)
    setAnswerDraft('')
  }

  function startEdit() {
    setDraft(ticket.message)
    setEditing(true)
  }

  async function save() {
    const trimmed = draft.trim()
    if (!trimmed) {
      toast.error(t.emptyMessage)
      return
    }
    setSaving(true)
    try {
      await onSaveMessage(trimmed)
      setEditing(false)
    } catch (err) {
      toast.error(err instanceof ApiError ? err.message : t.updateError)
    } finally {
      setSaving(false)
    }
  }

  async function submitAnswer() {
    const trimmed = answerDraft.trim()
    if (!trimmed) {
      toast.error(t.emptyAnswer)
      return
    }
    setAnswering(true)
    try {
      await onAnswer(trimmed)
      setAnswerDraft('')
      setAnswerEditing(false)
    } catch (err) {
      toast.error(err instanceof ApiError ? err.message : t.updateError)
    } finally {
      setAnswering(false)
    }
  }

  return (
    <li id={`ticket-${ticket.id}`} className={`ticket-card${resolved ? ' ticket-card--resolved' : ''}`}>
      <div className="ticket-card__top">
        <span className={`ticket-badge ${badge.modifier}`}>{badge.label}</span>
        <span className="ticket-card__context">{contextLabel(ticket.context)}</span>
        {ticket.category && <span className="ticket-card__category">{ticket.category}</span>}
        {ticket.entityName &&
          (entityLink ? (
            <Link className="ticket-card__entity ticket-card__entity--link" to={entityLink} title={t.entityLinkTitle}>
              {ticket.entityName}
            </Link>
          ) : (
            <span className="ticket-card__entity">{ticket.entityName}</span>
          ))}
      </div>

      {editing ? (
        <div className="ticket-card__edit">
          <label className="ticket-card__edit-label" htmlFor={`ticket-edit-${ticket.id}`}>
            {t.editLabel}
          </label>
          <textarea
            id={`ticket-edit-${ticket.id}`}
            className="ticket-card__textarea"
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            rows={4}
            autoFocus
          />
          <div className="ticket-card__actions">
            <button type="button" className="btn" onClick={() => setEditing(false)} disabled={saving}>
              {t.cancel}
            </button>
            <button type="button" className="btn btn--primary" onClick={save} disabled={saving}>
              {saving ? t.saving : t.save}
            </button>
          </div>
        </div>
      ) : (
        <>
          <div className="ticket-card__message">
            <RichText text={ticket.message} />
          </div>

          {ticket.thread.length > 0 && (
            <div className="ticket-card__thread">
              <span className="ticket-card__thread-label">{t.threadLabel}</span>
              <ol className="ticket-card__thread-list">
                {ticket.thread.map((entry, i) => (
                  <li
                    key={i}
                    className={`ticket-card__thread-entry ticket-card__thread-entry--${entry.role}`}
                  >
                    <span className="ticket-card__thread-role">
                      {entry.role === 'maintainer' ? t.threadMaintainer : t.threadBreeder}
                    </span>
                    <div className="ticket-card__thread-text">
                      <RichText text={entry.text} />
                    </div>
                    {entry.at && (
                      <span className="ticket-card__thread-date">{formatDate(entry.at)}</span>
                    )}
                  </li>
                ))}
              </ol>
            </div>
          )}

          {ticket.question && (
            <div className="ticket-card__question">
              <span className="ticket-card__question-label">{t.questionLabel}</span>
              <div className="ticket-card__question-text">
                <RichText text={ticket.question} />
              </div>
            </div>
          )}

          {showAnswerForm ? (
            <div className="ticket-card__answer-form">
              <label className="ticket-card__edit-label" htmlFor={`ticket-answer-${ticket.id}`}>
                {t.answerLabel}
              </label>
              <textarea
                id={`ticket-answer-${ticket.id}`}
                className="ticket-card__textarea"
                value={answerDraft}
                onChange={(e) => setAnswerDraft(e.target.value)}
                placeholder={t.answerPlaceholder}
                rows={3}
              />
              <div className="ticket-card__actions">
                {answerEditing && (
                  <button type="button" className="btn" onClick={cancelAmend} disabled={answering}>
                    {t.cancel}
                  </button>
                )}
                <button
                  type="button"
                  className="btn btn--primary"
                  onClick={submitAnswer}
                  disabled={answering}
                >
                  {answering ? t.answering : t.answerButton}
                </button>
              </div>
            </div>
          ) : ticket.answer ? (
            <div className="ticket-card__answer">
              <span className="ticket-card__answer-label">{t.answerLabel}</span>
              <div className="ticket-card__answer-text">
                <RichText text={ticket.answer} />
              </div>
              {ticket.answeredAt && (
                <span className="ticket-card__answer-date">
                  {t.answeredOn}: {formatDate(ticket.answeredAt)}
                </span>
              )}
              {canAmend && (
                <div className="ticket-card__actions">
                  <button type="button" className="btn" onClick={startAmend}>
                    {t.amendAnswer}
                  </button>
                </div>
              )}
            </div>
          ) : null}

          {resolved && ticket.fixNote && (
            <div className="ticket-card__changelog">
              <span className="ticket-card__changelog-label">{t.changelogLabel}</span>
              <div className="ticket-card__changelog-text">
                <RichText text={ticket.fixNote} />
              </div>
            </div>
          )}

          {resolved && !deleted && (
            <div className="ticket-card__helpful">
              {ticket.helpful === true ? (
                <span className="ticket-card__helpful-thanks">{t.helpfulThanks}</span>
              ) : (
                <>
                  <span className="ticket-card__helpful-q">{t.helpfulQuestion}</span>
                  <button type="button" className="btn btn--small" onClick={() => onHelpful(true)}>
                    {t.helpfulYes}
                  </button>
                  <button type="button" className="btn btn--small" onClick={() => onHelpful(false)}>
                    {t.helpfulNo}
                  </button>
                </>
              )}
            </div>
          )}

          {(ticket.attachments.length > 0 || !deleted) && (
            <div className="ticket-card__attachments">
              <span className="ticket-card__attachments-label">{t.attachmentsLabel}</span>
              <div className="ticket-card__attachments-grid">
                {ticket.attachments.map((att) =>
                  att.contentType.startsWith('image/') ? (
                    <a
                      key={att.id}
                      href={attachmentUrl(att.id)}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="ticket-card__attachment"
                    >
                      <img src={attachmentUrl(att.id)} alt={att.fileName} loading="lazy" />
                      {!deleted && (
                        <button
                          type="button"
                          className="ticket-card__attachment-remove"
                          title={t.removeAttachment}
                          aria-label={t.removeAttachment}
                          onClick={(e) => {
                            e.preventDefault()
                            onDeleteAttachment(att.id)
                          }}
                        >
                          ✕
                        </button>
                      )}
                    </a>
                  ) : (
                    <span key={att.id} className="ticket-card__attachment ticket-card__attachment--file">
                      <a href={attachmentUrl(att.id)} target="_blank" rel="noopener noreferrer">
                        {att.fileName}
                      </a>
                      {!deleted && (
                        <button
                          type="button"
                          className="ticket-card__attachment-remove"
                          title={t.removeAttachment}
                          aria-label={t.removeAttachment}
                          onClick={() => onDeleteAttachment(att.id)}
                        >
                          ✕
                        </button>
                      )}
                    </span>
                  ),
                )}
                {!deleted && (
                  <label className="ticket-card__attachment-add">
                    + {t.addAttachment}
                    <input
                      type="file"
                      accept="image/*"
                      hidden
                      onChange={(e) => {
                        const f = e.target.files?.[0]
                        if (f) onAddAttachment(f)
                        e.currentTarget.value = ''
                      }}
                    />
                  </label>
                )}
              </div>
            </div>
          )}

          {deleted && trashDaysLeft !== null && (
            <p className="ticket-card__trash-notice">
              {trashDaysLeft === 0
                ? t.trashCountdownToday
                : trashDaysLeft === 1
                  ? t.trashCountdownTomorrow
                  : t.trashCountdownDays.replace('{n}', String(trashDaysLeft))}
            </p>
          )}

          <dl className="ticket-card__meta">
            <div>
              <dt>{t.createdLabel}</dt>
              <dd>{formatDate(ticket.createdAt)}</dd>
            </div>
            {resolved && (
              <div>
                <dt>{t.resolvedLabel}</dt>
                <dd>{formatDate(ticket.resolvedAt)}</dd>
              </div>
            )}
            {ticket.reopenedAt && (
              <div>
                <dt>{t.reopenedLabel}</dt>
                <dd>{formatDate(ticket.reopenedAt)}</dd>
              </div>
            )}
            {deleted && (
              <div>
                <dt>{t.deletedLabel}</dt>
                <dd>{formatDate(ticket.deletedAt)}</dd>
              </div>
            )}
          </dl>
          <div className="ticket-card__actions">
            {deleted ? (
              <button type="button" className="btn btn--primary" onClick={onRestore}>
                {t.restore}
              </button>
            ) : (
              <>
                <button type="button" className="btn" onClick={onToggleStatus}>
                  {resolved ? t.reopen : t.markResolved}
                </button>
                {canEditMessage && (
                  <button type="button" className="btn" onClick={startEdit}>
                    {t.edit}
                  </button>
                )}
                <button type="button" className="btn btn--danger" onClick={onDelete}>
                  {t.delete}
                </button>
              </>
            )}
          </div>
        </>
      )}
    </li>
  )
}
