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

namespace Theriapolis.Core.World.Generation.Stages;

/// <summary>
/// Stage 16 — RailNetworkGen
/// A* from the capital (Sanctum Fidelis) to each Tier 2 city (except Heartstone).
/// Plus one transcontinental line: easternmost coastal Tier 2/3 to westernmost Tier 2.
/// Addendum A §2 exclusion enforced: rivers are impassable, river-adjacent parallel is INFINITY.
/// T-junctions are emitted as railway wyes: each sub-path endpoint sitting on
/// existing rail forks into two polylines whose visible track extends onto the
/// main line in opposite tangent directions, producing the familiar Y geometry
/// rather than a raw perpendicular corner.
/// </summary>
public sealed class RailNetworkGenStage : IWorldGenStage
{
    public string Name => "RailNetworkGen";

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

    private const FeatureFlags PathPreserveMask =
        FeatureFlags.HasRoad | FeatureFlags.HasRiver | FeatureFlags.HasRail | FeatureFlags.IsSettlement;

    // Railway-wye leg dimensions. When a sub-path endpoint sits on pre-existing
    // rail (not a settlement), EmitRailSubPath emits two polylines whose visible
    // track extends WYE_LEG_LENGTH_PX past the junction tile along ±t of the
    // main line. A ghost control point WYE_LEG_GHOST_PX further out biases the
    // Catmull-Rom tangent at the leg end toward the main line's direction,
    // producing a smooth ~45° merge. Both are 2 tiles — long enough to read as
    // a wye, short enough to stay within the junction's local area.
    private const float WYE_LEG_LENGTH_PX = 2f * C.WORLD_TILE_PIXELS;
    private const float WYE_LEG_GHOST_PX  = 2f * C.WORLD_TILE_PIXELS;

    public void Run(WorldGenContext ctx)
    {
        if (!C.ENABLE_RAIL)
        {
            ctx.LogMessage("[RailNetworkGen] Rail disabled via C.ENABLE_RAIL — skipping.");
            return;
        }

        var world    = ctx.World;
        var pathfinder = new AStarPathfinder();

        // Find capital
        var capital = world.Settlements.FirstOrDefault(s => s.Anchor == NarrativeAnchor.SanctumFidelis);
        if (capital == null)
        {
            ctx.LogMessage("[RailNetworkGen] No capital found — skipping rail.");
            return;
        }

        int polyId = 0;

        // ── Capital → each Tier 2 city (except Heartstone) ──────────────────
        var tier2 = world.Settlements
            .Where(s => s.Tier <= 2 && s != capital && s.Anchor != NarrativeAnchor.Heartstone)
            .ToList();

        foreach (var dest in tier2)
        {
            var costFn = RailCostFn(world);
            var path   = pathfinder.FindPath(capital.TileX, capital.TileY, dest.TileX, dest.TileY, costFn);

            if (path == null) { ctx.LogMessage($"[RailNetworkGen] No path to {dest.Name}"); continue; }

            PolylineBuilder.SmoothStaircases(world, path, PathPreserveMask, costFn);
            PolylineBuilder.LimitTurnAngle(world, path, C.MAX_RAIL_TURN_DEGREES, PathPreserveMask, costFn);

            var segments = PolylineBuilder.SplitByExistingFeature(world, path, FeatureFlags.HasRail);
            foreach (var seg in segments)
                EmitRailSubPath(world, seg, ref polyId, capital.Id, dest.Id);
            dest.HasRailStation = true;
        }

        capital.HasRailStation = true;

        // ── Transcontinental line ─────────────────────────────────────────────
        var eastPort = world.Settlements
            .Where(s => s.Tier <= 3 && !s.IsPoi &&
                        (world.Tiles[Math.Clamp(s.TileX, 0, W-1), Math.Clamp(s.TileY, 0, H-1)].Features
                         & (FeatureFlags.IsCoast | FeatureFlags.HasRiver)) != 0)
            .OrderByDescending(s => s.TileX)
            .FirstOrDefault();

        var westTier2 = world.Settlements
            .Where(s => s.Tier == 2 && s.Anchor != NarrativeAnchor.Heartstone)
            .OrderBy(s => s.TileX)
            .FirstOrDefault();

        if (eastPort != null && westTier2 != null && eastPort != westTier2)
        {
            var costFn = RailCostFn(world);
            var path   = pathfinder.FindPath(eastPort.TileX, eastPort.TileY, westTier2.TileX, westTier2.TileY, costFn);

            if (path != null)
            {
                PolylineBuilder.SmoothStaircases(world, path, PathPreserveMask, costFn);
                PolylineBuilder.LimitTurnAngle(world, path, C.MAX_RAIL_TURN_DEGREES, PathPreserveMask, costFn);

                var segments = PolylineBuilder.SplitByExistingFeature(world, path, FeatureFlags.HasRail);
                foreach (var seg in segments)
                    EmitRailSubPath(world, seg, ref polyId, eastPort.Id, westTier2.Id);
            }
        }

        // Verify Heartstone is not near rail
        var heartstone = world.Settlements.FirstOrDefault(s => s.Anchor == NarrativeAnchor.Heartstone);
        if (heartstone != null)
        {
            ref var ht = ref world.TileAt(heartstone.TileX, heartstone.TileY);
            if ((ht.Features & FeatureFlags.HasRail) != 0 ||
                (ht.Features & FeatureFlags.RailroadAdjacent) != 0)
                ctx.LogMessage("[RailNetworkGen] WARNING: Heartstone has rail adjacent — anchor placement issue.");
        }

        world.StageHashes["RailNetworkGen"] = world.HashPolylines();
        ctx.LogMessage($"[RailNetworkGen] Generated {world.Rails.Count} rail lines.");
    }

    // ── Cost function ─────────────────────────────────────────────────────────

    private static Func<int, int, int, int, byte, float> RailCostFn(WorldState world)
    {
        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 rail over water
            if (tile.Biome == BiomeId.Ocean) return float.PositiveInfinity;

            // Rails are engineered infrastructure that share tracks — always prefer
            // joining existing rail (no settlement halo, unlike roads).
            if ((tile.Features & FeatureFlags.HasRail) != 0) return C.EXISTING_RAIL_COST;

            // 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.RAIL_BRIDGE_COST + 1f + tile.Elevation * 4f;
            }

            // River-adjacent: parallel = INFINITY, perpendicular = RAIL_BRIDGE_COST
            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))
                    return C.RAIL_BRIDGE_COST;
            }

            // Base terrain cost (mountains expensive for rail)
            float elev = tile.Elevation;
            float terrainCost = 1f + elev * 4f;

            // Biome modifiers
            terrainCost += tile.Biome switch
            {
                BiomeId.Wetland or BiomeId.MarshEdge => 5f,
                BiomeId.MountainAlpine              => 8f,
                BiomeId.MountainForested            => 4f,
                _                                   => 0f,
            };

            return terrainCost;
        };
    }

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

    /// <summary>
    /// Emits one, two, or four polylines for a single sub-path depending on how
    /// many endpoints sit on pre-existing rail. A junction endpoint produces a
    /// wye: two polylines whose visible track extends in opposite tangent
    /// directions onto the main line, giving the familiar railway "Y" instead
    /// of a raw T. Non-junction endpoints contribute a single null tangent
    /// (one polyline) to the Cartesian product.
    /// </summary>
    private static void EmitRailSubPath(
        WorldState world,
        List<(int X, int Y)> seg,
        ref int polyId,
        int fromId, int toId)
    {
        if (seg.Count < 2) return;

        bool startIsJunction =
            (world.Tiles[seg[0].X, seg[0].Y].Features & FeatureFlags.HasRail) != 0 &&
            (world.Tiles[seg[0].X, seg[0].Y].Features & FeatureFlags.IsSettlement) == 0;
        bool endIsJunction =
            (world.Tiles[seg[^1].X, seg[^1].Y].Features & FeatureFlags.HasRail) != 0 &&
            (world.Tiles[seg[^1].X, seg[^1].Y].Features & FeatureFlags.IsSettlement) == 0;

        Vec2? startT = null, endT = null;
        if (startIsJunction)
        {
            var px = PolylineBuilder.TileToWorldPixel(seg[0].X, seg[0].Y);
            var t  = GetExistingRailTangent(world, px, fromId, toId);
            if (t.LengthSquared > 0.1f) startT = t;
        }
        if (endIsJunction)
        {
            var px = PolylineBuilder.TileToWorldPixel(seg[^1].X, seg[^1].Y);
            var t  = GetExistingRailTangent(world, px, fromId, toId);
            if (t.LengthSquared > 0.1f) endT = t;
        }

        Vec2?[] startDirs = startT.HasValue
            ? new Vec2?[] { startT.Value, new Vec2(-startT.Value.X, -startT.Value.Y) }
            : new Vec2?[] { null };
        Vec2?[] endDirs = endT.HasValue
            ? new Vec2?[] { endT.Value, new Vec2(-endT.Value.X, -endT.Value.Y) }
            : new Vec2?[] { null };

        foreach (var sd in startDirs)
        foreach (var ed in endDirs)
        {
            var poly = BuildRailPolyline(world, seg, polyId++, fromId, toId, sd, ed);
            world.Rails.Add(poly);
            PolylineBuilder.RasterizeToTileFlags(world, poly, setAdjacency: true);
        }
    }

    private static Polyline BuildRailPolyline(
        WorldState world,
        List<(int X, int Y)> path,
        int id, int fromId, int toId,
        Vec2? startWyeDir,
        Vec2? endWyeDir)
    {
        var poly = new Polyline
        {
            Type              = PolylineType.Rail,
            Id                = id,
            Width             = 1.5f,
            FromSettlementId  = fromId,
            ToSettlementId    = toId,
        };

        var controlPts = path.Select(p => PolylineBuilder.TileToWorldPixel(p.X, p.Y)).ToList();
        var origStart  = controlPts[0];
        var origEnd    = controlPts[^1];

        // Build the branch-body curve from the raw control points with no wye
        // ghosts inserted. This is the geometry shared between the two legs of
        // a wye. If the ghost control point were inserted here, its ±t
        // direction would propagate backward through Catmull-Rom's tangent
        // calculation into the segment beyond origStart (since legEnd would
        // become p0 for that segment), giving the +t and -t legs different
        // paths along the shared body — the "gore" artifact.
        var smoothed = PolylineBuilder.CatmullRomSmooth(controlPts);

        // Wye-leg splicing. Each leg-end replaces the main curve's endpoint
        // with a short Catmull-Rom curve (legEnd → origStart) computed
        // separately. The main curve is preserved verbatim from origStart
        // onward, guaranteeing identical branch-body geometry across both
        // legs of a wye.
        Vec2 effectiveStart = origStart;
        Vec2 effectiveEnd   = origEnd;

        if (startWyeDir.HasValue && controlPts.Count >= 2)
        {
            var d      = startWyeDir.Value;
            var legEnd = origStart + d * WYE_LEG_LENGTH_PX;
            var ghost  = legEnd    + d * WYE_LEG_GHOST_PX;
            // Segment 1 of [ghost, legEnd, origStart, controlPts[1]] is the
            // legEnd→origStart leg curve; its indices are
            // [SPLINE_SUBDIVISIONS .. 2*SPLINE_SUBDIVISIONS] inclusive
            // (legEnd, interior×(SUB−1), origStart).
            var legSmooth  = PolylineBuilder.CatmullRomSmooth(
                new List<Vec2> { ghost, legEnd, origStart, controlPts[1] });
            var legSegment = legSmooth.GetRange(C.SPLINE_SUBDIVISIONS, C.SPLINE_SUBDIVISIONS + 1);
            smoothed.RemoveAt(0);                   // drop duplicate origStart
            smoothed.InsertRange(0, legSegment);
            effectiveStart = legEnd;
        }

        if (endWyeDir.HasValue && controlPts.Count >= 2)
        {
            var d      = endWyeDir.Value;
            var legEnd = origEnd + d * WYE_LEG_LENGTH_PX;
            var ghost  = legEnd  + d * WYE_LEG_GHOST_PX;
            // Mirror: segment 1 of [controlPts[^2], origEnd, legEnd, ghost]
            // is the origEnd→legEnd leg curve.
            var legSmooth  = PolylineBuilder.CatmullRomSmooth(
                new List<Vec2> { controlPts[^2], origEnd, legEnd, ghost });
            var legSegment = legSmooth.GetRange(C.SPLINE_SUBDIVISIONS, C.SPLINE_SUBDIVISIONS + 1);
            smoothed.RemoveAt(smoothed.Count - 1);  // drop duplicate origEnd
            smoothed.AddRange(legSegment);
            effectiveEnd = legEnd;
        }

        // Rail has low meander (engineered). Applied after splicing so leg-end
        // pixels get noise perpendicular to the leg tangent rather than branch.
        ulong seed = world.WorldSeed ^ C.RNG_RAIL ^ (ulong)id;
        PolylineBuilder.ApplyMeanderNoise(smoothed, 1.5f, 0.04f, seed);

        // Pin endpoints exactly so meander drift doesn't misalign with the
        // main rail (wye case) or the destination settlement (non-wye case).
        smoothed[0]  = effectiveStart;
        smoothed[^1] = effectiveEnd;

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

    /// <summary>
    /// Returns the unit tangent of the nearest pre-existing rail polyline at a
    /// junction world-pixel position. Rails sharing both (fromId, toId) with the
    /// sub-path being built are skipped so sibling sub-paths of the same trip
    /// (which also terminate at this junction) don't poison the lookup with
    /// their own perpendicular approach direction. Returns a zero vector when
    /// no existing rail is within 2 tiles — caller should then skip ghost
    /// insertion and fall back to raw endpoint smoothing.
    /// </summary>
    private static Vec2 GetExistingRailTangent(
        WorldState world,
        Vec2 junction,
        int skipFromId,
        int skipToId)
    {
        Polyline? foundRail = null;
        int   bestIdx     = 0;
        float bestDistSq  = float.MaxValue;
        float maxRadiusSq = 4f * C.WORLD_TILE_PIXELS * C.WORLD_TILE_PIXELS; // 2 tiles

        foreach (var rail in world.Rails)
        {
            if (rail.FromSettlementId == skipFromId && rail.ToSettlementId == skipToId) continue;
            var pts = rail.Points;
            for (int i = 0; i < pts.Count; i++)
            {
                float d = Vec2.DistSq(pts[i], junction);
                if (d < bestDistSq) { bestDistSq = d; foundRail = rail; bestIdx = i; }
            }
        }

        if (foundRail == null || foundRail.Points.Count < 2) return new Vec2(0, 0);
        if (bestDistSq > maxRadiusSq) return new Vec2(0, 0);

        var fp = foundRail.Points;
        Vec2 tangent;
        if (bestIdx == 0)
            tangent = fp[1] - fp[0];
        else if (bestIdx == fp.Count - 1)
            tangent = fp[^1] - fp[^2];
        else
            tangent = fp[bestIdx + 1] - fp[bestIdx - 1];

        return tangent.Normalized;
    }
}
