/**
 * FEAT-4: Stammbaum (Ahnentafel) — interaktiver Stammbaum-Viewer.
 *
 * - react-d3-tree, horizontal (klassische Ahnentafel: Tier links, Vorfahren
 *   rechts; Vater oben, Mutter unten), Pinch-Zoom/Pan via d3-zoom plus
 *   explizite +/−/Einpassen-Buttons.
 * - Karten als foreignObject-HTML: Foto-Platzhalter (Fotos kommen mit FEAT-6),
 *   Name, ♂/♀, Farbschlag-Chip, Geburtsjahr. Tipp auf eine Karte wurzelt den
 *   Baum auf dieses Tier um; der Quellen-Cache bleibt erhalten, dadurch ist
 *   der Wechsel unmittelbar.
 * - Tiefe: 4 Generationen beim Laden (Board-Entscheidung), tiefere werden pro
 *   Ast über den ＋-Knopf nachgeladen. Die Tiefenbegrenzung steckt im
 *   Datenmodul (src/pedigree), nicht in react-d3-trees initialDepth — so sind
 *   nachgeladene Generationen sofort sichtbar und es wird nie blind vorab
 *   geladen.
 * - Druck/PDF: separate CSS-Grid-Ahnentafel (.stammbaum-print), per
 *   @media print sichtbar; Browser „Als PDF speichern“ ist der Export.
 */
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import Tree from 'react-d3-tree'
import type { CustomNodeElementProps, Point, RawNodeDatum } from 'react-d3-tree'
import { de } from '../strings/de'
import { ApiError } from '../api/client'
import { listColorVarieties } from '../api/lookups'
import { getInbreedingCoefficient } from '../api/pedigree'
import type { Gender, Gerbil } from '../api/types'
import { useApi } from '../hooks/useApi'
import { formatDate, genderLabel } from '../format/labels'
import { UNKNOWN_FARBSCHLAG, fromDisplayString, genotypeToFarbschlag } from '../genetics'
import {
  DEFAULT_GENERATIONS,
  ancestorsAt,
  buildPedigree,
  collectNodes,
  expandPedigree,
  toRawNodeDatum,
} from '../pedigree/build'
import { chipColorFor } from '../pedigree/chipColors'
import { createApiPedigreeSource } from '../pedigree/source'
import type { AnimalNode, PedigreeNode } from '../pedigree/types'
import './stammbaum.css'

/* Kartengeometrie (px im Baum-Koordinatensystem). */
const CARD_W = 190
const CARD_H = 88
/* nodeSize bei horizontaler Orientierung: x = Abstand der Generationen,
   y = vertikales Raster der Karten (siehe react-d3-tree generateTree). */
const NODE_X = 250
const NODE_Y = 100
const ZOOM_MIN = 0.2
const ZOOM_MAX = 2

function clamp(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value))
}

function errorMessage(err: unknown): string {
  if (err instanceof ApiError) return err.message
  return de.api.errors.unknown
}

/** Tiefe und Blattzahl des sichtbaren Baums (für „Einpassen“). */
function datumExtent(datum: RawNodeDatum): { depth: number; leaves: number } {
  if (!datum.children || datum.children.length === 0) return { depth: 0, leaves: 1 }
  let depth = 0
  let leaves = 0
  for (const child of datum.children) {
    const e = datumExtent(child)
    depth = Math.max(depth, e.depth + 1)
    leaves += e.leaves
  }
  return { depth, leaves }
}

interface View {
  zoom: number
  translate: Point
}

interface TreeState {
  forId: string | null
  root: AnimalNode | null
  error: string | null
}

export default function StammbaumPage() {
  const t = de.pages.stammbaum
  const { id = '' } = useParams()
  const navigate = useNavigate()

  /* ── Daten: Ahnenbaum (Cache überlebt das Umwurzeln) ── */
  const [source] = useState(createApiPedigreeSource)
  const [tree, setTree] = useState<TreeState>({ forId: null, root: null, error: null })

  useEffect(() => {
    let cancelled = false
    buildPedigree(source, id)
      .then((root) => {
        if (!cancelled) setTree({ forId: id, root, error: root ? null : t.notFound })
      })
      .catch((err: unknown) => {
        if (!cancelled) setTree({ forId: id, root: null, error: errorMessage(err) })
      })
    return () => {
      cancelled = true
    }
  }, [id, source, t])

  const loading = tree.forId !== id
  const root = loading ? null : tree.root

  const handleExpand = useCallback(
    (path: string) => {
      const current = tree.root
      if (!current || tree.forId !== id) return
      expandPedigree(source, current, path)
        .then((next) =>
          // Nur übernehmen, wenn inzwischen nicht umgewurzelt wurde.
          setTree((s) => (s.forId === id && s.root === current ? { ...s, root: next } : s)),
        )
        .catch(() => {
          /* Nachladen fehlgeschlagen: Baum unverändert lassen; erneuter Tipp versucht es wieder. */
        })
    },
    [tree, source, id],
  )

  /* ── Farbschlag: Lookup-Name, sonst aus dem Genotyp (GEN-1) ── */
  const colorVarieties = useApi(() => listColorVarieties(), [])
  const colorNameById = useMemo(
    () => new Map((colorVarieties.data ?? []).map((c) => [c.id, c.name])),
    [colorVarieties.data],
  )
  const farbschlagOf = useCallback(
    (g: Gerbil): string | null => {
      const byId = g.colorVarietyId ? colorNameById.get(g.colorVarietyId) : undefined
      if (byId) return byId
      if (g.genotype && g.genotype.trim()) {
        try {
          const name = genotypeToFarbschlag(fromDisplayString(g.genotype))
          return name === UNKNOWN_FARBSCHLAG ? null : name
        } catch {
          return null
        }
      }
      return null
    },
    [colorNameById],
  )

  /* ── Inzuchtkoeffizient (Endpunkt folgt serverseitig — bis dahin 404) ── */
  const inbreeding = useApi(() => getInbreedingCoefficient(id), [id])
  const inbreedingText =
    !inbreeding.loading && !inbreeding.error && inbreeding.data != null
      ? `${(inbreeding.data * 100).toLocaleString('de-DE', { maximumFractionDigits: 1 })} %`
      : t.inbreeding.unavailable

  /* ── react-d3-tree-Daten ── */
  const nodesByPath = useMemo(() => (root ? collectNodes(root) : null), [root])
  const datum = useMemo(() => (root ? toRawNodeDatum(root, t.unknown) : null), [root, t])

  /* ── Zeichenfläche vermessen (Erst-Zentrierung + Einpassen) ── */
  const canvasRef = useRef<HTMLDivElement | null>(null)
  const [size, setSize] = useState<{ w: number; h: number } | null>(null)
  const hasRoot = root !== null
  useEffect(() => {
    const el = canvasRef.current
    if (!el) return
    const update = () => setSize({ w: el.clientWidth, h: el.clientHeight })
    update()
    const observer = new ResizeObserver(update)
    observer.observe(el)
    return () => observer.disconnect()
  }, [hasRoot])

  /* ── Ansicht (Zoom/Position): Wurzelkarte links, vertikal zentriert —
        so sind die Eltern auch auf dem Smartphone sofort sichtbar. ── */
  const defaultView = useMemo<View | null>(() => {
    if (!size) return null
    const zoom = size.w < 520 ? 0.8 : 1
    return {
      zoom,
      translate: { x: (CARD_W / 2) * zoom + 24, y: size.h / 2 },
    }
  }, [size])

  const [viewOverride, setViewOverride] = useState<(View & { forId: string }) | null>(null)
  const view = viewOverride && viewOverride.forId === id ? viewOverride : defaultView
  /* Letzter Live-Stand aus d3 (Nutzer-Pan/-Pinch), Basis der Zoom-Buttons. */
  const liveView = useRef<(View & { forId: string }) | null>(null)
  const handleTreeUpdate = useCallback(
    (target: { node: unknown; zoom: number; translate: Point }) => {
      liveView.current = { forId: id, zoom: target.zoom, translate: target.translate }
    },
    [id],
  )

  const setView = useCallback(
    (next: View) => {
      setViewOverride({ forId: id, ...next })
      liveView.current = { forId: id, ...next }
    },
    [id],
  )

  /** Zoomt um `factor` um die Mitte der Zeichenfläche. */
  const applyZoom = useCallback(
    (factor: number) => {
      if (!size || !view) return
      const current = liveView.current && liveView.current.forId === id ? liveView.current : view
      const zoom = clamp(current.zoom * factor, ZOOM_MIN, ZOOM_MAX)
      const k = zoom / current.zoom
      const c = { x: size.w / 2, y: size.h / 2 }
      setView({
        zoom,
        translate: {
          x: c.x - (c.x - current.translate.x) * k,
          y: c.y - (c.y - current.translate.y) * k,
        },
      })
    },
    [size, view, id, setView],
  )

  /** Passt den ganzen Baum in die Zeichenfläche ein. */
  const fitView = useCallback(() => {
    if (!size || !datum) return
    const extent = datumExtent(datum)
    const width = extent.depth * NODE_X + CARD_W + 32
    const height = extent.leaves * NODE_Y + 32
    const zoom = clamp(Math.min(size.w / width, size.h / height), ZOOM_MIN, ZOOM_MAX)
    setView({ zoom, translate: { x: (CARD_W / 2 + 16) * zoom, y: size.h / 2 } })
  }, [size, datum, setView])

  /* ── Ahnenkarten ── */
  const renderNode = useCallback(
    ({ nodeDatum }: CustomNodeElementProps) => {
      const path = String(nodeDatum.attributes?.path ?? '')
      const node = nodesByPath?.get(path)
      return (
        <g>
          <foreignObject width={CARD_W} height={CARD_H} x={-CARD_W / 2} y={-CARD_H / 2}>
            {!node || node.kind === 'unknown' ? (
              <div className="pedigree-card pedigree-card--unknown">{t.unknown}</div>
            ) : (
              <PedigreeCard
                node={node}
                isRoot={path === ''}
                farbschlag={farbschlagOf(node.gerbil)}
                onOpen={() => navigate(`/rennmaeuse/${node.gerbil.id}/stammbaum`)}
                onExpand={() => handleExpand(path)}
              />
            )}
          </foreignObject>
        </g>
      )
    },
    [nodesByPath, farbschlagOf, navigate, handleExpand, t],
  )

  /* ── Zustände: Laden / Fehler ── */
  if (loading) return <p className="muted">{de.common.loading}</p>
  if (!root || !datum) {
    return (
      <section className="page">
        <p className="muted">{tree.error ?? t.notFound}</p>
        <Link to="/rennmaeuse" className="btn">
          {de.pages.gerbils.detail.back}
        </Link>
      </section>
    )
  }

  return (
    <>
      <section className="page stammbaum-screen">
        <header className="page-head">
          <h2>{t.titleFor(root.gerbil.name || de.pages.gerbils.nameless)}</h2>
          <div className="head-actions">
            <Link to={`/rennmaeuse/${root.gerbil.id}`} className="btn">
              {t.backToAnimal}
            </Link>
          </div>
        </header>

        <div className="stammbaum-toolbar">
          <button
            type="button"
            className="btn stammbaum-zoombtn"
            onClick={() => applyZoom(1.25)}
            aria-label={t.zoomIn}
            title={t.zoomIn}
          >
            ＋
          </button>
          <button
            type="button"
            className="btn stammbaum-zoombtn"
            onClick={() => applyZoom(0.8)}
            aria-label={t.zoomOut}
            title={t.zoomOut}
          >
            −
          </button>
          <button type="button" className="btn" onClick={fitView}>
            {t.zoomFit}
          </button>
          <span className="stammbaum-toolbar__spacer" />
          <span className="stammbaum-inzucht">
            {t.inbreeding.label}: <b>{inbreedingText}</b>
          </span>
          <button type="button" className="btn" onClick={() => window.print()}>
            {t.print}
          </button>
        </div>

        <div className="stammbaum-canvas" ref={canvasRef}>
          {view && (
            <Tree
              key={id}
              data={datum}
              renderCustomNodeElement={renderNode}
              orientation="horizontal"
              pathFunc="step"
              translate={view.translate}
              zoom={view.zoom}
              scaleExtent={{ min: ZOOM_MIN, max: ZOOM_MAX }}
              zoomable
              draggable
              collapsible={false}
              nodeSize={{ x: NODE_X, y: NODE_Y }}
              separation={{ siblings: 1, nonSiblings: 1 }}
              onUpdate={handleTreeUpdate}
            />
          )}
        </div>
        <p className="stammbaum-hint">{t.tapHint}</p>
      </section>

      {/* Druckansicht: am Bildschirm unsichtbar, ersetzt beim Drucken alles. */}
      <PrintPedigree root={root} farbschlagOf={farbschlagOf} inbreedingText={inbreedingText} />
    </>
  )
}

/* ── Ahnenkarte (interaktive Ansicht) ───────────────────────────── */

function PedigreeCard({
  node,
  isRoot,
  farbschlag,
  onOpen,
  onExpand,
}: {
  node: AnimalNode
  isRoot: boolean
  farbschlag: string | null
  onOpen: () => void
  onExpand: () => void
}) {
  const t = de.pages.stammbaum
  const g = node.gerbil
  const chip = farbschlag ? chipColorFor(farbschlag) : null
  const year = g.dateOfBirth ? g.dateOfBirth.slice(0, 4) : null
  return (
    <div
      className={isRoot ? 'pedigree-card pedigree-card--root' : 'pedigree-card'}
      onClick={isRoot ? undefined : onOpen}
      role={isRoot ? undefined : 'button'}
      title={isRoot ? undefined : t.tapHint}
    >
      {/* Foto-Platzhalter — echte Fotos kommen mit FEAT-6. */}
      <div className="pedigree-card__photo" aria-hidden="true">
        🐭
      </div>
      <div className="pedigree-card__body">
        <div className="pedigree-card__name">
          <SexIcon gender={g.gender} />
          <Link
            to={`/rennmaeuse/${g.id}`}
            className="pedigree-card__nametext"
            onClick={(e) => e.stopPropagation()}
          >
            {g.name || de.pages.gerbils.nameless}
          </Link>
        </div>
        {farbschlag && (
          <span
            className="pedigree-chip"
            style={chip ? { background: chip.bg, color: chip.fg } : undefined}
          >
            {farbschlag}
          </span>
        )}
        {year && <span className="pedigree-card__year">* {year}</span>}
      </div>
      {node.expandable && (
        <button
          type="button"
          className="pedigree-card__expand"
          aria-label={t.expand}
          title={t.expand}
          onClick={(e) => {
            e.stopPropagation()
            onExpand()
          }}
        >
          ＋
        </button>
      )}
    </div>
  )
}

function SexIcon({ gender }: { gender: Gender }) {
  const symbol = gender === 'male' ? '♂' : gender === 'female' ? '♀' : '?'
  return (
    <span
      className={`pedigree-card__sex pedigree-card__sex--${gender}`}
      title={genderLabel(gender)}
      aria-label={genderLabel(gender)}
    >
      {symbol}
    </span>
  )
}

/* ── Druckansicht: Ahnentafel als CSS-Grid (Zertifikats-Layout) ── */

function PrintPedigree({
  root,
  farbschlagOf,
  inbreedingText,
}: {
  root: AnimalNode
  farbschlagOf: (g: Gerbil) => string | null
  inbreedingText: string
}) {
  const t = de.pages.stammbaum
  const generations = Array.from({ length: DEFAULT_GENERATIONS + 1 }, (_, g) =>
    ancestorsAt(root, g),
  )
  const today = new Date().toLocaleDateString('de-DE')
  return (
    <div className="stammbaum-print">
      <header className="stammbaum-print__head">
        <h1>{t.titleFor(root.gerbil.name || de.pages.gerbils.nameless)}</h1>
        <p className="stammbaum-print__meta">
          {t.inbreeding.label}: {inbreedingText} · {t.printView.createdOn} {today}
        </p>
      </header>
      <div className="stammbaum-print__grid">
        {generations.map((_, g) => (
          <div key={`gen-${g}`} className="stammbaum-print__gen" style={{ gridColumn: g + 1, gridRow: 1 }}>
            {t.printView.generations[g]}
          </div>
        ))}
        {generations.flatMap((slots, g) => {
          const span = 2 ** (DEFAULT_GENERATIONS - g)
          return slots.map((node, i) => (
            <PrintCell
              key={`${g}:${i}`}
              node={node}
              gen={g}
              farbschlag={node?.kind === 'animal' ? farbschlagOf(node.gerbil) : null}
              style={{ gridColumn: g + 1, gridRow: `${i * span + 2} / span ${span}` }}
            />
          ))
        })}
      </div>
    </div>
  )
}

function PrintCell({
  node,
  gen,
  farbschlag,
  style,
}: {
  node: PedigreeNode | null
  gen: number
  farbschlag: string | null
  style: CSSProperties
}) {
  const t = de.pages.stammbaum
  const base = `stammbaum-print__cell stammbaum-print__cell--g${gen}`
  if (!node) return <div className={`${base} stammbaum-print__cell--empty`} style={style} />
  if (node.kind === 'unknown') {
    return (
      <div className={`${base} stammbaum-print__cell--unknown`} style={style}>
        {t.unknown}
      </div>
    )
  }
  const g = node.gerbil
  const sexSymbol = g.gender === 'male' ? '♂ ' : g.gender === 'female' ? '♀ ' : ''
  return (
    <div className={base} style={style}>
      <div className="stammbaum-print__name">
        {sexSymbol}
        {g.name || de.pages.gerbils.nameless}
      </div>
      {g.dateOfBirth && (
        <div className="stammbaum-print__sub">
          {t.printView.born} {formatDate(g.dateOfBirth)}
        </div>
      )}
      {farbschlag && <div className="stammbaum-print__sub">{farbschlag}</div>}
      {g.genotype && gen <= 2 && <div className="stammbaum-print__geno">{g.genotype}</div>}
    </div>
  )
}
