using Theriapolis.Core.Util;
using Theriapolis.Core.World.Polylines;

namespace Theriapolis.Core.World.Generation.Stages;

/// <summary>
/// Stage 17 — RoadNetworkGen
/// Minimum Spanning Tree of Tier 1–3 settlements + 30% shortcuts, routed via A* with
/// full Addendum A §2 exclusion (rivers and rail both excluded).
/// Tier 4 settlements get a footpath/dirt road to the nearest Tier 3+ neighbor.
/// </summary>
public sealed class RoadNetworkGenStage : IWorldGenStage
{
    public string Name => "RoadNetworkGen";

    private const int W = C.WORLD_WIDTH_TILES;
    private const int H = C.WORLD_HEIGHT_TILES;

    // Tiles SmoothStaircases must not reroute through or off of: existing
    // infrastructure, river crossings (bridge tiles), and settlement footprints.
    private const FeatureFlags PathPreserveMask =
        FeatureFlags.HasRoad | FeatureFlags.HasRiver | FeatureFlags.HasRail | FeatureFlags.IsSettlement;

    public void Run(WorldGenContext ctx)
    {
        var world = ctx.World;
        var rng   = ctx.Rngs["road"];

        var nodes = world.Settlements
            .Where(s => s.Tier <= 3 && !s.IsPoi)
            .ToList();

        if (nodes.Count < 2)
        {
            ctx.LogMessage("[RoadNetworkGen] Not enough settlements for road network.");
            return;
        }

        // ── Build MST edges via Kruskal (Euclidean edge weights) ─────────────
        var mstEdges = KruskalMST(nodes);

        // ── Add shortcut edges ────────────────────────────────────────────────
        int shortcutCount = (int)Math.Ceiling(mstEdges.Count * C.ROAD_SHORTCUT_FRACTION);
        var allEdges = AllEdges(nodes);
        allEdges = allEdges
            .Where(e => !mstEdges.Any(m => (m.a == e.a && m.b == e.b) || (m.a == e.b && m.b == e.a)))
            .OrderBy(e => e.weight)
            .Take(shortcutCount)
            .ToList();

        var routeEdges = mstEdges.Concat(allEdges).ToList();

        // ── Route each edge with A* ───────────────────────────────────────────
        var pathfinder = new AStarPathfinder();
        int polyId = 0;

        foreach (var edge in routeEdges)
        {
            var a = nodes[edge.a];
            var b = nodes[edge.b];

            var costFn = RoadCostFn(world, a.TileX, a.TileY, b.TileX, b.TileY);
            var path   = pathfinder.FindPath(a.TileX, a.TileY, b.TileX, b.TileY, costFn);

            if (path == null || path.Count < 2) continue;

            PolylineBuilder.SmoothStaircases(world, path, PathPreserveMask, costFn);

            var segments = PolylineBuilder.SplitByExistingFeature(world, path, FeatureFlags.HasRoad);

            // Ensure the first segment reaches the 'from' settlement and
            // the last segment reaches the 'to' settlement.
            EnsureEndpointSegment(world, segments, path, 0, a.TileX, a.TileY);
            EnsureEndpointSegment(world, segments, path, path.Count - 1, b.TileX, b.TileY);

            foreach (var seg in segments)
            {
                var poly = BuildRoadPolyline(world, seg, polyId++, a, b);
                world.Roads.Add(poly);
                PolylineBuilder.RasterizeToTileFlags(world, poly, setAdjacency: false);
            }
        }

        // ── Tier 4 connections ────────────────────────────────────────────────
        // Footpaths split by existing road so they don't parallel Tier 1-3 roads.
        // The village end gets a stub; the network end just joins at the junction.
        var tier4 = world.Settlements.Where(s => s.Tier == 4 && !s.IsPoi).ToList();
        foreach (var s4 in tier4)
        {
            var nearest = nodes
                .OrderBy(n => Dist(s4.TileX, s4.TileY, n.TileX, n.TileY))
                .FirstOrDefault();
            if (nearest == null) continue;

            var costFn = RoadCostFnRelaxed(world, s4.TileX, s4.TileY, nearest.TileX, nearest.TileY);
            var path   = pathfinder.FindPath(s4.TileX, s4.TileY, nearest.TileX, nearest.TileY, costFn);

            if (path == null || path.Count < 2) continue;

            PolylineBuilder.SmoothStaircases(world, path, PathPreserveMask, costFn);

            var segments = PolylineBuilder.SplitByExistingFeature(world, path, FeatureFlags.HasRoad);
            EnsureEndpointSegment(world, segments, path, 0, s4.TileX, s4.TileY);

            foreach (var seg in segments)
            {
                var poly = BuildRoadPolyline(world, seg, polyId++, s4, nearest, forceTier: 4);
                world.Roads.Add(poly);
                PolylineBuilder.RasterizeToTileFlags(world, poly, setAdjacency: false);
            }
        }

        // Bridge detection is deferred to PolylineCleanupStage so that bridges
        // align with roads after endpoint snapping and interior merging.

        world.StageHashes["RoadNetworkGen"] = world.HashPolylines();
        ctx.LogMessage($"[RoadNetworkGen] Generated {world.Roads.Count} roads.");
    }

    // ── Cost functions ────────────────────────────────────────────────────────

    private static Func<int, int, int, int, byte, float> RoadCostFn(WorldState world, int sx, int sy, int ex, int ey)
    {
        return (fx, fy, tx, ty, entryDir) =>
        {
            if ((uint)tx >= W || (uint)ty >= H) return float.PositiveInfinity;
            ref var tile = ref world.TileAt(tx, ty);

            // Ocean tiles are impassable — no roads over water
            if (tile.Biome == BiomeId.Ocean) return float.PositiveInfinity;

            // Strongly prefer joining an existing road over building a parallel one,
            // but NOT within the settlement halo — each route must find its own path
            // near endpoints to prevent multiple roads fanning through shared tiles.
            if ((tile.Features & FeatureFlags.HasRoad) != 0)
            {
                int dStart = Math.Max(Math.Abs(tx - sx), Math.Abs(ty - sy));
                int dEnd   = Math.Max(Math.Abs(tx - ex), Math.Abs(ty - ey));
                if (dStart > C.SETTLEMENT_HALO_RADIUS && dEnd > C.SETTLEMENT_HALO_RADIUS)
                    return C.EXISTING_ROAD_COST;
                // Within halo: fall through to normal terrain cost
            }

            // Settlement tiles are always reachable (they are road endpoints).
            // Extend Addendum A §2 to the settlement footprint: disallow parallel
            // travel on a rail tile EXCEPT at the actual start/end tile. Without
            // this, road and rail both exit a tier-1+ settlement through the
            // same rail-bearing footprint tiles for several steps, producing
            // visually overlapping smoothed polylines.
            if ((tile.Features & FeatureFlags.IsSettlement) != 0)
            {
                float settleCost = 1f + tile.Elevation * 2f;
                bool isEndpointTile = (tx == sx && ty == sy) || (tx == ex && ty == ey);
                if (!isEndpointTile
                    && (tile.Features & FeatureFlags.HasRail) != 0
                    && tile.RailDir != Dir.None)
                {
                    if (Dir.IsParallel(entryDir, tile.RailDir))
                        return float.PositiveInfinity;
                    if (Dir.IsPerpendicular(entryDir, tile.RailDir))
                        settleCost += C.CROSSING_COST;
                }
                return settleCost;
            }

            // River tiles: perpendicular crossing allowed at bridge cost; parallel travel blocked
            if ((tile.Features & FeatureFlags.HasRiver) != 0)
            {
                if (tile.RiverFlowDir != Dir.None && Dir.IsParallel(entryDir, tile.RiverFlowDir))
                    return float.PositiveInfinity;
                return C.BRIDGE_COST + 1f + tile.Elevation * 2f;
            }

            // Never on a rail tile
            if ((tile.Features & FeatureFlags.HasRail) != 0) return float.PositiveInfinity;

            float cost = 0f;

            // River-adjacent exclusion
            if ((tile.Features & FeatureFlags.RiverAdjacent) != 0 && tile.RiverFlowDir != Dir.None)
            {
                if (Dir.IsParallel(entryDir, tile.RiverFlowDir)) return float.PositiveInfinity;
                if (Dir.IsPerpendicular(entryDir, tile.RiverFlowDir)) cost += C.BRIDGE_COST;
            }

            // Rail-adjacent exclusion
            if ((tile.Features & FeatureFlags.RailroadAdjacent) != 0 && tile.RailDir != Dir.None)
            {
                if (Dir.IsParallel(entryDir, tile.RailDir)) return float.PositiveInfinity;
                if (Dir.IsPerpendicular(entryDir, tile.RailDir)) cost += C.CROSSING_COST;
            }

            // Setback cost — pushes road away from rivers/rail without prohibiting valleys
            int distToFeature = NearestFeatureDist(world, tx, ty);
            if (distToFeature > 0 && distToFeature <= C.SETBACK_DISTANCE)
                cost += C.SETBACK_COST_SCALE / distToFeature;

            // Base terrain cost
            float elev = tile.Elevation;
            cost += 1f + elev * 2f;
            cost += tile.Biome switch
            {
                BiomeId.Wetland or BiomeId.MarshEdge => 3f,
                BiomeId.MountainAlpine              => 6f,
                BiomeId.MountainForested            => 3f,
                _                                   => 0f,
            };

            return cost;
        };
    }

    internal static Func<int, int, int, int, byte, float> RoadCostFnRelaxed(WorldState world, int sx, int sy, int ex, int ey)
    {
        // Tier 4 footpaths — relaxed version (allow river crossing with bridge cost, still block rail)
        return (fx, fy, tx, ty, entryDir) =>
        {
            if ((uint)tx >= W || (uint)ty >= H) return float.PositiveInfinity;
            ref var tile = ref world.TileAt(tx, ty);
            if (tile.Biome == BiomeId.Ocean) return float.PositiveInfinity;

            // Strongly prefer joining an existing road, but not within settlement halo
            if ((tile.Features & FeatureFlags.HasRoad) != 0)
            {
                int dStart = Math.Max(Math.Abs(tx - sx), Math.Abs(ty - sy));
                int dEnd   = Math.Max(Math.Abs(tx - ex), Math.Abs(ty - ey));
                if (dStart > C.SETTLEMENT_HALO_RADIUS && dEnd > C.SETTLEMENT_HALO_RADIUS)
                    return C.EXISTING_ROAD_COST;
            }

            // Settlement tiles are always reachable (they are road endpoints),
            // even when a rail passes through them. Extend Addendum A §2 to the
            // settlement footprint: parallel travel on a rail tile is blocked
            // except at the actual start/end tile.
            if ((tile.Features & FeatureFlags.IsSettlement) != 0)
            {
                float settleCost = 1f + tile.Elevation * 2f;
                bool isEndpointTile = (tx == sx && ty == sy) || (tx == ex && ty == ey);
                if (!isEndpointTile
                    && (tile.Features & FeatureFlags.HasRail) != 0
                    && tile.RailDir != Dir.None)
                {
                    if (Dir.IsParallel(entryDir, tile.RailDir))
                        return float.PositiveInfinity;
                    if (Dir.IsPerpendicular(entryDir, tile.RailDir))
                        settleCost += C.CROSSING_COST;
                }
                return settleCost;
            }

            if ((tile.Features & FeatureFlags.HasRiver) != 0)
                return C.BRIDGE_COST * 0.5f + 1f + tile.Elevation * 2f;
            if ((tile.Features & FeatureFlags.HasRail) != 0) return float.PositiveInfinity;

            float cost = 1f + tile.Elevation * 2f;
            if ((tile.Features & FeatureFlags.RiverAdjacent) != 0) cost += C.BRIDGE_COST * 0.5f;
            return cost;
        };
    }

    private static int NearestFeatureDist(WorldState world, int x, int y)
    {
        for (int r = 1; r <= C.SETBACK_DISTANCE; r++)
        {
            for (int dy = -r; dy <= r; dy++)
            for (int dx = -r; dx <= r; dx++)
            {
                if (Math.Abs(dx) != r && Math.Abs(dy) != r) continue;
                int nx = x + dx, ny = y + dy;
                if ((uint)nx >= W || (uint)ny >= H) continue;
                var f = world.Tiles[nx, ny].Features;
                if ((f & (FeatureFlags.HasRiver | FeatureFlags.HasRail)) != 0) return r;
            }
        }
        return 0;
    }

    // ── MST via Kruskal ───────────────────────────────────────────────────────

    private record Edge(int a, int b, float weight);

    private static List<Edge> KruskalMST(List<Settlement> nodes)
    {
        var edges = AllEdges(nodes);
        edges.Sort((a, b) => a.weight.CompareTo(b.weight));

        int n = nodes.Count;
        var parent = Enumerable.Range(0, n).ToArray();
        int Find(int x) => parent[x] == x ? x : (parent[x] = Find(parent[x]));
        bool Union(int a, int b) { a = Find(a); b = Find(b); if (a == b) return false; parent[a] = b; return true; }

        var mst = new List<Edge>();
        foreach (var e in edges)
        {
            if (Union(e.a, e.b))
                mst.Add(e);
            if (mst.Count == n - 1) break;
        }
        return mst;
    }

    private static List<Edge> AllEdges(List<Settlement> nodes)
    {
        var edges = new List<Edge>();
        for (int i = 0; i < nodes.Count; i++)
        for (int j = i + 1; j < nodes.Count; j++)
        {
            float w = Dist(nodes[i].TileX, nodes[i].TileY, nodes[j].TileX, nodes[j].TileY);
            edges.Add(new Edge(i, j, w));
        }
        return edges;
    }

    /// <summary>
    /// If no segment starts/ends at the settlement tile, add a short stub segment
    /// from the settlement to the nearest existing road tile so the road visually
    /// reaches the settlement icon.
    /// </summary>
    private static void EnsureEndpointSegment(
        WorldState world,
        List<List<(int X, int Y)>> segments,
        List<(int X, int Y)> fullPath,
        int pathIndex,
        int settleX, int settleY)
    {
        var target = fullPath[pathIndex];
        if (target.X != settleX || target.Y != settleY) return;

        // Check if any segment already includes this tile
        foreach (var seg in segments)
        {
            if (seg[0].X == settleX && seg[0].Y == settleY) return;
            if (seg[^1].X == settleX && seg[^1].Y == settleY) return;
        }

        // If the entire path runs along existing roads, a stub would parallel them.
        if (segments.Count == 0) return;

        // If the adjacent segment's junction-anchor tile already has a road, the
        // new path attaches to the existing network at that junction and
        // transitively reaches the settlement through it — adding a stub here
        // would just duplicate the existing road's first/last tiles.
        var junction = pathIndex == 0 ? segments[0][0] : segments[^1][^1];
        if ((world.Tiles[junction.X, junction.Y].Features & FeatureFlags.HasRoad) != 0)
            return;

        // Build a short stub from the settlement to the next few tiles
        var stub = new List<(int X, int Y)>();
        if (pathIndex == 0)
        {
            // Grab the first few tiles of the path as a stub
            int end = Math.Min(fullPath.Count, 4);
            for (int i = 0; i < end; i++) stub.Add(fullPath[i]);
        }
        else
        {
            // Grab the last few tiles of the path as a stub
            int start = Math.Max(0, fullPath.Count - 4);
            for (int i = start; i < fullPath.Count; i++) stub.Add(fullPath[i]);
        }

        if (stub.Count >= 2) segments.Add(stub);
    }

    private static float Dist(int x1, int y1, int x2, int y2)
        => MathF.Sqrt((x1 - x2) * (float)(x1 - x2) + (y1 - y2) * (float)(y1 - y2));

    // ── Bridge detection ─────────────────────────────────────────────────────

    /// <summary>
    /// Detect bridges by finding actual geometric intersections between road and
    /// river polylines in world-pixel space.  Uses a tile-based spatial index on
    /// river segments for efficiency.  Bridges are deduplicated per tile across
    /// all roads so that shared crossings produce a single bridge.
    /// </summary>
    internal static void DetectBridges(WorldState world)
    {
        int px = C.WORLD_TILE_PIXELS;

        // Build spatial index: tile key → river segment references
        var index = new Dictionary<long, List<(int ri, int si)>>();
        for (int ri = 0; ri < world.Rivers.Count; ri++)
        {
            var rpts = world.Rivers[ri].Points;
            for (int si = 0; si < rpts.Count - 1; si++)
            {
                int x0 = (int)(rpts[si].X / px),     y0 = (int)(rpts[si].Y / px);
                int x1 = (int)(rpts[si + 1].X / px), y1 = (int)(rpts[si + 1].Y / px);
                AddToIndex(index, x0, y0, ri, si);
                if (x1 != x0 || y1 != y0) AddToIndex(index, x1, y1, ri, si);
            }
        }

        // Global dedup: one bridge per tile across all roads, so roads sharing a
        // crossing don't stack duplicate bridge sprites.
        var bridgeTiles = new HashSet<long>();

        foreach (var road in world.Roads)
        {
            var pts = road.Points;
            // A polyline needs at least one segment to intersect a river.
            // Short connector stubs created by EnsureSettlementConnectivity
            // can still cross rivers and legitimately need bridges.
            if (pts.Count < 2) continue;

            // Consider every segment. Global tile dedup (bridgeTiles) already
            // prevents duplicate bridges where two roads share a junction
            // crossing. Excluding segments near endpoints would miss legitimate
            // crossings on roads that start from a settlement and cross a river
            // within their first few points.
            //
            // Scan every tile in the segment's bounding box (plus a 1-tile
            // margin), not just the 3×3 neighborhood of pts[i]. A road segment
            // spanning several tiles — e.g. after PolylineCleanup merges/trims
            // points — can cross a river polyline in a tile that neither
            // endpoint's 3×3 neighborhood covers.
            for (int i = 0; i < pts.Count - 1; i++)
            {
                int tx0 = (int)(pts[i].X     / px), ty0 = (int)(pts[i].Y     / px);
                int tx1 = (int)(pts[i + 1].X / px), ty1 = (int)(pts[i + 1].Y / px);
                int minTx = Math.Min(tx0, tx1) - 1;
                int maxTx = Math.Max(tx0, tx1) + 1;
                int minTy = Math.Min(ty0, ty1) - 1;
                int maxTy = Math.Max(ty0, ty1) + 1;

                for (int ty = minTy; ty <= maxTy; ty++)
                for (int tx = minTx; tx <= maxTx; tx++)
                {
                    long key = TileKey(tx, ty);
                    if (!index.TryGetValue(key, out var segs)) continue;

                    foreach (var (ri, si) in segs)
                    {
                        var rpts = world.Rivers[ri].Points;
                        if (!SegmentsIntersect(pts[i], pts[i + 1], rpts[si], rpts[si + 1], out var crossPt, out var tOnRoad))
                            continue;

                        // Deduplicate: one bridge per tile
                        int btx = (int)(crossPt.X / px), bty = (int)(crossPt.Y / px);
                        if (!bridgeTiles.Add(TileKey(btx, bty))) continue;

                        // Walk the actual road polyline in each direction from the
                        // crossing so the deck endpoints sit ON the road instead of
                        // overshooting where meander has bent the path.
                        Vec2 start = WalkAlongPolyline(pts, i, tOnRoad, C.BRIDGE_DECK_HALF_LENGTH, forward: false);
                        Vec2 end   = WalkAlongPolyline(pts, i, tOnRoad, C.BRIDGE_DECK_HALF_LENGTH, forward: true);
                        world.Bridges.Add(new Bridge(start, end, road.Id));
                    }
                }
            }
        }
    }

    private static long TileKey(int tx, int ty) => ((long)ty << 32) | (uint)tx;

    private static void AddToIndex(Dictionary<long, List<(int ri, int si)>> index, int tx, int ty, int ri, int si)
    {
        long key = TileKey(tx, ty);
        if (!index.TryGetValue(key, out var list))
            index[key] = list = new List<(int, int)>();
        list.Add((ri, si));
    }

    private static bool SegmentsIntersect(Vec2 p1, Vec2 p2, Vec2 p3, Vec2 p4, out Vec2 point, out float tOnFirst)
    {
        Vec2 d1 = p2 - p1;
        Vec2 d2 = p4 - p3;
        float cross = d1.X * d2.Y - d1.Y * d2.X;
        if (MathF.Abs(cross) < 1e-6f) { point = default; tOnFirst = 0f; return false; }

        Vec2 d3 = p3 - p1;
        float t = (d3.X * d2.Y - d3.Y * d2.X) / cross;
        float u = (d3.X * d1.Y - d3.Y * d1.X) / cross;

        if (t >= 0f && t <= 1f && u >= 0f && u <= 1f)
        {
            point    = p1 + d1 * t;
            tOnFirst = t;
            return true;
        }
        point    = default;
        tOnFirst = 0f;
        return false;
    }

    /// <summary>
    /// Walk along a polyline starting from a point inside segment <paramref name="segIdx"/>
    /// at parameter <paramref name="segT"/>, travelling <paramref name="distance"/> world
    /// pixels along the actual polyline geometry. Returns the reached point, clamped to
    /// the polyline's end if distance exceeds the remaining length.
    /// </summary>
    private static Vec2 WalkAlongPolyline(List<Vec2> pts, int segIdx, float segT, float distance, bool forward)
    {
        Vec2 current   = pts[segIdx] + (pts[segIdx + 1] - pts[segIdx]) * segT;
        float remaining = distance;

        if (forward)
        {
            int i = segIdx + 1;
            while (remaining > 0f && i < pts.Count)
            {
                Vec2 next = pts[i];
                float segLen = Vec2.Dist(current, next);
                if (segLen >= remaining)
                    return current + (next - current).Normalized * remaining;
                remaining -= segLen;
                current = next;
                i++;
            }
            return current;
        }
        else
        {
            int i = segIdx;
            while (remaining > 0f && i >= 0)
            {
                Vec2 prev = pts[i];
                float segLen = Vec2.Dist(current, prev);
                if (segLen >= remaining)
                    return current + (prev - current).Normalized * remaining;
                remaining -= segLen;
                current = prev;
                i--;
            }
            return current;
        }
    }

    // ── Polyline construction ─────────────────────────────────────────────────

    private static Polyline BuildRoadPolyline(
        WorldState world,
        List<(int X, int Y)> path,
        int id,
        Settlement from, Settlement to,
        int forceTier = 0)
    {
        int effectiveTier = forceTier > 0 ? forceTier : Math.Max(from.Tier, to.Tier);
        return BuildRoadPolyline(world, path, id, from.Id, to.Id, effectiveTier);
    }

    internal static Polyline BuildRoadPolyline(
        WorldState world,
        List<(int X, int Y)> path,
        int id,
        int fromSettlementId,
        int toSettlementId,
        int tier)
    {
        RoadType roadType = tier switch
        {
            1    => RoadType.Highway,
            2    => RoadType.PostRoad,
            3    => RoadType.DirtRoad,
            4    => RoadType.Footpath,
            _    => RoadType.DirtRoad,
        };

        float width = roadType switch
        {
            RoadType.Highway  => 2f,
            RoadType.PostRoad => 1.5f,
            RoadType.DirtRoad => 1f,
            RoadType.Footpath => 0.5f,
            _                 => 1f,
        };

        var poly = new Polyline
        {
            Type               = PolylineType.Road,
            Id                 = id,
            Width              = width,
            RoadClassification = roadType,
            FromSettlementId   = fromSettlementId,
            ToSettlementId     = toSettlementId,
        };

        var controlPts = path.Select(p => PolylineBuilder.TileToWorldPixel(p.X, p.Y)).ToList();
        var startPt    = controlPts[0];
        var endPt      = controlPts[^1];
        var smoothed   = PolylineBuilder.CatmullRomSmooth(controlPts);

        // Roads get moderate meander
        ulong seed = world.WorldSeed ^ C.RNG_ROAD ^ (ulong)id;
        PolylineBuilder.ApplyMeanderNoise(smoothed, 2.5f, 0.06f, seed);

        // Smoothing + meander can drift road points onto river tiles.
        // Push them off unless they are a genuine perpendicular crossing (bridge).
        ConstrainAwayFromRivers(world, smoothed);

        // Pin endpoints so roads always reach their targets
        smoothed[0]  = startPt;
        smoothed[^1] = endPt;

        poly.Points.AddRange(smoothed);
        poly.SimplifiedPoints = PolylineBuilder.RDPSimplify(poly.Points);
        return poly;
    }

    /// <summary>
    /// Push road polyline points off HasRiver tiles when the road is running
    /// parallel to the river. Genuine river crossings — runs whose surrounding
    /// road segments geometrically intersect a river polyline — are left alone
    /// so the bridge-detection pass can place a bridge there.
    ///
    /// Runs of river-tile points that DON'T cross any river polyline indicate
    /// the road is travelling alongside a river (not across it). We handle
    /// those as a unit: pick one perpendicular side clear of river tiles, then
    /// shift every point in the run together. Per-point independent pushing
    /// (an earlier approach) produced zigzags when adjacent points chose
    /// opposite sides.
    /// </summary>
    private static void ConstrainAwayFromRivers(WorldState world, List<Vec2> points)
    {
        int px = C.WORLD_TILE_PIXELS;
        int n  = points.Count;

        var onRiver = new bool[n];
        for (int i = 0; i < n; i++)
        {
            int tx = (int)(points[i].X / px);
            int ty = (int)(points[i].Y / px);
            if ((uint)tx >= W || (uint)ty >= H) continue;
            onRiver[i] = (world.TileAt(tx, ty).Features & FeatureFlags.HasRiver) != 0;
        }

        int k = 1;
        while (k < n - 1)
        {
            if (!onRiver[k]) { k++; continue; }

            int runStart = k;
            int runEnd   = k;
            while (runEnd + 1 < n - 1 && onRiver[runEnd + 1]) runEnd++;

            // Geometric crossing check: if any road segment bordering or inside
            // this run intersects a river polyline, it's a genuine crossing.
            // Leave it alone so bridge detection can place a bridge.
            if (RunCrossesRiver(world, points, runStart - 1, runEnd + 1))
            {
                k = runEnd + 1;
                continue;
            }

            Vec2 anchorPrev = points[runStart - 1];
            Vec2 anchorNext = points[Math.Min(runEnd + 1, n - 1)];
            Vec2 tangent    = anchorNext - anchorPrev;
            if (tangent.LengthSquared < 1e-6f) { k = runEnd + 1; continue; }
            tangent = tangent.Normalized;
            Vec2 perp = tangent.Perp;

            Vec2 chosen = default;
            bool resolved = false;
            for (float dist = px * 0.5f; dist <= px * 3f; dist += px * 0.5f)
            {
                if (AllOffRiver(world, points, runStart, runEnd, perp, dist))
                { chosen = perp * dist; resolved = true; break; }
                if (AllOffRiver(world, points, runStart, runEnd, perp, -dist))
                { chosen = perp * -dist; resolved = true; break; }
            }

            if (resolved)
                for (int i = runStart; i <= runEnd; i++) points[i] = points[i] + chosen;

            k = runEnd + 1;
        }
    }

    private static bool AllOffRiver(WorldState world, List<Vec2> points, int start, int end, Vec2 perp, float dist)
    {
        int px = C.WORLD_TILE_PIXELS;
        for (int i = start; i <= end; i++)
        {
            Vec2 p = points[i] + perp * dist;
            int tx = (int)(p.X / px), ty = (int)(p.Y / px);
            if ((uint)tx >= W || (uint)ty >= H) return false;
            if ((world.TileAt(tx, ty).Features & FeatureFlags.HasRiver) != 0) return false;
        }
        return true;
    }

    /// <summary>
    /// True if any road polyline segment in <c>[segStart, segEnd-1]</c> intersects
    /// any river polyline segment. Uses a bounding-box prefilter on the road-run
    /// region so this stays fast even with long rivers.
    /// </summary>
    private static bool RunCrossesRiver(WorldState world, List<Vec2> points, int segStart, int segEnd)
    {
        segStart = Math.Max(0, segStart);
        segEnd   = Math.Min(points.Count - 1, segEnd);
        if (segEnd <= segStart) return false;

        float minX = float.MaxValue, minY = float.MaxValue;
        float maxX = float.MinValue, maxY = float.MinValue;
        for (int i = segStart; i <= segEnd; i++)
        {
            if (points[i].X < minX) minX = points[i].X;
            if (points[i].Y < minY) minY = points[i].Y;
            if (points[i].X > maxX) maxX = points[i].X;
            if (points[i].Y > maxY) maxY = points[i].Y;
        }
        float margin = C.WORLD_TILE_PIXELS;
        minX -= margin; minY -= margin; maxX += margin; maxY += margin;

        foreach (var river in world.Rivers)
        {
            var rpts = river.Points;
            for (int ri = 0; ri < rpts.Count - 1; ri++)
            {
                Vec2 a = rpts[ri], b = rpts[ri + 1];
                float sxMin = MathF.Min(a.X, b.X), sxMax = MathF.Max(a.X, b.X);
                float syMin = MathF.Min(a.Y, b.Y), syMax = MathF.Max(a.Y, b.Y);
                if (sxMax < minX || sxMin > maxX || syMax < minY || syMin > maxY) continue;

                for (int i = segStart; i < segEnd; i++)
                    if (SegmentsCross(points[i], points[i + 1], a, b)) return true;
            }
        }
        return false;
    }

    private static bool SegmentsCross(Vec2 p1, Vec2 p2, Vec2 p3, Vec2 p4)
    {
        Vec2 d1 = p2 - p1;
        Vec2 d2 = p4 - p3;
        float cross = d1.X * d2.Y - d1.Y * d2.X;
        if (MathF.Abs(cross) < 1e-6f) return false;
        Vec2 d3 = p3 - p1;
        float t = (d3.X * d2.Y - d3.Y * d2.X) / cross;
        float u = (d3.X * d1.Y - d3.Y * d1.X) / cross;
        return t >= 0f && t <= 1f && u >= 0f && u <= 1f;
    }
}
