using System.Security.Cryptography;
using Microsoft.EntityFrameworkCore;
using ROLAC.API.Data;
using ROLAC.API.Entities.Notifications;

namespace ROLAC.API.Services.Notifications;

/// <summary>
/// Line outbound push + webhook-driven binding/group operations. All sends write a
/// NotificationLog row; binding consumes a short-lived, single-use code.
/// </summary>
public sealed class LineNotificationService : ILineNotificationService
{
    private const string Channel = NotificationChannels.Line;
    private const string CodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";   // no I/O/0/1
    private const int CodeLength = 6;
    private static readonly TimeSpan CodeLifetime = TimeSpan.FromMinutes(15);

    private readonly AppDbContext _db;
    private readonly IMessageChannel _channel;

    public LineNotificationService(AppDbContext db, IMessageChannel channel)
    {
        _db = db;
        _channel = channel;
    }

    public async Task<NotificationResult> SendLineAsync(string body, int[] memberIds, int[] groupIds,
                                                        string sentByUserId, CancellationToken ct = default)
    {
        var failures = new List<NotificationFailure>();
        var sentCount = 0;

        var liveMemberIds = await _db.Members
            .Where(m => memberIds.Contains(m.Id))
            .Select(m => m.Id)
            .ToListAsync(ct);
        var bindings = await _db.MemberChannelBindings
            .Where(b => b.Channel == Channel && liveMemberIds.Contains(b.MemberId))
            .ToListAsync(ct);
        foreach (var binding in bindings)
        {
            var result = await _channel.PushToUserAsync(binding.ExternalId, body, ct);
            _db.NotificationLogs.Add(BuildLog(NotificationTargetTypes.User, binding.ExternalId,
                body, sentByUserId, result, memberId: binding.MemberId));
            if (result.Success) sentCount++;
            else failures.Add(new NotificationFailure($"member:{binding.MemberId}", result.Error ?? "unknown"));
        }

        var groups = await _db.MessagingGroups
            .Where(g => g.Channel == Channel && g.IsActive && groupIds.Contains(g.Id))
            .ToListAsync(ct);
        foreach (var group in groups)
        {
            var result = await _channel.PushToGroupAsync(group.ExternalId, body, ct);
            _db.NotificationLogs.Add(BuildLog(NotificationTargetTypes.Group, group.ExternalId,
                body, sentByUserId, result, groupId: group.Id));
            if (result.Success) sentCount++;
            else failures.Add(new NotificationFailure($"group:{group.Id}", result.Error ?? "unknown"));
        }

        await _db.SaveChangesAsync(ct);
        return new NotificationResult(sentCount, failures.Count, failures);
    }

    public async Task<string> GenerateLineBindingCodeAsync(int memberId, CancellationToken ct = default)
    {
        var code = GenerateCode();
        _db.LineBindingCodes.Add(new LineBindingCode
        {
            Code = code,
            MemberId = memberId,
            ExpiresAt = DateTime.UtcNow.Add(CodeLifetime),
        });
        await _db.SaveChangesAsync(ct);
        return code;
    }

    public async Task<LineBindingResult> TryBindMemberAsync(string externalId, string code, CancellationToken ct = default)
    {
        var normalized = code.Trim().ToUpperInvariant();
        var now = DateTime.UtcNow;

        var bindingCode = await _db.LineBindingCodes
            .FirstOrDefaultAsync(c => c.Code == normalized && c.ConsumedAt == null && c.ExpiresAt > now, ct);
        if (bindingCode is null)
            return new LineBindingResult(false, "綁定碼無效或已過期。", null);

        bindingCode.ConsumedAt = now;

        var existing = await _db.MemberChannelBindings
            .FirstOrDefaultAsync(b => b.Channel == Channel && b.MemberId == bindingCode.MemberId, ct);
        if (existing is null)
        {
            _db.MemberChannelBindings.Add(new MemberChannelBinding
            {
                MemberId = bindingCode.MemberId,
                Channel = Channel,
                ExternalId = externalId,
                BoundAt = now,
            });
        }
        else
        {
            existing.ExternalId = externalId;
            existing.BoundAt = now;
        }

        await _db.SaveChangesAsync(ct);
        return new LineBindingResult(true, "綁定成功！", bindingCode.MemberId);
    }

    public async Task RegisterGroupAsync(string externalId, CancellationToken ct = default)
    {
        var group = await _db.MessagingGroups
            .FirstOrDefaultAsync(g => g.Channel == Channel && g.ExternalId == externalId, ct);
        if (group is null)
        {
            _db.MessagingGroups.Add(new MessagingGroup
            {
                Channel = Channel,
                ExternalId = externalId,
                IsActive = true,
                RegisteredAt = DateTime.UtcNow,
            });
        }
        else
        {
            group.IsActive = true;
        }
        await _db.SaveChangesAsync(ct);
    }

    public async Task DeactivateGroupAsync(string externalId, CancellationToken ct = default)
    {
        var group = await _db.MessagingGroups
            .FirstOrDefaultAsync(g => g.Channel == Channel && g.ExternalId == externalId, ct);
        if (group is not null)
        {
            group.IsActive = false;
            await _db.SaveChangesAsync(ct);
        }
    }

    private static NotificationLog BuildLog(string targetType, string externalId, string body,
        string sentBy, MessageSendResult result, int? memberId = null, int? groupId = null) => new()
    {
        Channel          = Channel,
        TargetType       = targetType,
        TargetExternalId = externalId,
        MemberId         = memberId,
        MessagingGroupId = groupId,
        Body             = NotificationLogText.Truncate(body),
        Status           = result.Success ? NotificationStatuses.Sent : NotificationStatuses.Failed,
        Error            = result.Error,
        SentByUserId     = sentBy,
        SentAt           = DateTime.UtcNow,
    };

    private static string GenerateCode()
    {
        var bytes = RandomNumberGenerator.GetBytes(CodeLength);
        var chars = new char[CodeLength];
        for (var i = 0; i < CodeLength; i++)
            chars[i] = CodeAlphabet[bytes[i] % CodeAlphabet.Length];
        return new string(chars);
    }
}
