#!powershell

#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -PowerShell Ansible.ModuleUtils.CommandUtil

$module = [Ansible.Basic.AnsibleModule]::Create($args, @{
    options = @{
        dest = @{type = 'path'}
        repo = @{required = $true; aliases = @('name')}
        version = @{default = 'HEAD'; aliases = @('branch')}
        remote = @{default = 'origin'}
        recursive = @{default = $true; type = 'bool'}
        executable = @{default = $null; type = 'path'}
    }
    supports_check_mode = $false
})

$dest = $module.Params.dest
$repo = $module.Params.repo
$version = $module.Params.version
$remote = $module.Params.remote
$git = $module.Params.executable
if (!$git) {
    $git = Get-ExecutablePath 'git'
}

# ================================= Utilities ==================================

function Get-AbsolutePath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)] [String] $path
    )
    try {
        $result = Resolve-Path $path
    } catch {
        return $_[0].TargetObject
    }
    return $result
}

function Get-GitDir {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)] [String] $path
    )
    $git_dir = Join-Path $path '.git'
    # Check if this .git is a file.
    if ([System.IO.File]::Exists($git_dir)) {
        # Extract the gitdir: path from the .git file.
        $groups = Get-Content "$git_dir" | `
            Select-String '(gitdir:) (.*)' | `
            ForEach { $_.Matches[0].Groups[1..2] }
        $ref_prefix = $groups[0]
        $gitdir = $groups[1]
        if ($ref_prefix -ne 'gitdir:') {
            $module.FailJson('The .git file has invalid gitdir reference format.')
        }
        # Check if the repo path is absolute.
        if ([System.IO.Path]::IsPathRooted($gitdir)) {
            $git_dir = $gitdir
        } else {
            # Join with the input path to construct an absolute path.
            $git_dir = Join-Path $path $gitdir
        }
        if (![System.IO.Directory]::Exists($git_dir)) {
            throw "$git_dir is not a directory."
        }
    }
    return $git_dir
}

function Test-GitLocalChanges {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)] [String] $dest
    )
    $command = "`"$git`" status --porcelain"
    $result = Run-Command -command $command -working_directory $dest
    $changes = $result.stdout.Split([System.Environment]::NewLine, `
            [System.StringSplitOptions]::RemoveEmptyEntries) | `
        Where-Object { -not $_.StartsWith('??') } | Measure-Object -Line
    return $changes.Lines -gt 0
}

function Get-GitRemoteHeadBranch {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [String] $dest,
        [Parameter(Mandatory = $true)] [String] $remote
    )
    $command = "`"$git`" symbolic-ref --short refs/remotes/$remote/HEAD"
    $result = Run-Command -command $command -working_directory $dest
    if ($result.rc -ne 0) {
        $module.FailJson("Could not determine the default HEAD branch of remote: $remote" ` +
                         "$result.stdout $result.stderr")
    }
    return $result.stdout.Trim().Replace("$remote/", '')
}

function Get-GitCurrentSha {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [String] $dest
    )
    $command = "`"$git`" rev-parse HEAD"
    $result = Run-Command -command $command -working_directory $dest
    return $result.stdout.Trim()
}

function Invoke-GitClone {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [String] $repo,
        [Parameter(Mandatory = $true)] [String] $remote,
        [Parameter(Mandatory = $true)] [String] $dest
    )
    $dest_parent = Split-Path -Path $dest -Parent
    if (!(Test-Path $dest_parent)) {
        New-Item -Path $dest_parent -ItemType Directory
    }
    $command = "`"$git`" clone --recursive --origin $remote $repo $dest"
    if ($version -ne "HEAD") {
        $command += " --branch $version"
    }
    $result = Run-Command -command $command -working_directory $cwd
    if ($result.rc -ne 0) {
        $module.FailJson($result.stderr)
    }
}

function Invoke-GitCheckout {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [String] $dest,
        [Parameter(Mandatory = $true)] [String] $remote,
        [Parameter(Mandatory = $true)] [String] $version
    )
    if ($version -eq "HEAD") {
        $branch = Get-GitRemoteHeadBranch $dest $remote
    } else {
        $branch = $version
    }
    $result = Run-Command -command "`"$git`" checkout $branch" -working_directory $dest
    if ($result.rc -ne 0) {
        $module.FailJson("Failed to checkout version '$version'`n" + $result.stderr)
    }
}

function Invoke-GitFetch {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [String] $dest,
        [Parameter(Mandatory = $true)] [String] $remote,
        [Parameter(Mandatory = $true)] [String] $version
    )
    $command = "`"$git`" fetch --tags $remote"
    $result = Run-Command -command $command -working_directory $dest
    if ($result.rc -ne 0) {
        $module.FailJson("Failed to download remote objects and refs:`n" + `
                         $result.stderr)
    }
}

function Invoke-GitPull {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [String] $dest,
        [Parameter(Mandatory = $true)] [String] $remote,
        [Parameter(Mandatory = $true)] [String] $version
    )
    $result = Run-Command -command "`"$git`" pull $remote $version" -working_directory $dest
    if ($result.rc -ne 0) {
        $module.FailJson("Failed to pull version '$version' from remote '$origin':`n" + `
                         $result.stderr)
    }
}

function Invoke-GitSubmoduleUpdate {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [String] $dest
    )
    $result = Run-Command -command "`"$git`" submodule update --init" -working_directory $dest
    if ($result.rc -ne 0) {
        $module.FailJson("Failed to initialized/update submodules:`n" + $result.stderr)
    }
}

# ================================ Start logic =================================

if (!$dest) {
    $module.FailJson('The destination directory must be specified.')
}
$dest = Get-AbsolutePath $dest
$git_dir = Get-GitDir $dest
$gitconfig = Join-Path $git_dir 'config'

$module.Result.before = $null

if (($dest -and ![System.IO.File]::Exists($gitconfig))) {
    Invoke-GitClone $repo $remote $dest
    Invoke-GitCheckout $dest $remote $version
    $module.Result.changed = $true
} else {
    $module.Result.before = Get-GitCurrentSha $dest
    if (Test-GitLocalChanges $dest) {
        $module.FailJson('Local modifications exist in repository.')
    }
    Invoke-GitFetch $dest $remote $version
    Invoke-GitCheckout $dest $remote $version
    Invoke-GitPull $dest $remote $version
    $module.Result.after = Get-GitCurrentSha $dest
    if ($module.Result.before -ne $module.Result.after) {
        $module.Result.changed = $true
    }
}

if ($recursive) {
    Invoke-GitSubmoduleUpdate $dest
}

# Ensure the repository has the correct owner
$userName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$idRef = [System.Security.Principal.NTAccount]::new($userName)
Get-Item -Force $dest | foreach { `
    $_ ; $_ | Get-ChildItem -Force -Recurse `
} | foreach { `
    $acl = $_ | Get-Acl; $acl.SetOwner($idRef); $_ | Set-Acl -AclObject $acl `
}

$module.ExitJson()