"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  Background,
  BackgroundVariant,
  Controls,
  ReactFlow,
  MarkerType,
  type Node,
  type Edge,
  type Connection,
  type ReactFlowInstance,
  type OnNodesChange,
  type OnEdgesChange,
  type OnConnect,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { FlipHorizontal2, FlipVertical2, RotateCw } from "lucide-react";

import { GRID_SIZE, useDiagramStore, type EntropykNodeData } from "@/store/diagramStore";
import { portRole } from "@/lib/componentMeta";
import {
  MEDIA_COLOR,
  MEDIA_LABEL,
  mediaColor,
  mediaForEdge,
  type MediaKind,
} from "@/lib/mediaStyle";
import {
  findNearestEdge,
  isInsertableOnEdge,
  pipeMediaKind,
} from "@/lib/edgeInsert";
import { defaultParams } from "@/lib/componentMeta";
import EntropykNode from "./EntropykNode";
import NodeContextMenu, { type NodeContextMenuState } from "./NodeContextMenu";

const nodeTypes = { entropykNode: EntropykNode };

/** Slightly darken / lighten a hex colour for HP vs LP within the same medium. */
function shadeHex(hex: string, amount: number): string {
  const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  if (!m) return hex;
  const ch = (i: number) =>
    Math.max(0, Math.min(255, parseInt(m[i], 16) + amount))
      .toString(16)
      .padStart(2, "0");
  return `#${ch(1)}${ch(2)}${ch(3)}`;
}

interface CanvasProps {
  onOpenModuleEditor?: (moduleName: string) => void;
}

export default function Canvas({ onOpenModuleEditor }: CanvasProps) {
  const rfInstance = useRef<ReactFlowInstance<Node<EntropykNodeData>, Edge> | null>(null);
  const [ctxMenu, setCtxMenu] = useState<NodeContextMenuState | null>(null);
  const [connectionHint, setConnectionHint] = useState<string | null>(null);
  const hintTimer = useRef<number | null>(null);

  const scheduleClear = () => {
    if (hintTimer.current) window.clearTimeout(hintTimer.current);
    hintTimer.current = window.setTimeout(() => setConnectionHint(null), 2500);
  };

  const {
    nodes,
    edges,
    result,
    snapToGrid,
    selectedNodeId,
    onNodesChange,
    onEdgesChange,
    onConnect,
    addComponent,
    insertOnEdge,
    setSelected,
    rotateNode,
    flipNodeH,
    flipNodeV,
    removeNode,
  } = useDiagramStore();

  const selectedNode = useMemo(
    () => nodes.find((n) => n.id === selectedNodeId) ?? null,
    [nodes, selectedNodeId],
  );

  const onInit = useCallback((instance: ReactFlowInstance<Node<EntropykNodeData>, Edge>) => {
    rfInstance.current = instance;
    instance.fitView({ padding: 0.25 });
  }, []);

  // Modelica/Dymola: R = rotate, H = flip horizontal, V = flip vertical.
  // Prefer canvas focus — skip only when typing text in a field.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      const target = e.target as HTMLElement | null;
      const tag = target?.tagName;
      const typingText =
        !!target &&
        (tag === "TEXTAREA" ||
          target.isContentEditable ||
          (tag === "INPUT" &&
            (target as HTMLInputElement).type !== "checkbox" &&
            (target as HTMLInputElement).type !== "radio" &&
            (target as HTMLInputElement).type !== "button"));
      // Allow R/H/V from number fields only with Alt — otherwise canvas after click.
      if (typingText && !e.altKey) return;
      if (!selectedNodeId) return;
      if (e.key === "r" || e.key === "R") {
        e.preventDefault();
        rotateNode(selectedNodeId, e.shiftKey ? -1 : 1);
      } else if (e.key === "h" || e.key === "H") {
        e.preventDefault();
        flipNodeH(selectedNodeId);
      } else if (e.key === "v" || e.key === "V") {
        e.preventDefault();
        flipNodeV(selectedNodeId);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [selectedNodeId, rotateNode, flipNodeH, flipNodeV]);

  const onDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = "move";
  }, []);

  const onDrop = useCallback(
    (e: React.DragEvent) => {
      e.preventDefault();
      const type = e.dataTransfer.getData("application/entropyk-type");
      if (!type || !rfInstance.current) return;
      const position = rfInstance.current.screenToFlowPosition({
        x: e.clientX,
        y: e.clientY,
      });

      // Module instance from the workspace palette.
      if (type.startsWith("ModuleInstance:")) {
        const moduleName = type.slice("ModuleInstance:".length);
        const portsStr = e.dataTransfer.getData("application/entropyk-ports");
        let module_ports: string[] = [];
        try {
          if (portsStr) module_ports = JSON.parse(portsStr);
        } catch {}
        addComponent("ModuleInstance", position, {
          module_name: moduleName,
          module_ports: JSON.stringify(module_ports),
        });
        return;
      }

      // Pipe / duct / Probe dropped on a wire → splice into the circuit
      // (Modelica-style). Probe is a zero-residual measurement tap that uses
      // the same insertOnEdge path.
      if (isInsertableOnEdge(type)) {
        // A Probe can tap ANY medium (refrigerant / water / air) — a sensor sits
        // wherever the physics is, so we don't filter by media for it. Pipes
        // still get media filtering so a water pipe doesn't splice into a
        // refrigerant line.
        const prefer = type === "Probe" ? undefined : pipeMediaKind(type, defaultParams(type));
        const hit = findNearestEdge(position, nodes, edges, {
          // Probes are small targets — be more forgiving on the drop distance.
          maxDistance: type === "Probe" ? 96 : 64,
          preferMedia: prefer,
        });
        if (hit) {
          const result = insertOnEdge(type, hit.midpoint, hit.edge.id);
          if (result.ok) return;
          // Media mismatch / splice failed: fall through to free placement.
        }
      }

      addComponent(type, position);
    },
    [addComponent, insertOnEdge, nodes, edges],
  );

  const isValidConnection = useCallback((c: Connection | Edge) => {
    if (!c.source || !c.target) return false;
    if (c.source === c.target) {
      setConnectionHint("Connexion refusée : même composant.");
      scheduleClear();
      return false;
    }
    const srcOk = !c.sourceHandle || portRole(c.sourceHandle) === "source";
    const tgtOk = !c.targetHandle || portRole(c.targetHandle) === "target";
    if (!srcOk || !tgtOk) {
      setConnectionHint("Connexion refusée : branche une sortie vers une entrée.");
      scheduleClear();
      return false;
    }
    setConnectionHint(null);
    return true;
  }, []);

  /**
   * Colour wires by Modelica/TIL medium (refrigerant / water / air).
   * After a solve, keep the medium hue and only shade HP vs LP slightly;
   * pressure is shown as a label.
   */
  const styledEdges = useMemo<Edge[]>(() => {
    const nodeById = new Map(nodes.map((n) => [n.id, n]));
    const nameById = new Map(nodes.map((n) => [n.id, n.data.name]));

    const pByPair = new Map<string, number>();
    let pMin = Infinity;
    let pMax = -Infinity;
    if (result?.state) {
      for (const s of result.state) {
        if (s.source && s.target && typeof s.pressure_pa === "number") {
          pByPair.set(`${s.source}→${s.target}`, s.pressure_pa);
          pMin = Math.min(pMin, s.pressure_pa);
          pMax = Math.max(pMax, s.pressure_pa);
        }
      }
    }
    const mid = (pMin + pMax) / 2;
    const solved = pByPair.size > 0 && pMax > pMin;

    return edges.map((e) => {
      const kind = mediaForEdge(
        nodeById.get(e.source),
        e.sourceHandle,
        nodeById.get(e.target),
        e.targetHandle,
      );
      let color = mediaColor(kind);
      let label: string | undefined;
      if (solved) {
        const p = pByPair.get(`${nameById.get(e.source)}→${nameById.get(e.target)}`);
        if (p !== undefined) {
          // Within one medium: darker = high pressure, lighter = low pressure.
          color = p >= mid ? shadeHex(color, -28) : shadeHex(color, 36);
          label = `${(p / 1e5).toFixed(1)} bar`;
        }
      }
      return {
        ...e,
        type: "smoothstep",
        animated: solved,
        style: { stroke: color, strokeWidth: solved ? 2.4 : 2 },
        markerEnd: { type: MarkerType.ArrowClosed, color, width: 14, height: 14 },
        label,
        labelStyle: { fill: color, fontSize: 10, fontFamily: "var(--font-mono)", fontWeight: 600 },
        labelBgStyle: { fill: "#ffffff", fillOpacity: 0.92 },
        labelBgPadding: [4, 2] as [number, number],
        labelBgBorderRadius: 3,
        data: { ...(e.data as object), media: kind },
      };
    });
  }, [edges, nodes, result]);

  const onNodeContextMenu = useCallback(
    (e: React.MouseEvent, node: Node<EntropykNodeData>) => {
      e.preventDefault();
      setSelected(node.id);
      setCtxMenu({
        x: e.clientX,
        y: e.clientY,
        nodeId: node.id,
        nodeName: node.data.name,
      });
    },
    [setSelected],
  );

  // Keep RF `selected` in sync, preserving node identity when the flag is unchanged
  // (`undefined` and `false` both mean unselected — avoid remapping every render).
  const displayNodes = useMemo(
    () =>
      nodes.map((n) => {
        const selected = n.id === selectedNodeId;
        if (!!n.selected === selected) return n;
        return { ...n, selected };
      }),
    [nodes, selectedNodeId],
  );

  return (
    <div className="relative h-full w-full">
      <ReactFlow
        nodes={displayNodes}
        edges={styledEdges}
        nodeTypes={nodeTypes}
        onNodesChange={onNodesChange as OnNodesChange<Node<EntropykNodeData>>}
        onEdgesChange={onEdgesChange as OnEdgesChange<Edge>}
        onConnect={onConnect as OnConnect}
        onInit={onInit}
        onDrop={onDrop}
        onDragOver={onDragOver}
        isValidConnection={isValidConnection}
        onNodeClick={(_, node) => {
          setCtxMenu(null);
          setSelected(node.id);
        }}
        onNodeDoubleClick={(_, node) => {
          if (node.data.type === "ModuleInstance") {
            const mName = String(node.data.params.module_name ?? "");
            if (mName) onOpenModuleEditor?.(mName);
          }
        }}
        onNodeContextMenu={onNodeContextMenu}
        onPaneClick={() => {
          setCtxMenu(null);
          setSelected(null);
        }}
        deleteKeyCode={["Delete"]}
        defaultEdgeOptions={{ type: "smoothstep" }}
        connectionRadius={34}
        snapToGrid={snapToGrid}
        snapGrid={[GRID_SIZE, GRID_SIZE]}
        minZoom={0.2}
        maxZoom={2.5}
        fitView
        proOptions={{ hideAttribution: true }}
      >
        {/* Dymola-style diagram sheet: fine grid + coarse major lines. */}
        <Background
          id="minor"
          variant={BackgroundVariant.Lines}
          gap={GRID_SIZE}
          lineWidth={0.5}
          color="#eef2f7"
        />
        <Background
          id="major"
          variant={BackgroundVariant.Lines}
          gap={GRID_SIZE * 5}
          lineWidth={0.8}
          color="#dde5ee"
        />
        <Controls showInteractive={false} />
      </ReactFlow>
      {connectionHint && (
        <div
          role="status"
          aria-live="polite"
          className="pointer-events-none absolute bottom-3 left-1/2 z-20 -translate-x-1/2 rounded-md border border-[var(--warn)] bg-[var(--panel)] px-3 py-1.5 text-[11px] text-[var(--warn)] shadow-md"
        >
          {connectionHint}
        </div>
      )}
      <MediaLegend />
      {selectedNode && (
        <OrientationBar
          name={selectedNode.data.name}
          rotation={selectedNode.data.rotation ?? 0}
          flipH={!!selectedNode.data.flipH}
          flipV={!!selectedNode.data.flipV}
          onRotate={() => rotateNode(selectedNode.id, 1)}
          onFlipH={() => flipNodeH(selectedNode.id)}
          onFlipV={() => flipNodeV(selectedNode.id)}
        />
      )}
      <NodeContextMenu
        menu={ctxMenu}
        onClose={() => setCtxMenu(null)}
        onRotate={(dir) => ctxMenu && rotateNode(ctxMenu.nodeId, dir)}
        onFlipH={() => ctxMenu && flipNodeH(ctxMenu.nodeId)}
        onFlipV={() => ctxMenu && flipNodeV(ctxMenu.nodeId)}
        onDelete={() => ctxMenu && removeNode(ctxMenu.nodeId)}
      />
    </div>
  );
}

function OrientationBar({
  name,
  rotation,
  flipH,
  flipV,
  onRotate,
  onFlipH,
  onFlipV,
}: {
  name: string;
  rotation: number;
  flipH: boolean;
  flipV: boolean;
  onRotate: () => void;
  onFlipH: () => void;
  onFlipV: () => void;
}) {
  return (
    <div className="absolute left-1/2 top-3 z-10 flex -translate-x-1/2 items-center gap-1 rounded-md border border-[var(--line)] bg-white/95 px-2 py-1 shadow-sm">
      <span className="mono mr-1 max-w-[100px] truncate text-[10px] text-[var(--ink-faint)]">
        {name} · {rotation}°
        {flipH ? " · ↔" : ""}
        {flipV ? " · ↕" : ""}
      </span>
      <button type="button" className="orient-btn" title="Rotate 90° (R)" onClick={onRotate}>
        <RotateCw size={13} />
      </button>
      <button
        type="button"
        className="orient-btn"
        data-on={flipH}
        title="Flip horizontal (H)"
        onClick={onFlipH}
      >
        <FlipHorizontal2 size={13} />
      </button>
      <button
        type="button"
        className="orient-btn"
        data-on={flipV}
        title="Flip vertical (V)"
        onClick={onFlipV}
      >
        <FlipVertical2 size={13} />
      </button>
      <style>{`
        .orient-btn {
          display: grid;
          place-items: center;
          width: 26px;
          height: 26px;
          border-radius: 5px;
          color: var(--ink-dim);
        }
        .orient-btn:hover {
          background: var(--chrome-2);
          color: var(--accent);
        }
        .orient-btn[data-on="true"] {
          background: rgba(27, 111, 224, 0.12);
          color: var(--accent);
        }
      `}</style>
    </div>
  );
}

function MediaLegend() {
  const items: MediaKind[] = ["refrigerant", "water", "air"];
  return (
    <div
      className="pointer-events-none absolute bottom-3 left-3 z-10 flex items-center gap-3 rounded-md border border-[var(--line)] bg-white/95 px-2.5 py-1.5 shadow-sm"
      title="Couleurs milieu — convention TIL / Modelica HVAC"
    >
      <span className="eyebrow">Milieux</span>
      {items.map((k) => (
        <span key={k} className="flex items-center gap-1.5 text-[10px] text-[var(--ink-dim)]">
          <span
            className="inline-block h-0.5 w-4 rounded-full"
            style={{ background: MEDIA_COLOR[k] }}
          />
          {MEDIA_LABEL[k]}
        </span>
      ))}
    </div>
  );
}
