using System;
using System.Text;
using Microsoft.FSharp.Core;
using TotalRecall.Core;
using TotalRecall.Infrastructure.Memory;
using TotalRecall.Infrastructure.Storage;

namespace TotalRecall.Infrastructure.Sync;

/// <summary>
/// AOT-safe JSON payload builders for sync queue entries.
/// Avoids reflection-based JsonSerializer in the AOT binary.
/// </summary>
internal static class SyncPayload
{
    /// <summary>
    /// Serializes an <see cref="Entry"/> plus its <see cref="ContentType"/> into the
    /// snake_case JSON shape expected by cortex's PluginSyncMemoryRequest DTO. Emits
    /// every sync-relevant field so SyncService.FlushAsync can hydrate a full
    /// SyncEntry without dropping to placeholders.
    /// </summary>
    public static string Upsert(Entry entry, ContentType contentType, Tier tier)
    {
        var sb = new StringBuilder(256);
        sb.Append('{');

        AppendStringField(sb, "id", entry.Id);
        sb.Append(',');
        AppendStringField(sb, "content", entry.Content);
        sb.Append(',');
        AppendStringField(sb, "entry_type", EntryTypeMapping.ToDbValue(entry.EntryType));
        sb.Append(',');
        AppendStringField(sb, "content_type", contentType.IsMemory ? "Memory" : "Kb");
        sb.Append(',');
        AppendStringField(sb, "tier", TierNames.TierName(tier));
        sb.Append(',');

        // tags: string list → JSON array
        sb.Append("\"tags\":[");
        var firstTag = true;
        foreach (var tag in entry.Tags)
        {
            if (!firstTag) sb.Append(',');
            firstTag = false;
            sb.Append('"').Append(Escape(tag)).Append('"');
        }
        sb.Append("],");

        // source: string option → nullable string
        sb.Append("\"source\":");
        if (FSharpOption<string>.get_IsSome(entry.Source))
        {
            sb.Append('"').Append(Escape(entry.Source.Value)).Append('"');
        }
        else
        {
            sb.Append("null");
        }
        sb.Append(',');

        sb.Append("\"access_count\":").Append(entry.AccessCount).Append(',');

        // decay_score: use InvariantCulture "R" round-trip format
        sb.Append("\"decay_score\":")
          .Append(entry.DecayScore.ToString("R", System.Globalization.CultureInfo.InvariantCulture))
          .Append(',');

        // created_at / updated_at: int64 epoch-ms → ISO-8601 round-trip UTC
        var createdIso = DateTimeOffset.FromUnixTimeMilliseconds(entry.CreatedAt)
            .UtcDateTime.ToString("O", System.Globalization.CultureInfo.InvariantCulture);
        var updatedIso = DateTimeOffset.FromUnixTimeMilliseconds(entry.UpdatedAt)
            .UtcDateTime.ToString("O", System.Globalization.CultureInfo.InvariantCulture);
        AppendStringField(sb, "created_at", createdIso);
        sb.Append(',');
        AppendStringField(sb, "updated_at", updatedIso);
        sb.Append(',');

        // scope: non-null string per schema, but emit null when empty to match
        // cortex's optional-scope wire contract.
        sb.Append("\"scope\":");
        if (string.IsNullOrEmpty(entry.Scope))
        {
            sb.Append("null");
        }
        else
        {
            sb.Append('"').Append(Escape(entry.Scope)).Append('"');
        }

        sb.Append('}');
        return sb.ToString();
    }

    public static string Delete(string id)
        => $$"""{"id":"{{Escape(id)}}"}""";

    private static void AppendStringField(StringBuilder sb, string key, string value)
    {
        sb.Append('"').Append(key).Append("\":\"").Append(Escape(value)).Append('"');
    }

    // RFC 8259 string escaping. Escapes the two structural characters (\ and "),
    // the five short-form control escapes (\b \f \n \r \t), and EVERY remaining
    // C0 control character (U+0000–U+001F) as a \uXXXX sequence. Emitting a raw
    // control byte — as the previous \n\r\t-only implementation did for e.g.
    // U+0001 in a code snippet — produces invalid JSON that the drain side cannot
    // parse, wedging the whole sync queue. Non-control characters (incl. non-ASCII)
    // are valid unescaped in a UTF-8 JSON string and pass through verbatim.
    private static string Escape(string s)
    {
        StringBuilder? sb = null;
        for (var i = 0; i < s.Length; i++)
        {
            var c = s[i];
            string? rep = c switch
            {
                '\\' => "\\\\",
                '"' => "\\\"",
                '\b' => "\\b",
                '\f' => "\\f",
                '\n' => "\\n",
                '\r' => "\\r",
                '\t' => "\\t",
                _ => null, // either no escaping needed, or a \uXXXX control char (below)
            };

            if (rep is null && c >= ' ')
            {
                sb?.Append(c);
                continue;
            }

            // First char that needs escaping: allocate and back-fill the clean prefix.
            sb ??= new StringBuilder(s.Length + 16).Append(s, 0, i);
            if (rep is not null)
                sb.Append(rep);
            else
                // Remaining C0 control char (< U+0020) with no short form: \uXXXX.
                sb.Append("\\u").Append(((int)c).ToString("x4", System.Globalization.CultureInfo.InvariantCulture));
        }

        return sb?.ToString() ?? s;
    }
}
