using Dapper;
using Femto.Common.Domain;
using Femto.Common.Infrastructure.DbConnection;
using Femto.Modules.Auth.Application.Dto;
using Femto.Modules.Auth.Data;
using Femto.Modules.Auth.Infrastructure;
using Femto.Modules.Auth.Models;
using Microsoft.EntityFrameworkCore;

namespace Femto.Modules.Auth.Application;

internal class AuthService(
    AuthContext context,
    SessionStorage sessionStorage,
    IDbConnectionFactory connectionFactory
) : IAuthService
{
    public async Task<UserAndSession?> AuthenticateUserCredentials(
        string username,
        string password,
        CancellationToken cancellationToken = default
    )
    {
        var user = await context
            .Users.Where(u => u.Username == username)
            .SingleOrDefaultAsync(cancellationToken);

        if (user is null)
            return null;

        if (!user.HasPassword(password))
            return null;

        var session = new Session(user.Id, true);

        await sessionStorage.AddSession(session);

        return new(
            new UserInfo(user.Id, user.Username, user.Roles.Select(r => r.Role).ToList()),
            session
        );
    }

    public Task<UserInfo?> GetUserWithId(Guid? userId, CancellationToken cancellationToken)
    {
        return context
            .Users.Where(u => u.Id == userId)
            .Select(u => new UserInfo(u.Id, u.Username, u.Roles.Select(r => r.Role).ToList()))
            .SingleOrDefaultAsync(cancellationToken);
    }

    public async Task<Session> CreateNewSession(Guid userId)
    {
        var session = new Session(userId, true);

        await sessionStorage.AddSession(session);

        return session;
    }

    public async Task<Session> CreateWeakSession(Guid userId)
    {
        var session = new Session(userId, false);

        await sessionStorage.AddSession(session);

        return session;
    }

    public Task<Session?> GetSession(string sessionId)
    {
        return sessionStorage.GetSession(sessionId);
    }

    public async Task DeleteSession(string sessionId)
    {
        await sessionStorage.DeleteSession(sessionId);
    }

    public async Task<UserAndSession> CreateUserWithCredentials(
        string username,
        string password,
        string signupCode,
        CancellationToken cancellationToken = default
    )
    {
        var now = DateTimeOffset.UtcNow;

        var code = await context
            .SignupCodes.Where(c => c.Code == signupCode)
            .Where(c => c.ExpiresAt == null || c.ExpiresAt > now)
            .Where(c => c.RedeemingUserId == null)
            .SingleOrDefaultAsync(cancellationToken);

        if (code is null)
            throw new DomainError("invalid signup code");

        var usernameTaken = await context.Users.AnyAsync(
            u => u.Username == username,
            cancellationToken
        );

        if (usernameTaken)
            throw new DomainError("username taken");

        var user = new UserIdentity(username);

        await context.AddAsync(user, cancellationToken);

        user.SetPassword(password);

        code.Redeem(user.Id);

        var session = new Session(user.Id, true);

        await sessionStorage.AddSession(session);

        await context.SaveChangesAsync(cancellationToken);

        return new(new UserInfo(user), session);
    }

    public async Task AddSignupCode(
        string code,
        string recipientName,
        CancellationToken cancellationToken
    )
    {
        await context.SignupCodes.AddAsync(
            new SignupCode("", recipientName, code),
            cancellationToken
        );

        await context.SaveChangesAsync(cancellationToken);
    }

    public async Task<ICollection<SignupCodeDto>> GetSignupCodes(
        CancellationToken cancellationToken = default
    )
    {
        using var conn = connectionFactory.GetConnection();

        // lang=sql
        const string sql = """
            SELECT 
                sc.code as Code,
                sc.recipient_email as Email,
                sc.recipient_name as Name,
                sc.redeeming_user_id as RedeemedByUserId,
                u.username as RedeemedByUsername,
                sc.expires_at as ExpiresOn
            FROM authn.signup_code sc
            LEFT JOIN authn.user_identity u ON u.id = sc.redeeming_user_id
            ORDER BY sc.created_at DESC
            """;

        var result = await conn.QueryAsync<GetSignupCodesQueryResultRow>(sql, cancellationToken);

        return result
            .Select(row => new SignupCodeDto(
                row.Code,
                row.Email,
                row.Name,
                row.RedeemedByUserId,
                row.RedeemedByUsername,
                row.ExpiresOn
            ))
            .ToList();
    }

    public async Task<NewRememberMeToken> CreateRememberMeToken(Guid userId)
    {
        var (rememberMeToken, verifier) = LongTermSession.Create(userId);

        await context.AddAsync(rememberMeToken);
        await context.SaveChangesAsync();

        return new(rememberMeToken.Selector, verifier, rememberMeToken.Expires);
    }

    public async Task<(UserInfo?, NewRememberMeToken?)> GetUserWithRememberMeToken(
        RememberMeToken rememberMeToken
    )
    {
        var token = await context.LongTermSessions.SingleOrDefaultAsync(t =>
            t.Selector == rememberMeToken.Selector
        );

        if (token is null)
            return (null, null);

        if (!token.CheckVerifier(rememberMeToken.Verifier))
            return (null, null);

        var user = await context.Users.SingleOrDefaultAsync(u => u.Id == token.UserId);

        if (user is null)
            return (null, null);

        if (token.ExpiresSoon)
        {
            var (newToken, verifier) = LongTermSession.Create(user.Id);
            await context.AddAsync(newToken);
            await context.SaveChangesAsync();

            return (new(user), new(newToken.Selector, verifier, newToken.Expires));
        }

        return (new(user), null);
    }

    public async Task DeleteRememberMeToken(RememberMeToken rememberMeToken)
    {
        var session = await context.LongTermSessions.SingleOrDefaultAsync(s =>
            s.Selector == rememberMeToken.Selector
        );

        if (session is null)
            return;

        if (!session.CheckVerifier(rememberMeToken.Verifier))
            return;

        context.Remove(session);
        await context.SaveChangesAsync();
    }

    public async Task ChangePassword(Guid userId, string password, CancellationToken cancellationToken)
    {
        // change the password
        // invalidate long term sessions
        // invalidate sessions

        var user = await context.Users.SingleOrDefaultAsync(u => u.Id == userId,cancellationToken);

        if (user is null)
            throw new DomainError("invalid user");

        user.SetPassword(password);

        await context.SaveChangesAsync(cancellationToken);
    }

    public async Task InvalidateUserSessions(Guid userId, CancellationToken cancellationToken)
    {
        await sessionStorage.InvalidateUserSessions(userId);
    }

    private class GetSignupCodesQueryResultRow
    {
        public string Code { get; set; }
        public string Email { get; set; }
        public string Name { get; set; }
        public Guid? RedeemedByUserId { get; set; }
        public string? RedeemedByUsername { get; set; }
        public DateTimeOffset? ExpiresOn { get; set; }
    }
}
