using System;
using System.Collections.Generic;
using System.IO;
using BepInEx;
using LitJson;

namespace SVSimLoader;

internal static class CaptureWriter
{
    private static readonly object _lock = new object();
    private static string _sessionDir;
    private static string _trafficPath;
    private static string _battleTrafficPath;
    private static string _cardsPath;
    private static string _userDataPath;
    private static string _sbsPath;
    private static readonly HashSet<string> _seenSbsIds = new HashSet<string>();
    private static ulong _lastSeenSteamId;

    public static string SessionDirectory => _sessionDir;

    public static void Initialize()
    {
        string root = Path.Combine(Paths.BepInExRootPath, "svsim-captures");
        string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
        string host = SanitizeForPath(ExtractHost(SvSimConfig.ApplicationUrl));
        string session = string.IsNullOrEmpty(host) ? timestamp : $"{timestamp}_{host}";
        _sessionDir = Path.Combine(root, session);
        Directory.CreateDirectory(_sessionDir);
        _trafficPath = Path.Combine(_sessionDir, "traffic.ndjson");
        _battleTrafficPath = Path.Combine(_sessionDir, "battle-traffic.ndjson");
        _cardsPath = Path.Combine(_sessionDir, "cards.json");
        _userDataPath = Path.Combine(_sessionDir, "user-data.json");
        _sbsPath = Path.Combine(_sessionDir, "special-battle-settings.ndjson");
    }

    private static string ExtractHost(string url)
    {
        if (string.IsNullOrEmpty(url)) return null;
        try { return new Uri(url).Host; }
        catch { return null; }
    }

    private static string SanitizeForPath(string s)
    {
        if (string.IsNullOrEmpty(s)) return s;
        foreach (char c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_');
        return s;
    }

    public static void WriteCards(string json)
    {
        lock (_lock)
        {
            File.WriteAllText(_cardsPath, json);
        }
    }

    /// <summary>
    /// Append a single special_battle_setting payload to special-battle-settings.ndjson,
    /// keyed-deduped by sbs id (in-memory, per session). Story chapters reuse sbs rows
    /// heavily (one row spans many chapters across many classes), so dedup keeps the
    /// file compact. Bootstrap-side join with /info captures resolves the
    /// chapter → sbs_id mapping separately.
    /// </summary>
    public static void AppendSpecialBattleSetting(string sbsId, JsonData sbsPayload)
    {
        if (string.IsNullOrEmpty(sbsId) || sbsPayload == null) return;
        lock (_lock)
        {
            if (!_seenSbsIds.Add(sbsId)) return;
            var ts = DateTime.UtcNow.ToString("o");
            var line = "{\"ts\":\"" + ts
                + "\",\"id\":\"" + EscapeJsonString(sbsId)
                + "\",\"payload\":" + sbsPayload.ToJson() + "}";
            File.AppendAllText(_sbsPath, line + "\n");
        }
    }

    private static string EscapeJsonString(string s)
    {
        return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
    }

    public static void AppendTraffic(string direction, string url, bool encrypted, string body)
    {
        string envelope = JsonMapper.ToJson(new Dictionary<string, object>
        {
            { "ts", DateTime.UtcNow.ToString("o") },
            { "direction", direction },
            { "url", url },
            { "encrypted", encrypted },
        });
        AppendLineWithBody(_trafficPath, envelope, body);
    }

    public static void AppendBattleTraffic(string direction, string uri, string body)
    {
        string envelope = JsonMapper.ToJson(new Dictionary<string, object>
        {
            { "ts", DateTime.UtcNow.ToString("o") },
            { "direction", direction },
            { "uri", uri },
        });
        AppendLineWithBody(_battleTrafficPath, envelope, body);
    }

    /// <summary>
    /// Track the most recent steam_id seen on an outgoing request body. We need it to enrich
    /// the user-data dump (/load/index response doesn't carry steam_id but the import endpoint
    /// needs it to link the viewer).
    /// </summary>
    public static void RecordSteamId(ulong steamId)
    {
        if (steamId != 0) _lastSeenSteamId = steamId;
    }

    /// <summary>
    /// Extract essential viewer fields from a /load/index response and write them as a clean
    /// JSON file matching the /admin/import_viewer request shape. Skipped silently when no
    /// steam_id has been seen yet (need at least one prior request to know the link).
    /// </summary>
    public static void WriteUserDataFromLoadIndex(JsonData loadIndexData)
    {
        if (_lastSeenSteamId == 0)
        {
            Plugin.Log?.LogWarning("DumpUserData: no steam_id seen yet, skipping user-data dump.");
            return;
        }

        try
        {
            // SetResponseData hands us the FULL response envelope { data_headers, data }; the
            // viewer payload (user_info, user_crystal_count, user_card_list, ...) lives under the
            // inner `data` key. Descend into it before extracting — same as
            // ExaminationPatches.TryExtractSpecialBattleSettings does. Without this every SafeGet
            // below misses and the dump contains nothing but steam_id. The inner payload has no
            // top-level `data` key of its own, so this is safe if a caller ever pre-strips it.
            if (loadIndexData != null && loadIndexData.IsObject && loadIndexData.Keys.Contains("data"))
            {
                loadIndexData = loadIndexData["data"];
            }

            var dump = new Dictionary<string, object>
            {
                { "steam_id", _lastSeenSteamId }
            };

            var userInfo = SafeGet(loadIndexData, "user_info");
            if (userInfo != null)
            {
                Copy(userInfo, "name", dump, "display_name");
                Copy(userInfo, "country_code", dump, "country_code");
                Copy(userInfo, "selected_emblem_id", dump, "selected_emblem_id");
                Copy(userInfo, "selected_degree_id", dump, "selected_degree_id");
            }

            var tutorial = SafeGet(loadIndexData, "user_tutorial");
            if (tutorial != null) Copy(tutorial, "tutorial_step", dump, "tutorial_state");

            var crystal = SafeGet(loadIndexData, "user_crystal_count");
            if (crystal != null)
            {
                var cur = new Dictionary<string, object>();
                Copy(crystal, "crystal", cur, "crystals");
                Copy(crystal, "rupy", cur, "rupees");
                Copy(crystal, "red_ether", cur, "red_ether");
                if (cur.Count > 0) dump["currency"] = cur;
            }

            ExtractIdArray(loadIndexData, "user_sleeve_list", "sleeve_id", dump, "owned_sleeve_ids");
            ExtractIdArray(loadIndexData, "user_emblem_list", "emblem_id", dump, "owned_emblem_ids");
            ExtractIdArray(loadIndexData, "user_degree_list", "degree_id", dump, "owned_degree_ids");
            ExtractMyPageList(loadIndexData, dump);
            ExtractOwnedLeaderSkins(loadIndexData, dump);
            ExtractClasses(loadIndexData, dump);
            ExtractOwnedCards(loadIndexData, dump);
            ExtractItems(loadIndexData, dump);
            ExtractDecks(loadIndexData, dump);

            lock (_lock)
            {
                File.WriteAllText(_userDataPath, JsonMapper.ToJson(dump));
            }
            Plugin.Log?.LogInfo($"Dumped user data to user-data.json (steam_id={_lastSeenSteamId}).");
        }
        catch (Exception e)
        {
            Plugin.Log?.LogError($"DumpUserData failed: {e}");
        }
    }

    private static JsonData SafeGet(JsonData data, string key)
    {
        if (data == null || !data.IsObject) return null;
        try
        {
            return data.Keys.Contains(key) ? data[key] : null;
        }
        catch
        {
            return null;
        }
    }

    private static void Copy(JsonData source, string srcKey, Dictionary<string, object> dest, string destKey)
    {
        var val = SafeGet(source, srcKey);
        if (val == null) return;
        if (val.IsInt) dest[destKey] = (int)val;
        else if (val.IsLong) dest[destKey] = (long)val;
        else if (val.IsString) dest[destKey] = (string)val;
        else if (val.IsBoolean) dest[destKey] = (bool)val;
        else if (val.IsDouble) dest[destKey] = (double)val;
    }

    private static void ExtractIdArray(JsonData data, string listKey, string idField,
        Dictionary<string, object> dump, string destKey)
    {
        var list = SafeGet(data, listKey);
        if (list == null || !list.IsArray) return;
        var ids = new List<int>();
        for (int i = 0; i < list.Count; i++)
        {
            var entry = list[i];
            var idVal = SafeGet(entry, idField);
            if (idVal == null) continue;
            if (idVal.IsInt) ids.Add((int)idVal);
            else if (idVal.IsLong) ids.Add((int)(long)idVal);
        }
        if (ids.Count > 0) dump[destKey] = ids;
    }

    private static void ExtractMyPageList(JsonData data, Dictionary<string, object> dump)
    {
        // Wire shape is string[] per the audit (LoadDetail.cs:387-392 calls .ToString()).
        var list = SafeGet(data, "user_mypage_list");
        if (list == null || !list.IsArray) return;
        var ids = new List<int>();
        for (int i = 0; i < list.Count; i++)
        {
            var entry = list[i];
            if (entry == null) continue;
            if (entry.IsInt) ids.Add((int)entry);
            else if (entry.IsLong) ids.Add((int)(long)entry);
            else if (entry.IsString && int.TryParse((string)entry, out int parsed)) ids.Add(parsed);
        }
        if (ids.Count > 0) dump["owned_mypage_background_ids"] = ids;
    }

    private static void ExtractOwnedLeaderSkins(JsonData data, Dictionary<string, object> dump)
    {
        var list = SafeGet(data, "user_leader_skin_list");
        if (list == null || !list.IsArray) return;
        var ids = new List<int>();
        for (int i = 0; i < list.Count; i++)
        {
            var entry = list[i];
            var isOwned = SafeGet(entry, "is_owned");
            bool owned = isOwned != null && (
                (isOwned.IsBoolean && (bool)isOwned) ||
                (isOwned.IsInt && (int)isOwned != 0) ||
                (isOwned.IsLong && (long)isOwned != 0));
            if (!owned) continue;
            var idVal = SafeGet(entry, "leader_skin_id");
            if (idVal == null) continue;
            if (idVal.IsInt) ids.Add((int)idVal);
            else if (idVal.IsLong) ids.Add((int)(long)idVal);
        }
        if (ids.Count > 0) dump["owned_leader_skin_ids"] = ids;
    }

    private static void ExtractClasses(JsonData data, Dictionary<string, object> dump)
    {
        var list = SafeGet(data, "user_class_list");
        if (list == null || !list.IsArray) return;
        var classes = new List<Dictionary<string, object>>();
        for (int i = 0; i < list.Count; i++)
        {
            var entry = list[i];
            var classId = SafeGet(entry, "class_id");
            if (classId == null) continue;
            var c = new Dictionary<string, object>();
            if (classId.IsInt) c["class_id"] = (int)classId;
            else if (classId.IsLong) c["class_id"] = (int)(long)classId;
            else continue;
            Copy(entry, "level", c, "level");
            Copy(entry, "exp", c, "exp");
            classes.Add(c);
        }
        if (classes.Count > 0) dump["classes"] = classes;
    }

    private static void ExtractOwnedCards(JsonData data, Dictionary<string, object> dump)
    {
        var list = SafeGet(data, "user_card_list");
        if (list == null || !list.IsArray) return;
        var cards = new List<Dictionary<string, object>>();
        for (int i = 0; i < list.Count; i++)
        {
            var entry = list[i];
            var idVal = SafeGet(entry, "card_id");
            if (idVal == null) continue;
            long cardId;
            if (idVal.IsInt) cardId = (int)idVal;
            else if (idVal.IsLong) cardId = (long)idVal;
            else continue;

            var c = new Dictionary<string, object> { { "card_id", cardId } };

            var num = SafeGet(entry, "number");
            if (num != null)
            {
                if (num.IsInt) c["count"] = (int)num;
                else if (num.IsLong) c["count"] = (int)(long)num;
            }

            var prot = SafeGet(entry, "is_protected");
            if (prot != null)
            {
                c["is_protected"] =
                    (prot.IsBoolean && (bool)prot) ||
                    (prot.IsInt && (int)prot != 0) ||
                    (prot.IsLong && (long)prot != 0);
            }
            cards.Add(c);
        }
        if (cards.Count > 0) dump["owned_cards"] = cards;
    }

    private static void ExtractItems(JsonData data, Dictionary<string, object> dump)
    {
        var list = SafeGet(data, "user_item_list");
        if (list == null || !list.IsArray) return;
        var items = new List<Dictionary<string, object>>();
        for (int i = 0; i < list.Count; i++)
        {
            var entry = list[i];
            var idVal = SafeGet(entry, "item_id");
            if (idVal == null) continue;
            var item = new Dictionary<string, object>();
            if (idVal.IsInt) item["item_id"] = (int)idVal;
            else if (idVal.IsLong) item["item_id"] = (int)(long)idVal;
            else continue;
            var num = SafeGet(entry, "number");
            if (num != null)
            {
                if (num.IsInt) item["count"] = (int)num;
                else if (num.IsLong) item["count"] = (int)(long)num;
            }
            items.Add(item);
        }
        if (items.Count > 0) dump["items"] = items;
    }

    // /load/index splits decks into one container per format; the format is the KEY, not a
    // per-deck field. Values mirror the wire deck_format codes (Wizard/Data.cs FormatConvertApi).
    private struct DeckFormatKey
    {
        public string Key;
        public int Format;
        public DeckFormatKey(string key, int format) { Key = key; Format = format; }
    }

    private static readonly DeckFormatKey[] DeckFormatKeys =
    {
        new DeckFormatKey("user_deck_rotation",     1),
        new DeckFormatKey("user_deck_unlimited",    2),
        new DeckFormatKey("user_deck_pre_rotation", 3),
        new DeckFormatKey("user_deck_crossover",    4),
        new DeckFormatKey("user_deck_my_rotation",  5),
    };

    private static void ExtractDecks(JsonData data, Dictionary<string, object> dump)
    {
        var decks = new List<Dictionary<string, object>>();
        foreach (var fmt in DeckFormatKeys)
        {
            var container = SafeGet(data, fmt.Key);
            var deckList = SafeGet(container, "user_deck_list");
            if (deckList == null || !deckList.IsArray) continue;

            for (int i = 0; i < deckList.Count; i++)
            {
                var entry = deckList[i];
                // /load/index ships every deck slot, most of them empty placeholders. Skip the
                // empty ones — the import drops them anyway, and it keeps the dump to the few real
                // decks instead of ~100 empty slots.
                var cardArr = ExtractLongArray(entry, "card_id_array");
                if (cardArr == null || cardArr.Count == 0) continue;

                var d = new Dictionary<string, object> { { "deck_format", fmt.Format } };
                Copy(entry, "deck_no", d, "deck_no");
                Copy(entry, "deck_name", d, "deck_name");
                Copy(entry, "class_id", d, "class_id");
                Copy(entry, "sleeve_id", d, "sleeve_id");
                Copy(entry, "leader_skin_id", d, "leader_skin_id");
                Copy(entry, "is_random_leader_skin", d, "is_random_leader_skin");
                Copy(entry, "rotation_id", d, "my_rotation_id"); // UserDeck.rotation_id -> import my_rotation_id
                d["card_id_array"] = cardArr;
                decks.Add(d);
            }
        }
        if (decks.Count > 0) dump["decks"] = decks;
    }

    private static List<long> ExtractLongArray(JsonData entry, string key)
    {
        var arr = SafeGet(entry, key);
        if (arr == null || !arr.IsArray) return null;
        var ids = new List<long>();
        for (int i = 0; i < arr.Count; i++)
        {
            var v = arr[i];
            if (v == null) continue;
            if (v.IsInt) ids.Add((int)v);
            else if (v.IsLong) ids.Add((long)v);
        }
        return ids;
    }

    // Splice the body into the envelope as nested JSON (parseable) or escaped string
    // (fallback). Cannot route this through Dictionary<string,object> → JsonMapper.ToJson:
    // a LitJson.JsonData value inside such a dict makes the reflection serializer
    // mis-bracket and throw "Can't close an object here".
    private static void AppendLineWithBody(string path, string envelopeJson, string body)
    {
        string bodyJson;
        if (body == null)
        {
            bodyJson = "null";
        }
        else if (body.Length == 0)
        {
            bodyJson = "\"\"";
        }
        else
        {
            try { bodyJson = JsonMapper.ToObject(body).ToJson(); }
            catch { bodyJson = JsonMapper.ToJson(body); }
        }
        string trimmed = envelopeJson.Substring(0, envelopeJson.Length - 1);
        string line = trimmed + ",\"body\":" + bodyJson + "}";
        lock (_lock)
        {
            File.AppendAllText(path, line + "\n");
        }
    }
}
