using System.Globalization;
using System.Text.Json;
using Ehwrj.Core.Models;

namespace Ehwrj.Core.Services;

public static class CaptureFixtureAnalyzer
{
    private static readonly string[] RequiredFiles = ["map_info.json", "map_obj.json", "map.img"];
    private static readonly string[] OptionalFiles = ["state.json", "hudmsg.json", "gamechat.json"];

    public static CaptureFixtureReport AnalyzeDirectory(string directory)
    {
        var fullPath = Path.GetFullPath(directory);
        var files = RequiredFiles.Concat(OptionalFiles)
            .ToDictionary(static name => name, name => File.Exists(Path.Combine(fullPath, name)), StringComparer.Ordinal);
        var warnings = new List<string>();

        foreach (var required in RequiredFiles)
        {
            if (!files[required])
            {
                warnings.Add($"Missing required file: {required}");
            }
        }

        var mapInfo = ReadMapInfo(fullPath, warnings);
        var flightState = ReadFlightState(fullPath, warnings);
        var objects = ReadObjects(fullPath, flightState, warnings);
        var hudMessages = ReadMessages(fullPath, "hudmsg.json", warnings);
        var chatMessages = ReadMessages(fullPath, "gamechat.json", warnings);
        var mapImageBytes = ReadMapImageBytes(fullPath, warnings);

        var playerCount = objects.Count(static o => o.IsPlayer);
        var alliedAircraft = objects.Count(static o => o.IsAircraft && o.Kind is MapObjectKind.Ally or MapObjectKind.Squad or MapObjectKind.Player);
        var enemyAircraft = objects.Count(static o => o.IsAircraft && o.Kind == MapObjectKind.Enemy);
        var objectiveCount = objects.Count(static o => o.IsObjective);
        var unknownCount = objects.Count(static o => o.Kind == MapObjectKind.Unknown);
        var objectFields = CollectObjectFieldStats(objects, warnings);
        var unknownSamples = CollectUnknownObjectSamples(objects);
        var stateFieldNames = flightState.Values.Keys
            .Order(StringComparer.Ordinal)
            .Take(40)
            .ToArray();

        if (objects.Count == 0 && files["map_obj.json"])
        {
            warnings.Add("map_obj.json parsed successfully but did not contain any recognized objects.");
        }

        if (objects.Count > 0 && playerCount == 0)
        {
            warnings.Add("No player object was detected; follow/rotate/player panel behavior may need map_obj tuning.");
        }

        if (objects.Count > 0 && enemyAircraft == 0)
        {
            warnings.Add("No enemy aircraft were detected; spot radar tuning needs a richer capture.");
        }

        if (unknownCount > 0)
        {
            warnings.Add("Some map objects were not classified; inspect the unknown object samples and object field coverage.");
        }

        if (!HasMapBounds(mapInfo) && files["map_info.json"])
        {
            warnings.Add("Map bounds or grid size were not detected; projection may fall back to normalized coordinates.");
        }

        if (flightState.Values.Count == 0 && files["state.json"])
        {
            warnings.Add("state.json did not contain parseable telemetry fields.");
        }

        return new CaptureFixtureReport(
            fullPath,
            files,
            mapInfo,
            objects.Count,
            playerCount,
            alliedAircraft,
            enemyAircraft,
            objectiveCount,
            unknownCount,
            mapImageBytes,
            flightState.Values.Count,
            flightState.TrueAirspeedKmh.HasValue || flightState.IndicatedAirspeedKmh.HasValue,
            flightState.AltitudeMeters.HasValue,
            flightState.Mach,
            hudMessages.Count,
            chatMessages.Count,
            objectFields,
            unknownSamples,
            stateFieldNames,
            warnings);
    }

    private static MapInfo ReadMapInfo(string directory, List<string> warnings)
    {
        var path = Path.Combine(directory, "map_info.json");
        if (!File.Exists(path))
        {
            return MapInfo.Empty;
        }

        try
        {
            return MapInfo.FromJson(File.ReadAllText(path));
        }
        catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
        {
            warnings.Add($"map_info.json could not be parsed: {ex.Message}");
            return MapInfo.Empty;
        }
    }

    private static IReadOnlyList<MapObject> ReadObjects(string directory, FlightState flightState, List<string> warnings)
    {
        var path = Path.Combine(directory, "map_obj.json");
        if (!File.Exists(path))
        {
            return Array.Empty<MapObject>();
        }

        try
        {
            return MapObject.FromJson(File.ReadAllText(path), new ObjectTracker(), flightState);
        }
        catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
        {
            warnings.Add($"map_obj.json could not be parsed: {ex.Message}");
            return Array.Empty<MapObject>();
        }
    }

    private static FlightState ReadFlightState(string directory, List<string> warnings)
    {
        var path = Path.Combine(directory, "state.json");
        if (!File.Exists(path))
        {
            return FlightState.Empty;
        }

        try
        {
            return FlightState.FromJson(File.ReadAllText(path));
        }
        catch (IOException ex)
        {
            warnings.Add($"state.json could not be read: {ex.Message}");
            return FlightState.Empty;
        }
    }

    private static IReadOnlyList<BattleMessage> ReadMessages(string directory, string fileName, List<string> warnings)
    {
        var path = Path.Combine(directory, fileName);
        if (!File.Exists(path))
        {
            return Array.Empty<BattleMessage>();
        }

        try
        {
            return BattleMessage.FromJson(File.ReadAllText(path));
        }
        catch (IOException ex)
        {
            warnings.Add($"{fileName} could not be read: {ex.Message}");
            return Array.Empty<BattleMessage>();
        }
    }

    private static long? ReadMapImageBytes(string directory, List<string> warnings)
    {
        var path = Path.Combine(directory, "map.img");
        if (!File.Exists(path))
        {
            return null;
        }

        try
        {
            return new FileInfo(path).Length;
        }
        catch (IOException ex)
        {
            warnings.Add($"map.img could not be inspected: {ex.Message}");
            return null;
        }
    }

    private static bool HasMapBounds(MapInfo info)
    {
        return info.MinX.HasValue && info.MaxX.HasValue && info.MinY.HasValue && info.MaxY.HasValue;
    }

    private static IReadOnlyList<CaptureFieldStat> CollectObjectFieldStats(IReadOnlyList<MapObject> objects, List<string> warnings)
    {
        var counts = new Dictionary<string, int>(StringComparer.Ordinal);

        foreach (var obj in objects)
        {
            try
            {
                using var document = JsonDocument.Parse(obj.RawJson);
                if (document.RootElement.ValueKind != JsonValueKind.Object)
                {
                    continue;
                }

                foreach (var property in document.RootElement.EnumerateObject())
                {
                    counts[property.Name] = counts.TryGetValue(property.Name, out var count) ? count + 1 : 1;
                }
            }
            catch (JsonException ex)
            {
                warnings.Add($"Could not inspect raw object fields for {obj.Id}: {ex.Message}");
            }
        }

        return counts
            .OrderByDescending(static item => item.Value)
            .ThenBy(static item => item.Key, StringComparer.Ordinal)
            .Select(static item => new CaptureFieldStat(item.Key, item.Value))
            .ToArray();
    }

    private static IReadOnlyList<string> CollectUnknownObjectSamples(IReadOnlyList<MapObject> objects)
    {
        return objects
            .Where(static o => o.Kind == MapObjectKind.Unknown)
            .Take(8)
            .Select(SummarizeObject)
            .ToArray();
    }

    private static string SummarizeObject(MapObject obj)
    {
        var parts = new List<string> { $"id={obj.Id}" };

        AddPart(parts, "name", obj.Name);
        AddPart(parts, "type", obj.Type);
        AddPart(parts, "icon", obj.Icon);
        AddPart(parts, "team", obj.Team);
        AddPart(parts, "color", obj.Color);

        if (obj.X.HasValue && obj.Y.HasValue)
        {
            parts.Add($"xy={obj.X.Value.ToString("0.###", CultureInfo.InvariantCulture)},{obj.Y.Value.ToString("0.###", CultureInfo.InvariantCulture)}");
        }

        return string.Join(" ", parts);
    }

    private static void AddPart(List<string> parts, string name, string? value)
    {
        if (!string.IsNullOrWhiteSpace(value))
        {
            parts.Add($"{name}={value}");
        }
    }
}

public sealed record CaptureFieldStat(string Name, int Count);

public sealed class CaptureFixtureReport
{
    public CaptureFixtureReport(
        string directory,
        IReadOnlyDictionary<string, bool> files,
        MapInfo mapInfo,
        int objectCount,
        int playerCount,
        int alliedAircraftCount,
        int enemyAircraftCount,
        int objectiveCount,
        int unknownObjectCount,
        long? mapImageBytes,
        int stateFieldCount,
        bool hasSpeedTelemetry,
        bool hasAltitudeTelemetry,
        double? mach,
        int hudMessageCount,
        int chatMessageCount,
        IReadOnlyList<CaptureFieldStat> objectFields,
        IReadOnlyList<string> unknownObjectSamples,
        IReadOnlyList<string> stateFieldNames,
        IReadOnlyList<string> warnings)
    {
        Directory = directory;
        Files = files;
        MapInfo = mapInfo;
        ObjectCount = objectCount;
        PlayerCount = playerCount;
        AlliedAircraftCount = alliedAircraftCount;
        EnemyAircraftCount = enemyAircraftCount;
        ObjectiveCount = objectiveCount;
        UnknownObjectCount = unknownObjectCount;
        MapImageBytes = mapImageBytes;
        StateFieldCount = stateFieldCount;
        HasSpeedTelemetry = hasSpeedTelemetry;
        HasAltitudeTelemetry = hasAltitudeTelemetry;
        Mach = mach;
        HudMessageCount = hudMessageCount;
        ChatMessageCount = chatMessageCount;
        ObjectFields = objectFields;
        UnknownObjectSamples = unknownObjectSamples;
        StateFieldNames = stateFieldNames;
        Warnings = warnings;
    }

    public string Directory { get; }
    public IReadOnlyDictionary<string, bool> Files { get; }
    public MapInfo MapInfo { get; }
    public int ObjectCount { get; }
    public int PlayerCount { get; }
    public int AlliedAircraftCount { get; }
    public int EnemyAircraftCount { get; }
    public int ObjectiveCount { get; }
    public int UnknownObjectCount { get; }
    public long? MapImageBytes { get; }
    public int StateFieldCount { get; }
    public bool HasSpeedTelemetry { get; }
    public bool HasAltitudeTelemetry { get; }
    public double? Mach { get; }
    public int HudMessageCount { get; }
    public int ChatMessageCount { get; }
    public IReadOnlyList<CaptureFieldStat> ObjectFields { get; }
    public IReadOnlyList<string> UnknownObjectSamples { get; }
    public IReadOnlyList<string> StateFieldNames { get; }
    public IReadOnlyList<string> Warnings { get; }
    public bool HasRequiredFiles => Files.Where(static item => item.Key is "map_info.json" or "map_obj.json" or "map.img").All(static item => item.Value);
    public bool IsUsableForReplay => HasRequiredFiles && ObjectCount > 0 && MapImageBytes.GetValueOrDefault() > 0;

    public string ToText()
    {
        var lines = new List<string>
        {
            "Ehwrj capture quality report",
            $"Generated at: {DateTimeOffset.Now:O}",
            $"Directory: {Directory}",
            "",
            "Files:"
        };

        foreach (var file in Files.OrderBy(static item => item.Key, StringComparer.Ordinal))
        {
            lines.Add($"- {file.Key}: {(file.Value ? "present" : "missing")}");
        }

        lines.Add("");
        lines.Add("Map:");
        lines.Add($"- bounds: {FormatBounds(MapInfo)}");
        lines.Add($"- grid step: {FormatNumber(MapInfo.GridStepX)} x {FormatNumber(MapInfo.GridStepY)}");
        lines.Add($"- map image bytes: {MapImageBytes?.ToString(CultureInfo.InvariantCulture) ?? "missing"}");

        lines.Add("");
        lines.Add("Objects:");
        lines.Add($"- total: {ObjectCount}");
        lines.Add($"- player: {PlayerCount}");
        lines.Add($"- allied aircraft: {AlliedAircraftCount}");
        lines.Add($"- enemy aircraft: {EnemyAircraftCount}");
        lines.Add($"- objectives: {ObjectiveCount}");
        lines.Add($"- unknown: {UnknownObjectCount}");

        lines.Add("");
        lines.Add("Object field coverage:");
        if (ObjectFields.Count == 0)
        {
            lines.Add("- none");
        }
        else
        {
            foreach (var field in ObjectFields.Take(40))
            {
                lines.Add($"- {field.Name}: {field.Count}");
            }
        }

        lines.Add("");
        lines.Add("Unknown object samples:");
        if (UnknownObjectSamples.Count == 0)
        {
            lines.Add("- none");
        }
        else
        {
            foreach (var sample in UnknownObjectSamples)
            {
                lines.Add($"- {sample}");
            }
        }

        lines.Add("");
        lines.Add("Telemetry:");
        lines.Add($"- state fields: {StateFieldCount}");
        lines.Add($"- speed telemetry: {FormatBool(HasSpeedTelemetry)}");
        lines.Add($"- altitude telemetry: {FormatBool(HasAltitudeTelemetry)}");
        lines.Add($"- derived Mach: {FormatNumber(Mach)}");

        lines.Add("");
        lines.Add("State field names:");
        if (StateFieldNames.Count == 0)
        {
            lines.Add("- none");
        }
        else
        {
            foreach (var field in StateFieldNames)
            {
                lines.Add($"- {field}");
            }
        }

        lines.Add("");
        lines.Add("Messages:");
        lines.Add($"- HUD messages: {HudMessageCount}");
        lines.Add($"- game chat messages: {ChatMessageCount}");

        lines.Add("");
        lines.Add($"Replay usable: {FormatBool(IsUsableForReplay)}");
        lines.Add($"Warnings: {Warnings.Count}");
        foreach (var warning in Warnings)
        {
            lines.Add($"- {warning}");
        }

        return string.Join(Environment.NewLine, lines) + Environment.NewLine;
    }

    private static string FormatBounds(MapInfo info)
    {
        if (!info.MinX.HasValue || !info.MaxX.HasValue || !info.MinY.HasValue || !info.MaxY.HasValue)
        {
            return "unknown";
        }

        return $"{FormatNumber(info.MinX)}..{FormatNumber(info.MaxX)}, {FormatNumber(info.MinY)}..{FormatNumber(info.MaxY)}";
    }

    private static string FormatBool(bool value) => value ? "yes" : "no";

    private static string FormatNumber(double? value)
    {
        return value.HasValue
            ? value.Value.ToString("0.###", CultureInfo.InvariantCulture)
            : "unknown";
    }
}
