using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Linq;

namespace StellaOps.Vexer.Core;

public sealed record VexQuery(
    ImmutableArray<VexQueryFilter> Filters,
    ImmutableArray<VexQuerySort> Sort,
    int? Limit = null,
    int? Offset = null,
    string? View = null)
{
    public static VexQuery Empty { get; } = new(
        ImmutableArray<VexQueryFilter>.Empty,
        ImmutableArray<VexQuerySort>.Empty);

    public static VexQuery Create(
        IEnumerable<VexQueryFilter>? filters = null,
        IEnumerable<VexQuerySort>? sort = null,
        int? limit = null,
        int? offset = null,
        string? view = null)
    {
        var normalizedFilters = NormalizeFilters(filters);
        var normalizedSort = NormalizeSort(sort);
        return new VexQuery(normalizedFilters, normalizedSort, NormalizeBound(limit), NormalizeBound(offset), NormalizeView(view));
    }

    public VexQuery WithFilters(IEnumerable<VexQueryFilter> filters)
        => this with { Filters = NormalizeFilters(filters) };

    public VexQuery WithSort(IEnumerable<VexQuerySort> sort)
        => this with { Sort = NormalizeSort(sort) };

    public VexQuery WithBounds(int? limit = null, int? offset = null)
        => this with { Limit = NormalizeBound(limit), Offset = NormalizeBound(offset) };

    public VexQuery WithView(string? view)
        => this with { View = NormalizeView(view) };

    private static ImmutableArray<VexQueryFilter> NormalizeFilters(IEnumerable<VexQueryFilter>? filters)
    {
        if (filters is null)
        {
            return ImmutableArray<VexQueryFilter>.Empty;
        }

        return filters
            .Where(filter => !string.IsNullOrWhiteSpace(filter.Key))
            .Select(filter => new VexQueryFilter(filter.Key.Trim(), filter.Value?.Trim() ?? string.Empty))
            .OrderBy(filter => filter.Key, StringComparer.Ordinal)
            .ThenBy(filter => filter.Value, StringComparer.Ordinal)
            .ToImmutableArray();
    }

    private static ImmutableArray<VexQuerySort> NormalizeSort(IEnumerable<VexQuerySort>? sort)
    {
        if (sort is null)
        {
            return ImmutableArray<VexQuerySort>.Empty;
        }

        return sort
            .Where(s => !string.IsNullOrWhiteSpace(s.Field))
            .Select(s => new VexQuerySort(s.Field.Trim(), s.Descending))
            .OrderBy(s => s.Field, StringComparer.Ordinal)
            .ThenBy(s => s.Descending)
            .ToImmutableArray();
    }

    private static int? NormalizeBound(int? value)
    {
        if (value is null)
        {
            return null;
        }

        if (value.Value < 0)
        {
            return 0;
        }

        return value.Value;
    }

    private static string? NormalizeView(string? view)
        => string.IsNullOrWhiteSpace(view) ? null : view.Trim();
}

public sealed record VexQueryFilter(string Key, string Value);

public sealed record VexQuerySort(string Field, bool Descending);

public sealed partial record VexQuerySignature
{
    public static VexQuerySignature FromQuery(VexQuery query)
    {
        if (query is null)
        {
            throw new ArgumentNullException(nameof(query));
        }

        var components = new List<string>(query.Filters.Length + query.Sort.Length + 3);
        components.AddRange(query.Filters.Select(filter => $"{filter.Key}={filter.Value}"));
        components.AddRange(query.Sort.Select(sort => sort.Descending ? $"sort=-{sort.Field}" : $"sort=+{sort.Field}"));

        if (query.Limit is not null)
        {
            components.Add($"limit={query.Limit.Value.ToString(CultureInfo.InvariantCulture)}");
        }

        if (query.Offset is not null)
        {
            components.Add($"offset={query.Offset.Value.ToString(CultureInfo.InvariantCulture)}");
        }

        if (!string.IsNullOrWhiteSpace(query.View))
        {
            components.Add($"view={query.View}");
        }

        return new VexQuerySignature(string.Join('&', components));
    }

    public VexContentAddress ComputeHash()
    {
        using var sha = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(Value);
        var digest = sha.ComputeHash(bytes);
        var builder = new StringBuilder(digest.Length * 2);
        foreach (var b in digest)
        {
            _ = builder.Append(b.ToString("x2", CultureInfo.InvariantCulture));
        }

        return new VexContentAddress("sha256", builder.ToString());
    }
}
