# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

using namespace System.Collections.Generic
using namespace System.Management.Automation

$crontabcmd = "/usr/bin/crontab"

class CronJob {
    [string] $Minute
    [string] $Hour
    [string] $DayOfMonth
    [string] $Month
    [string] $DayOfWeek
    [string] $Command

    [string] ToString()
    {
        return "{0} {1} {2} {3} {4} {5}" -f
            $this.Minute, $this.Hour, $this.DayOfMonth, $this.Month, $this.DayOfWeek, $this.Command
    }
}

# Internal helper functions

function Get-CronTab ([String] $user) {
    $crontab = Invoke-CronTab -user $user -arguments "-l" -noThrow
    if ($crontab -is [ErrorRecord]) {
        if ($crontab.Exception.Message.StartsWith("no crontab for ")) {
            $crontab = @()
        }
        else {
            throw $crontab.Exception
        }
    }
    [string[]] $crontab
}

function ConvertTo-CronJob ([String] $crontab) {
    $split = $crontab -split " ", 6
    $cronjob = [CronJob]@{
        Minute = $split[0];
        Hour = $split[1];
        DayOfMonth= $split[2];
        Month =$split[3];
        DayOfWeek = $split[4];
        Command = $split[5]
    }
    $cronjob
}

function Invoke-CronTab ([String] $user, [String[]] $arguments, [Switch] $noThrow) {
    If ($user -ne [String]::Empty) {
        $arguments = Write-Output "-u" $UserName $arguments
    }

    Write-Verbose "Running: $crontabcmd $arguments"
    $output = & $crontabcmd @arguments 2>&1
    if ($LASTEXITCODE -ne 0 -and -not $noThrow) {
        $e = New-Object System.InvalidOperationException -ArgumentList $output.Exception.Message
        throw $e
    } else {
        $output
    }
}

function Import-CronTab ([String] $user, [String[]] $crontab) {
    $temp = New-TemporaryFile
    [String]::Join([Environment]::NewLine,$crontab) | Set-Content $temp.FullName
    Invoke-CronTab -user $user $temp.FullName
    Remove-Item $temp
}

# Public functions

function Remove-CronJob {
<#
.SYNOPSIS
  Removes the exactly matching cron job from the cron table
.DESCRIPTION
  Removes the exactly matching cron job from the cron table
.EXAMPLE
  Get-CronJob | Where-Object {%_.Command -like 'foo *'} | Remove-CronJob
.RETURNVALUE
  None
.PARAMETER UserName
  Optional parameter to specify a specific user's cron table
.PARAMETER Job
  Cron job object returned from Get-CronJob
.PARAMETER Force
  Don't prompt when removing the cron job
#>
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")]
    param (
        [ArgumentCompleter( { $wordToComplete = $args[2]; Get-CronTabUser | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object } )]
        [Alias("u")]
        [Parameter(Mandatory=$false)]
        [String]
        $UserName,

        [Alias("j")]
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [CronJob]
        $Job,

        [Switch]
        $Force
    )
    process {

        [string[]] $crontab = Get-CronTab -user $UserName
        $newcrontab = [List[string]]::new()
        $found = $false

        $JobAsString = $Job.ToString()
        foreach ($line in $crontab) {
            if ($JobAsString -ceq $line) {
                $found = $true
            } else {
                $newcrontab.Add($line)
            }
        }

        if (-not $found) {
            $e = New-Object System.Exception -ArgumentList "Job not found"
            throw $e
        }
        if ($Force -or $PSCmdlet.ShouldProcess($Job.Command,"Remove")) {
            Import-CronTab -user $UserName -crontab $newcrontab
        }
    }
}

function New-CronJob {
<#
.SYNOPSIS
  Create a new cron job
.DESCRIPTION
  Create a new job in the cron table.  Date and time parameters can be specified
  as ranges such as 10-30, as a list: 5,6,7, or combined 1-5,10-15.  An asterisk
  means 'first through last' (the entire allowed range).  Step values can be used
  with ranges or with an asterisk.  Every 2 hours can be specified as either
  0-23/2 or */2.
.EXAMPLE
  New-CronJob -Minute 10-30 -Hour 10-20/2 -DayOfMonth */2 -Command "/bin/bash -c 'echo hello' > ~/hello"
.RETURNVALUE
  If successful, an object representing the cron job is returned
.PARAMETER UserName
  Optional parameter to specify a specific user's cron table
.PARAMETER Minute
  Valid values are 0 to 59.  If not specified, defaults to *.
.PARAMETER Hour
  Valid values are 0-23.  If not specified, defaults to *.
.PARAMETER DayOfMonth
  Valid values are 1-31.  If not specified, defaults to *.
.PARAMETER Month
  Valid values are 1-12.  If not specified, defaults to *.
.PARAMETER DayOfWeek
  Valid values are 0-7.  0 and 7 are both Sunday.  If not specified, defaults to *.
.PARAMETER Command
  Command to execute at the scheduled time and day.
#>
    [CmdletBinding()]
    param (
        [ArgumentCompleter( { $wordToComplete = $args[2]; Get-CronTabUser | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object } )]
        [Alias("u")]
        [Parameter(Mandatory=$false)]
        [String]
        $UserName,

        [Alias("mi")][Parameter(Position=1)][String[]] $Minute = "*",
        [Alias("h")][Parameter(Position=2)][String[]] $Hour = "*",
        [Alias("dm")][Parameter(Position=3)][String[]] $DayOfMonth = "*",
        [Alias("mo")][Parameter(Position=4)][String[]] $Month = "*",
        [Alias("dw")][Parameter(Position=5)][String[]] $DayOfWeek = "*",
        [Alias("c")][Parameter(Mandatory=$true,Position=6)][String] $Command
    )
    process {
        # TODO: validate parameters, note that different versions of crontab support different capabilities
        $line = "{0} {1} {2} {3} {4} {5}" -f [String]::Join(",",$Minute), [String]::Join(",",$Hour),
            [String]::Join(",",$DayOfMonth), [String]::Join(",",$Month), [String]::Join(",",$DayOfWeek), $Command
        [string[]] $crontab = Get-CronTab -user $UserName
        $crontab += $line
        Import-CronTab -User $UserName -crontab $crontab
        ConvertTo-CronJob -crontab $line
    }
}

function Get-CronJob {
<#
.SYNOPSIS
  Returns the current cron jobs from the cron table
.DESCRIPTION
  Returns the current cron jobs from the cron table
.EXAMPLE
  Get-CronJob -UserName Steve
.RETURNVALUE
  CronJob objects
.PARAMETER UserName
  Optional parameter to specify a specific user's cron table
#>
    [CmdletBinding()]
    [OutputType([CronJob])]
    param (
        [Alias("u")][Parameter(Mandatory=$false)][String] $UserName
    )
    process {
        $crontab = Get-CronTab -user $UserName
        ForEach ($line in $crontab) {
            if ($line.Trim().Length -gt 0)
            {
                ConvertTo-CronJob -crontab $line
            }
        }
    }
}

function Get-CronTabUser {
<#
.SYNOPSIS
  Returns the users allowed to use crontab
#>
    [CmdletBinding()]
    [OutputType([String])]
    param()

    $allow = '/etc/cron.allow'
    if (Test-Path $allow)
    {
        Get-Content $allow
    }
    else
    {
        $users = Get-Content /etc/passwd | ForEach-Object { ($_ -split ':')[0] }
        $deny = '/etc/cron.deny'
        if (Test-Path $deny)
        {
            $denyUsers = Get-Content $deny
            $users | Where-Object { $denyUsers -notcontains $_ }
        }
        else
        {
            $users
        }
    }
}
