// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;

namespace System.DirectoryServices.AccountManagement
{
    [DirectoryRdnPrefix("CN")]
    public class AuthenticablePrincipal : Principal
    {
        //
        // Public Properties
        //

        // Enabled property
        private bool _enabled;          // the actual property value
        private LoadState _enabledChanged = LoadState.NotSet;   // change-tracking

        public Nullable<bool> Enabled
        {
            get
            {
                // Make sure we're not disposed or deleted.  Although HandleGet/HandleSet will check this,
                // we need to check these before we do anything else.
                CheckDisposedOrDeleted();

                // Different stores have different defaults as to the Enabled setting
                // (AD: creates disabled by default; SAM: creates enabled by default).
                // So if the principal is unpersisted (and thus we may not know what store it's
                // going to end up in), we'll just return null unless they previously
                // set an explicit value.
                if (this.unpersisted && (_enabledChanged != LoadState.Changed))
                {
                    GlobalDebug.WriteLineIf(
                                    GlobalDebug.Info,
                                    "AuthenticablePrincipal",
                                    "Enabled: returning null, unpersisted={0}, enabledChanged={1}",
                                    this.unpersisted,
                                    _enabledChanged);

                    return null;
                }

                return HandleGet<bool>(ref _enabled, PropertyNames.AuthenticablePrincipalEnabled, ref _enabledChanged);
            }

            set
            {
                // Make sure we're not disposed or deleted.  Although HandleGet/HandleSet will check this,
                // we need to check these before we do anything else.
                CheckDisposedOrDeleted();

                // We don't want to let them set a null value.
                if (!value.HasValue)
                    throw new ArgumentNullException(nameof(value));

                HandleSet<bool>(ref _enabled, value.Value, ref _enabledChanged,
                                  PropertyNames.AuthenticablePrincipalEnabled);
            }
        }

        //
        // AccountInfo-related properties/methods
        //

        private AccountInfo _accountInfo;

        private AccountInfo AccountInfo
        {
            get
            {
                // Make sure we're not disposed or deleted.
                CheckDisposedOrDeleted();

                if (_accountInfo == null)
                {
                    GlobalDebug.WriteLineIf(GlobalDebug.Info, "AuthenticablePrincipal", "AccountInfo: creating new AccountInfo");
                    _accountInfo = new AccountInfo(this);
                }

                return _accountInfo;
            }
        }

        public Nullable<DateTime> AccountLockoutTime
        {
            get { return this.AccountInfo.AccountLockoutTime; }
        }

        public Nullable<DateTime> LastLogon
        {
            get { return this.AccountInfo.LastLogon; }
        }

        public PrincipalValueCollection<string> PermittedWorkstations
        {
            get { return this.AccountInfo.PermittedWorkstations; }
        }

        public byte[] PermittedLogonTimes
        {
            get { return this.AccountInfo.PermittedLogonTimes; }
            set { this.AccountInfo.PermittedLogonTimes = value; }
        }

        public Nullable<DateTime> AccountExpirationDate
        {
            get { return this.AccountInfo.AccountExpirationDate; }
            set { this.AccountInfo.AccountExpirationDate = value; }
        }

        public bool SmartcardLogonRequired
        {
            get { return this.AccountInfo.SmartcardLogonRequired; }
            set { this.AccountInfo.SmartcardLogonRequired = value; }
        }

        public bool DelegationPermitted
        {
            get { return this.AccountInfo.DelegationPermitted; }
            set { this.AccountInfo.DelegationPermitted = value; }
        }

        public int BadLogonCount
        {
            get { return this.AccountInfo.BadLogonCount; }
        }

        public string HomeDirectory
        {
            get { return this.AccountInfo.HomeDirectory; }
            set { this.AccountInfo.HomeDirectory = value; }
        }

        public string HomeDrive
        {
            get { return this.AccountInfo.HomeDrive; }
            set { this.AccountInfo.HomeDrive = value; }
        }

        public string ScriptPath
        {
            get { return this.AccountInfo.ScriptPath; }
            set { this.AccountInfo.ScriptPath = value; }
        }

        public bool IsAccountLockedOut()
        {
            return this.AccountInfo.IsAccountLockedOut();
        }

        public void UnlockAccount()
        {
            this.AccountInfo.UnlockAccount();
        }

        //
        // PasswordInfo-related properties/methods
        //

        private PasswordInfo _passwordInfo;

        private PasswordInfo PasswordInfo
        {
            get
            {
                // Make sure we're not disposed or deleted.
                CheckDisposedOrDeleted();

                if (_passwordInfo == null)
                {
                    GlobalDebug.WriteLineIf(GlobalDebug.Info, "AuthenticablePrincipal", "PasswordInfo: creating new PasswordInfo");
                    _passwordInfo = new PasswordInfo(this);
                }

                return _passwordInfo;
            }
        }

        public Nullable<DateTime> LastPasswordSet
        {
            get { return this.PasswordInfo.LastPasswordSet; }
        }

        public Nullable<DateTime> LastBadPasswordAttempt
        {
            get { return this.PasswordInfo.LastBadPasswordAttempt; }
        }

        public bool PasswordNotRequired
        {
            get { return this.PasswordInfo.PasswordNotRequired; }
            set { this.PasswordInfo.PasswordNotRequired = value; }
        }

        public bool PasswordNeverExpires
        {
            get { return this.PasswordInfo.PasswordNeverExpires; }
            set { this.PasswordInfo.PasswordNeverExpires = value; }
        }

        public bool UserCannotChangePassword
        {
            get { return this.PasswordInfo.UserCannotChangePassword; }
            set { this.PasswordInfo.UserCannotChangePassword = value; }
        }

        public bool AllowReversiblePasswordEncryption
        {
            get { return this.PasswordInfo.AllowReversiblePasswordEncryption; }
            set { this.PasswordInfo.AllowReversiblePasswordEncryption = value; }
        }

        internal AdvancedFilters rosf;

        public virtual AdvancedFilters AdvancedSearchFilter
        {
            get
            {
                return rosf;
            }
        }

        public void SetPassword(string newPassword)
        {
            this.PasswordInfo.SetPassword(newPassword);
        }

        public void ChangePassword(string oldPassword, string newPassword)
        {
            this.PasswordInfo.ChangePassword(oldPassword, newPassword);
        }

        public void ExpirePasswordNow()
        {
            this.PasswordInfo.ExpirePasswordNow();
        }

        public void RefreshExpiredPassword()
        {
            this.PasswordInfo.RefreshExpiredPassword();
        }

        // Certificates property
        private X509Certificate2Collection _certificates = new X509Certificate2Collection();
        private readonly List<string> _certificateOriginalThumbprints = new List<string>();
        private LoadState _X509Certificate2CollectionLoaded = LoadState.NotSet;

        public X509Certificate2Collection Certificates
        {
            get
            {
                return HandleGet<X509Certificate2Collection>(ref _certificates,
                                    PropertyNames.AuthenticablePrincipalCertificates, ref _X509Certificate2CollectionLoaded);
            }
        }

        //
        // Public Methods
        //
        public static PrincipalSearchResult<AuthenticablePrincipal> FindByLockoutTime(PrincipalContext context, DateTime time, MatchType type)
        {
            return FindByLockoutTime<AuthenticablePrincipal>(context, time, type);
        }

        public static PrincipalSearchResult<AuthenticablePrincipal> FindByLogonTime(PrincipalContext context, DateTime time, MatchType type)
        {
            return FindByLogonTime<AuthenticablePrincipal>(context, time, type);
        }

        public static PrincipalSearchResult<AuthenticablePrincipal> FindByExpirationTime(PrincipalContext context, DateTime time, MatchType type)
        {
            return FindByExpirationTime<AuthenticablePrincipal>(context, time, type);
        }

        public static PrincipalSearchResult<AuthenticablePrincipal> FindByBadPasswordAttempt(PrincipalContext context, DateTime time, MatchType type)
        {
            return FindByBadPasswordAttempt<AuthenticablePrincipal>(context, time, type);
        }

        public static PrincipalSearchResult<AuthenticablePrincipal> FindByPasswordSetTime(PrincipalContext context, DateTime time, MatchType type)
        {
            return FindByPasswordSetTime<AuthenticablePrincipal>(context, time, type);
        }

        //
        // Protected implementations
        //

        protected static PrincipalSearchResult<T> FindByLockoutTime<T>(PrincipalContext context, DateTime time, MatchType type)
        {
            CheckFindByArgs(context, time, type, typeof(T));

            return new PrincipalSearchResult<T>(context.QueryCtx.FindByLockoutTime(time, type, typeof(T)));
        }
        protected static PrincipalSearchResult<T> FindByLogonTime<T>(PrincipalContext context, DateTime time, MatchType type)
        {
            CheckFindByArgs(context, time, type, typeof(T));

            return new PrincipalSearchResult<T>(context.QueryCtx.FindByLogonTime(time, type, typeof(T)));
        }
        protected static PrincipalSearchResult<T> FindByExpirationTime<T>(PrincipalContext context, DateTime time, MatchType type)
        {
            CheckFindByArgs(context, time, type, typeof(T));

            return new PrincipalSearchResult<T>(context.QueryCtx.FindByExpirationTime(time, type, typeof(T)));
        }
        protected static PrincipalSearchResult<T> FindByBadPasswordAttempt<T>(PrincipalContext context, DateTime time, MatchType type)
        {
            CheckFindByArgs(context, time, type, typeof(T));

            return new PrincipalSearchResult<T>(context.QueryCtx.FindByBadPasswordAttempt(time, type, typeof(T)));
        }
        protected static PrincipalSearchResult<T> FindByPasswordSetTime<T>(PrincipalContext context, DateTime time, MatchType type)
        {
            CheckFindByArgs(context, time, type, typeof(T));

            return new PrincipalSearchResult<T>(context.QueryCtx.FindByPasswordSetTime(time, type, typeof(T)));
        }

        //
        // Private implementation
        //
        protected internal AuthenticablePrincipal(PrincipalContext context)
        {
            if (context == null)
                throw new ArgumentException(SR.NullArguments);

            this.ContextRaw = context;
            this.unpersisted = true;
            this.rosf = new AdvancedFilters(this);
        }

        protected internal AuthenticablePrincipal(PrincipalContext context, string samAccountName, string password, bool enabled) : this(context)
        {
            if (samAccountName != null)
            {
                this.SamAccountName = samAccountName;
            }

            if (password != null)
            {
                this.SetPassword(password);
            }

            this.Enabled = enabled;
        }

        internal static AuthenticablePrincipal MakeAuthenticablePrincipal(PrincipalContext ctx)
        {
            AuthenticablePrincipal ap = new AuthenticablePrincipal(ctx);
            ap.unpersisted = false;

            return ap;
        }

        private static void CheckFindByArgs(PrincipalContext context, DateTime time, MatchType type, Type subtype)
        {
            if ((subtype != typeof(AuthenticablePrincipal)) &&
                 (!subtype.IsSubclassOf(typeof(AuthenticablePrincipal))))
                throw new ArgumentException(SR.AuthenticablePrincipalMustBeSubtypeOfAuthPrinc);

            if (context == null)
                throw new ArgumentNullException(nameof(context));

            if (subtype == null)
                throw new ArgumentNullException(nameof(subtype));
        }

        //
        // Load/Store
        //

        //
        // Loading with query results
        //

        internal override void LoadValueIntoProperty(string propertyName, object value)
        {
            switch (propertyName)
            {
                case PropertyNames.AuthenticablePrincipalCertificates:
                    LoadCertificateCollection((List<byte[]>)value);
                    RefreshOriginalThumbprintList();
                    _X509Certificate2CollectionLoaded = LoadState.Loaded;
                    break;

                case PropertyNames.AuthenticablePrincipalEnabled:
                    _enabled = (bool)value;
                    _enabledChanged = LoadState.Loaded;
                    break;

                default:
                    if (propertyName.StartsWith(PropertyNames.AcctInfoPrefix, StringComparison.Ordinal))
                    {
                        // If this is the first AccountInfo attribute we're loading,
                        // we'll need to create the AccountInfo to hold it
                        _accountInfo ??= new AccountInfo(this);

                        _accountInfo.LoadValueIntoProperty(propertyName, value);
                    }
                    else if (propertyName.StartsWith(PropertyNames.PwdInfoPrefix, StringComparison.Ordinal))
                    {
                        // If this is the first PasswordInfo attribute we're loading,
                        // we'll need to create the PasswordInfo to hold it
                        _passwordInfo ??= new PasswordInfo(this);

                        _passwordInfo.LoadValueIntoProperty(propertyName, value);
                    }
                    else
                    {
                        base.LoadValueIntoProperty(propertyName, value);
                    }
                    break;
            }
        }

        //
        // Getting changes to persist (or to build a query from a QBE filter)
        //

        // Given a property name, returns true if that property has changed since it was loaded, false otherwise.
        internal override bool GetChangeStatusForProperty(string propertyName)
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "AuthenticablePrincipal", "GetChangeStatusForProperty: name=" + propertyName);

            switch (propertyName)
            {
                case PropertyNames.AuthenticablePrincipalCertificates:
                    return HasCertificateCollectionChanged();

                case PropertyNames.AuthenticablePrincipalEnabled:
                    return _enabledChanged == LoadState.Changed;

                default:

                    // Check if the property is supported by AdvancedFilter class.
                    // If writeable properties are added to the rosf class then we will need
                    // to add some type of tag to the property names to differentiate them here
                    bool? val = rosf.GetChangeStatusForProperty(propertyName);

                    if (val.HasValue)
                        return val.Value;

                    if (propertyName.StartsWith(PropertyNames.AcctInfoPrefix, StringComparison.Ordinal))
                    {
                        if (_accountInfo == null)
                            return false;

                        return _accountInfo.GetChangeStatusForProperty(propertyName);
                    }
                    else if (propertyName.StartsWith(PropertyNames.PwdInfoPrefix, StringComparison.Ordinal))
                    {
                        if (_passwordInfo == null)
                            return false;

                        return _passwordInfo.GetChangeStatusForProperty(propertyName);
                    }
                    else
                    {
                        return base.GetChangeStatusForProperty(propertyName);
                    }
            }
        }

        // Given a property name, returns the current value for the property.
        internal override object GetValueForProperty(string propertyName)
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "AuthenticablePrincipal", "GetValueForProperty: name=" + propertyName);

            switch (propertyName)
            {
                case PropertyNames.AuthenticablePrincipalCertificates:
                    return _certificates;

                case PropertyNames.AuthenticablePrincipalEnabled:
                    return _enabled;

                default:

                    object val = rosf.GetValueForProperty(propertyName);

                    if (null != val)
                        return val;

                    if (propertyName.StartsWith(PropertyNames.AcctInfoPrefix, StringComparison.Ordinal))
                    {
                        if (_accountInfo == null)
                        {
                            // Should never happen, since GetChangeStatusForProperty returned false
                            Debug.Fail("AuthenticablePrincipal.GetValueForProperty(AcctInfo): shouldn't be here");
                            throw new InvalidOperationException();
                        }

                        return _accountInfo.GetValueForProperty(propertyName);
                    }
                    else if (propertyName.StartsWith(PropertyNames.PwdInfoPrefix, StringComparison.Ordinal))
                    {
                        if (_passwordInfo == null)
                        {
                            // Should never happen, since GetChangeStatusForProperty returned false
                            Debug.Fail("AuthenticablePrincipal.GetValueForProperty(PwdInfo): shouldn't be here");
                            throw new InvalidOperationException();
                        }

                        return _passwordInfo.GetValueForProperty(propertyName);
                    }
                    else
                    {
                        return base.GetValueForProperty(propertyName);
                    }
            }
        }

        // Reset all change-tracking status for all properties on the object to "unchanged".
        internal override void ResetAllChangeStatus()
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "AuthenticablePrincipal", "ResetAllChangeStatus");

            _enabledChanged = (_enabledChanged == LoadState.Changed) ? LoadState.Loaded : LoadState.NotSet;

            RefreshOriginalThumbprintList();

            _accountInfo?.ResetAllChangeStatus();

            _passwordInfo?.ResetAllChangeStatus();

            rosf.ResetAllChangeStatus();

            base.ResetAllChangeStatus();
        }

        //
        // Certificate support routines
        //

        // Given a list of certs (expressed as byte[]s), loads them into
        // the certificate collection
        private void LoadCertificateCollection(List<byte[]> certificatesToLoad)
        {
            // To support reload
            _certificates.Clear();
            Debug.Assert(_certificates.Count == 0);

            GlobalDebug.WriteLineIf(GlobalDebug.Info, "AuthenticablePrincipal", "LoadCertificateCollection: loading {0} certs", certificatesToLoad.Count);

            foreach (byte[] rawCert in certificatesToLoad)
            {
                try
                {
                    _certificates.Import(rawCert);
                }
                catch (System.Security.Cryptography.CryptographicException)
                {
                    // skip the invalid certificate
                    GlobalDebug.WriteLineIf(GlobalDebug.Warn, "AuthenticablePrincipal", "LoadCertificateCollection: skipped bad cert");
                    continue;
                }
            }
        }

        // Regenerates the certificateOriginalThumbprints list based on what's
        // currently in the certificate collection
        private void RefreshOriginalThumbprintList()
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "AuthenticablePrincipal", "RefreshOriginalThumbprintList: resetting thumbprints");

            _certificateOriginalThumbprints.Clear();

            foreach (X509Certificate2 certificate in _certificates)
            {
                _certificateOriginalThumbprints.Add(certificate.Thumbprint);
            }
        }

        // Returns true if the certificate collection has changed since the last time
        // certificateOriginalThumbprints was refreshed (i.e., since the last time
        // RefreshOriginalThumbprintList was called)
        private bool HasCertificateCollectionChanged()
        {
            // If the size isn't the same, the collection has certainly changed
            if (_certificates.Count != _certificateOriginalThumbprints.Count)
            {
                GlobalDebug.WriteLineIf(
                            GlobalDebug.Info,
                            "AuthenticablePrincipal",
                            "HasCertificateCollectionChanged: original count {0}, current count{1}",
                            _certificateOriginalThumbprints.Count,
                            _certificates.Count);
                return true;
            }

            // Make a copy of the thumbprint list, so we can alter the copy without effecting the original.
            List<string> remainingOriginalThumbprints = new List<string>(_certificateOriginalThumbprints);

            foreach (X509Certificate2 certificate in _certificates)
            {
                string thumbprint = certificate.Thumbprint;

                // If we found a cert whose thumbprint wasn't in the thumbprints list,
                // it was inserted --> collection has changed
                if (!remainingOriginalThumbprints.Contains(thumbprint))
                {
                    GlobalDebug.WriteLineIf(GlobalDebug.Info, "AuthenticablePrincipal", "RefreshOriginalThumbprintList: found inserted cert");
                    return true;
                }

                // We remove the thumbprint from the list so that if, for some reason, they inserted
                // a duplicate of a certificate that was already in the list, we'll detect it as an insert
                // when we encounter that cert a second time
                remainingOriginalThumbprints.Remove(thumbprint);
            }

            // If a certificate was deleted, there are two possibilities:
            //  (1) The removal caused the size to change.  We'll have caught it above and returned true.
            //  (2) The size didn't change (because there was also an insert).  We'll have caught the insert
            //      above and returned true.  Note that even if they insert a duplicate of a cert that was
            //      already in the collection, we'll catch it because we remove the thumbprint from the
            //      local copy of the thumbprint list each time we use that thumbprint.

            Debug.Assert(remainingOriginalThumbprints.Count == 0);
            return false;
        }
    }
}
