"use client";

import { ChevronDown, ChevronRight } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Avatar } from "@/components/ui/Avatar";
import { Button } from "@/components/ui/Button";
import { SearchInput } from "@/components/ui/SearchInput";
import { SegmentedControl } from "@/components/ui/SegmentedControl";
import { LazyGraphOrgChart } from "./LazyGraphOrgChart";
import type { ChartNode, OrgEmployee } from "./types";

type ViewMode = "list" | "graph";

export function EmployeeTree({ employees, focusId = null }: { employees: OrgEmployee[]; focusId?: string | null }) {
  // Seeded with the focus target so their own reports are already unfolded;
  // the chain *above* them comes from ancestorExpandIds.
  const [expanded, setExpanded] = useState<Set<string>>(() => (focusId ? new Set([focusId]) : new Set()));
  const [query, setQuery] = useState("");
  const [mode, setMode] = useState<ViewMode>("list");
  const focusRef = useRef<HTMLDivElement>(null);

  const { childrenByManager, totalReportsById, root } = useMemo(() => {
    const byManager = new Map<string, OrgEmployee[]>();
    for (const e of employees) {
      const key = e.manager_id ?? "__root__";
      if (!byManager.has(key)) byManager.set(key, []);
      byManager.get(key)!.push(e);
    }
    for (const list of byManager.values()) list.sort((a, b) => a.last_name.localeCompare(b.last_name));

    // Nothing in the schema forbids a manager_id cycle (A reports to B
    // reports to A), and a reorg that moves a lead under one of their own
    // reports would create one. Every walk below is recursive, so an
    // unguarded cycle is an infinite recursion that hangs the tab rather
    // than a wrong number — hence the on-path set.
    const totals = new Map<string, number>();
    function countTotal(id: string, path: Set<string>): number {
      const memo = totals.get(id);
      if (memo !== undefined) return memo;
      if (path.has(id)) return 0;
      path.add(id);
      let total = 0;
      for (const child of byManager.get(id) ?? []) total += 1 + countTotal(child.id, path);
      path.delete(id);
      totals.set(id, total);
      return total;
    }
    for (const e of employees) countTotal(e.id, new Set());

    return { childrenByManager: byManager, totalReportsById: totals, root: byManager.get("__root__") ?? [] };
  }, [employees]);

  // Both badges name a person by id, so the lookup is shared rather than
  // rebuilt per node.
  const nameById = useMemo(() => new Map(employees.map((e) => [e.id, `${e.first_name} ${e.last_name}`])), [employees]);
  const nameOf = useCallback((id: string | null) => (id ? nameById.get(id) : undefined), [nameById]);

  const matchIds = useMemo(() => {
    // A focus target behaves exactly like a search hit — same ancestor
    // auto-expand, same ring, same fitView in graph mode — until the user
    // starts typing, at which point their own search takes over.
    if (query.trim().length < 2) return focusId ? new Set([focusId]) : null;
    const q = query.trim().toLowerCase();
    const matches = new Set<string>();
    for (const e of employees) {
      if (
        `${e.first_name} ${e.last_name}`.toLowerCase().includes(q) ||
        e.job_title.toLowerCase().includes(q) ||
        String(e.personnel_number).includes(q)
      ) {
        matches.add(e.id);
      }
    }
    return matches;
  }, [query, employees, focusId]);

  // The chain above the focused person is auto-expanded, so the row only
  // exists after that render — scroll once it does.
  useEffect(() => {
    if (!focusId) return;
    focusRef.current?.scrollIntoView({ block: "center", behavior: "smooth" });
  }, [focusId, mode]);

  const ancestorExpandIds = useMemo(() => {
    if (!matchIds) return null;
    const byId = new Map(employees.map((e) => [e.id, e]));
    const toExpand = new Set<string>();
    for (const id of matchIds) {
      let current = byId.get(id);
      while (current?.manager_id && !toExpand.has(current.manager_id)) {
        toExpand.add(current.manager_id);
        current = byId.get(current.manager_id);
      }
    }
    return toExpand;
  }, [matchIds, employees]);

  // useCallback-stable: GraphOrgChart's layout memo depends on this
  // reference, so an unstable function would force a Dagre re-layout on
  // every unrelated re-render.
  const toggle = useCallback((id: string) => {
    setExpanded((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }, []);

  const isExpanded = useCallback((id: string): boolean => expanded.has(id) || (ancestorExpandIds?.has(id) ?? false), [expanded, ancestorExpandIds]);

  const chartTree = useMemo<ChartNode[]>(() => {
    function toChartNode(e: OrgEmployee, ancestors: Set<string>): ChartNode {
      const children = ancestors.has(e.id) ? [] : (childrenByManager.get(e.id) ?? []);
      const total = totalReportsById.get(e.id) ?? 0;
      const nextAncestors = new Set(ancestors).add(e.id);
      return {
        id: e.id,
        kind: "person",
        label: `${e.first_name} ${e.last_name}`,
        sublabel: e.job_title,
        href: `/employees/${e.id}`,
        avatar: { firstName: e.first_name, lastName: e.last_name },
        totalReports: total,
        matched: matchIds?.has(e.id) ?? false,
        absent: e.absent,
        // Who is standing in for this absent person: their own acting
        // manager, which is the same one their reports moved to.
        coveredBy: e.absent ? nameOf(e.manager_id) : undefined,
        coveringFor: nameOf(e.formal_manager_id),
        children: children.map((c) => toChartNode(c, nextAncestors)),
      };
    }
    return root.map((r) => toChartNode(r, new Set()));
  }, [root, childrenByManager, totalReportsById, matchIds, nameOf]);

  function renderNode(e: OrgEmployee, depth: number, ancestors: Set<string>) {
    const children = ancestors.has(e.id) ? [] : (childrenByManager.get(e.id) ?? []);
    const hasChildren = children.length > 0;
    const expandedNow = isExpanded(e.id);
    const isMatch = matchIds?.has(e.id) ?? false;
    const totalReports = totalReportsById.get(e.id) ?? 0;

    return (
      <div key={e.id}>
        <div
          ref={e.id === focusId ? focusRef : undefined}
          className={`flex items-center gap-2 rounded px-2 py-1.5 hover:bg-surface ${isMatch ? "bg-brand-100" : ""}`}
          style={{ paddingLeft: depth * 24 + 8 }}
        >
          {hasChildren ? (
            <button type="button" onClick={() => toggle(e.id)} className="shrink-0 text-ink-muted">
              {expandedNow ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
            </button>
          ) : (
            <span className="w-4 shrink-0" />
          )}
          <Avatar firstName={e.first_name} lastName={e.last_name} size="sm" />
          <Link href={`/employees/${e.id}`} className="min-w-0 flex-1 hover:underline">
            <span className="text-sm font-semibold text-ink">
              {e.first_name} {e.last_name}
            </span>
            <span className="ml-2 text-xs text-ink-muted">{e.job_title}</span>
          </Link>
          {/* The stand-in arrangement is stated on both sides: on the absent
              person (whose team has moved away) and on anyone now reporting
              elsewhere — otherwise the chart would silently show a reporting
              line that is not the recorded one. */}
          {e.absent && (
            <span className="shrink-0 rounded-full bg-warning-bg px-2 py-0.5 text-[11px] font-semibold text-warning-text">
              Abwesend{nameOf(e.manager_id) ? ` · Vertretung: ${nameOf(e.manager_id)}` : ""}
            </span>
          )}
          {e.formal_manager_id && (
            <span className="shrink-0 rounded-full bg-info-bg px-2 py-0.5 text-[11px] font-semibold text-info-text">
              Vertretung für {nameOf(e.formal_manager_id)}
            </span>
          )}
          {hasChildren && (
            <span className="shrink-0 text-xs text-ink-muted">
              {children.length} direkt · {totalReports} gesamt
            </span>
          )}
        </div>
        {hasChildren && expandedNow && (
          <div>{children.map((c) => renderNode(c, depth + 1, new Set(ancestors).add(e.id)))}</div>
        )}
      </div>
    );
  }

  return (
    <div className="rounded border border-border bg-white p-4">
      <div className="mb-4 flex flex-wrap items-center gap-3">
        <SearchInput label="Organigramm durchsuchen" placeholder="Name, Pers.-Nr., Titel…" value={query} onChange={setQuery} />
        <Button variant="secondary" size="sm" onClick={() => setExpanded(new Set(root.map((r) => r.id)))}>
          Bereiche anzeigen
        </Button>
        <Button variant="secondary" size="sm" onClick={() => setExpanded(new Set())}>
          Alles einklappen
        </Button>
        <SegmentedControl<ViewMode> value={mode} onChange={setMode} options={[{ value: "list", label: "Liste" }, { value: "graph", label: "Grafisch" }]} />
      </div>
      {mode === "list" ? (
        <div>{root.map((r) => renderNode(r, 0, new Set()))}</div>
      ) : (
        <LazyGraphOrgChart tree={chartTree} isExpanded={isExpanded} onToggle={toggle} matchedIds={matchIds} />
      )}
    </div>
  );
}
