using System.Globalization;
using System.Linq;
using System.Text.Json;
using TakeoutSaaS.Application.App.Stores.Dto;
using TakeoutSaaS.Application.App.Stores.Services;
using TakeoutSaaS.Domain.Stores.Entities;

namespace TakeoutSaaS.Infrastructure.App.Services;

/// <summary>
/// 配送范围检测服务实现。
/// </summary>
public sealed class DeliveryZoneService : IDeliveryZoneService
{
    private const double CoordinateTolerance = 1e-6;

    /// <inheritdoc />
    public StoreDeliveryCheckResultDto CheckPointInZones(
        IReadOnlyList<StoreDeliveryZone> zones,
        double longitude,
        double latitude)
    {
        // 1. 无配送区域直接返回
        if (zones is null || zones.Count == 0)
        {
            return new StoreDeliveryCheckResultDto { InRange = false };
        }
        // 2. (空行后) 逐个检测多边形命中
        foreach (var zone in zones)
        {
            if (!TryReadPolygons(zone.PolygonGeoJson, out var polygons))
            {
                continue;
            }
            foreach (var polygon in polygons)
            {
                if (!IsPointInPolygon(polygon, longitude, latitude))
                {
                    continue;
                }
                return new StoreDeliveryCheckResultDto
                {
                    InRange = true,
                    DeliveryZoneId = zone.Id,
                    DeliveryZoneName = zone.ZoneName
                };
            }
        }
        // 3. (空行后) 未命中任何区域
        return new StoreDeliveryCheckResultDto { InRange = false };
    }

    private static bool TryReadPolygons(string geoJson, out List<Polygon> polygons)
    {
        polygons = [];
        if (string.IsNullOrWhiteSpace(geoJson))
        {
            return false;
        }
        try
        {
            using var document = JsonDocument.Parse(geoJson);
            if (!TryReadPolygons(document.RootElement, polygons))
            {
                return false;
            }
            return polygons.Count > 0;
        }
        catch (JsonException)
        {
            return false;
        }
    }

    private static bool TryReadPolygons(JsonElement root, ICollection<Polygon> polygons)
    {
        if (root.ValueKind == JsonValueKind.String)
        {
            var inner = root.GetString();
            if (string.IsNullOrWhiteSpace(inner))
            {
                return false;
            }
            using var innerDocument = JsonDocument.Parse(inner);
            return TryReadPolygons(innerDocument.RootElement, polygons);
        }

        if (root.ValueKind != JsonValueKind.Object)
        {
            return false;
        }

        if (!TryGetPropertyIgnoreCase(root, "type", out var typeElement)
            || typeElement.ValueKind != JsonValueKind.String)
        {
            return false;
        }

        var type = typeElement.GetString();
        if (string.Equals(type, "FeatureCollection", StringComparison.OrdinalIgnoreCase))
        {
            if (!TryGetPropertyIgnoreCase(root, "features", out var featuresElement)
                || featuresElement.ValueKind != JsonValueKind.Array)
            {
                return false;
            }

            foreach (var featureElement in featuresElement.EnumerateArray())
            {
                if (featureElement.ValueKind != JsonValueKind.Object)
                {
                    continue;
                }
                if (!TryGetPropertyIgnoreCase(featureElement, "geometry", out var geometryElement))
                {
                    continue;
                }
                TryReadPolygons(geometryElement, polygons);
            }
            return polygons.Count > 0;
        }

        if (string.Equals(type, "Feature", StringComparison.OrdinalIgnoreCase))
        {
            if (!TryGetPropertyIgnoreCase(root, "geometry", out var geometryElement))
            {
                return false;
            }
            return TryReadPolygons(geometryElement, polygons);
        }

        if (string.Equals(type, "Polygon", StringComparison.OrdinalIgnoreCase))
        {
            if (!TryGetPropertyIgnoreCase(root, "coordinates", out var coordinatesElement))
            {
                return false;
            }
            if (!TryReadPolygonFromCoordinates(coordinatesElement, out var polygon))
            {
                return false;
            }
            polygons.Add(polygon);
            return true;
        }

        if (string.Equals(type, "MultiPolygon", StringComparison.OrdinalIgnoreCase))
        {
            if (!TryGetPropertyIgnoreCase(root, "coordinates", out var coordinatesElement)
                || coordinatesElement.ValueKind != JsonValueKind.Array)
            {
                return false;
            }

            foreach (var polygonElement in coordinatesElement.EnumerateArray())
            {
                if (!TryReadPolygonFromCoordinates(polygonElement, out var polygon))
                {
                    continue;
                }
                polygons.Add(polygon);
            }
            return polygons.Count > 0;
        }

        return false;
    }

    private static bool TryReadPolygonFromCoordinates(JsonElement coordinatesElement, out Polygon polygon)
    {
        polygon = default!;
        if (!TryReadRings(coordinatesElement, out var rings) || rings.Count == 0)
        {
            return false;
        }
        var outer = rings[0];
        if (outer.Count < 3)
        {
            return false;
        }
        var holes = rings.Count > 1 ? rings.Skip(1).ToList() : [];
        polygon = new Polygon(outer, holes);
        return true;
    }

    private static bool TryReadRings(JsonElement element, out List<List<Point>> rings)
    {
        rings = [];
        if (element.ValueKind != JsonValueKind.Array || element.GetArrayLength() == 0)
        {
            return false;
        }

        if (IsPositionArray(element[0]))
        {
            if (!TryReadRing(element, out var ring))
            {
                return false;
            }
            rings.Add(ring);
            return true;
        }

        foreach (var ringElement in element.EnumerateArray())
        {
            if (!TryReadRing(ringElement, out var ring))
            {
                return false;
            }
            rings.Add(ring);
        }

        return rings.Count > 0;
    }

    private static bool TryReadRing(JsonElement ringElement, out List<Point> ring)
    {
        ring = [];
        if (ringElement.ValueKind != JsonValueKind.Array)
        {
            return false;
        }

        foreach (var pointElement in ringElement.EnumerateArray())
        {
            if (!TryReadPosition(pointElement, out var point))
            {
                return false;
            }
            ring.Add(point);
        }

        if (ring.Count >= 2 && AreSamePoint(ring[0], ring[^1]))
        {
            ring.RemoveAt(ring.Count - 1);
        }

        return ring.Count >= 3;
    }

    private static bool TryReadPosition(JsonElement pointElement, out Point point)
    {
        point = default;
        if (pointElement.ValueKind != JsonValueKind.Array || pointElement.GetArrayLength() < 2)
        {
            return false;
        }

        if (!TryGetCoordinate(pointElement[0], out var longitude)
            || !TryGetCoordinate(pointElement[1], out var latitude))
        {
            return false;
        }

        point = new Point(longitude, latitude);
        return true;
    }

    private static bool TryGetCoordinate(JsonElement element, out double value)
    {
        value = 0;
        if (element.ValueKind == JsonValueKind.Number)
        {
            return element.TryGetDouble(out value);
        }
        if (element.ValueKind == JsonValueKind.String)
        {
            return double.TryParse(element.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out value);
        }
        return false;
    }

    private static bool IsPositionArray(JsonElement element)
        => element.ValueKind == JsonValueKind.Array
           && element.GetArrayLength() >= 2
           && TryGetCoordinate(element[0], out _)
           && TryGetCoordinate(element[1], out _);

    private static bool TryGetPropertyIgnoreCase(JsonElement element, string propertyName, out JsonElement value)
    {
        if (element.TryGetProperty(propertyName, out value))
        {
            return true;
        }

        foreach (var property in element.EnumerateObject())
        {
            if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase))
            {
                value = property.Value;
                return true;
            }
        }

        value = default;
        return false;
    }

    private static bool IsPointInPolygon(Polygon polygon, double x, double y)
    {
        if (!IsPointInRing(polygon.Outer, x, y))
        {
            return false;
        }

        foreach (var hole in polygon.Holes)
        {
            if (IsPointInRing(hole, x, y))
            {
                return false;
            }
        }

        return true;
    }

    private static bool IsPointInRing(IReadOnlyList<Point> ring, double x, double y)
    {
        if (IsPointOnBoundary(ring, x, y))
        {
            return true;
        }

        var inside = false;
        for (var i = 0; i < ring.Count; i++)
        {
            var j = i == 0 ? ring.Count - 1 : i - 1;
            var xi = ring[i].Longitude;
            var yi = ring[i].Latitude;
            var xj = ring[j].Longitude;
            var yj = ring[j].Latitude;
            var intersects = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi + double.Epsilon) + xi);
            if (intersects)
            {
                inside = !inside;
            }
        }

        return inside;
    }

    private static bool IsPointOnBoundary(IReadOnlyList<Point> ring, double x, double y)
    {
        for (var i = 0; i < ring.Count; i++)
        {
            var j = i == ring.Count - 1 ? 0 : i + 1;
            if (IsPointOnSegment(ring[i], ring[j], x, y))
            {
                return true;
            }
        }
        return false;
    }

    private static bool IsPointOnSegment(Point start, Point end, double x, double y)
    {
        var cross = (end.Longitude - start.Longitude) * (y - start.Latitude)
                    - (end.Latitude - start.Latitude) * (x - start.Longitude);
        if (Math.Abs(cross) > CoordinateTolerance)
        {
            return false;
        }

        var dot = (x - start.Longitude) * (x - end.Longitude)
                  + (y - start.Latitude) * (y - end.Latitude);
        return dot <= CoordinateTolerance;
    }

    private static bool AreSamePoint(Point first, Point second)
        => Math.Abs(first.Longitude - second.Longitude) <= CoordinateTolerance
           && Math.Abs(first.Latitude - second.Latitude) <= CoordinateTolerance;

    private readonly record struct Point(double Longitude, double Latitude);

    private sealed record Polygon(IReadOnlyList<Point> Outer, IReadOnlyList<IReadOnlyList<Point>> Holes);
}
