using System.Text.Json;
using TakeoutSaaS.Application.App.Stores.Services;

namespace TakeoutSaaS.Infrastructure.App.Services;

/// <summary>
/// GeoJSON 校验服务实现。
/// </summary>
public sealed class GeoJsonValidationService : IGeoJsonValidationService
{
    private const double CoordinateTolerance = 1e-6;

    /// <inheritdoc />
    public GeoJsonValidationResult ValidatePolygon(string geoJson)
    {
        // 1. 基础校验
        if (string.IsNullOrWhiteSpace(geoJson))
        {
            return BuildInvalid("GeoJSON 不能为空");
        }
        // 2. (空行后) 解析与验证结构
        try
        {
            using var document = JsonDocument.Parse(geoJson);
            var root = document.RootElement;
            if (root.ValueKind != JsonValueKind.Object)
            {
                return BuildInvalid("GeoJSON 格式错误");
            }
            if (!root.TryGetProperty("type", out var typeElement) || typeElement.ValueKind != JsonValueKind.String)
            {
                return BuildInvalid("GeoJSON 缺少 type");
            }
            var type = typeElement.GetString();
            if (!string.Equals(type, "Polygon", StringComparison.OrdinalIgnoreCase))
            {
                return BuildInvalid("仅支持 Polygon 类型");
            }
            if (!root.TryGetProperty("coordinates", out var coordinatesElement) || coordinatesElement.ValueKind != JsonValueKind.Array)
            {
                return BuildInvalid("GeoJSON 缺少 coordinates");
            }
            if (coordinatesElement.GetArrayLength() == 0)
            {
                return BuildInvalid("GeoJSON coordinates 为空");
            }
            var ringElement = coordinatesElement[0];
            if (ringElement.ValueKind != JsonValueKind.Array)
            {
                return BuildInvalid("GeoJSON 坐标格式错误");
            }
            var points = new List<Point>();
            foreach (var pointElement in ringElement.EnumerateArray())
            {
                if (pointElement.ValueKind != JsonValueKind.Array || pointElement.GetArrayLength() < 2)
                {
                    return BuildInvalid("坐标点格式错误");
                }
                if (!pointElement[0].TryGetDouble(out var longitude) || !pointElement[1].TryGetDouble(out var latitude))
                {
                    return BuildInvalid("坐标点必须为数值");
                }
                points.Add(new Point(longitude, latitude));
            }
            if (points.Count < 3)
            {
                return BuildInvalid("多边形至少需要 3 个点");
            }
            var distinctCount = CountDistinct(points);
            if (distinctCount < 3)
            {
                return BuildInvalid("多边形坐标点不足");
            }
            var normalized = Normalize(points, out var normalizedJson);
            if (normalized.Count < 4)
            {
                return BuildInvalid("多边形至少需要 4 个点（含闭合点）");
            }
            if (HasSelfIntersection(normalized))
            {
                return BuildInvalid("多边形存在自相交");
            }
            return new GeoJsonValidationResult
            {
                IsValid = true,
                NormalizedGeoJson = normalizedJson
            };
        }
        catch (JsonException)
        {
            return BuildInvalid("GeoJSON 解析失败");
        }
    }

    private static GeoJsonValidationResult BuildInvalid(string message) => new()
    {
        IsValid = false,
        ErrorMessage = message
    };

    private static int CountDistinct(IReadOnlyList<Point> points)
    {
        var distinct = new List<Point>();
        foreach (var point in points)
        {
            if (distinct.Any(existing => AreSamePoint(existing, point)))
            {
                continue;
            }
            distinct.Add(point);
        }
        return distinct.Count;
    }

    private static List<Point> Normalize(IReadOnlyList<Point> points, out string? normalizedJson)
    {
        var normalized = new List<Point>(points);
        if (!AreSamePoint(normalized[0], normalized[^1]))
        {
            normalized.Add(normalized[0]);
            normalizedJson = BuildGeoJson(normalized);
            return normalized;
        }
        normalizedJson = null;
        return normalized;
    }

    private static string BuildGeoJson(IReadOnlyList<Point> points)
    {
        var coordinates = points
            .Select(point => new[] { point.Longitude, point.Latitude })
            .ToArray();
        var payload = new Dictionary<string, object?>
        {
            { "type", "Polygon" },
            { "coordinates", new[] { coordinates } }
        };
        return JsonSerializer.Serialize(payload);
    }

    private static bool HasSelfIntersection(IReadOnlyList<Point> points)
    {
        var segmentCount = points.Count - 1;
        for (var i = 0; i < segmentCount; i++)
        {
            var a1 = points[i];
            var a2 = points[i + 1];
            for (var j = i + 1; j < segmentCount; j++)
            {
                if (Math.Abs(i - j) <= 1)
                {
                    continue;
                }
                if (i == 0 && j == segmentCount - 1)
                {
                    continue;
                }
                var b1 = points[j];
                var b2 = points[j + 1];
                if (SegmentsIntersect(a1, a2, b1, b2))
                {
                    return true;
                }
            }
        }
        return false;
    }

    private static bool SegmentsIntersect(Point p1, Point q1, Point p2, Point q2)
    {
        var o1 = Orientation(p1, q1, p2);
        var o2 = Orientation(p1, q1, q2);
        var o3 = Orientation(p2, q2, p1);
        var o4 = Orientation(p2, q2, q1);

        if (o1 != o2 && o3 != o4)
        {
            return true;
        }
        if (o1 == 0 && OnSegment(p1, p2, q1))
        {
            return true;
        }
        if (o2 == 0 && OnSegment(p1, q2, q1))
        {
            return true;
        }
        if (o3 == 0 && OnSegment(p2, p1, q2))
        {
            return true;
        }
        if (o4 == 0 && OnSegment(p2, q1, q2))
        {
            return true;
        }
        return false;
    }

    private static int Orientation(Point p, Point q, Point r)
    {
        var value = (q.Latitude - p.Latitude) * (r.Longitude - q.Longitude)
                    - (q.Longitude - p.Longitude) * (r.Latitude - q.Latitude);
        if (Math.Abs(value) <= CoordinateTolerance)
        {
            return 0;
        }
        return value > 0 ? 1 : 2;
    }

    private static bool OnSegment(Point p, Point q, Point r)
        => q.Longitude <= Math.Max(p.Longitude, r.Longitude) + CoordinateTolerance
           && q.Longitude >= Math.Min(p.Longitude, r.Longitude) - CoordinateTolerance
           && q.Latitude <= Math.Max(p.Latitude, r.Latitude) + CoordinateTolerance
           && q.Latitude >= Math.Min(p.Latitude, r.Latitude) - 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);
}
