using Application.Abstractions.Messaging;
using Application.Abstractions.Data;
using Application.Abstractions.Forum;
using SharedKernel.Results;
using SharedKernel.Storage;
using Microsoft.EntityFrameworkCore;
using System.Text.RegularExpressions;

namespace Application.Features.Api.Forum.Post.Update;

public sealed class Handler(IAppDbContext db, IFileStorage fileStorage, IBoardPermissionService permissionService) : ICommandHandler<Command, Result>
{
    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> Handle(Command request, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(request.Subject))
        {
            return Result.Failure(Error.Problem("Post.SubjectRequired", "제목을 입력해주세요."));
        }

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

        if (post.MemberID != request.MemberID)
        {
            var reqMember = await db.Member.AsNoTracking().FirstOrDefaultAsync(x => x.ID == request.MemberID, ct);
            if (reqMember is null || !reqMember.IsAdmin)
            {
                var mgr = await permissionService.GetBoardManagerAsync(post.BoardID, request.MemberID, ct);
                if (mgr is null || !mgr.CanEdit)
                {
                    return Result.Failure(Error.Forbidden("Post.Forbidden", "수정 권한이 없습니다."));
                }
            }
        }

        post.BoardPrefixID = request.BoardPrefixID;
        post.Subject = request.Subject.Trim();
        post.Content = request.Content ?? string.Empty;
        post.IsSecret = request.IsSecret;
        post.IsNotice = request.IsNotice;
        post.IsSpeaker = request.IsSpeaker;
        post.UpdatedAt = DateTime.UtcNow;

        var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.Post, post.ID);

        // 이미지 처리 (새 이미지 추가)
        if (request.Images is { Count: > 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.PostImage.AddAsync(new Domain.Entities.Forum.Posts.PostImage
                    {
                        BoardID = post.BoardID,
                        PostID = post.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);
                }
            }

            // content의 data:image/ 플레이스홀더를 실제 이미지 경로로 순서대로 치환
            var content = post.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));
                    }
                }
            }
            post.Content = content;

            if (string.IsNullOrEmpty(post.Thumbnail))
            {
                post.Thumbnail = savedImageUrls[0];
            }

            // 카운터 재계산
            var imageCount = await db.PostImage.CountAsync(x => x.PostID == post.ID && !x.IsDisabled, ct);
            post.Images = (byte)Math.Min(imageCount + savedImageUrls.Count, byte.MaxValue);
        }

        // 파일 처리 (새 파일 추가)
        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(post.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));
                }
            }

            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.PostFile.AddAsync(new Domain.Entities.Forum.Posts.PostFile
                    {
                        BoardID = post.BoardID,
                        PostID = post.ID,
                        UUID = fileUuid,
                        FileName = file.FileName,
                        HashedName = result.FileName,
                        Path = uploadPath.ToRelativePath(),
                        Url = result.Url,
                        Extension = ext,
                        ContentType = file.ContentType,
                        Size = result.Size
                    }, ct);
                }
            }

            var fileCount = await db.PostFile.CountAsync(x => x.PostID == post.ID && !x.IsDisabled, ct);
            post.Files = (byte)Math.Min(fileCount + request.Files.Count, byte.MaxValue);
        }

        // 미디어 처리 (새 미디어 추가)
        if (request.Medias is { Count: > 0 })
        {
            foreach (var mediaUrl in request.Medias)
            {
                if (!string.IsNullOrWhiteSpace(mediaUrl))
                {
                    await db.PostMedia.AddAsync(new Domain.Entities.Forum.Posts.PostMedia
                    {
                        BoardID = post.BoardID,
                        PostID = post.ID,
                        Url = mediaUrl.Trim()
                    }, ct);
                }
            }

            var mediaCount = await db.PostMedia.CountAsync(x => x.PostID == post.ID && !x.IsDisabled, ct);
            post.Medias = (byte)Math.Min(mediaCount + request.Medias.Count(m => !string.IsNullOrWhiteSpace(m)), byte.MaxValue);
        }

        // 태그 처리 (diff 로직)
        if (request.Tags is not null)
        {
            var existingPostTags = await db.PostTag.Include(x => x.Tag).Where(x => x.PostID == post.ID).ToListAsync(ct);
            var newTagSlugs = request.Tags.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim().ToLowerInvariant().Replace(' ', '-')).ToHashSet();
            var existingSlugs = existingPostTags.Select(x => x.Tag.Slug).ToHashSet();

            // 삭제할 태그: 기존에 있지만 새 목록에 없는 것
            var toRemove = existingPostTags.Where(x => !newTagSlugs.Contains(x.Tag.Slug)).ToList();

            foreach (var postTag in toRemove)
            {
                db.PostTag.Remove(postTag);

                if (postTag.Tag.UsageCount > 0)
                {
                    postTag.Tag.UsageCount--;
                    postTag.Tag.UpdatedAt = DateTime.UtcNow;
                }
            }

            // 추가할 태그: 새 목록에 있지만 기존에 없는 것
            var toAdd = request.Tags
                .Where(t => !string.IsNullOrWhiteSpace(t))
                .Select(t => t.Trim())
                .Where(t =>
                {
                    return !existingSlugs.Contains(
                        t.ToLowerInvariant().Replace(' ', '-')
                    );
                })
                .ToList();

            foreach (var tagName in toAdd)
            {
                var slug = tagName.ToLowerInvariant().Replace(' ', '-');

                var tag = await db.Tag.FirstOrDefaultAsync(x => x.Slug == slug, ct);
                if (tag is null)
                {
                    tag = new Domain.Entities.Forum.Posts.Tag
                    {
                        Name = tagName,
                        Slug = slug
                    };

                    await db.Tag.AddAsync(tag, ct);
                    await db.SaveChangesAsync(ct);
                }

                tag.UsageCount++;
                tag.UpdatedAt = DateTime.UtcNow;

                await db.PostTag.AddAsync(new Domain.Entities.Forum.Posts.PostTag
                {
                    BoardID = post.BoardID,
                    PostID = post.ID,
                    TagID = tag.ID
                }, ct);
            }

            post.Tags = (byte)Math.Min(newTagSlugs.Count, byte.MaxValue);
        }

        await db.SaveChangesAsync(ct);

        return Result.Success();
    }
}
