using namespace Microsoft.Win32;
using namespace System.Security.AccessControl;
using namespace System.Security.Principal;

enum SetupStage {
    Configure
    Install
    CreateUser
}

enum UserStage {
    Create
    Configure
    Cleanup
    Completed
}

$null = New-Module {
    [string] $configRoot = "HKLM:\Software\PortValhalla";
    [string] $stageOption = "Stage";
    [string] $userOption = "SetupUser";
    [string] $userStageOption = "UserStage";
    [string] $accountOption = "MSAccount";
    [string] $finishedOption = "Finished";
    [RegistryKey] $key = $null;

    <#
        .SYNOPSIS
        Converts the specified path to linux and escapes it for the use in a script.

        .PARAMETER Path
        The path to convert.
    #>
    function ConvertTo-LinuxPath {
        param(
            [string] $Path
        )

        & {
            $ErrorActionPreference = 'Continue';
            $completed = $false;

            while (-not $completed) {
                $job = Start-Job {
                    $env:Value = Resolve-Path $Using:Path;
                    $env:WSLENV = "Value/p";
                    $result = wsl -- bash -c 'echo "$Value"';
                    wsl -e printf "%q" "$result";
                };

                $result = Receive-Job -Wait $job;


                if ((Split-Path -Leaf $Path) -ne (Split-Path -Leaf $result)) {
                    Write-Error "The result of the path conversion of ``$Path`` was unexpected: ``$result``";
                    continue;
                }

                if ($job.State -ne ([System.Management.Automation.JobState]::Completed)) {
                    Write-Error "An error occurred while converting ``$Path`` to a Linux path.`nOutput: ``$result``";
                    continue;
                }

                $completed = $true;
            }

            $result;
        };
    }

    <#
        .SYNOPSIS
        Gets the registry key containing options related to the setup.
    #>
    function Get-SetupConfigKey {
        if (-not (Test-Path $configRoot)) {
            $key = New-Item $configRoot;
            $acl = Get-Acl $configRoot;

            $acl.AddAccessRule(
                [RegistryAccessRule]::new(
                    [SecurityIdentifier]::new([WellKnownSidType]::BuiltinUsersSid, $null),
                    [RegistryRights]::FullControl,
                    [InheritanceFlags]::ObjectInherit -bor [InheritanceFlags]::ContainerInherit,
                    [PropagationFlags]::None,
                    [AccessControlType]::Allow));

            Set-Acl $configRoot $acl;
        } else {
            $key = Get-Item $configRoot;
        }

        return $key;
    }

    <#
        .SYNOPSIS
        Runs a script based on the `config.fish` script.

        .PARAMETER Script
        The script to run.
    #>
    function Invoke-ConfigScript {
        param(
            [string] $Script
        )

        $scriptPath = "$PSScriptRoot/../../Common/Scripts/config.fish";

        if ($env:CONFIG_MODULE) {
            $output = & {
                if (-not $IsWindows) {
                    $escapedPath = (fish -c 'string escape $argv' "$scriptPath");
                    fish -c ". $escapedPath; $Script";
                } else {
                    function fish {
                        wsl --shell-type login -- nix --extra-experimental-features "nix-command flakes" run nixpkgs`#fish -- $args
                    }

                    $output = fish -c ". $(ConvertTo-LinuxPath $scriptPath); $Script";

                    if (-not $?) {
                        Write-Error "The configuration could not be retrieved!";
                    } else {
                        $output;
                    }
                }
            }

            if (-not ($output -and ($output | Test-Json))) {
                Write-Error "The value ``$output`` is not valid JSON.";
            } else {
                $output | ConvertFrom-Json;
            }
        } else {
            $null;
        }
    }

    <#
        .SYNOPSIS
        Gets a configuration option.

        .PARAMETER Name
        The name of the option to get.
    #>
    function Get-Config {
        param(
            [string] $Name,
            [Parameter(ValueFromRemainingArguments)]
            [string[]] $ArgumentList
        )

        Invoke-ConfigScript "getConfig $Name --json $ArgumentList";
    }

    <#
        .SYNOPSIS
        Gets the name of the config root.
    #>
    function Get-ConfigRootName {
        return "valhalla.$($IsWindows ? "windows" : "linux")";
    }

    <#
        .SYNOPSIS
        Gets the name of the user root.
    #>
    function Get-UserRootName {
        return "$(Get-ConfigRootName).$($IsWindows ? "winUsers" : "users")";
    }

    <#
        .SYNOPSIS
        Gets a user configuration.

        .PARAMETER UserName
        The name of the user to get the configuration for.

        .PARAMETER Name
        The name of the configuration to get.
    #>
    function Get-UserConfig {
        param(
            [string] $UserName = ($IsWindows ? $env:UserName : $env:USER),
            [Parameter(Mandatory, Position = 0)]
            [string] $Name
        )

        if ((Get-Users) -contains $UserName) {
            Get-Config "$(Get-UserRootName).$UserName.$Name";
        } else {
            return $null;
        }
    }

    <#
        .SYNOPSIS
        Gets the attributes of a configuration object.

        .PARAMETER Name
        The name of the configuration to get the attributes of.
    #>
    function Get-Attributes {
        param(
            [string] $Name
        )

        Invoke-ConfigScript "getAttributes $Name";
    }

    <#
        .SYNOPSIS
        Gets the names of the users to create.
    #>
    function Get-Users {
        [OutputType([string[]])]
        param()
        Get-Attributes "$(Get-UserRootName)";
    }

    <#
        .SYNOPSIS
        Gets the name of the setup user.
    #>
    function Get-SetupUser {
        [OutputType([string])]
        param()
        Get-Config "$(Get-ConfigRootName).setupUser.name";
    }

    <#
        .SYNOPSIS
        Gets the value of an option related to the setup.

        .PARAMETER Name
        The name of the option value to get.
    #>
    function Get-SetupOption {
        param(
            [string] $Name
        )

        $key = Get-SetupConfigKey;

        if ($key.GetValueNames().Contains($Name)) {
            return $key.GetValue($Name);
        } else {
            return $null;
        }
    }

    <#
        .SYNOPSIS
        Sets the value of an option related to the setup.

        .PARAMETER Name
        The name of the option to set.

        .PARAMETER Value
        The value to set the option to.
    #>
    function Set-SetupOption {
        param(
            [string] $Name,
            $Value
        )

        $key = Get-SetupConfigKey;
        $null = Set-ItemProperty ($key.PSPath) -Name $Name -Value $Value;
    }

    <#
        .SYNOPSIS
        Gets the name of the current setup stage.
    #>
    function Get-Stage {
        $stage = Get-SetupOption $stageOption;

        if ($null -ne $stage) {
            $stage = [SetupStage]$stage;
        }

        return $stage;
    }

    <#
        .SYNOPSIS
        Sets the current stage.

        .PARAMETER Name
        The name to set the current stage to.
    #>
    function Set-Stage {
        param(
            $Name
        )

        if (-not (($null -eq $Name) -or ($Name -is [string]))) {
            $Name = ([SetupStage]$Name).ToString();
        }

        $null = Set-SetupOption $stageOption $Name;
    }

    <#
        .SYNOPSIS
        Gets the current user to set up.
    #>
    function Get-CurrentUser {
        return (Get-SetupOption $userOption) ?? 0;
    }

    <#
        .SYNOPSIS
        Sets the index of the current user to set up.

        .PARAMETER Value
        The index of the user to set up.
    #>
    function Set-CurrentUser {
        param(
            [int] $Value
        )

        Set-SetupOption $userOption $value;
    }

    <#
        .SYNOPSIS
        Gets the name of the current stage of the user setup.
    #>
    function Get-UserStage {
        $stage = Get-SetupOption $userStageOption;

        if ($null -ne $stage) {
            $stage = [UserStage]$stage;
        }

        return $stage;
    }

    <#
        .SYNOPSIS
        Sets the current stage of the user setup.

        .PARAMETER Name
        The name of the stage to set.
    #>
    function Set-UserStage {
        param(
            $Name
        )

        if (-not (($null -eq $Name) -or ($Name -is [string]))) {
            $Name = ([UserStage]$Name).ToString();
        }

        $null = Set-SetupOption $userStageOption $Name;
    }

    <#
        .SYNOPSIS
        Gets the name of the microsoft account to create.
    #>
    function Get-MSAccountName {
        return Get-SetupOption $accountOption;
    }

    <#
        .SYNOPSIS
        Sets the name of the microsoft account to create.

        .PARAMETER Name
        The name of the microsoft account to create.
    #>
    function Set-MSAccountName {
        param(
            [string] $Name
        )

        Set-SetupOption $accountOption $Name;
    }

    <#
        .SYNOPSIS
        Gets a value indicating whether the setup has finished.
    #>
    function Get-IsFinished {
        return [bool](Get-SetupOption $finishedOption);
    }

    <#
        .SYNOPSIS
        Sets a value indicating whether the setup has finished.
    #>
    function Set-IsFinished {
        param(
            $Value
        )

        Set-SetupOption $finishedOption $true;
    }

    <#
        .SYNOPSIS
        Checks whether the specified software collection is enabled.

        .PARAMETER Name
        The name of the collection to check.
    #>
    function Test-Collection {
        param(
            [string] $Name
        )

        Get-Config "$(Get-ConfigRootName).software.$Name";
    }

    <#
        .SYNOPSIS
        Checks whether the running system is a QEMU virtual machine.
    #>
    function Test-Qemu {
        ((Get-WmiObject win32_computersystem).Manufacturer) -eq "QEMU";
    }

    <#
        .SYNOPSIS
        Checks whether the current user is the setup user.
    #>
    function Test-SetupUser {
        ($IsWindows ? $env:UserName : $env:USER) -eq (Get-SetupUser);
    }

    <#
        .SYNOPSIS
        Checks whether the active session is executed with admin rights.
    #>
    function Test-Admin {
        net session 2> $null | Out-Null;
        return $?;
    }
}