using FabAccessAPI.Schema;
using S22.Sasl;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Exception = System.Exception;

namespace FabAccessAPI
{
    /// Authentication Identity
    ///
    /// Under the hood a string because the form depends heavily on the method
    public struct AuthCId
    {
        public string Id { get; private set; }

        public AuthCId(string id) : this() { Id = id; }
    }

    /// Authorization Identity
    ///
    /// This identity is internal to FabAccess and completely independent from the authentication
    /// method or source
    public struct AuthZId
    {
        /// Main User ID. Generally an user name or similar
        public string Uid;

        /// Sub user ID. 
        ///
        /// Can change scopes for permissions, e.g. having a +admin account with more permissions than
        /// the default account and +dashboard et.al. accounts that have restricted permissions for
        /// their applications
        public string Subuid;

        /// Realm this account originates.
        ///
        /// The Realm is usually described by a domain name but local policy may dictate an unrelated
        /// mapping
        public string Realm;
    }

    /// Authentication/Authorization user object.
    ///
    /// This struct contains the user as is passed to the actual authentication/authorization
    /// subsystems
    ///
    public struct AuthUser
    {
        /// Contains the Authentication ID used
        ///
        /// The authentication ID is an identifier for the authentication exchange. This is different
        /// than the ID of the user to be authenticated; for example when using x509 the authcid is
        /// the dn of the certificate, when using GSSAPI the authcid is of form `<userid>@<REALM>`
        public AuthCId Authcid;

        /// Contains the Authorization ID
        ///
        /// This is the identifier of the user to *authenticate as*. This in several cases is different
        /// to the `authcid`: 
        /// If somebody wants to authenticate as somebody else, su-style.
        /// If a person wants to authenticate as a higher-permissions account, e.g. foo may set authzid foo+admin
        /// to split normal user and "admin" accounts.
        /// If a method requires a specific authcid that is different from the identifier of the user
        /// to authenticate as, e.g. GSSAPI, x509 client certificates, API TOKEN authentication.
        public AuthZId Authzid;

        /// Contains the authentication method used
        ///
        /// For the most part this is the SASL method
        public string AuthMethod;

        /// Method-specific key-value pairs
        ///
        /// Each method can use their own key-value pairs.
        /// E.g. EXTERNAL encodes the actual method used (x509 client certs, UID/GID for unix sockets,
        /// ...)
        public Dictionary<string, string> Kvs;

    }

    // Authentication has two parts: Granting the authentication itself and then performing the
    // authentication.
    // Granting the authentication checks if 
    // a) the given authcid fits with the given (authMethod, kvs). In general a failure here indicates
    //    a programming failure — the authcid come from the same source as that tuple
    // b) the given authcid may authenticate as the given authzid. E.g. if a given client certificate
    //    has been configured for that user, if a GSSAPI user maps to a given user, 
    public enum AuthError
    {
        /// Authentication ID is bad/unknown/..
        BadAuthcid,
        /// Authorization ID is unknown/..
        BadAuthzid,
        /// Authorization ID is not of form user+uid@realm
        MalformedAuthzid,
        /// User may not use that authorization id
        NotAllowedAuthzid,

    }

    public class UnauthorizedException : Exception { }
    public class UnsupportedMechanismException : Exception { }

    /// <summary>
    /// THIS IS VERY INCOMPLETE!
    /// </summary>
    public class Auth
    {
        #region Log
        private static readonly log4net.ILog _Log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        #endregion

        private IAuthenticationSystem _authCap;
        public Auth(IAuthenticationSystem authCap)
        {
            _authCap = authCap;
        }

        public Task<IReadOnlyList<string>> GetMechanisms()
        {
            return _authCap.Mechanisms();
        }

        public async Task<bool> Authenticate(string mech, Dictionary<string, object> properties)
        {

            var m = SaslFactory.Create(mech);
            foreach (KeyValuePair<string, object> entry in properties)
            {
                m.Properties.Add(entry.Key, entry.Value);
            }

            var initialResponse = new Request.initialResponse();
            if (m.HasInitial)
            {
                initialResponse.Initial = m.GetResponse(new byte[0]);
            }

            var req = new Request
            {
                Mechanism = m.Name,
                InitialResponse = initialResponse
            };

            var resp = await _authCap.Start(req);
            while (!m.IsCompleted)
            {
                if (resp.which == Response.WHICH.Challence)
                {
                    var additional = m.GetResponse(resp.Challence.ToArray());
                    resp = await _authCap.Step(additional);
                }
                else
                {
                    break;
                }
            }

            if (resp.which == Response.WHICH.Outcome)
            {
                if (resp.Outcome.Result == Response.Result.successful)
                {
                    return true;
                }
                else
                {
                    //TODO: Provide meaningful info about auth failure
                    return false;
                }
            }

            return false;
        }
    }
}