using dodohold.core;
using System.Net;
using System.Globalization;
using YunhuiKit;

namespace molilian.core
{
    public static class ZlongForwardCore
    {
        public const string RedisPrefix = ":union_parse:xhs_zlong_forward";
        public const string ExceptionRedisPrefix = $"{RedisPrefix}:exception";
        private const string ExceptionAggregateType = "all";
        private const string ForwardUrlTemplate = "https://tracker.z-long.cn/ad/tracker/special/liutiyun/{0}";
        private static readonly HttpClient HttpClient = new()
        {
            Timeout = TimeSpan.FromSeconds(10)
        };

        public static async Task ForwardAsync(string channel, string oaid)
        {
            string normalizedChannel = NormalizeChannel(channel);
            if (string.IsNullOrEmpty(normalizedChannel)) return;

            string requestUrl = string.Empty;
            try
            {
                long unixTimeMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                string forwardUrl = string.Format(CultureInfo.InvariantCulture, ForwardUrlTemplate, normalizedChannel);
                requestUrl = $"{forwardUrl}?oaid={WebUtility.UrlEncode(oaid ?? string.Empty)}&datetime={unixTimeMilliseconds}";
            }
            catch (Exception ex)
            {
                _ = new LoggerLibrary("zlong_forward")
                    .Info(normalizedChannel, requestUrl)
                    .Info(ex.Message, ex.StackTrace)
                    .SaveAsync();
                return;
            }

            try
            {
                SaveCount(normalizedChannel);
            }
            catch (Exception ex)
            {
                _ = new LoggerLibrary("zlong_forward")
                    .Info(normalizedChannel, requestUrl)
                    .Info(ex.Message, ex.StackTrace)
                    .SaveAsync();
            }

            try
            {
                using var response = await HttpClient.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead);
                if (response.IsSuccessStatusCode) return;

                SaveExceptionCount(normalizedChannel, $"status_{(int)response.StatusCode}");
            }
            catch (TaskCanceledException)
            {
                SaveExceptionCount(normalizedChannel, "timeout");
            }
            catch (Exception)
            {
                SaveExceptionCount(normalizedChannel, "request_error");
            }
        }

        private static void SaveCount(string channel)
        {
            DateTime now = DateTime.Now;
            var metrics = GetBucketMetrics(now);

            foreach (var metric in metrics)
            {
                string key = GetCountKey(channel, metric.bucketType, metric.bucketValue);
                SaveMetricCount(key, GetIndexKey(channel, metric.bucketType), metric.expireSeconds);
            }
        }

        public static async Task<ZlongForwardReportResult> QueryReportAsync(string channels, string bucketType, DateTime? start, DateTime? end, int page, int size)
        {
            string[] selectedChannels = NormalizeChannels(channels);
            string normalizedBucketType = NormalizeBucketType(bucketType);
            page = Math.Max(page, 1);
            size = size <= 0 ? 20 : size;

            var list = new List<ZlongForwardReportItem>();
            DateTime? startTime = start?.Date;
            DateTime? endTime = end?.Date.AddDays(1).AddTicks(-1);

            foreach (var channel in selectedChannels)
            {
                string[] keys = await TkLogCore.GetTotalKeysAsync(GetIndexKey(channel, normalizedBucketType)) ?? Array.Empty<string>();
                foreach (var key in keys)
                {
                    if (!TryParseKey(key, out var item)) continue;
                    if (!IsMatched(item, normalizedBucketType, startTime, endTime)) continue;

                    list.Add(item);
                }

                if (!ZlongForwardChannels.UsesLegacyKeys(channel)) continue;

                string[] legacyKeys = await TkLogCore.GetTotalKeysAsync(GetLegacyIndexKey(normalizedBucketType)) ?? Array.Empty<string>();
                foreach (var key in legacyKeys)
                {
                    if (!TryParseKey(key, out var item)) continue;
                    if (!IsMatched(item, normalizedBucketType, startTime, endTime)) continue;

                    list.Add(item);
                }
            }

            var totalTasks = list.Select(async item =>
            {
                var totalTask = TkLogCore.GetTotalAsync(item.redis_key);
                var exceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(item.redis_key, item.channel, item.bucket_type, item.bucket_value));
                await Task.WhenAll(totalTask, exceptionTask);

                item.total = totalTask.Result;
                item.exception_count = exceptionTask.Result;
                return item;
            });

            list = MergeItems(await Task.WhenAll(totalTasks))
                .Where(x => x.total > 0 || x.exception_count > 0)
                .ToList();

            list = list
                .OrderByDescending(x => x.report_time)
                .ThenBy(x => GetChannelSortOrder(x.channel))
                .ThenByDescending(x => x.bucket_value)
                .ToList();

            int count = list.Count;
            int skip = (page - 1) * size;
            int pageCount = count == 0 ? 0 : (int)Math.Ceiling(count / (double)size);

            return new ZlongForwardReportResult
            {
                list = list.Skip(skip).Take(size).ToList(),
                page = page,
                size = size,
                count = count,
                pageCount = pageCount,
                summary = await GetSummaryAsync(selectedChannels),
            };
        }

        public static async Task<ZlongForwardReportSummary> GetSummaryAsync(string channels)
        {
            return await GetSummaryAsync(NormalizeChannels(channels));
        }

        private static async Task<ZlongForwardReportSummary> GetSummaryAsync(string[] channels)
        {
            DateTime now = DateTime.Now;
            var hourTask = GetMetricTotalAsync(channels, "hour", now.ToString("yyyyMMddHH"));
            var dayTask = GetMetricTotalAsync(channels, "day", now.ToString("yyyyMMdd"));
            var monthTask = GetMetricTotalAsync(channels, "month", now.ToString("yyyyMM"));
            var yearTask = GetMetricTotalAsync(channels, "year", now.ToString("yyyy"));

            var hourExceptionTask = GetMetricTotalAsync(channels, "hour", now.ToString("yyyyMMddHH"), true);
            var dayExceptionTask = GetMetricTotalAsync(channels, "day", now.ToString("yyyyMMdd"), true);
            var monthExceptionTask = GetMetricTotalAsync(channels, "month", now.ToString("yyyyMM"), true);
            var yearExceptionTask = GetMetricTotalAsync(channels, "year", now.ToString("yyyy"), true);

            await Task.WhenAll(hourTask, dayTask, monthTask, yearTask, hourExceptionTask, dayExceptionTask, monthExceptionTask, yearExceptionTask);

            return new ZlongForwardReportSummary
            {
                hour = hourTask.Result,
                day = dayTask.Result,
                month = monthTask.Result,
                year = yearTask.Result,
                hour_exception = hourExceptionTask.Result,
                day_exception = dayExceptionTask.Result,
                month_exception = monthExceptionTask.Result,
                year_exception = yearExceptionTask.Result,
            };
        }

        private static async Task<int> GetMetricTotalAsync(string[] channels, string bucketType, string bucketValue, bool isException = false)
        {
            var tasks = new List<Task<int>>();

            foreach (var channel in channels)
            {
                string key = isException
                    ? GetChannelExceptionCountKey(channel, ExceptionAggregateType, bucketType, bucketValue)
                    : GetCountKey(channel, bucketType, bucketValue);

                tasks.Add(TkLogCore.GetTotalAsync(key));

                if (!ZlongForwardChannels.UsesLegacyKeys(channel)) continue;

                string legacyKey = isException
                    ? GetLegacyExceptionCountKey(ExceptionAggregateType, bucketType, bucketValue)
                    : GetLegacyCountKey(bucketType, bucketValue);

                tasks.Add(TkLogCore.GetTotalAsync(legacyKey));
            }

            await Task.WhenAll(tasks);
            return tasks.Sum(task => task.Result);
        }

        private static bool IsMatched(ZlongForwardReportItem item, string bucketType, DateTime? startTime, DateTime? endTime)
        {
            if (!string.Equals(item.bucket_type, bucketType, StringComparison.OrdinalIgnoreCase)) return false;
            if (startTime.HasValue && item.report_time < startTime.Value) return false;
            if (endTime.HasValue && item.report_time > endTime.Value) return false;

            return true;
        }

        private static List<ZlongForwardReportItem> MergeItems(IEnumerable<ZlongForwardReportItem> items)
        {
            return items
                .GroupBy(item => $"{item.channel}:{item.bucket_type}:{item.bucket_value}", StringComparer.OrdinalIgnoreCase)
                .Select(group =>
                {
                    var first = group.First();
                    return new ZlongForwardReportItem
                    {
                        channel = first.channel,
                        bucket_type = first.bucket_type,
                        bucket_value = first.bucket_value,
                        report_time = first.report_time,
                        display_time = first.display_time,
                        total = group.Sum(item => item.total),
                        exception_count = group.Sum(item => item.exception_count),
                        redis_key = $"{first.channel}:{first.bucket_type}:{first.bucket_value}",
                    };
                })
                .ToList();
        }

        private static string NormalizeBucketType(string bucketType)
        {
            return bucketType?.Trim().ToLowerInvariant() switch
            {
                "hour" => "hour",
                "month" => "month",
                "year" => "year",
                _ => "day",
            };
        }

        private static string[] NormalizeChannels(string channels)
        {
            return ZlongForwardChannels.NormalizeChannels(channels);
        }

        private static string NormalizeChannel(string channel)
        {
            return ZlongForwardChannels.NormalizeChannel(channel);
        }

        private static int GetChannelSortOrder(string channel)
        {
            return ZlongForwardChannels.GetSortOrder(channel);
        }

        private static string GetIndexKey(string channel, string bucketType)
        {
            return $"{RedisPrefix}:index:{channel}:{bucketType}";
        }

        private static string GetLegacyIndexKey(string bucketType)
        {
            return $"{RedisPrefix}:index:{bucketType}";
        }

        private static string GetCountKey(string channel, string bucketType, string bucketValue)
        {
            return $"{RedisPrefix}:{channel}:{bucketType}:{bucketValue}";
        }

        private static string GetLegacyCountKey(string bucketType, string bucketValue)
        {
            return $"{RedisPrefix}:{bucketType}:{bucketValue}";
        }

        private static string GetExceptionCountKey(string redisKey, string channel, string bucketType, string bucketValue)
        {
            return IsLegacyCountKey(redisKey)
                ? GetLegacyExceptionCountKey(ExceptionAggregateType, bucketType, bucketValue)
                : GetChannelExceptionCountKey(channel, ExceptionAggregateType, bucketType, bucketValue);
        }

        private static string GetChannelExceptionCountKey(string channel, string exceptionType, string bucketType, string bucketValue)
        {
            return $"{ExceptionRedisPrefix}:{channel}:{exceptionType}:{bucketType}:{bucketValue}";
        }

        private static string GetLegacyExceptionCountKey(string exceptionType, string bucketType, string bucketValue)
        {
            return $"{ExceptionRedisPrefix}:{exceptionType}:{bucketType}:{bucketValue}";
        }

        private static bool IsLegacyCountKey(string key)
        {
            var parts = key.Split(':', StringSplitOptions.RemoveEmptyEntries);
            return parts.Length == 4;
        }

        private static string GetExceptionIndexKey(string channel, string exceptionType, string bucketType)
        {
            return $"{ExceptionRedisPrefix}:index:{channel}:{exceptionType}:{bucketType}";
        }

        private static (string bucketType, string bucketValue, int expireSeconds)[] GetBucketMetrics(DateTime now)
        {
            return
            [
                ("hour", now.ToString("yyyyMMddHH"), 86400 * 90),
                ("day", now.ToString("yyyyMMdd"), 86400 * 366),
                ("month", now.ToString("yyyyMM"), 86400 * 3660),
                ("year", now.ToString("yyyy"), 86400 * 3660),
            ];
        }

        private static void SaveMetricCount(string key, string indexKey, int expireSeconds)
        {
            RedisHelper.IncrBy(key);
            RedisHelper.Expire(key, expireSeconds);
            RedisHelper.SAdd(indexKey, key);
            RedisHelper.Expire(indexKey, expireSeconds);
        }

        private static void SaveExceptionCount(string channel, string exceptionType)
        {
            DateTime now = DateTime.Now;
            var metrics = GetBucketMetrics(now);

            foreach (var metric in metrics)
            {
                string key = GetChannelExceptionCountKey(channel, exceptionType, metric.bucketType, metric.bucketValue);
                SaveMetricCount(key, GetExceptionIndexKey(channel, exceptionType, metric.bucketType), metric.expireSeconds);

                string aggregateKey = GetChannelExceptionCountKey(channel, ExceptionAggregateType, metric.bucketType, metric.bucketValue);
                SaveMetricCount(aggregateKey, GetExceptionIndexKey(channel, ExceptionAggregateType, metric.bucketType), metric.expireSeconds);
            }
        }

        private static bool TryParseKey(string key, out ZlongForwardReportItem item)
        {
            item = new ZlongForwardReportItem();
            if (string.IsNullOrWhiteSpace(key)) return false;

            var parts = key.Split(':', StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length < 4) return false;

            string channel = ZlongForwardChannels.LegacyChannel;
            string bucketType = parts[^2];
            string bucketValue = parts[^1];
            if (parts.Length >= 5)
            {
                channel = NormalizeChannel(parts[^3]);
                if (string.IsNullOrEmpty(channel)) return false;
            }

            if (!TryParseBucket(bucketType, bucketValue, out var reportTime, out var displayTime)) return false;

            item.channel = channel;
            item.bucket_type = bucketType;
            item.bucket_value = bucketValue;
            item.report_time = reportTime;
            item.display_time = displayTime;
            item.redis_key = key;
            return true;
        }

        private static bool TryParseBucket(string bucketType, string bucketValue, out DateTime reportTime, out string displayTime)
        {
            reportTime = DateTime.MinValue;
            displayTime = string.Empty;

            string format = bucketType switch
            {
                "hour" => "yyyyMMddHH",
                "day" => "yyyyMMdd",
                "month" => "yyyyMM",
                "year" => "yyyy",
                _ => string.Empty,
            };
            if (string.IsNullOrEmpty(format)) return false;

            bool success = DateTime.TryParseExact(
                bucketValue,
                format,
                CultureInfo.InvariantCulture,
                DateTimeStyles.None,
                out reportTime);

            if (!success) return false;

            displayTime = bucketType switch
            {
                "hour" => reportTime.ToString("yyyy-MM-dd HH:00"),
                "day" => reportTime.ToString("yyyy-MM-dd"),
                "month" => reportTime.ToString("yyyy-MM"),
                "year" => reportTime.ToString("yyyy"),
                _ => bucketValue,
            };
            return true;
        }
    }

    public class ZlongForwardReportSummary
    {
        public long hour { get; set; }
        public long day { get; set; }
        public long month { get; set; }
        public long year { get; set; }
        public long hour_exception { get; set; }
        public long day_exception { get; set; }
        public long month_exception { get; set; }
        public long year_exception { get; set; }
    }

    public class ZlongForwardReportItem
    {
        public string channel { get; set; } = string.Empty;
        public string bucket_type { get; set; } = string.Empty;
        public string bucket_value { get; set; } = string.Empty;
        public DateTime report_time { get; set; } = DateTime.MinValue;
        public string display_time { get; set; } = string.Empty;
        public long total { get; set; }
        public long exception_count { get; set; }
        public string redis_key { get; set; } = string.Empty;
    }

    public class ZlongForwardReportResult
    {
        public int page { get; set; }
        public int pageCount { get; set; }
        public int size { get; set; }
        public int count { get; set; }
        public List<ZlongForwardReportItem> list { get; set; } = [];
        public ZlongForwardReportSummary summary { get; set; } = new();
    }
}
