import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import ReactFlow, {
  Background,
  BaseEdge,
  ConnectionMode,
  EdgeLabelRenderer,
  Handle,
  MarkerType,
  Panel,
  Position,
  ReactFlowProvider,
  useEdgesState,
  useNodesState,
  useReactFlow,
  type Connection,
  type Edge,
  type EdgeProps,
  type Node,
  type NodeProps,
  type OnConnectStartParams,
} from 'reactflow';
import 'reactflow/dist/style.css';

import { collectDescendantGroupIds } from '../../shared/npcs/npcGroups';
import type {
  NpcGroupId,
  NpcId,
  NpcRelationId,
  ProjectNpc,
  ProjectNpcGroup,
  ProjectNpcRelation,
} from '../../shared/types';
import { Select } from '../shared/ui/controls';
import { useAssetUrl } from '../shared/useAssetImageUrl';

import styles from './NpcGraph.module.css';

type OpenEdgeMenuFn = (relationId: NpcRelationId, x: number, y: number) => void;
type SelectSourceNpcFn = (sourceNpcId: NpcId) => void;
const OpenEdgeMenuContext = createContext<OpenEdgeMenuFn | null>(null);
const SelectSourceNpcContext = createContext<SelectSourceNpcFn | null>(null);

export type GraphGroupFilter = 'all' | 'ungrouped' | NpcGroupId;

export type NpcGraphUiStrings = {
  zoomBar: string;
  zoomIn: string;
  zoomOut: string;
  fitAll: string;
  editRelation: string;
  deleteRelation: string;
  untitled: string;
  graphFilter: string;
  graphFilterAll: string;
  graphFilterUngrouped: string;
};

type NpcNodeData = {
  name: string;
  avatarAssetId: ProjectNpc['avatarAssetId'];
  active: boolean;
  groupColor: string | null;
  dimmed: boolean;
};

const NPC_ACCENT = '#60a5fa';
const NPC_EDGE_IDLE = '#a1a1aa';
/** Согласовано с `.node` в CSS (ширина + типичная высота карточки). */
const NPC_NODE_W = 120;
const NPC_NODE_H = 150;
/** Расстояние между параллельными связями одной пары НПС. */
const PARALLEL_EDGE_GAP = 21;

type Side = 'left' | 'right' | 'top' | 'bottom';

type NpcEdgeData = {
  label: string;
  /** Смещение в мировых координатах (общее для пары, без учёта направления). */
  offset: number;
  relationId: NpcRelationId;
  sourceNpcId: NpcId;
  targetNpcId: NpcId;
  highlighted: boolean;
};

/** Любые связи между одной парой НПС (A→B и B→A) — в одной группе разведения. */
function undirectedPairKey(a: NpcId, b: NpcId): string {
  return a < b ? `${a}__${b}` : `${b}__${a}`;
}

function sideToPosition(side: Side): Position {
  switch (side) {
    case 'left':
      return Position.Left;
    case 'right':
      return Position.Right;
    case 'top':
      return Position.Top;
    case 'bottom':
      return Position.Bottom;
  }
}

/** Выбираем стороны карточек так, чтобы линия выходила наружу и не шла сквозь блок. */
function pickEndpointSides(
  sourcePos: { x: number; y: number },
  targetPos: { x: number; y: number },
): { sourceSide: Side; targetSide: Side } {
  const sx = sourcePos.x + NPC_NODE_W / 2;
  const sy = sourcePos.y + NPC_NODE_H / 2;
  const tx = targetPos.x + NPC_NODE_W / 2;
  const ty = targetPos.y + NPC_NODE_H / 2;
  const dx = tx - sx;
  const dy = ty - sy;
  if (Math.abs(dx) >= Math.abs(dy)) {
    return dx >= 0
      ? { sourceSide: 'right', targetSide: 'left' }
      : { sourceSide: 'left', targetSide: 'right' };
  }
  return dy >= 0 ? { sourceSide: 'bottom', targetSide: 'top' } : { sourceSide: 'top', targetSide: 'bottom' };
}

function NpcNode({ data, selected }: NodeProps<NpcNodeData>) {
  const url = useAssetUrl(data.avatarAssetId);
  const sides: Side[] = ['left', 'right', 'top', 'bottom'];
  const accent = data.groupColor ?? NPC_ACCENT;
  return (
    <div
      className={[
        styles.node,
        data.active || selected ? styles.nodeActive : '',
        data.dimmed ? styles.nodeDimmed : '',
      ]
        .filter(Boolean)
        .join(' ')}
      style={
        data.groupColor
          ? ({
              '--npc-group-color': accent,
              borderColor: data.active || selected ? accent : undefined,
            } as React.CSSProperties)
          : undefined
      }
    >
      {sides.map((side) => (
        <React.Fragment key={side}>
          <Handle type="source" position={sideToPosition(side)} id={`s-${side}`} className={styles.handle} />
          <Handle type="target" position={sideToPosition(side)} id={`t-${side}`} className={styles.handle} />
        </React.Fragment>
      ))}
      <div className={styles.avatar}>
        {url ? <img className={styles.avatarImg} src={url} alt="" draggable={false} /> : null}
      </div>
      <div className={styles.name}>{data.name || '—'}</div>
    </div>
  );
}

/**
 * Кривая с перпендикуляром в мировых координатах.
 * Базис берём от «меньшего» id к «большему», чтобы A→B и B→A с разными offset не совпадали.
 */
function parallelCubicPath(
  sourceNpcId: NpcId,
  targetNpcId: NpcId,
  sourceX: number,
  sourceY: number,
  targetX: number,
  targetY: number,
  worldOffset: number,
): { path: string; labelX: number; labelY: number } {
  // Канонический вектор между концами (не зависит от направления стрелки).
  const [ax, ay, bx, by] =
    sourceNpcId < targetNpcId ? [sourceX, sourceY, targetX, targetY] : [targetX, targetY, sourceX, sourceY];
  const cdx = bx - ax;
  const cdy = by - ay;
  const clen = Math.sqrt(cdx * cdx + cdy * cdy) || 1;
  const px = (-cdy / clen) * worldOffset;
  const py = (cdx / clen) * worldOffset;

  // Сдвигаем всю кривую (включая концы у ручек), чтобы линии не сливались.
  const sx = sourceX + px;
  const sy = sourceY + py;
  const tx = targetX + px;
  const ty = targetY + py;
  const dx = tx - sx;
  const dy = ty - sy;
  const c1x = sx + dx * 0.35;
  const c1y = sy + dy * 0.35;
  const c2x = sx + dx * 0.65;
  const c2y = sy + dy * 0.65;
  return {
    path: `M ${sx},${sy} C ${c1x},${c1y} ${c2x},${c2y} ${tx},${ty}`,
    labelX: (sx + tx) / 2,
    labelY: (sy + ty) / 2,
  };
}

function LabeledNpcEdge({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
  style,
  markerEnd,
  data,
}: EdgeProps<NpcEdgeData>) {
  const openEdgeMenu = useContext(OpenEdgeMenuContext);
  const selectSourceNpc = useContext(SelectSourceNpcContext);
  const worldOffset = data?.offset ?? 0;
  const sourceNpcId = data?.sourceNpcId;
  const targetNpcId = data?.targetNpcId;
  const { path, labelX, labelY } =
    sourceNpcId && targetNpcId
      ? parallelCubicPath(sourceNpcId, targetNpcId, sourceX, sourceY, targetX, targetY, worldOffset)
      : {
          path: `M ${sourceX},${sourceY} L ${targetX},${targetY}`,
          labelX: (sourceX + targetX) / 2,
          labelY: (sourceY + targetY) / 2,
        };
  const label = data?.label ?? '';
  const relationId = data?.relationId;
  const highlighted = Boolean(data?.highlighted);

  return (
    <>
      <BaseEdge
        id={id}
        path={path}
        interactionWidth={24}
        {...(style ? { style } : {})}
        {...(markerEnd ? { markerEnd } : {})}
      />
      {label && relationId ? (
        <EdgeLabelRenderer>
          <div
            className={[styles.edgeLabel, highlighted ? styles.edgeLabelActive : '', 'nodrag', 'nopan']
              .filter(Boolean)
              .join(' ')}
            style={{
              position: 'absolute',
              left: labelX,
              top: labelY,
              zIndex: highlighted ? 20 : 5,
            }}
            title={label}
            onClick={(e) => {
              e.stopPropagation();
              if (sourceNpcId) selectSourceNpc?.(sourceNpcId);
            }}
            onContextMenu={(e) => {
              e.preventDefault();
              e.stopPropagation();
              openEdgeMenu?.(relationId, e.clientX, e.clientY);
            }}
          >
            {label}
          </div>
        </EdgeLabelRenderer>
      ) : null}
    </>
  );
}

function ZoomToolbar({ ui }: { ui: NpcGraphUiStrings }) {
  const { zoomIn, zoomOut, fitView } = useReactFlow();
  return (
    <Panel position="bottom-right">
      <div className={styles.zoomBar} role="toolbar" aria-label={ui.zoomBar}>
        <button type="button" className={styles.zoomBtn} onClick={() => zoomIn()} aria-label={ui.zoomIn}>
          +
        </button>
        <button type="button" className={styles.zoomBtn} onClick={() => zoomOut()} aria-label={ui.zoomOut}>
          −
        </button>
        <button
          type="button"
          className={styles.zoomBtn}
          onClick={() => fitView({ padding: 0.2 })}
          aria-label={ui.fitAll}
        >
          ⤢
        </button>
      </div>
    </Panel>
  );
}

function FilterToolbar({
  ui,
  groups,
  value,
  onChange,
}: {
  ui: NpcGraphUiStrings;
  groups: ProjectNpcGroup[];
  value: GraphGroupFilter;
  onChange: (v: GraphGroupFilter) => void;
}) {
  return (
    <Panel position="top-left">
      <Select
        {...(styles.filterSelect ? { className: styles.filterSelect } : {})}
        ariaLabel={ui.graphFilter}
        value={value}
        onChange={(next) => onChange(next as GraphGroupFilter)}
        options={[
          { value: 'all', label: ui.graphFilterAll },
          { value: 'ungrouped', label: ui.graphFilterUngrouped },
          ...groups.map((g) => ({ value: g.id, label: g.name })),
        ]}
      />
    </Panel>
  );
}

export type NpcGraphProps = {
  npcs: ProjectNpc[];
  relations: ProjectNpcRelation[];
  npcGroups: ProjectNpcGroup[];
  selectedNpcId: NpcId | null;
  graphFilter: GraphGroupFilter;
  onGraphFilterChange: (filter: GraphGroupFilter) => void;
  graphUi: NpcGraphUiStrings;
  onSelect: (npcId: NpcId) => void;
  onConnectRequest: (sourceNpcId: NpcId, targetNpcId: NpcId) => void;
  onNodePositionCommit: (npcId: NpcId, x: number, y: number) => void;
  onEditRelation: (relationId: NpcRelationId) => void;
  onDeleteRelation: (relationId: NpcRelationId) => void;
};

function NpcGraphInner({
  npcs,
  relations,
  npcGroups,
  selectedNpcId,
  graphFilter,
  onGraphFilterChange,
  graphUi,
  onSelect,
  onConnectRequest,
  onNodePositionCommit,
  onEditRelation,
  onDeleteRelation,
}: NpcGraphProps) {
  const [menu, setMenu] = useState<{ relationId: NpcRelationId; left: number; top: number } | null>(null);
  /** Откуда реально начали тянуть связь (Loose mode может перевернуть source/target). */
  const connectFromRef = useRef<NpcId | null>(null);

  const edgeTypes = useMemo(() => ({ npcRelation: LabeledNpcEdge }), []);
  const nodeTypes = useMemo(() => ({ npc: NpcNode }), []);

  const menuPosition = useMemo(() => {
    if (!menu) return null;
    const pad = 8;
    const mw = 180;
    const mh = 88;
    return {
      left: Math.max(pad, Math.min(menu.left, window.innerWidth - mw - pad)),
      top: Math.max(pad, Math.min(menu.top, window.innerHeight - mh - pad)),
    };
  }, [menu]);

  const groupColorById = useMemo(() => new Map(npcGroups.map((g) => [g.id, g.color])), [npcGroups]);

  const filterGroupIds = useMemo(() => {
    if (graphFilter === 'all' || graphFilter === 'ungrouped') return null;
    return collectDescendantGroupIds(npcGroups, graphFilter);
  }, [graphFilter, npcGroups]);

  const isNpcDimmed = useCallback(
    (npc: ProjectNpc) => {
      if (graphFilter === 'all') return false;
      if (graphFilter === 'ungrouped') return npc.groupId !== null;
      if (!filterGroupIds) return true;
      return npc.groupId === null || !filterGroupIds.has(npc.groupId);
    },
    [filterGroupIds, graphFilter],
  );

  const initialNodes: Node<NpcNodeData>[] = useMemo(
    () =>
      npcs.map((n) => ({
        id: n.id,
        type: 'npc',
        position: { x: n.x, y: n.y },
        data: {
          name: n.name,
          avatarAssetId: n.avatarAssetId,
          active: n.id === selectedNpcId,
          groupColor: n.groupId ? (groupColorById.get(n.groupId) ?? null) : null,
          dimmed: isNpcDimmed(n),
        },
      })),
    [groupColorById, isNpcDimmed, npcs, selectedNpcId],
  );

  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);

  const builtEdges: Edge<NpcEdgeData>[] = useMemo(() => {
    const posById = new Map<string, { x: number; y: number }>();
    for (const n of nodes) posById.set(n.id, n.position);
    // Пока nodes ещё пуст/не синхронизирован — берём координаты из проекта.
    for (const n of npcs) {
      if (!posById.has(n.id)) posById.set(n.id, { x: n.x, y: n.y });
    }

    // Группируем ВСЕ связи между парой НПС (оба направления), чтобы не накладывались.
    const groups = new Map<string, ProjectNpcRelation[]>();
    for (const r of relations) {
      const key = undirectedPairKey(r.sourceNpcId, r.targetNpcId);
      const list = groups.get(key) ?? [];
      list.push(r);
      groups.set(key, list);
    }
    const out: Edge<NpcEdgeData>[] = [];
    for (const group of groups.values()) {
      const sorted = [...group].sort((a, b) => a.id.localeCompare(b.id));
      const total = sorted.length;
      sorted.forEach((r, index) => {
        const sourcePos = posById.get(r.sourceNpcId) ?? { x: 0, y: 0 };
        const targetPos = posById.get(r.targetNpcId) ?? { x: 0, y: 0 };
        const { sourceSide, targetSide } = pickEndpointSides(sourcePos, targetPos);
        const offset = (index - (total - 1) / 2) * PARALLEL_EDGE_GAP;
        const highlighted = selectedNpcId !== null && r.sourceNpcId === selectedNpcId;
        const sourceNpc = npcs.find((n) => n.id === r.sourceNpcId);
        const targetNpc = npcs.find((n) => n.id === r.targetNpcId);
        const edgeDimmed =
          graphFilter !== 'all' &&
          ((sourceNpc && isNpcDimmed(sourceNpc)) || (targetNpc && isNpcDimmed(targetNpc)));
        const color = highlighted ? NPC_ACCENT : NPC_EDGE_IDLE;
        out.push({
          id: r.id,
          source: r.sourceNpcId,
          target: r.targetNpcId,
          sourceHandle: `s-${sourceSide}`,
          targetHandle: `t-${targetSide}`,
          type: 'npcRelation',
          zIndex: highlighted ? 10 : 0,
          data: {
            label: r.label,
            offset,
            relationId: r.id,
            sourceNpcId: r.sourceNpcId,
            targetNpcId: r.targetNpcId,
            highlighted,
          },
          style: {
            stroke: color,
            strokeWidth: highlighted ? 2.5 : 2,
            opacity: edgeDimmed ? 0.2 : 1,
          },
          markerEnd: {
            type: MarkerType.ArrowClosed,
            width: 16,
            height: 16,
            color,
          },
        });
      });
    }
    return out;
  }, [graphFilter, isNpcDimmed, nodes, npcs, relations, selectedNpcId]);

  useEffect(() => {
    setNodes(initialNodes);
  }, [initialNodes, setNodes]);

  useEffect(() => {
    setEdges(builtEdges);
  }, [builtEdges, setEdges]);

  const onConnectStart = useCallback((_event: unknown, params: OnConnectStartParams) => {
    connectFromRef.current = params.nodeId ? (params.nodeId as NpcId) : null;
  }, []);

  const onConnect = useCallback(
    (conn: Connection) => {
      if (!conn.source || !conn.target || conn.source === conn.target) return;
      const from = connectFromRef.current;
      if (from && (from === conn.source || from === conn.target)) {
        const to = (from === conn.source ? conn.target : conn.source) as NpcId;
        onConnectRequest(from, to);
        return;
      }
      onConnectRequest(conn.source as NpcId, conn.target as NpcId);
    },
    [onConnectRequest],
  );

  const openEdgeMenu = useCallback((relationId: NpcRelationId, x: number, y: number) => {
    setMenu({ relationId, left: x, top: y });
  }, []);

  const selectSourceNpc = useCallback(
    (sourceNpcId: NpcId) => {
      setMenu(null);
      onSelect(sourceNpcId);
    },
    [onSelect],
  );

  return (
    <OpenEdgeMenuContext.Provider value={openEdgeMenu}>
      <SelectSourceNpcContext.Provider value={selectSourceNpc}>
        <div className={styles.wrap}>
          <ReactFlow
            nodes={nodes}
            edges={edges}
            onNodesChange={onNodesChange}
            onEdgesChange={onEdgesChange}
            onConnectStart={onConnectStart}
            onConnect={onConnect}
            onConnectEnd={() => {
              connectFromRef.current = null;
            }}
            nodeTypes={nodeTypes}
            edgeTypes={edgeTypes}
            connectionMode={ConnectionMode.Loose}
            fitView
            proOptions={{ hideAttribution: true }}
            onNodeClick={(_e, node) => {
              setMenu(null);
              onSelect(node.id as NpcId);
            }}
            onNodeDragStop={(_e, node) => {
              onNodePositionCommit(node.id as NpcId, node.position.x, node.position.y);
            }}
            onEdgeClick={(e, edge) => {
              e.stopPropagation();
              setMenu(null);
              const sourceId = (edge.data as NpcEdgeData | undefined)?.sourceNpcId ?? (edge.source as NpcId);
              onSelect(sourceId);
            }}
            onEdgeContextMenu={(e, edge) => {
              e.preventDefault();
              e.stopPropagation();
              const relationId =
                (edge.data as NpcEdgeData | undefined)?.relationId ?? (edge.id as NpcRelationId);
              openEdgeMenu(relationId, e.clientX, e.clientY);
            }}
            onPaneClick={() => setMenu(null)}
            onPaneContextMenu={(e) => {
              e.preventDefault();
              setMenu(null);
            }}
          >
            <Background gap={18} size={1} color="#27272a" />
            <FilterToolbar
              ui={graphUi}
              groups={npcGroups}
              value={graphFilter}
              onChange={onGraphFilterChange}
            />
            <ZoomToolbar ui={graphUi} />
          </ReactFlow>
          {menu && menuPosition
            ? createPortal(
                <>
                  <button
                    type="button"
                    aria-label="close"
                    className={styles.menuBackdrop}
                    onClick={() => setMenu(null)}
                    onContextMenu={(e) => {
                      e.preventDefault();
                      setMenu(null);
                    }}
                  />
                  <div
                    className={styles.menu}
                    style={{ left: menuPosition.left, top: menuPosition.top }}
                    data-npc-edge-menu="1"
                    onMouseDown={(e) => e.stopPropagation()}
                  >
                    <button
                      type="button"
                      className={styles.menuItem}
                      onClick={() => {
                        onEditRelation(menu.relationId);
                        setMenu(null);
                      }}
                    >
                      {graphUi.editRelation}
                    </button>
                    <button
                      type="button"
                      className={[styles.menuItem, styles.menuItemDanger].join(' ')}
                      onClick={() => {
                        onDeleteRelation(menu.relationId);
                        setMenu(null);
                      }}
                    >
                      {graphUi.deleteRelation}
                    </button>
                  </div>
                </>,
                document.body,
              )
            : null}
        </div>
      </SelectSourceNpcContext.Provider>
    </OpenEdgeMenuContext.Provider>
  );
}

export function NpcGraph(props: NpcGraphProps) {
  return (
    <ReactFlowProvider>
      <NpcGraphInner {...props} />
    </ReactFlowProvider>
  );
}
