using Ease.NetCore.DataAccess;
using Ease.NetCore.DataAccess.SQL;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using OnlineSalesAutoCrop.CoreAPI.Models.Global;
using OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems;
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Auth;
using System;
using System.Data;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.Auth;

public class RefreshTokenService : IRefreshTokenService
{
    private readonly AppSettings _settings;
    public RefreshTokenService(IOptions<AppSettings> settings)
    {
        _settings = settings.Value;
    }

    public async Task<UserByRefreshTokenResponse> GetUserByRefreshTokenAsync(string refreshToken)
    {
        UserByRefreshTokenResponse response = new();
        try
        {
            using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode);
            try
            {
                response = await GetUserByRefreshTokenAsync(tc, refreshToken);
                tc.End();
            }
            catch (Exception ie)
            {
                tc?.HandleError();

                throw DBCustomError.GenerateCustomError(ie);
            }
        }
        catch (Exception e)
        {
            throw new InvalidOperationException(e.Message, e);
        }

        return response;
    }

    public async Task<GenerateRefreshTokenResponse> GenerateRefreshTokenByUserAsync(GenerateRefreshTokenRequest request)
    {
        GenerateRefreshTokenResponse refreshTokenResponse = new();
        try
        {
            using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
            try
            {
                // Rotate: revoke old token, issue new one
                RevokeRefreshTokenByUserIdAsync(tc, request.UserId);

                refreshTokenResponse=  await IssueTokensAsync(tc,request.UserId, request.IpAddress);

                tc.End();
            }
            catch (Exception ie)
            {
                tc?.HandleError();

                throw DBCustomError.GenerateCustomError(ie);
            }
        }
        catch (Exception e)
        {
            throw new InvalidOperationException(e.Message, e);
        }

        return refreshTokenResponse;
    }

    public async Task<string> GenerateRefreshTokenAsync(string refreshToken, string ipAddress)
    {
        string refreshTokenResponse = string.Empty;
        try
        {
            using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
            try
            {
                bool isValid = IsValidRefreshTokenAsync(tc, refreshToken);
                if(string.IsNullOrEmpty(refreshToken) && !isValid)
                {
                    throw new Exception($"Refresh token has expired or been revoked- {refreshToken}");
                }

                var storedToken = await GetByTokenHashAsync(tc, refreshToken);

                RevokeRefreshTokenByUserIdAsync(tc, storedToken.UserId);

                var result = await IssueTokensAsync(tc, storedToken.UserId, ipAddress);
                refreshTokenResponse = result.RefreshToken;

                tc.End();
            }
            catch (Exception ie)
            {
                tc?.HandleError();

                throw DBCustomError.GenerateCustomError(ie);
            }
        }
        catch (Exception e)
        {
            throw new InvalidOperationException(e.Message, e);
        }

        return refreshTokenResponse;
    }

    public async Task<bool> InvalidRefreshTokenAsync( TransactionContext tc, int userId)
    {
        bool response = false;
        try
        {
            RevokeRefreshTokenByUserIdAsync(tc, userId);
            response = true;
        }
        catch (Exception e)
        {
            throw new InvalidOperationException(e.Message, e);
        }

        return response;
    }

    #region Private Methods
    // ----- private helpers -----

    private bool IsValidRefreshTokenAsync(TransactionContext tc, string refreshToken )
    {
        try
        {
            string sql = SQLParser.MakeSQL("Select Count(1) FROM RefreshTokens where TokenHash = %s AND RevokedAt is null", refreshToken);
            var returnValue = tc.ExecuteScalar(sql);
            return Convert.ToInt32(returnValue) > 0 ? true : false;  
        }
        catch(Exception)
        {
            throw;
        }
    }

    private bool AddAsync(TransactionContext tc, InsertRefreshTokenRequest refreshToken)
    {
        bool returnValue = false;

        try
        {
            SqlParameter[] p =
            [
                SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.VarChar, pValue: refreshToken.UserId),
                SqlHelperExtension.CreateInParam(pName: "@TokenHash", pType: SqlDbType.VarChar, pValue: refreshToken.TokenHash),
                SqlHelperExtension.CreateInParam(pName: "@IpAddress", pType: SqlDbType.VarChar, pValue: refreshToken.IpAddress),
                SqlHelperExtension.CreateInParam(pName: "@CreatedAt", pType: SqlDbType.DateTime, pValue: DateTime.Now),
                SqlHelperExtension.CreateInParam(pName: "@ExpiredAt", pType: SqlDbType.DateTime, pValue: refreshToken.ExpiresAt),
            ];
            _ = tc.ExecuteNonQuerySp(spName: "dbo.InsertRefreshToken", parameterValues: p);

            returnValue = true;
        }
        catch (Exception e)
        {
            throw new InvalidOperationException(e.Message, e);
        }

        return returnValue;
    }

    private async Task<RefreshTokenResponse> GetByTokenHashAsync(TransactionContext tc, string tokenHash)
    {
        RefreshTokenResponse response = new();
        try
        {
            SqlParameter[] p =
            [
               SqlHelperExtension.CreateInParam(pName: "@RefreshToken", pType: SqlDbType.VarChar, pValue: tokenHash),
            ];
            using (IDataReader dr = await tc.ExecuteReaderSpAsync("dbo.GetRefreshTokenByTokenHash", parameterValues: p))
            {
                if (dr.Read())
                {
                    response.UserId = dr.GetInt32(0);
                    response.TokenHash = dr.GetString(1);
                    response.IpAddress = dr.GetString(2);
                    response.ExpiredAt = dr.GetDateTime(3);
                    response.RevokedAt = dr.IsDBNull(4) ? null : dr.GetDateTime(4);
                }
                dr.Close();
            }
        }
        catch (Exception e)
        {
            throw new InvalidOperationException(e.Message, e);
        }

        return response;
    }

    private bool RevokeRefreshTokenByUserIdAsync(TransactionContext tc, int userId)
    {
        bool returnValue = false;
        try
        {
            SqlParameter[] p =
                [
                    SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.Int, pValue: userId)
                ];
            _ = tc.ExecuteNonQuerySp(spName: "dbo.RevokedAllRefreshTokenByUserId", parameterValues: p);

            returnValue = true;

        }
        catch (Exception e)
        {
            throw new InvalidOperationException(e.Message, e);
        }

        return returnValue;
    }

    private async Task<UserByRefreshTokenResponse> GetUserByRefreshTokenAsync(TransactionContext tc, string refreshToken)
    {
        UserByRefreshTokenResponse response = new();
        try
        {
            SqlParameter[] p =
            [
               SqlHelperExtension.CreateInParam(pName: "@RefreshToken", pType: SqlDbType.VarChar, pValue: refreshToken),
            ];
            using (IDataReader dr = await tc.ExecuteReaderSpAsync("dbo.GetUserByRefreshToken", parameterValues: p))
            {
                if (dr.Read())
                {
                    response.UserId = dr.GetInt32(0);
                    response.LoginId = dr.GetString(1);
                    response.EmailAddress = dr.IsDBNull(2) ? string.Empty : dr.GetString(2);
                    response.AuthKey = dr.IsDBNull(3) ? string.Empty : dr.GetString(3);
                    response.IsActive = dr.IsDBNull(4) ? true : false;
                }
                dr.Close();
            }
        }
        catch (Exception e)
        {
            throw new InvalidOperationException(e.Message, e);
        }

        return response;
    }

    private async Task<GenerateRefreshTokenResponse> IssueTokensAsync(TransactionContext tc, int userId, string ipAddress)
    {
        string rawRefreshToken = GenerateRowToken();
        string tokenHash = HashToken(rawRefreshToken);
        var refreshToken = new InsertRefreshTokenRequest
        {
            UserId = userId,
            TokenHash = tokenHash,
            IpAddress = ipAddress,
            CreatedAt = DateTime.UtcNow,
            ExpiresAt = DateTime.UtcNow.AddDays(_settings.RefreshTokenDuration)
        };

        AddAsync(tc,refreshToken);

        return new GenerateRefreshTokenResponse
        {
            RefreshToken = tokenHash,
            ExpireTime = DateTime.UtcNow.AddDays(_settings.RefreshTokenDuration)
        };
    }

    private static string HashToken(string token)
    {
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
        return Convert.ToBase64String(bytes);
    }

    private string GenerateRowToken()
    {
        var bytes = new byte[64];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(bytes);
        return Convert.ToBase64String(bytes);
    }

    

    #endregion
}
