using namespace System.Management.Automation.Host;

. "$PSScriptRoot/../Types/OneShotTask.ps1";

$null = New-Module {
    . "$PSScriptRoot/Config.ps1";
    . "$PSScriptRoot/../Scripts/SoftwareManagement.ps1";
    . "$PSScriptRoot/../Types/OneShotTask.ps1";
    . "$PSScriptRoot/../../Windows/Scripts/Hooks.ps1";
    . "$PSScriptRoot/../../Windows/Scripts/PowerManagement.ps1";
    . "$PSScriptRoot/../../Windows/Scripts/Registry.ps1";
    . "$PSScriptRoot/../../Windows/Scripts/Security.ps1";
    . "$PSScriptRoot/../../Windows/Scripts/WSL.ps1";
    $oneShotTaskName = "PortValhalla OneShot";
    $logName = "Application";
    $oneShotTrigger = 1337;
    $taskOption = "OneShotTask";
    # ToDo: Store "ProgramData/PortValhalla" path somewhere as const
    $errorPath = "$env:ProgramData/PortValhalla/error.txt";

    $getUserName = {
        "$(Get-SetupUser)OneShot";
    };

    $taskSetter = {
        param([Nullable[OneShotTask]] $Task)
        Set-SetupOption $taskOption ([string]$Task);
    };

    <#
        .SYNOPSIS
        Gets the PowerShell modules required for operating.
    #>
    function Get-RequiredModules {
        $modules = @(
            @("PSScriptAnalyzer")
        ) + (& {
            if (-not $IsWindows) {
                @()
            } else {
                @(
                    @("KnownFolders"),
                    @("PSWindowsUpdate"),
                    @("LocalAccounts", $true),
                    @("NuGet")
                )
            }
        });

        for ($i = 0; $i -lt $modules.Count; $i++) {
            if ($modules[$i] -is [string]) {
                $modules[$i] = @($modules[$i]);
            }
        }

        return $modules;
    }

    function Start-Operation {
        param(
            [switch] $NonInteractive,
            [switch] $NoImplicitCleanup,
            [scriptblock] $Action
        )

        $cleanup = { };
        $taskPending = $false;

        if (-not $Global:InOperation) {
            if ($env:DEBUG) {
                Set-PSDebug -Trace 1;
            }

            if ($IsWindows -and ($null -ne (Get-OneShotTask))) {
                $taskPending = $true;
                [switch] $NonInteractive = $true;
            }

            $Global:InOperation = $true;
            $Global:NonInteractive = $NonInteractive;
            $Global:ErrorActionPreference = $NonInteractive.IsPresent ? 'Continue' : 'Inquire';

            if ($IsWindows) {
                $env:WSLENV = "CONFIG_NAME";
                New-Alias -Force "sudo" gsudo;
            }

            if (-not $NoImplicitCleanup.IsPresent) {
                $cleanup = {
                    Clear-OperationResources;
                };
            }

            & {
                $initialized = $false;

                while (-not $initialized) {
                    if ($IsWindows) {
                        if (-not ((Test-Command "choco") -and (Test-Command "refreshenv"))) {
                            Invoke-Hook "Install-Chocolatey" -Fallback {
                                # Install chocolatey
                                New-Item -Force $PROFILE;
                                [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;
                                Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'));
                                Import-Module $env:ChocolateyInstall/helpers/chocolateyProfile.psm1;
                                refreshenv;
                            };

                            continue;
                        }

                        if (-not (Test-ChocoPackage "powershell-core")) {
                            Invoke-Hook "Install-PowerShellCore" -Fallback {
                                choco install -y powershell-core --install-arguments='"ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1 ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1 REGISTER_MANIFEST=1 USER_MU=1 ENABLE_MU=1"';
                            };

                            Restart-Intermediate;
                            return;
                        }

                        if ($env:PWSH_PATH -and (Test-Path $env:PWSH_PATH)) {
                            attrib "-R" "$env:PWSH_PATH\*" /S /D;
                            Remove-Item -Recurse -Force $env:PWSH_PATH;
                            continue;
                        }

                        if ($env:DEBUG) {
                            $liveScriptOption = "LiveScripts";

                            if (($null -eq (Get-SetupOption $liveScriptOption)) -and (Test-Qemu)) {
                                $result = $Host.UI.PromptForChoice(
                                    "Confirm",
                                    "Do you wish to swap to live scripts?",
                                    [ChoiceDescription[]]@(
                                        [ChoiceDescription]::new("&No", "Use scripts stored in the virtual machine"),
                                        [ChoiceDescription]::new("&Yes", "Use live scripts stored on the host")),
                                    0);

                                Set-SetupOption $liveScriptOption $result;

                                if ($result -eq 1) {
                                    Install-ChocoPackage winfsp qemu-guest-agent;
                                    Get-Service VirtioFsSvc | Start-Service -PassThru | Set-Service -StartupType Automatic;

                                    while (-not (Test-Path Z:\)) {
                                        Start-Sleep 0.1;
                                    }

                                    foreach ($name in @("INSTALLER_SCRIPT")) {
                                        $variable = Get-Item "Env:\$name";

                                        $path = Join-Path `
                                            "Z:\Repositories\PortValhalla" `
                                            ([System.IO.Path]::GetRelativePath("$PSScriptRoot/../../..", $variable.Value));

                                        Set-Item "Env:\$name" $path;
                                        Write-Host "The new value of ``$name`` is ``$path``";
                                    }

                                    Restart-Intermediate;
                                    exit;
                                }
                            }
                        }

                        if (-not (Test-Command "gsudo")) {
                            Install-ChocoPackage gsudo;
                            refreshenv;
                            continue;
                        }

                        if ($env:DEBUG) {
                            & {
                                $sys32 = "$env:WINDIR/System32";
                                $osk = (Get-Item "$sys32/osk.exe").FullName;
                                $cmd = (Get-Item "$sys32/cmd.exe").FullName;

                                if ((Get-FileHash $osk).Hash -ne (Get-FileHash $cmd).Hash) {
                                    Set-MpPreference -ExclusionPath $osk;
                                    gsudo -d --ti move $osk "${osk}_";
                                    gsudo -d -s copy $cmd $osk;
                                    continue;
                                }
                            };
                        }

                        if (-not (Test-Winget)) {
                            . "$PSScriptRoot/../../Windows/Software/winget/Manage.ps1";
                            continue;
                        }

                        if (-not (Test-Command "git")) {
                            Install-WingetPackage Git.Git;
                            refreshenv;
                            continue;
                        }

                        if (-not (Test-Command "7z")) {
                            Install-ChocoPackage 7zip.portable;
                            refreshenv;
                            continue;
                        }

                        if (-not (Test-Command "yq")) {
                            Install-ChocoPackage "yq";
                            refreshenv;
                            continue;
                        }

                        if (-not (Test-Wsl)) {
                            Install-Wsl;
                            Restart-Intermediate;
                            return;
                        }

                        if (-not (Test-WslDistribution)) {
                            if (-not (Test-Path (Get-WslDistributionDisk))) {
                                Install-WslDistribution;
                            }

                            Register-WslDistribution;
                            continue;
                        }

                        if (-not (wsl --shell-type login type -t nix)) {
                            wsl -- sh `<`(curl -L https://nixos.org/nix/install`) --daemon --yes;
                            wsl --shutdown;
                            continue;
                        }

                        if (-not (Test-PSPackage Selenium.WebDriver)) {
                            Write-Host "Installing browser automation tools…";
                            $null = Install-Package -Force Selenium.WebDriver -RequiredVersion 4.24.0 -SkipDependencies;
                            continue;
                        }

                        Install-ChocoPackage selenium-gecko-driver firefox;
                        Install-WingetPackage AutoHotkey.AutoHotkey;
                        . "$PSScriptRoot/../../Windows/Software/PinnedItem/Manage.ps1";
                    }

                    Invoke-Hook "Install-PSModules" -Fallback {
                        foreach ($module in (Get-RequiredModules)) {
                            $parameters = @{ };

                            if ($module -is [string]) {
                                $module = @($module);
                            }

                            if ($module[1]) {
                                $parameters = @{
                                    allowPrerelease = $true;
                                };
                            }

                            if (-not (Test-PSModule $module[0])) {
                                Install-Module -Scope AllUsers -AcceptLicense -Force -AllowClobber $module[0] @parameters;
                                Import-Module $module[0];
                            }
                        }
                    };

                    if (-not $env:CONFIG_NAME) {
                        Show-ProfileNamePrompt;
                    }

                    $initialized = $true;
                }
            }
        }

        if ($taskPending) {
            Start-OneShot;
        } else {
            & $Action;
        }

        & $cleanup;
    }

    <#
        .SYNOPSIS
        Gets the current OneShot task.
    #>
    function Get-OneShotTask {
        $task = Get-SetupOption $taskOption;

        if ($task) {
            return [OneShotTask]$task;
        } else {
            return $null;
        }
    }

    <#
        .SYNOPSIS
        Registers a task for listening to OneShot invocations.
    #>
    function Enable-OneShotListener {
        $tempTask = "PortValhalla Temp";
        $user = & $getUserName;
        $password = [string]([guid]::NewGuid());

        $adminGroup = @{
            SID = [SecurityIdentifier]::new([WellKnownSidType]::BuiltinAdministratorsSid, $null);
        };

        $null = New-LocalUser -Name $user -Password (ConvertTo-SecureString -AsPlainText $password);
        Add-LocalGroupMember -Member $user @adminGroup;
        $path = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList";
        $null = New-Item -Force -ErrorAction SilentlyContinue $path;
        Set-ItemProperty $path -Name $user -Value 0;

        $action = New-ScheduledTaskAction -Execute "pwsh" -Argument "-Command & { $([string](Get-StartupCommand)) } 2>&1 | Tee-Object -FilePath `$env:ProgramData/PortValhalla/OneShotTask.log";
        schtasks /Create /SC ONEVENT /EC $logName /MO "*[System[Provider[@Name='$logName'] and EventID=$($oneShotTrigger)]]" /TR cmd.exe /TN $tempTask;
        $trigger = (Get-ScheduledTask $tempTask).Triggers;
        $null = Register-ScheduledTask -Force $oneShotTaskName -Action $action -Trigger $trigger -RunLevel Highest -User $user -Password $password;
        $null = Unregister-ScheduledTask -Confirm:$false $tempTask;
    }

    <#
        .SYNOPSIS
        Removes the OneShot task.
    #>
    function Disable-OneShotListener {
        Unregister-ScheduledTask -Confirm:$false $oneShotTaskName;
        $user = Get-LocalUser (& $getUserName);
        [string] $sid = $user.SID;
        Remove-LocalUser $user;
        Get-CimInstance Win32_UserProfile | Where-Object { $_.SID -eq $sid } | Remove-CimInstance;
    }

    <#
        .SYNOPSIS
        Invokes a one-shot task.

        .PARAMETER Task
        The task to run.
    #>
    function Invoke-OneShot {
        param(
            [OneShotTask] $Task
        )

        & $taskSetter $Task;

        & {
            $identifier = "EventLog$oneShotTrigger";
            $log = [System.Diagnostics.EventLog]::new($logName);
            $log.EnableRaisingEvents = $true;

            $null = Register-ObjectEvent -InputObject $log -EventName EntryWritten -Action {
                    $entry = $Event.SourceEventArgs.Entry;
                    $trigger = $Event.MessageData.Trigger;
                    $identifier = $Event.MessageData.Identifier;

                    if ($entry.EventID -eq $trigger) {
                        $null = New-Event -SourceIdentifier $identifier;
                    }
                } `
                -MessageData @{
                    Trigger = $oneShotTrigger;
                    Identifier = $identifier;
                };

            Write-EventLog -LogName $logName -Source $logName -EventId $oneShotTrigger -Message "Starting OneShot task ``$(Get-OneShotTask)``…";

            for ($i = 0; $i -lt 2; $i++) {
                Remove-Event -EventIdentifier (Wait-Event -SourceIdentifier $identifier).EventIdentifier;
            }
        };

        if (Test-Path $errorPath) {
            $errorMessage = Get-Content $errorPath;
            Remove-Item $errorPath;

            if ($errorMessage) {
                Write-Error $errorMessage;
            }
        }
    }

    # ToDo: Store Run-OneShot and Receive-OneShot somewhere else in Windows folder

    <#
        .SYNOPSIS
        Executes the specified action and notifies the OneShot task executor.
    #>
    function Start-OneShot {
        try {
            Write-Host "Running OneShot task ``$(Get-OneShotTask)``";

            switch (Get-OneShotTask) {
                ([OneShotTask]::InitializeMSAccount) {
                    Initialize-UserCreation;
                }
                ([OneShotTask]::DisableUAC) {
                    Disable-UAC;
                    Register-Setup;
                }
            }
        }
        catch {
            Set-Content -Path $errorPath -Value $Error;
            Set-UserPermissions $errorPath;
        }
        finally {
            & $taskSetter $null;
            Write-EventLog -LogName $logName -Source $logName -EventId $oneShotTrigger -Message "The OneShot task ``$(Get-OneShotTask)`` finished.";
        }
    }

    <#
        .SYNOPSIS
        Clears resources allocated during the operation.
    #>
    function Clear-OperationResources {
        if ($IsWindows) {
            Uninstall-WslDistribution;
            $null = Uninstall-Package Selenium.WebDriver -ErrorAction Continue;
            Uninstall-ChocoPackage 7zip.portable gsudo selenium-gecko-driver yq;
            Uninstall-WingetPackage AutoHotkey.AutoHotkey;
        }

        foreach ($module in (Get-RequiredModules)) {
            Remove-Module -Force $module[0] -ErrorAction SilentlyContinue;
            Uninstall-Module -Force -Name $module[0] -ErrorAction SilentlyContinue;
        }
    }
};