using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Ikar.Server.Data;
using Ikar.Server.Data.Entities;
using Ikar.Server.Infrastructure;
using Ikar.Server.Infrastructure.Auth;
using Ikar.Shared;

namespace Ikar.Server.Controllers;

[ApiController]
[Route("api/[controller]")]
public sealed class AuthController(
    IkarDbContext dbContext,
    TokenService tokenService,
    PresenceTracker presenceTracker,
    EmailAuthChallengeStore emailAuthChallengeStore,
    IEmailCodeSender emailCodeSender) : ControllerBase
{
    private readonly PasswordHasher<User> _passwordHasher = new();

    [HttpPost("register")]
    [AllowAnonymous]
    public async Task<ActionResult<AuthSessionDto>> Register(RegisterRequest request, CancellationToken cancellationToken)
    {
        var username = request.Username?.Trim() ?? string.Empty;
        var displayName = request.DisplayName?.Trim() ?? string.Empty;
        var password = request.Password ?? string.Empty;
        var phoneNumber = request.PhoneNumber?.Trim() ?? string.Empty;
        var normalizedPhoneNumber = PhoneNumberNormalizer.Normalize(phoneNumber);

        if (string.IsNullOrWhiteSpace(username) ||
            string.IsNullOrWhiteSpace(displayName) ||
            password.Length < 6 ||
            normalizedPhoneNumber is null)
        {
            return ValidationProblem("Username, display name, phone number and a password of at least 6 characters are required.");
        }

        var normalizedUsername = username.ToUpperInvariant();
        if (await dbContext.Users.AnyAsync(x => x.NormalizedUsername == normalizedUsername, cancellationToken))
        {
            return Conflict("Username is already taken.");
        }

        if (await dbContext.Users.AnyAsync(x => x.NormalizedPhoneNumber == normalizedPhoneNumber, cancellationToken))
        {
            return Conflict("Phone number is already used.");
        }

        var user = new User
        {
            Username = username,
            NormalizedUsername = normalizedUsername,
            DisplayName = displayName,
            PhoneNumber = phoneNumber,
            NormalizedPhoneNumber = normalizedPhoneNumber
        };

        user.PasswordHash = _passwordHasher.HashPassword(user, password);
        dbContext.Users.Add(user);
        try
        {
            await dbContext.SaveChangesAsync(cancellationToken);
        }
        catch (DbUpdateException)
        {
            var usernameTaken = await dbContext.Users
                .AsNoTracking()
                .AnyAsync(x => x.NormalizedUsername == normalizedUsername, cancellationToken);
            if (usernameTaken)
            {
                return Conflict("Username is already taken.");
            }

            var phoneTaken = await dbContext.Users
                .AsNoTracking()
                .AnyAsync(x => x.NormalizedPhoneNumber == normalizedPhoneNumber, cancellationToken);
            if (phoneTaken)
            {
                return Conflict("Phone number is already used.");
            }

            throw;
        }

        var session = await tokenService.CreateSessionAsync(
            user,
            ClientVersionRequestReader.Read(Request),
            cancellationToken);
        return Ok(session with { User = user.ToDto(presenceTracker) });
    }

    [HttpPost("login")]
    [AllowAnonymous]
    public async Task<ActionResult<AuthSessionDto>> Login(LoginRequest request, CancellationToken cancellationToken)
    {
        var username = request.Username?.Trim() ?? string.Empty;
        var password = request.Password ?? string.Empty;
        var normalizedUsername = username.ToUpperInvariant();
        var user = await dbContext.Users.SingleOrDefaultAsync(
            x => x.NormalizedUsername == normalizedUsername,
            cancellationToken);

        if (user is null)
        {
            return Unauthorized("Invalid username or password.");
        }

        if (user.IsBot)
        {
            return Unauthorized("Bot accounts cannot sign in through the user client.");
        }

        if (user.IsBlocked)
        {
            return StatusCode(StatusCodes.Status423Locked, "User is blocked.");
        }

        var verification = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
        if (verification == PasswordVerificationResult.Failed)
        {
            return Unauthorized("Invalid username or password.");
        }

        var session = await tokenService.CreateSessionAsync(
            user,
            ClientVersionRequestReader.Read(Request),
            cancellationToken);
        return Ok(session with { User = user.ToDto(presenceTracker) });
    }

    [HttpPost("request-code")]
    [AllowAnonymous]
    public Task<ActionResult<PhoneCodeChallengeDto>> RequestPhoneCode(
        RequestPhoneCodeRequest request,
        CancellationToken cancellationToken)
    {
        return Task.FromResult<ActionResult<PhoneCodeChallengeDto>>(
            StatusCode(
                StatusCodes.Status410Gone,
                new { error = "Phone-code authentication is disabled. Request a code by email." }));
    }

    [HttpPost("verify-code")]
    [AllowAnonymous]
    public Task<ActionResult<AuthSessionDto>> VerifyPhoneCode(
        VerifyPhoneCodeRequest request,
        CancellationToken cancellationToken)
    {
        return Task.FromResult<ActionResult<AuthSessionDto>>(
            StatusCode(
                StatusCodes.Status410Gone,
                new { error = "Phone-code authentication is disabled. Verify a code sent by email." }));
    }

    [HttpPost("request-email-code")]
    [AllowAnonymous]
    public async Task<ActionResult<EmailCodeChallengeDto>> RequestEmailCode(
        RequestEmailCodeRequest request,
        CancellationToken cancellationToken)
    {
        var normalizedEmail = EmailAddressNormalizer.Normalize(request.Email);
        if (normalizedEmail is null)
        {
            return ValidationProblem("Valid email is required.");
        }

        var normalizedPhoneNumber = PhoneNumberNormalizer.Normalize(request.PhoneNumber);
        if (normalizedPhoneNumber is null)
        {
            return ValidationProblem("Valid phone number is required.");
        }

        var email = request.Email.Trim();
        var (user, conflictMessage) = await LoadEmailPhoneUserAsync(
            normalizedEmail,
            normalizedPhoneNumber,
            cancellationToken);
        if (conflictMessage is not null)
        {
            return Conflict(conflictMessage);
        }

        if (RequiresAllowedRegistrationDomain(user, normalizedEmail) &&
            !EmailAddressNormalizer.IsAllowedRegistrationDomain(normalizedEmail))
        {
            return BadRequest(EmailAddressNormalizer.UnsupportedRegistrationDomainMessage);
        }

        if (user?.IsBlocked == true)
        {
            return StatusCode(StatusCodes.Status423Locked, "User is blocked.");
        }

        EmailAuthChallengeStore.EmailAuthChallenge challenge;
        try
        {
            challenge = emailAuthChallengeStore.Create(normalizedEmail, normalizedPhoneNumber, user is not null);
        }
        catch (AuthChallengeRateLimitException ex)
        {
            Response.Headers.RetryAfter = Math.Max(1, (int)Math.Ceiling(ex.RetryAfter.TotalSeconds)).ToString();
            return StatusCode(
                StatusCodes.Status429TooManyRequests,
                new { error = "Please wait before requesting another email code." });
        }
        try
        {
            await emailCodeSender.SendLoginCodeAsync(email, challenge.Code, challenge.ExpiresAt, cancellationToken);
        }
        catch
        {
            emailAuthChallengeStore.Remove(challenge.Id);
            throw;
        }

        return Ok(new EmailCodeChallengeDto(
            challenge.Id,
            challenge.ExpiresAt,
            IsRegistered: false));
    }

    [HttpPost("verify-email-code")]
    [AllowAnonymous]
    public async Task<ActionResult<AuthSessionDto>> VerifyEmailCode(
        VerifyEmailCodeRequest request,
        CancellationToken cancellationToken)
    {
        var normalizedEmail = EmailAddressNormalizer.Normalize(request.Email);
        if (normalizedEmail is null)
        {
            return ValidationProblem("Valid email is required.");
        }

        var normalizedPhoneNumber = PhoneNumberNormalizer.Normalize(request.PhoneNumber);
        if (normalizedPhoneNumber is null)
        {
            return ValidationProblem("Valid phone number is required.");
        }

        var challenge = emailAuthChallengeStore.Consume(
            request.ChallengeId?.Trim() ?? string.Empty,
            normalizedEmail,
            normalizedPhoneNumber,
            request.Code?.Trim() ?? string.Empty);
        if (challenge is null)
        {
            return Unauthorized("Invalid or expired verification code.");
        }

        var (user, conflictMessage) = await LoadEmailPhoneUserAsync(
            normalizedEmail,
            normalizedPhoneNumber,
            cancellationToken);
        if (conflictMessage is not null)
        {
            return Conflict(conflictMessage);
        }

        if (RequiresAllowedRegistrationDomain(user, normalizedEmail) &&
            !EmailAddressNormalizer.IsAllowedRegistrationDomain(normalizedEmail))
        {
            return BadRequest(EmailAddressNormalizer.UnsupportedRegistrationDomainMessage);
        }

        if (user is null)
        {
            var displayName = request.DisplayName?.Trim() ?? string.Empty;
            if (string.IsNullOrWhiteSpace(displayName))
            {
                return ValidationProblem("Display name is required for a new account.");
            }

            user = await CreateEmailPhoneUserAsync(
                normalizedEmail,
                request.Email,
                normalizedPhoneNumber,
                request.PhoneNumber,
                displayName,
                cancellationToken);
        }

        if (user.IsBot)
        {
            return Unauthorized("Bot accounts cannot sign in through the user client.");
        }

        if (user.IsBlocked)
        {
            return StatusCode(StatusCodes.Status423Locked, "User is blocked.");
        }

        await EnsureEmailPhoneLinkedAsync(
            user,
            normalizedEmail,
            request.Email,
            normalizedPhoneNumber,
            request.PhoneNumber,
            cancellationToken);

        var session = await tokenService.CreateSessionAsync(
            user,
            ClientVersionRequestReader.Read(Request),
            cancellationToken);
        return Ok(session with { User = user.ToDto(presenceTracker) });
    }

    [HttpPost("refresh")]
    [AllowAnonymous]
    public async Task<ActionResult<AuthSessionDto>> Refresh(RefreshSessionRequest request, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(request.RefreshToken))
        {
            return Unauthorized();
        }

        var session = await tokenService.RefreshSessionAsync(
            request.RefreshToken,
            ClientVersionRequestReader.Read(Request),
            cancellationToken);
        if (session is null)
        {
            return Unauthorized();
        }

        return Ok(session with { User = session.User with { IsOnline = presenceTracker.IsOnline(session.User.Id) } });
    }

    [HttpPost("logout")]
    [Authorize]
    public async Task<IActionResult> Logout(CancellationToken cancellationToken)
    {
        await tokenService.RevokeSessionAsync(User.GetRequiredSessionId(), cancellationToken);
        return NoContent();
    }

    private async Task<User> CreatePhoneOnlyUserAsync(
        string normalizedPhoneNumber,
        string rawPhoneNumber,
        string displayName,
        CancellationToken cancellationToken)
    {
        var username = await GenerateInternalUsernameAsync(cancellationToken);
        var user = new User
        {
            Username = username,
            NormalizedUsername = username.ToUpperInvariant(),
            DisplayName = displayName,
            PhoneNumber = rawPhoneNumber.Trim(),
            NormalizedPhoneNumber = normalizedPhoneNumber
        };

        user.PasswordHash = _passwordHasher.HashPassword(user, Guid.NewGuid().ToString("N"));
        dbContext.Users.Add(user);
        await dbContext.SaveChangesAsync(cancellationToken);
        return user;
    }

    private async Task<User> CreateEmailPhoneUserAsync(
        string normalizedEmail,
        string rawEmail,
        string normalizedPhoneNumber,
        string rawPhoneNumber,
        string displayName,
        CancellationToken cancellationToken)
    {
        var username = await GenerateInternalUsernameAsync(cancellationToken);
        var user = new User
        {
            Username = username,
            NormalizedUsername = username.ToUpperInvariant(),
            DisplayName = displayName,
            Email = rawEmail.Trim(),
            NormalizedEmail = normalizedEmail,
            PhoneNumber = rawPhoneNumber.Trim(),
            NormalizedPhoneNumber = normalizedPhoneNumber
        };

        user.PasswordHash = _passwordHasher.HashPassword(user, Guid.NewGuid().ToString("N"));
        dbContext.Users.Add(user);
        await dbContext.SaveChangesAsync(cancellationToken);
        return user;
    }

    private async Task<(User? User, string? ConflictMessage)> LoadEmailPhoneUserAsync(
        string normalizedEmail,
        string normalizedPhoneNumber,
        CancellationToken cancellationToken)
    {
        var users = await dbContext.Users
            .Where(x =>
                x.NormalizedEmail == normalizedEmail ||
                x.NormalizedPhoneNumber == normalizedPhoneNumber)
            .ToListAsync(cancellationToken);

        var emailUser = users.SingleOrDefault(x => x.NormalizedEmail == normalizedEmail);
        var phoneUser = users.SingleOrDefault(x => x.NormalizedPhoneNumber == normalizedPhoneNumber);
        if (emailUser is not null && phoneUser is not null && emailUser.Id != phoneUser.Id)
        {
            return (null, "Email and phone number belong to different accounts.");
        }

        var user = emailUser ?? phoneUser;
        if (user is null)
        {
            return (null, null);
        }

        if (!string.IsNullOrWhiteSpace(user.NormalizedEmail) &&
            !string.Equals(user.NormalizedEmail, normalizedEmail, StringComparison.Ordinal))
        {
            return (null, "Phone number is already linked to another email.");
        }

        if (!string.IsNullOrWhiteSpace(user.NormalizedPhoneNumber) &&
            !string.Equals(user.NormalizedPhoneNumber, normalizedPhoneNumber, StringComparison.Ordinal))
        {
            return (null, "Email is already linked to another phone number.");
        }

        return (user, null);
    }

    private static bool RequiresAllowedRegistrationDomain(User? user, string normalizedEmail) =>
        user is null ||
        !string.Equals(user.NormalizedEmail, normalizedEmail, StringComparison.Ordinal);

    private async Task EnsureEmailPhoneLinkedAsync(
        User user,
        string normalizedEmail,
        string rawEmail,
        string normalizedPhoneNumber,
        string rawPhoneNumber,
        CancellationToken cancellationToken)
    {
        var changed = false;
        if (string.IsNullOrWhiteSpace(user.NormalizedEmail))
        {
            user.Email = rawEmail.Trim();
            user.NormalizedEmail = normalizedEmail;
            changed = true;
        }

        if (string.IsNullOrWhiteSpace(user.NormalizedPhoneNumber))
        {
            user.PhoneNumber = rawPhoneNumber.Trim();
            user.NormalizedPhoneNumber = normalizedPhoneNumber;
            changed = true;
        }

        if (changed)
        {
            await dbContext.SaveChangesAsync(cancellationToken);
        }
    }

    private async Task<string> GenerateInternalUsernameAsync(CancellationToken cancellationToken)
    {
        while (true)
        {
            var candidate = $"user_{Guid.NewGuid():N}".ToLowerInvariant();
            var exists = await dbContext.Users
                .AsNoTracking()
                .AnyAsync(x => x.NormalizedUsername == candidate.ToUpperInvariant(), cancellationToken);
            if (!exists)
            {
                return candidate;
            }
        }
    }
}
