using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using TwoFactorAuthNet;
using QRCoder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Sockeye.Models;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using System.Linq;
using System;
using System.Threading.Tasks;
using Sockeye.Biz;

//required to inject configuration in constructor
using Microsoft.Extensions.Configuration;

namespace Sockeye.Api.Controllers
{
    /// <summary>
    /// Authentication controller
    /// </summary>
    [ApiController]
    [ApiVersion("8.0")]
    [Route("api/v{version:apiVersion}/auth")]
    [Produces("application/json")]
    [Authorize]
    public class AuthController : ControllerBase
    {
        private readonly AyContext ct;
        private readonly ILogger<AuthController> log;
        private readonly IConfiguration _configuration;
        private readonly ApiServerState serverState;
        private const int JWT_LIFETIME_DAYS = 5;

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="context"></param>
        /// <param name="logger"></param>
        /// <param name="configuration"></param>
        /// <param name="apiServerState"></param>       
        public AuthController(AyContext context, ILogger<AuthController> logger, IConfiguration configuration, ApiServerState apiServerState)
        {
            ct = context;
            log = logger;
            _configuration = configuration;
            serverState = apiServerState;
        }

        //AUTHENTICATE CREDS 
        //RETURN JWT        

        /// <summary>
        /// Create credentials to receive a JSON web token
        /// </summary>
        /// <remarks>
        /// This route is used to authenticate to the Sockeye API.
        /// Once you have a token you need to include it in all requests that require authentication like this:
        /// <code>Authorization: Bearer [TOKEN]</code>
        /// Note the space between Bearer and the token. Also, do not include the square brackets      
        /// </remarks>  
        /// <param name="creds"></param>
        /// <returns></returns>       
        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> PostCreds([FromBody] AuthController.CredentialsParam creds) //if was a json body then //public JsonResult PostCreds([FromBody] string login, [FromBody] string password)
        {

            //NOTE: lockout or other login impacting state is processed later in ReturnUserCredsOnSuccessfulAuthentication() because many of those states need to have exceptions once the user is known
            //or return alternate result of auth etc

          

            if (string.IsNullOrWhiteSpace(creds.Login) || string.IsNullOrWhiteSpace(creds.Password))
            {
                //Make a failed pw wait
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
            }


            //Multiple users are allowed the same password and login
            //Salt will differentiate them so get all users that match login, then try to match pw             
            var users = await ct.User.Where(z => z.Login == creds.Login && z.Active == true && z.AllowLogin == true).ToListAsync();

            foreach (User u in users)
            {
                string hashed = Hasher.hash(u.Salt, creds.Password);
                if (hashed == u.Password)
                {


                    //TWO FACTOR ENABLED??
                    //if 2fa enabled then need to validate it before sending token, so we're halfway there and need to send a 2fa prompt
                    if (u.TwoFactorEnabled)
                    {
                        //Generate a temporary token to identify and verify this is the same user
                        u.TempToken = Hasher.GenerateSalt().Replace("=", "").Replace("+", "");
                        await ct.SaveChangesAsync();
                        var UOpt = await ct.UserOptions.AsNoTracking().FirstAsync(z => z.UserId == u.Id);

                        List<string> TranslationKeysToFetch = new List<string> { "AuthTwoFactor", "AuthEnterPin", "AuthVerifyCode", "Cancel", "AuthPinInvalid" };
                        var LT = await TranslationBiz.GetSubsetStaticAsync(TranslationKeysToFetch, UOpt.TranslationId);

                        return Ok(ApiOkResponse.Response(new
                        {
                            AuthTwoFactor = LT["AuthTwoFactor"],
                            AuthEnterPin = LT["AuthEnterPin"],
                            AuthVerifyCode = LT["AuthVerifyCode"],
                            AuthPinInvalid = LT["AuthPinInvalid"],
                            Cancel = LT["Cancel"],
                            tfa = true,
                            tt = u.TempToken
                        }));
                    }

                    //Not 2fa, Valid password, user is authorized
                    return await ReturnUserCredsOnSuccessfulAuthentication(u);


                }
            }

            //No users matched, it's a failed login
            //Make a failed pw wait            
            await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
            return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
        }




        /// <summary>
        /// Verify tfa code
        /// </summary>
        /// <remarks>
        /// This route is used to authenticate to the Sockeye API via tfa code.
        /// Once you have a token you need to include it in all requests that require authentication like this:
        /// <code>Authorization: Bearer [TOKEN]</code>
        /// Note the space between Bearer and the token. Also, do not include the square brackets      
        /// </remarks>  
        /// <param name="pin"></param>
        /// <returns></returns>       
        [HttpPost("tfa-authenticate")]
        [AllowAnonymous]
        public async Task<IActionResult> TfaAuthenticate([FromBody] TFAPinParam pin)
        {
            //a bit different as ops users can still login if the state is opsonly 
            //so the only real barrier here would be a completely closed api


            if (serverState.IsClosed)
            {
                return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
            }

            if (string.IsNullOrWhiteSpace(pin.Pin))
            {
                //Make a failed pw wait
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
            }

            //Match to temp token that would have been set by initial credentialed login for 2fa User        
            var user = await ct.User.Where(z => z.TempToken == pin.TempToken && z.Active == true && z.AllowLogin==true && z.TwoFactorEnabled == true).FirstOrDefaultAsync();


            if (user != null)
            {
                //Valid temp token, now check the pin code is right
                if (string.IsNullOrWhiteSpace(user.TotpSecret))
                {
                    await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                    return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa not activated"));
                }

                //ok, something to validate, let's validate it
                var tfa = new TwoFactorAuth("Sockeye");
                if (!tfa.VerifyCode(user.TotpSecret, pin.Pin.Replace(" ", "").Trim()))
                {
                    await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                    return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
                }

                //User is valid and authenticated
                //clear temp token
                user.TempToken = string.Empty;
                await ct.SaveChangesAsync();

                return await ReturnUserCredsOnSuccessfulAuthentication(user);
            }

            //No users matched, it's a failed login
            //Make a failed pw wait            
            await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
            return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
        }



        //return creds and or process lockout handling here
        private async Task<IActionResult> ReturnUserCredsOnSuccessfulAuthentication(User u)
        {

            bool licenseLockout = false;
            //check if server available to SuperUser account only (closed or migrate mode)
            //if it is it means we got here either because there is no license 
            //and only *the* SuperUser account can login now or we're in migrate mode
            if (serverState.IsClosed )
            {
                //if not SuperUser account then boot closed
                //SuperUser account is always ID 1
                if (u.Id != 1)
                {
                    return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
                }
               
            }
            //Restrict auth due to server state?
            //If we're here it's the superuser or the server state is not closed, but it might be ops only

            //If the server is ops only then this user needs to be ops or else they are not allowed in
            if ((u.Id != 1) && serverState.IsOpsOnly &&
            !u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdmin) &&
            !u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminRestricted))
            {
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
            }


            //build the key (JWT set in startup.cs)
            byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.SOCKEYE_JWT_SECRET);

            //create a new datetime offset of now in utc time
            var iat = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);//timespan zero means zero time off utc / specifying this is a UTC datetime

            //###################################
            //Lifetime of jwt token
            //after this point the user will no longer be able to make requests without logging in again 
            //and the client will automatically send them to the login screen          
            //so this is auto logout after this time period

            //security wise  the length of time is not an issue how long this is because our system allows to revoke tokens as they are checked on every access
            //the adivce online is to make it short and use refresh tokens but that's not an issue with our system since we both issue and validate
            //the tokens ourselves

            //The only down side is that an expired license at the server will not prevent people from continuing to work until their token expires
            //an expired license only stops a fresh login
            //so whatever this value is will allow people who haven't logged out to continue to work until it expires

            //so this really only controls how long we allow them to work with an expired ayanova license which would be a rare occurence I suspect 
            //so really to prevent fuckery for people 5 days seems fine meaning they won't need to sign in again all business week if they want to continue working
            var exp = new DateTimeOffset(DateTime.Now.AddDays(JWT_LIFETIME_DAYS).ToUniversalTime(), TimeSpan.Zero);



            //=============== download token ===================                  
            //Generate a download token and store it with the user account
            //string DownloadToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
            string DownloadToken = Hasher.GenerateSalt();
            DownloadToken = DownloadToken.Replace("=", "");
            DownloadToken = DownloadToken.Replace("+", "");
            u.DlKey = DownloadToken;
            u.DlKeyExpire = exp.UtcDateTime;

            //=======================================================

            var payload = new Dictionary<string, object>()
                        {
                           // { "iat", iat.ToUnixTimeSeconds().ToString() },
                            { "exp", exp.ToUnixTimeSeconds().ToString() },//in payload exp must be in unix epoch time per standard
                            { "iss", "rockfish.ayanova.com" },
                            { "id", u.Id.ToString() }
                        };


            //NOTE: probably don't need Jose.JWT as am using Microsoft jwt stuff to validate routes so it should also be able to
            //issue tokens as well, but it looked cmplex and this works so unless need to remove in future keeping it.
            string token = Jose.JWT.Encode(payload, secretKey, Jose.JwsAlgorithm.HS256);

            //save auth token to ensure single sign on only
            u.CurrentAuthToken = token;

            u.LastLogin = DateTime.UtcNow;

            await ct.SaveChangesAsync();

            //KEEP this, masked version of IP address
            //Not sure if this is necessary or not but if it turns out to be then make it a boot option
            // log.LogInformation($"User number \"{u.Id}\" logged in from \"{Util.StringUtil.MaskIPAddress(HttpContext.Connection.RemoteIpAddress.ToString())}\" ok");

            log.LogInformation($"User \"{u.Name}\" logged in from \"{HttpContext.Connection.RemoteIpAddress.ToString()}\" ok");


            //return appropriate data for user type...
            if (u.UserType == UserType.Customer | u.UserType == UserType.HeadOffice)
            {
                //customer type has special rights restrictions for UI features so return them here so client UI can enable or disable
                var effectiveRights = await UserBiz.CustomerUserEffectiveRightsAsync(u.Id);
                //A non active Customer or Head Office record's contacts are also not allowed to login
                if (!effectiveRights.EntityActive)
                {
                    log.LogInformation($"Customer contact user \"{u.Name}\" attempted login was denied due to inactive parent (Customer or HeadOffice)");
                    await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                    return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
                }
                return Ok(ApiOkResponse.Response(new
                {
                    token = token,
                    name = u.Name,
                    usertype = u.UserType,
                    roles = ((int)u.Roles).ToString(),
                    dlt = DownloadToken,
                    tfa = u.TwoFactorEnabled,
                    CustomerRights = effectiveRights
                }));
            }
            else
            {
                //Non customer user
                return Ok(ApiOkResponse.Response(new
                {
                    token = token,
                    name = u.Name,
                    usertype = u.UserType,
                    roles = ((int)u.Roles).ToString(),
                    dlt = DownloadToken,
                    tfa = u.TwoFactorEnabled,
                    l = licenseLockout
                }));
            }

            //------------------------ /STANDARD BLOCK -------------------------
        }









        /// <summary>
        /// Change Password
        /// </summary>    
        /// <param name="changecreds"></param>
        /// <returns></returns>       
        [HttpPost("change-password")]
        public async Task<IActionResult> ChangePassword([FromBody] AuthController.ChangePasswordParam changecreds)
        {
            //Note: need to be authenticated to use this, only called from own user's UI 
            //it still asks for old creds in case someone attempts to do this on another user's logged in session
            //Also it checks here that this is in fact the same user account calling this method as the user attempting to be modified

            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
            if (!ModelState.IsValid)
                return BadRequest(new ApiErrorResponse(ModelState));

            if (string.IsNullOrWhiteSpace(changecreds.OldPassword) || string.IsNullOrWhiteSpace(changecreds.LoginName))
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
            }

            if (string.IsNullOrWhiteSpace(changecreds.NewPassword))
                return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "NewPassword"));

            if (changecreds.NewPassword != changecreds.ConfirmPassword)
                return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "ConfirmPassword", "NewPassword does not match ConfirmPassword"));

            //Multiple users are allowed the same password and login
            //Salt will differentiate them so get all users that match login, then try to match pw             
            var users = await ct.User.AsNoTracking().Where(z => z.Login == changecreds.LoginName).ToListAsync();

            foreach (User u in users)
            {
                string hashed = Hasher.hash(u.Salt, changecreds.OldPassword);
                if (hashed == u.Password)
                {

                    //If the user is inactive they may not login
                    if (!u.Active || !u.AllowLogin)
                    {
                        //respond like bad creds so as not to leak information   
                        await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                        return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
                    }

                    //double check it's the currently logged in User's own User object only
                    //otherwise it's feasible someone could change someone else's password through their own change password form with a mis-type or intentional hack
                    if (u.Id != UserIdFromContext.Id(HttpContext.Items))
                    {
                        await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                        return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
                    }

                    //fetch and update user
                    //Instantiate the business object handler       
                    UserBiz biz = UserBiz.GetBiz(ct, HttpContext);
                    await biz.ChangePasswordAsync(u.Id, changecreds.NewPassword);

                    return NoContent();

                }
            }

            //No users matched, it's a failed login
            //Make a failed pw wait            
            await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);

            return BadRequest(new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
        }


        /// <summary>
        /// Change Password via reset token
        /// </summary>    
        /// <param name="resetcreds"></param>
        /// <returns></returns>       
        [HttpPost("reset-password")]
        [AllowAnonymous]
        public async Task<IActionResult> ResetPassword([FromBody] AuthController.ResetPasswordParam resetcreds)
        {
            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));

            if (!ModelState.IsValid)
            {
                return BadRequest(new ApiErrorResponse(ModelState));
            }

            if (string.IsNullOrWhiteSpace(resetcreds.PasswordResetCode))
            {
                //Make a fail wait                           
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "PasswordResetCode", "Reset code is required"));
            }

            if (string.IsNullOrWhiteSpace(resetcreds.Password))
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "Password", "Password is required"));
            }

            //look for user with this reset code          
            var user = await ct.User.AsNoTracking().Where(z => z.PasswordResetCode == resetcreds.PasswordResetCode).FirstOrDefaultAsync();
            if (user == null)
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "PasswordResetCode", "Reset code not valid"));
            }

            if (string.IsNullOrWhiteSpace(user.PasswordResetCode) || user.PasswordResetCodeExpire == null)
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "PasswordResetCode", "Reset code not valid"));
            }

            //vet the expiry
            var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
            if (user.PasswordResetCodeExpire < utcNow.DateTime)
            {//if reset code expired before now
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "PasswordResetCodeExpire", "Reset code has expired"));
            }
            //Ok, were in, it's all good, accept the new password and update the user record
            UserBiz biz = UserBiz.GetBiz(ct, HttpContext);
            await biz.ChangePasswordAsync(user.Id, resetcreds.Password);
            return NoContent();
        }

        /// <summary>
        /// Generate time limited password reset code for User
        /// and email link to them so they can set their password
        /// 
        /// </summary>
        /// <param name="id">User id</param>
        /// <param name="apiVersion">From route path</param>
        /// <returns>New concurrency code</returns>
        [HttpPost("request-reset-password/{id}")]
        public async Task<IActionResult> SendPasswordResetCode([FromRoute] long id, ApiVersion apiVersion)
        {
            //Note: this is not allowed for an anonymous users because it's only intended for now to work for staff user's who will send the request on behalf of the User
            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
            UserBiz biz = UserBiz.GetBiz(ct, HttpContext);
            if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType))
                return StatusCode(403, new ApiNotAuthorizedResponse());
            if (!ModelState.IsValid)
                return BadRequest(new ApiErrorResponse(ModelState));
            uint res = await biz.SendPasswordResetCode(id);
            if (res == 0)
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return BadRequest(new ApiErrorResponse(biz.Errors));
            }
            else
                return Ok(ApiOkResponse.Response(new
                {
                    concurrency = res
                }));
        }




        /// <summary>
        /// Generate TOTP secret and return for use in auth app
        /// 
        /// </summary>     
        /// <param name="apiVersion">From route path</param>
        /// <returns>Authentication app activation code</returns>
        [HttpGet("totp")]
        public async Task<IActionResult> GenerateAndSendTOTP(ApiVersion apiVersion)
        {
            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
            UserBiz biz = UserBiz.GetBiz(ct, HttpContext);

            if (!ModelState.IsValid)
                return BadRequest(new ApiErrorResponse(ModelState));

            //get user and save the secret
            var UserId = UserIdFromContext.Id(HttpContext.Items);

            var u = await ct.User.FirstOrDefaultAsync(z => z.Id == UserId);
            if (u == null)//should never happen but ?
                return StatusCode(403, new ApiNotAuthorizedResponse());

            //this is to stop someone from messing up someone's login accidentally or maliciously by simply hitting the route logged in as them
            if (u.TwoFactorEnabled)
                return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa already enabled"));

            var tfa = new TwoFactorAuth("Sockeye");
            u.TotpSecret = tfa.CreateSecret(160);
            await ct.SaveChangesAsync();

            //https://github.com/google/google-authenticator/wiki/Key-Uri-Format
            //otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
            //this format tested and works with Google, Microsoft Authy, Duo authenticators
            string payload = $"otpauth://totp/Sockeye:{u.Name}?secret={u.TotpSecret}&issuer=Sockeye&algorithm=SHA1&digits=6&period=30";//NOTE: the 30 here is seconds the totp code is allowed to be used before a new one is required

            QRCodeGenerator qrGenerator = new QRCodeGenerator();
            QRCodeData qrCodeData = qrGenerator.CreateQrCode(payload, QRCodeGenerator.ECCLevel.Q);
            //    Base64QRCode qrCode = new Base64QRCode(qrCodeData);
            //     string qrCodeImageAsBase64 = qrCode.GetGraphic(3);

            PngByteQRCode qpng = new PngByteQRCode(qrCodeData);
            string qrCodeImageAsBase64 = Convert.ToBase64String(qpng.GetGraphic(3));

            return Ok(ApiOkResponse.Response(new
            {
                s = u.TotpSecret,
                qr = qrCodeImageAsBase64
            }));
        }


        /// <summary>
        /// Confirm 2fa ready to use
        /// 
        /// </summary>
        /// <param name="pin">Auth app 6 digit passcode</param>
        /// <param name="apiVersion">From route path</param>
        /// <returns>OK on success</returns>
        [HttpPost("totp-validate")]
        public async Task<IActionResult> ValidateTOTP([FromBody] TFAPinParam pin, ApiVersion apiVersion)
        {
            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));

            if (!ModelState.IsValid)
                return BadRequest(new ApiErrorResponse(ModelState));

            //get user
            var UserId = UserIdFromContext.Id(HttpContext.Items);

            var u = await ct.User.FirstOrDefaultAsync(z => z.Id == UserId);
            if (u == null)//should never happen but ?
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return StatusCode(403, new ApiNotAuthorizedResponse());
            }

            if (u.TwoFactorEnabled)
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa already enabled"));
            }

            if (string.IsNullOrWhiteSpace(u.TotpSecret))
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
                return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa activation code not requested yet (missed a step?)"));
            }

            //ok, something to validate, let's validate it
            var tfa = new TwoFactorAuth("Sockeye");
            var ret = tfa.VerifyCode(u.TotpSecret, pin.Pin.Replace(" ", "").Trim());
            if (ret == true)
            {
                //enable 2fa on user account
                u.TwoFactorEnabled = true;
                await ct.SaveChangesAsync();
            }
            else
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
            }

            return Ok(ApiOkResponse.Response(new
            {
                ok = ret
            }));
        }


        /// <summary>
        /// Disable (turn off) 2fa for  user account
        /// (For other user id requires full privileges)
        /// </summary>  
        /// <param name="id">User id</param>    
        /// <param name="apiVersion">From route path</param>
        /// <returns>OK on success</returns>
        [HttpPost("totp-disable/{id}")]
        public async Task<IActionResult> DisableTOTP([FromRoute] long id, ApiVersion apiVersion)
        {
            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));

            if (!ModelState.IsValid)
                return BadRequest(new ApiErrorResponse(ModelState));

            var UserId = UserIdFromContext.Id(HttpContext.Items);
            if (id != UserId) //doing it on behalf of someone else
            {
                if (!Authorized.HasModifyRole(HttpContext.Items, SockType.User))
                    return StatusCode(403, new ApiNotAuthorizedResponse());
            }


            var u = await ct.User.FirstOrDefaultAsync(z => z.Id == id);
            if (u == null)//should never happen but ?
                return StatusCode(403, new ApiNotAuthorizedResponse());

            u.TotpSecret = null;
            u.TempToken = null;
            u.TwoFactorEnabled = false;
            await ct.SaveChangesAsync();
            return NoContent();
        }


        //generate an internal JWT key for reporting purposes used by corejobnotify to send reports as manager account
        internal static string GenRpt(long overrideLanguageId)
        {
            byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.SOCKEYE_JWT_SECRET);
            var iat = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
            var exp = new DateTimeOffset(DateTime.Now.AddMinutes(ServerBootConfig.SOCKEYE_REPORT_RENDERING_TIMEOUT).ToUniversalTime(), TimeSpan.Zero);
            var payload = new Dictionary<string, object>()
                        {
                            { "exp", exp.ToUnixTimeSeconds().ToString() },//in payload exp must be in unix epoch time per standard
                            { "iss", "rockfish.ayanova.com" },
                            { "id", "1"},
                            { "rpl",overrideLanguageId.ToString() }
                        };
            return Jose.JWT.Encode(payload, secretKey, Jose.JwsAlgorithm.HS256);
        }

        //------------------------------------------------------

        public class CredentialsParam
        {
            [System.ComponentModel.DataAnnotations.Required]
            public string Login { get; set; }
            [System.ComponentModel.DataAnnotations.Required]
            public string Password { get; set; }

        }


        public class ChangePasswordParam
        {
            [System.ComponentModel.DataAnnotations.Required]
            public string LoginName { get; set; }
            [System.ComponentModel.DataAnnotations.Required]
            public string OldPassword { get; set; }
            [System.ComponentModel.DataAnnotations.Required]
            public string NewPassword { get; set; }
            [System.ComponentModel.DataAnnotations.Required]
            public string ConfirmPassword { get; set; }

        }


        public class ResetPasswordParam
        {
            [System.ComponentModel.DataAnnotations.Required]
            public string PasswordResetCode { get; set; }
            [System.ComponentModel.DataAnnotations.Required]
            public string Password { get; set; }
        }

        public class TFAPinParam
        {
            [System.ComponentModel.DataAnnotations.Required]
            public string Pin { get; set; }
            public string TempToken { get; set; }

        }

    }//eoc
}//eons