using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace StellaOps.Policy;

public sealed record PolicyScoringBindingResult(
    bool Success,
    PolicyScoringConfig? Config,
    ImmutableArray<PolicyIssue> Issues);

public static class PolicyScoringConfigBinder
{
    private const string DefaultResourceName = "StellaOps.Policy.Schemas.policy-scoring-default.json";

    private static readonly JsonSerializerOptions SerializerOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        ReadCommentHandling = JsonCommentHandling.Skip,
        AllowTrailingCommas = true,
    };

    private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
        .WithNamingConvention(CamelCaseNamingConvention.Instance)
        .IgnoreUnmatchedProperties()
        .Build();

    public static PolicyScoringConfig LoadDefault()
    {
        var assembly = Assembly.GetExecutingAssembly();
        using var stream = assembly.GetManifestResourceStream(DefaultResourceName)
            ?? throw new InvalidOperationException($"Embedded resource '{DefaultResourceName}' not found.");
        using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
        var json = reader.ReadToEnd();
        var binding = Bind(json, PolicyDocumentFormat.Json);
        if (!binding.Success || binding.Config is null)
        {
            throw new InvalidOperationException("Failed to load default policy scoring configuration.");
        }

        return binding.Config;
    }

    public static PolicyScoringBindingResult Bind(string content, PolicyDocumentFormat format)
    {
        if (string.IsNullOrWhiteSpace(content))
        {
            var issue = PolicyIssue.Error("scoring.empty", "Scoring configuration content is empty.", "$");
            return new PolicyScoringBindingResult(false, null, ImmutableArray.Create(issue));
        }

        try
        {
            var root = Parse(content, format);
            if (root is not JsonObject obj)
            {
                var issue = PolicyIssue.Error("scoring.invalid", "Scoring configuration must be a JSON object.", "$");
                return new PolicyScoringBindingResult(false, null, ImmutableArray.Create(issue));
            }

            var issues = ImmutableArray.CreateBuilder<PolicyIssue>();
            var config = BuildConfig(obj, issues);
            var hasErrors = issues.Any(issue => issue.Severity == PolicyIssueSeverity.Error);
            return new PolicyScoringBindingResult(!hasErrors, config, issues.ToImmutable());
        }
        catch (JsonException ex)
        {
            var issue = PolicyIssue.Error("scoring.parse.json", $"Failed to parse scoring JSON: {ex.Message}", "$");
            return new PolicyScoringBindingResult(false, null, ImmutableArray.Create(issue));
        }
        catch (YamlDotNet.Core.YamlException ex)
        {
            var issue = PolicyIssue.Error("scoring.parse.yaml", $"Failed to parse scoring YAML: {ex.Message}", "$");
            return new PolicyScoringBindingResult(false, null, ImmutableArray.Create(issue));
        }
    }

    private static JsonNode? Parse(string content, PolicyDocumentFormat format)
    {
        return format switch
        {
            PolicyDocumentFormat.Json => JsonNode.Parse(content, new JsonNodeOptions { PropertyNameCaseInsensitive = true }),
            PolicyDocumentFormat.Yaml => ConvertYamlToJsonNode(content),
            _ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported scoring configuration format."),
        };
    }

    private static JsonNode? ConvertYamlToJsonNode(string content)
    {
        var yamlObject = YamlDeserializer.Deserialize<object?>(content);
        return PolicyBinderUtilities.ConvertYamlObject(yamlObject);
    }

    private static PolicyScoringConfig BuildConfig(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
    {
        var version = ReadString(obj, "version", issues, required: true) ?? PolicyScoringConfig.BaselineVersion;

        var severityWeights = ReadSeverityWeights(obj, issues);
        var quietPenalty = ReadDouble(obj, "quietPenalty", issues, defaultValue: 45);
        var warnPenalty = ReadDouble(obj, "warnPenalty", issues, defaultValue: 15);
        var ignorePenalty = ReadDouble(obj, "ignorePenalty", issues, defaultValue: 35);
        var trustOverrides = ReadTrustOverrides(obj, issues);

        return new PolicyScoringConfig(
            version,
            severityWeights,
            quietPenalty,
            warnPenalty,
            ignorePenalty,
            trustOverrides);
    }

    private static ImmutableDictionary<PolicySeverity, double> ReadSeverityWeights(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
    {
        if (!obj.TryGetPropertyValue("severityWeights", out var node) || node is not JsonObject severityObj)
        {
            issues.Add(PolicyIssue.Error("scoring.severityWeights.missing", "severityWeights section is required.", "$.severityWeights"));
            return ImmutableDictionary<PolicySeverity, double>.Empty;
        }

        var builder = ImmutableDictionary.CreateBuilder<PolicySeverity, double>();
        foreach (var severity in Enum.GetValues<PolicySeverity>())
        {
            var key = severity.ToString();
            if (!severityObj.TryGetPropertyValue(key, out var valueNode))
            {
                issues.Add(PolicyIssue.Warning("scoring.severityWeights.default", $"Severity '{key}' not specified; defaulting to 0.", $"$.severityWeights.{key}"));
                builder[severity] = 0;
                continue;
            }

            var value = ExtractDouble(valueNode, issues, $"$.severityWeights.{key}");
            builder[severity] = value;
        }

        return builder.ToImmutable();
    }

    private static double ReadDouble(JsonObject obj, string property, ImmutableArray<PolicyIssue>.Builder issues, double defaultValue)
    {
        if (!obj.TryGetPropertyValue(property, out var node))
        {
            issues.Add(PolicyIssue.Warning("scoring.numeric.default", $"{property} not specified; defaulting to {defaultValue:0.##}.", $"$.{property}"));
            return defaultValue;
        }

        return ExtractDouble(node, issues, $"$.{property}");
    }

    private static double ExtractDouble(JsonNode? node, ImmutableArray<PolicyIssue>.Builder issues, string path)
    {
        if (node is null)
        {
            issues.Add(PolicyIssue.Warning("scoring.numeric.null", $"Value at {path} missing; defaulting to 0.", path));
            return 0;
        }

        if (node is JsonValue value)
        {
            if (value.TryGetValue(out double number))
            {
                return number;
            }

            if (value.TryGetValue(out string? text) && double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out number))
            {
                return number;
            }
        }

        issues.Add(PolicyIssue.Error("scoring.numeric.invalid", $"Value at {path} is not numeric.", path));
        return 0;
    }

    private static ImmutableDictionary<string, double> ReadTrustOverrides(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
    {
        if (!obj.TryGetPropertyValue("trustOverrides", out var node) || node is not JsonObject trustObj)
        {
            return ImmutableDictionary<string, double>.Empty;
        }

        var builder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
        foreach (var pair in trustObj)
        {
            var value = ExtractDouble(pair.Value, issues, $"$.trustOverrides.{pair.Key}");
            builder[pair.Key] = value;
        }

        return builder.ToImmutable();
    }

    private static string? ReadString(JsonObject obj, string property, ImmutableArray<PolicyIssue>.Builder issues, bool required)
    {
        if (!obj.TryGetPropertyValue(property, out var node) || node is null)
        {
            if (required)
            {
                issues.Add(PolicyIssue.Error("scoring.string.missing", $"{property} is required.", $"$.{property}"));
            }
            return null;
        }

        if (node is JsonValue value && value.TryGetValue(out string? text))
        {
            return text?.Trim();
        }

        issues.Add(PolicyIssue.Error("scoring.string.invalid", $"{property} must be a string.", $"$.{property}"));
        return null;
    }
}

internal static class PolicyBinderUtilities
{
    public static JsonNode? ConvertYamlObject(object? value)
    {
        switch (value)
        {
            case null:
                return null;
            case string s:
                return JsonValue.Create(s);
            case bool b:
                return JsonValue.Create(b);
            case sbyte or byte or short or ushort or int or uint or long or ulong or float or double or decimal:
                return JsonValue.Create(Convert.ToDouble(value, CultureInfo.InvariantCulture));
            case IDictionary dictionary:
            {
                var obj = new JsonObject();
                foreach (DictionaryEntry entry in dictionary)
                {
                    if (entry.Key is null)
                    {
                        continue;
                    }

                    obj[entry.Key.ToString()!] = ConvertYamlObject(entry.Value);
                }

                return obj;
            }
            case IEnumerable enumerable:
            {
                var array = new JsonArray();
                foreach (var item in enumerable)
                {
                    array.Add(ConvertYamlObject(item));
                }

                return array;
            }
            default:
                return JsonValue.Create(value.ToString());
        }
    }
}
