using System.Globalization;
using System.Text.Json;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Net;

namespace YMhut.Box.Core.SolarSystem;

public sealed partial class SolarEphemerisService(
    AppPaths paths,
    IHttpService httpService,
    ILogService? logService = null) : ISolarEphemerisService
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
    {
        WriteIndented = true
    };

    private static readonly string[] PlanetIds =
    [
        "mercury",
        "venus",
        "earth",
        "mars",
        "jupiter",
        "saturn",
        "uranus",
        "neptune"
    ];

    private static readonly Dictionary<string, string> HorizonsIds = new(StringComparer.OrdinalIgnoreCase)
    {
        ["mercury"] = "199",
        ["venus"] = "299",
        ["earth"] = "399",
        ["mars"] = "499",
        ["jupiter"] = "599",
        ["saturn"] = "699",
        ["uranus"] = "799",
        ["neptune"] = "899"
    };

    private static readonly Dictionary<string, string> MiriadeNames = new(StringComparer.OrdinalIgnoreCase)
    {
        ["mercury"] = "Mercury",
        ["venus"] = "Venus",
        ["earth"] = "Earth",
        ["mars"] = "Mars",
        ["jupiter"] = "Jupiter",
        ["saturn"] = "Saturn",
        ["uranus"] = "Uranus",
        ["neptune"] = "Neptune"
    };

    public async Task<SolarEphemerisPayload> GetCurrentAsync(CancellationToken cancellationToken = default)
    {
        var epoch = DateTimeOffset.UtcNow;
        if (TryReadCache(epoch, out var cached, preciseOnly: false))
        {
            return cached;
        }

        _ = cancellationToken;
        return CreateChinaApproximatePayload(epoch);
    }

    public static bool TryParseMiriadePayload(string json, out HeliocentricAu vector)
    {
        return TryParseMiriadeVector(json, out vector);
    }

    public async Task<SolarEphemerisPayload?> GetPreciseCurrentAsync(CancellationToken cancellationToken = default)
    {
        var epoch = DateTimeOffset.UtcNow;
        if (TryReadCache(epoch, out var cached, preciseOnly: true))
        {
            return cached;
        }

        SolarEphemerisPayload? payload = null;
        try
        {
            payload = await FetchChinaRemoteAsync(epoch, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception) when (exception is not OperationCanceledException)
        {
            await WriteLogAsync("Warning", "solar", "China ephemeris sync failed", exception.Message, cancellationToken).ConfigureAwait(false);
        }

        if (payload is null)
        {
            try
            {
                payload = await FetchMiriadeAsync(epoch, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception exception) when (exception is not OperationCanceledException)
            {
                await WriteLogAsync("Warning", "solar", "IMCCE Miriade sync failed", exception.Message, cancellationToken).ConfigureAwait(false);
            }
        }

        if (payload is null)
        {
            try
            {
                payload = await FetchHorizonsAsync(epoch, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception exception) when (exception is not OperationCanceledException)
            {
                await WriteLogAsync("Warning", "solar", "JPL Horizons sync failed", exception.Message, cancellationToken).ConfigureAwait(false);
            }
        }

        if (payload is not null)
        {
            WriteCache(payload);
        }

        return payload;
    }

    private static Task<SolarEphemerisPayload?> FetchChinaRemoteAsync(DateTimeOffset epoch, CancellationToken cancellationToken)
    {
        _ = epoch;
        _ = cancellationToken;
        return Task.FromResult<SolarEphemerisPayload?>(null);
    }

    private async Task<SolarEphemerisPayload?> FetchHorizonsAsync(DateTimeOffset epoch, CancellationToken cancellationToken)
    {
        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeout.CancelAfter(TimeSpan.FromSeconds(10));

        var tasks = PlanetIds.Select(id => FetchHorizonsPlanetAsync(id, epoch, timeout.Token)).ToArray();
        var planets = await Task.WhenAll(tasks).ConfigureAwait(false);

        return planets.All(planet => planet is not null)
            ? new SolarEphemerisPayload("ephemeris:sync", "JPL Horizons", epoch, planets!)
            : null;
    }

    private async Task<SolarEphemerisPayload?> FetchMiriadeAsync(DateTimeOffset epoch, CancellationToken cancellationToken)
    {
        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeout.CancelAfter(TimeSpan.FromSeconds(8));

        var tasks = PlanetIds.Select(id => FetchMiriadePlanetAsync(id, epoch, timeout.Token)).ToArray();
        var planets = await Task.WhenAll(tasks).ConfigureAwait(false);

        return planets.All(planet => planet is not null)
            ? new SolarEphemerisPayload("ephemeris:sync", "IMCCE Miriade", epoch, planets!)
            : null;
    }

    private async Task<SolarEphemerisPlanet?> FetchHorizonsPlanetAsync(string id, DateTimeOffset epoch, CancellationToken cancellationToken)
    {
        var command = Uri.EscapeDataString($"'{HorizonsIds[id]}'");
        var start = Uri.EscapeDataString(epoch.UtcDateTime.ToString("yyyy-MMM-dd HH:mm", CultureInfo.InvariantCulture));
        var stop = Uri.EscapeDataString(epoch.UtcDateTime.AddHours(1).ToString("yyyy-MMM-dd HH:mm", CultureInfo.InvariantCulture));
        var uri = new Uri($"https://ssd.jpl.nasa.gov/api/horizons.api?format=json&COMMAND={command}&OBJ_DATA=NO&MAKE_EPHEM=YES&EPHEM_TYPE=VECTORS&CENTER=@sun&START_TIME={start}&STOP_TIME={stop}&STEP_SIZE=1h&OUT_UNITS=AU-D");
        var response = await httpService.GetStringAsync(uri, cancellationToken).ConfigureAwait(false);
        return TryParseHorizonsVector(response, out var vector)
            ? new SolarEphemerisPlanet(id, vector)
            : null;
    }

    private async Task<SolarEphemerisPlanet?> FetchMiriadePlanetAsync(string id, DateTimeOffset epoch, CancellationToken cancellationToken)
    {
        var julianDate = ToJulianDate(epoch.UtcDateTime).ToString("F6", CultureInfo.InvariantCulture);
        var name = Uri.EscapeDataString($"p:{MiriadeNames[id]}");
        var uri = new Uri($"https://ssp.imcce.fr/webservices/miriade/api/ephemcc.php?-name={name}&-type=Planet&-ep={julianDate}&-nbd=1&-step=1d&-observer=@sun&-tcoor=2&-rplane=2&-mime=json");
        var response = await httpService.GetStringAsync(uri, cancellationToken).ConfigureAwait(false);
        return TryParseMiriadeVector(response, out var vector)
            ? new SolarEphemerisPlanet(id, vector)
            : null;
    }

    private static bool TryParseHorizonsVector(string json, out HeliocentricAu vector)
    {
        vector = new HeliocentricAu(0, 0, 0);
        using var document = JsonDocument.Parse(json);
        if (!document.RootElement.TryGetProperty("result", out var result) ||
            result.ValueKind != JsonValueKind.String)
        {
            return false;
        }

        var text = result.GetString() ?? string.Empty;
        var start = text.IndexOf("$$SOE", StringComparison.Ordinal);
        var end = text.IndexOf("$$EOE", StringComparison.Ordinal);
        if (start < 0 || end <= start)
        {
            return false;
        }

        var block = text[(start + 5)..end];
        var x = ExtractHorizonsNumber(block, "X");
        var y = ExtractHorizonsNumber(block, "Y");
        var z = ExtractHorizonsNumber(block, "Z");
        if (x is null || y is null || z is null)
        {
            return false;
        }

        vector = new HeliocentricAu(x.Value, y.Value, z.Value);
        return true;
    }

    private static double? ExtractHorizonsNumber(string text, string key)
    {
        var index = text.IndexOf(key + " =", StringComparison.Ordinal);
        if (index < 0)
        {
            index = text.IndexOf(key + "=", StringComparison.Ordinal);
        }

        if (index < 0)
        {
            return null;
        }

        var equals = text.IndexOf('=', index);
        if (equals < 0)
        {
            return null;
        }

        var span = text[(equals + 1)..];
        var token = span.Split([' ', '\r', '\n', '\t'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
        return double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : null;
    }

    private static bool TryParseMiriadeVector(string json, out HeliocentricAu vector)
    {
        vector = new HeliocentricAu(0, 0, 0);
        using var document = JsonDocument.Parse(json);
        var root = document.RootElement;
        if (root.ValueKind == JsonValueKind.Array &&
            root.GetArrayLength() > 0 &&
            TryGetVectorFromObject(root[0], out vector))
        {
            return true;
        }

        if (root.ValueKind == JsonValueKind.Object)
        {
            if (TryGetVectorFromObject(root, out vector))
            {
                return true;
            }

            if (root.TryGetProperty("data", out var data) &&
                data.ValueKind == JsonValueKind.Array &&
                data.GetArrayLength() > 0 &&
                (TryGetVectorFromObject(data[0], out vector) || TryGetSphericalVector(data[0], out vector)))
            {
                return true;
            }

            foreach (var property in root.EnumerateObject())
            {
                if (property.Value.ValueKind == JsonValueKind.Array && property.Value.GetArrayLength() > 0 &&
                    (TryGetVectorFromObject(property.Value[0], out vector) || TryGetSphericalVector(property.Value[0], out vector)))
                {
                    return true;
                }
            }
        }

        return false;
    }

    private static bool TryGetVectorFromObject(JsonElement element, out HeliocentricAu vector)
    {
        vector = new HeliocentricAu(0, 0, 0);
        if (element.ValueKind != JsonValueKind.Object)
        {
            return false;
        }

        var x = TryGetDouble(element, "x", "X", "px", "PX");
        var y = TryGetDouble(element, "y", "Y", "py", "PY");
        var z = TryGetDouble(element, "z", "Z", "pz", "PZ");
        if (x is null || y is null || z is null)
        {
            return false;
        }

        vector = new HeliocentricAu(x.Value, y.Value, z.Value);
        return true;
    }

    private static double? TryGetDouble(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!element.TryGetProperty(name, out var value))
            {
                continue;
            }

            if (value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var number))
            {
                return number;
            }

            if (value.ValueKind == JsonValueKind.String &&
                double.TryParse(value.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out number))
            {
                return number;
            }
        }

        return null;
    }

    private static bool TryGetSphericalVector(JsonElement element, out HeliocentricAu vector)
    {
        vector = new HeliocentricAu(0, 0, 0);
        if (element.ValueKind != JsonValueKind.Object)
        {
            return false;
        }

        if (!element.TryGetProperty("RA", out var raElement) ||
            !element.TryGetProperty("DEC", out var decElement))
        {
            return false;
        }

        var distance = TryGetDouble(element, "Dobs", "delta", "distance");
        if (distance is null ||
            !TryParseRightAscension(raElement.GetString(), out var raRadians) ||
            !TryParseDeclination(decElement.GetString(), out var decRadians))
        {
            return false;
        }

        var cosDec = Math.Cos(decRadians);
        vector = new HeliocentricAu(
            distance.Value * cosDec * Math.Cos(raRadians),
            distance.Value * cosDec * Math.Sin(raRadians),
            distance.Value * Math.Sin(decRadians));
        return true;
    }

    private static bool TryParseRightAscension(string? value, out double radians)
    {
        radians = 0;
        if (string.IsNullOrWhiteSpace(value))
        {
            return false;
        }

        var parts = value.Trim().Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (parts.Length == 3 &&
            double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var hours) &&
            double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var minutes) &&
            double.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds))
        {
            var sign = hours < 0 ? -1 : 1;
            var totalHours = sign * (Math.Abs(hours) + minutes / 60d + seconds / 3600d);
            radians = ToRadians(totalHours * 15d);
            return true;
        }

        if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalHours))
        {
            radians = ToRadians(decimalHours * 15d);
            return true;
        }

        return false;
    }

    private static bool TryParseDeclination(string? value, out double radians)
    {
        radians = 0;
        if (string.IsNullOrWhiteSpace(value))
        {
            return false;
        }

        var trimmed = value.Trim();
        var sign = trimmed.StartsWith("-", StringComparison.Ordinal) ? -1d : 1d;
        trimmed = trimmed.TrimStart('+', '-');
        var parts = trimmed.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (parts.Length == 3 &&
            double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var degrees) &&
            double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var minutes) &&
            double.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds))
        {
            radians = ToRadians(sign * (Math.Abs(degrees) + minutes / 60d + seconds / 3600d));
            return true;
        }

        if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalDegrees))
        {
            radians = ToRadians(decimalDegrees);
            return true;
        }

        return false;
    }

    private SolarEphemerisPayload CreateLocalPayload(DateTimeOffset epoch)
    {
        var planets = PlanetIds
            .Select(id => new SolarEphemerisPlanet(id, ApproximatePosition(id, epoch)))
            .ToArray();
        return new SolarEphemerisPayload("ephemeris:sync", "local", epoch, planets);
    }

    private SolarEphemerisPayload CreateChinaApproximatePayload(DateTimeOffset epoch)
    {
        var planets = PlanetIds
            .Select(id => new SolarEphemerisPlanet(id, ApproximatePosition(id, epoch)))
            .ToArray();
        return new SolarEphemerisPayload("ephemeris:sync", "China Ephemeris Approx", epoch, planets);
    }

    private static HeliocentricAu ApproximatePosition(string id, DateTimeOffset epoch)
    {
        var element = ApproximateElements[id];
        var jd = ToJulianDate(epoch.UtcDateTime);
        var t = (jd - 2451545.0) / 36525.0;
        var a = element.A + element.ARate * t;
        var e = element.E + element.ERate * t;
        var inclination = ToRadians(NormalizeDegrees(element.I + element.IRate * t));
        var meanLongitude = NormalizeDegrees(element.L + element.LRate * t);
        var perihelion = NormalizeDegrees(element.Perihelion + element.PerihelionRate * t);
        var node = ToRadians(NormalizeDegrees(element.Node + element.NodeRate * t));
        var argumentOfPerihelion = ToRadians(perihelion) - node;
        var meanAnomaly = ToRadians(NormalizeDegrees(meanLongitude - perihelion));
        var eccentricAnomaly = SolveKepler(meanAnomaly, e);
        var xv = a * (Math.Cos(eccentricAnomaly) - e);
        var yv = a * Math.Sqrt(1 - e * e) * Math.Sin(eccentricAnomaly);
        var trueAnomaly = Math.Atan2(yv, xv);
        var radius = Math.Sqrt(xv * xv + yv * yv);
        var angle = argumentOfPerihelion + trueAnomaly;

        return new HeliocentricAu(
            radius * (Math.Cos(node) * Math.Cos(angle) - Math.Sin(node) * Math.Sin(angle) * Math.Cos(inclination)),
            radius * (Math.Sin(node) * Math.Cos(angle) + Math.Cos(node) * Math.Sin(angle) * Math.Cos(inclination)),
            radius * (Math.Sin(angle) * Math.Sin(inclination)));
    }

    private bool TryReadCache(DateTimeOffset epoch, out SolarEphemerisPayload payload, bool preciseOnly)
    {
        payload = null!;
        var path = CachePath();
        if (!File.Exists(path))
        {
            return false;
        }

        try
        {
            var text = File.ReadAllText(path);
            var cached = JsonSerializer.Deserialize<SolarEphemerisPayload>(text, JsonOptions);
            if (cached is null || cached.Planets.Count == 0)
            {
                return false;
            }

            if (preciseOnly && IsLocalApproximateSource(cached.Source))
            {
                return false;
            }

            if (epoch - cached.EpochUtc > TimeSpan.FromHours(6))
            {
                return false;
            }

            payload = cached;
            return true;
        }
        catch
        {
            return false;
        }
    }

    private static bool IsLocalApproximateSource(string source)
    {
        return string.Equals(source, "local", StringComparison.OrdinalIgnoreCase) ||
            source.Contains("Approx", StringComparison.OrdinalIgnoreCase);
    }

    private void WriteCache(SolarEphemerisPayload payload)
    {
        try
        {
            var path = CachePath();
            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            File.WriteAllText(path, JsonSerializer.Serialize(payload, JsonOptions));
        }
        catch
        {
        }
    }

    private string CachePath()
    {
        return Path.Combine(paths.Cache, "SolarEphemeris", "current.json");
    }

    private Task WriteLogAsync(string level, string category, string message, string detail, CancellationToken cancellationToken)
    {
        return logService?.WriteAsync(level, category, message, detail, cancellationToken) ?? Task.CompletedTask;
    }

    private static double ToJulianDate(DateTime utc)
    {
        return new DateTimeOffset(DateTime.SpecifyKind(utc, DateTimeKind.Utc)).ToUnixTimeMilliseconds() / 86400000d + 2440587.5d;
    }

    private static double SolveKepler(double meanAnomaly, double eccentricity)
    {
        var eccentricAnomaly = eccentricity < 0.8 ? meanAnomaly : Math.PI;
        for (var index = 0; index < 12; index++)
        {
            var delta = (eccentricAnomaly - eccentricity * Math.Sin(eccentricAnomaly) - meanAnomaly) /
                (1 - eccentricity * Math.Cos(eccentricAnomaly));
            eccentricAnomaly -= delta;
            if (Math.Abs(delta) < 1e-10)
            {
                break;
            }
        }

        return eccentricAnomaly;
    }

    private static double NormalizeDegrees(double value)
    {
        var normalized = value % 360;
        return normalized < 0 ? normalized + 360 : normalized;
    }

    private static double ToRadians(double degrees) => degrees * Math.PI / 180d;

    private sealed record Elements(
        double A,
        double ARate,
        double E,
        double ERate,
        double I,
        double IRate,
        double L,
        double LRate,
        double Perihelion,
        double PerihelionRate,
        double Node,
        double NodeRate);

    private static readonly Dictionary<string, Elements> ApproximateElements = new(StringComparer.OrdinalIgnoreCase)
    {
        ["mercury"] = new(0.38709927, 0.00000037, 0.20563593, 0.00001906, 7.00497902, -0.00594749, 252.25032350, 149472.67411175, 77.45779628, 0.16047689, 48.33076593, -0.12534081),
        ["venus"] = new(0.72333566, 0.00000390, 0.00677672, -0.00004107, 3.39467605, -0.00078890, 181.97909950, 58517.81538729, 131.60246718, 0.00268329, 76.67984255, -0.27769418),
        ["earth"] = new(1.00000261, 0.00000562, 0.01671123, -0.00004392, -0.00001531, -0.01294668, 100.46457166, 35999.37244981, 102.93768193, 0.32327364, 0, 0),
        ["mars"] = new(1.52371034, 0.00001847, 0.09339410, 0.00007882, 1.84969142, -0.00813131, -4.55343205, 19140.30268499, -23.94362959, 0.44441088, 49.55953891, -0.29257343),
        ["jupiter"] = new(5.20288700, -0.00011607, 0.04838624, -0.00013253, 1.30439695, -0.00183714, 34.39644051, 3034.74612775, 14.72847983, 0.21252668, 100.47390909, 0.20469106),
        ["saturn"] = new(9.53667594, -0.00125060, 0.05386179, -0.00050991, 2.48599187, 0.00193609, 49.95424423, 1222.49362201, 92.59887831, -0.41897216, 113.66242448, -0.28867794),
        ["uranus"] = new(19.18916464, -0.00196176, 0.04725744, -0.00004397, 0.77263783, -0.00242939, 313.23810451, 428.48202785, 170.95427630, 0.40805281, 74.01692503, 0.04240589),
        ["neptune"] = new(30.06992276, 0.00026291, 0.00859048, 0.00005105, 1.77004347, 0.00035372, -55.12002969, 218.45945325, 44.96476227, -0.32241464, 131.78422574, -0.00508664)
    };
}
