using Application.Abstractions.Messaging;
using Application.Abstractions.Messaging.Email;
using Application.Abstractions.Data;
using Application.Abstractions.Forum;
using Domain.Entities.Forum.Comments;
using Domain.Entities.Forum.ValueObject;
using SharedKernel.Results;
using SharedKernel.Storage;
using Microsoft.EntityFrameworkCore;
using System.Text.RegularExpressions;

namespace Application.Features.Api.Forum.Comment.Create;

public sealed class Handler(IAppDbContext db, IFileStorage fileStorage, IBoardPermissionService permissionService, IMailService mailService) : ICommandHandler<Command, Result<int>>
{
    private static readonly string[] AllowedImageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"];
    private static readonly string[] AllowedFileExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt", ".zip", ".rar", ".7z", ".hwp", ".hwpx", ".csv"];

    public async Task<Result<int>> Handle(Command request, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(request.Content))
        {
            return Result.Failure<int>(Error.Problem("Comment.ContentRequired", "내용을 입력해주세요."));
        }

        var post = await db.Post.FirstOrDefaultAsync(x => x.ID == request.PostID, ct);
        if (post is null)
        {
            return Result.Failure<int>(Error.NotFound("Comment.PostNotFound", "게시글을 찾을 수 없습니다."));
        }

        var member = await db.Member.AsNoTracking().FirstOrDefaultAsync(x => x.ID == request.MemberID, ct);
        if (member is null)
        {
            return Result.Failure<int>(Error.NotFound("Comment.MemberNotFound", "회원 정보를 찾을 수 없습니다."));
        }

        // 댓글 권한 확인
        var boardMeta = await db.BoardMeta.AsNoTracking().FirstOrDefaultAsync(x => x.BoardID == post.BoardID, ct);
        var isQnABoard = boardMeta?.List.Layout == BoardLayout.QnA;

        if (boardMeta is not null)
        {
            // 댓글 내용 길이 검증
            var contentLength = request.Content.Trim().Length;

            if (boardMeta.Comment.MinContentLength > 0 && contentLength < boardMeta.Comment.MinContentLength)
            {
                return Result.Failure<int>(Error.Problem("Comment.MinContentLength", $"내용은 {boardMeta.Comment.MinContentLength}자 이상 작성해주세요."));
            }

            if (boardMeta.Comment.MaxContentLength > 0 && contentLength > boardMeta.Comment.MaxContentLength)
            {
                return Result.Failure<int>(Error.Problem("Comment.MaxContentLength", $"내용은 {boardMeta.Comment.MaxContentLength}자 이내로 작성해주세요."));
            }

            // 1:1 문의 게시판: 최고관리자·매니저만 댓글/대댓글 작성 가능
            if (isQnABoard)
            {
                if (!member.IsAdmin)
                {
                    var mgr = await permissionService.GetBoardManagerAsync(post.BoardID, member.ID, ct);
                    if (mgr is null)
                    {
                        return Result.Failure<int>(Error.Forbidden("Comment.QnAPermissionDenied", "1:1 문의 게시판은 관리자만 답변할 수 있습니다."));
                    }
                }
            }
            else
            {
                var requiredPermission = request.ParentID.HasValue ? boardMeta.Permission.ReplyWrite : boardMeta.Permission.CommentWrite;
                var requiredPermissionName = request.ParentID.HasValue ? "답글 작성 권한" : "댓글 작성 권한";

                if (!await permissionService.HasPermissionAsync(member, post.BoardID, requiredPermission, ct))
                {
                    return Result.Failure<int>(Error.Forbidden("Comment.PermissionDenied", $"{requiredPermissionName}이 없습니다."));
                }
            }

            // 파일 업로드 권한 확인
            if ((request.Images is { Count: > 0 } || request.Files is { Count: > 0 }) &&
                !await permissionService.HasPermissionAsync(member, post.BoardID, boardMeta.Permission.FileUpload, ct)
            ) {
                return Result.Failure<int>(Error.Forbidden("Comment.FileUploadDenied", "파일 업로드 권한이 없습니다."));
            }
        }

        sbyte depth = 0;
        var isReply = false;

        // 부모 댓글이 있을 경우 답글로 처리
        if (request.ParentID.HasValue)
        {
            var parent = await db.Comment.AsNoTracking().FirstOrDefaultAsync(x => x.ID == request.ParentID.Value, ct);
            if (parent is null)
            {
                return Result.Failure<int>(Error.NotFound("Comment.ParentNotFound", "상위 댓글을 찾을 수 없습니다."));
            }

            depth = (sbyte)(parent.Depth + 1);
            isReply = true;

            // 부모 댓글 답글 수 증가
            var parentEntity = await db.Comment.FirstOrDefaultAsync(x => x.ID == request.ParentID.Value, ct);
            if (parentEntity is not null)
            {
                parentEntity.Replies++;
            }
        }

        var comment = new Domain.Entities.Forum.Comments.Comment
        {
            BoardID = post.BoardID,
            PostID = request.PostID,
            MemberID = request.MemberID,
            ParentID = request.ParentID,
            Depth = depth,
            Content = request.Content.Trim(),
            IsReply = isReply,
            IsSecret = request.IsSecret,
            Name = member.Name,
            SID = member.SID,
            Email = member.Email
        };

        // Mention 처리
        if (!string.IsNullOrWhiteSpace(request.Mention))
        {
            var rawHandle = request.Mention.Trim();
            var handleValue = rawHandle.TrimStart('@').Trim();
            var mentionMember = await db.Member.AsNoTracking().FirstOrDefaultAsync(x => x.Name == handleValue || x.SID == handleValue, ct);

            if (mentionMember is not null)
            {
                comment.MentionMemberID = mentionMember.ID;
                comment.CommentMention = new CommentMention
                {
                    BoardID = post.BoardID,
                    PostID = request.PostID,
                    MemberID = mentionMember.ID,
                    RawHandle = rawHandle
                };
            }
        }

        await db.Comment.AddAsync(comment, ct);

        // Post 댓글 카운트 증가
        post.Comments++;
        post.LastCommentUpdatedAt = DateTime.UtcNow;

        // 1:1 문의 게시판: 댓글 작성 시 답변 완료 표시
        if (isQnABoard && !request.ParentID.HasValue)
        {
            post.IsReply = true;
        }

        // Board 댓글 카운트 증가
        var board = await db.Board.FirstOrDefaultAsync(x => x.ID == post.BoardID, ct);
        if (board is not null)
        {
            board.Comments++;
            board.UpdatedAt = DateTime.UtcNow;
        }

        // MemberStats 댓글 수 증가
        var memberStats = await db.MemberStats.FirstOrDefaultAsync(x => x.MemberID == request.MemberID, ct);
        if (memberStats is not null)
        {
            memberStats.CommentCount++;
        }

        await db.SaveChangesAsync(ct);

        // 파일 처리 (comment.ID 확보 후)
        var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.Comment, comment.ID);

        // 이미지 처리
        if (request.Images is { Count: > 0 })
        {
            byte imageCount = 0;
            var savedImageUrls = new List<string>();

            foreach (var image in request.Images)
            {
                var result = await fileStorage.SaveFileAsync(image, uploadPath, AllowedImageExtensions, ct);
                if (result is not null)
                {
                    var ext = Path.GetExtension(image.FileName).ToLowerInvariant();

                    await db.CommentImage.AddAsync(new CommentImage
                    {
                        BoardID = post.BoardID,
                        PostID = request.PostID,
                        CommentID = comment.ID,
                        FileName = image.FileName,
                        HashedName = result.FileName,
                        Path = uploadPath.ToRelativePath(),
                        Url = result.Url,
                        Extension = ext,
                        ContentType = image.ContentType,
                        Size = result.Size,
                        Width = result.Width,
                        Height = result.Height
                    }, ct);

                    savedImageUrls.Add(result.Url);
                    imageCount++;
                }
            }

            // content의 data:image/ 플레이스홀더를 실제 이미지 경로로 순서대로 치환
            var content = comment.Content;
            foreach (var url in savedImageUrls)
            {
                var idx = content.IndexOf("data:image/", StringComparison.Ordinal);
                if (idx >= 0)
                {
                    var endIdx = content.IndexOfAny(['"', '\''], idx);
                    if (endIdx > idx)
                    {
                        content = string.Concat(content.AsSpan(0, idx), url, content.AsSpan(endIdx));
                    }
                }
            }

            comment.Content = content;
            comment.Images = imageCount;
        }

        // 파일 처리
        if (request.Files is { Count: > 0 })
        {
            // content HTML에서 file-embed의 data-uuid와 data-name 매핑 추출
            var fileUuidMap = new List<(string Name, Guid Uuid)>();
            var uuidMatches = Regex.Matches(comment.Content, @"data-uuid=""([^""]+)""\s+data-name=""([^""]+)""");
            foreach (Match match in uuidMatches)
            {
                if (Guid.TryParse(match.Groups[1].Value, out var uuid))
                {
                    fileUuidMap.Add((match.Groups[2].Value, uuid));
                }
            }

            byte fileCount = 0;

            foreach (var file in request.Files)
            {
                var result = await fileStorage.SaveFileAsync(file, uploadPath, AllowedFileExtensions, ct);
                if (result is not null)
                {
                    var ext = Path.GetExtension(file.FileName).ToLowerInvariant();

                    // content의 file-embed에서 매칭되는 UUID 사용, 없으면 새로 생성
                    var mapIdx = fileUuidMap.FindIndex(x => x.Name == file.FileName);
                    Guid fileUuid;
                    if (mapIdx >= 0)
                    {
                        fileUuid = fileUuidMap[mapIdx].Uuid;
                        fileUuidMap.RemoveAt(mapIdx);
                    }
                    else
                    {
                        fileUuid = Guid.NewGuid();
                    }

                    await db.CommentFile.AddAsync(new Domain.Entities.Forum.Comments.CommentFile
                    {
                        BoardID = post.BoardID,
                        PostID = request.PostID,
                        CommentID = comment.ID,
                        UUID = fileUuid,
                        FileName = file.FileName,
                        HashedName = result.FileName,
                        Path = uploadPath.ToRelativePath(),
                        Url = result.Url,
                        Extension = ext,
                        ContentType = file.ContentType,
                        Size = result.Size
                    }, ct);

                    fileCount++;
                }
            }

            comment.Files = fileCount;
        }

        // 미디어 처리
        if (request.Medias is { Count: > 0 })
        {
            byte mediaCount = 0;

            foreach (var mediaUrl in request.Medias)
            {
                if (!string.IsNullOrWhiteSpace(mediaUrl))
                {
                    await db.CommentMedia.AddAsync(new CommentMedia
                    {
                        BoardID = post.BoardID,
                        PostID = request.PostID,
                        CommentID = comment.ID,
                        Url = mediaUrl.Trim()
                    }, ct);

                    mediaCount++;
                }
            }

            comment.Medias = mediaCount;
        }

        // 댓글 작성 보상 (경험치/토큰 — RewardService 단일 관문, 캡·멱등·자동승급). 아래 SaveChanges 로 일괄 커밋.
        await Application.Helpers.RewardService.GrantForCommentAsync(db, request.MemberID, comment.ID, request.Content.Trim().Length, ct);

        await db.SaveChangesAsync(ct);

        // 이메일 알림 발송
        if (boardMeta?.NotifyTemplate is not null && boardMeta.Notify is not null)
        {
            try
            {
                var notify = boardMeta.Notify;
                var template = boardMeta.NotifyTemplate;
                var notifyFlags = request.ParentID.HasValue ? notify.ReplyWriteNotifyEnum : notify.CommentWriteNotifyEnum;
                var emailSubject = request.ParentID.HasValue ? template.ReplyWriteEmailNotifySubject : template.CommentWriteEmailNotifySubject;
                var emailContent = request.ParentID.HasValue ? template.ReplyWriteEmailNotifyContent : template.CommentWriteEmailNotifyContent;

                if (notifyFlags != 0 && !string.IsNullOrWhiteSpace(emailSubject) && !string.IsNullOrWhiteSpace(emailContent))
                {
                    var recipients = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                    // 게시글 작성자
                    if (notifyFlags.HasFlag(BoardNotify.PostAuthor))
                    {
                        var postAuthor = await db.Member.AsNoTracking().FirstOrDefaultAsync(x => x.ID == post.MemberID, ct);
                        if (postAuthor is not null && !string.IsNullOrWhiteSpace(postAuthor.Email))
                        {
                            recipients.Add(postAuthor.Email);
                        }
                    }

                    // 부모 댓글 작성자 (대댓글일 때)
                    if (notifyFlags.HasFlag(BoardNotify.CommentAuthor) && request.ParentID.HasValue)
                    {
                        var parentComment = await db.Comment.AsNoTracking().FirstOrDefaultAsync(x => x.ID == request.ParentID.Value, ct);
                        if (parentComment is not null)
                        {
                            var commentAuthor = await db.Member.AsNoTracking().FirstOrDefaultAsync(x => x.ID == parentComment.MemberID, ct);
                            if (commentAuthor is not null && !string.IsNullOrWhiteSpace(commentAuthor.Email))
                            {
                                recipients.Add(commentAuthor.Email);
                            }
                        }
                    }

                    // 최고관리자
                    if (notifyFlags.HasFlag(BoardNotify.Admin))
                    {
                        var admins = await db.Member.AsNoTracking().Where(x => x.IsAdmin).ToListAsync(ct);
                        foreach (var admin in admins)
                        {
                            if (!string.IsNullOrWhiteSpace(admin.Email))
                            {
                                recipients.Add(admin.Email);
                            }
                        }
                    }

                    // 게시판 매니저
                    if (notifyFlags.HasFlag(BoardNotify.Manager))
                    {
                        var managers = await db.BoardManager.AsNoTracking().Where(x => x.BoardID == post.BoardID).ToListAsync(ct);
                        foreach (var mgr in managers)
                        {
                            var mgrMember = await db.Member.AsNoTracking().FirstOrDefaultAsync(x => x.ID == mgr.MemberID, ct);
                            if (mgrMember is not null && !string.IsNullOrWhiteSpace(mgrMember.Email))
                            {
                                recipients.Add(mgrMember.Email);
                            }
                        }
                    }

                    // 본인에게는 발송하지 않음
                    recipients.Remove(member.Email);

                    foreach (var email in recipients)
                    {
                        await mailService.SendAsync(new SendData(email, emailSubject, emailContent), ct);
                    }
                }
            }
            catch
            {
                // 이메일 발송 실패 시 댓글 등록은 성공 처리
            }
        }

        return comment.ID;
    }
}
