import {
  boundsFromCorners,
  isValidCardQuad,
  orderQuadCorners,
} from './scanner-card-warp.js';

/** Detection canvas dimensions (smaller than video for performance). */
export const DETECTION_CANVAS_WIDTH = 320;
export const DETECTION_CANVAS_HEIGHT = 240;

/** Milliseconds before a tracked card is dropped when not re-detected. */
export const TRACK_STALE_MS = 3000;

/** Shape-detection polling interval in the camera scanner hook. */
export const SHAPE_DETECTION_INTERVAL_MS = 200;

/** Verification polling interval in the camera scanner hook. */
export const VERIFICATION_INTERVAL_MS = 500;

/** Minimum stable frames before a tracked card is sent for identification. */
export const MIN_STABLE_COUNT_FOR_VERIFY = 3;

/** Minimum time (ms) a card must be tracked before verification. */
export const MIN_FIRST_SEEN_MS_FOR_VERIFY = 500;

/** Delay after camera start before detection loops begin. */
export const DETECTION_START_DELAY_MS = 300;

/** Max verification attempts per tracked card before requiring reset. */
export const MAX_SCAN_ATTEMPTS = 3;

/** Ms before a negative tracker can retry without large movement. */
export const NEGATIVE_RETRY_MS = 5000;

/** Terminal tracker statuses that should not absorb new shape detections. */
export const TERMINAL_TRACKER_STATUSES = new Set(['confirmed', 'scanned']);

/** Tracked cards eligible for one-shot server verification. */
export function selectCardsReadyForVerification(
  trackedCards,
  now = Date.now(),
  {
    minStableCount = MIN_STABLE_COUNT_FOR_VERIFY,
    maxScanAttempts = MAX_SCAN_ATTEMPTS,
    minFirstSeenMs = MIN_FIRST_SEEN_MS_FOR_VERIFY,
  } = {}
) {
  return trackedCards.filter(
    (card) =>
      card.status === 'detecting' &&
      card.stableCount >= minStableCount &&
      card.scanAttempts < maxScanAttempts &&
      now - card.firstSeen > minFirstSeenMs
  );
}

/** Convert detection-canvas coordinates to video pixel coordinates. */
export function convertToVideoCoordinates(detection, canvas, video) {
  const scaleX = video.videoWidth / canvas.width;
  const scaleY = video.videoHeight / canvas.height;

  return {
    x: detection.x * scaleX,
    y: detection.y * scaleY,
    width: detection.width * scaleX,
    height: detection.height * scaleY,
  };
}

/** Convert a point from detection canvas space to video pixel space. */
export function convertPointToVideoCoordinates(point, canvas, video) {
  const scaleX = video.videoWidth / canvas.width;
  const scaleY = video.videoHeight / canvas.height;
  return { x: point.x * scaleX, y: point.y * scaleY };
}

/** True when overlap area exceeds `threshold` of the smaller rectangle. */
export function rectanglesOverlap(rect1, rect2, threshold = 0.3) {
  const x1 = Math.max(rect1.x, rect2.x);
  const y1 = Math.max(rect1.y, rect2.y);
  const x2 = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
  const y2 = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);

  if (x2 <= x1 || y2 <= y1) return false;

  const overlapArea = (x2 - x1) * (y2 - y1);
  const rect1Area = rect1.width * rect1.height;
  const rect2Area = rect2.width * rect2.height;
  const smallerArea = Math.min(rect1Area, rect2Area);

  return overlapArea / smallerArea > threshold;
}

function buildGrayscaleAndEdges(imageData, width, height) {
  const data = imageData.data;
  const grayscale = new Uint8Array(width * height);
  const edges = new Uint8Array(width * height);

  for (let i = 0, px = 0; i < data.length; i += 4, px++) {
    grayscale[px] = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
  }

  for (let y = 1; y < height - 1; y++) {
    for (let x = 1; x < width - 1; x++) {
      const idx = y * width + x;

      const gx =
        -grayscale[idx - 1] +
        grayscale[idx + 1] +
        -2 * grayscale[idx - 1 + width] +
        2 * grayscale[idx + 1 + width] +
        -grayscale[idx - 1 + 2 * width] +
        grayscale[idx + 1 + 2 * width];

      const gy =
        -grayscale[idx - width] -
        2 * grayscale[idx] -
        grayscale[idx + width] +
        grayscale[idx - width + 2 * width] +
        2 * grayscale[idx + 2 * width] +
        grayscale[idx + width + 2 * width];

      const magnitude = Math.sqrt(gx * gx + gy * gy);
      edges[idx] = magnitude > 90 ? 255 : 0;
    }
  }

  return { grayscale, edges };
}

/**
 * Refine a coarse card bbox into four corners using edge strength in each quadrant.
 * Exported for unit tests.
 */
export function refineCardCornersFromEdges(edges, width, height, bbox) {
  const { x, y, width: boxWidth, height: boxHeight } = bbox;
  const centerX = x + boxWidth / 2;
  const centerY = y + boxHeight / 2;

  const quadrants = [
    { minX: x, maxX: centerX, minY: y, maxY: centerY },
    { minX: centerX, maxX: x + boxWidth, minY: y, maxY: centerY },
    { minX: centerX, maxX: x + boxWidth, minY: centerY, maxY: y + boxHeight },
    { minX: x, maxX: centerX, minY: centerY, maxY: y + boxHeight },
  ];

  const corners = quadrants.map((quad) => {
    let best = null;
    let bestScore = -1;

    for (let py = Math.floor(quad.minY); py < quad.maxY; py++) {
      for (let px = Math.floor(quad.minX); px < quad.maxX; px++) {
        if (px < 0 || py < 0 || px >= width || py >= height) continue;
        const idx = py * width + px;
        if (edges[idx] !== 255) continue;

        const dx = px - centerX;
        const dy = py - centerY;
        const score = Math.hypot(dx, dy);
        if (score > bestScore) {
          bestScore = score;
          best = { x: px, y: py };
        }
      }
    }

    if (!best) {
      return {
        x: (quad.minX + quad.maxX) / 2,
        y: (quad.minY + quad.maxY) / 2,
      };
    }

    return best;
  });

  return orderQuadCorners(corners);
}

function scoreCardCandidate(edges, width, height, bbox) {
  const { x, y, width: w, height: h } = bbox;
  let edgeCount = 0;
  let totalPixels = 0;
  let perimeterEdges = 0;

  for (let sy = y; sy < y + h; sy += 3) {
    for (let sx = x; sx < x + w; sx += 3) {
      if (sx < 0 || sy < 0 || sx >= width || sy >= height) continue;
      const idx = sy * width + sx;
      totalPixels++;
      if (edges[idx] !== 255) continue;
      edgeCount++;
      const isPerimeter =
        sx < x + w * 0.15 || sx > x + w * 0.85 || sy < y + h * 0.15 || sy > y + h * 0.85;
      if (isPerimeter) perimeterEdges++;
    }
  }

  const edgeDensity = edgeCount / (totalPixels || 1);
  const perimeterRatio = perimeterEdges / (edgeCount || 1);
  if (edgeDensity <= 0.18 || edgeDensity >= 0.65 || perimeterRatio <= 0.35 || edgeCount <= 60) {
    return null;
  }

  return edgeDensity * 100 + perimeterRatio * 60 + edgeCount / 10;
}

function findCardCandidates(edges, width, height) {
  const minCardWidth = Math.floor(width * 0.2);
  const maxCardWidth = Math.floor(width * 0.85);
  const minCardHeight = Math.floor(height * 0.25);
  const maxCardHeight = Math.floor(height * 0.7);
  const candidates = [];

  for (let y = 0; y < height - minCardHeight; y += 12) {
    for (let x = 0; x < width - minCardWidth; x += 12) {
      for (let w = minCardWidth; w <= maxCardWidth && x + w < width; w += 16) {
        for (let h = minCardHeight; h <= maxCardHeight && y + h < height; h += 16) {
          const aspectRatio = w / h;
          if (aspectRatio < 0.63 || aspectRatio > 0.77) continue;

          const score = scoreCardCandidate(edges, width, height, { x, y, width: w, height: h });
          if (score == null || score <= 50) continue;

          const corners = refineCardCornersFromEdges(edges, width, height, {
            x,
            y,
            width: w,
            height: h,
          });
          const bounds = boundsFromCorners(corners);
          if (!isValidCardQuad(corners)) continue;

          candidates.push({
            x: bounds.x,
            y: bounds.y,
            width: bounds.width,
            height: bounds.height,
            corners,
            score: score + (isValidCardQuad(corners) ? 20 : 0),
            aspectRatio,
          });
        }
      }
    }
  }

  return candidates.sort((a, b) => b.score - a.score).slice(0, 5);
}

/**
 * Draw the current video frame on `detectionCanvas` and return up to 5 card-like
 * detections in video coordinates, including perspective corners when found.
 */
export function detectCardShapesFromFrame(video, detectionCanvas) {
  if (!video || !detectionCanvas) return [];

  const ctx = detectionCanvas.getContext('2d');
  detectionCanvas.width = DETECTION_CANVAS_WIDTH;
  detectionCanvas.height = DETECTION_CANVAS_HEIGHT;

  ctx.drawImage(video, 0, 0, detectionCanvas.width, detectionCanvas.height);

  const imageData = ctx.getImageData(0, 0, detectionCanvas.width, detectionCanvas.height);
  const { edges } = buildGrayscaleAndEdges(
    imageData,
    detectionCanvas.width,
    detectionCanvas.height
  );

  const candidates = findCardCandidates(edges, detectionCanvas.width, detectionCanvas.height);

  return candidates.map((candidate) => {
    const videoBounds = convertToVideoCoordinates(candidate, detectionCanvas, video);
    const videoCorners = candidate.corners.map((corner) =>
      convertPointToVideoCoordinates(corner, detectionCanvas, video)
    );

    return {
      ...videoBounds,
      corners: videoCorners,
      score: candidate.score,
      aspectRatio: candidate.aspectRatio,
    };
  });
}

/**
 * Merge fresh shape detections into the tracked-card list.
 * Returns updated cards and the next card id counter.
 */
export function mergeDetectedShapesIntoTrackedCards(
  trackedCards,
  detectedShapes,
  nextCardId,
  now = Date.now()
) {
  const updatedCards = [...trackedCards];

  for (let i = updatedCards.length - 1; i >= 0; i--) {
    if (now - updatedCards[i].lastSeen > TRACK_STALE_MS) {
      console.log(
        `🗑️ Removing stale tracked card ${updatedCards[i].id} (last seen ${Math.round((now - updatedCards[i].lastSeen) / 1000)}s ago)`
      );
      updatedCards.splice(i, 1);
    }
  }

  let idCounter = nextCardId;

  detectedShapes.forEach((shape) => {
    let matchedCard = null;

    for (const card of updatedCards) {
      if (TERMINAL_TRACKER_STATUSES.has(card.status)) continue;
      if (rectanglesOverlap(shape, card.bounds, 0.4)) {
        matchedCard = card;
        break;
      }
    }

    if (matchedCard) {
      const previousBounds = matchedCard.bounds;
      matchedCard.bounds = {
        x: shape.x,
        y: shape.y,
        width: shape.width,
        height: shape.height,
      };
      if (shape.corners?.length === 4) {
        matchedCard.corners = shape.corners.map((corner) => ({ ...corner }));
      }
      matchedCard.lastSeen = now;
      matchedCard.stableCount = Math.min(matchedCard.stableCount + 1, 10);

      if (matchedCard.status === 'negative') {
        const negativeSince = matchedCard.negativeAt ?? matchedCard.lastSeen;
        const positionChange =
          Math.abs(previousBounds.x - shape.x) + Math.abs(previousBounds.y - shape.y);
        if (positionChange > 25 || now - negativeSince > NEGATIVE_RETRY_MS) {
          console.log(`🔄 Card ${matchedCard.id} ready for re-scan`);
          matchedCard.status = 'detecting';
          matchedCard.scanAttempts = 0;
          matchedCard.stableCount = Math.max(matchedCard.stableCount, MIN_STABLE_COUNT_FOR_VERIFY);
          matchedCard.negativeAt = undefined;
        }
      }
    } else {
      const newCard = {
        id: idCounter++,
        bounds: {
          x: shape.x,
          y: shape.y,
          width: shape.width,
          height: shape.height,
        },
        corners: shape.corners?.length === 4 ? shape.corners.map((corner) => ({ ...corner })) : null,
        status: 'detecting',
        firstSeen: now,
        lastSeen: now,
        stableCount: 1,
        scanAttempts: 0,
      };

      updatedCards.push(newCard);
      console.log(`🎯 New card shape detected: ${newCard.id}`);
    }
  });

  return { cards: updatedCards, nextCardId: idCounter };
}
