using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Ikar.Server.Data;
using Ikar.Server.Data.Entities;
using Ikar.Server.Infrastructure;
using Ikar.Server.Infrastructure.Storage;
using Ikar.Shared;

namespace Ikar.Server.Controllers;

[ApiController]
[Authorize]
[Route("api/[controller]")]
public sealed class UsersController(
    IkarDbContext dbContext,
    PresenceTracker presenceTracker,
    AvatarStorageService avatarStorageService) : ControllerBase
{
    private const long MaxAvatarSizeBytes = 5 * 1024 * 1024;
    private const int ResolveContactsBatchSize = 500;
    private static readonly HashSet<string> AllowedAvatarContentTypes =
    [
        "image/gif",
        "image/heic",
        "image/heif",
        "image/jpeg",
        "image/png",
        "image/webp"
    ];

    private static readonly HashSet<string> AllowedAvatarExtensions =
    [
        ".gif",
        ".heic",
        ".heif",
        ".jpeg",
        ".jpg",
        ".png",
        ".webp"
    ];

    [HttpGet("me")]
    public async Task<ActionResult<UserSummaryDto>> GetMe(CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var user = await dbContext.Users
            .Include(x => x.Sessions)
            .AsNoTracking()
            .SingleAsync(x => x.Id == userId, cancellationToken);
        return Ok(user.ToDto(presenceTracker));
    }

    [HttpGet("{userId:guid}")]
    public async Task<ActionResult<UserSummaryDto>> GetUser(Guid userId, CancellationToken cancellationToken)
    {
        var currentUserId = User.GetRequiredUserId();
        var user = await dbContext.Users
            .Include(x => x.Sessions)
            .AsNoTracking()
            .SingleOrDefaultAsync(x => x.Id == userId && !x.IsBlocked, cancellationToken);

        if (user is null)
        {
            return NotFound();
        }

        if (user.Id != currentUserId && !user.IsBot)
        {
            var hasSharedChat = await dbContext.ChatMembers
                .AsNoTracking()
                .Where(x => x.UserId == currentUserId)
                .Join(
                    dbContext.ChatMembers.AsNoTracking().Where(x => x.UserId == userId),
                    left => left.ChatId,
                    right => right.ChatId,
                    (left, _) => left.ChatId)
                .AnyAsync(cancellationToken);

            if (!hasSharedChat)
            {
                return NotFound();
            }
        }

        return Ok(user.ToDto(presenceTracker));
    }

    [HttpGet("{userId:guid}/bot-commands")]
    public async Task<ActionResult<IReadOnlyList<BotCommandDto>>> GetBotCommands(
        Guid userId,
        CancellationToken cancellationToken)
    {
        var botUser = await dbContext.Users
            .AsNoTracking()
            .Include(x => x.BotProfile)
            .ThenInclude(x => x!.Commands)
            .SingleOrDefaultAsync(x => x.Id == userId && x.IsBot && !x.IsBlocked, cancellationToken);

        if (botUser?.BotProfile is null || !botUser.BotProfile.IsEnabled)
        {
            return NotFound();
        }

        return Ok(botUser.BotProfile.Commands
            .OrderBy(x => x.SortOrder)
            .Select(x => new BotCommandDto(x.Command, x.Description))
            .ToList());
    }

    [HttpPut("me")]
    public async Task<ActionResult<UserSummaryDto>> UpdateMe(
        UpdateMyProfileRequest request,
        CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var displayName = request.DisplayName?.Trim() ?? string.Empty;
        var about = string.IsNullOrWhiteSpace(request.About)
            ? null
            : request.About.Trim();

        if (string.IsNullOrWhiteSpace(displayName))
        {
            return ValidationProblem("Display name is required.");
        }

        if (displayName.Length > 120)
        {
            return ValidationProblem("Display name must be 120 characters or fewer.");
        }

        if (about?.Length > 70)
        {
            return ValidationProblem("About must be 70 characters or fewer.");
        }

        var user = await dbContext.Users
            .Include(x => x.Sessions)
            .SingleAsync(x => x.Id == userId, cancellationToken);

        user.DisplayName = displayName;
        user.About = about;
        await dbContext.SaveChangesAsync(cancellationToken);

        return Ok(user.ToDto(presenceTracker));
    }

    [HttpPost("me/avatar")]
    public async Task<ActionResult<UserSummaryDto>> UploadMyAvatar(
        [FromForm] IFormFile? file,
        CancellationToken cancellationToken)
    {
        if (file is null)
        {
            return ValidationProblem("Avatar file is required.");
        }

        if (file.Length <= 0)
        {
            return ValidationProblem("Avatar file is empty.");
        }

        if (file.Length > MaxAvatarSizeBytes)
        {
            return ValidationProblem("Avatar file must be 5 MB or smaller.");
        }

        if (!IsSupportedAvatar(file))
        {
            return ValidationProblem("Avatar must be PNG, JPG, HEIC, HEIF, WEBP or GIF.");
        }

        var userId = User.GetRequiredUserId();
        var user = await dbContext.Users
            .Include(x => x.Sessions)
            .SingleAsync(x => x.Id == userId, cancellationToken);

        var previousAvatarPath = user.AvatarStoragePath;
        var storedAvatar = await avatarStorageService.SaveAsync(userId, file, cancellationToken);

        user.AvatarStoragePath = storedAvatar.PhysicalPath;
        user.AvatarContentType = storedAvatar.ContentType;
        await dbContext.SaveChangesAsync(cancellationToken);

        avatarStorageService.DeleteIfExists(previousAvatarPath);
        return Ok(user.ToDto(presenceTracker));
    }

    [HttpDelete("me/avatar")]
    public async Task<ActionResult<UserSummaryDto>> DeleteMyAvatar(CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var user = await dbContext.Users
            .Include(x => x.Sessions)
            .SingleAsync(x => x.Id == userId, cancellationToken);

        var previousAvatarPath = user.AvatarStoragePath;
        user.AvatarStoragePath = null;
        user.AvatarContentType = null;
        await dbContext.SaveChangesAsync(cancellationToken);

        avatarStorageService.DeleteIfExists(previousAvatarPath);
        return Ok(user.ToDto(presenceTracker));
    }

    [HttpGet("me/privacy")]
    public async Task<ActionResult<PrivacySettingsDto>> GetMyPrivacySettings(CancellationToken cancellationToken)
    {
        var settings = await GetOrCreatePrivacySettingsAsync(User.GetRequiredUserId(), cancellationToken);
        return Ok(ToPrivacyDto(settings));
    }

    [HttpPut("me/privacy")]
    public async Task<ActionResult<PrivacySettingsDto>> UpdateMyPrivacySettings(
        UpdatePrivacySettingsRequest request,
        CancellationToken cancellationToken)
    {
        var settings = await GetOrCreatePrivacySettingsAsync(User.GetRequiredUserId(), cancellationToken);
        settings.PhoneNumberVisibility = NormalizePrivacyVisibility(request.PhoneNumberVisibility, fallback: "contacts");
        settings.LastSeenVisibility = NormalizePrivacyVisibility(request.LastSeenVisibility, fallback: "contacts");
        settings.ProfilePhotoVisibility = NormalizePrivacyVisibility(request.ProfilePhotoVisibility, fallback: "everyone");
        if (request.BlockedUserIds is not null)
        {
            settings.BlockedUserIds = SerializeUserIdList(request.BlockedUserIds, userId: User.GetRequiredUserId());
        }

        if (request.AlwaysAllowUserIds is not null)
        {
            settings.AlwaysAllowUserIds = SerializeUserIdList(request.AlwaysAllowUserIds, userId: User.GetRequiredUserId());
        }

        if (request.NeverAllowUserIds is not null)
        {
            settings.NeverAllowUserIds = SerializeUserIdList(request.NeverAllowUserIds, userId: User.GetRequiredUserId());
        }

        settings.PasscodeLockEnabled = request.PasscodeLockEnabled;
        settings.ShowMessagePreview = request.ShowMessagePreview;
        settings.UpdatedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);
        return Ok(ToPrivacyDto(settings));
    }

    [HttpGet("me/sessions")]
    public async Task<ActionResult<IReadOnlyList<UserSessionDto>>> GetMySessions(CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var currentSessionId = User.GetRequiredSessionId();
        var sessions = await dbContext.UserSessions
            .AsNoTracking()
            .Where(x => x.UserId == userId)
            .OrderByDescending(x => x.LastSeenAt)
            .Select(x => new UserSessionDto(
                x.Id,
                x.CreatedAt,
                x.LastSeenAt,
                x.Id == currentSessionId,
                x.Id == currentSessionId ? "This device" : "Ikar session"))
            .ToListAsync(cancellationToken);
        return Ok(sessions);
    }

    [HttpDelete("me/sessions/{sessionId:guid}")]
    public async Task<IActionResult> RevokeMySession(Guid sessionId, CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var session = await dbContext.UserSessions
            .SingleOrDefaultAsync(x => x.Id == sessionId && x.UserId == userId, cancellationToken);
        if (session is null)
        {
            return NotFound();
        }

        dbContext.UserSessions.Remove(session);
        await dbContext.SaveChangesAsync(cancellationToken);
        return NoContent();
    }

    [AllowAnonymous]
    [HttpGet("{userId:guid}/avatar/{avatarFileName}")]
    public async Task<IActionResult> GetAvatar(
        Guid userId,
        string avatarFileName,
        CancellationToken cancellationToken)
    {
        var avatar = await dbContext.Users
            .AsNoTracking()
            .Where(x => x.Id == userId)
            .Select(x => new
            {
                x.AvatarStoragePath,
                x.AvatarContentType,
                ProfilePhotoVisibility = x.PrivacySettings == null ? "everyone" : x.PrivacySettings.ProfilePhotoVisibility,
                AlwaysAllowUserIds = x.PrivacySettings == null ? string.Empty : x.PrivacySettings.AlwaysAllowUserIds,
                NeverAllowUserIds = x.PrivacySettings == null ? string.Empty : x.PrivacySettings.NeverAllowUserIds
            })
            .SingleOrDefaultAsync(cancellationToken);

        if (avatar is null || string.IsNullOrWhiteSpace(avatar.AvatarStoragePath))
        {
            return NotFound();
        }

        var storedFileName = Path.GetFileName(avatar.AvatarStoragePath);
        if (!string.Equals(storedFileName, avatarFileName, StringComparison.OrdinalIgnoreCase) ||
            !System.IO.File.Exists(avatar.AvatarStoragePath))
        {
            return NotFound();
        }

        var viewerUserId = TryGetAuthenticatedUserId();
        if (!await CanViewAvatarAsync(
                ownerUserId: userId,
                viewerUserId,
                avatar.ProfilePhotoVisibility,
                avatar.AlwaysAllowUserIds,
                avatar.NeverAllowUserIds,
                cancellationToken))
        {
            return NotFound();
        }

        return PhysicalFile(
            avatar.AvatarStoragePath,
            string.IsNullOrWhiteSpace(avatar.AvatarContentType) ? "application/octet-stream" : avatar.AvatarContentType);
    }

    [HttpGet("search")]
    public async Task<ActionResult<IReadOnlyList<UserSummaryDto>>> Search([FromQuery] string? q, CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var query = q?.Trim();
        var normalizedPhone = PhoneNumberNormalizer.Normalize(query);

        var usersQuery = dbContext.Users
            .AsNoTracking()
            .Where(x => x.Id != userId && !x.IsBlocked);
        if (!string.IsNullOrWhiteSpace(query))
        {
            var normalized = query.ToUpperInvariant();
            usersQuery = usersQuery.Where(x =>
                x.NormalizedUsername.Contains(normalized) ||
                EF.Functions.Like(x.DisplayName, $"%{query}%") ||
                (normalizedPhone != null && x.NormalizedPhoneNumber == normalizedPhone));
        }

        var users = await usersQuery
            .Include(x => x.Sessions)
            .OrderBy(x => x.DisplayName)
            .Take(25)
            .ToListAsync(cancellationToken);

        return Ok(users.Select(x => x.ToDto(presenceTracker)).ToList());
    }

    [HttpPost("contacts/resolve")]
    public async Task<ActionResult<IReadOnlyList<ContactMatchDto>>> ResolveContacts(
        ResolveContactsRequest request,
        CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var normalizedPhones = request.PhoneNumbers
            .Select(PhoneNumberNormalizer.Normalize)
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .Distinct(StringComparer.Ordinal)
            .ToList();

        if (normalizedPhones.Count == 0)
        {
            return Ok(Array.Empty<ContactMatchDto>());
        }

        var phoneOrder = normalizedPhones
            .Select((phone, index) => new { phone, index })
            .ToDictionary(x => x.phone!, x => x.index, StringComparer.Ordinal);

        var users = new List<Data.Entities.User>(normalizedPhones.Count);
        foreach (var batch in normalizedPhones.Chunk(ResolveContactsBatchSize))
        {
            var batchPhones = batch.ToList();
            var batchUsers = await dbContext.Users
                .Include(x => x.Sessions)
                .AsNoTracking()
                .Where(x =>
                    x.Id != userId &&
                    !x.IsBlocked &&
                    x.NormalizedPhoneNumber != null &&
                    batchPhones.Contains(x.NormalizedPhoneNumber))
                .ToListAsync(cancellationToken);

            users.AddRange(batchUsers);
        }

        var matches = users
            .Where(x => x.NormalizedPhoneNumber is not null)
            .OrderBy(x => phoneOrder[x.NormalizedPhoneNumber!])
            .Select(x => new ContactMatchDto(
                x.NormalizedPhoneNumber!,
                x.ToDto(presenceTracker)))
            .ToList();

        return Ok(matches);
    }

    private async Task<UserPrivacySettings> GetOrCreatePrivacySettingsAsync(Guid userId, CancellationToken cancellationToken)
    {
        var settings = await dbContext.UserPrivacySettings
            .SingleOrDefaultAsync(x => x.UserId == userId, cancellationToken);
        if (settings is not null)
        {
            return settings;
        }

        settings = new UserPrivacySettings
        {
            UserId = userId,
            PhoneNumberVisibility = "contacts",
            LastSeenVisibility = "contacts",
            ProfilePhotoVisibility = "everyone",
            BlockedUserIds = string.Empty,
            AlwaysAllowUserIds = string.Empty,
            NeverAllowUserIds = string.Empty,
            ShowMessagePreview = true,
            UpdatedAt = DateTimeOffset.UtcNow
        };
        dbContext.UserPrivacySettings.Add(settings);
        await dbContext.SaveChangesAsync(cancellationToken);
        return settings;
    }

    private static PrivacySettingsDto ToPrivacyDto(UserPrivacySettings settings) =>
        new(
            settings.PhoneNumberVisibility,
            settings.LastSeenVisibility,
            settings.ProfilePhotoVisibility,
            DeserializeUserIdList(settings.BlockedUserIds),
            DeserializeUserIdList(settings.AlwaysAllowUserIds),
            DeserializeUserIdList(settings.NeverAllowUserIds),
            settings.PasscodeLockEnabled,
            settings.ShowMessagePreview,
            settings.UpdatedAt);

    private static string NormalizePrivacyVisibility(string? value, string fallback)
    {
        var normalized = value?.Trim().ToLowerInvariant();
        return normalized is "everyone" or "contacts" or "nobody" ? normalized : fallback;
    }

    private static string SerializeUserIdList(IEnumerable<Guid> userIds, Guid userId) =>
        string.Join(
            ',',
            userIds
                .Where(x => x != Guid.Empty && x != userId)
                .Distinct()
                .Take(500)
                .Select(x => x.ToString("D")));

    private Guid? TryGetAuthenticatedUserId()
    {
        var value = User.FindFirstValue(ClaimTypes.NameIdentifier);
        return Guid.TryParse(value, out var userId) ? userId : null;
    }

    private async Task<bool> CanViewAvatarAsync(
        Guid ownerUserId,
        Guid? viewerUserId,
        string? profilePhotoVisibility,
        string? alwaysAllowUserIds,
        string? neverAllowUserIds,
        CancellationToken cancellationToken)
    {
        if (viewerUserId == ownerUserId)
        {
            return true;
        }

        var neverAllow = DeserializeUserIdList(neverAllowUserIds).ToHashSet();
        if (viewerUserId is not null && neverAllow.Contains(viewerUserId.Value))
        {
            return false;
        }

        var visibility = NormalizePrivacyVisibility(profilePhotoVisibility, fallback: "everyone");
        if (viewerUserId is null)
        {
            return visibility == "everyone" && neverAllow.Count == 0;
        }

        var alwaysAllow = DeserializeUserIdList(alwaysAllowUserIds).ToHashSet();
        if (alwaysAllow.Contains(viewerUserId.Value))
        {
            return true;
        }

        return visibility switch
        {
            "everyone" => true,
            "contacts" => await HasSharedChatAsync(ownerUserId, viewerUserId.Value, cancellationToken),
            _ => false
        };
    }

    private async Task<bool> HasSharedChatAsync(
        Guid firstUserId,
        Guid secondUserId,
        CancellationToken cancellationToken) =>
        await dbContext.ChatMembers
            .AsNoTracking()
            .Where(x => x.UserId == firstUserId)
            .Join(
                dbContext.ChatMembers.AsNoTracking().Where(x => x.UserId == secondUserId),
                left => left.ChatId,
                right => right.ChatId,
                (left, _) => left.ChatId)
            .AnyAsync(cancellationToken);

    private static IReadOnlyList<Guid> DeserializeUserIdList(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return [];
        }

        return value
            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(x => Guid.TryParse(x, out var parsed) ? parsed : Guid.Empty)
            .Where(x => x != Guid.Empty)
            .Distinct()
            .ToList();
    }

    private static bool IsSupportedAvatar(IFormFile file)
    {
        var extension = Path.GetExtension(file.FileName ?? string.Empty).ToLowerInvariant();
        if (!string.IsNullOrWhiteSpace(extension) && AllowedAvatarExtensions.Contains(extension))
        {
            return true;
        }

        return !string.IsNullOrWhiteSpace(file.ContentType) &&
               AllowedAvatarContentTypes.Contains(file.ContentType.Trim().ToLowerInvariant());
    }
}
