using System.Text.Json;
using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Models.Profile;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Identity;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace MemberCenter.Infrastructure.Services;

public sealed class ProfileService : IProfileService
{
    private const int MaxProfileImageSizeBytes = 512 * 1024;

    private static readonly HashSet<string> AllowedProfileImageContentTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        "image/webp",
        "image/png",
        "image/jpeg"
    };

    private static readonly HashSet<string> AllowedGenders = new(StringComparer.OrdinalIgnoreCase)
    {
        "male",
        "female",
        "other",
        "unspecified"
    };

    private readonly MemberCenterDbContext _dbContext;
    private readonly IAuditLogWriter _auditLogWriter;
    private readonly UserManager<ApplicationUser> _userManager;

    public ProfileService(MemberCenterDbContext dbContext, IAuditLogWriter auditLogWriter, UserManager<ApplicationUser> userManager)
    {
        _dbContext = dbContext;
        _auditLogWriter = auditLogWriter;
        _userManager = userManager;
    }

    public async Task<UserProfileDto> GetProfileAsync(Guid userId)
    {
        var user = await _userManager.FindByIdAsync(userId.ToString());
        if (user is null)
        {
            throw new InvalidOperationException("User not found.");
        }

        var profile = await EnsureProfileAsync(userId);
        await _dbContext.SaveChangesAsync();
        return MapProfile(profile, user.Email ?? string.Empty);
    }

    public async Task<UserProfileDto> SaveProfileAsync(Guid userId, SaveUserProfileRequest request)
    {
        var user = await _userManager.FindByIdAsync(userId.ToString());
        if (user is null)
        {
            throw new InvalidOperationException("User not found.");
        }

        var profile = await EnsureProfileAsync(userId);
        profile.LastName = RequireValue(request.LastName, "LastName", 100);
        profile.FirstName = RequireValue(request.FirstName, "FirstName", 100);
        profile.NickName = CleanValue(request.NickName, 100);
        profile.MobilePhone = CleanValue(request.MobilePhone, 50);
        profile.LandlinePhone = CleanValue(request.LandlinePhone, 50);
        profile.DateOfBirth = request.DateOfBirth;
        profile.Gender = NormalizeGender(request.Gender);
        profile.CompanyName = CleanValue(request.CompanyName, 200);
        profile.Department = CleanValue(request.Department, 200);
        profile.JobTitle = CleanValue(request.JobTitle, 200);
        profile.CompanyPhone = CleanValue(request.CompanyPhone, 50);
        profile.TaxId = CleanValue(request.TaxId, 32);
        profile.InvoiceTitle = CleanValue(request.InvoiceTitle, 200);
        profile.Remark = CleanValue(request.Remark, 1000);
        profile.UpdatedAt = DateTimeOffset.UtcNow;

        await _auditLogWriter.WriteAsync("user", userId, "profile.updated", new
        {
            user_id = userId,
            email = user.Email,
            profile.Gender
        });

        await _dbContext.SaveChangesAsync();
        return MapProfile(profile, user.Email ?? string.Empty);
    }

    public async Task<IReadOnlyList<UserAddressDto>> ListAddressesAsync(Guid userId)
    {
        var addresses = await _dbContext.UserAddresses
            .Where(x => x.UserId == userId)
            .OrderByDescending(x => x.IsDefault)
            .ThenBy(x => x.Label)
            .ToListAsync();

        return addresses.Select(MapAddress).ToList();
    }

    public async Task<UserAddressDto?> GetAddressAsync(Guid userId, Guid addressId)
    {
        var address = await _dbContext.UserAddresses
            .FirstOrDefaultAsync(x => x.UserId == userId && x.Id == addressId);

        return address is null ? null : MapAddress(address);
    }

    public async Task<UserAddressDto> SaveAddressAsync(Guid userId, SaveUserAddressRequest request)
    {
        await using var transaction = await _dbContext.Database.BeginTransactionAsync();

        var address = request.Id.HasValue
            ? await _dbContext.UserAddresses.FirstOrDefaultAsync(x => x.UserId == userId && x.Id == request.Id.Value)
            : null;
        var wasDefault = address?.IsDefault == true;

        if (request.Id.HasValue && address is null)
        {
            throw new InvalidOperationException("Address not found.");
        }

        if (address is null)
        {
            address = new UserAddress
            {
                Id = Guid.NewGuid(),
                UserId = userId,
                CreatedAt = DateTimeOffset.UtcNow
            };
            _dbContext.UserAddresses.Add(address);
        }

        address.Label = RequireValue(request.Label, "Label", 100);
        address.RecipientName = RequireValue(request.RecipientName, "RecipientName", 100);
        address.RecipientPhone = RequireValue(request.RecipientPhone, "RecipientPhone", 50);
        address.CountryCode = RequireCountryCode(request.CountryCode);
        address.PostalCode = CleanValue(request.PostalCode, 20);
        address.StateRegion = CleanValue(request.StateRegion, 100);
        address.City = CleanValue(request.City, 100);
        address.District = CleanValue(request.District, 100);
        address.AddressLine1 = RequireValue(request.AddressLine1, "AddressLine1", 255);
        address.AddressLine2 = CleanValue(request.AddressLine2, 255);
        address.CompanyName = CleanValue(request.CompanyName, 200);
        address.AddressMetaJson = ParseOptionalJson(request.AddressMetaJson);
        address.UpdatedAt = DateTimeOffset.UtcNow;

        var otherAddresses = await _dbContext.UserAddresses
            .Where(x => x.UserId == userId && x.Id != address.Id)
            .ToListAsync();

        var shouldBeDefault = !otherAddresses.Any()
            || request.IsDefault
            || (!otherAddresses.Any(x => x.IsDefault) && wasDefault);

        // Persist the address first with a non-default state so the unique index
        // never sees two defaults during the switch.
        address.IsDefault = false;
        await _dbContext.SaveChangesAsync();

        if (shouldBeDefault)
        {
            await _dbContext.UserAddresses
                .Where(x => x.UserId == userId && x.Id != address.Id && x.IsDefault)
                .ExecuteUpdateAsync(setters => setters
                    .SetProperty(x => x.IsDefault, false)
                    .SetProperty(x => x.UpdatedAt, DateTimeOffset.UtcNow));

            address.IsDefault = true;
        }
        else
        {
            address.IsDefault = false;
        }

        await _auditLogWriter.WriteAsync("user", userId, request.Id.HasValue ? "address.updated" : "address.created", new
        {
            user_id = userId,
            address_id = address.Id,
            address.IsDefault
        });

        await _dbContext.SaveChangesAsync();
        await transaction.CommitAsync();
        return MapAddress(address);
    }

    public async Task DeleteAddressAsync(Guid userId, Guid addressId)
    {
        await using var transaction = await _dbContext.Database.BeginTransactionAsync();

        var addresses = await _dbContext.UserAddresses
            .Where(x => x.UserId == userId)
            .OrderByDescending(x => x.IsDefault)
            .ThenBy(x => x.CreatedAt)
            .ToListAsync();

        var address = addresses.FirstOrDefault(x => x.Id == addressId);
        if (address is null)
        {
            throw new InvalidOperationException("Address not found.");
        }

        if (addresses.Count == 1)
        {
            throw new InvalidOperationException("Cannot delete the last address.");
        }

        if (address.IsDefault)
        {
            var replacement = addresses
                .Where(x => x.Id != addressId)
                .OrderByDescending(x => x.CreatedAt)
                .FirstOrDefault();

            if (replacement is not null)
            {
                _dbContext.UserAddresses.Remove(address);
                await _dbContext.SaveChangesAsync();

                replacement.IsDefault = true;
                replacement.UpdatedAt = DateTimeOffset.UtcNow;
            }
            else
            {
                _dbContext.UserAddresses.Remove(address);
            }
        }
        else
        {
            _dbContext.UserAddresses.Remove(address);
        }

        await _auditLogWriter.WriteAsync("user", userId, "address.deleted", new
        {
            user_id = userId,
            address_id = addressId
        });

        await _dbContext.SaveChangesAsync();
        await transaction.CommitAsync();
    }

    public async Task<UserProfileImageDto?> GetProfileImageAsync(Guid userId)
    {
        var image = await _dbContext.UserProfileImages
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.UserId == userId);

        return image is null ? null : MapProfileImage(image);
    }

    public async Task SaveProfileImageAsync(Guid userId, byte[] content, string contentType, int width, int height)
    {
        if (content.Length == 0)
        {
            throw new InvalidOperationException("Profile image is required.");
        }

        if (content.Length > MaxProfileImageSizeBytes)
        {
            throw new InvalidOperationException("Profile image exceeds the size limit.");
        }

        if (!AllowedProfileImageContentTypes.Contains(contentType))
        {
            throw new InvalidOperationException("Profile image type is not supported.");
        }

        if (width <= 0 || height <= 0)
        {
            throw new InvalidOperationException("Profile image dimensions are invalid.");
        }

        var user = await _userManager.FindByIdAsync(userId.ToString());
        if (user is null)
        {
            throw new InvalidOperationException("User not found.");
        }

        var image = await _dbContext.UserProfileImages.FirstOrDefaultAsync(x => x.UserId == userId);
        if (image is null)
        {
            image = new UserProfileImage { UserId = userId };
            _dbContext.UserProfileImages.Add(image);
        }

        image.Content = content;
        image.ContentType = contentType;
        image.Width = width;
        image.Height = height;
        image.SizeBytes = content.Length;
        image.UpdatedAt = DateTimeOffset.UtcNow;

        await _auditLogWriter.WriteAsync("user", userId, "profile.image_updated", new
        {
            user_id = userId,
            email = user.Email,
            content_type = image.ContentType,
            size_bytes = image.SizeBytes,
            width = image.Width,
            height = image.Height
        });

        await _dbContext.SaveChangesAsync();
    }

    private async Task<UserProfile> EnsureProfileAsync(Guid userId)
    {
        var profile = await _dbContext.UserProfiles.FirstOrDefaultAsync(x => x.UserId == userId);
        if (profile is not null)
        {
            return profile;
        }

        profile = new UserProfile
        {
            UserId = userId,
            LastName = string.Empty,
            FirstName = string.Empty,
            Gender = "unspecified",
            UpdatedAt = DateTimeOffset.UtcNow
        };
        _dbContext.UserProfiles.Add(profile);
        return profile;
    }

    private static UserProfileDto MapProfile(UserProfile profile, string email) =>
        new(
            profile.UserId,
            email,
            profile.LastName,
            profile.FirstName,
            profile.NickName,
            profile.MobilePhone,
            profile.LandlinePhone,
            profile.DateOfBirth,
            profile.Gender,
            profile.CompanyName,
            profile.Department,
            profile.JobTitle,
            profile.CompanyPhone,
            profile.TaxId,
            profile.InvoiceTitle,
            profile.Remark);

    private static UserAddressDto MapAddress(UserAddress address) =>
        new(
            address.Id,
            address.UserId,
            address.Label,
            address.RecipientName,
            address.RecipientPhone,
            address.CountryCode,
            address.PostalCode,
            address.StateRegion,
            address.City,
            address.District,
            address.AddressLine1,
            address.AddressLine2,
            address.CompanyName,
            address.IsDefault,
            address.AddressMetaJson?.RootElement.GetRawText());

    private static UserProfileImageDto MapProfileImage(UserProfileImage image) =>
        new(
            image.UserId,
            image.Content,
            image.ContentType,
            image.Width,
            image.Height,
            image.SizeBytes,
            image.UpdatedAt);

    private static string RequireValue(string? value, string fieldName, int maxLength)
    {
        var cleaned = CleanValue(value, maxLength);
        if (string.IsNullOrWhiteSpace(cleaned))
        {
            throw new InvalidOperationException($"{fieldName} is required.");
        }

        return cleaned;
    }

    private static string? CleanValue(string? value, int maxLength)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        var trimmed = value.Trim();
        if (trimmed.Length > maxLength)
        {
            throw new InvalidOperationException($"Value exceeds max length {maxLength}.");
        }

        return trimmed;
    }

    private static string NormalizeGender(string? value)
    {
        var normalized = CleanValue(value, 20)?.ToLowerInvariant() ?? "unspecified";
        if (!AllowedGenders.Contains(normalized))
        {
            throw new InvalidOperationException("Invalid gender.");
        }

        return normalized;
    }

    private static string RequireCountryCode(string? value)
    {
        var normalized = RequireValue(value, "CountryCode", 2).ToUpperInvariant();
        if (normalized.Length != 2)
        {
            throw new InvalidOperationException("CountryCode must be 2 characters.");
        }

        return normalized;
    }

    private static JsonDocument? ParseOptionalJson(string? raw)
    {
        var cleaned = CleanValue(raw, 4000);
        return string.IsNullOrWhiteSpace(cleaned) ? null : JsonDocument.Parse(cleaned);
    }
}
