/**
 * Drop a 2-port insertable (pipe / duct) onto an existing wire:
 * A ──→ B  becomes  A ──→ pipe ──→ B
 *
 * The pipe is oriented along the edge (Modelica-style) so inlet faces the
 * upstream component — avoids React Flow arrow loops from wrong handle sides.
 */

import type { Edge, Node } from "@xyflow/react";
import type { EntropykNodeData } from "./configBuilder";
import { nodeSize } from "./componentMeta";
import { mediaForEdge, mediaForPort, type MediaKind } from "./mediaStyle";
import {
  centerNodePosition,
  orientationAlongEdge,
  type RotationDeg,
} from "./orientation";

export const PIPE_TYPES = new Set([
  "Pipe",
  "RefrigerantPipe",
  "WaterPipe",
  "AirDuct",
  "PipeWater",
  "PipeAir",
]);

/**
 * 2-port pass-through components that splice into an existing wire via
 * `insertOnEdge`. Includes the Pipe family (above) plus the `Probe`
 * measurement tap (calibration redesign — every measurement lives on a Probe
 * the user drops onto a line).
 */
export const INSERTABLE_ON_EDGE_TYPES: ReadonlySet<string> = new Set([
  ...PIPE_TYPES,
  "Probe",
]);

export function isPipeType(type: string): boolean {
  return PIPE_TYPES.has(type) || type.includes("Pipe") || type === "AirDuct";
}

/** True for any component the canvas should splice onto a wire on drop. */
export function isInsertableOnEdge(type: string): boolean {
  return isPipeType(type) || type === "Probe";
}

export function pipeMediaKind(type: string, params?: Record<string, unknown>): MediaKind {
  if (type === "WaterPipe" || type === "PipeWater") return "water";
  if (type === "AirDuct" || type === "PipeAir") return "air";
  if (type === "RefrigerantPipe") return "refrigerant";
  const fluid = params?.fluid;
  if (typeof fluid === "string") {
    const f = fluid.toLowerCase();
    if (f === "air" || f.startsWith("air")) return "air";
    if (f === "water" || f.includes("glycol") || f === "meg" || f === "brine") return "water";
  }
  return "refrigerant";
}

function distToSegment(
  p: { x: number; y: number },
  a: { x: number; y: number },
  b: { x: number; y: number },
): number {
  const dx = b.x - a.x;
  const dy = b.y - a.y;
  const len2 = dx * dx + dy * dy;
  if (len2 < 1e-9) return Math.hypot(p.x - a.x, p.y - a.y);
  let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
  t = Math.max(0, Math.min(1, t));
  const proj = { x: a.x + t * dx, y: a.y + t * dy };
  return Math.hypot(p.x - proj.x, p.y - proj.y);
}

function nodeCenter(node: Node<EntropykNodeData>): { x: number; y: number } {
  const size = nodeSize(node.data.type);
  const w = (node.measured?.width ?? node.width ?? size.w) as number;
  const h = (node.measured?.height ?? node.height ?? size.h) as number;
  return { x: node.position.x + w / 2, y: node.position.y + h / 2 };
}

export interface NearestEdgeHit {
  edge: Edge;
  distance: number;
  media: MediaKind;
  midpoint: { x: number; y: number };
  sourceCenter: { x: number; y: number };
  targetCenter: { x: number; y: number };
}

/** Find the closest edge to a flow-space point (optionally prefer matching media). */
export function findNearestEdge(
  point: { x: number; y: number },
  nodes: Node<EntropykNodeData>[],
  edges: Edge[],
  options?: { maxDistance?: number; preferMedia?: MediaKind },
): NearestEdgeHit | null {
  const maxDistance = options?.maxDistance ?? 56;
  const byId = new Map(nodes.map((n) => [n.id, n]));
  let best: NearestEdgeHit | null = null;
  let bestScore = Infinity;

  for (const edge of edges) {
    const s = byId.get(edge.source);
    const t = byId.get(edge.target);
    if (!s || !t) continue;
    const a = nodeCenter(s);
    const b = nodeCenter(t);
    const distance = distToSegment(point, a, b);
    if (distance > maxDistance) continue;
    const media = mediaForEdge(s, edge.sourceHandle, t, edge.targetHandle);
    const mediaPenalty =
      options?.preferMedia && media !== options.preferMedia && media !== "unknown"
        ? 40
        : 0;
    const score = distance + mediaPenalty;
    if (score < bestScore) {
      bestScore = score;
      best = {
        edge,
        distance,
        media,
        midpoint: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },
        sourceCenter: a,
        targetCenter: b,
      };
    }
  }
  return best;
}

export interface InsertPlan {
  node: Node<EntropykNodeData>;
  edgesToAdd: Edge[];
  edgeIdToRemove: string;
}

/** Build nodes/edges mutation to splice a 2-port component into an edge. */
export function planEdgeInsert(args: {
  type: string;
  id: string;
  name: string;
  position: { x: number; y: number };
  params: Record<string, number | string | boolean>;
  edge: Edge;
  circuit?: number;
  sourceCenter: { x: number; y: number };
  targetCenter: { x: number; y: number };
}): InsertPlan {
  const { type, id, name, params, edge } = args;
  const rotation: RotationDeg = orientationAlongEdge(args.sourceCenter, args.targetCenter);
  const size = nodeSize(type);
  // Center pipe on the edge midpoint (ignore caller position if it's off-center).
  const mid = {
    x: (args.sourceCenter.x + args.targetCenter.x) / 2,
    y: (args.sourceCenter.y + args.targetCenter.y) / 2,
  };
  const position = centerNodePosition(mid, size);

  const node: Node<EntropykNodeData> = {
    id,
    type: "entropykNode",
    position,
    data: {
      type,
      name,
      circuit: args.circuit ?? 0,
      rotation,
      flipH: false,
      flipV: false,
      params,
    },
  };

  // Orthogonal routing — smoother arrows than default when handles face correctly.
  const edgeType = "smoothstep";

  const edgesToAdd: Edge[] = [
    {
      id: `${edge.id}-a-${id.slice(0, 8)}`,
      source: edge.source,
      sourceHandle: edge.sourceHandle ?? undefined,
      target: id,
      targetHandle: "inlet",
      animated: false,
      type: edgeType,
    },
    {
      id: `${edge.id}-b-${id.slice(0, 8)}`,
      source: id,
      sourceHandle: "outlet",
      target: edge.target,
      targetHandle: edge.targetHandle ?? undefined,
      animated: false,
      type: edgeType,
    },
  ];

  return { node, edgesToAdd, edgeIdToRemove: edge.id };
}

export function mediaCompatible(pipe: MediaKind, edge: MediaKind): boolean {
  if (edge === "unknown" || pipe === "unknown") return true;
  return pipe === edge;
}

export function circuitFromEdge(
  edge: Edge,
  nodes: Node<EntropykNodeData>[],
): number {
  const s = nodes.find((n) => n.id === edge.source);
  return s?.data.circuit ?? 0;
}

export function validatePipeOnEdge(
  pipeType: string,
  params: Record<string, number | string | boolean>,
  edge: Edge,
  nodes: Node<EntropykNodeData>[],
): { ok: boolean; reason?: string } {
  const pipeKind = pipeMediaKind(pipeType, params);
  const byId = new Map(nodes.map((n) => [n.id, n]));
  const edgeKind = mediaForEdge(
    byId.get(edge.source),
    edge.sourceHandle,
    byId.get(edge.target),
    edge.targetHandle,
  );
  if (!mediaCompatible(pipeKind, edgeKind)) {
    return {
      ok: false,
      reason: `Ce conduit (${pipeKind}) ne correspond pas à la ligne (${edgeKind}).`,
    };
  }
  const probe = mediaForPort(
    {
      data: {
        type: pipeType,
        name: "p",
        circuit: 0,
        rotation: 0,
        flipH: false,
        flipV: false,
        params,
      },
    },
    "inlet",
  );
  if (!mediaCompatible(probe, edgeKind)) {
    return { ok: false, reason: `Milieu incompatible (${probe} vs ${edgeKind}).` };
  }
  return { ok: true };
}
