using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Libraries;

namespace Oxide.Plugins
{
    [Info("Orangemart", "saulteafarmer", "0.3.0")]
    [Description("Allows players to buy and sell in-game units and VIP status using Bitcoin Lightning Network payments via LNbits")]
    public class Orangemart : CovalencePlugin
    {
        // Configuration sections and keys
        private static class ConfigSections
        {
            public const string Commands = "Commands";
            public const string CurrencySettings = "CurrencySettings";
            public const string Discord = "Discord";
            public const string InvoiceSettings = "InvoiceSettings";
            public const string VIPSettings = "VIPSettings";
        }

        private static class ConfigKeys
        {
            // Commands
            public const string BuyCurrencyCommandName = "BuyCurrencyCommandName";
            public const string SendCurrencyCommandName = "SendCurrencyCommandName";
            public const string BuyVipCommandName = "BuyVipCommandName";

            // CurrencySettings
            public const string CurrencyItemID = "CurrencyItemID";
            public const string CurrencyName = "CurrencyName";
            public const string CurrencySkinID = "CurrencySkinID";
            public const string PricePerCurrencyUnit = "PricePerCurrencyUnit";
            public const string SatsPerCurrencyUnit = "SatsPerCurrencyUnit";

            // Discord
            public const string DiscordChannelName = "DiscordChannelName";
            public const string DiscordWebhookUrl = "DiscordWebhookUrl";

            // InvoiceSettings
            public const string BlacklistedDomains = "BlacklistedDomains";
            public const string WhitelistedDomains = "WhitelistedDomains";
            public const string CheckIntervalSeconds = "CheckIntervalSeconds";
            public const string InvoiceTimeoutSeconds = "InvoiceTimeoutSeconds";
            public const string LNbitsApiKey = "LNbitsApiKey";
            public const string LNbitsBaseUrl = "LNbitsBaseUrl";
            public const string MaxRetries = "MaxRetries";

            // VIPSettings
            public const string VipPermissionGroup = "VipPermissionGroup";
            public const string VipPrice = "VipPrice";
        }

        // Configuration variables
        private int currencyItemID;
        private string buyCurrencyCommandName;
        private string sendCurrencyCommandName;
        private string buyVipCommandName;
        private int vipPrice;
        private string vipPermissionGroup;
        private string currencyName;
        private int satsPerCurrencyUnit;
        private int pricePerCurrencyUnit;
        private string discordChannelName;
        private ulong currencySkinID;
        private int checkIntervalSeconds;
        private int invoiceTimeoutSeconds;
        private int maxRetries;
        private List<string> blacklistedDomains = new List<string>();
        private List<string> whitelistedDomains = new List<string>();
        private const string SellLogFile = "Orangemart/send_bitcoin.json";
        private const string BuyInvoiceLogFile = "Orangemart/buy_invoices.json";
        private LNbitsConfig config;
        private List<PendingInvoice> pendingInvoices = new List<PendingInvoice>();
        private Dictionary<string, int> retryCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

        // LNbits Configuration
        private class LNbitsConfig
        {
            public string BaseUrl { get; set; }
            public string ApiKey { get; set; }
            public string DiscordWebhookUrl { get; set; }

            public static LNbitsConfig ParseLNbitsConnection(string baseUrl, string apiKey, string discordWebhookUrl)
            {
                var trimmedBaseUrl = baseUrl.TrimEnd('/');
                if (!Uri.IsWellFormedUriString(trimmedBaseUrl, UriKind.Absolute))
                    throw new Exception("Invalid base URL in connection string.");

                return new LNbitsConfig
                {
                    BaseUrl = trimmedBaseUrl,
                    ApiKey = apiKey,
                    DiscordWebhookUrl = discordWebhookUrl
                };
            }
        }

        // Invoice and Payment Classes
        private class InvoiceResponse
        {
            [JsonProperty("payment_request")]
            public string PaymentRequest { get; set; }

            [JsonProperty("payment_hash")]
            public string PaymentHash { get; set; }
        }

        private class SellInvoiceLogEntry
        {
            public string SteamID { get; set; }
            public string LightningAddress { get; set; }
            public bool Success { get; set; }
            public int SatsAmount { get; set; }
            public string PaymentHash { get; set; }
            public bool CurrencyReturned { get; set; }
            public DateTime Timestamp { get; set; }
            public int RetryCount { get; set; }
        }

        private class BuyInvoiceLogEntry
        {
            public string SteamID { get; set; }
            public string InvoiceID { get; set; }
            public bool IsPaid { get; set; }
            public DateTime Timestamp { get; set; }
            public int Amount { get; set; }
            public bool CurrencyGiven { get; set; }
            public bool VipGranted { get; set; }
            public int RetryCount { get; set; }
        }

        private class PendingInvoice
        {
            public string RHash { get; set; }
            public IPlayer Player { get; set; }
            public int Amount { get; set; }
            public string Memo { get; set; }
            public DateTime CreatedAt { get; set; }
            public PurchaseType Type { get; set; }
        }

        private enum PurchaseType
        {
            Currency,
            Vip,
            SendBitcoin
        }

        private class PaymentStatusResponse
        {
            [JsonProperty("paid")]
            public bool Paid { get; set; }

            [JsonProperty("preimage")]
            public string Preimage { get; set; }
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                bool configChanged = false;

                // Parse LNbits connection settings
                config = LNbitsConfig.ParseLNbitsConnection(
                    GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.LNbitsBaseUrl, "https://your-lnbits-instance.com", ref configChanged),
                    GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.LNbitsApiKey, "your-lnbits-admin-api-key", ref configChanged),
                    GetConfigValue(ConfigSections.Discord, ConfigKeys.DiscordWebhookUrl, "https://discord.com/api/webhooks/your_webhook_url", ref configChanged)
                );

                // Parse Currency Settings
                currencyItemID = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.CurrencyItemID, 1776460938, ref configChanged);
                currencyName = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.CurrencyName, "blood", ref configChanged);
                satsPerCurrencyUnit = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.SatsPerCurrencyUnit, 1, ref configChanged);
                pricePerCurrencyUnit = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.PricePerCurrencyUnit, 1, ref configChanged);
                currencySkinID = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.CurrencySkinID, 0UL, ref configChanged);

                // Parse Command Names
                buyCurrencyCommandName = GetConfigValue(ConfigSections.Commands, ConfigKeys.BuyCurrencyCommandName, "buyblood", ref configChanged);
                sendCurrencyCommandName = GetConfigValue(ConfigSections.Commands, ConfigKeys.SendCurrencyCommandName, "sendblood", ref configChanged);
                buyVipCommandName = GetConfigValue(ConfigSections.Commands, ConfigKeys.BuyVipCommandName, "buyvip", ref configChanged);

                // Parse VIP Settings
                vipPrice = GetConfigValue(ConfigSections.VIPSettings, ConfigKeys.VipPrice, 1000, ref configChanged);
                vipPermissionGroup = GetConfigValue(ConfigSections.VIPSettings, ConfigKeys.VipPermissionGroup, "vip", ref configChanged);

                // Parse Discord Settings
                discordChannelName = GetConfigValue(ConfigSections.Discord, ConfigKeys.DiscordChannelName, "mart", ref configChanged);

                // Parse Invoice Settings
                checkIntervalSeconds = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.CheckIntervalSeconds, 10, ref configChanged);
                invoiceTimeoutSeconds = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.InvoiceTimeoutSeconds, 300, ref configChanged);
                maxRetries = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.MaxRetries, 25, ref configChanged);

                blacklistedDomains = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.BlacklistedDomains, new List<string> { "example.com", "blacklisted.net" }, ref configChanged)
                    .Select(d => d.ToLower()).ToList();

                whitelistedDomains = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.WhitelistedDomains, new List<string>(), ref configChanged)
                    .Select(d => d.ToLower()).ToList();

                MigrateConfig();

                if (configChanged)
                {
                    SaveConfig();
                }
            }
            catch (Exception ex)
            {
                PrintError($"Failed to load configuration: {ex.Message}");
            }
        }

        private void MigrateConfig()
        {
            bool configChanged = false;

            if (!(Config[ConfigSections.InvoiceSettings] is Dictionary<string, object> invoiceSettings))
            {
                invoiceSettings = new Dictionary<string, object>();
                Config[ConfigSections.InvoiceSettings] = invoiceSettings;
                configChanged = true;
            }

            if (!invoiceSettings.ContainsKey(ConfigKeys.WhitelistedDomains))
            {
                invoiceSettings[ConfigKeys.WhitelistedDomains] = new List<string>();
                configChanged = true;
            }

            if (configChanged)
            {
                SaveConfig();
            }
        }

        private T GetConfigValue<T>(string section, string key, T defaultValue, ref bool configChanged)
        {
            if (!(Config[section] is Dictionary<string, object> data))
            {
                data = new Dictionary<string, object>();
                Config[section] = data;
                configChanged = true;
            }

            if (!data.TryGetValue(key, out var value))
            {
                value = defaultValue;
                data[key] = value;
                configChanged = true;
            }

            try
            {
                if (value is T tValue)
                {
                    return tValue;
                }
                else if (typeof(T) == typeof(List<string>))
                {
                    if (value is IEnumerable<object> enumerable)
                    {
                        return (T)(object)enumerable.Select(item => item.ToString()).ToList();
                    }
                    else if (value is string singleString)
                    {
                        return (T)(object)new List<string> { singleString };
                    }
                    else
                    {
                        PrintError($"Unexpected type for [{section}][{key}]. Using default value.");
                        data[key] = defaultValue;
                        configChanged = true;
                        return defaultValue;
                    }
                }
                else if (typeof(T) == typeof(ulong))
                {
                    if (value is long longVal)
                    {
                        return (T)(object)(ulong)longVal;
                    }
                    else if (value is ulong ulongVal)
                    {
                        return (T)(object)ulongVal;
                    }
                    else
                    {
                        return (T)Convert.ChangeType(value, typeof(T));
                    }
                }
                else
                {
                    return (T)Convert.ChangeType(value, typeof(T));
                }
            }
            catch (Exception ex)
            {
                PrintError($"Error converting config value for [{section}][{key}]: {ex.Message}. Using default value.");
                data[key] = defaultValue;
                configChanged = true;
                return defaultValue;
            }
        }

        protected override void LoadDefaultConfig()
        {
            Config[ConfigSections.Commands] = new Dictionary<string, object>
            {
                [ConfigKeys.BuyCurrencyCommandName] = "buyblood",
                [ConfigKeys.BuyVipCommandName] = "buyvip",
                [ConfigKeys.SendCurrencyCommandName] = "sendblood"
            };

            Config[ConfigSections.CurrencySettings] = new Dictionary<string, object>
            {
                [ConfigKeys.CurrencyItemID] = 1776460938,
                [ConfigKeys.CurrencyName] = "blood",
                [ConfigKeys.CurrencySkinID] = 0UL,
                [ConfigKeys.PricePerCurrencyUnit] = 1,
                [ConfigKeys.SatsPerCurrencyUnit] = 1
            };

            Config[ConfigSections.Discord] = new Dictionary<string, object>
            {
                [ConfigKeys.DiscordChannelName] = "mart",
                [ConfigKeys.DiscordWebhookUrl] = "https://discord.com/api/webhooks/your_webhook_url"
            };

            Config[ConfigSections.InvoiceSettings] = new Dictionary<string, object>
            {
                [ConfigKeys.BlacklistedDomains] = new List<string> { "example.com", "blacklisted.net" },
                [ConfigKeys.WhitelistedDomains] = new List<string>(),
                [ConfigKeys.CheckIntervalSeconds] = 10,
                [ConfigKeys.InvoiceTimeoutSeconds] = 300,
                [ConfigKeys.LNbitsApiKey] = "your-lnbits-admin-api-key",
                [ConfigKeys.LNbitsBaseUrl] = "https://your-lnbits-instance.com",
                [ConfigKeys.MaxRetries] = 25
            };

            Config[ConfigSections.VIPSettings] = new Dictionary<string, object>
            {
                [ConfigKeys.VipPermissionGroup] = "vip",
                [ConfigKeys.VipPrice] = 1000
            };
        }

        private void Init()
        {
            // Register permissions
            permission.RegisterPermission("orangemart.buycurrency", this);
            permission.RegisterPermission("orangemart.sendcurrency", this);
            permission.RegisterPermission("orangemart.buyvip", this);
        }

        private void OnServerInitialized()
        {
            if (config == null)
            {
                PrintError("Plugin configuration is not properly set up. Please check your configuration file.");
                return;
            }

            // Register commands
            AddCovalenceCommand(buyCurrencyCommandName, nameof(CmdBuyCurrency), "orangemart.buycurrency");
            AddCovalenceCommand(sendCurrencyCommandName, nameof(CmdSendCurrency), "orangemart.sendcurrency");
            AddCovalenceCommand(buyVipCommandName, nameof(CmdBuyVip), "orangemart.buyvip");

            // Start a timer to check pending invoices periodically
            timer.Every(checkIntervalSeconds, CheckPendingInvoices);
        }

        private void Unload()
        {
            pendingInvoices.Clear();
            retryCounts.Clear();
        }

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["UsageSendCurrency"] = "Usage: /{0} <amount> <lightning_address>",
                ["NeedMoreCurrency"] = "You need more {0}. You currently have {1}.",
                ["FailedToReserveCurrency"] = "Failed to reserve currency. Please try again.",
                ["FailedToQueryLightningAddress"] = "Failed to query Lightning address for an invoice.",
                ["FailedToAuthenticate"] = "Failed to authenticate with LNbits.",
                ["InvoiceCreatedCheckDiscord"] = "Invoice created! Please check the #{0} channel on Discord to complete your payment.",
                ["FailedToCreateInvoice"] = "Failed to create an invoice. Please try again later.",
                ["FailedToProcessPayment"] = "Failed to process payment. Please try again later.",
                ["CurrencySentSuccess"] = "You have successfully sent {0} {1}!",
                ["PurchaseSuccess"] = "You have successfully purchased {0} {1}!",
                ["PurchaseVipSuccess"] = "You have successfully purchased VIP status!",
                ["InvalidCommandUsage"] = "Usage: /{0} <amount>",
                ["NoPermission"] = "You do not have permission to use this command.",
                ["FailedToFindBasePlayer"] = "Failed to find base player object for player {0}.",
                ["FailedToCreateCurrencyItem"] = "Failed to create {0} item for player {1}.",
                ["AddedToVipGroup"] = "Player {0} added to VIP group '{1}'.",
                ["InvoiceExpired"] = "Your invoice for {0} sats has expired. Please try again.",
                ["BlacklistedDomain"] = "The domain '{0}' is currently blacklisted. Please use a different Lightning address.",
                ["NotWhitelistedDomain"] = "The domain '{0}' is not whitelisted. Please use a Lightning address from the following domains: {1}.",
                ["InvalidLightningAddress"] = "The Lightning Address provided is invalid or cannot be resolved.",
                ["PaymentProcessing"] = "Your payment is being processed. You will receive a confirmation once it's complete."
            }, this);
        }

        private string Lang(string key, string userId = null, params object[] args)
        {
            return string.Format(lang.GetMessage(key, this, userId), args);
        }

        private List<Item> GetAllInventoryItems(BasePlayer player)
        {
            List<Item> allItems = new List<Item>();

            // Main Inventory
            if (player.inventory.containerMain != null)
                allItems.AddRange(player.inventory.containerMain.itemList);

            // Belt (Hotbar)
            if (player.inventory.containerBelt != null)
                allItems.AddRange(player.inventory.containerBelt.itemList);

            // Wear (Clothing)
            if (player.inventory.containerWear != null)
                allItems.AddRange(player.inventory.containerWear.itemList);

            return allItems;
        }

        private void CheckPendingInvoices()
        {
            foreach (var invoice in pendingInvoices.ToList())
            {
                string localPaymentHash = invoice.RHash.ToLower();
                CheckInvoicePaid(localPaymentHash, isPaid =>
                {
                    if (isPaid)
                    {
                        pendingInvoices.Remove(invoice);

                        switch (invoice.Type)
                        {
                            case PurchaseType.Currency:
                                RewardPlayer(invoice.Player, invoice.Amount);
                                break;
                            case PurchaseType.Vip:
                                GrantVip(invoice.Player);
                                break;
                            case PurchaseType.SendBitcoin:
                                invoice.Player.Reply(Lang("CurrencySentSuccess", invoice.Player.Id, invoice.Amount, currencyName));
                                break;
                        }

                        if (invoice.Type == PurchaseType.SendBitcoin)
                        {
                            var logEntry = new SellInvoiceLogEntry
                            {
                                SteamID = GetPlayerId(invoice.Player),
                                LightningAddress = ExtractLightningAddress(invoice.Memo),
                                Success = true,
                                SatsAmount = invoice.Amount,
                                PaymentHash = invoice.RHash,
                                CurrencyReturned = false,
                                Timestamp = DateTime.UtcNow,
                                RetryCount = retryCounts.ContainsKey(invoice.RHash) ? retryCounts[invoice.RHash] : 0
                            };
                            LogSellTransaction(logEntry);

                            Puts($"Invoice {invoice.RHash} marked as paid. RetryCount: {logEntry.RetryCount}");
                        }
                        else
                        {
                            var logEntry = CreateBuyInvoiceLogEntry(
                                player: invoice.Player,
                                invoiceID: invoice.RHash,
                                isPaid: true,
                                amount: invoice.Type == PurchaseType.SendBitcoin ? invoice.Amount : invoice.Amount * pricePerCurrencyUnit,
                                type: invoice.Type,
                                retryCount: retryCounts.ContainsKey(invoice.RHash) ? retryCounts[invoice.RHash] : 0
                            );
                            LogBuyInvoice(logEntry);
                            Puts($"Invoice {invoice.RHash} marked as paid. RetryCount: {logEntry.RetryCount}");
                        }

                        retryCounts.Remove(invoice.RHash);
                    }
                    else
                    {
                        if (!retryCounts.ContainsKey(localPaymentHash))
                        {
                            retryCounts[localPaymentHash] = 0;
                            Puts($"Initialized retry count for paymentHash: {localPaymentHash}");
                        }

                        retryCounts[localPaymentHash]++;
                        Puts($"Retry count for paymentHash {localPaymentHash}: {retryCounts[localPaymentHash]} of {maxRetries}");

                        if (retryCounts[localPaymentHash] >= maxRetries)
                        {
                            pendingInvoices.Remove(invoice);
                            int finalRetryCount = retryCounts[localPaymentHash];
                            retryCounts.Remove(localPaymentHash);
                            PrintWarning($"Invoice for player {GetPlayerId(invoice.Player)} expired (amount: {invoice.Amount} sats).");

                            invoice.Player.Reply(Lang("InvoiceExpired", invoice.Player.Id, invoice.Amount));

                            if (invoice.Type == PurchaseType.SendBitcoin)
                            {
                                var basePlayer = invoice.Player.Object as BasePlayer;
                                if (basePlayer != null)
                                {
                                    ReturnCurrency(basePlayer, invoice.Amount / satsPerCurrencyUnit);
                                    Puts($"Refunded {invoice.Amount / satsPerCurrencyUnit} {currencyName} to player {basePlayer.UserIDString} due to failed payment.");
                                }
                                else
                                {
                                    PrintError($"Failed to find base player object for player {invoice.Player.Id} to refund currency.");
                                }

                                var failedLogEntry = new SellInvoiceLogEntry
                                {
                                    SteamID = GetPlayerId(invoice.Player),
                                    LightningAddress = ExtractLightningAddress(invoice.Memo),
                                    Success = false,
                                    SatsAmount = invoice.Amount,
                                    PaymentHash = invoice.RHash,
                                    CurrencyReturned = true,
                                    Timestamp = DateTime.UtcNow,
                                    RetryCount = finalRetryCount
                                };
                                LogSellTransaction(failedLogEntry);
                                Puts($"Invoice {localPaymentHash} expired after {finalRetryCount} retries.");
                            }
                            else
                            {
                                var failedLogEntry = CreateBuyInvoiceLogEntry(
                                    player: invoice.Player,
                                    invoiceID: localPaymentHash,
                                    isPaid: false,
                                    amount: invoice.Type == PurchaseType.SendBitcoin ? invoice.Amount : invoice.Amount * pricePerCurrencyUnit,
                                    type: invoice.Type,
                                    retryCount: finalRetryCount
                                );
                                LogBuyInvoice(failedLogEntry);
                                Puts($"Invoice {localPaymentHash} expired after {finalRetryCount} retries.");
                            }
                        }
                        else
                        {
                            PrintWarning($"Retrying invoice {localPaymentHash}. Attempt {retryCounts[localPaymentHash]} of {maxRetries}.");
                        }
                    }
                });
            }
        }

        private void CheckInvoicePaid(string paymentHash, Action<bool> callback)
        {
            string normalizedPaymentHash = paymentHash.ToLower();
            string url = $"{config.BaseUrl}/api/v1/payments/{normalizedPaymentHash}";

            var headers = new Dictionary<string, string>
            {
                { "Content-Type", "application/json" },
                { "X-Api-Key", config.ApiKey }
            };

            MakeWebRequest(url, null, (code, response) =>
            {
                if (code != 200 || string.IsNullOrEmpty(response))
                {
                    PrintError($"Error checking invoice status: HTTP {code}");
                    callback(false);
                    return;
                }

                try
                {
                    var paymentStatus = JsonConvert.DeserializeObject<PaymentStatusResponse>(response);
                    callback(paymentStatus != null && paymentStatus.Paid);
                }
                catch (Exception ex)
                {
                    PrintError($"Failed to parse invoice status response: {ex.Message}");
                    callback(false);
                }
            }, RequestMethod.GET, headers);
        }

        private void CmdSendCurrency(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission("orangemart.sendcurrency"))
            {
                player.Reply(Lang("NoPermission", player.Id));
                return;
            }

            if (args.Length != 2 || !int.TryParse(args[0], out int amount) || amount <= 0)
            {
                player.Reply(Lang("UsageSendCurrency", player.Id, sendCurrencyCommandName));
                return;
            }

            string lightningAddress = args[1];

            if (!IsLightningAddressAllowed(lightningAddress))
            {
                string domain = GetDomainFromLightningAddress(lightningAddress);
                if (whitelistedDomains.Any())
                {
                    string whitelist = string.Join(", ", whitelistedDomains);
                    player.Reply(Lang("NotWhitelistedDomain", player.Id, domain, whitelist));
                }
                else
                {
                    player.Reply(Lang("BlacklistedDomain", player.Id, domain));
                }
                return;
            }

            var basePlayer = player.Object as BasePlayer;
            if (basePlayer == null)
            {
                player.Reply(Lang("FailedToFindBasePlayer", player.Id));
                return;
            }

            int currencyAmount = GetAllInventoryItems(basePlayer).Where(IsCurrencyItem).Sum(item => item.amount);

            if (currencyAmount < amount)
            {
                player.Reply(Lang("NeedMoreCurrency", player.Id, currencyName, currencyAmount));
                return;
            }

            if (!TryReserveCurrency(basePlayer, amount))
            {
                player.Reply(Lang("FailedToReserveCurrency", player.Id));
                return;
            }

            player.Reply(Lang("PaymentProcessing", player.Id));

            SendBitcoin(lightningAddress, amount * satsPerCurrencyUnit, (success, paymentHash) =>
            {
                if (success && !string.IsNullOrEmpty(paymentHash))
                {
                    LogSellTransaction(
                        new SellInvoiceLogEntry
                        {
                            SteamID = GetPlayerId(player),
                            LightningAddress = lightningAddress,
                            Success = true,
                            SatsAmount = amount * satsPerCurrencyUnit,
                            PaymentHash = paymentHash,
                            CurrencyReturned = false,
                            Timestamp = DateTime.UtcNow,
                            RetryCount = 0
                        }
                    );

                    var pendingInvoice = new PendingInvoice
                    {
                        RHash = paymentHash.ToLower(),
                        Player = player,
                        Amount = amount * satsPerCurrencyUnit,
                        Memo = $"Sending {amount} {currencyName} to {lightningAddress}",
                        CreatedAt = DateTime.UtcNow,
                        Type = PurchaseType.SendBitcoin
                    };
                    pendingInvoices.Add(pendingInvoice);

                    Puts($"Outbound payment to {lightningAddress} initiated. PaymentHash: {paymentHash}");
                }
                else
                {
                    player.Reply(Lang("FailedToProcessPayment", player.Id));

                    LogSellTransaction(
                        new SellInvoiceLogEntry
                        {
                            SteamID = GetPlayerId(player),
                            LightningAddress = lightningAddress,
                            Success = false,
                            SatsAmount = amount * satsPerCurrencyUnit,
                            PaymentHash = null,
                            CurrencyReturned = true,
                            Timestamp = DateTime.UtcNow,
                            RetryCount = 0
                        }
                    );

                    Puts($"Outbound payment to {lightningAddress} failed to initiate.");

                    ReturnCurrency(basePlayer, amount);
                    Puts($"Returned {amount} {currencyName} to player {basePlayer.UserIDString} due to failed payment.");
                }
            });
        }

        private bool IsLightningAddressAllowed(string lightningAddress)
        {
            string domain = GetDomainFromLightningAddress(lightningAddress);
            if (string.IsNullOrEmpty(domain))
                return false;

            if (whitelistedDomains.Any())
            {
                return whitelistedDomains.Contains(domain.ToLower());
            }
            else
            {
                return !blacklistedDomains.Contains(domain.ToLower());
            }
        }

        private string GetDomainFromLightningAddress(string lightningAddress)
        {
            if (string.IsNullOrEmpty(lightningAddress))
                return null;

            var parts = lightningAddress.Split('@');
            return parts.Length == 2 ? parts[1].ToLower() : null;
        }

        private void SendBitcoin(string lightningAddress, int satsAmount, Action<bool, string> callback)
        {
            ResolveLightningAddress(lightningAddress, satsAmount, bolt11 =>
            {
                if (string.IsNullOrEmpty(bolt11))
                {
                    PrintError($"Failed to resolve Lightning Address: {lightningAddress}");
                    callback(false, null);
                    return;
                }

                SendPayment(bolt11, satsAmount, (success, paymentHash) =>
                {
                    if (success && !string.IsNullOrEmpty(paymentHash))
                    {
                        StartPaymentStatusCheck(paymentHash, isPaid =>
                        {
                            callback(isPaid, isPaid ? paymentHash : null);
                        });
                    }
                    else
                    {
                        callback(false, null);
                    }
                });
            });
        }

        private void StartPaymentStatusCheck(string paymentHash, Action<bool> callback)
        {
            if (!retryCounts.ContainsKey(paymentHash))
            {
                retryCounts[paymentHash] = 0;
            }

            Timer timerInstance = null;
            timerInstance = timer.Repeat(checkIntervalSeconds, maxRetries, () =>
            {
                CheckInvoicePaid(paymentHash, isPaid =>
                {
                    if (isPaid)
                    {
                        callback(true);
                        timerInstance.Destroy();
                    }
                    else
                    {
                        retryCounts[paymentHash]++;
                        Puts($"PaymentHash {paymentHash} not yet paid. Retry {retryCounts[paymentHash]} of {maxRetries}.");

                        if (retryCounts[paymentHash] >= maxRetries)
                        {
                            callback(false);
                            timerInstance.Destroy();
                        }
                    }
                });
            });
        }

        private void CmdBuyCurrency(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission("orangemart.buycurrency"))
            {
                player.Reply(Lang("NoPermission", player.Id));
                return;
            }

            if (args.Length != 1 || !int.TryParse(args[0], out int amount) || amount <= 0)
            {
                player.Reply(Lang("InvalidCommandUsage", player.Id, buyCurrencyCommandName));
                return;
            }

            int amountSats = amount * pricePerCurrencyUnit;

            CreateInvoice(amountSats, $"Buying {amount} {currencyName}", invoiceResponse =>
            {
                if (invoiceResponse != null)
                {
                    SendInvoiceToDiscord(player, invoiceResponse.PaymentRequest, amountSats, $"Buying {amount} {currencyName}");

                    player.Reply(Lang("InvoiceCreatedCheckDiscord", player.Id, discordChannelName));

                    var pendingInvoice = new PendingInvoice
                    {
                        RHash = invoiceResponse.PaymentHash.ToLower(),
                        Player = player,
                        Amount = amount,
                        Memo = $"Buying {amount} {currencyName}",
                        CreatedAt = DateTime.UtcNow,
                        Type = PurchaseType.Currency
                    };
                    pendingInvoices.Add(pendingInvoice);

                    ScheduleInvoiceExpiry(pendingInvoice);
                }
                else
                {
                    player.Reply(Lang("FailedToCreateInvoice", player.Id));
                }
            });
        }

        private void CmdBuyVip(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission("orangemart.buyvip"))
            {
                player.Reply(Lang("NoPermission", player.Id));
                return;
            }

            int amountSats = vipPrice;

            CreateInvoice(amountSats, "Buying VIP Status", invoiceResponse =>
            {
                if (invoiceResponse != null)
                {
                    SendInvoiceToDiscord(player, invoiceResponse.PaymentRequest, amountSats, "Buying VIP Status");

                    player.Reply(Lang("InvoiceCreatedCheckDiscord", player.Id, discordChannelName));

                    var pendingInvoice = new PendingInvoice
                    {
                        RHash = invoiceResponse.PaymentHash.ToLower(),
                        Player = player,
                        Amount = amountSats,
                        Memo = "Buying VIP Status",
                        CreatedAt = DateTime.UtcNow,
                        Type = PurchaseType.Vip
                    };
                    pendingInvoices.Add(pendingInvoice);

                    ScheduleInvoiceExpiry(pendingInvoice);
                }
                else
                {
                    player.Reply(Lang("FailedToCreateInvoice", player.Id));
                }
            });
        }

        private void ScheduleInvoiceExpiry(PendingInvoice pendingInvoice)
        {
            timer.Once(invoiceTimeoutSeconds, () =>
            {
                if (pendingInvoices.Contains(pendingInvoice))
                {
                    pendingInvoices.Remove(pendingInvoice);
                    PrintWarning($"Invoice for player {GetPlayerId(pendingInvoice.Player)} expired (amount: {pendingInvoice.Amount} sats).");

                    int finalRetryCount = retryCounts.ContainsKey(pendingInvoice.RHash) ? retryCounts[pendingInvoice.RHash] : 0;

                    if (pendingInvoice.Type == PurchaseType.SendBitcoin)
                    {
                        var basePlayer = pendingInvoice.Player.Object as BasePlayer;
                        if (basePlayer != null)
                        {
                            ReturnCurrency(basePlayer, pendingInvoice.Amount / satsPerCurrencyUnit);
                            Puts($"Refunded {pendingInvoice.Amount / satsPerCurrencyUnit} {currencyName} to player {basePlayer.UserIDString} due to failed payment.");
                        }
                        else
                        {
                            PrintError($"Failed to find base player object for player {pendingInvoice.Player.Id} to refund currency.");
                        }

                        var logEntry = new SellInvoiceLogEntry
                        {
                            SteamID = GetPlayerId(pendingInvoice.Player),
                            LightningAddress = ExtractLightningAddress(pendingInvoice.Memo),
                            Success = false,
                            SatsAmount = pendingInvoice.Amount,
                            PaymentHash = pendingInvoice.RHash,
                            CurrencyReturned = true,
                            Timestamp = DateTime.UtcNow,
                            RetryCount = finalRetryCount
                        };
                        LogSellTransaction(logEntry);
                        Puts($"Invoice {pendingInvoice.RHash} for player {GetPlayerId(pendingInvoice.Player)} expired and logged.");
                    }
                    else
                    {
                        var logEntry = CreateBuyInvoiceLogEntry(
                            player: pendingInvoice.Player,
                            invoiceID: pendingInvoice.RHash,
                            isPaid: false,
                            amount: pendingInvoice.Type == PurchaseType.SendBitcoin ? pendingInvoice.Amount : pendingInvoice.Amount * pricePerCurrencyUnit,
                            type: pendingInvoice.Type,
                            retryCount: finalRetryCount
                        );
                        LogBuyInvoice(logEntry);
                        Puts($"Invoice {pendingInvoice.RHash} for player {GetPlayerId(pendingInvoice.Player)} expired and logged.");
                    }
                }
            });
        }

        private void SendPayment(string bolt11, int satsAmount, Action<bool, string> callback)
        {
            string url = $"{config.BaseUrl}/api/v1/payments";
            var requestBody = new
            {
                @out = true,
                bolt11 = bolt11,
                amount = satsAmount
            };
            string jsonBody = JsonConvert.SerializeObject(requestBody);

            var headers = new Dictionary<string, string>
            {
                { "X-Api-Key", config.ApiKey },
                { "Content-Type", "application/json" }
            };

            MakeWebRequest(url, jsonBody, (code, response) =>
            {
                if (code != 200 && code != 201)
                {
                    PrintError($"Error processing payment: HTTP {code}");
                    callback(false, null);
                    return;
                }

                try
                {
                    var paymentResponse = JsonConvert.DeserializeObject<Dictionary<string, object>>(response);
                    string paymentHash = paymentResponse.ContainsKey("payment_hash") ? paymentResponse["payment_hash"].ToString() : null;

                    if (!string.IsNullOrEmpty(paymentHash))
                    {
                        callback(true, paymentHash);
                    }
                    else
                    {
                        PrintError("Payment hash (rhash) is missing or invalid in the response.");
                        callback(false, null);
                    }
                }
                catch (Exception ex)
                {
                    PrintError($"Exception occurred while parsing payment response: {ex.Message}");
                    callback(false, null);
                }
            }, RequestMethod.POST, headers);
        }

        private void SendInvoiceToDiscord(IPlayer player, string invoice, int amountSats, string memo)
        {
            if (string.IsNullOrEmpty(config.DiscordWebhookUrl))
            {
                PrintError("Discord webhook URL is not configured.");
                return;
            }

            string qrCodeUrl = $"https://api.qrserver.com/v1/create-qr-code/?data={Uri.EscapeDataString(invoice)}&size=200x200";

            var webhookPayload = new
            {
                content = $"**{player.Name}**, please pay **{amountSats} sats** using the Lightning Network.",
                embeds = new[]
                {
                    new
                    {
                        title = "Payment Invoice",
                        description = $"{memo}\n\nPlease pay the following Lightning invoice to complete your purchase:\n\n```\n{invoice}\n```",
                        image = new
                        {
                            url = qrCodeUrl
                        },
                        fields = new[]
                        {
                            new { name = "Amount", value = $"{amountSats} sats", inline = true },
                            new { name = "Steam ID", value = GetPlayerId(player), inline = true }
                        }
                    }
                }
            };

            string jsonPayload = JsonConvert.SerializeObject(webhookPayload);

            MakeWebRequest(config.DiscordWebhookUrl, jsonPayload, (code, response) =>
            {
                if (code != 204)
                {
                    PrintError($"Failed to send invoice to Discord webhook: HTTP {code}");
                }
                else
                {
                    Puts($"Invoice sent to Discord for player {GetPlayerId(player)}.");
                }
            }, RequestMethod.POST, new Dictionary<string, string> { { "Content-Type", "application/json" } });
        }

        private void RewardPlayer(IPlayer player, int amount)
        {
            player.Reply($"You have successfully purchased {amount} {currencyName}!");

            var basePlayer = player.Object as BasePlayer;

            if (basePlayer != null)
            {
                var currencyItem = ItemManager.CreateByItemID(currencyItemID, amount);
                if (currencyItem != null)
                {
                    if (currencySkinID > 0)
                    {
                        currencyItem.skin = currencySkinID;
                    }

                    basePlayer.GiveItem(currencyItem);
                    Puts($"Gave {amount} {currencyName} (skinID: {currencySkinID}) to player {basePlayer.UserIDString}.");
                }
                else
                {
                    PrintError($"Failed to create {currencyName} item for player {basePlayer.UserIDString}.");
                }
            }
            else
            {
                PrintError($"Failed to find base player object for player {player.Id}.");
            }
        }

        private void GrantVip(IPlayer player)
        {
            player.Reply("You have successfully purchased VIP status!");

            permission.AddUserGroup(player.Id, vipPermissionGroup);

            Puts($"Player {GetPlayerId(player)} added to VIP group '{vipPermissionGroup}'.");
        }

        private bool TryReserveCurrency(BasePlayer player, int amount)
        {
            var items = GetAllInventoryItems(player).Where(IsCurrencyItem).ToList();
            int totalCurrency = items.Sum(item => item.amount);

            if (totalCurrency < amount)
            {
                return false;
            }

            int remaining = amount;

            foreach (var item in items)
            {
                if (item.amount > remaining)
                {
                    item.UseItem(remaining);
                    break;
                }
                else
                {
                    remaining -= item.amount;
                    item.Remove();
                }

                if (remaining <= 0)
                {
                    break;
                }
            }

            return true;
        }

        private void ReturnCurrency(BasePlayer player, int amount)
        {
            var returnedCurrency = ItemManager.CreateByItemID(currencyItemID, amount);
            if (returnedCurrency != null)
            {
                if (currencySkinID > 0)
                {
                    returnedCurrency.skin = currencySkinID;
                }
                returnedCurrency.MoveToContainer(player.inventory.containerMain);
            }
            else
            {
                PrintError($"Failed to create {currencyName} item to return to player {player.UserIDString}.");
            }
        }

        private bool IsCurrencyItem(Item item)
        {
            return item.info.itemid == currencyItemID && (currencySkinID == 0 || item.skin == currencySkinID);
        }

        private void LogSellTransaction(SellInvoiceLogEntry logEntry)
        {
            var logs = LoadSellLogData();
            logs.Add(logEntry);
            SaveSellLogData(logs);
            Puts($"[Orangemart] Logged sell transaction: {JsonConvert.SerializeObject(logEntry)}");
        }

        private List<SellInvoiceLogEntry> LoadSellLogData()
        {
            var path = Path.Combine(Interface.Oxide.DataDirectory, SellLogFile);
            return File.Exists(path)
                ? JsonConvert.DeserializeObject<List<SellInvoiceLogEntry>>(File.ReadAllText(path))
                : new List<SellInvoiceLogEntry>();
        }

        private void SaveSellLogData(List<SellInvoiceLogEntry> data)
        {
            var path = Path.Combine(Interface.Oxide.DataDirectory, SellLogFile);
            var directory = Path.GetDirectoryName(path);
            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            File.WriteAllText(path, JsonConvert.SerializeObject(data, Formatting.Indented));
        }

        private void LogBuyInvoice(BuyInvoiceLogEntry logEntry)
        {
            var logPath = Path.Combine(Interface.Oxide.DataDirectory, BuyInvoiceLogFile);
            var directory = Path.GetDirectoryName(logPath);
            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            List<BuyInvoiceLogEntry> invoiceLogs = File.Exists(logPath)
                ? JsonConvert.DeserializeObject<List<BuyInvoiceLogEntry>>(File.ReadAllText(logPath)) ?? new List<BuyInvoiceLogEntry>()
                : new List<BuyInvoiceLogEntry>();

            invoiceLogs.Add(logEntry);
            File.WriteAllText(logPath, JsonConvert.SerializeObject(invoiceLogs, Formatting.Indented));
            Puts($"[Orangemart] Logged buy invoice: {JsonConvert.SerializeObject(logEntry)}");
        }

        private BuyInvoiceLogEntry CreateBuyInvoiceLogEntry(IPlayer player, string invoiceID, bool isPaid, int amount, PurchaseType type, int retryCount)
        {
            return new BuyInvoiceLogEntry
            {
                SteamID = GetPlayerId(player),
                InvoiceID = invoiceID,
                IsPaid = isPaid,
                Timestamp = DateTime.UtcNow,
                Amount = type == PurchaseType.SendBitcoin ? amount : amount * pricePerCurrencyUnit,
                CurrencyGiven = isPaid && type == PurchaseType.Currency,
                VipGranted = isPaid && type == PurchaseType.Vip,
                RetryCount = retryCount
            };
        }

        private void CreateInvoice(int amountSats, string memo, Action<InvoiceResponse> callback)
        {
            string url = $"{config.BaseUrl}/api/v1/payments";

            var requestBody = new
            {
                @out = false,
                amount = amountSats,
                memo = memo
            };
            string jsonBody = JsonConvert.SerializeObject(requestBody);

            var headers = new Dictionary<string, string>
            {
                { "X-Api-Key", config.ApiKey },
                { "Content-Type", "application/json" }
            };

            MakeWebRequest(url, jsonBody, (code, response) =>
            {
                if (code != 200 && code != 201)
                {
                    PrintError($"Error creating invoice: HTTP {code}");
                    callback(null);
                    return;
                }

                try
                {
                    var invoiceResponse = JsonConvert.DeserializeObject<InvoiceResponse>(response);
                    callback(invoiceResponse != null && !string.IsNullOrEmpty(invoiceResponse.PaymentHash) ? invoiceResponse : null);
                }
                catch (Exception ex)
                {
                    PrintError($"Failed to deserialize invoice response: {ex.Message}");
                    callback(null);
                }
            }, RequestMethod.POST, headers);
        }

        private string GetPlayerId(IPlayer player)
        {
            var basePlayer = player.Object as BasePlayer;
            return basePlayer != null ? basePlayer.UserIDString : player.Id;
        }

        private void MakeWebRequest(string url, string jsonData, Action<int, string> callback, RequestMethod method = RequestMethod.GET, Dictionary<string, string> headers = null)
        {
            webrequest.Enqueue(url, jsonData, (code, response) =>
            {
                if (string.IsNullOrEmpty(response) && (code < 200 || code >= 300))
                {
                    PrintError($"Web request to {url} returned empty response or HTTP {code}");
                    callback(code, null);
                }
                else
                {
                    callback(code, response);
                }
            }, this, method, headers ?? new Dictionary<string, string> { { "Content-Type", "application/json" } });
        }

        private void ResolveLightningAddress(string lightningAddress, int amountSats, Action<string> callback)
        {
            var parts = lightningAddress.Split('@');
            if (parts.Length != 2)
            {
                PrintError($"Invalid Lightning Address format: {lightningAddress}");
                callback(null);
                return;
            }

            string user = parts[0];
            string domain = parts[1];

            string lnurlEndpoint = $"https://{domain}/.well-known/lnurlp/{user}";

            var headers = new Dictionary<string, string>
            {
                { "Content-Type", "application/json" }
            };

            MakeWebRequest(lnurlEndpoint, null, (code, response) =>
            {
                if (code != 200 || string.IsNullOrEmpty(response))
                {
                    PrintError($"Failed to fetch LNURL for {lightningAddress}: HTTP {code}");
                    callback(null);
                    return;
                }

                try
                {
                    var lnurlResponse = JsonConvert.DeserializeObject<LNURLResponse>(response);
                    if (lnurlResponse == null || string.IsNullOrEmpty(lnurlResponse.Callback))
                    {
                        PrintError($"Invalid LNURL response for {lightningAddress}");
                        callback(null);
                        return;
                    }

                    long amountMsat = (long)amountSats * 1000;

                    string callbackUrl = lnurlResponse.Callback;

                    string callbackUrlWithAmount = $"{callbackUrl}?amount={amountMsat}";

                    MakeWebRequest(callbackUrlWithAmount, null, (payCode, payResponse) =>
                    {
                        if (payCode != 200 || string.IsNullOrEmpty(payResponse))
                        {
                            PrintError($"Failed to perform LNURL Pay for {lightningAddress}: HTTP {payCode}");
                            callback(null);
                            return;
                        }

                        try
                        {
                            var payAction = JsonConvert.DeserializeObject<LNURLPayResponse>(payResponse);
                            if (payAction == null || string.IsNullOrEmpty(payAction.Pr))
                            {
                                PrintError($"Invalid LNURL Pay response for {lightningAddress}");
                                callback(null);
                                return;
                            }

                            callback(payAction.Pr);
                        }
                        catch (Exception ex)
                        {
                            PrintError($"Error parsing LNURL Pay response: {ex.Message}");
                            callback(null);
                        }
                    }, RequestMethod.GET, headers);
                }
                catch (Exception ex)
                {
                    PrintError($"Error parsing LNURL response: {ex.Message}");
                    callback(null);
                }
            }, RequestMethod.GET, headers);
        }

        private class LNURLResponse
        {
            [JsonProperty("tag")]
            public string Tag { get; set; }

            [JsonProperty("callback")]
            public string Callback { get; set; }

            [JsonProperty("minSendable")]
            public long MinSendable { get; set; }

            [JsonProperty("maxSendable")]
            public long MaxSendable { get; set; }

            [JsonProperty("metadata")]
            public string Metadata { get; set; }

            [JsonProperty("commentAllowed")]
            public int CommentAllowed { get; set; }

            [JsonProperty("allowsNostr")]
            public bool AllowsNostr { get; set; }

            [JsonProperty("nostrPubkey")]
            public string NostrPubkey { get; set; }
        }

        private class LNURLPayResponse
        {
            [JsonProperty("pr")]
            public string Pr { get; set; }

            [JsonProperty("routes")]
            public List<object> Routes { get; set; }
        }

        private string ExtractLightningAddress(string memo)
        {
            // Extract the Lightning Address from the memo
            // Expected format: "Sending {amount} {currency} to {lightning_address}"
            var parts = memo.Split(" to ");
            return parts.Length == 2 ? parts[1] : "unknown@unknown.com";
        }
    }
}