MSOLLicenseManagement.psm1

#############################################################################################
# DISCLAIMER: #
# #
# THE SAMPLE SCRIPTS ARE NOT SUPPORTED UNDER ANY MICROSOFT STANDARD SUPPORT #
# PROGRAM OR SERVICE. THE SAMPLE SCRIPTS ARE PROVIDED AS IS WITHOUT WARRANTY #
# OF ANY KIND. MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING, WITHOUT #
# LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR A PARTICULAR #
# PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLE SCRIPTS #
# AND DOCUMENTATION REMAINS WITH YOU. IN NO EVENT SHALL MICROSOFT, ITS AUTHORS, OR #
# ANYONE ELSE INVOLVED IN THE CREATION, PRODUCTION, OR DELIVERY OF THE SCRIPTS BE LIABLE #
# FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS #
# PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) #
# ARISING OUT OF THE USE OF OR INABILITY TO USE THE SAMPLE SCRIPTS OR DOCUMENTATION, #
# EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES #
#############################################################################################

#==========================
# Utility Functions
#==========================
# Writes output to a log file with a time date stamp
Function Write-Log 
{
    Param ([string]$string)
    
    # Get the current date
    [string]$date = Get-Date -Format G
    # Write everything to our log file
    ( "[" + $date + "] - " + $string) | Out-File -FilePath $LogFile -Append
    
    # If NonInteractive true then suppress host output
    if (!($NonInteractive)){
        ( "[" + $date + "] - " + $string) | Write-Host
    }
}

# Make sure that we are connected to the MSOL Server if not connect us
#TODO: Need to move from $error.clear() to try-catch
Function Test-MSOLServiceConnection
{
    # Set the Error Action Prefernce to stop
    $ErrorActionPreference = "Stop"

    # Make sure that the MSOL Module is installed
    if ($null -eq (Get-Module -ListAvailable MSOnline))
    {
        Write-Error "MSOL Module Not installed. Please install from https://docs.microsoft.com/en-us/office365/enterprise/powershell/connect-to-office-365-powershell" -ErrorAction Stop
    }

    # Make sure we are connected to the MSOL service
    $error.clear()
    
    # Call Get-msolaccountsku if we are not connected we will get an error if we are we will not
    $null = Get-MSOLAccountSKU -ErrorVariable error -ErrorAction SilentlyContinue

    # Check to see if we threw an error
    if ($error.count -gt 0) 
    {
        # If we have the expected error for not being connected the call connect-msolservice
        if ($error.Exception -like "*You must call the Connect-MsolService cmdlet*")
        {
            Write-Log "Not Connected to MSOLService calling Connect-MSOLService"
            import-module MSOnline
            Connect-MsolService
        }
        # If we get any other error then throw and error and stop
        else
        {
            Write-log ("Unexpected Error encountered" + $error)
            Write-Error "Unexpected Error stopping execution" -ErrorAction Stop
        }
    }
    else
    {
        # Do nothing because we are connected
    }
}

# Write out the current user collection for the purpose of being able to resume
Function Write-UserSet {}

# Updates a progress bar
Function Update-Progress 
{
    Param 
    (
        [Parameter(Mandatory=$true)]
        [int]$CurrentCount,
        [Parameter(Mandatory=$true)]
        [int]$MaxCount,
        [Parameter(Mandatory=$true)]
        [string]$Message
    )

    # If currentcount = maxcount we are done so we need to kill the progress bar
    if ($CurrentCount -ge $MaxCount)
    {
        Write-Progress -Completed -Activity "User Modification"
    }
    # Every 25 update the progress bar
    elseif ($CurrentCount%25 -eq 0)
    {

        [int]$Percent = (($CurrentCount/$MaxCount)*100)
        [string]$Operation = ([string]$Percent + "% " + $Message)
        Write-Progress -Activity "User Modification" -CurrentOperation $Operation -PercentComplete $Percent
    }
    else
    {
        # Nothing to do if not divisible by 25
    }
    

}

# Test the logfile path to ensure the dir exists and that a file was provided
Function Test-LogPath
{
    param (
        # Log File e.g. c:\temp\out.log
        [Parameter(Mandatory)]
        [string]
        $LogFile
    )

    #Get the parent of the provided path
    $parent = split-path $LogFile -Parent

    # If the parent is null then the user provided the root of a drive for the log file
    if ([string]::IsNullOrEmpty($parent))
    {
        Write-Error -Message "Log File path appears to be a drive root. Please provide a folder and file name with -logfile e.g. C:\Temp\out.log" -ErrorAction Stop
        break
    }

    # Verify that the parent folder exists since we won't create it if it doesn't
    if (!(test-path $parent))
    {
        Write-Error -Message ("Path not found " + $parent + " ;please provide a valid path.") -ErrorAction Stop
        break
    }

    # Check to make sure the path contains a file with an extension
    if ((($LogFile).split('\'))[-1] -notmatch '\.')
    {
        Write-Error -Message ("Log file path does not appear to contain a file extension. Please provide a path and file name e.g. c:\temp\out.log")
        break
    }
    
    # Check to see if the value passed in a container (folder) if so we need to abort
    if (test-path $LogFile -PathType Container)
    {
        Write-Error  -Message "Log File Path appears to be a folder. Please provide a file name with -logfile e.g. C:\Temp\out.log" -ErrorAction Stop
        break
    }

}

# Validate we have provided a valid SKU or collect one if we have provided none
Function Select-SKU
{
    param 
    (
        [string]$SKUToCheck,
        [string]$Message

    )    
    
    # If we don't have a value for the SKU then we need to ask for one.
    if ([string]::IsNullOrEmpty($SKUToCheck)) {
        
        Write-Log ("No SKU value provided. Please select from the availible account SKUs")
    
        # Setup a counter
        $i = 1
        
        # Write out the provided message
        Write-Host "`n$Message`n"
        
        
        # Get all of the SKUs and display them with a "selection number"
        $TenantSKU = Get-MSOLAccountSKU
        ForEach ($sku in $TenantSKU) 
        {    
            Write-Host ([string]$i + " - " + $sku.SKUPartNumber)    
            $i++
        }
        
        # Collect the selected item from the user
        [int]$Selected = 0
        While (($Selected -ge $i) -or ($Selected -le 0) -or ($null -eq $Selected)) 
        {
            [int]$Selected = Read-Host ("`nSelect the Number for the SKU to use.")    
        }
        
        Return ($TenantSKU[$Selected-1].AccountSkuID)
    
    }
    # We need to verify that the submitted SKU is valid
    else {
    
        Write-Log ("Verifying SKU " + $SKUToCheck)
        
        # If we can't find it then throw and error and abort
        if ($null -eq (Get-MSOLAccountSKU | Where-Object {$_.AccountSKUID -eq $SKUToCheck}))
        {
            Write-Log ("[ERROR] - Did not find SKU with ID: " + $SKUToCheck)
            Write-Error ("Unable to locate SKU " + $SKUToCheck) -ErrorAction Stop
        }
        # If we found it log it
        else 
        {
            Write-Log ("Found SKU")
            Return $SKUToCheck
        }
    }    
}

# Takes in a list of plans and creates a new list based currently disabled plans + list
Function Update-DisabledPlan
{

    param 
    (
        [string]$SKU,
        [string[]]$PlansToDisable,
        [string]$UserUPN
    )

    Write-Log "Determining Plans to Disable"

    # Null out our array
    [array]$CurrentDisabledPlans = $null
    
    # Get the MSOLUser object from the provided UPN to get the current plan options
    $user = Get-MsolUser -UserPrincipalName $UserUPN
    
    # Get the License object
    $license = $user.licenses | Where-Object {$_.accountskuid -eq $SKU}
    
    # Make sure we found the license on the user
    if ($null -eq $license)
    {
        Write-Log ("[ERROR] - Cannot find SKU " +  $SKU + " assigned to " + $UserUPN)
        Write-Error -Message ("Cannot find SKU " + $SKU + " assigned to " + $UserUPN) -ErrorAction Stop
    }
    
    # Get currently disabled plans
    [array]$CurrentDisabledPlans = ($license.servicestatus | Where-Object {$_.provisioningstatus -eq "disabled"})

    # Make sure we got back some disabled plans
    if ($CurrentDisabledPlans.Count -le 0)
    {
        
        Write-Log "No Currently Disabled Plans."

        # Pull our plans to disable into Output and return it
        [string[]]$Output = $PlansToDisable.ToUpper()
        
        Return $Output

    }
    
    # There are currently disabled plans we need to combine the current with the new
    else 
    {                
        # Show the currently Disabled Plans and then determine the new list
        foreach ($plan in $CurrentDisabledPlans)
        {
            [string[]]$CurrentDisabledPlansNames = $CurrentDisabledPlansNames + ($plan.serviceplan.servicename).toupper()
        }

        Write-Log ("Currently Disabled plans:" + $CurrentDisabledPlansNames)
    
        # Combine the two lists of disabled plans
        [string[]]$Output = $CurrentDisabledPlansNames + $PlansToDisable

        # Make all of them uppercase for comparison
        $Output = $Output.toupper()

        # Throw out all of the duplicates
        $Output = ($Output | Select-Object -Unique)

        Return $Output
    }

    Write-Error "Problem with Update-DisabledPlan should never end up here." -ErrorAction Stop
}

# Takes in a list of plans and creates a new list based on currently enabled plans + list
Function Update-EnabledPlan 
{
    param 
    (
        [string]$SKU,
        [string[]]$PlansToEnable,
        [string]$UserUPN
    )

    Write-Log "Determining Plans to Enable"

    # Null out our values
    [string[]]$CurrentDisabledPlansNames = $null
    [array]$CurrentDisabledPlans = $null
    
    # Get the MSOLUser object from the provided UPN to get the current plan options
    $user = Get-MsolUser -UserPrincipalName $UserUPN
    
    # Get the License object
    $license = $user.licenses | Where-Object {$_.accountskuid -eq $SKU}
    
    # Make sure we found the license on the user
    if ($null -eq $license)
    {
        Write-Log ("[ERROR] - Cannot find SKU " +  $SKU + " assigned to " + $UserUPN)
        Write-Error -Message ("Cannot find SKU " + $SKU + " assigned to " + $UserUPN) -ErrorAction Stop
    }
    
    # Get currently disabled plans
    [array]$CurrentDisabledPlans = ($license.servicestatus | Where-Object {$_.provisioningstatus -eq "disabled"})
    
    # If there are no currently disabled plans then return null since there are no plans to update
    if ($null -eq $CurrentDisabledPlans)
    {
        Write-Log "No Currently Disabled Plans; No Plan updates needed."
        $Output = "0x0"
        Return $Output
    }
    # Otherwise we need to show what is currently disabled and then calculate the new list to disable
    else 
    {
        # Pull out the plan names
        foreach ($plan in $CurrentDisabledPlans)
        {
            [string[]]$CurrentDisabledPlansNames = $CurrentDisabledPlansNames + ($plan.serviceplan.servicename).toupper()
        }

        Write-Log ("Currently Disabled plans:" + $CurrentDisabledPlansNames) 
    
        # Go thru each plan that needs to be enabled and remove it from the list of currently disabled plans
        foreach ($Plan in $PlansToEnable) 
        {        
            # Remove the plans we want to enable from the list of disabled plans
            $CurrentDisabledPlansNames = ($CurrentDisabledPlansNames | Where-Object {$_ -ne $Plan})            
        }    

        # Make sure we havne't pulled out all disabled plans and if we have just return a null
        if ($null -eq $CurrentDisabledPlansNames)
        {
            $Output = $null
            Return $Output
        }
        # As long as we have at least one return that one
        else 
        {
            Return $CurrentDisabledPlansNames
        }
    }

    Write-Error "Something went horribly Wrong with Update-EnabledPlan" -ErrorAction Stop
}

# Takes in a list of plans and creates a new list based on currently enabled plans + list
Function Get-PlansToMaintain 
{
    param 
    (
        [string]$SKU,
        [string]$SKUToReplace,
        [string]$UserUPN
    )

    Write-Log "Determining Plans to Maintain"

    # Null out our values
    [string[]]$CurrentDisabledPlansNames = $null
    [array]$CurrentDisabledPlans = $null
    
    # Get the Plan names for our new SKU
    [string[]]$NewPlans = ((Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $SKU}).servicestatus.serviceplan.servicename).toupper()

    # Get the MSOLUser object from the provided UPN to get the current plan options
    $user = Get-MsolUser -UserPrincipalName $UserUPN
    
    # Get the License object
    $license = $user.licenses | Where-Object {$_.accountskuid -eq $SKUToReplace}
    
    # Make sure we found the license on the user
    if ($null -eq $license)
    {
        Write-Log ("[ERROR] - Cannot find SKU " +  $SKUToReplace + " assigned to " + $UserUPN)
        Write-Error -Message ("Cannot find SKU " + $SKUToReplace + " assigned to " + $UserUPN) -ErrorAction Stop
    }
    
    # Get currently disabled plans
    [array]$CurrentDisabledPlans = ($license.servicestatus | Where-Object {$_.provisioningstatus -eq "disabled"})
    
    # If there are no currently disabled plans then return null since there are no plans to update
    if ($null -eq $CurrentDisabledPlans)
    {
        Write-Log "No Currently Disabled Plans; No Plan updates needed."
        $Output = "0x0"
        Return $Output
    }
    # Otherwise we need to show what is currently disabled and then calculate the new list to disable
    else 
    {
        # Pull out the plan names
        foreach ($plan in $CurrentDisabledPlans)
        {
            [string[]]$CurrentDisabledPlansNames = $CurrentDisabledPlansNames + ($plan.serviceplan.servicename).toupper()
        }

        Write-Log ("Plans disabled in current SKU: " + $CurrentDisabledPlansNames)

        # Go thru each plan that needs to be disabled and if it is there remove it from the list of plans in the new SKU
        foreach ($Plan in $CurrentDisabledPlansNames) 
        {        
            # Builds a list of plans to enable from the new SKU
            $NewPlans = ($NewPlans | Where-Object {$_ -ne $Plan})
        }    

        # If we didn't get back any plans to enable then something went wrong and we need to error out
        if ($null -eq $NewPlans)
        {
            Write-Error "All Plans will be disabled." -ErrorAction Stop
        }
        # Take back our list of plans that need to be enabled and get back a list of plans to disable
        else 
        {
            [string[]]$Output = Set-EnabledPlan -SKU $SKU -PlansToEnable $NewPlans
            Return $Output
        }
    }

    Write-Error "Something went horribly Wrong with Update-EnabledPlan" -ErrorAction Stop
}

# Tests a list of plans agiast availible plans in the SKU
# Return the first plan that fails or null
Function Test-Plan
{
    param 
    (
        [string[]]$Plan,
        [string]$Sku
    )

    Write-Log "Validating Provided Plans"
    $PlansInSKU = (Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $sku}).servicestatus.serviceplan.servicename

    # Take each of the provided plans and check it against the plans we found in the SKU
    foreach ($PlanToCheck in $Plan)
    {
        if ($PlansInSKU -contains $PlanToCheck){}
        # If we don't find it then throw an error and stop
        else 
        { 
            Write-Log ("[ERROR] - Failed to find plan: " + $PlanToCheck)
            Write-Error ("Invalid plan " + $PlanToCheck + " provided in plan list. Please correct input and try again.") -ErrorAction Stop
        }
    }

    # If we validate them all then log it
    Write-Log "All Plans Valid"
}

# Determine if we have an array with UPNs or just a single UPN / UPN array unlabeled
Function Test-UserObject 
{
    param ([array]$ToTest)

    # See if we can get the UserPrincipalName property off of the input object
    # If we can't then we need to see if this is a UPN and convert it into an object for acceptable input
    if ($null -eq $ToTest[0].UserPrincipalName)
    {
        # Very basic check to see if this is a UPN
        if ($ToTest[0] -match '@')
        {
            [array]$Output = $ToTest | Select-Object -Property @{Name="UserPrincipalName";Expression={$_}}
            Return $Output
        }
        else 
        {
            Write-Log "[ERROR] - Unable to determine if input is a UserPrincipalName"
            Write-Log "Please provide a UPN or array of objects with propertly UserPrincipalName populated"
            Write-Error "Unable to determine if input is a User Principal Name" -ErrorAction Stop
        }
    }
    # If we can pull the value of UserPrincipalName then just return the same object back
    else
    {
        Return $ToTest
    }


}

# sets list of disabled plans to provided list
# Nothing to do here this one is provided for consistency and future use if needed
Function Set-DisabledPlan {}

# sets list of enabled plans to provided list
Function Set-EnabledPlan 
{
    param
    (
        [string]$SKU,
        [string[]]$PlansToEnable
    )

    Write-Log "Determining plans to Disable"
    [string[]]$Output = (Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $SKU}).servicestatus.serviceplan.servicename
    
    Foreach ($PlanToRemove in $PlansToEnable)
    {
        # Take out each of the plans that were provided to us
        $Output = $Output | Where-Object {$_ -ne $PlanToRemove}
    }

    Return $Output
}

# Creates the LicenseOptions variable for setting diabled plans
Function Set-LicenseOption
{

    param 
    (
        [string[]]$DisabledPlansArray,
        [Parameter(Mandatory=$true)]
        [string]$SKU    
    )

    # if there are no plans to disable we create the default option set
    if ($null -eq $DisabledPlansArray){
        Write-log "Setting all SKU Plans to Enabled"
        $licenseOptions = New-MsolLicenseOptions -AccountSKUId $SKU
    }
    # Otherwise add the disabled plans to the option set
    else {
        Write-Log "Setting Disabled Plans License Options"
        Write-Log ("Disabled Plans: " + $DisabledPlansArray)
        $licenseOptions = New-MsolLicenseOptions -AccountSKUId $SKU -DisabledPlans $DisabledPlansArray
    }
    
    Return $licenseOptions
}

#==========================

# Adds a SKU to a specified collection of users
Function Add-MSOLUserLicense 
{
    Param
    (
        [Parameter(Mandatory)]
        [array]$Users,
        [Parameter(Mandatory)]
        [string]$LogFile,
        [string]$SKU,
        [string]$Location,
        [string[]]$PlansToDisable,
        [string[]]$PlansToEnable
    )

    # Make sure we have a valid log file path
    Test-LogPath -LogFile $LogFile

    # Make sure we have the connection to MSOL
    Test-MSOLServiceConnection
    
    # Make user our Users object is valid
    [array]$Users = Test-UserObject -ToTest $Users

    # If no value of SKU passed in then call Select-Sku to allow one to be picked
    if ([string]::IsNullOrEmpty($SKU))
    {
        $SKU = Select-SKU -Message "Select SKU to Add to the Users:"
    }
    # If a value has been passed in verify it
    else
    {
        $SKU = Select-SKU -SKUToCheck $SKU
    }

    # Make sure we didn't get both enable and disable
    if (!([string]::IsNullOrEmpty($PlansToDisable)) -and !([string]::IsNullOrEmpty($PlansToEnable)))
    {
        Write-Log "[ERROR] - Cannot use both -PlansToDisable and -PlansToEnable at the same time"
        Write-Error "Cannot use both -PlansToDisable and -PlansToEnable at the same time" -ErrorAction Stop
    }

    # Testing the plan inputs to make sure they are valid
    if (!([string]::IsNullOrEmpty($PlansToDisable)))
    {
        Test-Plan -Sku $SKU -Plan $PlansToDisable
        
        # Get the license options
        $LicenseOption = Set-LicenseOption -DisabledPlansArray $PlansToDisable -SKU $SKU    

    }
    # If plans to enable has a value then we test them
    elseif (!([string]::IsNullOrEmpty($PlansToEnable)))
    {
        Test-Plan -Sku $SKU -Plan $PlansToEnable

        # Get the disabled plans and License options
        [string[]]$CalculatedPlansToDisable = Set-EnabledPlan -SKU $SKU -Plan $PlansToEnable
        $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU
    }
    # If neither has been provided then just set default options
    else 
    {
        $LicenseOption = Set-LicenseOption -SKU $SKU    
    }

    # "Zero" out the user counter
    $i = 1
    
    # Add License to the users passed in
    Foreach ($Account in $Users)    
    {

        Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====")

        # Set our skip variable to false so we process the user
        # Will change it to true if we need to skip the actual change
        $Skip = $false
    
        # Check to see if the sku we are trying to assign is already on the user is so break out of the foreach
        if (!($null -eq ((Get-MsolUser -UserPrincipalName $Account.UserPrincipalName).licenses | Where-Object {$_.accountskuid -eq $SKU})))
        {
            Write-Log ("[WARNING] - " + $SKU + " is already assigned to the user.")
            Write-Warning "User already has $SKU assigned. Please use Set-MSOLUserLicensePlans or Update-MSOLUserLicensePlans to modify existing Plans."
            $Skip = $true
        }
        else {}
        
        if ($Skip)
        {
            Write-Log "Skipping user " + $Account.UserPrincipalName
        }
        else 
        {
            # If location has a value then we need to set it
            if (!([string]::IsNullOrEmpty($Location)))
            {
                $command = ("Set-MsolUser -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -UsageLocation " + $Location)
                Write-Log ("Running: " + $Command)
                Invoke-Expression $Command
            }

            # Build and run our license set command
            [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -AddLicenses " + $SKU + " -LicenseOptions `$LicenseOption -ErrorAction Stop -ErrorVariable CatchError")
            Write-Log ("Running: " + $Command)
            # Try our command
            try 
            { 
                Invoke-Expression $Command 
            }
            # If we have any error write out and stop
            # Doing this so I can customize the error later
            ## TODO: Update error with resume information!
            catch
            { 
                Write-Log ("[ERROR] - " + $CatchError.ErrorRecord)
                Write-Error ("Failed to successfully add license to user " + $account.UserPrincipalName) -ErrorAction Stop
            }
        }

        # Update the progress bar and increment our counter
        Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Coverting from Inherited to Explicit"

        # Update our user counter
        $i++
    }

    <#
 
     .SYNOPSIS
    Adds licenses to users.
 
    .DESCRIPTION
    Adds a license SKU to a user or collection of users.
 
    * Can specify plans to enable or disable
    * Logs all activity to a log file
    * Generates and error and stops if there are any problems setting the license
    * Can set location if desired
 
    .PARAMETER Users
    Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property.
 
    .PARAMETER SKU
    SKU that should be added.
 
    .PARAMETER Location
    If provided will set the location of the user.
 
    .PARAMETER PlansToDisable
    Comma seperated list of SKU plans to Disable.
 
    .PARAMETER PlansToEnable
    Comma seperated list of SKU plans to Enable.
 
    .PARAMETER LogFile
    File to log all actions taken by the function.
 
    .OUTPUTS
    Log file showing all actions taken by the function.
 
    .EXAMPLE
    Add-MSOLUserLicense -Users $NewUsers -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK
     
    Adds the ENTERPRISEPACK SKU to all users in $NewUsers with all plans turned on.
 
    .EXAMPLE
    Add-MSOLUserLicense -Users $NewUsers -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -PlanstoEnable Deskless,Sway,Teams1
 
    Adds the ENTERPRISEPACK SKU to all users in $NewUsers with ONLY the Deskless, Sway, and Teams1 Plans turned on.
         
    #>
    
}

# Find all MSOLusers that have a specific SKU assigned
## Not sure if I am going to make this one or not? Still trying to figure out if this would be useful or if there is a fast way to do this
## Sweeping all users to try and get this would be annoying in large tenants ... wonder if there is something I can do with the REST API
Function Find-MSOLUserBySku {}

# List out all availible plans inside a specified SKU
Function Get-MSOLUserSKUPlans {}

# Removes a SKU from a specified collection of users
Function Remove-MSOLUserLicense 
{
    Param
    (
        [Parameter(Mandatory)]
        [array]$Users,
        [Parameter(Mandatory)]
        [string]$LogFile,
        [string]$SKU
    )
    
    # Make sure we have a valid log file path
    Test-LogPath -LogFile $LogFile

    # Make sure we have the connection to MSOL
    Test-MSOLServiceConnection

    # Make user our Users object is valid
    [array]$Users = Test-UserObject -ToTest $Users

    # If no value of SKU passed in then call Select-Sku to allow one to be picked
    if ([string]::IsNullOrEmpty($SKU))
    {
        $SKU = Select-SKU -Message "Select SKU to remove from the Users:"
    }
    # If a value has been passed in verify it
    else
    {
        $SKU = Select-SKU -SKUToCheck $SKU
    }

    # "Zero" out the user counter
    $i = 1
    
    # Add License to the users passed in
    Foreach ($Account in $Users)    
    {
        
        Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====")

        # Build and run our license set command
        [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -RemoveLicense " + $SKU + " -ErrorAction Stop -ErrorVariable CatchError")
        Write-Log ("Running: " + $Command)
        # Try our command
        try 
        { 
            Invoke-Expression $Command 
        }
        # If we have any error write out and stop
        # Doing this so I can customize the error later
        ## TODO: Update error with resume information!
        catch
        { 
            Write-Log ("[ERROR] - " + $CatchError.ErrorRecord)
            Write-Error ("Failed to successfully remove license from user " + $account.UserPrincipalName) -ErrorAction Stop
        }
        
        # Update the progress bar and increment our counter
        Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Removing SKU"

        # Update our user counter
        $i++
    }

    <#
  
    .SYNOPSIS
    Removes a license SKU from users.
 
    .DESCRIPTION
    Removes a licese SKU from a user or collection of users.
 
    * Prompts for SKU if none provided
    * Logs all activity to a log file
    * Generates and error and stops if there are any problems removing the license
     
    .PARAMETER Users
    Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property.
 
    .PARAMETER SKU
    SKU that should be removed.
 
    .PARAMETER LogFile
    File to log all actions taken by the function.
 
    .OUTPUTS
    Log file showing all actions taken by the function.
 
    .EXAMPLE
    Remove-MSOLUserLicense -Users $LeavingEmployees -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK
     
    Removes the SKU ENTERPRISEPACK from all users in the $LeavingEmployees variable
 
    #>

}

# Changes from one SKU to another SKU for a collection of users
Function Switch-MSOLUserLicense 
{
    Param
    (
        [Parameter(Mandatory)]
        [array]$Users,
        [Parameter(Mandatory)]
        [string]$LogFile,
        [string]$SKU,
        [string]$SKUToReplace,
        [string[]]$PlansToDisable,
        [string[]]$PlansToEnable,
        [switch]$AttemptToMaintainPlans
    )

    # Make sure we have a valid log file path
    Test-LogPath -LogFile $LogFile

    # Make sure we have the connection to MSOL
    Test-MSOLServiceConnection

    # Set Error prefernece to stop
    $ErrorActionPreference = "Stop"

    # Make user our Users object is valid
    [array]$Users = Test-UserObject -ToTest $Users

    # Make sure we didn't get an invalid switch combination
    if ($AttemptToMaintainPlans -and (!([string]::IsNullOrEmpty($PlansToDisable)) -or !([string]::IsNullOrEmpty($PlansToEnable))))
    {
        Write-Log "[ERROR] - Cannot use -AttemptToMaintainPlans with -PlansToDisable or -PlansToEnable"
        Write-Error "Cannot use -AttemptToMaintainPlans with -PlansToDisable or -PlansToEnable. Please review input and try again." -ErrorAction Stop
    }

    # If no value of SKU was passed in then call Select-Sku to allow one to be picked
    if ([string]::IsNullOrEmpty($SKU))
    {
        $SKU = Select-SKU -Message "Select New SKU For Users:"
    }
    # If a value has been passed in verify it
    else
    {
        $SKU = Select-SKU -SKUToCheck $SKU
    }
    
    # If no value of SKUToReplace was passed in then call Select-Sku to allow one to be picked
    if ([string]::IsNullOrEmpty($SKUToReplace))
    {
        $SKUToReplace = Select-SKU -Message "Select SKU to be replaced"
    }
    # If a value has been passed in verify it
    else
    {
        $SKUToReplace = Select-SKU -SKUToCheck $SKUToReplace
    }

    ## Make sure skutoreplace and sku don't match
    if ($SKUToReplace.toupper() -eq $SKU.ToUpper())
    {
        Write-Log "[ERROR] - `$SKU and `$SKUToReplace match. Unable to replace license with itself."
        Write-Log "[ERROR] - Please use Update-MSOLUserLicensePlan or Set-MSOLUserLicensePlan to modify license plans."
        Write-Error -Message "-SKU and -SKUToReplace can not have the same value." -ErrorAction Stop
    }
    
    # Make sure we didn't get both enable and disable
    if (!([string]::IsNullOrEmpty($PlansToDisable)) -and !([string]::IsNullOrEmpty($PlansToEnable)))
    {
        Write-Log "[ERROR] - Cannot use both -PlansToDisable and -PlansToEnable at the same time"
        Write-Error "Cannot use both -PlansToDisable and -PlansToEnable at the same time" -ErrorAction Stop
    }

    # Testing the plan inputs to make sure they are valid
    if (!([string]::IsNullOrEmpty($PlansToDisable)))
    {
        Test-Plan -Sku $SKU -Plan $PlansToDisable
        
        # Get the license options
        $LicenseOption = Set-LicenseOption -DisabledPlansArray $PlansToDisable -SKU $SKU    

    }
    # If plans to enable has a value then we test them
    elseif (!([string]::IsNullOrEmpty($PlansToEnable)))
    {
        Test-Plan -Sku $SKU -Plan $PlansToEnable

        # Get the disabled plans and License options
        [string[]]$CalculatedPlansToDisable = Set-EnabledPlan -SKU $SKU -Plan $PlansToEnable
        $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU
    }
    elseif($AttemptToMaintainPlans)
    {
        Write-Log "Will attempt to maintain existing user plans when moving to new License"
        
        $ExistingPlans = (Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $SKUToReplace}).servicestatus.serviceplan.servicename
        $NewPlans = (Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $SKU}).servicestatus.serviceplan.servicename

        foreach ($Plan in $NewPlans)
        {
            # If the new plan name isn't in the existing list of plans then we will end up enabling it
            if ($ExistingPlans -contains $Plan){}
            else 
            {
                # Add to our list of plans we will be enabling by default
                [string[]]$PlansEnabledByDefault = $PlansEnabledByDefault + $Plan
            }
            
        }

        # If we generated at least one plan that didn't match between the two SKUs then inform the user and get consent to continue
        if ($PlansEnabledByDefault.Count -gt 0)
        {
            Write-Log "Cannot match the following plans between the two SKUs so they will be enabled by default:"
            Write-Log $PlansEnabledByDefault

            # Prompt the user to upgrade or not
            $title = "Agree to Enable"
            $message = "When the SKU switch is complete the plans listed above will be enabled by default. `nIs this OK?"
            $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes","Continues Switching SKUs enabling the listed plans."
            $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No","Stops the function."
            $options = [System.Management.Automation.Host.ChoiceDescription[]]($Yes, $No)
            $result = $host.ui.PromptForChoice($title, $message, $options, 0) 

            # Check to see what the user choose
            switch ($result)
            {
                
                0 
                {
                    Write-Log "Agreed to enabled listed plans by default."
                }
                1 
                {
                    Write-Log "[ERROR] - Did not accept default plan enablement."
                    Write-Error "User terminated function. Unable to enable default plans." -ErrorAction Stop
                }
            }
        }
    }
    # If neither has been provided then just set default options
    else 
    {
        $LicenseOption = Set-LicenseOption -SKU $SKU    
    }

    # "Zero" out the user counter
    $i = 1

    foreach ($Account in $Users)
    {
        Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====")
        
        # Set our skip variable to false
        $Skip=$false

        # Null out our reused variables
        $CalculatedPlansToDisable = $null
    
        # Check to see if the sku we are trying to assign is already on the user is so break out of the foreach
        if (!($null -eq ((Get-MsolUser -UserPrincipalName $Account.UserPrincipalName).licenses | Where-Object {$_.accountskuid -eq $SKU})))
        {
            Write-Log ("[WARNING] - " + $SKU + " is already assigned to the user.")
            Write-Warning "User already has $SKU assigned."
            $Skip = $true
        }
        else {}
        
        if ($Skip)
        {
            Write-Log ("Skipping user " + $Account.UserPrincipalName)
        }
        else 
        {
            # Since attempt to maintain is per user we need to calculate per user license options
            if ($AttemptToMaintainPlans)
            {
                # Get the disabled plans and License options
                [string[]]$CalculatedPlansToDisable = Get-PlansToMaintain -SKU $SKU -SKUToReplace $SKUToReplace -UserUPN $account.UserPrincipalName

                # If we found no disabled plans then turn on everything
                if ($CalculatedPlansToDisable -eq "0x0")
                {
                    Write-Log ("Turning on all Plans for " + $Sku + " on user " + $Account.UserPrincipalName)
                    $LicenseOption = Set-LicenseOption -SKU $SKU                
                }
                # Turn on only the plans we found that need to be disable in the new SKU
                else
                {
                    $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU
                }            
            }
            # If we are not trying to maintain plans we can use the license options that were calculated before the foreach
            else {}

            [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -AddLicenses " + $SKU + " -RemoveLicenses " + $SKUToReplace + " -LicenseOptions `$LicenseOption -ErrorAction Stop -ErrorVariable CatchError")
            Write-Log ("Running: " + $Command)
            # Try our command
            try 
            { 
                Invoke-Expression $Command
            }
            # If we have any error write out and stop
            # Doing this so I can customize the error later
            ## TODO: Update error with resume information!
            catch
            { 
                Write-Log ("[ERROR] - " + $CatchError.ErrorRecord)
                Write-Error ("Failed to successfully switch licenses for user " + $account.UserPrincipalName) -ErrorAction Stop
            }            
        }

        # Update the progress bar and increment our counter
        Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Switching Licenses"

        # Update our user counter
        $i++
    
    }
    
    <#
 
     .SYNOPSIS
    Switches a user from one SKU to another SKU
 
    .DESCRIPTION
    Replaces one SKU with another SKU on a collection of users.
 
    * Prompts for SKUs if none are provided
    * Allows for Disabling or Enabling Plans on the new SKU
    * -AttemptToMaintainPlans will try to keep the current plan setting for the user on the new SKU
 
    .PARAMETER Users
    Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property.
 
    .PARAMETER SKU
    SKU that should be added.
 
    .PARAMETER SKUToReplace
    SKU that will be removed.
 
    .PARAMETER Location
    If provided will set the location of the user.
 
    .PARAMETER PlansToDisable
    Comma seperated list of SKU plans to Disable.
 
    .PARAMETER PlansToEnable
    Comma seperated list of SKU plans to Enable.
 
    .PARAMETER AttemptToMaintainPlans
    Tries to keep the same plan states on the new SKU that is being assigned. For plans that are unique
    to the new SKU it will default to enabling them.
 
    .PARAMETER LogFile
    File to log all actions taken by the function.
 
    .OUTPUTS
    Log file showing all actions taken by the function.
 
    .EXAMPLE
    Switch-MSOLUserLicense -Users $NewUsers -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -SKUToReplace company:STANDARDPACK
     
    Replaces the STANDARDPACK with the ENTERPRISEPACK and enables all plans.
 
    .EXAMPLE
    Add-MSOLUserLicense -Users $NewUsers -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -SKUTOReplace company:STANDARDPACK -PlanstoDisable Deskless,Sway,Teams1
 
    Replaces the STANDARDPACK with the ENTERPRISEPACK and disables Deskless, Sway, and Teams.
         
    #>
    
}

# Adds or subtracts plans from the specified SKU
Function Update-MSOLUserLicensePlan 
{
    Param
    (
        [Parameter(Mandatory)]
        [array]$Users,
        [Parameter(Mandatory)]
        [string]$LogFile,
        [string]$SKU,
        [string[]]$PlansToDisable,
        [string[]]$PlansToEnable
    )

    # Make sure we have a valid log file path
    Test-LogPath -LogFile $LogFile

    # Make sure we have the connection to MSOL
    Test-MSOLServiceConnection

    # Make user our Users object is valid
    [array]$Users = Test-UserObject -ToTest $Users

    # If no value of SKU passed in then call Select-Sku to allow one to be picked
    if ([string]::IsNullOrEmpty($SKU))
    {
        $SKU = Select-SKU -Message "Select SKU to be updated:"
    }
    # If a value has been passed in verify it
    else
    {
        $SKU = Select-SKU -SKUToCheck $SKU
    }

    # Make sure we didn't get both enable and disable
    if (!([string]::IsNullOrEmpty($PlansToDisable)) -and !([string]::IsNullOrEmpty($PlansToEnable)))
    {
        Write-Log "[ERROR] - Cannot use both -PlansToDisable and -PlansToEnable at the same time"
        Write-Error "Cannot use both -PlansToDisable and -PlansToEnable at the same time" -ErrorAction Stop
    }

    # Testing the plan inputs to make sure they are valid
    if (!([string]::IsNullOrEmpty($PlansToDisable)))
    {
        Test-Plan -Sku $SKU -Plan $PlansToDisable
    }
    # If plans to enable has a value then we test them
    elseif (!([string]::IsNullOrEmpty($PlansToEnable)))
    {
        Test-Plan -Sku $SKU -Plan $PlansToEnable
    }
    # If neither has been provided then we don't need to do anything
    else {}

    # "Zero" out the user counter
    $i = 1
    
    # Update the Plans for the Users Passed in
    Foreach ($Account in $Users)    
    {

        # Set our skip variable to false so we process the user
        # Will change it to true if we need to skip the actual change
        $Skip = $false

        Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====")

        # Testing the plan inputs to make sure they are valid
        if (!([string]::IsNullOrEmpty($PlansToDisable)))
        {        
            # Get the license options
            [string[]]$CalculatedPlansToDisable = Update-DisabledPlan -SKU $SKU -Plan $PlansToDisable -UserUPN $Account.UserPrincipalName
            $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU

        }
        # If planstoenable has a value then we need to determine what plans to enable and set them
        elseif (!([string]::IsNullOrEmpty($PlansToEnable)))
        {
            # Get the disabled plans and License options
            [string[]]$CalculatedPlansToDisable = Update-EnabledPlan -SKU $SKU -Plan $PlansToEnable -UserUPN $account.UserPrincipalName
            
            # If the return is null then everything is already on and we don't need to do anything
            if ($null -eq $CalculatedPlansToDisable)
            {
                Write-Log ("Turning on all Plans for " + $Sku + " on user " + $Account.UserPrincipalName)
                $LicenseOption = Set-LicenseOption -SKU $SKU                
            }
            # Check to see if we got back the return for all plans enabled
            elseif ($CalculatedPlansToDisable -contains "0x0")
            {
                Write-Log ("No actions needed for " + $Account.UserPrincipalName)
                $Skip = $true
            }
            # If we have a value then build our license options with the new list of plans to disable
            else 
            {
                $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU
            }
            
        }
        # If Neither disable or enable were passed then we turn on all plans
        else 
        {
            Write-Log ("Turning on all Plans for " + $Sku + " on user " + $Account.UserPrincipalName)
            $LicenseOption = Set-LicenseOption -SKU $SKU
        }
        
        # If we determined no actions are needed then skip the user
        if ($Skip){Write-Log "Skipping user"}
        # Process the user
        else 
        {

            # Build and run our license set command
            [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -LicenseOptions `$LicenseOption -ErrorAction Stop -ErrorVariable CatchError")
            Write-Log ("Running: " + $Command)
            # Try our command
            try 
            { 
                Invoke-Expression $Command 
            }
            # If we have any error write out and stop
            # Doing this so I can customize the error later
            ## TODO: Update error with resume information!
            catch
            { 
                Write-Log ("[ERROR] - " + $CatchError.ErrorRecord)
                Write-Error ("Failed to successfully add license to user " + $account.UserPrincipalName) -ErrorAction Stop
            }
        }
        

            # Update the progress bar and increment our counter
            Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Updating License Plan"

            # Update our user counter
            $i++
        }

    <#
  
    .SYNOPSIS
    Updates the current plan settings for an assigned SKU while maintaining existing plan settings.
 
    .DESCRIPTION
    Updates the current plan settings for an assigned SKU while maintaining existing plan settings.
 
    * UPDATES existing plan settings with the new enabled or disabled plans
    * All current plan setting are left in place.
    * Can specify plans to enable or disable
    * Logs all activity to a log file
    * Generates an error and stops if there are any problems setting the license
     
    .PARAMETER Users
    Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property.
 
    .PARAMETER SKU
    SKU that should be modified.
 
    .PARAMETER PlansToDisable
    Comma seperated list of SKU plans to Disable.
 
    .PARAMETER PlansToEnable
    Comma seperated list of SKU plans to Enable.
 
    .PARAMETER LogFile
    File to log all actions taken by the function.
 
    .OUTPUTS
    Log file showing all actions taken by the function.
 
    .EXAMPLE
    Update-MSOLUserLicensePlan -Users $Promoted -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -PlanstoEnable SWAY,TEAMS1,EXCHANGE_S_ENTERPRISE,YAMMER_ENTERPRISE,OFFICESUBSCRIPTION
 
    Adds the enabled Plans SWAY,TEAMS1,EXCHANGE_S_ENTERPRISE,YAMMER_ENTERPRISE,OFFICESUBSCRIPTION to the ENTERPRISEPACK for all users in $Promoted. Maintaining any existing plan settings.
     
    #>


}

# Overwrites plans on the specified SKU
Function Set-MSOLUserLicensePlan 
{
    Param
    (
        [Parameter(Mandatory)]
        [array]$Users,
        [Parameter(Mandatory)]
        [string]$LogFile,
        [string]$SKU,
        [string[]]$PlansToDisable,
        [string[]]$PlansToEnable
    )

    # Make sure we have a valid log file path
    Test-LogPath -LogFile $LogFile

    # Make sure we have the connection to MSOL
    Test-MSOLServiceConnection

    # Make user our Users object is valid
    [array]$Users = Test-UserObject -ToTest $Users

    # If no value of SKU passed in then call Select-Sku to allow one to be picked
    if ([string]::IsNullOrEmpty($SKU))
    {
        $SKU = Select-SKU -Message "Select SKU to Set:"
    }
    # If a value has been passed in verify it
    else
    {
        $SKU = Select-SKU -SKUToCheck $SKU
    }

    # Make sure we didn't get both enable and disable
    if (!([string]::IsNullOrEmpty($PlansToDisable)) -and !([string]::IsNullOrEmpty($PlansToEnable)))
    {
        Write-Log "[ERROR] - Cannot use both -PlansToDisable and -PlansToEnable at the same time"
        Write-Error "Cannot use both -PlansToDisable and -PlansToEnable at the same time" -ErrorAction Stop
    }

    # Testing the plan inputs to make sure they are valid
    if (!([string]::IsNullOrEmpty($PlansToDisable)))
    {
        Test-Plan -Sku $SKU -Plan $PlansToDisable
        
        # Get the license options
        $LicenseOption = Set-LicenseOption -DisabledPlansArray $PlansToDisable -SKU $SKU    

    }
    # If plans to enable has a value then we test them
    elseif (!([string]::IsNullOrEmpty($PlansToEnable)))
    {
        Test-Plan -Sku $SKU -Plan $PlansToEnable

        # Get the disabled plans and License options
        [string[]]$CalculatedPlansToDisable = Set-EnabledPlan -SKU $SKU -Plan $PlansToEnable
        $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU
    }
    # If neither has been provided then just set default options
    else 
    {
        $LicenseOption = Set-LicenseOption -SKU $SKU    
    }

    # "Zero" out the user counter
    $i = 1
    
    # Add License to the users passed in
    Foreach ($Account in $Users)    
    {
        # Build and run our license set command
        [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -LicenseOptions `$LicenseOption -ErrorAction Stop -ErrorVariable CatchError")
        Write-Log ("Running: " + $Command)
        # Try our command
        try 
        { 
            Invoke-Expression $Command 
        }
        # If we have any error write out and stop
        # Doing this so I can customize the error later
        ## TODO: Update error with resume information!
        catch
        { 
            Write-Log ("[ERROR] - " + $CatchError.ErrorRecord)
            Write-Error ("Failed to successfully add license to user " + $account.UserPrincipalName) -ErrorAction Stop
        }
        

        # Update the progress bar and increment our counter
        Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Setting License Plan"

        # Update our user counter
        $i++
    }

    <#
  
    .SYNOPSIS
    Set the disabled plans on a user overwriting current settings.
 
    .DESCRIPTION
    Overwrites the current plan settings for an assigned SKU with the new plan settings provided.
 
    * OVERWRITES existing plan settings and makes them match the provided values
    * Can specify plans to enable or disable
    * Logs all activity to a log file
    * Generates and error and stops if there are any problems setting the license
     
    .PARAMETER Users
    Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property.
 
    .PARAMETER SKU
    SKU that should be added.
 
    .PARAMETER PlansToDisable
    Comma seperated list of SKU plans to Disable.
 
    .PARAMETER PlansToEnable
    Comma seperated list of SKU plans to Enable.
 
    .PARAMETER LogFile
    File to log all actions taken by the function.
 
    .OUTPUTS
    Log file showing all actions taken by the function.
 
    .EXAMPLE
    Set-MSOLUserLicensePlan -Users $Promoted -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK
     
    Enable all of the plans in the ENTERPRISEPACK SKU for all users in $Promoted. Overwriting any current settings.
 
    .EXAMPLE
    Set-MSOLUserLicensePlan -Users $Promoted -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -PlanstoEnable SWAY,TEAMS1,EXCHANGE_S_ENTERPRISE,YAMMER_ENTERPRISE,OFFICESUBSCRIPTION
 
    Set the enabled Plans for the ENTERPRISEPACK to be SWAY,TEAMS1,EXCHANGE_S_ENTERPRISE,YAMMER_ENTERPRISE,OFFICESUBSCRIPTION for all users in $Promoted. This will overwrite any current plan settings for the user.
     
    #>

}

# Converts from an inherited SKU to an Explicit SKU with the same plan settings
Function Convert-MSOLUserLicenseToExplicit
{
        param 
    (
        [Parameter(Mandatory)]
        [array]$Users,
        [Parameter(Mandatory)]
        [string]$LogFile,
        [string]$SKU
    )
    
    # Make sure we have a valid log file path
    Test-LogPath -LogFile $LogFile

    # Make sure we have the connection to MSOL
    Test-MSOLServiceConnection
    
    # Start Processing
    Write-Log ("Converting " + $SKU + " from inherited to explicit")

    # Make user our Users object is valid
    [array]$Users = Test-UserObject -ToTest $Users

    # If no value of SKU passed in then call Select-Sku to allow one to be picked
    if ([string]::IsNullOrEmpty($SKU))
    {
        $SKU = Select-SKU -Message "Select SKU to Convert to Explicit:"
    }
    # If a value has been passed in verify it
    else
    {
        $SKU = Select-SKU -SKUToCheck $SKU
    }    
    
    # Start Processing
    Write-Log ("Converting " + $SKU + " from inherited to explicit")

    # "Zero" out of counter
    $i = 1
    
    Foreach ($Account in $Users)
    {

        Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====")

        # Set our skip variable to false so we process the user
        # Will change it to true if we need to skip the actual change
        $Skip = $false
        
        # Null out the variables we use
        [array]$CurrentDisabledPlansArray = $null
        $CurrentLicenseDetails = $null

        # Verify that the SKU is inherited and isn't already explicit ... don't want to overwrite any explicit settings
        try 
        {
             $MSOLUser = Get-MsolUser -UserPrincipalName $Account.UserPrincipalName -ErrorAction Stop
        }
        catch
        {
            Write-Log ("[Error] - Unable to find user:`n " + $_.Exception)
            Write-Error $_
            break
        }
        
        $CurrentLicenseDetails = ($MSOLUser.licenses | Where-Object {$_.accountskuid -eq $SKU})

        # Make sure we found the SKU on the user
        if ($Null -eq $CurrentLicenseDetails)
        {
            Write-Log ("[Warning] - SKU " + $SKU + " not Assigned to user " + $Account.UserPrincipalName)
            Write-Warning ("SKU " + $SKU + " not Assigned to user " + $Account.UserPrincipalName)
            
            # Since we can't set this we need to skip it
            $Skip = $true
        }
        # Make sure the SKU is inherited
        elseif (($CurrentLicenseDetails.GroupsAssigningLicense -contains $Users.objectid) -or ($CurrentLicenseDetails.GroupsAssigningLicense.count -le 0))
        {
            Write-Log ("[Warning] - User " + $Account.UserPrincipalName + " already has explicit assignment for " + $SKU)
            Write-Warning ("User " + $Account.UserPrincipalName + " already has explicit assignment for " + $SKU)
            
            # Since it is already explict we need to skip the actual set
            $Skip = $true
            
        }
        else {
                            
            # Get the currently disabled plans
            [array]$CurrentDisabledPlansArray = (($CurrentLicenseDetails).ServiceStatus | Where-Object {$_.ProvisioningStatus -eq "Disabled"}).serviceplan | Select-Object -ExpandProperty ServiceName
            Write-Log ("Disabling Plans: " + [string]$CurrentDisabledPlansArray)
        }

        # If we set SKIP to true then we are not processing this user log it and move on
        if ($Skip -eq $true)
        {
            Write-Log ("Skipping user " + $Account.UserPrincipalName)
        }
        # Else we need to set the license
        else 
        {
            # Create our License Options from the disabled plan array
            [Microsoft.Online.Administration.LicenseOption]$CurrentSKULicenseOptions = Set-LicenseOption -DisabledPlansArray $CurrentDisabledPlansArray -SKU $SKU

            # Build, log, and run the command
            $cmd = "Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -AddLicenses " + $SKU + " -LicenseOptions `$CurrentSKULicenseOptions"
            Write-Log ("Running: " + $cmd)
            Invoke-Expression $cmd
        }

        # Update the progress bar and increment our counter
        Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Coverting from Inherited to Explicit"
        $i++
    }

    <#
  
    .SYNOPSIS
    Converts a specified SKU from Group assiged to Explicitly assigned
 
    .DESCRIPTION
    Will explicitly apply the SKU specified to a user if that user is inheriting it from Group based licensing.
    https://docs.microsoft.com/en-us/azure/active-directory/active-directory-licensing-whatis-azure-portal
 
    * Will skip any users that don't have the specified SKU
    * Will skip any users that already have the SKU explicitly assigned
    * Will maintain the current plan state for the SKU on that user
     
    .PARAMETER Users
    Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property.
 
    .PARAMETER SKU
    SKU that should be converted to explict from inherited.
 
    .PARAMETER LogFile
    File to log all actions taken by the function.
 
    .OUTPUTS
    Log file showing all actions taken by the function.
 
    .EXAMPLE
    Convert-MSOLUserLicenseToExplicit -Users $UsersToConvert -SKU company:AAD_PREMIUM -logfile C:\temp\conversion.log
     
    Converts inherited AAD_PREMIUM licenses into explicit licenses for all users in $UsersToConvert and logs all actions in C:\temp\conversion.log
     
    #>

}

# Generates an MSOL User License Report
# TODO: Allow function to take input from the pipeline
Function Get-MSOLUserLicenseReport
{
    param 
    (
        [array]$Users,
        [Parameter(Mandatory)]
        [string]$LogFile,
        [switch]$OverWrite=$false
    )
    
    # Make sure we have a valid log file path
    Test-LogPath -LogFile $LogFile

    # Make sure we have the connection to MSOL
    Test-MSOLServiceConnection
    
    Write-Log "Generating Sku and Plan Report"

    # Get all of the availible SKUs
    $AllSku = Get-MsolAccountSku
    Write-Log ("Found " + $AllSku.count + " SKUs in the tenant")
    
    # Make sure out plan array is null
    [array]$Plans = $null

    # Build a list of all of the plans from all of the SKUs
    foreach ($Sku in $AllSku){
        $SKU.servicestatus.serviceplan | ForEach-Object {[array]$Plans = $Plans + $_.servicename}
    }

    # Need just the unique plans
    $Plans = $Plans | Select-Object -Unique | Sort-Object
    Write-Log ("Found " + $Plans.count + " Unique plans in the tenant")

    # Make sure the output array is null
    $Output = $null

    # Create a false SKU object so we populate the first entry in the array with all needed values so we get a good CSV export
    # Basically a cheat so we can easily use export-csv we will remove this entry from the actual output
    $Object = $Null
    $Object = New-Object -TypeName PSobject
    $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value "BASELINE_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value "BASELINE_IGNORE@contoso.com"
    $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value "BASELINELOCATION_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value "BASELINESKU_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "BASELINEINHERIT_IGNORE"

    # Populate a value into all of the plan names
    foreach ($value in $Plans)
    {

        $Object | Add-Member -MemberType NoteProperty -Name $value -Value "---"
    }

    # Create the output list array
    $Output = New-Object System.Collections.ArrayList

    # Populate in the generic object to the list array
    $Output.Add($Object) | Out-Null

    # Add this object as the first object in out output array
    # [array]$Output = $Output + $Object
    
    # Make sure our UserToProcess array is created and is null
    [array]$UserToProcess = $null

    # See if our user array is null and pull all users if needed
    if ($null -eq $Users)
    {

        Write-Log "Getting all users in the tenant."
        Write-Log "This can take some time."

        # Get all of the users in the tenant
        [array]$UserToProcess = Get-MsolUser -All
        Write-log ("Found " + $UserToProcess.count + " users in the tenant")

    }
    
    # Gather just the users provided
    else 
    {
        
        # Make user our Users object is valid
        [array]$Users = Test-UserObject -ToTest $Users

        Write-Log "Gathering License information for provided users"
        $i = 0
        
        # Get the data for each user to use in generating the report
        foreach ($account in $Users)
        {
            $i++
            [array]$UserToProcess = [array]$UserToProcess + (Get-MsolUser -UserPrincipalName $Account.UserPrincipalName)
            if (!($i%100)){Write-log ("Gathered Data for " + $i + " Users")}
        }

        Write-log ("Found " + $UserToProcess.count + " users to report on")
    }

    # Now that we have all of the user objects we need to start processing them and building our output object

    # Setup a counter for informing the user of progress
    $i = 0
    
    # Null out the group cache array
    # Array holds group names and GUIDs so we do not have a lookup a group more than once
    [array]$GroupCache = $Null

    # Process each user
    Write-Log "Generating Report"
    foreach ($UserObject in $UserToProcess) 
    {
    
        # Increase the counter
        $i++
        
        # Output every 100 users for progress
        if (!($i%100)){Write-log ("Finished " + $i + " Users")}
        
        # If no license is assigned we need to create that object
        if ($UserObject.isLicensed -eq $false){
        
            $Object = $null
        
            # Create our object and populate in our values
            $Object = New-Object -TypeName PSobject
            $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value $UserObject.displayname
            $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $UserObject.userprincipalname
            $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value $UserObject.UsageLocation
            $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value UNASSIGNED
            $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "Explicit"
        
            # Add the object to the output array
            #[array]$Output = $Output + $Object
            $Output.Add($Object) | Out-Null
        }
        
        # If we have a license then add that information
        else {
            # We can have more than one license on a user so we need to do each one seperatly
            foreach ($license in $UserObject.licenses) 
            {
        
                $Object = $null
            
                # Create our object and populate in our values
                $Object = New-Object -TypeName PSobject
                $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value $UserObject.displayname
                $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $UserObject.userprincipalname
                $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value $UserObject.UsageLocation
                $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value $license.accountskuid
                
                # Add each of the plans for the license in along with its status
                foreach ($value in $license.servicestatus)
                {
                
                    $Object | Add-Member -MemberType NoteProperty -Name $value.serviceplan.servicename -Value $value.ProvisioningStatus
                
                }
                
                # Get the inherited from / status of the license based on GBL
                # If there are no groups in GroupsAssigningLicense then it is explicitly assigned
                if ($license.GroupsAssigningLicense.count -le 0) 
                {
                    
                    $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "Explicit"                
                
                }
                # If it is populated then we need to process ALL values
                else
                {
                
                    # Null out our assigned by string
                    [string]$assignedby = $null
                    
                    # Process each group entry and work to resolve them
                    foreach ($entry in $license.GroupsAssigningLicense)
                    {
                    
                        # If the GUID = the ObjectID then it is direct assigned
                        if ($entry.guid -eq $UserObject.objectid){[string]$assignedby = $assignedby + "Explicit;" }
                        
                        # If it doesn't match the objectid of the user then it is a group and we need to resolve it
                        else 
                        {
                        
                            # if groupcache isn't populated yet then we know we need to look it up
                            if ($null -eq $GroupCache)
                            {
                                [array]$GroupCache = $GroupCache + (Get-MsolGroup -objectid $entry.guid | Select-Object -Property objectid,displayname)
                            }
                            # If not null check if we have a cache hit
                            else
                            {
                                # If the guid is in the groupcache then no action needed
                                if ($GroupCache.objectid -contains $entry.guid){}
                                
                                # If we have a cache miss then we need to populate it into the cache
                                else { [array]$GroupCache = $GroupCache + (Get-MsolGroup -objectid $entry.guid | Select-Object -Property objectid,displayname)}
                            }
                            
                            # Add the group information from the cache to the output
                            [string]$assignedby = $assignedby + ($GroupCache | Where-Object {$_.objectid -eq $entry.guid}).DisplayName + ";"
                        }
                    
                    }
                
                    # Add the value of assignment to the object
                    $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value ($assignedby.trimend(";"))
                }
                
                
                # Add the object created from the user information to the output array
                #[array]$Output = $Output + $Object
                $Output.Add($Object) | Out-Null
            }
        }
    }
    
    # Now that we have all of our output objects in an array we need to output them to a file
    # If overwrite has been set then just take the file name as the date and force overwrite the file
    if ($OverWrite) 
    {
        # Build our file name
        $path = Join-path (Split-Path $LogFile -Parent) ("License_Report_" + [string](Get-Date -UFormat %Y%m%d) + ".csv")
    }
    
    # Default behavior will be to create a new incremental file name
    else 
    {
        # Build our file name
        $RootPath = Join-path (Split-Path $LogFile -Parent) ("License_Report_" + [string](Get-Date -UFormat %Y%m%d))
        $TryPath = $RootPath + "*"
        
        # Find any existing files that start with our planed file name
        $FilesInPath = Get-ChildItem $TryPath

        # Found files so we need to increment our file name with _X
        if ($FilesInPath.count -gt 0)
        {
            $Path = $RootPath + "_" + ($FilesInPath.count) + ".csv"
        }
        # Didn't find anything so we are good with the base file name
        else 
        {
            $Path = $RootPath + ".csv"
        }
    }

    Write-Log ("Exporting report to " + $path)

    # Export everything to the CSV file
    $Output | Export-Csv $path -Force -NoTypeInformation
        
    # Pull it back in so we can remove our fake object
    $Temp = Import-Csv $path
    $Temp | Where-Object {$_.UserPrincipalName -ne "BASELINE_IGNORE@contoso.com"} | Export-Csv $path -Force -NoTypeInformation
    
    Write-log "Report generation finished"

    <#
  
    .SYNOPSIS
    Generates a comprehensive license report.
 
    .DESCRIPTION
    Generates a license report on a all users or a specified group of users.
    By Default it will generate a new report file each time it is run.
 
    Report includes the following:
    * All licenses assigned to each user provided.
    * State of all plans inside of each license assignment
    * Where the user is getting the license from Group name(s) and/or Explict.
     
    .PARAMETER Users
    Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property.
 
    .PARAMETER OverWrite
    If specified it will Over Write the current output CSV file instead of generating a new one.
 
    .PARAMETER LogFile
    File to log all actions taken by the function.
 
    .OUTPUTS
    Log file showing all actions taken by the function.
    CSV file named License_Report_YYYYMMDD_X.csv that contains the report.
 
    .EXAMPLE
    Get-MSOLUserLicenseReport -LogFile C:\temp\report.log
 
    Creates a new License_Report_YYYYMMDD_X.csv file with the license report in the c:\temp directory for all users.
 
    .EXAMPLE
    Get-MSOLUserLicenseReport -LogFile C:\temp\report.log -Users $SalesUsers -Overwrite
 
    OverWrites the existing c:\temp\License_Report_YYYYMMDD.csv file with a license report for all users in $SalesUsers.
     
    #>

}