import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  ReactFlow,
  Background,
  Controls,
  MiniMap,
  useNodesState,
  useEdgesState,
  type Node,
  type Edge,
  type NodeProps,
  type Connection,
  Handle,
  Position,
} from '@xyflow/react';
import dagre from '@dagrejs/dagre';
import '@xyflow/react/dist/style.css';
import { Spin, Empty, Tag, Avatar, Modal, Select, message } from 'antd';
import { api } from '@/lib/api';
import type { GraphData, GraphNode as GNode, GraphEdge, ContactSource, ConnectionType } from '@/types/api';
import {
  CONTACT_SOURCE_LABELS,
  CONTACT_SOURCE_COLORS,
  CONNECTION_TYPE_LABELS,
} from '@/types/api';

const SOURCE_AVATAR_COLORS: Record<ContactSource, string> = {
  USER: '#1890ff',
  ADDRESS_OCCUPANT: '#52c41a',
  CAMPAIGN_SENDER: '#722ed1',
  SHIFT_SIGNUP: '#13c2c2',
  SMS_CONTACT: '#fa8c16',
  DONATION: '#fadb14',
  MANUAL: '#8c8c8c',
};

function getInitials(name: string): string {
  if (!name) return '?';
  const parts = name.trim().split(/\s+/);
  const first = parts[0];
  const last = parts[parts.length - 1];
  if (parts.length >= 2 && first && last) {
    return ((first[0] || '') + (last[0] || '')).toUpperCase();
  }
  return (name[0] || '?').toUpperCase();
}

const NODE_WIDTH = 180;
const NODE_HEIGHT = 70;

/** Custom node component for the graph */
function PersonNode({ data }: NodeProps) {
  const nodeData = data as { displayName: string; source: ContactSource; email: string | null };
  return (
    <div
      style={{
        padding: '8px 12px',
        borderRadius: 8,
        background: 'rgba(30, 30, 50, 0.95)',
        border: `2px solid ${SOURCE_AVATAR_COLORS[nodeData.source] || '#8c8c8c'}`,
        minWidth: NODE_WIDTH,
        textAlign: 'center',
        cursor: 'pointer',
      }}
    >
      <Handle type="target" position={Position.Top} style={{ background: '#555', width: 6, height: 6 }} />
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <Avatar
          size={28}
          style={{ backgroundColor: SOURCE_AVATAR_COLORS[nodeData.source], fontWeight: 600, fontSize: 11, flexShrink: 0 }}
        >
          {getInitials(nodeData.displayName)}
        </Avatar>
        <div style={{ minWidth: 0, textAlign: 'left' }}>
          <div
            style={{
              fontSize: 12,
              fontWeight: 600,
              color: '#fff',
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              maxWidth: 120,
            }}
          >
            {nodeData.displayName}
          </div>
          <Tag
            color={CONTACT_SOURCE_COLORS[nodeData.source]}
            style={{ fontSize: 9, margin: 0, lineHeight: '14px', padding: '0 4px' }}
          >
            {CONTACT_SOURCE_LABELS[nodeData.source]}
          </Tag>
        </div>
      </div>
      <Handle type="source" position={Position.Bottom} style={{ background: '#555', width: 6, height: 6 }} />
    </div>
  );
}

const nodeTypes = { person: PersonNode };

const connectionTypeOptions = Object.entries(CONNECTION_TYPE_LABELS).map(([value, label]) => ({
  value,
  label,
}));

function applyDagreLayout(nodes: Node[], edges: Edge[]): Node[] {
  // Separate connected nodes (have at least one edge) from isolated ones
  const connectedIds = new Set<string>();
  for (const e of edges) {
    connectedIds.add(e.source);
    connectedIds.add(e.target);
  }

  const connectedNodes = nodes.filter((n) => connectedIds.has(n.id));
  const isolatedNodes = nodes.filter((n) => !connectedIds.has(n.id));

  // Layout connected nodes with dagre (tree layout)
  let dagreMaxY = 0;
  if (connectedNodes.length > 0) {
    const g = new dagre.graphlib.Graph();
    g.setDefaultEdgeLabel(() => ({}));
    g.setGraph({ rankdir: 'TB', nodesep: 80, ranksep: 100 });

    connectedNodes.forEach((node) => {
      g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
    });
    edges.forEach((edge) => {
      g.setEdge(edge.source, edge.target);
    });
    dagre.layout(g);

    for (const node of connectedNodes) {
      const pos = g.node(node.id);
      node.position = { x: pos.x - NODE_WIDTH / 2, y: pos.y - NODE_HEIGHT / 2 };
      dagreMaxY = Math.max(dagreMaxY, pos.y + NODE_HEIGHT / 2);
    }
  }

  // Arrange isolated nodes in a grid below the connected graph
  if (isolatedNodes.length > 0) {
    const GAP_X = NODE_WIDTH + 30;
    const GAP_Y = NODE_HEIGHT + 30;
    const cols = Math.max(1, Math.ceil(Math.sqrt(isolatedNodes.length)));
    const startY = connectedNodes.length > 0 ? dagreMaxY + 80 : 0;
    // Center the grid horizontally
    const gridWidth = cols * GAP_X;
    const startX = -gridWidth / 2;

    isolatedNodes.forEach((node, i) => {
      const col = i % cols;
      const row = Math.floor(i / cols);
      node.position = {
        x: startX + col * GAP_X,
        y: startY + row * GAP_Y,
      };
    });
  }

  return [...connectedNodes, ...isolatedNodes];
}

interface ConnectionGraphProps {
  onNodeClick: (personId: string) => void;
  filters?: { source?: string; tag?: string; minScore?: number };
}

export default function ConnectionGraph({ onNodeClick, filters }: ConnectionGraphProps) {
  const [loading, setLoading] = useState(true);
  const [graphData, setGraphData] = useState<GraphData | null>(null);
  const [nodes, setNodes, onNodesChange] = useNodesState([] as Node[]);
  const [edges, setEdges, onEdgesChange] = useEdgesState([] as Edge[]);

  // Connection creation state
  const [pendingConnection, setPendingConnection] = useState<Connection | null>(null);
  const [connectionType, setConnectionType] = useState<ConnectionType>('HOUSEHOLD');
  const [saving, setSaving] = useState(false);

  // Build a lookup from node ID → graph node data (for contactId resolution)
  const nodeDataMap = useMemo(() => {
    const map = new Map<string, GNode>();
    if (graphData) {
      for (const gn of graphData.nodes) {
        map.set(gn.id, gn);
      }
    }
    return map;
  }, [graphData]);

  const fetchGraph = useCallback(async () => {
    setLoading(true);
    try {
      const { data } = await api.get<GraphData>('/people/graph', {
        params: filters,
      });
      setGraphData(data);
    } catch {
      setGraphData(null);
    } finally {
      setLoading(false);
    }
  }, [filters]);

  // Keep fetchGraph in a ref so onConnect and handleCreateConnection always have the latest
  const fetchGraphRef = useRef(fetchGraph);
  fetchGraphRef.current = fetchGraph;

  useEffect(() => {
    fetchGraph();
  }, [fetchGraph]);

  useEffect(() => {
    if (!graphData || graphData.nodes.length === 0) {
      setNodes([]);
      setEdges([]);
      return;
    }

    const flowNodes: Node[] = graphData.nodes.map((gn: GNode) => ({
      id: gn.id,
      type: 'person',
      position: { x: 0, y: 0 },
      data: {
        displayName: gn.displayName,
        source: gn.source,
        email: gn.email,
      },
    }));

    const flowEdges: Edge[] = graphData.edges.map((ge: GraphEdge) => ({
      id: ge.id,
      source: ge.source,
      target: ge.target,
      label: ge.label || CONNECTION_TYPE_LABELS[ge.type] || ge.type,
      animated: ge.isBidirectional,
      style: { stroke: '#8c8c8c' },
      labelStyle: { fontSize: 10, fill: '#aaa' },
      labelBgStyle: { fill: 'rgba(20,20,40,0.9)' },
      labelBgPadding: [4, 2] as [number, number],
      markerEnd: ge.isBidirectional ? undefined : { type: 'arrowclosed' as const, color: '#8c8c8c' },
    }));

    const layoutNodes = applyDagreLayout(flowNodes, flowEdges);
    setNodes(layoutNodes);
    setEdges(flowEdges);
  }, [graphData]); // eslint-disable-line react-hooks/exhaustive-deps

  const handleNodeClick = useCallback(
    (_event: React.MouseEvent, node: Node) => {
      onNodeClick(node.id);
    },
    [onNodeClick],
  );

  /** When user finishes dragging a connection between two handles */
  const handleConnect = useCallback((connection: Connection) => {
    if (!connection.source || !connection.target) return;
    if (connection.source === connection.target) return;
    setPendingConnection(connection);
    setConnectionType('HOUSEHOLD');
  }, []);

  /** Persist the connection via API */
  const handleCreateConnection = useCallback(async () => {
    if (!pendingConnection?.source || !pendingConnection?.target) return;

    const sourceNode = nodeDataMap.get(pendingConnection.source);
    const targetNode = nodeDataMap.get(pendingConnection.target);
    if (!sourceNode || !targetNode) return;

    // Determine which end is a Contact — the API requires a contactId as the "from" side
    let fromContactId = sourceNode.contactId;
    let toId = pendingConnection.target;

    // If source isn't a contact but target is, swap
    if (!fromContactId && targetNode.contactId) {
      fromContactId = targetNode.contactId;
      toId = pendingConnection.source;
    }

    if (!fromContactId) {
      message.warning('At least one person must be a managed contact to create a connection.');
      setPendingConnection(null);
      return;
    }

    // Parse the target unified ID (e.g. "contact:abc123" or "user:xyz")
    const colonIdx = toId.indexOf(':');
    const toPersonType = colonIdx >= 0 ? toId.substring(0, colonIdx) : 'contact';
    const toPersonId = colonIdx >= 0 ? toId.substring(colonIdx + 1) : toId;

    setSaving(true);
    try {
      await api.post(`/people/contacts/${fromContactId}/connections`, {
        toPersonType,
        toPersonId,
        type: connectionType,
        isBidirectional: true,
      });
      message.success('Connection created');
      fetchGraphRef.current();
    } catch {
      message.error('Failed to create connection');
    } finally {
      setSaving(false);
      setPendingConnection(null);
    }
  }, [pendingConnection, nodeDataMap, connectionType]);

  if (loading) {
    return (
      <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
        <Spin size="large" />
      </div>
    );
  }

  if (!graphData || graphData.nodes.length === 0) {
    return (
      <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
        <Empty
          description="No people found. Add contacts or register users to see the graph."
          image={Empty.PRESENTED_IMAGE_SIMPLE}
        />
      </div>
    );
  }

  return (
    <div style={{ width: '100%', height: '100%' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onNodeClick={handleNodeClick}
        onConnect={handleConnect}
        nodeTypes={nodeTypes}
        fitView
        minZoom={0.2}
        maxZoom={2}
        connectionLineStyle={{ stroke: '#8c8c8c', strokeWidth: 2 }}
        proOptions={{ hideAttribution: true }}
      >
        <Background color="rgba(255,255,255,0.05)" gap={20} />
        <Controls
          showInteractive={false}
          style={{ background: 'rgba(30,30,50,0.9)', borderRadius: 6, border: '1px solid rgba(255,255,255,0.1)' }}
        />
        <MiniMap
          nodeColor={(node) => {
            const src = (node.data as { source?: ContactSource })?.source;
            return src ? SOURCE_AVATAR_COLORS[src] : '#8c8c8c';
          }}
          maskColor="rgba(0,0,0,0.6)"
          style={{ background: 'rgba(20,20,40,0.9)', borderRadius: 6 }}
        />
      </ReactFlow>

      {/* Connection type picker modal */}
      <Modal
        title="Create Connection"
        open={!!pendingConnection}
        onOk={handleCreateConnection}
        onCancel={() => setPendingConnection(null)}
        confirmLoading={saving}
        okText="Create"
        width={360}
      >
        <div style={{ marginBottom: 8 }}>
          <label style={{ fontSize: 13, fontWeight: 500 }}>Connection Type</label>
        </div>
        <Select
          value={connectionType}
          onChange={setConnectionType}
          options={connectionTypeOptions}
          style={{ width: '100%' }}
        />
      </Modal>
    </div>
  );
}
