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,
    PhoneAuthChallengeStore phoneAuthChallengeStore) : 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 async Task<ActionResult<PhoneCodeChallengeDto>> RequestPhoneCode(
        RequestPhoneCodeRequest request,
        CancellationToken cancellationToken)
    {
        var normalizedPhoneNumber = PhoneNumberNormalizer.Normalize(request.PhoneNumber);
        if (normalizedPhoneNumber is null)
        {
            return ValidationProblem("Valid phone number is required.");
        }

        var user = await dbContext.Users
            .AsNoTracking()
            .SingleOrDefaultAsync(x => x.NormalizedPhoneNumber == normalizedPhoneNumber, cancellationToken);
        if (user?.IsBlocked == true)
        {
            return StatusCode(StatusCodes.Status423Locked, "User is blocked.");
        }

        var challenge = phoneAuthChallengeStore.Create(normalizedPhoneNumber, user is not null);
        return Ok(new PhoneCodeChallengeDto(
            challenge.Id,
            challenge.ExpiresAt,
            challenge.Code,
            challenge.IsRegistered));
    }

    [HttpPost("verify-code")]
    [AllowAnonymous]
    public async Task<ActionResult<AuthSessionDto>> VerifyPhoneCode(
        VerifyPhoneCodeRequest request,
        CancellationToken cancellationToken)
    {
        var normalizedPhoneNumber = PhoneNumberNormalizer.Normalize(request.PhoneNumber);
        if (normalizedPhoneNumber is null)
        {
            return ValidationProblem("Valid phone number is required.");
        }

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

        var user = await dbContext.Users.SingleOrDefaultAsync(
            x => x.NormalizedPhoneNumber == normalizedPhoneNumber,
            cancellationToken);

        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 CreatePhoneOnlyUserAsync(normalizedPhoneNumber, request.PhoneNumber, displayName, cancellationToken);
        }

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

        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<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;
            }
        }
    }
}
