import { useContext, useEffect, useRef, useState } from 'react';
import * as React from 'react';
import { Sankey, Tooltip } from 'recharts';
import { useNavigate } from 'react-router-dom';
import DJClientContext from '../../providers/djclient';
import LoadingIcon from '../../icons/LoadingIcon';

// Match badge background colors from index.css .node_type__* classes
const TYPE_COLORS = {
  source: '#ccf7e5',
  transform: '#ccefff',
  metric: '#fad7dd',
  dimension: '#ffefd0',
  cube: '#dbafff',
};

const TYPE_BORDER_COLORS = {
  source: '#00b368',
  transform: '#0063b4',
  metric: '#a2283e',
  dimension: '#a96621',
  cube: '#580076',
};

const TYPE_LAYER_ORDER = ['source', 'transform', 'dimension', 'metric', 'cube'];

// Returns a slightly darker version of the pastel fill for the node border
const DARKER_FILL = {
  source: '#8de8c3',
  transform: '#8ed6f7',
  metric: '#f0a3b0',
  dimension: '#ffd08a',
  cube: '#bc80f5',
};

function SankeyNode({
  x,
  y,
  width,
  height,
  payload,
  currentNodeName,
  rightmostType,
  onNavigate,
  hoveredNodeName,
  onNodeHover,
}) {
  if (!payload) return null;
  if (payload.type === 'phantom') return <g />;
  const isHovered = hoveredNodeName === payload.name;
  const isDimmed = hoveredNodeName && !isHovered;
  const baseFill = TYPE_COLORS[payload.type] ?? '#f1f5f9';
  const hoverFill = DARKER_FILL[payload.type] ?? '#cbd5e1';
  const borderColor = DARKER_FILL[payload.type] ?? '#cbd5e1';
  const isCurrent = payload.name === currentNodeName;
  const label = (payload.display_name || payload.name || '').split('.').pop();
  const isRightmost = payload.type === rightmostType;
  const labelX = isRightmost ? x + width + 8 : x - 8;
  const labelAnchor = isRightmost ? 'start' : 'end';

  return (
    <g
      style={{
        cursor: payload.name ? 'pointer' : 'default',
        opacity: isDimmed ? 0.55 : 1,
        transition: 'opacity 0.15s',
      }}
      onMouseEnter={() => onNodeHover && onNodeHover(payload.name)}
      onMouseLeave={() => onNodeHover && onNodeHover(null)}
      onClick={() =>
        payload.name && onNavigate && onNavigate('/nodes/' + payload.name)
      }
    >
      <rect
        x={x}
        y={y}
        width={width}
        height={height}
        fill={isHovered ? hoverFill : baseFill}
        fillOpacity={1}
        stroke={isCurrent || isHovered ? borderColor : 'none'}
        strokeWidth={1.5}
        rx={2}
      />
      <text
        x={labelX}
        y={y + height / 2}
        textAnchor={labelAnchor}
        dominantBaseline="middle"
        fontSize={11}
        fill="#374151"
        style={{ userSelect: 'none', pointerEvents: 'none' }}
      >
        {label}
      </text>
    </g>
  );
}

function SankeyLink({
  sourceX,
  targetX,
  sourceY,
  targetY,
  sourceControlX,
  targetControlX,
  linkWidth,
  index,
  payload,
  hoveredNodeName,
}) {
  const [linkHovered, setLinkHovered] = useState(false);
  const hw = Math.max(linkWidth, 1);
  const d = `
    M${sourceX},${sourceY - hw / 2}
    C${sourceControlX},${sourceY - hw / 2} ${targetControlX},${
    targetY - hw / 2
  } ${targetX},${targetY - hw / 2}
    L${targetX},${targetY + hw / 2}
    C${targetControlX},${targetY + hw / 2} ${sourceControlX},${
    sourceY + hw / 2
  } ${sourceX},${sourceY + hw / 2}
    Z
  `;
  const targetType = payload?.target?.type;
  if (targetType === 'phantom') return <g />;
  const sourceType = payload?.source?.type;
  const fromColor = TYPE_COLORS[sourceType] ?? '#e2e8f0';
  const toColor = TYPE_COLORS[targetType] ?? '#e2e8f0';
  const gradientId = `link-grad-${index}`;

  const isConnected =
    hoveredNodeName &&
    (payload?.source?.name === hoveredNodeName ||
      payload?.target?.name === hoveredNodeName);
  const opacity = linkHovered
    ? 0.85
    : hoveredNodeName
    ? isConnected
      ? 0.8
      : 0.15
    : 0.38;

  return (
    <g>
      <defs>
        <linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
          <stop offset="0%" stopColor={fromColor} stopOpacity={opacity} />
          <stop offset="100%" stopColor={toColor} stopOpacity={opacity} />
        </linearGradient>
      </defs>
      <path
        d={d}
        fill={`url(#${gradientId})`}
        stroke="none"
        style={{ cursor: 'pointer', transition: 'opacity 0.15s' }}
        onMouseEnter={() => setLinkHovered(true)}
        onMouseLeave={() => setLinkHovered(false)}
      />
    </g>
  );
}

export default function NodeDataFlowTab({ djNode }) {
  const djClient = useContext(DJClientContext).DataJunctionAPI;
  const navigate = useNavigate();
  const [sankeyData, setSankeyData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [containerWidth, setContainerWidth] = useState(0);
  const [hoveredNodeName, setHoveredNodeName] = useState(null);
  const containerRef = useRef(null);

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    // Read initial width synchronously so the chart fills the container on first paint
    setContainerWidth(el.getBoundingClientRect().width);
    const observer = new ResizeObserver(entries => {
      setContainerWidth(entries[0].contentRect.width);
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    if (!djNode?.name) return;
    setLoading(true);
    Promise.all([
      djClient.upstreamsGQL(djNode.name),
      djClient.downstreamsGQL(djNode.name),
    ])
      .then(async ([upstreamNodes, downstreamNodes]) => {
        const normalize = n => ({ ...n, type: n.type?.toLowerCase() });
        const upstream = (upstreamNodes || []).map(normalize);
        const downstream = (downstreamNodes || []).map(normalize);

        // Fetch downstream cubes in one batch call
        const cubeNames = downstream
          .filter(n => n.type === 'cube')
          .map(n => n.name);
        const cubeNodes = await djClient.findCubesWithMetrics(cubeNames);

        const nonCubeDownstreams = downstream.filter(n => n.type !== 'cube');
        const allNodes = [
          djNode,
          ...upstream,
          ...cubeNodes,
          ...nonCubeDownstreams,
        ];

        // Deduplicate nodes
        const seen = new Set();
        const nodes = [];
        allNodes.forEach(n => {
          if (n && !seen.has(n.name)) {
            seen.add(n.name);
            nodes.push(n);
          }
        });

        // Sort so seed node is first within its type group — it will appear at the top
        // of its column when sort={false} is used.
        const seedName = djNode?.name;
        nodes.sort((a, b) => {
          const aLayer = TYPE_LAYER_ORDER.indexOf(a.type);
          const bLayer = TYPE_LAYER_ORDER.indexOf(b.type);
          if (aLayer !== bLayer) return aLayer - bLayer;
          if (a.name === seedName) return -1;
          if (b.name === seedName) return 1;
          return 0;
        });

        const nodeIndex = {};
        nodes.forEach((n, i) => {
          nodeIndex[n.name] = i;
        });

        const links = [];
        nodes.forEach(node => {
          (node.current?.parents || node.parents || []).forEach(parent => {
            if (
              parent.name &&
              nodeIndex[parent.name] !== undefined &&
              nodeIndex[node.name] !== undefined
            ) {
              links.push({
                source: nodeIndex[parent.name],
                target: nodeIndex[node.name],
                value: 1,
              });
            }
          });
        });

        // recharts forces any node with no outgoing links to maxDepth (the cube column).
        // Fix: give free-floating metrics a tiny phantom outgoing link so they stay in
        // the metric column. The phantom node renders as invisible.
        const hasOutgoing = new Set(links.map(l => l.source));
        const phantomLinks = [];
        nodes.forEach((node, i) => {
          if (node.type === 'metric' && !hasOutgoing.has(i)) {
            phantomLinks.push({ source: i, target: nodes.length, value: 0.01 });
          }
        });
        if (phantomLinks.length > 0) {
          nodes.push({
            name: '__phantom__',
            type: 'phantom',
            display_name: '',
          });
          links.push(...phantomLinks);
        }

        setSankeyData({ nodes, links });
        setLoading(false);
      })
      .catch(err => {
        console.error(err);
        setLoading(false);
      });
  }, [djNode, djClient]);

  // Always render the sentinel div so containerRef is mounted before data loads
  if (loading || !sankeyData || sankeyData.links.length === 0) {
    return (
      <div>
        {/* Sentinel must be in DOM so ResizeObserver fires even during loading */}
        <div ref={containerRef} style={{ width: '100%', height: 0 }} />
        {loading ? (
          <div style={{ padding: '2rem' }}>
            <LoadingIcon />
          </div>
        ) : (
          <div style={{ padding: '2rem', color: '#64748b', fontSize: 14 }}>
            No data flow relationships found for this node.
          </div>
        )}
      </div>
    );
  }

  const counts = sankeyData.nodes.reduce((acc, n) => {
    if (n.type !== 'phantom') acc[n.type] = (acc[n.type] || 0) + 1;
    return acc;
  }, {});

  const summaryParts = TYPE_LAYER_ORDER.filter(t => counts[t]).map(
    t => `${counts[t]} ${t}${counts[t] > 1 ? 's' : ''}`,
  );

  // Height driven by the tallest column, not total node count
  const colDepths = {};
  sankeyData.nodes.forEach(n => {
    if (n.type !== 'phantom') {
      const col = TYPE_LAYER_ORDER.indexOf(n.type);
      colDepths[col] = (colDepths[col] || 0) + 1;
    }
  });
  const maxColNodes = Math.max(...Object.values(colDepths), 1);
  // Derive chart height so each node is at least MIN_NODE_HEIGHT px tall.
  // recharts fills: chartHeight - top - bottom = maxNodes * nodeHeight + (maxNodes - 1) * nodePadding
  // Solving for chartHeight given a desired minimum nodeHeight:
  const MIN_NODE_HEIGHT = 24;
  const NODE_PADDING = 12;
  const MARGIN_V = 20; // top + bottom margin
  const chartHeight = Math.max(
    280,
    maxColNodes * (MIN_NODE_HEIGHT + NODE_PADDING) - NODE_PADDING + MARGIN_V,
  );

  // Rightmost column determines label side (right); everything else labels left
  const rightmostType =
    TYPE_LAYER_ORDER.slice()
      .reverse()
      .find(t => counts[t]) ?? 'metric';

  // Measure label widths per side to set margins
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  ctx.font = '11px system-ui, sans-serif';
  const measureLabel = n =>
    ctx.measureText((n.display_name || n.name || '').split('.').pop()).width;
  const rightNodes = sankeyData.nodes.filter(n => n.type === rightmostType);
  const leftNodes = sankeyData.nodes.filter(
    n => n.type !== rightmostType && n.type !== 'phantom',
  );
  const rightMargin =
    Math.ceil(Math.max(0, ...rightNodes.map(measureLabel))) + 16;
  const leftMargin =
    Math.ceil(Math.max(0, ...leftNodes.map(measureLabel))) + 16;

  const nodeEl = (
    <SankeyNode
      currentNodeName={djNode?.name}
      rightmostType={rightmostType}
      onNavigate={navigate}
      hoveredNodeName={hoveredNodeName}
      onNodeHover={setHoveredNodeName}
    />
  );
  const linkEl = <SankeyLink hoveredNodeName={hoveredNodeName} />;

  return (
    <div style={{ padding: '1.5rem 0.75rem' }}>
      <div
        style={{
          fontSize: 13,
          fontWeight: 700,
          color: '#374151',
          textTransform: 'uppercase',
          letterSpacing: '0.06em',
          marginBottom: '0.75rem',
        }}
      >
        {summaryParts.join(' → ')}
      </div>
      <div
        style={{
          display: 'flex',
          gap: '1rem',
          marginBottom: '1.25rem',
          flexWrap: 'wrap',
        }}
      >
        {TYPE_LAYER_ORDER.filter(t => counts[t]).map(type => (
          <span
            key={type}
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: 5,
              fontSize: 12,
              color: '#64748b',
            }}
          >
            <span
              style={{
                width: 12,
                height: 12,
                background: TYPE_COLORS[type],
                border: `1.5px solid ${TYPE_BORDER_COLORS[type]}`,
                borderRadius: 2,
                display: 'inline-block',
              }}
            />
            {type}
          </span>
        ))}
      </div>
      {/* Sentinel div: measures true available width without the Sankey influencing it */}
      <div ref={containerRef} style={{ width: '100%', height: 0 }} />
      {containerWidth > 0 && (
        <Sankey
          width={containerWidth}
          height={chartHeight}
          data={sankeyData}
          nodePadding={NODE_PADDING}
          nodeWidth={16}
          margin={{ top: 10, right: rightMargin, bottom: 10, left: leftMargin }}
          node={nodeEl}
          link={linkEl}
          sort={false}
        >
          <Tooltip
            content={({ active, payload }) => {
              if (!active || !payload?.length) return null;
              const item = payload[0]?.payload;
              if (!item) return null;
              const name =
                item.name ||
                `${item.source?.display_name || item.source?.name} → ${
                  item.target?.display_name || item.target?.name
                }`;
              return (
                <div
                  style={{
                    background: 'white',
                    border: '1px solid #e2e8f0',
                    padding: '8px 12px',
                    borderRadius: 4,
                    fontSize: 12,
                    boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
                  }}
                >
                  <div style={{ fontWeight: 600 }}>{name}</div>
                  {item.type && (
                    <div
                      style={{
                        color: TYPE_COLORS[item.type] || '#64748b',
                        marginTop: 2,
                      }}
                    >
                      {item.type}
                    </div>
                  )}
                </div>
              );
            }}
          />
        </Sankey>
      )}
    </div>
  );
}
