using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.RegularExpressions;
using Humanizer;
using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
using NLog;

namespace KfChatDotNetBot.Commands;

[NoPrefixRequired]
public class XeetEmbedCommand : ICommand
{
    private static string LoadingGif = "[img]https://i.ddos.lgbt/u/3sKyHs.webp[/img]";
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
    public List<Regex> Patterns { get; set; } =
    [
        new Regex(@"https?:\/\/x\.com\/(?:\#!\/)?(\w+)\/(?:status|statuses|thread)\/(?<xeetId>\d+)", RegexOptions.IgnoreCase),
        new Regex(@"https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/(?:status|statuses|thread)\/(?<xeetId>\d+)",
            RegexOptions.IgnoreCase),
        new Regex(@"https?:\/\/mobile\.twitter\.com\/(?:\#!\/)?(\w+)\/(?:status|statuses|thread)\/(?<xeetId>\d+)",
            RegexOptions.IgnoreCase)
    ];
    public string? HelpText { get; } = "Embed Xeets";
    public UserRight RequiredRight { get; } = UserRight.Loser;
    public TimeSpan Timeout { get; } = TimeSpan.FromSeconds(30);

    public RateLimitOptionsModel? RateLimitOptions { get; } = new()
    {
        MaxInvocations = 3,
        Window = TimeSpan.FromSeconds(30),
        // Really don't want to get rate-limited by FxTwitter hence global rate-limits
        Flags = RateLimitFlags.Global
    };
    public bool WhisperCanInvoke => false;

    public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments,
        CancellationToken ctx)
    {
        var kiwiFarmsUsername = await SettingsProvider.GetValueAsync(BuiltIn.Keys.KiwiFarmsUsername);
        if (message.Author.Username == kiwiFarmsUsername.Value)
        {
            return;
        }
        var xeetEnabled = await SettingsProvider.GetValueAsync(BuiltIn.Keys.XeetEnabled);
        if (!xeetEnabled.ToBoolean()) return;

        var loadingMessage = await botInstance.SendChatMessageAsync($"{LoadingGif} Fetching tweet...", true);

        var success = await botInstance.WaitForChatMessageAsync(loadingMessage, TimeSpan.FromSeconds(10), ctx);
        if (!success) throw new InvalidOperationException();

        try
        {
            var xeetId = arguments["xeetId"].Value;
            var tweetData = await FetchTweetDataAsync(xeetId, ctx);

            if (tweetData == null)
            {
                throw new InvalidOperationException("tweetData was null");
            }

            var tweet = tweetData.Tweet;

            var mediaUrls = new List<string>();
            if (tweet.HasAnyMedia())
            {
                mediaUrls = await ProcessMediaAsync(tweet, ctx);
            }

            var messages = await BuildTweetMessagesAsync(tweet, xeetId, mediaUrls, ctx);

            if (loadingMessage.ChatMessageUuid != null)
            {
                await botInstance.KfClient.DeleteMessageAsync(loadingMessage.ChatMessageUuid);
            }

            if (messages.Count == 0)
            {
                return;
            }

            if (messages.Count > 4)
            {
                // bail, we don't want to spam the chat with giant threads of messages if something goes wrong with the splitting logic
                Logger.Warn($"Aborting sending Xeet embed - message count {messages.Count} exceeds threshold");
                return;
            }

            foreach (var msg in messages)
            {
                await botInstance.SendChatMessageAsync(msg, true);
            }
            // send archive link message
            var url = $"https://nitter.net/{tweet.Author.ScreenName}/status/{xeetId}";
            await botInstance.SendChatMessageAsync(
                $"[url=https://archive.is/submit/?url={url}]Archive Xeet on archive.is[/url]", true);
        }
        catch
        {
            // Delete loading message on error
            if (loadingMessage.ChatMessageUuid != null)
            {
                await botInstance.KfClient.DeleteMessageAsync(loadingMessage.ChatMessageUuid);
            }
            throw;
        }
    }

    private async Task<FxTwitterResponse?> FetchTweetDataAsync(string xeetId, CancellationToken ctx)
    {
        var api = $"https://api.fxtwitter.com/status/{xeetId}";

        var proxy = await SettingsProvider.GetValueAsync(BuiltIn.Keys.Proxy);
        var handler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.All,
            AllowAutoRedirect = true
        };
        if (proxy.Value != null)
        {
            handler.UseProxy = true;
            handler.Proxy = new WebProxy(proxy.Value);
            Logger.Debug($"Configured to use proxy {proxy.Value}");
        }
        // Yes, very ghetto but we do need a "real" UA for FxTwitter from my experience
        var ua = await SettingsProvider.GetValueAsync(BuiltIn.Keys.CaptureYtDlpUserAgent);

        using var client = new HttpClient(handler);
        client.DefaultRequestHeaders.UserAgent.ParseAdd(ua.Value);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        return await client.GetFromJsonAsync<FxTwitterResponse>(api, cancellationToken: ctx);
    }

    private async Task<List<string>> ProcessMediaAsync(FxTweet tweet, CancellationToken ctx)
    {
        var uploadedUrls = new List<string>();

        if (!await Zipline.IsZiplineEnabled())
        {
            Logger.Warn("Zipline is not enabled, skipping media upload");
            return uploadedUrls;
        }

        try
        {
            if (tweet.Media?.Photos != null)
            {
                foreach (var photo in tweet.Media.Photos)
                {
                    try
                    {
                        var url = await DownloadAndUploadImageAsync(photo.Url, ctx);
                        if (!string.IsNullOrEmpty(url))
                        {
                            uploadedUrls.Add(url);
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Error(ex, $"Failed to process photo: {photo.Url}");
                    }
                }
            }

            if (tweet.Media?.Videos != null)
            {
                foreach (var video in tweet.Media.Videos)
                {
                    try
                    {
                        var url = await DownloadAndConvertVideoAsync(video, ctx);
                        if (!string.IsNullOrEmpty(url))
                        {
                            uploadedUrls.Add(url);
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Error(ex, $"Failed to process video: {video.Url}");
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Logger.Error(ex, "Error processing media");
        }

        return uploadedUrls;
    }

    private async Task<string?> DownloadAndUploadImageAsync(string imageUrl, CancellationToken ctx)
    {
        using var httpClient = new HttpClient();
        var imageBytes = await httpClient.GetByteArrayAsync(imageUrl, ctx);

        using var imageStream = new MemoryStream(imageBytes);
        var url = await Zipline.Upload(imageStream, new MediaTypeHeaderValue("image/jpeg"), "9h", ctx);

        Logger.Info($"Uploaded image to Zipline: {url}");
        return url;
    }

    private async Task<string?> DownloadAndConvertVideoAsync(FxVideo video, CancellationToken ctx)
    {
        var maxDurationSetting = await SettingsProvider.GetValueAsync(BuiltIn.Keys.XeetMaxVideoDurationSeconds);
        var maxDuration = maxDurationSetting.ToType<int>();

        if (video.Duration > maxDuration)
        {
            Logger.Info($"Skipping video conversion: duration {video.Duration}s exceeds max {maxDuration}s");
            return null;
        }

        // Get the best quality variant
        var bestVariant = video.Variants
            .Where(v => v.Bitrate.HasValue)
            .OrderByDescending(v => v.Bitrate)
            .FirstOrDefault() ?? video.Variants.FirstOrDefault();

        if (bestVariant == null)
        {
            Logger.Warn("No video variant found");
            return null;
        }

        var videoUrl = bestVariant.Url;
        Logger.Info($"Downloading video from {videoUrl} (duration: {video.Duration}s)");

        using var httpClient = new HttpClient();
        var videoBytes = await httpClient.GetByteArrayAsync(videoUrl, ctx);

        var tempVideoPath = Path.Combine(Path.GetTempPath(), $"tweet_video_{Guid.NewGuid()}.mp4");
        var tempWebpPath = Path.Combine(Path.GetTempPath(), $"tweet_video_{Guid.NewGuid()}.webp");

        try
        {
            await File.WriteAllBytesAsync(tempVideoPath, videoBytes, ctx);

            var ffmpegPath = await SettingsProvider.GetValueAsync(BuiltIn.Keys.FFmpegBinaryPath);

            var ffmpegArgs = $"-i \"{tempVideoPath}\" -vf \"fps=20,scale='min(640,iw)':'min(480,ih)':force_original_aspect_ratio=decrease\" -c:v libwebp -threads 4 -lossless 0 -quality 75 -loop 0 -an \"{tempWebpPath}\"";

            var processInfo = new ProcessStartInfo
            {
                FileName = ffmpegPath.Value ?? "ffmpeg",
                Arguments = ffmpegArgs,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };

            using var process = Process.Start(processInfo);
            if (process == null)
            {
                Logger.Error("Failed to start FFmpeg process");
                return null;
            }

            await process.WaitForExitAsync(ctx);

            if (process.ExitCode != 0)
            {
                var error = await process.StandardError.ReadToEndAsync(ctx);
                Logger.Error($"FFmpeg conversion failed: {error}");
                return null;
            }

            await using var webpStream = File.OpenRead(tempWebpPath);
            var url = await Zipline.Upload(webpStream, new MediaTypeHeaderValue("image/webp"), "9h", ctx);

            Logger.Info($"Uploaded video as WebP to Zipline: {url}");
            return url;
        }
        finally
        {
            try
            {
                if (File.Exists(tempVideoPath)) File.Delete(tempVideoPath);
                if (File.Exists(tempWebpPath)) File.Delete(tempWebpPath);
            }
            catch (Exception ex)
            {
                Logger.Warn(ex, "Failed to cleanup temp files");
            }
        }
    }

    private async Task<List<string>> BuildTweetMessagesAsync(FxTweet tweet, string xeetId, List<string> mediaUrls, CancellationToken ctx)
    {
        var bodyBuilder = new StringBuilder();
        var footerBuilder = new StringBuilder();

        // Build header - main tweet author and timestamp (always goes first)
        var created = DateTimeOffset.FromUnixTimeSeconds(tweet.CreatedTimestamp);
        var header = $"[b]{tweet.Author.Name}[/b] (@{tweet.Author.ScreenName}) - {created.Humanize(DateTimeOffset.UtcNow)}[br]";

        // Handle reply chain (if this tweet is a reply)
        if (!string.IsNullOrEmpty(tweet.ReplyingToStatus))
        {
            try
            {
                var replyData = await FetchTweetDataAsync(tweet.ReplyingToStatus, ctx);
                if (replyData?.Tweet != null)
                {
                    var replyTweet = replyData.Tweet;
                    var replyCreated = DateTimeOffset.FromUnixTimeSeconds(replyTweet.CreatedTimestamp);
                    bodyBuilder.Append($"[i]↩️  Replying to:[/i][br]");
                    bodyBuilder.Append($"[b]{replyTweet.Author.Name}[/b] (@{replyTweet.Author.ScreenName}) - {replyCreated.Humanize(DateTimeOffset.UtcNow)}[br]");

                    var replyText = replyTweet.Text;
                    const int replyTextLimit = 250;
                    if (replyText.Utf8LengthBytes() > replyTextLimit)
                    {
                        replyText = replyText.TruncateBytes(replyTextLimit).TrimEnd() + "…";
                    }
                    bodyBuilder.Append($"{replyText}[br][br]");
                }
            }
            catch (Exception ex)
            {
                Logger.Error(ex, $"Failed to fetch reply tweet: {tweet.ReplyingToStatus}");
            }
        }

        // Main tweet text
        var mainText = tweet.Text;
        bodyBuilder.Append($"{mainText}[br]");

        if (mediaUrls.Count > 0)
        {
            if (mediaUrls.Count >= 2)
            {
                bodyBuilder.Append("[spoiler=\"Media attachments\"]");
            }
            foreach (var mediaUrl in mediaUrls)
            {
                bodyBuilder.Append($"[img]{mediaUrl}[/img]");
            }
            if (mediaUrls.Count > 2)
            {
                bodyBuilder.Append("[/spoiler]");
            }
            bodyBuilder.Append("[br]");
        }

        // Handle quote tweet (if this tweet quotes another)
        if (tweet.Quote != null)
        {
            bodyBuilder.Append("[br]");
            var quoteTweet = tweet.Quote;
            var quoteCreated = DateTimeOffset.FromUnixTimeSeconds(quoteTweet.CreatedTimestamp);
            bodyBuilder.Append($"[i]💬 Quoting:[/i][br]");
            bodyBuilder.Append($"[b]{quoteTweet.Author.Name}[/b] (@{quoteTweet.Author.ScreenName}) - {quoteCreated.Humanize(DateTimeOffset.UtcNow)}[br]");

            var quoteText = quoteTweet.Text;
            const int quoteTextLimit = 250;
            if (quoteText.Utf8LengthBytes() > quoteTextLimit)
            {
                quoteText = quoteText.TruncateBytes(quoteTextLimit).TrimEnd() + "…";
            }
            bodyBuilder.Append($"{quoteText}[br]");
        }

        // Build footer (stats + links) - this will always be on the last message
        footerBuilder.Append($"💬 {tweet.Replies:N0} 🔁 {tweet.Retweets:N0} ❤️ {tweet.Likes:N0} 👁️ {tweet.Views:N0}[br]");
        footerBuilder.Append($"[url={tweet.Url}]X.com[/url] | [url=https://xcancel.com/{tweet.Author.ScreenName}/status/{xeetId}]Xcancel[/url]");

        // Split message if needed, with header always first and footer always last
        var messages = (header + bodyBuilder + footerBuilder).FancySplitMessage();

        return messages;
    }
}