import type { Polygon, Position } from 'geojson';
import type { BoundaryIndex } from './boundaryIndex';

/**
 * Snap-to-boundary auto-complete.
 *
 * When both ends of the user's stroke land near the PI outer boundary, we
 * close the shape by tracing the boundary itself between those two points,
 * rather than drawing a straight line across.
 *
 * Ring construction (this is the part that was previously wrong):
 *
 *     drawn stroke   A ────────────▶ B          (user's line, west-bound arc)
 *     boundary trace B ────────────▶ A          (walked BACK along the border)
 *
 * The two halves must run in OPPOSITE directions to form a simple closed ring.
 * The previous implementation appended the boundary trace in the same A→B
 * direction as the drawn stroke, producing a self-intersecting "bowtie": the
 * ring jumped from B back to A, retraced A→B along the border, then jumped to
 * A again. Those two jumps rendered as long straight lines cutting clean across
 * the region -- the "gross long-distance straightline simplifications" reported
 * in testing -- and the self-intersection also made `turf.intersect` throw
 * during the later clip step, which silently dropped shapes.
 */

export interface AutoCompleteResult {
  polygon: Polygon;
  /**
   * How many leading coordinates of the ring came from the user's own stroke.
   * Everything from this index up to the closing vertex is boundary-derived and
   * must NOT be simplified -- those vertices are the border, verbatim.
   */
  drawnCount: number;
}

/**
 * @param drawnRing  the user's open stroke, first point A .. last point B
 * @param index      spatial index over the PI boundary rings
 * @param snapKm     how close each endpoint must be to the border to qualify
 * @param maxAreaRatio reject the completion if it balloons the drawn area by
 *                     more than this factor (catches "I drew a small bite but
 *                     got half the country closed in")
 */
export function autoCompleteAlongBoundary(
  drawnRing: Position[],
  index: BoundaryIndex,
  snapKm = 1,
  maxAreaRatio = 10,
): AutoCompleteResult | null {
  if (drawnRing.length < 3) return null;

  const start = drawnRing[0];
  const end = drawnRing[drawnRing.length - 1];

  const startHit = index.nearest(start);
  const endHit = index.nearest(end);
  if (!startHit || !endHit) return null;
  if (startHit.distKm > snapKm || endHit.distKm > snapKm) return null;
  // Both ends must be on the same ring for a trace between them to mean
  // anything (e.g. one end on the mainland outline, the other on an enclave).
  if (startHit.ringIdx !== endHit.ringIdx) return null;

  // Trace the border from B back to A -- note the argument order. This is what
  // makes the ring simple rather than a bowtie.
  //
  // Both ways round the ring are considered, not just the shorter one. Picking
  // purely on length and then failing the area check below meant auto-complete
  // gave up entirely whenever the short way happened to sweep in far more
  // ground than the user implied -- the shape then closed with a plain straight
  // line and no explanation, which is the "sometimes it just jumps with a
  // straightline" behaviour Bill reported. The long way round is frequently the
  // one that matches intent, so it now gets its turn.
  const traces = index.traceCandidates(endHit, startHit);
  if (traces.length === 0) return null;

  // The stroke ends up to `snapKm` short of the border; splicing in the snapped
  // points is what makes the drawn line visibly MEET the white boundary instead
  // of stopping just inside it.
  // Dedupe the DRAWN half before assembling, so `drawnCount` below counts the
  // ring that actually ships rather than the raw input.
  //
  // This matters more than it looks. `drawnCount` is the index the simplifier
  // uses to decide where the user's stroke stops and the boundary trace starts,
  // and everything from it onward is left verbatim. Counting the pre-dedupe
  // length meant that every duplicate coordinate dropped here (freehand emits
  // them whenever the pointer reports the same position twice) shifted that
  // index further into the traced half -- handing the simplifier a run of real
  // boundary vertices to thin at a ~685 m tolerance. Thinned border vertices
  // read exactly as Bill described: a straight chord cutting across a boundary
  // excursion, starting and ending at points that look arbitrary because they
  // are whichever vertices Douglas-Peucker happened to keep.
  const drawnDeduped = dedupeAdjacent(drawnRing);
  if (drawnDeduped.length < 3) return null;

  // The area the user implied by closing their own arc with a straight edge.
  const drawnClosed = dedupeAdjacent([...drawnDeduped, drawnDeduped[0]]);
  const userArea = drawnClosed.length >= 4 ? ringAreaAbs(drawnClosed) : 0;

  // Evaluate each candidate trace and keep the one that stays closest to the
  // area the user implied, rejecting any that balloons past `maxAreaRatio`
  // (that guard is what stops "I drew a small bite but got half the country").
  let best: Position[] | null = null;
  let bestRatio = Infinity;
  for (const trace of traces) {
    if (trace.length < 2) continue;
    const ring = dedupeAdjacent([
      ...drawnDeduped, // A .. B
      ...trace, // B(snapped) .. A(snapped), following the border
      drawnDeduped[0], // close back onto A
    ]);
    // Only the join points can collapse here, and dedupe drops the LATER of a
    // duplicate pair, so the drawn prefix keeps its length.
    if (ring.length < 4) continue;
    if (userArea <= 0) {
      best = ring;
      break;
    }
    const ratio = ringAreaAbs(ring) / userArea;
    if (ratio > maxAreaRatio) continue;
    if (ratio < bestRatio) {
      bestRatio = ratio;
      best = ring;
    }
  }
  if (!best) return null;

  return {
    polygon: { type: 'Polygon', coordinates: [best] },
    drawnCount: drawnDeduped.length,
  };
}

/** Drop consecutive duplicate coordinates, which break turf and terra-draw. */
function dedupeAdjacent(ring: Position[]): Position[] {
  const out: Position[] = [];
  for (const c of ring) {
    const prev = out[out.length - 1];
    if (!prev || prev[0] !== c[0] || prev[1] !== c[1]) out.push(c);
  }
  return out;
}

/** Shoelace area in square degrees. Comparative use only, not real-world area. */
function ringAreaAbs(ring: Position[]): number {
  let area = 0;
  for (let i = 0, n = ring.length - 1; i < n; i++) {
    area += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];
  }
  return Math.abs(area) / 2;
}
