import type { Node, Edge } from '@xyflow/react';
import { MarkerType } from '@xyflow/react';

// ─── Types ──────────────────────────────────────────────────────

export interface ScenarioJson {
  id: string;
  version: number;
  name: string;
  startNodeId: string;
  variables: Record<string, { type: string; default: unknown }>;
  nodes: Record<string, ScenarioNodeJson>;
}

export interface ScenarioNodeJson {
  id: string;
  type: string;
  data: Record<string, unknown>;
  next?: string | null;
}

// ─── Layout Constants (n8n style) ───────────────────────────────

const NODE_WIDTH = 220;
const VERTICAL_GAP = 60;
const HORIZONTAL_GAP = 300;
const GRID_SIZE = 20;

const NODE_HEIGHTS: Record<string, number> = {
  message: 140,
  action: 100,
  condition: 110,
  input: 110,
  delay: 100,
  referral: 140,
};

function getNodeHeight(node: ScenarioNodeJson): number {
  const base = NODE_HEIGHTS[node.type] ?? 80;
  // Message nodes with buttons are taller
  if (node.type === 'message') {
    const buttons = (node.data.buttons ?? []) as unknown[];
    if (buttons.length > 0) return base + 30;
  }
  return base;
}

function snapToGrid(val: number): number {
  return Math.round(val / GRID_SIZE) * GRID_SIZE;
}

// ─── Edge helpers ──────────────────────────────────────────────

const EDGE_COLOR = '#29b6f6';

const edgeMarker = {
  type: MarkerType.ArrowClosed as const,
  width: 14,
  height: 14,
  color: EDGE_COLOR,
};

function mkEdge(
  id: string,
  source: string,
  target: string,
  _color = EDGE_COLOR,
  opts?: { sourceHandle?: string; dash?: boolean },
): Edge {
  return {
    id,
    source,
    sourceHandle: opts?.sourceHandle ?? 'source-bottom',
    target,
    targetHandle: 'target-top',
    type: 'default',
    style: {
      strokeWidth: 2,
      stroke: EDGE_COLOR,
      ...(opts?.dash ? { strokeDasharray: '6,3' } : {}),
    },
    markerEnd: edgeMarker,
  };
}

// ─── Get all targets from a node (for layout) ──────────────────

function getNodeTargets(node: ScenarioNodeJson): string[] {
  const targets: string[] = [];

  if (node.type === 'message') {
    const buttons = (node.data.buttons ?? []) as Array<{
      action: string;
      target?: string;
    }>;
    for (const btn of buttons) {
      if (btn.action === 'goto' && btn.target) {
        if (!targets.includes(btn.target)) targets.push(btn.target);
      }
    }
  }

  if (node.type === 'condition') {
    const rules = (node.data.rules ?? []) as Array<{ target: string }>;
    for (const rule of rules) {
      if (rule.target && !targets.includes(rule.target)) targets.push(rule.target);
    }
    const def = node.data.default as string | undefined;
    if (def && !targets.includes(def)) targets.push(def);
  }

  if (node.type === 'referral') {
    const reach = node.data.onReach as string | undefined;
    const friend = node.data.friendStartNode as string | undefined;
    if (reach && !targets.includes(reach)) targets.push(reach);
    if (friend && !targets.includes(friend)) targets.push(friend);
  }

  return targets;
}

// ─── Auto-layout algorithm ─────────────────────────────────────

function autoLayout(scenario: ScenarioJson): Map<string, { x: number; y: number }> {
  const positions = new Map<string, { x: number; y: number }>();
  const placed = new Set<string>();

  // 1. Main column: follow next chain from startNode
  let y = 0;
  let currentId: string | undefined | null = scenario.startNodeId;
  const mainColumn: string[] = [];

  while (currentId && !placed.has(currentId) && scenario.nodes[currentId]) {
    mainColumn.push(currentId);
    placed.add(currentId);
    positions.set(currentId, { x: snapToGrid(0), y: snapToGrid(y) });
    y += getNodeHeight(scenario.nodes[currentId]!) + VERTICAL_GAP;
    currentId = scenario.nodes[currentId]!.next;
  }

  // 2. BFS: collect unplaced nodes reachable from main column
  let colIndex = 1;
  let queue = [...mainColumn];

  while (queue.length > 0) {
    const nextQueue: string[] = [];
    const rightCol: string[] = [];
    const leftCol: string[] = [];

    for (const srcId of queue) {
      const srcNode = scenario.nodes[srcId];
      if (!srcNode) continue;
      const targets = getNodeTargets(srcNode);

      for (const tgt of targets) {
        if (!placed.has(tgt) && scenario.nodes[tgt]) {
          // Alternate right/left
          if (colIndex % 2 === 1) {
            rightCol.push(tgt);
          } else {
            leftCol.push(tgt);
          }
          placed.add(tgt);

          // Also follow this node's next chain
          let chainId = scenario.nodes[tgt].next;
          while (chainId && !placed.has(chainId) && scenario.nodes[chainId]) {
            if (colIndex % 2 === 1) {
              rightCol.push(chainId);
            } else {
              leftCol.push(chainId);
            }
            placed.add(chainId);
            chainId = scenario.nodes[chainId]!.next;
          }
        }
      }
    }

    // Place right column
    if (rightCol.length > 0) {
      let cy = 0;
      for (const nid of rightCol) {
        positions.set(nid, {
          x: snapToGrid(colIndex * HORIZONTAL_GAP),
          y: snapToGrid(cy),
        });
        cy += getNodeHeight(scenario.nodes[nid]!) + VERTICAL_GAP;
        nextQueue.push(nid);
      }
    }

    // Place left column
    if (leftCol.length > 0) {
      let cy = 0;
      for (const nid of leftCol) {
        positions.set(nid, {
          x: snapToGrid(-colIndex * HORIZONTAL_GAP),
          y: snapToGrid(cy),
        });
        cy += getNodeHeight(scenario.nodes[nid]!) + VERTICAL_GAP;
        nextQueue.push(nid);
      }
    }

    if (rightCol.length > 0 || leftCol.length > 0) colIndex++;
    queue = nextQueue;
  }

  // 3. Orphans: nodes not reachable from startNode
  let orphanX = (colIndex + 1) * HORIZONTAL_GAP;
  for (const id of Object.keys(scenario.nodes)) {
    if (!placed.has(id)) {
      positions.set(id, { x: snapToGrid(orphanX), y: 0 });
      orphanX += NODE_WIDTH + 60;
      placed.add(id);
    }
  }

  return positions;
}

// ─── Convert scenario JSON -> React Flow ────────────────────────

export function scenarioToFlow(
  scenario: ScenarioJson,
  savedPositions?: Record<string, { x: number; y: number }> | null,
): { nodes: Node[]; edges: Edge[] } {
  const nodes: Node[] = [];
  const edges: Edge[] = [];

  // Compute positions: use saved or auto-layout
  const layoutPositions = autoLayout(scenario);
  const useSaved = savedPositions && Object.keys(savedPositions).length > 0;

  function getPosition(id: string): { x: number; y: number } {
    if (useSaved && savedPositions[id]) {
      return savedPositions[id];
    }
    return layoutPositions.get(id) ?? { x: 0, y: 0 };
  }

  // Build nodes + edges
  for (const [id, nodeJson] of Object.entries(scenario.nodes)) {
    const isStart = id === scenario.startNodeId;
    const position = getPosition(id);

    if (nodeJson.type === 'message') {
      const buttons = (nodeJson.data.buttons ?? []) as Array<{
        text: string;
        action: string;
        target?: string;
        url?: string;
      }>;

      nodes.push({
        id,
        type: 'scenario-message',
        position,
        data: {
          ...nodeJson.data,
          nodeId: id,
          nodeType: 'message',
          next: nodeJson.next,
          isStart,
          buttons,
        },
      });

      // Edge: next (main flow)
      if (nodeJson.next) {
        edges.push(mkEdge(`${id}->next`, id, nodeJson.next));
      }

      // Edges: button goto targets
      buttons.forEach((btn, i) => {
        if (btn.action === 'goto' && btn.target && scenario.nodes[btn.target]) {
          edges.push(
            mkEdge(`${id}->btn${i}`, id, btn.target, EDGE_COLOR, {
              sourceHandle: `source-btn-${i}`,
            }),
          );
        }
      });
      continue;
    }

    if (nodeJson.type === 'condition') {
      nodes.push({
        id,
        type: 'scenario-condition',
        position,
        data: {
          ...nodeJson.data,
          nodeId: id,
          nodeType: 'condition',
          next: nodeJson.next,
          isStart,
        },
      });

      const rules = (nodeJson.data.rules ?? []) as Array<{ condition: string; target: string }>;
      rules.forEach((rule, i) => {
        if (rule.target && scenario.nodes[rule.target]) {
          edges.push(
            mkEdge(`${id}->rule${i}`, id, rule.target, EDGE_COLOR, {
              sourceHandle: `source-rule-${i}`,
            }),
          );
        }
      });

      const def = nodeJson.data.default as string | undefined;
      if (def && scenario.nodes[def]) {
        edges.push(mkEdge(`${id}->def`, id, def, EDGE_COLOR, { sourceHandle: 'source-default' }));
      }

      if (nodeJson.next) {
        edges.push(mkEdge(`${id}->next`, id, nodeJson.next));
      }
      continue;
    }

    if (nodeJson.type === 'referral') {
      nodes.push({
        id,
        type: 'scenario-referral',
        position,
        data: {
          ...nodeJson.data,
          nodeId: id,
          nodeType: 'referral',
          next: nodeJson.next,
          isStart,
        },
      });

      const onReach = nodeJson.data.onReach as string | undefined;
      const friendStart = nodeJson.data.friendStartNode as string | undefined;

      if (onReach && scenario.nodes[onReach]) {
        edges.push(
          mkEdge(`${id}->reach`, id, onReach, EDGE_COLOR, {
            sourceHandle: 'source-reach',
          }),
        );
      }
      if (friendStart && scenario.nodes[friendStart]) {
        edges.push(
          mkEdge(`${id}->friend`, id, friendStart, EDGE_COLOR, {
            sourceHandle: 'source-friend',
            dash: true,
          }),
        );
      }
      if (nodeJson.next) {
        edges.push(mkEdge(`${id}->next`, id, nodeJson.next));
      }
      continue;
    }

    // action, input, delay — generic
    nodes.push({
      id,
      type: `scenario-${nodeJson.type}`,
      position,
      data: {
        ...nodeJson.data,
        nodeId: id,
        nodeType: nodeJson.type,
        next: nodeJson.next,
        isStart,
      },
    });

    if (nodeJson.next) {
      edges.push(mkEdge(`${id}->next`, id, nodeJson.next));
    }
  }

  return { nodes, edges };
}

// ─── Convert React Flow -> scenario JSON ────────────────────────

export function flowToScenario(
  flowNodes: Node[],
  meta: {
    name: string;
    startNodeId: string;
    variables: Record<string, { type: string; default: unknown }>;
  },
): ScenarioJson {
  const nodes: Record<string, ScenarioNodeJson> = {};

  for (const fn of flowNodes) {
    const raw = fn.data;
    const nodeType = (raw.nodeType as string) ?? fn.type?.replace('scenario-', '') ?? 'message';
    const next = raw.next as string | undefined;

    const data: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(raw)) {
      if (!['nodeId', 'nodeType', 'isStart', 'next'].includes(k)) {
        data[k] = v;
      }
    }

    nodes[fn.id] = { id: fn.id, type: nodeType, data, next: next ?? null };
  }

  return {
    id: '',
    version: 1,
    name: meta.name,
    startNodeId: meta.startNodeId,
    variables: meta.variables,
    nodes,
  };
}

// ─── Extract positions ─────────────────────────────────────────

export function extractPositions(nodes: Node[]): Record<string, { x: number; y: number }> {
  const positions: Record<string, { x: number; y: number }> = {};
  for (const node of nodes) {
    positions[node.id] = {
      x: Math.round(node.position.x),
      y: Math.round(node.position.y),
    };
  }
  return positions;
}
