using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TakeoutSaaS.Application.App.Members.MessageReach.Dto;
using TakeoutSaaS.Application.App.Members.MessageReach.Options;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Domain.Membership.Entities;
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Domain.Membership.Repositories;
using TakeoutSaaS.Module.Sms.Abstractions;
using TakeoutSaaS.Module.Sms.Models;
using TakeoutSaaS.Module.Sms.Options;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;

namespace TakeoutSaaS.Application.App.Members.MessageReach.Services;

/// <summary>
/// 会员消息触达应用服务实现。
/// </summary>
public sealed class MemberMessageReachAppService(
    IMemberMessageReachRepository memberMessageReachRepository,
    IMemberRepository memberRepository,
    IMiniUserRepository miniUserRepository,
    ISmsSenderResolver smsSenderResolver,
    IOptionsMonitor<SmsOptions> smsOptionsMonitor,
    IOptionsMonitor<MemberMessagingOptions> memberMessagingOptionsMonitor,
    IMemberMessageWeChatSender memberMessageWeChatSender,
    ILogger<MemberMessageReachAppService> logger)
    : IMemberMessageReachAppService
{
    private static readonly IReadOnlyDictionary<string, string> AudienceTagAliasMap = BuildAudienceTagAliasMap();
    private static readonly IReadOnlyDictionary<string, string> AudienceTagDisplayMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        ["highfrequency"] = "高频客户",
        ["newcustomer"] = "新客",
        ["dormant"] = "沉睡客户",
        ["lost"] = "流失客户",
        ["lunchregular"] = "午餐常客",
        ["highspend"] = "大额消费"
    };

    private static readonly IReadOnlySet<string> EmptyTagSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

    /// <inheritdoc />
    public async Task<MemberMessageReachStatsDto> GetStatsAsync(long tenantId, CancellationToken cancellationToken = default)
    {
        // 1. 校验租户上下文。
        EnsureTenantId(tenantId);

        // 2. 读取当月统计快照并计算转化率。
        var now = DateTime.UtcNow;
        var monthStart = new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc);
        var monthEnd = monthStart.AddMonths(1);
        var snapshot = await memberMessageReachRepository.GetMonthlyStatsAsync(tenantId, monthStart, monthEnd, cancellationToken);
        var openRate = MemberMessageReachMapping.ResolveRatePercent(snapshot.ReadRecipientCount, snapshot.SentRecipientCount);
        var conversionRate = MemberMessageReachMapping.ResolveRatePercent(snapshot.ConvertedRecipientCount, snapshot.SentRecipientCount);

        // 3. 返回页面统计 DTO。
        return new MemberMessageReachStatsDto
        {
            MonthlySentCount = snapshot.SentMessageCount,
            ReachMemberCount = snapshot.ReachMemberCount,
            OpenRate = openRate,
            ConversionRate = conversionRate
        };
    }

    /// <inheritdoc />
    public async Task<MemberMessageReachListResultDto> SearchMessagesAsync(
        long tenantId,
        SearchMemberMessageInput input,
        CancellationToken cancellationToken = default)
    {
        // 1. 校验租户与查询参数。
        EnsureTenantId(tenantId);
        var page = input.Page <= 0 ? 1 : input.Page;
        var pageSize = Math.Clamp(input.PageSize, 1, 100);
        var status = MemberMessageReachMapping.TryParseStatus(input.Status);
        var channel = MemberMessageReachMapping.TryParseChannel(input.Channel);

        // 2. 调用仓储分页查询。
        var (items, total) = await memberMessageReachRepository.SearchMessagesAsync(
            tenantId,
            status,
            channel,
            input.Keyword,
            page,
            pageSize,
            cancellationToken);

        // 3. 映射分页结果。
        return new MemberMessageReachListResultDto
        {
            Items = items.Select(ToMessageListItem).ToList(),
            TotalCount = total,
            Page = page,
            PageSize = pageSize
        };
    }

    /// <inheritdoc />
    public async Task<MemberMessageReachDetailDto?> GetMessageDetailAsync(
        long tenantId,
        long messageId,
        CancellationToken cancellationToken = default)
    {
        // 1. 查询主消息记录。
        EnsureTenantId(tenantId);
        var message = await memberMessageReachRepository.FindMessageByIdAsync(tenantId, messageId, cancellationToken);
        if (message is null)
        {
            return null;
        }

        // 2. 查询并映射收件明细。
        var recipients = await memberMessageReachRepository.GetRecipientsAsync(tenantId, messageId, cancellationToken);
        var channels = MemberMessageReachMapping.DeserializeStringArray(message.ChannelsJson);
        var audienceTags = MemberMessageReachMapping.DeserializeStringArray(message.AudienceTagsJson);

        // 3. 返回详情数据。
        return new MemberMessageReachDetailDto
        {
            MessageId = message.Id,
            TemplateId = message.TemplateId,
            Title = message.Title,
            Content = message.Content,
            Channels = channels,
            AudienceType = MemberMessageReachMapping.ToAudienceTypeText(message.AudienceType),
            AudienceTags = audienceTags,
            AudienceText = BuildAudienceText(message.AudienceType, audienceTags, message.EstimatedReachCount),
            EstimatedReachCount = message.EstimatedReachCount,
            ScheduleType = MemberMessageReachMapping.ToScheduleTypeText(message.ScheduleType),
            ScheduledAt = message.ScheduledAt,
            Status = MemberMessageReachMapping.ToStatusText(message.Status),
            SentAt = message.SentAt,
            SentCount = message.SentCount,
            ReadCount = message.ReadCount,
            ConvertedCount = message.ConvertedCount,
            OpenRate = MemberMessageReachMapping.ResolveRatePercent(message.ReadCount, message.SentCount),
            ConversionRate = MemberMessageReachMapping.ResolveRatePercent(message.ConvertedCount, message.SentCount),
            LastError = message.LastError,
            Recipients = recipients.Select(MemberMessageReachMapping.ToRecipientDto).ToList()
        };
    }

    /// <inheritdoc />
    public async Task<MemberMessageDispatchMetaDto?> GetDispatchMetaAsync(
        long tenantId,
        long messageId,
        CancellationToken cancellationToken = default)
    {
        // 1. 查询消息并返回调度元数据。
        EnsureTenantId(tenantId);
        var message = await memberMessageReachRepository.FindMessageByIdAsync(tenantId, messageId, cancellationToken);
        if (message is null)
        {
            return null;
        }

        return ToDispatchMeta(message);
    }

    /// <inheritdoc />
    public async Task<MemberMessageDispatchMetaDto> SaveMessageAsync(
        long tenantId,
        SaveMemberMessageInput input,
        CancellationToken cancellationToken = default)
    {
        // 1. 入参校验与基础归一化。
        EnsureTenantId(tenantId);
        var submitAction = NormalizeSubmitAction(input.SubmitAction);
        var title = NormalizeRequiredText(input.Title, 128, nameof(input.Title));
        var content = NormalizeRequiredText(input.Content, 4096, nameof(input.Content));
        var channels = MemberMessageReachMapping.NormalizeChannels(input.Channels);
        var audienceType = MemberMessageReachMapping.ParseAudienceType(input.AudienceType);
        var audienceTags = MemberMessageReachMapping.NormalizeTags(input.AudienceTags);
        var scheduleType = MemberMessageReachMapping.ParseScheduleType(input.ScheduleType);
        var scheduledAt = NormalizeScheduledAt(scheduleType, submitAction, input.ScheduledAt);
        if (audienceType == MemberMessageAudienceType.Tags && audienceTags.Count == 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "按标签筛选时至少选择一个标签");
        }

        // 2. 估算目标人群并读取/创建消息实体。
        var profiles = await ResolveAudienceProfilesAsync(tenantId, audienceType, audienceTags, cancellationToken);
        var estimatedReachCount = profiles.Count;
        var isNew = !input.MessageId.HasValue;
        MemberReachMessage message;
        if (isNew)
        {
            message = new MemberReachMessage
            {
                TenantId = tenantId
            };
            await memberMessageReachRepository.AddMessageAsync(message, cancellationToken);
        }
        else
        {
            message = await memberMessageReachRepository.FindMessageByIdAsync(tenantId, input.MessageId!.Value, cancellationToken)
                ?? throw new BusinessException(ErrorCodes.NotFound, "消息不存在");
            EnsureMessageEditable(message);
        }

        // 3. 覆盖消息字段并重置发送态字段。
        message.StoreId = input.StoreId;
        message.TemplateId = input.TemplateId;
        message.Title = title;
        message.Content = content;
        message.ChannelsJson = MemberMessageReachMapping.SerializeStringArray(channels);
        message.AudienceType = audienceType;
        message.AudienceTagsJson = MemberMessageReachMapping.SerializeStringArray(audienceTags);
        message.EstimatedReachCount = estimatedReachCount;
        message.ScheduleType = scheduleType;
        message.ScheduledAt = scheduleType == MemberMessageScheduleType.Scheduled ? scheduledAt : null;
        message.Status = submitAction == "send" ? MemberMessageStatus.Pending : MemberMessageStatus.Draft;
        message.HangfireJobId = null;
        message.SentAt = null;
        message.SentCount = 0;
        message.ReadCount = 0;
        message.ConvertedCount = 0;
        message.LastError = null;

        // 4. 编辑场景清理旧收件记录，确保再次发送时数据一致。
        if (!isNew)
        {
            await memberMessageReachRepository.RemoveRecipientsAsync(tenantId, message.Id, cancellationToken);
        }

        // 5. 持久化并返回调度信息。
        await memberMessageReachRepository.SaveChangesAsync(cancellationToken);
        return ToDispatchMeta(message);
    }

    /// <inheritdoc />
    public async Task BindDispatchJobAsync(
        long tenantId,
        long messageId,
        string? hangfireJobId,
        CancellationToken cancellationToken = default)
    {
        // 1. 查询并绑定任务 ID。
        EnsureTenantId(tenantId);
        var message = await memberMessageReachRepository.FindMessageByIdAsync(tenantId, messageId, cancellationToken)
            ?? throw new BusinessException(ErrorCodes.NotFound, "消息不存在");
        message.HangfireJobId = string.IsNullOrWhiteSpace(hangfireJobId)
            ? null
            : Truncate(hangfireJobId.Trim(), 64);

        // 2. 保存更新结果。
        await memberMessageReachRepository.UpdateMessageAsync(message, cancellationToken);
        await memberMessageReachRepository.SaveChangesAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<string?> DeleteMessageAsync(
        long tenantId,
        long messageId,
        CancellationToken cancellationToken = default)
    {
        // 1. 查询待删除消息并校验状态。
        EnsureTenantId(tenantId);
        var message = await memberMessageReachRepository.FindMessageByIdAsync(tenantId, messageId, cancellationToken)
            ?? throw new BusinessException(ErrorCodes.NotFound, "消息不存在");
        if (message.Status == MemberMessageStatus.Sending)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "消息发送中，暂不允许删除");
        }

        // 2. 先删收件明细，再删主记录。
        var oldHangfireJobId = message.HangfireJobId;
        await memberMessageReachRepository.RemoveRecipientsAsync(tenantId, messageId, cancellationToken);
        await memberMessageReachRepository.DeleteMessageAsync(message, cancellationToken);

        // 3. 持久化删除。
        await memberMessageReachRepository.SaveChangesAsync(cancellationToken);
        return oldHangfireJobId;
    }

    /// <inheritdoc />
    public async Task<MemberMessageAudienceEstimateDto> EstimateAudienceAsync(
        long tenantId,
        MemberMessageAudienceEstimateInput input,
        CancellationToken cancellationToken = default)
    {
        // 1. 解析目标规则。
        EnsureTenantId(tenantId);
        var audienceType = MemberMessageReachMapping.ParseAudienceType(input.AudienceType);
        var tags = MemberMessageReachMapping.NormalizeTags(input.Tags);
        if (audienceType == MemberMessageAudienceType.Tags && tags.Count == 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "按标签筛选时至少选择一个标签");
        }

        // 2. 计算可触达人数。
        var profiles = await ResolveAudienceProfilesAsync(tenantId, audienceType, tags, cancellationToken);
        return new MemberMessageAudienceEstimateDto
        {
            ReachCount = profiles.Count
        };
    }

    /// <inheritdoc />
    public async Task<MemberMessageTemplateListResultDto> SearchTemplatesAsync(
        long tenantId,
        SearchMemberMessageTemplateInput input,
        CancellationToken cancellationToken = default)
    {
        // 1. 归一化分页参数。
        EnsureTenantId(tenantId);
        var page = input.Page <= 0 ? 1 : input.Page;
        var pageSize = Math.Clamp(input.PageSize, 1, 100);
        var category = MemberMessageReachMapping.TryParseTemplateCategory(input.Category);

        // 2. 分页查询模板。
        var (items, total) = await memberMessageReachRepository.SearchTemplatesAsync(
            tenantId,
            category,
            input.Keyword,
            page,
            pageSize,
            cancellationToken);

        // 3. 映射并返回。
        return new MemberMessageTemplateListResultDto
        {
            Items = items.Select(MemberMessageReachMapping.ToTemplateDto).ToList(),
            TotalCount = total,
            Page = page,
            PageSize = pageSize
        };
    }

    /// <inheritdoc />
    public async Task<MemberMessageTemplateDto?> GetTemplateAsync(
        long tenantId,
        long templateId,
        CancellationToken cancellationToken = default)
    {
        // 1. 查询模板详情。
        EnsureTenantId(tenantId);
        var template = await memberMessageReachRepository.FindTemplateByIdAsync(tenantId, templateId, cancellationToken);
        return template is null ? null : MemberMessageReachMapping.ToTemplateDto(template);
    }

    /// <inheritdoc />
    public async Task<MemberMessageTemplateDto> SaveTemplateAsync(
        long tenantId,
        SaveMemberMessageTemplateInput input,
        CancellationToken cancellationToken = default)
    {
        // 1. 校验并归一化模板入参。
        EnsureTenantId(tenantId);
        var name = NormalizeRequiredText(input.Name, 64, nameof(input.Name));
        var content = NormalizeRequiredText(input.Content, 4096, nameof(input.Content));
        var category = MemberMessageReachMapping.ParseTemplateCategory(input.Category);

        // 2. 校验同租户模板名称唯一。
        var existingTemplateByName = await memberMessageReachRepository.FindTemplateByNameAsync(tenantId, name, cancellationToken);
        if (existingTemplateByName is not null && existingTemplateByName.Id != input.TemplateId.GetValueOrDefault())
        {
            throw new BusinessException(ErrorCodes.BadRequest, "模板名称已存在");
        }

        // 3. 查询或创建模板实体。
        var isNew = !input.TemplateId.HasValue;
        MemberMessageTemplate template;
        if (isNew)
        {
            template = new MemberMessageTemplate
            {
                TenantId = tenantId,
                UsageCount = 0
            };
            await memberMessageReachRepository.AddTemplateAsync(template, cancellationToken);
        }
        else
        {
            template = await memberMessageReachRepository.FindTemplateByIdAsync(tenantId, input.TemplateId!.Value, cancellationToken)
                ?? throw new BusinessException(ErrorCodes.NotFound, "模板不存在");
            await memberMessageReachRepository.UpdateTemplateAsync(template, cancellationToken);
        }

        // 4. 赋值并保存模板。
        template.Name = name;
        template.Content = content;
        template.Category = category;
        await memberMessageReachRepository.SaveChangesAsync(cancellationToken);
        return MemberMessageReachMapping.ToTemplateDto(template);
    }

    /// <inheritdoc />
    public async Task DeleteTemplateAsync(
        long tenantId,
        long templateId,
        CancellationToken cancellationToken = default)
    {
        // 1. 查询模板并执行删除。
        EnsureTenantId(tenantId);
        var template = await memberMessageReachRepository.FindTemplateByIdAsync(tenantId, templateId, cancellationToken)
            ?? throw new BusinessException(ErrorCodes.NotFound, "模板不存在");
        await memberMessageReachRepository.DeleteTemplateAsync(template, cancellationToken);

        // 2. 持久化删除。
        await memberMessageReachRepository.SaveChangesAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task ExecuteDispatchAsync(
        long tenantId,
        long messageId,
        CancellationToken cancellationToken = default)
    {
        // 1. 查询消息并校验状态。
        EnsureTenantId(tenantId);
        var message = await memberMessageReachRepository.FindMessageByIdAsync(tenantId, messageId, cancellationToken);
        if (message is null)
        {
            logger.LogWarning("消息发送任务未找到消息记录，TenantId={TenantId} MessageId={MessageId}", tenantId, messageId);
            return;
        }

        if (message.Status == MemberMessageStatus.Sending || message.Status == MemberMessageStatus.Sent)
        {
            logger.LogInformation(
                "消息发送任务跳过，状态无需发送，TenantId={TenantId} MessageId={MessageId} Status={Status}",
                tenantId,
                messageId,
                message.Status);
            return;
        }

        if (message.Status != MemberMessageStatus.Pending)
        {
            logger.LogWarning(
                "消息发送任务跳过，状态非待发送，TenantId={TenantId} MessageId={MessageId} Status={Status}",
                tenantId,
                messageId,
                message.Status);
            return;
        }

        // 2. 将消息状态推进为发送中并清理旧任务标识。
        message.Status = MemberMessageStatus.Sending;
        message.HangfireJobId = null;
        message.LastError = null;
        await memberMessageReachRepository.UpdateMessageAsync(message, cancellationToken);
        await memberMessageReachRepository.SaveChangesAsync(cancellationToken);

        try
        {
            // 3. 解析目标人群与渠道配置。
            var audienceTags = MemberMessageReachMapping.DeserializeStringArray(message.AudienceTagsJson);
            var audienceProfiles = await ResolveAudienceProfilesAsync(tenantId, message.AudienceType, audienceTags, cancellationToken);
            var channels = MemberMessageReachMapping.DeserializeStringArray(message.ChannelsJson)
                .Select(MemberMessageReachMapping.ParseChannel)
                .Distinct()
                .ToList();
            if (channels.Count == 0)
            {
                throw new BusinessException(ErrorCodes.BadRequest, "消息渠道为空，无法执行发送");
            }

            // 4. 清理旧收件明细并准备渠道所需映射。
            await memberMessageReachRepository.RemoveRecipientsAsync(tenantId, messageId, cancellationToken);
            var openIdMap = await ResolveMiniUserOpenIdMapAsync(tenantId, audienceProfiles, cancellationToken);

            // 5. 按“会员 x 渠道”创建发送明细。
            var recipients = new List<MemberReachRecipient>(Math.Max(1, audienceProfiles.Count * channels.Count));
            var errorMessages = new List<string>();
            foreach (var profile in audienceProfiles)
            {
                foreach (var channel in channels)
                {
                    var recipient = new MemberReachRecipient
                    {
                        TenantId = tenantId,
                        MessageId = messageId,
                        MemberId = profile.Id,
                        Channel = channel,
                        Status = MemberMessageRecipientStatus.Pending,
                        Mobile = string.IsNullOrWhiteSpace(profile.Mobile) ? null : profile.Mobile.Trim()
                    };

                    try
                    {
                        // 5.1 按渠道执行真实发送。
                        switch (channel)
                        {
                            case MemberMessageChannel.InApp:
                                recipient.Status = MemberMessageRecipientStatus.Sent;
                                recipient.SentAt = DateTime.UtcNow;
                                break;

                            case MemberMessageChannel.Sms:
                            {
                                var phone = NormalizePhoneNumber(profile.Mobile);
                                if (string.IsNullOrWhiteSpace(phone))
                                {
                                    throw new BusinessException(ErrorCodes.BadRequest, "会员手机号为空");
                                }

                                recipient.Mobile = phone;
                                await SendSmsAsync(phone, message.Title, message.Content, cancellationToken);
                                recipient.Status = MemberMessageRecipientStatus.Sent;
                                recipient.SentAt = DateTime.UtcNow;
                                break;
                            }

                            case MemberMessageChannel.WeChatMini:
                            {
                                if (!openIdMap.TryGetValue(profile.UserId, out var openId) || string.IsNullOrWhiteSpace(openId))
                                {
                                    throw new BusinessException(ErrorCodes.BadRequest, "会员未绑定小程序 OpenId");
                                }

                                recipient.OpenId = openId;
                                await memberMessageWeChatSender.SendAsync(openId, message.Title, message.Content, cancellationToken);
                                recipient.Status = MemberMessageRecipientStatus.Sent;
                                recipient.SentAt = DateTime.UtcNow;
                                break;
                            }

                            default:
                                throw new BusinessException(ErrorCodes.BadRequest, "不支持的消息渠道");
                        }
                    }
                    catch (Exception ex)
                    {
                        // 5.2 单个收件人发送失败不影响整体流程，保留失败明细。
                        recipient.Status = MemberMessageRecipientStatus.Failed;
                        recipient.ErrorMessage = Truncate(CleanupErrorMessage(ex.Message), 512);
                        errorMessages.Add($"会员{profile.Id}-{MemberMessageReachMapping.ToChannelText(channel)}:{recipient.ErrorMessage}");
                    }

                    recipients.Add(recipient);
                }
            }

            // 6. 写入收件明细并回填消息统计。
            await memberMessageReachRepository.AddRecipientsAsync(recipients, cancellationToken);
            message.EstimatedReachCount = audienceProfiles.Count;
            message.SentCount = recipients.Count(item => item.Status == MemberMessageRecipientStatus.Sent);
            message.ReadCount = recipients.Count(item => item.ReadAt.HasValue);
            message.ConvertedCount = recipients.Count(item => item.ConvertedAt.HasValue);
            message.SentAt = DateTime.UtcNow;
            message.Status = message.SentCount > 0 ? MemberMessageStatus.Sent : MemberMessageStatus.Failed;
            message.LastError = BuildErrorSummary(errorMessages);

            // 7. 若使用模板发送，更新模板使用次数。
            if (message.TemplateId.HasValue)
            {
                var template = await memberMessageReachRepository.FindTemplateByIdAsync(tenantId, message.TemplateId.Value, cancellationToken);
                if (template is not null)
                {
                    template.UsageCount += 1;
                    template.LastUsedAt = DateTime.UtcNow;
                    await memberMessageReachRepository.UpdateTemplateAsync(template, cancellationToken);
                }
            }

            // 8. 保存最终状态。
            await memberMessageReachRepository.UpdateMessageAsync(message, cancellationToken);
            await memberMessageReachRepository.SaveChangesAsync(cancellationToken);
        }
        catch (Exception ex)
        {
            // 9. 全局异常兜底写失败态，并保留错误摘要。
            logger.LogError(ex, "执行会员消息发送失败，TenantId={TenantId} MessageId={MessageId}", tenantId, messageId);
            message.Status = MemberMessageStatus.Failed;
            message.SentAt = DateTime.UtcNow;
            message.SentCount = 0;
            message.ReadCount = 0;
            message.ConvertedCount = 0;
            message.LastError = Truncate(CleanupErrorMessage(ex.Message), 1024);
            await memberMessageReachRepository.UpdateMessageAsync(message, cancellationToken);
            await memberMessageReachRepository.SaveChangesAsync(cancellationToken);
        }
    }

    private async Task SendSmsAsync(
        string phoneNumber,
        string title,
        string content,
        CancellationToken cancellationToken)
    {
        // 1. 读取短信模板配置并解析场景模板编码。
        var smsOptions = smsOptionsMonitor.CurrentValue;
        var messageOptions = memberMessagingOptionsMonitor.CurrentValue;
        var smsScene = string.IsNullOrWhiteSpace(messageOptions.SmsScene)
            ? "member_message"
            : messageOptions.SmsScene.Trim();
        if (!smsOptions.SceneTemplates.TryGetValue(smsScene, out var templateCode) ||
            string.IsNullOrWhiteSpace(templateCode))
        {
            throw new BusinessException(ErrorCodes.BadRequest, $"未配置短信模板场景：{smsScene}");
        }

        // 2. 组装变量并调用短信通道发送。
        var sender = smsSenderResolver.Resolve();
        var variables = new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["title"] = Truncate(title.Trim(), 20),
            ["content"] = Truncate(content.Trim(), 64)
        };
        var request = new SmsSendRequest(phoneNumber, templateCode, variables, smsOptions.DefaultSignName);
        var result = await sender.SendAsync(request, cancellationToken);
        if (!result.Success)
        {
            throw new BusinessException(ErrorCodes.InternalServerError, $"短信发送失败：{result.Message}");
        }
    }

    private async Task<IReadOnlyList<MemberProfile>> ResolveAudienceProfilesAsync(
        long tenantId,
        MemberMessageAudienceType audienceType,
        IReadOnlyList<string> tags,
        CancellationToken cancellationToken)
    {
        // 1. 获取租户全部会员。
        var profiles = await memberRepository.GetProfilesAsync(tenantId, cancellationToken);
        if (profiles.Count == 0)
        {
            return [];
        }

        // 2. 全量人群直接返回。
        if (audienceType == MemberMessageAudienceType.All)
        {
            return profiles;
        }

        // 3. 标签人群解析标签规则并构建会员标签映射。
        var normalizedInputTags = tags
            .Select(ToCanonicalAudienceTag)
            .Where(tag => !string.IsNullOrWhiteSpace(tag))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();
        if (normalizedInputTags.Count == 0)
        {
            return [];
        }

        var profileIds = profiles.Select(profile => profile.Id).ToList();
        var profileTags = await memberRepository.GetProfileTagsByMemberIdsAsync(tenantId, profileIds, cancellationToken);
        var profileTagLookup = profileTags
            .GroupBy(item => item.MemberProfileId)
            .ToDictionary(
                group => group.Key,
                group => group
                    .Select(tag => ToCanonicalAudienceTag(tag.TagName))
                    .Where(tag => !string.IsNullOrWhiteSpace(tag))
                    .ToHashSet(StringComparer.OrdinalIgnoreCase));

        // 4. 应用固定规则筛选目标会员。
        var now = DateTime.UtcNow;
        var selected = profiles
            .Where(profile =>
            {
                var tagsOfProfile = profileTagLookup.TryGetValue(profile.Id, out var set)
                    ? set
                    : EmptyTagSet;

                return normalizedInputTags.Any(tag => MatchesAudienceTag(profile, tagsOfProfile, tag, now));
            })
            .ToList();
        return selected;
    }

    private async Task<Dictionary<long, string>> ResolveMiniUserOpenIdMapAsync(
        long tenantId,
        IReadOnlyList<MemberProfile> profiles,
        CancellationToken cancellationToken)
    {
        // 1. 收集会员关联的小程序用户标识。
        var miniUserIds = profiles
            .Select(profile => profile.UserId)
            .Where(userId => userId > 0)
            .Distinct()
            .ToList();
        if (miniUserIds.Count == 0)
        {
            return [];
        }

        // 2. 批量查询并映射 OpenId。
        var miniUsers = await miniUserRepository.GetByIdsAsync(miniUserIds, tenantId, cancellationToken);
        return miniUsers
            .Where(user => !string.IsNullOrWhiteSpace(user.OpenId))
            .ToDictionary(user => user.Id, user => user.OpenId, comparer: EqualityComparer<long>.Default);
    }

    private static MemberMessageReachListItemDto ToMessageListItem(MemberReachMessage source)
    {
        var channels = MemberMessageReachMapping.DeserializeStringArray(source.ChannelsJson);
        var audienceTags = MemberMessageReachMapping.DeserializeStringArray(source.AudienceTagsJson);
        return new MemberMessageReachListItemDto
        {
            MessageId = source.Id,
            Title = source.Title,
            Channels = channels,
            AudienceText = BuildAudienceText(source.AudienceType, audienceTags, source.EstimatedReachCount),
            EstimatedReachCount = source.EstimatedReachCount,
            Status = MemberMessageReachMapping.ToStatusText(source.Status),
            SentAt = source.SentAt,
            ScheduledAt = source.ScheduledAt,
            OpenRate = MemberMessageReachMapping.ResolveRatePercent(source.ReadCount, source.SentCount),
            ConversionRate = MemberMessageReachMapping.ResolveRatePercent(source.ConvertedCount, source.SentCount)
        };
    }

    private static MemberMessageDispatchMetaDto ToDispatchMeta(MemberReachMessage source)
    {
        return new MemberMessageDispatchMetaDto
        {
            MessageId = source.Id,
            Status = MemberMessageReachMapping.ToStatusText(source.Status),
            ScheduleType = MemberMessageReachMapping.ToScheduleTypeText(source.ScheduleType),
            ScheduledAt = source.ScheduledAt,
            HangfireJobId = source.HangfireJobId
        };
    }

    private static void EnsureTenantId(long tenantId)
    {
        if (tenantId <= 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "tenantId 非法");
        }
    }

    private static void EnsureMessageEditable(MemberReachMessage message)
    {
        if (message.Status == MemberMessageStatus.Sending)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "消息发送中，暂不允许编辑");
        }

        if (message.Status == MemberMessageStatus.Sent)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "已发送消息不允许编辑");
        }
    }

    private static string NormalizeSubmitAction(string? submitAction)
    {
        var action = (submitAction ?? string.Empty).Trim().ToLowerInvariant();
        return action switch
        {
            "draft" => "draft",
            "send" => "send",
            _ => throw new BusinessException(ErrorCodes.BadRequest, "submitAction 非法")
        };
    }

    private static string NormalizeRequiredText(string? value, int maxLength, string fieldName)
    {
        var normalized = (value ?? string.Empty).Trim();
        if (string.IsNullOrWhiteSpace(normalized))
        {
            throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 不能为空");
        }

        if (normalized.Length > maxLength)
        {
            throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 长度不能超过 {maxLength}");
        }

        return normalized;
    }

    private static DateTime? NormalizeScheduledAt(
        MemberMessageScheduleType scheduleType,
        string submitAction,
        DateTime? scheduledAt)
    {
        if (scheduleType == MemberMessageScheduleType.Immediate)
        {
            return null;
        }

        if (submitAction == "draft")
        {
            return scheduledAt?.ToUniversalTime();
        }

        if (!scheduledAt.HasValue)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "定时发送必须设置 scheduledAt");
        }

        var utcTime = scheduledAt.Value.ToUniversalTime();
        if (utcTime <= DateTime.UtcNow.AddMinutes(1))
        {
            throw new BusinessException(ErrorCodes.BadRequest, "定时发送时间必须晚于当前时间 1 分钟");
        }

        return utcTime;
    }

    private static bool MatchesAudienceTag(
        MemberProfile profile,
        IReadOnlySet<string> profileTags,
        string targetTag,
        DateTime nowUtc)
    {
        var hasProfileTag = profileTags.Contains(targetTag);

        return targetTag switch
        {
            "newcustomer" => hasProfileTag || profile.JoinedAt >= nowUtc.AddDays(-30),
            "dormant" => hasProfileTag || (profile.JoinedAt <= nowUtc.AddDays(-90) && profile.StoredBalance <= 0m && profile.PointsBalance <= 0),
            "lost" => hasProfileTag || (profile.JoinedAt <= nowUtc.AddDays(-180) && profile.Status != MemberStatus.Active),
            "highspend" => hasProfileTag || profile.StoredRechargeBalance >= 1000m || profile.StoredBalance >= 1000m,
            _ => hasProfileTag
        };
    }

    private static string BuildAudienceText(
        MemberMessageAudienceType audienceType,
        IReadOnlyList<string> tags,
        int estimatedReachCount)
    {
        if (audienceType == MemberMessageAudienceType.All)
        {
            return "全部会员";
        }

        var displayTags = tags
            .Select(ResolveAudienceDisplayName)
            .Where(tag => !string.IsNullOrWhiteSpace(tag))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();
        if (displayTags.Count == 0)
        {
            return $"标签人群({estimatedReachCount}人)";
        }

        return $"{string.Join("、", displayTags)}({estimatedReachCount}人)";
    }

    private static string ResolveAudienceDisplayName(string sourceTag)
    {
        var canonical = ToCanonicalAudienceTag(sourceTag);
        return AudienceTagDisplayMap.TryGetValue(canonical, out var displayName)
            ? displayName
            : (sourceTag ?? string.Empty).Trim();
    }

    private static string ToCanonicalAudienceTag(string? sourceTag)
    {
        var normalized = NormalizeAudienceTag(sourceTag);
        return AudienceTagAliasMap.TryGetValue(normalized, out var canonical)
            ? canonical
            : normalized;
    }

    private static string NormalizeAudienceTag(string? sourceTag)
    {
        if (string.IsNullOrWhiteSpace(sourceTag))
        {
            return string.Empty;
        }

        var trimmed = sourceTag.Trim().ToLowerInvariant();
        var filtered = trimmed
            .Where(ch => ch is not (' ' or '-' or '_'))
            .ToArray();
        return new string(filtered);
    }

    private static IReadOnlyDictionary<string, string> BuildAudienceTagAliasMap()
    {
        var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["highfrequency"] = "highfrequency",
            ["高频客户"] = "highfrequency",
            ["高频用户"] = "highfrequency",
            ["高频"] = "highfrequency",

            ["newcustomer"] = "newcustomer",
            ["新客"] = "newcustomer",
            ["新客户"] = "newcustomer",

            ["dormant"] = "dormant",
            ["沉睡客户"] = "dormant",
            ["沉睡用户"] = "dormant",

            ["lost"] = "lost",
            ["流失客户"] = "lost",
            ["流失用户"] = "lost",

            ["lunchregular"] = "lunchregular",
            ["午餐常客"] = "lunchregular",

            ["highspend"] = "highspend",
            ["大额消费"] = "highspend",
            ["高消费"] = "highspend"
        };

        return map;
    }

    private static string? BuildErrorSummary(IReadOnlyList<string> errors)
    {
        if (errors.Count == 0)
        {
            return null;
        }

        var content = string.Join(" | ", errors.Take(5));
        return Truncate(content, 1024);
    }

    private static string CleanupErrorMessage(string? message)
    {
        return (message ?? string.Empty)
            .Replace('\r', ' ')
            .Replace('\n', ' ')
            .Trim();
    }

    private static string? NormalizePhoneNumber(string? mobile)
    {
        if (string.IsNullOrWhiteSpace(mobile))
        {
            return null;
        }

        var trimmed = mobile.Trim();
        return trimmed.StartsWith("+", StringComparison.Ordinal) ? trimmed : $"+86{trimmed}";
    }

    private static string Truncate(string? value, int maxLength)
    {
        var normalized = (value ?? string.Empty).Trim();
        if (normalized.Length <= maxLength)
        {
            return normalized;
        }

        return normalized[..maxLength];
    }
}
