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

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

namespace AyaNova.Api.Controllers
{
    /// <summary>
    /// Authentication controller
    /// </summary>
    [ApiController]
    [ApiVersion("8.0")]
    [Route("api/v{version:apiVersion}/auth")]
    [Produces("application/json")]
    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 = 7;

        /// <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 AyaNova 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]
        public async Task<IActionResult> PostCreds([FromBody] AuthController.CredentialsParam creds) //if was a json body then //public JsonResult PostCreds([FromBody] string login, [FromBody] string password)
        {
            //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 && AyaNova.Core.License.ActiveKey.KeyDoesNotNeedAttention)
            {
                return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
            }

        

#if (DEBUG)
          

            #region TESTING

            //TEST JWT's with various flaws for testing purposes:
            if (creds.Login == "INTEGRATION_TEST")
            {

                //build the key (JWT set in startup.cs)
                byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_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
                var exp = new DateTimeOffset(DateTime.Now.AddDays(30).ToUniversalTime(), TimeSpan.Zero);
                string Issuer = "ayanova.com";
                var Algorithm = Jose.JwsAlgorithm.HS256;

                //Pre JWT creation test payloads
                switch (creds.Password)
                {
                    case "EXPIRED":
                        exp = new DateTimeOffset(DateTime.Now.AddDays(-30).ToUniversalTime(), TimeSpan.Zero);
                        break;
                    case "WRONG_ISSUER":
                        Issuer = "Bogus";
                        break;
                    case "NO_ALGORITHM":
                        Algorithm = Jose.JwsAlgorithm.none;
                        break;
                    case "WRONG_SECRET":
                        secretKey = System.Text.Encoding.ASCII.GetBytes("xxxxxxThisIsObviouslyWrongxxxxxx");
                        break;
                }


                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", Issuer },
                            { "id", "1" }
                        };
                string TestToken = Jose.JWT.Encode(payload, secretKey, Algorithm);


                //Post JWT creation test payloads
                switch (creds.Password)
                {
                    case "TRUNCATED_SIGNATURE":
                        TestToken = TestToken.Substring(0, TestToken.Length - 3);
                        break;
                    case "TRANSPOSE_SIGNATURE":
                        //Transpose two characters in the signature
                        int len = TestToken.Length;
                        var Transposed = TestToken.Substring(0, len - 5) + TestToken[len - 4] + TestToken[len - 5] + TestToken.Substring(len - 3, 3);
                        TestToken = Transposed;
                        break;
                }


                return Ok(ApiOkResponse.Response(new
                {
                    token = TestToken,
                    name = "SuperUser Account - TESTING",
                    roles = "0"
                }));

            }

            #endregion testing
#endif

            if (string.IsNullOrWhiteSpace(creds.Login) || string.IsNullOrWhiteSpace(creds.Password))
            {
                //Make a failed pw wait
                await Task.Delay(AyaNova.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).ToListAsync();

            foreach (User u in users)
            {
                string hashed = Hasher.hash(u.Salt, creds.Password);
                if (hashed == u.Password)
                {
                    //Valid password, user is effectively authorized at this point

                    //check if server closed
                    //if it is it means we got here only because there is no license 
                    //and only *the* SuperUser account can login now
                    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 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 (serverState.IsOpsOnly &&
                    !u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminFull) &&
                    !u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminLimited))
                    {
                        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.AYANOVA_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
                    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.DateTime;

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

                    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", "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 Ok(ApiOkResponse.Response(new
                    {
                        token = token,
                        name = u.Name,
                        usertype = u.UserType,
                        roles = ((int)u.Roles).ToString(),
                        dlt = DownloadToken
                    }));
                }
            }

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


        /// <summary>
        /// Change Password
        /// </summary>    
        /// <param name="changecreds"></param>
        /// <returns></returns>       
        [HttpPost("changepassword")]
        public async Task<IActionResult> ChangePassword([FromBody] AuthController.ChangePasswordParam changecreds)
        {
            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));

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



            int nFailedAuthDelay = 3000;//should be just long enough to make brute force a hassle but short enough to not annoy people who just mistyped their creds to login


            if (string.IsNullOrWhiteSpace(changecreds.OldPassword) || string.IsNullOrWhiteSpace(changecreds.LoginName))
            {
                //Make a failed pw wait
                await Task.Delay(nFailedAuthDelay);
                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)
                    {
                        //respond like bad creds so as not to leak information                         
                        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(nFailedAuthDelay);

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

            //return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
        }

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

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

        }


    }//eoc
}//eons