using System;
using System.Collections.Generic;
using System.Data;
using System.Net;
using System.Threading.Tasks;
using Dapper;
using CSRedis;
using dodohold.core;
using System.Text.Json;
using YunhuiKit;

namespace molilian.core
{
    public enum TrackType
    {
        Expose,
        Click
    }

    public partial class TracksCore
    {
        private const string RedisPrefix = ":tracks_v123";
        private const int DailyExpireSeconds = 40 * 86400;
        private const int HourlyExpireSeconds = 7 * 86400; // keep a week of hourly buckets
        private const string DefaultDimensionValue = "";
        private const int LinkCacheExpireSeconds = 30 * 86400;
        private const string LinkCacheVersion = "v2";
        private const string TrackRequestLogKey = ":tracks:request:logs";
        private static readonly HashSet<string> SupportEventTypes = new(StringComparer.OrdinalIgnoreCase) { "expose", "click" };


        /// <summary>
        /// 生成一个监测链接（仅返回 path，不包含域名），同时落地到 track_links
        /// </summary>
        public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, string typename, string scene, string uniqueId, string description = "", bool forceNew = false)
        {
            return CreateLinkAsync(eventType, string.Empty, typename, scene, uniqueId, description, forceNew);
        }

        /// <summary>
        /// 生成一个监测链接（仅返回 path，不包含域名），同时落地到 track_links
        /// </summary>
        public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, string platform, string typename, string scene, string uniqueId, string description = "", bool forceNew = false)
        {
            eventType = NormalizeEventType(eventType);
            platform = Normalize(platform);
            typename = Normalize(typename);
            scene = NormalizeOrAll(scene);
            uniqueId = NormalizeOrAll(uniqueId);
            description = Normalize(description);

            if (!SupportEventTypes.Contains(eventType))
            {
                return Task.FromResult<TrackLinkDTO>(null);
            }

            string cacheKey = GetLinkCacheKey(eventType, platform, typename, scene, uniqueId);
            if (!forceNew)
            {
                try
                {
                    int cachedId = RedisHelper.Get<int>(cacheKey);
                    if (cachedId > 0)
                    {
                        var cachedLink = RedisHelper.Get<TrackLinkDTO>(GetLinkIdCacheKey(cachedId));
                        if (cachedLink != null) return Task.FromResult(cachedLink);
                    }
                }
                catch
                {
                    // ignore cache errors
                }
            }

            try
            {
                using var conn = DBContext.GetOpenConnection();
                TrackLinkDTO exist = null;
                if (!forceNew)
                {
                    exist = new DBContext.Table(conn, "track_links")
                        .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND typename=@typename AND scene=@scene AND unique_id=@unique_id",
                            new { event_type = eventType, platform, typename, scene, unique_id = uniqueId });
                }

                if (exist != null)
                {
                    exist.description = description;
                    string path = BuildPath(exist.id, uniqueId);
                    if (string.IsNullOrEmpty(exist.path) || !exist.path.Contains("track_id"))
                    {
                        exist.path = path;
                        new DBContext.Table(conn, "track_links")
                            .Add("path", path)
                            .Add("platform", platform)
                            .Add("typename", typename)
                            .Add("description", description)
                            .Add("update_time", DateTime.Now)
                            .Where("id=@id", new { exist.id })
                            .Update();
                    }
                    _ = RedisHelper.Set(cacheKey, exist.id, LinkCacheExpireSeconds);
                    _ = RedisHelper.Set(GetLinkIdCacheKey(exist.id), exist, LinkCacheExpireSeconds);
                    return Task.FromResult(exist);
                }

                var item = new TrackLinkDTO
                {
                    event_type = eventType,
                    platform = platform,
                    typename = typename,
                    scene = scene,
                    unique_id = uniqueId,
                    path = string.Empty,
                    description = description,
                    create_time = DateTime.Now,
                    update_time = DateTime.Now
                };

                var id = conn.Insert(item);
                if (id != null && int.TryParse(id.ToString(), out var linkId))
                {
                    item.id = linkId;
                    item.path = BuildPath(linkId, uniqueId);
                    new DBContext.Table(conn, "track_links")
                        .Add("path", item.path)
                        .Add("description", description)
                        .Add("update_time", DateTime.Now)
                        .Where("id=@id", new { item.id })
                        .Update();
                }
                _ = RedisHelper.Set(cacheKey, item.id, LinkCacheExpireSeconds);
                _ = RedisHelper.Set(GetLinkIdCacheKey(item.id), item, LinkCacheExpireSeconds);
                return Task.FromResult(item);
            }
            catch (Exception ex)
            {
                _ = new LoggerLibrary("TracksCore", "CreateLink")
                    .Info(ex.Message, ex.StackTrace)
                    .SaveAsync();
                return Task.FromResult<TrackLinkDTO>(null);
            }
        }

        /// <summary>
        /// 曝光/点击触发：计入 Redis，失败不影响返回
        /// </summary>
        public static Task<bool> TrackAsync(TrackLinkDTO link, string scene = "", int accountId = 0)
        {

            string dateStr = DateTime.Now.ToString("yyyyMMdd");
            string hourStr = DateTime.Now.ToString("yyyyMMddHH");
            string metricScene = ResolveMetricScene(link, scene);
            accountId = Math.Max(accountId, 0);
            string indexValue = BuildMetricIndexValue(link.event_type, link.id, metricScene);

            var metrics = new List<(string key, string indexKey, string indexValue, int expire)>
            {
                (BuildDailyCountKey(link.event_type, link.id, dateStr, metricScene), $"{RedisPrefix}:daily:index:{dateStr}:track", indexValue, DailyExpireSeconds),
                (BuildHourlyCountKey(link.event_type, link.id, hourStr, metricScene), $"{RedisPrefix}:hour:index:{hourStr}:track", indexValue, HourlyExpireSeconds),
            };
            if (accountId > 0)
            {
                string accountIndexValue = BuildMetricIndexValue(link.event_type, link.id, metricScene, accountId);
                metrics.Add((BuildDailyCountKey(link.event_type, link.id, dateStr, metricScene, accountId), $"{RedisPrefix}:daily:index:{dateStr}:track", accountIndexValue, DailyExpireSeconds));
                metrics.Add((BuildHourlyCountKey(link.event_type, link.id, hourStr, metricScene, accountId), $"{RedisPrefix}:hour:index:{hourStr}:track", accountIndexValue, HourlyExpireSeconds));
            }

            try
            {
                foreach (var metric in metrics)
                {
                    RedisHelper.IncrBy(metric.key);
                    RedisHelper.Expire(metric.key, metric.expire);

                    RedisHelper.SAdd(metric.indexKey, metric.indexValue);
                    RedisHelper.Expire(metric.indexKey, metric.expire);
                }
                return Task.FromResult(true);
            }
            catch (Exception ex)
            {
                _ = new LoggerLibrary("TracksCore", "TrackAsync")
                    .Info(ex.Message, ex.StackTrace)
                    .SaveAsync();
                return Task.FromResult(false);
            }
        }

        /// <summary>
        /// 日报：拉取指定日期 Redis 计数并落地到 track_daily_report，可高频重复执行。
        /// </summary>
        public static async Task<int> FlushDailyAsync(DateTime targetDate)
        {
            string dateStr = targetDate.ToString("yyyyMMdd");
            string indexKey = $"{RedisPrefix}:daily:index:{dateStr}:track";

            var indexMembers = await RedisHelper.SMembersAsync<string>(indexKey) ?? [];
            if (indexMembers == null || indexMembers.Length == 0) return 0;

            var buckets = new Dictionary<string, DailyReportBucket>();
            using var conn = DBContext.GetOpenConnection();
            var linkCache = new Dictionary<int, TrackLinkDTO>();
            foreach (var member in indexMembers)
            {
                var parts = member.Split('|');
                if (parts.Length < 2 || parts.Length > 4) continue;

                string eventType = NormalizeEventType(parts[0]);
                if (!SupportEventTypes.Contains(eventType)) continue;
                if (!int.TryParse(parts[1], out var trackId) || trackId <= 0) continue;
                bool hasScenePart = parts.Length >= 3;
                string memberScene = hasScenePart ? DecodeIndexPart(parts[2]) : string.Empty;
                int accountId = 0;
                if (parts.Length == 4 && (!int.TryParse(parts[3], out accountId) || accountId <= 0)) continue;

                string countKey = BuildDailyCountKey(eventType, trackId, dateStr, hasScenePart ? memberScene : string.Empty, accountId);
                int total = await RedisHelper.GetAsync<int>(countKey);
                if (total <= 0) continue;

                if (!linkCache.TryGetValue(trackId, out var link))
                {
                    link = new DBContext.Table(conn, "track_links")
                        .Get<TrackLinkDTO>("id=@id", new { id = trackId });
                    if (link != null) linkCache[trackId] = link;
                }

                if (link != null)
                {
                    string linkEventType = NormalizeEventType(link.event_type);
                    if (!SupportEventTypes.Contains(linkEventType)) continue;
                    if (!string.Equals(eventType, linkEventType, StringComparison.OrdinalIgnoreCase))
                    {
                        _ = new LoggerLibrary("TracksCore", "FlushDaily")
                            .Info($"skip stale track bucket: member={member}, link_event_type={linkEventType}", string.Empty)
                            .SaveAsync();
                        continue;
                    }
                    eventType = linkEventType;
                }

                string reportScene = ResolveMetricScene(link, memberScene);
                string bucketKey = BuildMetricIndexValue(eventType, trackId, reportScene, accountId);
                if (!buckets.TryGetValue(bucketKey, out var bucket))
                {
                    bucket = new DailyReportBucket
                    {
                        EventType = eventType,
                        TrackId = trackId,
                        Scene = reportScene,
                        AccountId = accountId,
                        Link = link
                    };
                    buckets[bucketKey] = bucket;
                }
                bucket.Total += total;
            }

            int rows = 0;
            foreach (var bucket in buckets.Values)
            {
                await UpsertDailyReportAsync(conn, targetDate.Date, bucket.EventType, bucket.TrackId, bucket.Scene, bucket.AccountId, bucket.Link, bucket.Total);
                rows++;
            }
            return rows;
        }

        private static Task<int> UpsertDailyReportAsync(IDbConnection conn, DateTime reportDate, string eventType, int trackId, string scene, int accountId, TrackLinkDTO? link, int total)
        {
            const string sql = @"
INSERT INTO track_daily_report
    (report_date, event_type, track_link_id, account_id, platform, typename, scene, unique_id, event_count, create_time, update_time)
VALUES
    (@reportDate, @eventType, @trackId, @accountId, @platform, @typename, @scene, @uniqueId, @eventCount, @now, @now)
ON DUPLICATE KEY UPDATE
    event_count = VALUES(event_count),
    account_id = VALUES(account_id),
    platform = VALUES(platform),
    typename = VALUES(typename),
    scene = VALUES(scene),
    unique_id = VALUES(unique_id),
    update_time = VALUES(update_time);";

            return conn.ExecuteAsync(sql, new
            {
                reportDate,
                eventType,
                trackId,
                accountId,
                platform = link?.platform ?? string.Empty,
                typename = link?.typename ?? string.Empty,
                scene,
                uniqueId = link?.unique_id ?? string.Empty,
                eventCount = total,
                now = DateTime.Now
            });
        }

        public static async Task<List<TrackHourlyReportDTO>> GetHourlyReportAsync(int trackId, DateTime targetDate, string scene = "", int accountId = 0)
        {
            var result = new List<TrackHourlyReportDTO>();
            if (trackId <= 0) return result;

            var link = await GetLinkByIdAsync(trackId);
            if (link == null) return result;

            string eventType = NormalizeEventType(link.event_type);
            if (!SupportEventTypes.Contains(eventType)) return result;

            string metricScene = ResolveMetricScene(link, scene);
            string linkScene = ResolveMetricScene(link, string.Empty);
            string dateStr = targetDate.ToString("yyyyMMdd");
            accountId = Math.Max(accountId, 0);
            for (int hour = 0; hour < 24; hour++)
            {
                string hourStr = $"{dateStr}{hour:00}";
                int total = await RedisHelper.GetAsync<int>(BuildHourlyCountKey(eventType, trackId, hourStr, metricScene, accountId));
                if (accountId == 0 && !string.IsNullOrEmpty(metricScene) && metricScene == linkScene)
                {
                    total += await RedisHelper.GetAsync<int>(BuildHourlyCountKey(eventType, trackId, hourStr, string.Empty));
                }
                int callCount = await GetTrackHourlyCallCountAsync(link, hourStr, accountId);

                result.Add(new TrackHourlyReportDTO
                {
                    track_link_id = trackId,
                    event_type = eventType,
                    platform = link.platform ?? string.Empty,
                    typename = link.typename ?? string.Empty,
                    scene = metricScene,
                    unique_id = link.unique_id ?? string.Empty,
                    account_id = accountId,
                    report_date = targetDate.Date,
                    hour = hour,
                    event_count = total,
                    call_count = callCount
                });
            }

            return result;
        }

        private static async Task<int> GetTrackHourlyCallCountAsync(TrackLinkDTO link, string hourStr, int accountId)
        {
            if (accountId <= 0) return 0;

            string channelName = GetTrackCallChannelName(link);
            if (string.IsNullOrEmpty(channelName)) return 0;

            return await TkLogCore.GetTotalAsync($":parse_total:{channelName}_{accountId}:{hourStr}");
        }

        private static string GetTrackCallChannelName(TrackLinkDTO link)
        {
            string platform = Normalize(link.platform).ToLowerInvariant();

            if (link.id is 19 or 20 || platform == "jd" || platform == "13" || platform.Contains("京东"))
            {
                return "jd";
            }

            if (link.id is 21 or 22 || platform == "pdd" || platform == "9" || platform.Contains("拼多多"))
            {
                return "pdd";
            }

            return "tb";
        }

        public static string BuildPath(TrackType type, TkDataDTO result)
        {
            int trackId = type == TrackType.Click ? 1 : 2;
            if (result == null) return string.Empty;
            string unique_id = $"1|{result.itemId}_{result.mktId}";
            return BuildPath(trackId, unique_id, GetTrackScene(result.parse_type, result.riskStrategy), result.accountId);
        }
        public static string BuildPath(TrackType type, JdDataDTO result)
        {
            int trackId = type == TrackType.Click ? 19 : 20;
            if (result == null) return string.Empty;
            string unique_id = $"13|{result.shortLinkurl.UrlEncode()}";
            return BuildPath(trackId, unique_id, GetTrackScene(result.parse_type, result.riskStrategy), result.accountId);
        }
        public static string BuildPath(TrackType type, PddDataDTO result)
        {
            int trackId = type == TrackType.Click ? 21 : 22;
            if (result == null) return string.Empty;
            string unique_id = $"9|{result.shortLinkurl.UrlEncode()}";
            return BuildPath(trackId, unique_id, GetTrackScene(result.parse_type, result.riskStrategy), result.accountId);
        }

        public static string BuildBrwSimilarPath(TrackType type, TkDataDTO result, PromotionQueryItemDTO similar_goods = null, int index = 0)
        {
            int trackId = type == TrackType.Click ? 23 : 24;
            if (result == null) return string.Empty;
            string unique_id = $"1|{result.itemId}_{result.mktId}";
            string scene = string.Empty;

            if (similar_goods != null)
            {
                unique_id = $"1|{result.itemId}_{result.mktId}_{index}";
                scene = "similar";
            }

            return BuildPath(trackId, unique_id, scene, result.accountId);
        }
        public static string BuildPath(int trackId, string unique_id = "", string scene = "", int accountId = 0)
        {
            scene = NormalizeReportScene(scene);
            string url = $"https://api.molilian.com/tracks/track?track_id={trackId}&unique_id={unique_id}";
            if (!string.IsNullOrEmpty(scene)) url += $"&scene={scene.UrlEncode()}";
            if (accountId > 0) url += $"&account_id={accountId}";
            return url;
        }

        public static string ResolveMetricScene(TrackLinkDTO? link, string scene = "")
        {
            scene = NormalizeReportScene(scene);
            if (!string.IsNullOrEmpty(scene)) return scene;
            return NormalizeOrAll(link?.scene ?? string.Empty);
        }

        public static string GetLinkCacheKey(string eventType, string typename, string scene, string uniqueId)
        {
            return GetLinkCacheKey(eventType, string.Empty, typename, scene, uniqueId);
        }

        public static string GetLinkCacheKey(string eventType, string platform, string typename, string scene, string uniqueId)
        {
            eventType = NormalizeEventType(eventType);
            platform = Normalize(platform);
            typename = Normalize(typename);
            scene = NormalizeOrAll(scene);
            uniqueId = NormalizeOrAll(uniqueId);
            return $"{RedisPrefix}:cache:{LinkCacheVersion}:link:{eventType}:{platform}:{typename}:{scene}:{uniqueId}";
        }

        public static string GetLinkIdCacheKey(int trackId)
        {
            return $"{RedisPrefix}:cache:{LinkCacheVersion}:linkid:{trackId}";
        }

        public static async Task<TrackLinkDTO> GetLinkByIdAsync(int trackId)
        {
            if (trackId <= 0) return null;
            string cacheKey = GetLinkIdCacheKey(trackId);
            try
            {
                var cached = RedisHelper.Get<TrackLinkDTO>(cacheKey);
                if (cached != null) return cached;
            }
            catch { }

            try
            {
                var item = new DBContext.Table("track_links").Get<TrackLinkDTO>("id=@id", new { id = trackId });
                if (item != null)
                {
                    _ = RedisHelper.Set(cacheKey, item, LinkCacheExpireSeconds);
                }
                return item;
            }
            catch
            {
                return null;
            }
        }

        public static Task<bool> LogTrackRequestAsync(TrackRequestLogDTO dto)
        {
            try
            {
                dto.event_type = NormalizeEventType(dto.event_type);
                dto.platform = Normalize(dto.platform);
                dto.typename = Normalize(dto.typename);
                dto.scene = NormalizeOrAll(dto.scene);
                dto.unique_id = NormalizeOrAll(dto.unique_id);
                dto.ip = Normalize(dto.ip);
                dto.user_agent = Normalize(dto.user_agent);
                dto.referer = Normalize(dto.referer);
                dto.create_time = DateTime.Now;

                RedisHelper.RPush(TrackRequestLogKey, dto);
                return Task.FromResult(true);
            }
            catch
            {
                return Task.FromResult(false);
            }
        }

        public static async Task<int> InsertTrackRequestLogAsync(int limit, YunhuiKit.RedisClient redis)
        {
            int count = 0;
            try
            {
                using var conn = DBContext.GetOpenConnection();
                for (int i = 0; i < limit; i++)
                {
                    var entity = await redis.LPopAsync<TrackRequestLogDTO>(TrackRequestLogKey);
                    if (entity == null) break;
                    try
                    {
                        if (entity.create_time == default) entity.create_time = DateTime.Now;
                        conn.Insert(entity);
                        count++;
                    }
                    catch
                    {
                        // ignore malformed item
                    }
                }
            }
            catch
            {
                return count;
            }
            return count;
        }

        private static string Normalize(string value)
        {
            return (value ?? string.Empty).Trim();
        }

        private static string NormalizeReportScene(string value)
        {
            var scene = Normalize(value);
            return scene == "默认场景" ? string.Empty : scene;
        }

        private static string NormalizeOrAll(string value)
        {
            var result = Normalize(value);
            return string.IsNullOrEmpty(result) ? DefaultDimensionValue : result;
        }

        private static string NormalizeEventType(string value)
        {
            return Normalize(value).ToLowerInvariant();
        }

        private static string GetTrackScene(string parseType, string riskStrategy)
        {
            parseType = NormalizeReportScene(parseType);
            if (!string.IsNullOrEmpty(parseType)) return parseType;

            riskStrategy = NormalizeReportScene(riskStrategy);
            if (!string.IsNullOrEmpty(riskStrategy)) return riskStrategy;

            return DefaultDimensionValue;
        }

        private static string BuildDailyCountKey(string eventType, int trackId, string dateStr, string scene, int accountId = 0)
        {
            eventType = NormalizeEventType(eventType);
            scene = NormalizeReportScene(scene);
            string key = $"{RedisPrefix}:daily:{eventType}:{trackId}:{dateStr}";
            if (!string.IsNullOrEmpty(scene)) key += $":{EncodeIndexPart(scene)}";
            if (accountId > 0) key += $":account:{accountId}";
            return key;
        }

        private static string BuildHourlyCountKey(string eventType, int trackId, string hourStr, string scene, int accountId = 0)
        {
            eventType = NormalizeEventType(eventType);
            scene = NormalizeReportScene(scene);
            string key = $"{RedisPrefix}:hour:{eventType}:{trackId}:{hourStr}";
            if (!string.IsNullOrEmpty(scene)) key += $":{EncodeIndexPart(scene)}";
            if (accountId > 0) key += $":account:{accountId}";
            return key;
        }

        private static string BuildMetricIndexValue(string eventType, int trackId, string scene, int accountId = 0)
        {
            eventType = NormalizeEventType(eventType);
            scene = NormalizeReportScene(scene);
            if (accountId > 0) return $"{eventType}|{trackId}|{EncodeIndexPart(scene)}|{accountId}";
            if (string.IsNullOrEmpty(scene)) return $"{eventType}|{trackId}";
            return $"{eventType}|{trackId}|{EncodeIndexPart(scene)}";
        }

        private static string EncodeIndexPart(string value)
        {
            return Normalize(value).UrlEncode();
        }

        private static string DecodeIndexPart(string value)
        {
            try
            {
                return Normalize(value).UrlDecode();
            }
            catch
            {
                return Normalize(value);
            }
        }

        private sealed class DailyReportBucket
        {
            public string EventType { get; set; } = string.Empty;
            public int TrackId { get; set; }
            public string Scene { get; set; } = string.Empty;
            public int AccountId { get; set; }
            public TrackLinkDTO? Link { get; set; }
            public int Total { get; set; }
        }

        private static int GetTrackLinkId(IDbConnection conn, string eventType, string platform, string scene, string uniqueId)
        {
            try
            {
                var record = new DBContext.Table(conn, "track_links")
                    .Fields("id")
                    .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
                        new { event_type = eventType, platform, scene, unique_id = uniqueId });
                if (record == null) return 0;
                return record.id;
            }
            catch
            {
                return 0;
            }
        }
    }

}
