namespace DSInternals.PowerShell.Commands
{
    using DSInternals.Common;
    using DSInternals.Common.Interop;
    using DSInternals.PowerShell.Properties;
    using DSInternals.SAM;
    using DSInternals.SAM.Interop;
    using System.ComponentModel;
    using System.Management.Automation;
    using System.Security.Principal;

    [Cmdlet(VerbsCommon.Set, "SamAccountPasswordHash")]
    [OutputType("None")]
    public class SetSamAccountPasswordHashCommand : SamCommandBase
    {
        private const string ParameterSetBySid = "BySid";
        private const string ParameterSetByLogonName = "ByLogonName";
        // TODO: Support -Force parameter

        #region Parameters

        [Parameter(
            HelpMessage = @"Specify user's login.",
            Mandatory = true,
            ValueFromPipelineByPropertyName = true,
            ParameterSetName = ParameterSetByLogonName
        )]
        [ValidateNotNullOrEmpty]
        public string SamAccountName
        {
            get;
            set;
        }

        [Parameter(
            HelpMessage = @"Specify the user's domain.",
            Mandatory = true,
            ValueFromPipelineByPropertyName = true,
            ParameterSetName = ParameterSetByLogonName
        )]
        [ValidateNotNullOrEmpty]
        public string Domain
        {
            get;
            set;
        }

        [Parameter(
            HelpMessage = @"Specify user SID.",
            Mandatory = true,
            ValueFromPipelineByPropertyName = true,
            ParameterSetName = ParameterSetBySid
        )]
        [ValidateNotNull]
        public SecurityIdentifier Sid
        {
            get;
            set;
        }

        [Parameter(
            HelpMessage = "Specify a new NT password hash value in hexadecimal format.",
            Mandatory = true,
            ValueFromPipelineByPropertyName = true
        )]
        [ValidateNotNull]
        [ValidateCount(DSInternals.Common.Cryptography.NTHash.HashSize, DSInternals.Common.Cryptography.NTHash.HashSize)]
        [AcceptHexString]
        public byte[] NTHash
        {
            get;
            set;
        }

        [Parameter(
            HelpMessage = "Specify a new LM password hash value in hexadecimal format.",
            Mandatory = false,
            ValueFromPipelineByPropertyName = true
        )]
        [ValidateNotNull]
        [ValidateCount(DSInternals.Common.Cryptography.LMHash.HashSize, DSInternals.Common.Cryptography.LMHash.HashSize)]
        [AcceptHexString]
        public byte[] LMHash
        {
            get;
            set;
        }

        #endregion Parameters

        #region Cmdlet Overrides

        protected override void ProcessRecord()
        {
            try
            {
                /* Retrieve domain SID of the current user. */
                SecurityIdentifier domainSid;
                // TODO: Domain name to SID translation Cache
                // TODO: Get default domain from server
                switch(this.ParameterSetName)
                {
                    case ParameterSetByLogonName:
                        // TODO: Extract as resource:
                        this.WriteVerbose(string.Format("Setting password hash on account {0}\\{1}.", this.Domain, this.SamAccountName));
                        
                        if (this.Domain.Contains("."))
                        {
                            // This is not a hard check, because dots are actually allowed in NetBIOS names, although not recommended.
                            // TODO: Extract as a resource
                            this.WriteWarning("The domain name supplied appears to be a DNS name instead of NetBIOS name.");
                        }

                        // We need to translate domain name to SID:
                        domainSid = this.SamServer.LookupDomain(this.Domain);
                        break;
                    case ParameterSetBySid:
                        if (!this.Sid.IsAccountSid())
                        {
                            // Allow the processing to continue on this error:
                            // TODO: Extract as resource:
                            PSArgumentException ex = new PSArgumentException("The SID provided is not a account SID.", "Sid");
                            this.WriteError(ex.ErrorRecord);
                        }
                        // TODO: Extract as resource:
                        this.WriteVerbose(string.Format("Setting password hash on account {0}.", this.Sid));
                        // We already know the SID:
                        domainSid = this.Sid.AccountDomainSid;
                        break;
                    default:
                        // This should never happen:
                        throw new PSInvalidOperationException(Resources.InvalidParameterSetMessage);
                }
                /* Connect to the domain. */
                using (SamDomain domain = this.SamServer.OpenDomain(domainSid, SamDomainAccessMask.Lookup))
                {
                    /* Retrieve RID of the current user. */
                    int userId = (this.ParameterSetName == ParameterSetBySid) ? this.Sid.GetRid() : domain.LookupUser(this.SamAccountName);
                    /* Open the user account and reset password: */
                    using (SamUser user = domain.OpenUser(userId, SamUserAccessMask.ForcePasswordChange))
                    {
                        user.SetPasswordHash(this.NTHash, this.LMHash);
                    }
                }
            }
            catch (Win32Exception ex)
            {
                ErrorCategory category = ((Win32ErrorCode)ex.NativeErrorCode).ToPSCategory();
                object identity = (this.Sid != null) ? this.Sid.ToString() : this.SamAccountName;
                ErrorRecord error = new ErrorRecord(ex, "WinAPIErrorProcess", category, identity);
                // Allow the processing to continue on this error:
                this.WriteError(error);
            }
        }

        #endregion Cmdlet Overrides
    }
}