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 App.Metrics;

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

namespace AyaNova.Api.Controllers
{
    /// <summary>
    /// Authentication controller
    /// </summary>
    [ApiVersion("8.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [Produces("application/json")]
    public class AuthController : Controller
    {
        private readonly AyContext ct;
        private readonly ILogger<AuthController> log;
        private readonly IConfiguration _configuration;
        private readonly ApiServerState serverState;
        private readonly IMetrics metrics;

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="context"></param>
        /// <param name="logger"></param>
        /// <param name="configuration"></param>
        /// <param name="apiServerState"></param>
        /// <param name="Metrics"></param>
        public AuthController(AyContext context, ILogger<AuthController> logger, IConfiguration configuration, ApiServerState apiServerState, IMetrics Metrics)//these two are injected, see startup.cs
        {
            ct = context;
            log = logger;
            _configuration = configuration;
            serverState = apiServerState;
            metrics = Metrics;
        }

        //AUTHENTICATE CREDS 
        //RETURN JWT        

        /// <summary>
        /// Post 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.IsOpenOrOpsOnly)
            {
                return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
            }
            int nFailedAuthDelay = 10000;
#if (DEBUG)
            nFailedAuthDelay = 1;
#endif

            if (string.IsNullOrWhiteSpace(creds.Login) || string.IsNullOrWhiteSpace(creds.Password))
            {
                metrics.Measure.Meter.Mark(MetricsRegistry.FailedLoginMeter);
                //Make a failed pw wait
                await Task.Delay(nFailedAuthDelay);
                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.AsNoTracking().Where(m => m.Login == creds.Login).ToListAsync();

            foreach (User u in users)
            {
                string hashed = Hasher.hash(u.Salt, creds.Password);
                if (hashed == u.Password)
                {
                    //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(ApiErrorCode.API_CLOSED, null, serverState.Reason));
                    }


                    //If the user is inactive they may not login
                    if (!u.Active)
                    {
                        return StatusCode(401, new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, null, "User deactivated"));
                    }

                    //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);

                    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() },
                            { "ayanova/roles", ((int)u.Roles).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);

                    //TODO: Before release double check that security of JWT is still ok:
                    //https://assets.pentesterlab.com/jwt_security_cheatsheet/jwt_security_cheatsheet.pdf
                    //https://github.com/ticarpi/jwt_tool


                    log.LogDebug($"User number \"{u.Id}\" logged in from \"{Util.StringUtil.MaskIPAddress(HttpContext.Connection.RemoteIpAddress.ToString())}\" ok");
                    metrics.Measure.Meter.Mark(MetricsRegistry.SuccessfulLoginMeter);

                    //TODO: This needs to return the authorization roles of the user in the payload and it should all be in the token
                    //and remove the issued, expires id etc so that all that is returned is an encoded token with that info in it
                    return Ok(new ApiOkResponse(new
                    {
                        // ok = 1,
                        // issued = iat,
                        // expires = exp,
                        token = token
                        //,
                        //id = u.Id
                    }));
                }
            }

            //No users matched, it's a failed login
            //Make a failed pw wait  
            metrics.Measure.Meter.Mark(MetricsRegistry.FailedLoginMeter);
            await Task.Delay(nFailedAuthDelay);
            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; }

        }


    }//eoc
}//eons