#!powershell

# Copyright: (c) 2017, Red Hat, Inc.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

#Requires -Module Ansible.ModuleUtils.Legacy

Set-StrictMode -Version 2

$ErrorActionPreference = "Stop"
$ConfirmPreference = "None"

$log_path = $null

Function Write-DebugLog {
    Param(
        [string]$msg
    )

    $DebugPreference = "Continue"
    $ErrorActionPreference = "Continue"
    $date_str = Get-Date -Format u
    $msg = "$date_str $msg"

    Write-Debug $msg
    if($log_path) {
        Add-Content $log_path $msg
    }
}

$required_features = @("AD-Domain-Services","RSAT-ADDS")

Function Get-MissingFeatures {
    Write-DebugLog "Checking for missing Windows features..."

    $features = @(Get-WindowsFeature $required_features)

    If($features.Count -ne $required_features.Count) {
        Throw "One or more Windows features required for a domain controller are unavailable"
    }

    $missing_features = @($features | Where-Object InstallState -ne Installed)

    return ,$missing_features # no, the comma's not a typo- allows us to return an empty array
}

Function Ensure-FeatureInstallation {
    # ensure RSAT-ADDS and AD-Domain-Services features are installed

    Write-DebugLog "Ensuring required Windows features are installed..."
    $feature_result = Install-WindowsFeature $required_features
    $result.reboot_required = $feature_result.RestartNeeded

    If(-not $feature_result.Success) {
        Exit-Json -message ("Error installing AD-Domain-Services and RSAT-ADDS features: {0}" -f ($feature_result | Out-String))
    }
}

# return the domain we're a DC for, or null if not a DC
Function Get-DomainControllerDomain {
    Write-DebugLog "Checking for domain controller role and domain name"

    $sys_cim = Get-CIMInstance Win32_ComputerSystem

    $is_dc = $sys_cim.DomainRole -in (4,5) # backup/primary DC
    # this will be our workgroup or joined-domain if we're not a DC
    $domain = $sys_cim.Domain

    Switch($is_dc) {
        $true { return $domain }
        Default { return $null }
    }
}

Function Create-Credential {
    Param(
        [string] $cred_user,
        [string] $cred_password
    )

    $cred = New-Object System.Management.Automation.PSCredential($cred_user, $($cred_password | ConvertTo-SecureString -AsPlainText -Force))

    Return $cred
}

Function Get-OperationMasterRoles {
    $assigned_roles = @((Get-ADDomainController -Server localhost).OperationMasterRoles)

    Return ,$assigned_roles # no, the comma's not a typo- allows us to return an empty array
}

$result = @{
    changed = $false
    reboot_required = $false
}

$params = Parse-Args -arguments $args -supports_check_mode $true

$dns_domain_name = Get-AnsibleParam -obj $params -name "dns_domain_name"
$safe_mode_password= Get-AnsibleParam -obj $params -name "safe_mode_password"
$domain_admin_user = Get-AnsibleParam -obj $params -name "domain_admin_user" -failifempty $result
$domain_admin_password= Get-AnsibleParam -obj $params -name "domain_admin_password" -failifempty $result
$local_admin_password= Get-AnsibleParam -obj $params -name "local_admin_password"
$database_path = Get-AnsibleParam -obj $params -name "database_path" -type "path"
$sysvol_path = Get-AnsibleParam -obj $params -name "sysvol_path" -type "path"
$read_only = Get-AnsibleParam -obj $params -name "read_only" -type "bool" -default $false
$site_name = Get-AnsibleParam -obj $params -name "site_name" -type "str" -failifempty $read_only

$state = Get-AnsibleParam -obj $params -name "state" -validateset ("domain_controller", "member_server") -failifempty $result

$log_path = Get-AnsibleParam -obj $params -name "log_path"
$_ansible_check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false

$global:log_path = $log_path

Try {
    # ensure target OS support; < 2012 doesn't have cmdlet support for DC promotion
    If(-not (Get-Command Install-WindowsFeature -ErrorAction SilentlyContinue)) {
        Fail-Json -message "win_domain_controller requires at least Windows Server 2012"
    }

    # validate args
    If($state -eq "domain_controller") {
        If(-not $dns_domain_name) {
            Fail-Json -message "dns_domain_name is required when desired state is 'domain_controller'"
        }

        If(-not $safe_mode_password) {
            Fail-Json -message "safe_mode_password is required when desired state is 'domain_controller'"
        }

        # ensure that domain admin user is in UPN or down-level domain format (prevent hang from https://support.microsoft.com/en-us/kb/2737935)
        If(-not $domain_admin_user.Contains("\") -and -not $domain_admin_user.Contains("@")) {
            Fail-Json -message "domain_admin_user must be in domain\user or user@domain.com format"
        }
    }
    Else { # member_server
        If(-not $local_admin_password) {
            Fail-Json -message "local_admin_password is required when desired state is 'member_server'"
        }
    }

    # short-circuit "member server" check, since we don't need feature checks for this...

    $current_dc_domain = Get-DomainControllerDomain

    If($state -eq "member_server" -and -not $current_dc_domain) {
        Exit-Json $result
    }

    # all other operations will require the AD-DS and RSAT-ADDS features...

    $missing_features = Get-MissingFeatures

    If($missing_features.Count -gt 0) {
        Write-DebugLog ("Missing Windows features ({0}), need to install" -f ($missing_features -join ", "))
        $result.changed = $true # we need to install features
        If($_ansible_check_mode) {
            # bail out here- we can't proceed without knowing the features are installed
            Write-DebugLog "check-mode, exiting early"
            Exit-Json $result
        }

        Ensure-FeatureInstallation | Out-Null
    }

    $domain_admin_cred = Create-Credential -cred_user $domain_admin_user -cred_password $domain_admin_password

    switch($state) {
        domain_controller {
            If(-not $safe_mode_password) {
                Fail-Json -message "safe_mode_password is required for state=domain_controller"
            }

            If($current_dc_domain) {
                # FUTURE: implement managed Remove/Add to change domains?

                If($current_dc_domain -ne $dns_domain_name) {
                    Fail-Json "$(hostname) is a domain controller for domain $current_dc_domain; changing DC domains is not implemented"
                }
            }

            # need to promote to DC
            If(-not $current_dc_domain) {
                Write-DebugLog "Not currently a domain controller; needs promotion"
                $result.changed = $true
                If($_ansible_check_mode) {
                    Write-DebugLog "check-mode, exiting early"
                    Fail-Json -message $result
                }

                $result.reboot_required = $true

                $safe_mode_secure = $safe_mode_password | ConvertTo-SecureString -AsPlainText -Force
                Write-DebugLog "Installing domain controller..."
                $install_params = @{
                    DomainName = $dns_domain_name
                    Credential = $domain_admin_cred
                    SafeModeAdministratorPassword = $safe_mode_secure
                }
                if ($database_path) {
                    $install_params.DatabasePath = $database_path
                }
                if ($sysvol_path) {
                    $install_params.SysvolPath = $sysvol_path
                }
                if ($read_only) {
                    # while this is a switch value, if we set on $false site_name is required
                    # https://github.com/ansible/ansible/issues/35858
                    $install_params.ReadOnlyReplica = $true
                }
                if ($site_name) {
                    $install_params.SiteName = $site_name
                }
                try
                {
                    $null = Install-ADDSDomainController -NoRebootOnCompletion -Force @install_params
                } catch [Microsoft.DirectoryServices.Deployment.DCPromoExecutionException] {
                    # ExitCode 15 == 'Role change is in progress or this computer needs to be restarted.'
                    # DCPromo exit codes details can be found at https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/troubleshooting-domain-controller-deployment
                    if ($_.Exception.ExitCode -eq 15) {
                        $result.reboot_required = $true
                    } else {
                        Fail-Json -obj $result -message "Failed to install ADDSDomainController with DCPromo: $($_.Exception.Message)"
                    }
                }
                # If $_.FullyQualifiedErrorId -eq 'Test.VerifyUserCredentialPermissions.DCPromo.General.25,Microsoft.DirectoryServices.Deployment.PowerShell.Commands.InstallADDSDomainControllerCommand'
                # the module failed to resolve the given dns domain name

                Write-DebugLog "Installation complete, trying to start the Netlogon service"
                # The Netlogon service is set to auto start but is not started. This is
                # required for Ansible to connect back to the host and reboot in a
                # later task. Even if this fails Ansible can still connect but only
                # with ansible_winrm_transport=basic so we just display a warning if
                # this fails.
                try {
                    Start-Service -Name Netlogon
                } catch {
                    Write-DebugLog "Failed to start the Netlogon service: $($_.Exception.Message)"
                    Add-Warning -obj $result -message "Failed to start the Netlogon service after promoting the host, Ansible may be unable to connect until the host is manually rebooting: $($_.Exception.Message)"
                }

                Write-DebugLog "Domain Controller setup completed, needs reboot..."
            }
        }
        member_server {
            If(-not $local_admin_password) {
                Fail-Json -message "local_admin_password is required for state=domain_controller"
            }
            # at this point we already know we're a DC and shouldn't be...
            Write-DebugLog "Need to uninstall domain controller..."
            $result.changed = $true

            Write-DebugLog "Checking for operation master roles assigned to this DC..."

            $assigned_roles = Get-OperationMasterRoles

            # FUTURE: figure out a sane way to hand off roles automatically (designated recipient server, randomly look one up?)
            If($assigned_roles.Count -gt 0) {
                Fail-Json -message ("This domain controller has operation master role(s) ({0}) assigned; they must be moved to other DCs before demotion (see Move-ADDirectoryServerOperationMasterRole)" -f ($assigned_roles -join ", "))
            }

            If($_ansible_check_mode) {
                Write-DebugLog "check-mode, exiting early"
                Exit-Json $result
            }

            $result.reboot_required = $true

            $local_admin_secure = $local_admin_password | ConvertTo-SecureString -AsPlainText -Force

            Write-DebugLog "Uninstalling domain controller..."
            Uninstall-ADDSDomainController -NoRebootOnCompletion -LocalAdministratorPassword $local_admin_secure -Credential $domain_admin_cred
            Write-DebugLog "Uninstallation complete, needs reboot..."
        }
        default { throw ("invalid state {0}" -f $state) }
    }

    Exit-Json $result
}
Catch {
    $excep = $_

    Write-DebugLog "Exception: $($excep | out-string)"

    Throw
}
