using System.Globalization;
using System.Text;
using HellionChat.Code;

namespace HellionChat.Export;

internal enum ExportFormat
{
    Markdown,
    Json,
    Csv,
}

internal static class ExportFormatExt
{
    internal static string Extension(this ExportFormat fmt) =>
        fmt switch
        {
            ExportFormat.Markdown => "md",
            ExportFormat.Json => "json",
            ExportFormat.Csv => "csv",
            _ => "txt",
        };

    internal static string Filter(this ExportFormat fmt) =>
        fmt switch
        {
            ExportFormat.Markdown => ".md",
            ExportFormat.Json => ".json",
            ExportFormat.Csv => ".csv",
            _ => ".txt",
        };
}

// Serializes message snapshots to Markdown, JSON, or CSV.
// Caller handles pre-filtering except sender substring, which requires deserialized SeString.TextValue.
internal static class MessageExporter
{
    internal record FilterDescription(
        IReadOnlyCollection<int>? ChatTypes,
        DateTimeOffset? From,
        DateTimeOffset? To,
        string? SenderSubstring
    );

    internal static int ExportToFile(
        string path,
        ExportFormat format,
        IEnumerable<Message> messages,
        FilterDescription filter
    )
    {
        var matching = filter.SenderSubstring is { Length: > 0 } needle
            ? messages.Where(m => MatchesSender(m, needle))
            : messages;

        using var writer = new StreamWriter(path, append: false, encoding: Encoding.UTF8);
        return format switch
        {
            ExportFormat.Markdown => WriteMarkdown(writer, matching, filter),
            ExportFormat.Json => WriteJson(writer, matching, filter),
            ExportFormat.Csv => WriteCsv(writer, matching, filter),
            _ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
        };
    }

    private static bool MatchesSender(Message m, string needle) =>
        m.SenderSource.TextValue.Contains(needle, StringComparison.OrdinalIgnoreCase);

    private static int WriteMarkdown(
        StreamWriter w,
        IEnumerable<Message> messages,
        FilterDescription filter
    )
    {
        w.WriteLine("# Hellion Chat Export");
        w.WriteLine();
        w.WriteLine($"Generated: {DateTimeOffset.Now:yyyy-MM-dd HH:mm zzz}");
        WriteFilterSummaryMarkdown(w, filter);
        w.WriteLine();

        DateTimeOffset? lastDate = null;
        var count = 0;
        foreach (var m in messages)
        {
            count++;
            var localDate = m.Date.ToLocalTime();
            if (lastDate is null || localDate.Date != lastDate.Value.Date)
            {
                w.WriteLine();
                w.WriteLine($"## {localDate:yyyy-MM-dd}");
                w.WriteLine();
                lastDate = localDate;
            }

            var chatType = (ChatType)(ushort)m.Code.Type;
            var sender = m.SenderSource.TextValue.Trim().Trim('<', '>', '[', ']', ':').Trim();
            var content = m.ContentSource.TextValue;

            if (string.IsNullOrEmpty(sender))
                w.WriteLine($"**[{localDate:HH:mm}] {chatType}:** {content}");
            else
                w.WriteLine($"**[{localDate:HH:mm}] {chatType} {sender}:** {content}");
        }

        w.WriteLine();
        w.WriteLine($"---");
        w.WriteLine($"Total messages: {count}");
        return count;
    }

    private static void WriteFilterSummaryMarkdown(StreamWriter w, FilterDescription filter)
    {
        if (filter.ChatTypes is { Count: > 0 })
            w.WriteLine(
                $"ChatTypes: {string.Join(", ", filter.ChatTypes.Select(t => $"{(ChatType)(ushort)t}({t})"))}"
            );
        if (filter.From is not null)
            w.WriteLine($"From: {filter.From.Value.ToLocalTime():yyyy-MM-dd HH:mm}");
        if (filter.To is not null)
            w.WriteLine($"To: {filter.To.Value.ToLocalTime():yyyy-MM-dd HH:mm}");
        if (filter.SenderSubstring is { Length: > 0 })
            w.WriteLine($"Sender contains: \"{filter.SenderSubstring}\"");
    }

    private static int WriteJson(
        StreamWriter w,
        IEnumerable<Message> messages,
        FilterDescription filter
    )
    {
        // Manual JSON to avoid System.Text.Json policy coupling.
        w.Write("{\n  \"exported_at\": \"");
        w.Write(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
        w.Write("\",\n  \"plugin\": \"Hellion Chat\",\n");
        w.Write("  \"filter\": {\n");
        w.Write("    \"chat_types\": ");
        if (filter.ChatTypes is { Count: > 0 })
            w.Write("[" + string.Join(",", filter.ChatTypes) + "]");
        else
            w.Write("null");
        w.Write(",\n    \"from\": ");
        w.Write(
            filter.From is null
                ? "null"
                : "\"" + filter.From.Value.ToString("O", CultureInfo.InvariantCulture) + "\""
        );
        w.Write(",\n    \"to\": ");
        w.Write(
            filter.To is null
                ? "null"
                : "\"" + filter.To.Value.ToString("O", CultureInfo.InvariantCulture) + "\""
        );
        w.Write(",\n    \"sender_substring\": ");
        w.Write(filter.SenderSubstring is null ? "null" : JsonString(filter.SenderSubstring));
        w.Write("\n  },\n  \"messages\": [\n");

        var first = true;
        var count = 0;
        foreach (var m in messages)
        {
            if (!first)
                w.Write(",\n");
            first = false;
            count++;

            var chatType = (ChatType)(ushort)m.Code.Type;
            w.Write("    {");
            w.Write($"\"id\":\"{m.Id}\"");
            w.Write($",\"date\":\"{m.Date.ToString("O", CultureInfo.InvariantCulture)}\"");
            w.Write($",\"chat_type\":{(int)m.Code.Type}");
            w.Write($",\"chat_type_name\":\"{chatType}\"");
            w.Write($",\"source_kind\":{m.Code.Source}");
            w.Write($",\"target_kind\":{m.Code.Target}");
            w.Write($",\"receiver\":{m.Receiver}");
            w.Write($",\"content_id\":{m.ContentId}");
            w.Write($",\"sender\":{JsonString(m.SenderSource.TextValue)}");
            w.Write($",\"content\":{JsonString(m.ContentSource.TextValue)}");
            w.Write("}");
        }

        w.Write("\n  ],\n");
        w.Write($"  \"total\": {count}\n}}\n");
        return count;
    }

    private static int WriteCsv(
        StreamWriter w,
        IEnumerable<Message> messages,
        FilterDescription filter
    )
    {
        // Header always written so empty exports remain importable.
        w.WriteLine("Date,ChatType,ChatTypeName,Sender,Content,Receiver,ContentId");
        var count = 0;
        foreach (var m in messages)
        {
            count++;
            var chatType = (ChatType)(ushort)m.Code.Type;
            w.Write(m.Date.ToString("O", CultureInfo.InvariantCulture));
            w.Write(',');
            w.Write((int)m.Code.Type);
            w.Write(',');
            w.Write(CsvString(chatType.ToString()));
            w.Write(',');
            w.Write(CsvString(m.SenderSource.TextValue));
            w.Write(',');
            w.Write(CsvString(m.ContentSource.TextValue));
            w.Write(',');
            w.Write(m.Receiver);
            w.Write(',');
            w.Write(m.ContentId);
            w.WriteLine();
        }
        return count;
    }

    private static string JsonString(string s)
    {
        var sb = new StringBuilder(s.Length + 2);
        sb.Append('"');
        foreach (var c in s)
        {
            switch (c)
            {
                case '"':
                    sb.Append("\\\"");
                    break;
                case '\\':
                    sb.Append("\\\\");
                    break;
                case '\b':
                    sb.Append("\\b");
                    break;
                case '\f':
                    sb.Append("\\f");
                    break;
                case '\n':
                    sb.Append("\\n");
                    break;
                case '\r':
                    sb.Append("\\r");
                    break;
                case '\t':
                    sb.Append("\\t");
                    break;
                default:
                    if (c < 0x20)
                        sb.Append($"\\u{(int)c:x4}");
                    else
                        sb.Append(c);
                    break;
            }
        }
        sb.Append('"');
        return sb.ToString();
    }

    private static string CsvString(string s)
    {
        if (s.IndexOfAny(['"', ',', '\n', '\r']) < 0)
            return s;
        return "\"" + s.Replace("\"", "\"\"") + "\"";
    }
}
