import { BaseGenerator } from "./BaseGenerator.js";
import { CrystalSpiresTextureGenerator } from "./textures/CrystalSpiresTextureGenerator.js";

/**
 * Generates the "Crystal Spires" biome.
 * Structure: Distinct vertical pillars with attached platform discs.
 */
export class CrystalSpiresGenerator extends BaseGenerator {
  constructor(grid, seed) {
    super(grid, seed);
    this.generatedAssets = {
      palette: {},
      spawnZones: { player: [], enemy: [] },
    };
    this.preloadAssets();
  }

  preloadAssets() {
    const texGen = new CrystalSpiresTextureGenerator(this.seed);

    // ID 1: Marble Pillar
    this.generatedAssets.palette[1] = texGen.generateMarble(64);

    // ID 15: Crystal Scatter
    this.generatedAssets.palette[15] = texGen.generateCrystal(64);

    // ID 20: Bridge
    this.generatedAssets.palette[20] = texGen.generateBridge(64);
  }

  /**
   * @param {number} spireCount - Number of main vertical pillars.
   * @param {number} numFloors - Number of vertical levels (platforms).
   * @param {number} floorSpacing - Height between platforms.
   */
  generate(spireCount = 4, numFloors = 3, floorSpacing = 6) {
    this.grid.fill(0); // Start with Void (Sky)

    // Reset generation data, passing empty arrays/objects but keeping palette if it exists
    this.generatedAssets.spawnZones = {
      player: [],
      enemy: [],
    };
    this.generatedAssets.spires = [];
    this.generatedAssets.bridges = [];

    // Track spires for connections
    // Each spire: { x, z, radius, platforms: [{y, r}] }
    const spires = [];

    // 1. Generate Main Spires (Vertical Columns)
    // Distribute them somewhat evenly or randomly but avoiding overlap
    for (let i = 0; i < spireCount; i++) {
      let x, z, valid;
      let attempts = 0;
      const radius = this.rng.rangeInt(2, 3); // Thinner pillars

      // Try to find a spot not too close to others
      // Dynamic spacing: On small maps (20x20), 10 is too big. Use min(10, width/3).
      const minSpacing = Math.min(10, this.width / 3);

      do {
        valid = true;
        x = this.rng.rangeInt(5, this.width - 6);
        z = this.rng.rangeInt(5, this.depth - 6);

        for (const s of spires) {
          if (this.dist2D(x, z, s.x, s.z) < minSpacing) valid = false;
        }
        attempts++;
      } while (!valid && attempts < 10);

      if (valid) {
        this.buildPillar(x, z, radius, this.height);
        // Platforms generated later in a global pass
        spires.push({ x, z, radius, platforms: [] });
      }
    }
    this.generatedAssets.spires = spires;
    this.generatedAssets.spires = spires;

    // 2. Generate Platforms (Sparse Levels)
    // Create global levels to ensure vertical progression
    // 2. Generate Platforms (Sparse Levels)
    // Create global levels to ensure vertical progression

    // Adaptive Height Logic:
    // With height=10, we have range [1...7] roughly (headroom 3).
    // If numFloors=3, we need to squeeze them.
    // Ensure we fit at least `numFloors` if physically possible.
    const maxUsableY = this.height - 3;
    const minStartY = 1;
    const usableHeight = maxUsableY - minStartY;

    // Recalculate spacing if default doesn't fit
    // e.g. 3 floors. (3-1) gaps. Spacing = usable / (floors-1)? No, floors are at y0, y0+s, y0+2s
    // Last floor y = start + (numFloors-1)*spacing.
    // If start=2. max=7. range=5. 2 gaps. spacing=2.5.

    let actualSpacing = floorSpacing;
    let actualStartY = 4;

    // If simple check fails
    if (actualStartY + (numFloors - 1) * actualSpacing > maxUsableY) {
      // Try to compress
      actualStartY = 2; // Lower start
      if (numFloors > 1) {
        actualSpacing = Math.floor(
          (maxUsableY - actualStartY) / (numFloors - 1)
        );
        if (actualSpacing < 3) actualSpacing = 3; // Minimum viable spacing
      }
    }

    for (let f = 0; f < numFloors; f++) {
      const y = actualStartY + f * actualSpacing;
      if (y > maxUsableY) break;

      // Pick 1-2 random spires for this level
      // Pick random spires for this level
      const levelSpires = [];
      // Ensure at least 2 platforms per level (if possible), or half the spires.
      // This guarantees vertical options and prevents single-chain linearity.
      const count = Math.max(2, Math.floor(spireCount / 2));

      // Shuffle spires to pick random ones
      const shuffled = [...spires].sort(() => this.rng.next() - 0.5);

      for (let i = 0; i < Math.min(count, shuffled.length); i++) {
        const s = shuffled[i];
        const discRadius = this.rng.rangeInt(3, 5);
        this.buildDisc(s.x, y, s.z, discRadius);
        s.platforms.push({
          x: s.x,
          y,
          z: s.z,
          radius: discRadius,
          connections: [],
        });
      }
    }

    // 3. Connect Spires (Moved to end to carve through crystals)
    // this.connectSpires(spires);

    // 4. Define Spawn Zones
    // Player starts at the BOTTOM, Enemy at the TOP
    const allPlatforms = [];
    for (const s of spires) {
      for (const p of s.platforms) {
        allPlatforms.push(p);
      }
    }

    if (allPlatforms.length > 0) {
      // Sort by Height (Y)
      allPlatforms.sort((a, b) => a.y - b.y);

      const playerPlat = allPlatforms[0];
      const enemyPlat = allPlatforms[allPlatforms.length - 1];

      this.markSpawnZone(playerPlat, "player");
      this.markSpawnZone(enemyPlat, "enemy");
    }

    // 5. Scatter Cover (Post-generation)
    // ID 15 = Crystal formations
    // 3% Density (Clusters are larger so lower density)
    this.scatterCrystalClusters(15, 0.03);

    // 6. Connect Spires (Bridges) - Runs last to ensure pathways are clear
    this.connectSpires(spires);

    // 7. Cleanup Reserved Headroom (ID 99 -> Air 0)
    // We used ID 99... (Actually we removed ID 99 logic? No, buildBridge uses it?
    // Wait, new Bezier logic DOES NOT set ID 99. It validates instead.
    // So this cleanup loop is harmless but unnecessary.
    for (let i = 0; i < this.grid.cells.length; i++) {
      if (this.grid.cells[i] === 99) {
        this.grid.cells[i] = 0;
      }
    }

    // 8. Ensure Global Connectivity
    // Identify orphans and link via Teleporters
    this.ensureGlobalConnectivity(spires);
  }

  buildPillar(centerX, centerZ, radius, height) {
    // Clean, straight pillars as per Biome Spec
    for (let y = 0; y < height; y++) {
      for (let x = centerX - radius; x <= centerX + radius; x++) {
        for (let z = centerZ - radius; z <= centerZ + radius; z++) {
          if (this.dist2D(x, z, centerX, centerZ) <= radius) {
            // ID 1 (Stone) center
            this.grid.setCell(x, y, z, 1);
          }
        }
      }
    }
  }

  buildDisc(centerX, y, centerZ, radius) {
    for (let x = centerX - radius; x <= centerX + radius; x++) {
      for (let z = centerZ - radius; z <= centerZ + radius; z++) {
        // Circular disc
        if (this.dist2D(x, z, centerX, centerZ) <= radius) {
          this.grid.setCell(x, y, z, 1); // Floor
          // Clear headroom?
          this.grid.setCell(x, y + 1, z, 0);
          this.grid.setCell(x, y + 2, z, 0);
          this.grid.setCell(x, y + 3, z, 0);
        }
      }
    }
  }

  /**
   * Connects platforms within the SAME spire (Vertical/Spiral connectivity).
   * Ensures that a single spire is walkable from bottom to top.
   */
  connectVerticalLevels(spires) {
    // console.log("Running connectVerticalLevels...");
    spires.forEach((s, sIdx) => {
      // Sort by Height (Y)
      const sortedPlats = [...s.platforms].sort((a, b) => a.y - b.y);
      // console.log(`Spire ${sIdx} has ${sortedPlats.length} platforms.`);

      for (let i = 0; i < sortedPlats.length - 1; i++) {
        const pA = sortedPlats[i];
        const pB = sortedPlats[i + 1];

        const dy = pB.y - pA.y;
        // console.log(`Spire ${sIdx} Pair ${i}->${i+1}: dy=${dy}`);
        
        // Limit vertical reach for single bridge
        if (dy > 18) {
             // console.log("  Skipped: Too far");
             continue; 
        }

        // We need to pick points on the rim that are offset to create a spiral.
        // If we pick same angle, it's a vertical ladder (steep).
        // Ideally 90-180 degrees offset.

        // Use random angle offset for variety? Or fixed for stability?
        // Let's use 120 degrees.
        const angleA = this.rng.next() * Math.PI * 2;
        const angleB = angleA + Math.PI * 0.66; // 120 deg

        const rA = pA.radius - 0.5; // Inset slightly
        const rB = pB.radius - 0.5;

        const start = {
          x: pA.x + Math.cos(angleA) * rA,
          y: pA.y,
          z: pA.z + Math.sin(angleA) * rA,
        };

        const end = {
          x: pB.x + Math.cos(angleB) * rB,
          y: pB.y,
          z: pB.z + Math.sin(angleB) * rB,
        };

        const success = this.buildBridge(start, end, { x: s.x, z: s.z }, 0.0);
        // console.log(`  BuildBridge Result: ${success}`);

        if (success) {
          pA.connections.push(start);
          pB.connections.push(end);
          this.generatedAssets.bridges.push({
            fromSpire: sIdx, 
            toSpire: sIdx, // Same Spire
            fromPlatIdx: s.platforms.indexOf(pA),
            toPlatIdx: s.platforms.indexOf(pB),
            start,
            end,
          });
        }
      }
    });
  }
  connectSpires(spires) {
    // 0. Vertical Pass (Self-Connect)
    this.connectVerticalLevels(spires);

    const connectedPairs = new Set();

    // For each spire, try to connect to *multiple* nearest neighbors
    // For each spire, try to connect to *multiple* nearest neighbors
    for (let i = 0; i < spires.length; i++) {
      const sA = spires[i];

      // Find all neighbors
      const neighbors = [];
      for (let j = 0; j < spires.length; j++) {
        if (i === j) continue;
        const dist = this.dist2D(sA.x, sA.z, spires[j].x, spires[j].z);
        neighbors.push({ spire: spires[j], dist, index: j });
      }
      neighbors.sort((a, b) => a.dist - b.dist);

      // Connect to closest neighbors until we have 2 connections (or run out)
      // Connect to closest neighbors until we have 2 distinct neighbor Spires connected
      let connectedNeighbors = 0;
      let backupBridge = null; // Store crowded connection as fallback

      for (let n = 0; n < neighbors.length; n++) {
        // Stop if we have connected to enough neighbor SPIRES (not bridges)
        // Increased from 2 to 4 to ensure we find the "Vertical Next" neighbor even if it's further away.
        if (connectedNeighbors >= 4) break;

        const nObj = neighbors[n];
        const sB = nObj.spire;
        let bridgesToThisNeighbor = 0;

        // Redundancy: Allow bidirectional checks to ensure connectivity even if one side considers it crowded/skipped.
        // Overlapping bridges (ID 20) are harmless.
        // const pairId = [i, nObj.index].sort().join("-");
        // if (connectedPairs.has(pairId)) continue;
        // connectedPairs.add(pairId);

        // Connect ALL valid platform pairs between these spires
        // (Subject to spacing constraints)
        for (const pA of sA.platforms) {
          for (const pB of sB.platforms) {
            // Limit: Cap unlimited bridges per pair to prevent chaotic webs, but allow enough for verticality.
            if (bridgesToThisNeighbor >= 5) break;

            const yd = Math.abs(pB.y - pA.y);
            const dist2d = Math.sqrt(
              Math.pow(pB.x - pA.x, 2) + Math.pow(pB.z - pA.z, 2)
            );

            // dist2d is center-to-center. rimDist is edge-to-edge.
            const rimDist = dist2d - pA.radius - pB.radius;

            // Requirements:
            // 1. Gap > 1.0 (Prevent overlapping meshes, but allow short steep bridges)
            // Requirements:
            // 1. Gap > 1.0 OR Vertical Diff > 3.0 (Allow vertical shafts/ladders even if horizontally close)
            // 2. Vertical Diff <= 12 (Reach)
            // 3. Slope <= 3.0 (Steep but walkable stairs for short distances)

            // Note: If Gap is small (e.g. 0.5) but Yd is large (e.g. 10), the total distance is large enough (>2.0) for the failsafe.
            // We only want to prevent "overlapping touches" where Gap~0 and Yd~0.

            const reqYdMax = Math.max(rimDist, 3) * 3.0;
            const validGap = rimDist > 1.0 || yd > 3.0;
            const validYdAbs = yd <= 12;
            const validSlope = yd <= reqYdMax;

            if (validGap && validYdAbs && validSlope) {
              // Calculate Edge Points
              const dx_direct = pB.x - pA.x;
              const dz_direct = pB.z - pA.z;

              const dist = Math.sqrt(
                dx_direct * dx_direct + dz_direct * dz_direct
              );
              if (dist2d > 0) {
                const nx = dx_direct / dist2d;
                const nz = dz_direct / dist2d;
                // Dynamic Logic:
                // 1. If touching (rimDist < 2.0), use TANGENT (90 deg) to build on the side (Spiral).
                // 2. If spaced, use DIRECT (0 deg) to build across the gap (Bridge).
                const isTouching = rimDist < 2.0;

                let startPt, endPt;
                // Spiral Mode (Touching): Use 0.0 (Exact Rim) to avoid clipping under the floor.
                // Direct Mode (Spaced): Use 1.5 (Internal) for solid floor connection.
                const penetration = isTouching ? 0.0 : 1.5;

                // Calculate vertical slope constraint
                // Standard stairs are 1:1. We can allow maybe 1.5:1 for jumping/steep ramps.
                const validSlope = yd <= Math.max(rimDist, 2) * 1.5;
                if (!validSlope) continue;

                if (isTouching) {
                  // Spiral Mode: Use Tangent Vector (-nz, nx)
                  // Try "Right" Tangent first
                  const tx = -nz;
                  const tz = nx;

                  // For Spiral, both platforms connect on the same "side" (e.g. North face of both).
                  startPt = this.getUncrowdedRimPoint(
                    pA,
                    tx,
                    tz,
                    penetration,
                    sA.radius
                  );
                  endPt = this.getUncrowdedRimPoint(
                    pB,
                    tx,
                    tz,
                    penetration,
                    sB.radius
                  ); // Same vector for B!

                  // If blocked, try "Left" Tangent
                  if (!startPt || !endPt) {
                    startPt = this.getUncrowdedRimPoint(
                      pA,
                      -tx,
                      -tz,
                      penetration,
                      sA.radius
                    );
                    endPt = this.getUncrowdedRimPoint(
                      pB,
                      -tx,
                      -tz,
                      penetration,
                      sB.radius
                    );
                  }
                } else {
                  // Direct Mode: Use Normal Vector (nx, nz)
                  startPt = this.getUncrowdedRimPoint(
                    pA,
                    nx,
                    nz,
                    penetration,
                    sA.radius
                  );
                  endPt = this.getUncrowdedRimPoint(
                    pB,
                    -nx,
                    -nz,
                    penetration,
                    sB.radius
                  ); // Invert for B
                }

                if (startPt && endPt) {
                  // COLLISION CHECK: Ensure bridge doesn't clip ANY spire pillars
                  // We check against ALL spires (not just A and B, though they are most likely)
                  let blocked = false;
                  for (const s of spires) {
                    // Ignore the source and target pillars themselves (bridges obviously touch them)
                    if (s === sA || s === sB) continue;

                    // Check against pillar Radius (usually 2-3). Give 1.5 margin.
                    // Spire radius is stored in s.radius
                    if (
                      this.checkCylinderCollision(
                        startPt.x,
                        startPt.z,
                        endPt.x,
                        endPt.z,
                        s.x,
                        s.z,
                        s.radius + 1.0
                      )
                    ) {
                      blocked = true;
                      break;
                    }
                  }

                  if (!blocked) {
                    pA.connections.push(startPt);
                    pB.connections.push(endPt);

                    this.buildBridge(
                      { x: startPt.x, y: pA.y, z: startPt.z },
                      { x: endPt.x, y: pB.y, z: endPt.z },
                      { x: sA.x, z: sA.z }
                    );
                    bridgesToThisNeighbor++;

                    this.generatedAssets.bridges.push({
                      fromSpire: i,
                      toSpire: nObj.index,
                      fromPlatIdx: sA.platforms.indexOf(pA),
                      toPlatIdx: sB.platforms.indexOf(pB),
                      start: { x: startPt.x, y: pA.y, z: startPt.z },
                      end: { x: endPt.x, y: pB.y, z: endPt.z },
                      yd,
                      dist: rimDist,
                    });
                  }
                } else if (!backupBridge && isTouching) {
                  // Forced Spiral Backup
                  const tx = -nz;
                  const tz = nx;
                  const idealStartX = pA.x + tx * (pA.radius - penetration);
                  const idealStartZ = pA.z + tz * (pA.radius - penetration);
                  const idealEndX = pB.x + tx * (pB.radius - penetration);
                  const idealEndZ = pB.z + tz * (pB.radius - penetration);

                  backupBridge = {
                    start: { x: idealStartX, y: pA.y, z: idealStartZ },
                    end: { x: idealEndX, y: pB.y, z: idealEndZ },
                    targetSpireIndex: nObj.index,
                    targetPlatIdx: nObj.platIdx,
                    pA,
                    bestP: pB,
                    startPt: { x: idealStartX, z: idealStartZ },
                    endPt: { x: idealEndX, z: idealEndZ },
                  };
                } else if (!backupBridge) {
                  // Forced Direct Backup
                  const idealStartX = pA.x + nx * (pA.radius - penetration);
                  const idealStartZ = pA.z + nz * (pA.radius - penetration);
                  const idealEndX = pB.x - nx * (pB.radius - penetration);
                  const idealEndZ = pB.z - nz * (pB.radius - penetration);

                  backupBridge = {
                    start: { x: idealStartX, y: pA.y, z: idealStartZ },
                    end: { x: idealEndX, y: pB.y, z: idealEndZ },
                    targetSpireIndex: nObj.index,
                    targetPlatIdx: sB.platforms.indexOf(pB),
                    pA,
                    bestP: pB,
                    startPt: { x: idealStartX, z: idealStartZ },
                    endPt: { x: idealEndX, z: idealEndZ },
                  };
                }
              }
            }
          }
        }

        // If we built ANY bridges to this neighbor, count it
        // If we built ANY bridges to this neighbor, count it
        if (bridgesToThisNeighbor > 0) {
          connectedNeighbors++;
        }
      }

      // Fallback: If we haven't connected to enough NEIGHBORS (2) and have a valid (but crowded) backup, use it.
      if (connectedNeighbors < 2 && backupBridge) {
        backupBridge.pA.connections.push(backupBridge.startPt);
        backupBridge.bestP.connections.push(backupBridge.endPt);
        this.buildBridge(backupBridge.start, backupBridge.end, {
          x: spires[i].x,
          z: spires[i].z,
        });

        // Track backup bridge too!
        this.generatedAssets.bridges.push({
          fromSpire: i,
          toSpire: backupBridge.targetSpireIndex,
          fromPlatIdx: sA.platforms.indexOf(pA),
          toPlatIdx: backupBridge.targetPlatIdx, // Need to ensure backupBridge has this!
          start: backupBridge.start,
          end: backupBridge.end,
          yd: Math.abs(backupBridge.end.y - backupBridge.start.y),
          dist: this.dist2D(
            backupBridge.start.x,
            backupBridge.start.z,
            backupBridge.end.x,
            backupBridge.end.z
          ),
        });
      }
    }

    // ORPHAN RESCUE PASS
    // Ensure every platform has at least 1 connection
    for (let i = 0; i < spires.length; i++) {
      const s = spires[i];
      for (const p of s.platforms) {
        if (p.connections.length === 0) {
          // Determine if this is truly isolated or just top/bottom
          // Actually, ALL platforms safely reachable should be connected.

          // Find ANY valid neighbor platform
          let bestTarget = null;
          let minMetric = Infinity;

          for (let j = 0; j < spires.length; j++) {
            if (i === j) continue;
            const neighbor = spires[j];
            for (const np of neighbor.platforms) {
              const dist = this.dist2D(p.x, p.z, np.x, np.z);
              const dy = Math.abs(p.y - np.y);

              // Relaxed constraints for rescue
              if (dist < 20 && dy < 15) {
                let metric = dist + dy * 2; // Penalize height
                // STEEPNESS PENALTY: If slope > 1 (dy > dist), it's very hard for A* to build without spiraling.
                // Penalize heavily to prefer cross-spire bridges (dist > dy).
                if (dist < dy) metric += 1000;

                if (metric < minMetric) {
                  minMetric = metric;
                  bestTarget = { sp: neighbor, pl: np, sIdx: j };
                }
              }
            }
          }

          if (bestTarget) {
            // Force build
            // Use direct vector
            const dx = bestTarget.pl.x - p.x;
            const dz = bestTarget.pl.z - p.z;
            const dLen = Math.sqrt(dx * dx + dz * dz);

            // Failsafe for stacked platforms (dLen ~ 0)
            const nx = dLen > 0.001 ? dx / dLen : 1;
            const nz = dLen > 0.001 ? dz / dLen : 0;

            // console.log("Rescue:", {pX: p.x, pY: p.y, pZ: p.z, tX: bestTarget.pl.x, tY: bestTarget.pl.y, tZ: bestTarget.pl.z, sR: s.radius, tR: bestTarget.sp.radius});

            // Use simpler rim points (0.5 penetration)
            const startPt = this.getUncrowdedRimPoint(p, nx, nz, 0.5, s.radius);
            const endPt = this.getUncrowdedRimPoint(
              bestTarget.pl,
              -nx,
              -nz,
              0.5,
              bestTarget.sp.radius
            );

            if (startPt && endPt) {
              // Construct full 3D points
              const start3D = { x: startPt.x, y: p.y, z: startPt.z };
              const end3D = { x: endPt.x, y: bestTarget.pl.y, z: endPt.z };

              if (this.buildBridge(start3D, end3D, { x: s.x, z: s.z }, 0.0)) {
                p.connections.push(startPt);
                bestTarget.pl.connections.push(endPt);

                this.generatedAssets.bridges.push({
                  fromSpire: i,
                  toSpire: bestTarget.sIdx,
                  fromPlatIdx: s.platforms.indexOf(p), // Find index
                  toPlatIdx: spires[bestTarget.sIdx].platforms.indexOf(
                    bestTarget.pl
                  ),
                  start: start3D,
                  end: end3D,
                  yd: Math.abs(end3D.y - start3D.y),
                  dist: dLen - p.radius - bestTarget.pl.radius,
                });
              }

              // console.log(`Rescued Orphan Spire ${i} Plat Y=${p.y} -> Spire ${bestTarget.sIdx} Y=${bestTarget.pl.y}`);
            }
          }
        }
      }

      // COMPONENT LINKING PASS (Global Connectivity)
      // 1. Identify Connected Components
      let allPlats = [];
      spires.forEach((s, sIdx) => {
        s.platforms.forEach((p, pIdx) => {
          p.componentId = -1;
          p.globalId = `${sIdx}:${pIdx}`;
          p.debugSpireIdx = sIdx;
          allPlats.push(p);
        });
      });

      // Graph via Union-Find using Bridges List
      const unionSet = new Map();
      const findRoot = (id) => {
        if (!unionSet.has(id)) unionSet.set(id, id);
        if (unionSet.get(id) === id) return id;
        const root = findRoot(unionSet.get(id));
        unionSet.set(id, root);
        return root;
      };
      const union = (idA, idB) => {
        const rootA = findRoot(idA);
        const rootB = findRoot(idB);
        if (rootA !== rootB) unionSet.set(rootA, rootB);
      };

      // Process existing bridges
      this.generatedAssets.bridges.forEach((b) => {
        // ROBUST: Use indices directly
        if (b.fromPlatIdx !== undefined && b.toPlatIdx !== undefined) {
          const idA = `${b.fromSpire}:${b.fromPlatIdx}`;
          const idB = `${b.toSpire}:${b.toPlatIdx}`;
          union(idA, idB);
        } else {
          // Fallback for any legacy bridges (shouldn't happen with new code)
          let pA = null,
            pB = null;
          const sA = spires[b.fromSpire];
          const sB = spires[b.toSpire];
          for (let i = 0; i < sA.platforms.length; i++) {
            if (Math.abs(sA.platforms[i].y - b.start.y) < 1.0) {
              pA = `${b.fromSpire}:${i}`;
              break;
            }
          }
          for (let i = 0; i < sB.platforms.length; i++) {
            if (Math.abs(sB.platforms[i].y - b.end.y) < 1.0) {
              pB = `${b.toSpire}:${i}`;
              break;
            }
          }
          if (pA && pB) union(pA, pB);
        }
      });

      // Ensure all singular nodes are in the set
      allPlats.forEach((p) => findRoot(p.globalId));

      // Group by Root
      const components = new Map();
      allPlats.forEach((p) => {
        const root = findRoot(p.globalId);
        if (!components.has(root)) components.set(root, []);
        components.get(root).push(p);
      });

      if (components.size > 1) {
        // Sort components by average Y height
        const sortedComps = Array.from(components.values()).sort((cA, cB) => {
          const avgYA = cA.reduce((sum, p) => sum + p.y, 0) / cA.length;
          const avgYB = cB.reduce((sum, p) => sum + p.y, 0) / cB.length;
          return avgYA - avgYB;
        });

        // Chain them! 0->1, 1->2...
        for (let i = 0; i < sortedComps.length - 1; i++) {
          const cA = sortedComps[i];
          const cB = sortedComps[i + 1];

          // Find closest pair between cA and cB
          let bestPair = null;
          let minD = Infinity;

          for (const pA of cA) {
            for (const pB of cB) {
              const dist = this.dist2D(pA.x, pA.z, pB.x, pB.z);
              const dy = Math.abs(pA.y - pB.y);
              let metric = dist + dy * 2;
              // STEEPNESS PENALTY: Prevent impossible same-spire vertical connections
              if (dist < dy) metric += 1000;

              if (metric < minD) {
                minD = metric;
                bestPair = { pA, pB };
              }
            }
          }

          if (bestPair) {
            const { pA, pB } = bestPair;
            // Force Build
            const dx = pB.x - pA.x;
            const dz = pB.z - pA.z;
            const dLen = Math.sqrt(dx * dx + dz * dz);
            // Guard 0 len
            const nx = dLen > 0.001 ? dx / dLen : 1;
            const nz = dLen > 0.001 ? dz / dLen : 0;

            // Get Spire Index for radius
            const sRadA = spires[pA.debugSpireIdx].radius;
            const sRadB = spires[pB.debugSpireIdx].radius;

            const startPt = this.getUncrowdedRimPoint(pA, nx, nz, 0.5, sRadA);
            const endPt = this.getUncrowdedRimPoint(pB, -nx, -nz, 0.5, sRadB);

            if (startPt && endPt) {
              const start3D = { x: startPt.x, y: pA.y, z: startPt.z };
              const end3D = { x: endPt.x, y: pB.y, z: endPt.z };

              const centerA = spires[pA.debugSpireIdx];
              if (
                this.buildBridge(
                  start3D,
                  end3D,
                  { x: centerA.x, z: centerA.z },
                  0.0
                )
              ) {
                pA.connections.push(startPt);
                pB.connections.push(endPt);

                this.generatedAssets.bridges.push({
                  fromSpire: pA.debugSpireIdx,
                  toSpire: pB.debugSpireIdx,
                  start: start3D,
                  end: end3D,
                  yd: Math.abs(end3D.y - start3D.y),
                  dist: dLen - pA.radius - pB.radius,
                });
              }
            }
          }
        }
      }
    }
  }

  /**
   * Tries to find a rim point near the ideal normal (nx, nz) that isn't crowded.
   * @param {number} pillarRadius - The radius of the central pillar to avoid clipping into.
   */
  getUncrowdedRimPoint(platform, nx, nz, penetration = 1.5, pillarRadius = 3) {
    // Angles to try: 0, +20, -20, +40, -40
    const angles = [0, 0.35, -0.35, 0.7, -0.7]; // Radians (approx 20, 40 deg)

    const idealAngle = Math.atan2(nz, nx);

    for (const offset of angles) {
      const angle = idealAngle + offset;
      const rX = Math.cos(angle);
      const rZ = Math.sin(angle);

      // Ensure we don't penetrate into the solid pillar
      // Pillar is at center. We need dist > pillarRadius.
      // Let's enforce min dist = pillarRadius + 2.5 (margin for unit center)
      const validDist = Math.max(
        platform.radius - penetration,
        pillarRadius + 2.5
      );

      const px = platform.x + rX * validDist;
      const pz = platform.z + rZ * validDist;

      let crowded = false;

      // 1. Check existing connections (Hub Spacing)
      for (const c of platform.connections) {
        if (this.dist2D(px, pz, c.x, c.z) < 2.5) {
          // INCREASED from 2.0 to 2.5 to prevent "Wall Effect"
          crowded = true;
          break;
        }
      }

      if (!crowded) {
        // 2. Check Pillar Collision (Don't clip through THIS spire's center)
        // Spire center is platform center (mostly)
        // Ensure the path OUTWARDS doesn't clip.
        // Actually, bridges go OUT. So checking if (px,pz) is inside isn't enough.
        // We need to check the path to the TARGET.
        // We don't have target here, but we can assume generally outwards.
        // But wait, the main issue is clipping *other* spires or the *current* spire if the angle is crazy.
        // Since we are moving along the rim, we are safe from *this* pillar unless we go backwards.
        // The main clipping check needs to happen in connectSpires where we have both points.

        return { x: px, z: pz };
      }
    }
    return null;
  }

  // A* Removed

  /**
   * Generates a bridge using Quadratic Bezier Curve.
   * STRICT ADDITIVE LOGIC (CoA 2): Cannot overwrite non-zero or carve headroom.
   * Returns true if successful, false if blocked.
   */
  generateBezierBridge(pA, pB, bridgeDef) {
    // 1. Calculate Edge Points (Anchors)
    // Vector A->B
    const dx = pB.x - pA.x;
    const dz = pB.z - pA.z;
    const dist = Math.sqrt(dx * dx + dz * dz);
    if (dist < 0.1) return false;

    const nx = dx / dist;
    const nz = dz / dist;

    // Radius offsets (Start at Rim)
    const startX = pA.x + nx * (pA.radius - 0.5);
    const startZ = pA.z + nz * (pA.radius - 0.5);
    const endX = pB.x - nx * (pB.radius - 0.5);
    const endZ = pB.z - nz * (pB.radius - 0.5);

    // Control Point (Midpoint + Offsets)
    const midX = (startX + endX) / 2;
    const midZ = (startZ + endZ) / 2;
    const midY = (pA.y + pB.y) / 2;

    // Arc logic:
    // If short bridge, small arc. If long bridge, larger arc.
    // Randomize slightly for organic feel.
    const arcFactor = dist < 5.0 ? 0.5 : dist / 8.0;
    const archHeight = this.rng.range(1, 4) * arcFactor;

    // Curve logic:
    // Lateral offset relative to distance
    const curveMag = this.rng.range(-dist / 4, dist / 4);
    const cx = midX - nz * curveMag;
    const cz = midZ + nx * curveMag;
    const cy = midY + archHeight;

    const p1 = { x: cx, y: cy, z: cz }; // Control
    const p0 = { x: startX, y: pA.y, z: startZ }; // Start
    const p2 = { x: endX, y: pB.y, z: endZ }; // End

    // Voxelization
    const steps = Math.ceil(dist * 2.5); // Fine sampling
    const pathVoxels = [];
    const visited = new Set();

    for (let i = 0; i <= steps; i++) {
      const t = i / steps;
      const invT = 1 - t;

      // Quadratic Bezier Formula
      const x = invT * invT * p0.x + 2 * invT * t * p1.x + t * t * p2.x;
      const y = invT * invT * p0.y + 2 * invT * t * p1.y + t * t * p2.y;
      const z = invT * invT * p0.z + 2 * invT * t * p1.z + t * t * p2.z;

      const ix = Math.round(x);
      const iy = Math.round(y);
      const iz = Math.round(z);
      const key = `${ix},${iy},${iz}`;

      if (!visited.has(key)) {
        visited.add(key);
        pathVoxels.push({ x: ix, y: iy, z: iz });
      }
    }

    // Validation Loop (The "Simulation")
    for (let i = 0; i < pathVoxels.length; i++) {
      const v = pathVoxels[i];

      // Bounds
      if (!this.grid.isValidBounds(v.x, v.y, v.z)) return false;

      // Check COAs
      const id = this.grid.getCell(v.x, v.y, v.z);
      // Allow overwriting Air (0) or existing Bridge (20) or Teleporter (22)
      // Fail if hitting Stone(1), Crystal(15), etc.
      if (id !== 0 && id !== 20 && id !== 22) {
        // Exception: Anchors. If we are very close to start/end, allows overlap (merged mesh)
        // index check is rough. Distance check is better.
        const dStart =
          (v.x - p0.x) ** 2 + (v.y - p0.y) ** 2 + (v.z - p0.z) ** 2;
        const dEnd = (v.x - p2.x) ** 2 + (v.y - p2.y) ** 2 + (v.z - p2.z) ** 2;
        if (dStart > 4 && dEnd > 4) return false; // Collision mid-air
      }

      // Headroom (Strict 2 blocks Air)
      // We assume we can't carve.
      const h1 = this.grid.getCell(v.x, v.y + 1, v.z);
      const h2 = this.grid.getCell(v.x, v.y + 2, v.z);
      // Headroom must be Empty or Bridge (if intersecting?? No, intersecting bridges is bad unless clearance)
      // Spec: "Bridge MUST NOT be generated if it passes directly underneath another bridge with less than 2 voxels"
      // So if h1 or h2 is Bridge(20), ABORT.
      if (h1 !== 0 || h2 !== 0) return false;

      // Gradient Check (CoA 1)
      if (i > 0) {
        const prev = pathVoxels[i - 1];
        if (Math.abs(v.y - prev.y) > 1) return false; // Step too steep
      }
    }

    // Placement Loop (Commit)
    for (const v of pathVoxels) {
      const id = this.grid.getCell(v.x, v.y, v.z);
      if (id === 0) {
        this.grid.setCell(v.x, v.y, v.z, 20); // Hard Light
      }
    }

    // Store Asset properties
    bridgeDef.start = { x: p0.x, y: p0.y, z: p0.z };
    bridgeDef.end = { x: p2.x, y: p2.y, z: p2.z };
    this.generatedAssets.bridges.push(bridgeDef);

    return true;
  }

  // Alias for compatibility if needed, or update callers to use generateBezierBridge
  // Actually, callers verify logic.
  // I will repurpose buildBridge to wrap this logic.
  buildBridge(start, end, fromCenter = null, minDist = 2.0) {
    // Wrapper for generateBezierBridge
    // Simple Bezier logic for compatibility check
    const p0 = start;
    const p2 = end;

    const midX = (p0.x + p2.x) / 2;
    const midZ = (p0.z + p2.z) / 2;
    const midY = (p0.y + p2.y) / 2;

    const dx = p2.x - p0.x;
    const dz = p2.z - p0.z;
    const dist = Math.sqrt(dx * dx + dz * dz);

    const nx = dx / dist;
    const nz = dz / dist;

    const arcFactor = dist < 5.0 ? 0.5 : dist / 8.0;
    const archHeight = 3.0 * arcFactor; // Simplified random
    const curveMag = 0; // Straight arch for simple builds (or random if rng available?)
    // We don't have RNG access easily here unless we use this.rng
    // Assume this.rng exists.
    if (this.rng) {
      const cm = this.rng.range(-dist / 4, dist / 4);
      const ah = this.rng.range(1, 4) * arcFactor;
      // Recalc
    }

    // Re-use generateBezierBridge logic?
    // "generateBezierBridge" requires pA, pB (Platforms).
    // Here we have points.
    // We'll duplicate logic briefly to ensure it works.

    const p1 = { x: midX, y: midY + archHeight, z: midZ };

    const steps = Math.ceil(dist * 2.5);
    const pathVoxels = [];
    const visited = new Set();

    for (let i = 0; i <= steps; i++) {
      const t = i / steps;
      const invT = 1 - t;
      const x = invT * invT * p0.x + 2 * invT * t * p1.x + t * t * p2.x;
      const y = invT * invT * p0.y + 2 * invT * t * p1.y + t * t * p2.y;
      const z = invT * invT * p0.z + 2 * invT * t * p1.z + t * t * p2.z;
      const ix = Math.round(x);
      const iy = Math.round(y);
      const iz = Math.round(z);
      const key = `${ix},${iy},${iz}`;
      if (!visited.has(key)) {
        visited.add(key);
        pathVoxels.push({ x: ix, y: iy, z: iz });
      }
    }

    // VALIDATION
    for (let i = 0; i < pathVoxels.length; i++) {
      const v = pathVoxels[i];
      if (!this.grid.isValidBounds(v.x, v.y, v.z)) return false;
      const id = this.grid.getCell(v.x, v.y, v.z);
      if (id !== 0 && id !== 20 && id !== 22) {
        const dStart =
          (v.x - p0.x) ** 2 + (v.y - p0.y) ** 2 + (v.z - p0.z) ** 2;
        const dEnd = (v.x - p2.x) ** 2 + (v.y - p2.y) ** 2 + (v.z - p2.z) ** 2;
        if (dStart > 4 && dEnd > 4) return false;
      }
      const h1 = this.grid.getCell(v.x, v.y + 1, v.z);
      const h2 = this.grid.getCell(v.x, v.y + 2, v.z);
      if (h1 !== 0 || h2 !== 0) return false;
      if (i > 0) {
        if (Math.abs(v.y - pathVoxels[i - 1].y) > 1) return false;
      }
    }

    // PLACEMENT
    for (const v of pathVoxels) {
      if (this.grid.getCell(v.x, v.y, v.z) === 0) {
        this.grid.setCell(v.x, v.y, v.z, 20);
      }
    }
    return true;
  }

  // Legacy Removed

  checkCylinderCollision(x1, z1, x2, z2, cx, cz, r) {
    // Check if line segment (x1,z1)-(x2,z2) intersects circle (cx,cz,r)
    // AND is not just touching the endpoints (which are on the rim)

    // Vector d = p2 - p1
    const dx = x2 - x1;
    const dz = z2 - z1;

    // Vector f = p1 - center
    const fx = x1 - cx;
    const fz = z1 - cz;

    const a = dx * dx + dz * dz;
    const b = 2 * (fx * dx + fz * dz);
    const c = fx * fx + fz * fz - r * r;

    let discriminant = b * b - 4 * a * c;

    if (discriminant < 0) return false; // No intersection

    // Check if intersection is within segment (0 <= t <= 1)
    discriminant = Math.sqrt(discriminant);
    const t1 = (-b - discriminant) / (2 * a);
    const t2 = (-b + discriminant) / (2 * a);

    // We ignore intersections at exactly t=0 or t=1 because valid bridges start/end on rims
    // So we check if 0.05 < t < 0.95
    if ((t1 > 0.05 && t1 < 0.95) || (t2 > 0.05 && t2 < 0.95)) {
      return true;
    }

    return false;
  }

  dist2D(x1, z1, x2, z2) {
    return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(z2 - z1, 2));
  }

  /**
   * Scatters clusters of crystal blocks to create jagged cover.
   */
  scatterCrystalClusters(id, density) {
    const validSpots = [];

    // 1. Find valid floor spots
    for (let x = 1; x < this.width - 1; x++) {
      for (let z = 1; z < this.depth - 1; z++) {
        for (let y = 1; y < this.height - 3; y++) {
          // -3 to allow for height
          const currentId = this.grid.getCell(x, y, z);
          const belowId = this.grid.getCell(x, y - 1, z);

          if (currentId === 0 && belowId !== 0 && belowId !== 20) {
            validSpots.push({ x, y, z });
          }
        }
      }
    }

    // 2. Place Clusters
    for (const pos of validSpots) {
      if (this.rng.chance(density)) {
        // Center
        this.grid.setCell(pos.x, pos.y, pos.z, id);

        // 1-3 Extra Blocks for cluster feel
        const extras = this.rng.rangeInt(1, 4);
        for (let i = 0; i < extras; i++) {
          // Random adjacent offset (including up)
          const ox = this.rng.rangeInt(-1, 1);
          const oy = this.rng.rangeInt(0, 1); // Tend slightly upwards
          const oz = this.rng.rangeInt(-1, 1);

          // Don't overwrite if not air
          if (this.grid.getCell(pos.x + ox, pos.y + oy, pos.z + oz) === 0) {
            this.grid.setCell(pos.x + ox, pos.y + oy, pos.z + oz, id);
          }
        }
      }
    }
  }

  markSpawnZone(platform, type) {
    // Scan the disc area for valid standing spots
    const r = platform.radius;
    const y = platform.y;

    for (let x = platform.x - r; x <= platform.x + r; x++) {
      for (let z = platform.z - r; z <= platform.z + r; z++) {
        // Must be within radius
        if (this.dist2D(x, z, platform.x, platform.z) > r - 1) continue; // Stay slightly away from edge

        // Check for solid floor and empty air
        const floorId = this.grid.getCell(x, y, z);
        const air1 = this.grid.getCell(x, y + 1, z);
        // We know we built floor at y, cleared 1,2,3

        if (floorId !== 0 && air1 === 0) {
          this.generatedAssets.spawnZones[type].push({ x, y: y + 1, z });
        }
      }
    }
  }

  /**
   * Post-Generation Verification
   * Replicates game movement logic to ensure every generated bridge is walkable.
   * If a bridge fails (e.g. switchbacks, blockages), it is removed from assets.
   */
  /**
   * Ensure Connectivity via Flood Fill & Teleporters.
   */
  ensureGlobalConnectivity(spires) {
    console.log("Ensure Connectivity running...");
    if (!spires || spires.length === 0) return;

    const adj = new Map();
    const platById = new Map();
    const getPlatId = (sIdx, pIdx) => `${sIdx}:${pIdx}`;

    let nodeCount = 0;
    spires.forEach((s, sIdx) => {
      s.platforms.forEach((p, pIdx) => {
        const id = getPlatId(sIdx, pIdx);
        adj.set(id, { id, p, neighbors: new Set() });
        platById.set(id, p);
        nodeCount++;
      });
    });
    console.log(`Adjacency Graph Nodes: ${nodeCount}`);

    let edgeCount = 0;
    this.generatedAssets.bridges.forEach((b) => {
      if (b.fromPlatIdx !== undefined && b.toPlatIdx !== undefined) {
        const idA = getPlatId(b.fromSpire, b.fromPlatIdx);
        const idB = getPlatId(b.toSpire, b.toPlatIdx);
        if (adj.has(idA) && adj.has(idB)) {
          adj.get(idA).neighbors.add(idB);
          adj.get(idB).neighbors.add(idA);
          edgeCount++;
        }
      } else {
        console.log("Bridge missing indices:", b);
      }
    });
    console.log(`Adjacency Graph Edges: ${edgeCount}`);

    const visited = new Set();
    const components = [];

    for (const [id, node] of adj) {
      if (!visited.has(id)) {
        const comp = [];
        const q = [id];
        visited.add(id);
        while (q.length > 0) {
          const curr = q.shift();
          comp.push(curr);
          adj.get(curr).neighbors.forEach((nId) => {
            if (!visited.has(nId)) {
              visited.add(nId);
              q.push(nId);
            }
          });
        }
        components.push(comp);
      }
    }

    console.log(`Connected Components: ${components.length}`);

    if (components.length > 1) {
      components.sort((a, b) => b.length - a.length);
      const targetComp = components[0];
      const targetNodeId = targetComp[0];
      const targetPlat = platById.get(targetNodeId);

      for (let i = 1; i < components.length; i++) {
        const orphanComp = components[i];
        const orphanNodeId = orphanComp[0];
        const orphanPlat = platById.get(orphanNodeId);

        console.log(
          `Linking Component ${i} (Size ${orphanComp.length}) to Main`
        );
        this.placeTeleporter(targetPlat, orphanPlat);
      }
    }
  }

  placeTeleporter(pA, pB) {
    const findSpot = (p) => {
      const r = p.radius - 1;
      for (let x = Math.round(p.x - r); x <= Math.round(p.x + r); x++) {
        for (let z = Math.round(p.z - r); z <= Math.round(p.z + r); z++) {
          const y = Math.round(p.y);
          const floor = this.grid.getCell(x, y, z);
          const air = this.grid.getCell(x, y + 1, z);
          if (floor !== 0 && air === 0) {
            return { x, y: y + 1, z };
          }
        }
      }
      return null;
    };

    const sA = findSpot(pA);
    const sB = findSpot(pB);

    if (sA && sB) {
      this.grid.setCell(sA.x, sA.y, sA.z, 22);
      this.grid.setCell(sB.x, sB.y, sB.z, 22);
      console.log(
        `Placed Teleporter between (${sA.x},${sA.y},${sA.z}) and (${sB.x},${sB.y},${sB.z})`
      );
    } else {
      console.log("Failed to find spot for Teleporter");
    }
  }
}
