import Konva from "konva";

export type Guide = {
  id: string;
  orientation: "vertical" | "horizontal";
  position: number;
};

const SNAP_THRESHOLD_PX = 5;

export type SnappedPositionResult = {
  position: { x: number; y: number };
  activeGuideIds: string[];
  smartGuides: SmartGuideLine[];
};

export type SmartGuideLine = {
  orientation: "vertical" | "horizontal";
  position: number;
};

type AxisSnapCandidate = {
  guideId?: string;
  delta: number;
  distance: number;
  priority: number;
  guides: SmartGuideLine[];
};

type RectBounds = {
  left: number;
  right: number;
  top: number;
  bottom: number;
  centerX: number;
  centerY: number;
  width: number;
  height: number;
};

type Axis = "x" | "y";

/** Matches Konva `getClientRect` / `IRect`. */
type BoundsRect = {
  x: number;
  y: number;
  width: number;
  height: number;
};

const toRectBounds = (rect: BoundsRect): RectBounds => {
  const left = rect.x;
  const top = rect.y;
  const right = rect.x + rect.width;
  const bottom = rect.y + rect.height;
  return {
    left,
    right,
    top,
    bottom,
    centerX: left + rect.width / 2,
    centerY: top + rect.height / 2,
    width: rect.width,
    height: rect.height,
  };
};

const axisLine = (axis: Axis, position: number): SmartGuideLine => ({
  orientation: axis === "x" ? "vertical" : "horizontal",
  position,
});

const isBetterCandidate = (
  next: AxisSnapCandidate,
  current: AxisSnapCandidate | null,
) => {
  if (!current) return true;
  if (next.distance < current.distance) return true;
  if (next.distance > current.distance) return false;
  return next.priority < current.priority;
};

const collectAlignmentCandidates = (
  axis: Axis,
  anchors: number[],
  targets: Array<{
    position: number;
    guideId?: string;
    priority: number;
  }>,
  threshold: number,
) => {
  const matches: AxisSnapCandidate[] = [];
  targets.forEach((target) => {
    anchors.forEach((anchor) => {
      const distance = Math.abs(anchor - target.position);
      if (distance > threshold) return;
      matches.push({
        guideId: target.guideId,
        delta: target.position - anchor,
        distance,
        priority: target.priority,
        guides: [axisLine(axis, target.position)],
      });
    });
  });
  return matches;
};

const overlapsOnCrossAxis = (axis: Axis, a: RectBounds, b: RectBounds) => {
  if (axis === "x") {
    return !(a.bottom <= b.top || a.top >= b.bottom);
  }
  return !(a.right <= b.left || a.left >= b.right);
};

const collectEqualSpacingCandidates = (
  axis: Axis,
  moving: RectBounds,
  others: RectBounds[],
  threshold: number,
) => {
  const matches: AxisSnapCandidate[] = [];
  if (others.length < 2) return matches;

  const sorted = [...others].sort((a, b) =>
    axis === "x" ? a.left - b.left : a.top - b.top,
  );

  for (let i = 0; i < sorted.length - 1; i += 1) {
    const first = sorted[i];
    const second = sorted[i + 1];
    if (
      !overlapsOnCrossAxis(axis, moving, first) ||
      !overlapsOnCrossAxis(axis, moving, second)
    ) {
      continue;
    }

    const gap =
      axis === "x" ? second.left - first.right : second.top - first.bottom;
    if (gap <= 0) continue;

    if (axis === "x") {
      const targetLeft = first.right + gap;
      const leftDistance = Math.abs(moving.left - targetLeft);
      if (leftDistance <= threshold) {
        matches.push({
          delta: targetLeft - moving.left,
          distance: leftDistance,
          priority: 3,
          guides: [
            axisLine("x", targetLeft),
            axisLine("x", targetLeft + moving.width),
          ],
        });
      }

      const targetRight = second.left - gap;
      const rightDistance = Math.abs(moving.right - targetRight);
      if (rightDistance <= threshold) {
        matches.push({
          delta: targetRight - moving.right,
          distance: rightDistance,
          priority: 3,
          guides: [
            axisLine("x", targetRight - moving.width),
            axisLine("x", targetRight),
          ],
        });
      }
    } else {
      const targetTop = first.bottom + gap;
      const topDistance = Math.abs(moving.top - targetTop);
      if (topDistance <= threshold) {
        matches.push({
          delta: targetTop - moving.top,
          distance: topDistance,
          priority: 3,
          guides: [
            axisLine("y", targetTop),
            axisLine("y", targetTop + moving.height),
          ],
        });
      }

      const targetBottom = second.top - gap;
      const bottomDistance = Math.abs(moving.bottom - targetBottom);
      if (bottomDistance <= threshold) {
        matches.push({
          delta: targetBottom - moving.bottom,
          distance: bottomDistance,
          priority: 3,
          guides: [
            axisLine("y", targetBottom - moving.height),
            axisLine("y", targetBottom),
          ],
        });
      }
    }
  }

  return matches;
};

const findBestCandidate = (
  candidates: AxisSnapCandidate[],
): AxisSnapCandidate | null => {
  let best: AxisSnapCandidate | null = null;
  candidates.forEach((candidate) => {
    if (isBetterCandidate(candidate, best)) {
      best = candidate;
    }
  });
  return best;
};

export const getSnappedPosition = (
  node: Konva.Node,
  guides: Guide[],
  scale: number,
): SnappedPositionResult => {
  const layer = node.getLayer();
  const rect = node.getClientRect({
    skipShadow: true,
    ...(layer ? { relativeTo: layer } : {}),
  });

  const threshold = SNAP_THRESHOLD_PX / Math.max(scale, 0.01);
  const moving = toRectBounds(rect);
  let x = node.x();
  let y = node.y();
  const activeGuideIds: string[] = [];
  const smartGuides: SmartGuideLine[] = [];

  const manualVerticalTargets = guides
    .filter((guide) => guide.orientation === "vertical")
    .map((guide) => ({ position: guide.position, guideId: guide.id }));
  const manualHorizontalTargets = guides
    .filter((guide) => guide.orientation === "horizontal")
    .map((guide) => ({ position: guide.position, guideId: guide.id }));

  const stage = node.getStage();
  const stageWidth = stage ? stage.width() / stage.scaleX() : 0;
  const stageHeight = stage ? stage.height() / stage.scaleY() : 0;

  const otherRects: RectBounds[] = [];
  if (layer) {
    layer
      .find((candidate: Konva.Node) => Boolean(candidate))
      .forEach((candidate: Konva.Node) => {
        if (candidate === node) return;
        if (!candidate.draggable()) return;
        if (!candidate.isVisible()) return;
        if (candidate.id() === node.id()) return;
        if (candidate.name() === "guide-line") return;
        const candidateRect = candidate.getClientRect({
          skipShadow: true,
          relativeTo: layer,
        });
        if (candidateRect.width <= 0 || candidateRect.height <= 0) return;
        otherRects.push(toRectBounds(candidateRect));
      });
  }

  const verticalCandidates: AxisSnapCandidate[] = [
    ...collectAlignmentCandidates(
      "x",
      [moving.left, moving.centerX, moving.right],
      [
        ...manualVerticalTargets.map((target) => ({
          position: target.position,
          guideId: target.guideId,
          priority: 0,
        })),
        ...otherRects.flatMap((other) => [
          { position: other.left, priority: 1 },
          { position: other.centerX, priority: 1 },
          { position: other.right, priority: 1 },
        ]),
        { position: stageWidth / 2, priority: 2 },
      ],
      threshold,
    ),
    ...collectEqualSpacingCandidates("x", moving, otherRects, threshold),
  ];

  const verticalSnap = findBestCandidate(verticalCandidates);
  if (verticalSnap) {
    x += verticalSnap.delta;
    if (verticalSnap.guideId) {
      activeGuideIds.push(verticalSnap.guideId);
    }
    smartGuides.push(...verticalSnap.guides);
  }

  const horizontalCandidates: AxisSnapCandidate[] = [
    ...collectAlignmentCandidates(
      "y",
      [moving.top, moving.centerY, moving.bottom],
      [
        ...manualHorizontalTargets.map((target) => ({
          position: target.position,
          guideId: target.guideId,
          priority: 0,
        })),
        ...otherRects.flatMap((other) => [
          { position: other.top, priority: 1 },
          { position: other.centerY, priority: 1 },
          { position: other.bottom, priority: 1 },
        ]),
        { position: stageHeight / 2, priority: 2 },
      ],
      threshold,
    ),
    ...collectEqualSpacingCandidates("y", moving, otherRects, threshold),
  ];

  const horizontalSnap = findBestCandidate(horizontalCandidates);
  if (horizontalSnap) {
    y += horizontalSnap.delta;
    if (horizontalSnap.guideId) {
      activeGuideIds.push(horizontalSnap.guideId);
    }
    smartGuides.push(...horizontalSnap.guides);
  }

  return {
    position: { x, y },
    activeGuideIds,
    smartGuides,
  };
};

export const drawSmartGuides = (
  layer: Konva.Layer | null,
  guides: SmartGuideLine[],
  stageWidth: number,
  stageHeight: number,
) => {
  if (!layer) return;
  layer.destroyChildren();

  guides.forEach((guide) => {
    const line = new Konva.Line({
      points:
        guide.orientation === "vertical"
          ? [guide.position, 0, guide.position, stageHeight]
          : [0, guide.position, stageWidth, guide.position],
      stroke: "#ef4444",
      strokeWidth: 1,
      dash: [4, 4],
      listening: false,
      perfectDrawEnabled: false,
    });
    layer.add(line);
  });

  layer.visible(guides.length > 0);
  layer.batchDraw();
};

export const clearSmartGuides = (layer: Konva.Layer | null) => {
  if (!layer) return;
  layer.destroyChildren();
  layer.visible(false);
  layer.batchDraw();
};
