WrikeExplorer.psm1


# GENERAL
enum WrikeStatus {
    Scheduled
    InProgress
    Completed
    Cancelled
    Failed
}

# BI EXPORT
class WrikeDataExportResource {
    [string]$Name
    [uri]$Url
}

class WrikeDataExport {
    [string]$Id
    [datetime]$CompletedDate
    [WrikeStatus]$Status
    [WrikeDataExportResource[]]$Resources
}

enum WrikeColumnDataType {
    Number
    String
    Date
    Boolean
}

class WrikeDataExportColumn {
    [string] $Id
    [string] $Alias
    [WrikeColumnDataType] $DataType
    [object] $ForeignKey
}

class WrikeDataExportSchema {
    [string] $Id
    [string] $Alias
    [WrikeDataExportColumn[]] $Columns
}

# CONTACTS
enum WrikeUserType {
    Person
    Group
}

class WrikeUserProfile {
    [string] $AccountId
    [string] $Email
    [string] $Role
    [bool] $External
    [bool] $Admin
    [bool] $Owner
}

class WrikeContact {
    [string] $Id
    [string] $FirstName
    [string] $LastName
    [WrikeUserType] $Type
    [WrikeUserProfile[]] $Profiles
    [uri] $AvatarUrl
    [string] $TimeZone
    [string] $Locale
    [bool] $Deleted
    [bool] $Me
    [string[]] $MemberIds
    [object] $Metadata
    [bool] $MyTeam
    [string] $Title
    [string] $CompanyName
    [string] $Phone
    [string] $Location
    [string] $WorkScheduleId
    [object] $CurrentBillRate
    [object] $CurrentCostRate
}

enum WrikeRateSource {
    User
    JobRole
}

class WrikeBudgetRateChangeHistory {
    [WrikeRateSource] $RateSource
    [double] $RateValue
    [datetime] $StartDate
    [datetime] $EndDate
}

class WrikeContactHistory {
    [string] $Id
    [WrikeBudgetRateChangeHistory[]] $BillRateHistory
    [WrikeBudgetRateChangeHistory[]] $CostRateHistory
}

class WrikeErrorDescription {
    [string] $Error
    [string] $ErrorDescription
}


class WrikeApiVersion {
    [int] $Major
    [int] $Minor
}

enum WrikeProjectStatus {
    Green
    Yellow
    Red
    Completed
    OnHold
    Cancelled
    Custom
}

enum WrikeContractType {
    Billable
    NonBillable
}

class WrikeProject {
    [string] $AuthorId
    [string[]] $OwnerIds
    [WrikeProjectStatus] $Status
    [string] $CustomStatusId
    [datetime] $StartDate
    [datetime] $EndDate
    [datetime] $CreatedDate
    [datetime] $CompletedDate
    [WrikeContractType] $ContractType
    [object] $Finance
}

enum WrikeFolderTreeScope {
    WsRoot      # Virtual root folder of account
    RbRoot      # Virtual Recycle Bin folder of account
    WsFolder    # Folder in account
    RbFolder    # Folder is in Recycle Bin (deleted folder)
    WsTask      # Task in account
    RbTask      # Task is in Recycle Bin (deleted task)
}

enum WrikeColor {
    None
    Person
    Purple1
    Purple2
    Purple3
    Purple4
    Indigo1
    Indigo2
    Indigo3
    Indigo4
    DarkBlue1
    DarkBlue2
    DarkBlue3
    DarkBlue4
    Blue1
    Blue2
    Blue3
    Blue4
    Turquoise1
    Turquoise2
    Turquoise3
    Turquoise4
    DarkCyan1
    DarkCyan2
    DarkCyan3
    DarkCyan4
    Green1
    Green2
    Green3
    Green4
    YellowGreen1
    YellowGreen2
    YellowGreen3
    YellowGreen4
    Yellow1
    Yellow2
    Yellow3
    Yellow4
    Orange1
    Orange2
    Orange3
    Orange4
    Red1
    Red2
    Red3
    Red4
    Pink1
    Pink2
    Pink3
    Pink4
    Gray1
    Gray2
    Gray3
}

class WrikeFolderTree {
    [string] $Id
    [string] $Title
    [string] $Color
    [string[]] $ChildIds
    [WrikeFolderTreeScope] $Scope
    [WrikeProject] $Project
    [bool] $Space
}

class WrikeFolder {
    [string] $Id
    [string] $AccountId
    [string] $Title
    [datetime] $CreatedDate
    [datetime] $UpdatedDate
    [string] $BriefDescription
    [string] $Description
    [WrikeColor] $Color
    [string[]] $SharedIds
    [string[]] $ParentIds
    [string[]] $ChildIds
    [string[]] $SuperParentIds
    [WrikeFolderTreeScope] $Scope
    [bool] $HasAttachments
    [int] $AttachmentCount
    [string] $Permalink
    [string] $WorkflowId
    [object[]] $Metadata
    [object[]] $CustomFields
    [string[]] $CustomColumnIds
    [WrikeProject] $Project
}


enum WrikeFieldComparator {
    EqualTo
    IsEmpty
    IsNotEmpty
    LessThan
    LessOrEqualTo
    GreaterThan
    GreaterOrEqualTo
    InRange
    NotInRange
    Contains
    StartsWith
    EndsWith
    ContainsAll
    ContainsAny
}

class WrikeFieldFilter {
    [string] $Id
    [WrikeFieldComparator] $Comparator
    [string[]] $Value
    [string] $MinValue
    [string] $MaxValue

    [string] ToString() {
        $obj = [ordered]@{ id = $this.Id; comparator = $this.Comparator.ToString() }
        if ($null -ne $this.Value -and $this.Value.Count -gt 0) {
            if ($this.Value.Count -eq 1) {
                $obj.value = $this.Value[0]
            }
            else {
                $obj.values = $this.Value
            }
        }
        else {
            if (![string]::IsNullOrWhiteSpace($this.MinValue)) {
                $obj.minValue = $this.MinValue
            }
            if (![string]::IsNullOrWhiteSpace($this.MaxValue)) {
                $obj.maxValue = $this.MaxValue
            }
        }
        return ($obj | ConvertTo-Json -Compress)
    }
}


enum WrikeCurrency {
    USD
    EUR
    GBP
    RUB
    BRL
    AED
    ARS
    BYR
    CAD
    CLP
    COP
    CZK
    DKK
    HKD
    HUF
    INR
    IDR
    ILS
    JPY
    KRW
    MYR
    MXN
    NZD
    NOK
    PEN
    PHP
    PLN
    QAR
    RON
    SAR
    SGD
    ZAR
    SEK
    CHF
    TWD
    THB
    TRY
    UAH
    VND
    CNY
    AUD
    AMD
    BWP
}

enum WrikeAggregationType {
    None
    Sum
    Average
}

enum WrikeCustomFieldType {
    Text
    DropDown
    Numeric
    Currency
    Percentage
    Date
    Duration
    Checkbox
    Contacts
    Multiple
}

class WrikeFieldType {
    static [WrikeCustomFieldType[]] $Comparable = @( [WrikeCustomFieldType]::Text, [WrikeCustomFieldType]::DropDown, [WrikeCustomFieldType]::Numeric, [WrikeCustomFieldType]::Currency, [WrikeCustomFieldType]::Percentage, [WrikeCustomFieldType]::Date, [WrikeCustomFieldType]::Duration )
    static [WrikeCustomFieldType[]] $String = @( [WrikeCustomFieldType]::Text, [WrikeCustomFieldType]::DropDown )
    static [WrikeCustomFieldType[]] $Collection = @( [WrikeCustomFieldType]::Contacts, [WrikeCustomFieldType]::Multiple )
    static [WrikeCustomFieldType[]] $Boolean = @( [WrikeCustomFieldType]::Checkbox )
}

class WrikeCustomFieldSettings {
    [string] $InheritanceType
    [int] $DecimalPlaces
    [bool] $UseThousandsSeparator
    [WrikeCurrency] $Currency
    [WrikeAggregationType] $Aggregation
    [string[]] $Values
    [bool] $AllowOtherValues
    [string[]] $Contacts
    [bool] $ReadOnly
    [bool] $AllowTime
}

class WrikeCustomField {
    [string] $Id
    [string] $AccountId
    [string] $Title
    [WrikeCustomFieldType] $Type
    [string] $SpaceId
    [string[]] $SharedIds
    [WrikeCustomFieldSettings] $Settings
}

class WrikeProjectHistory {
    [object[]] $ActualFees
    [object[]] $ActualCost
    [object[]] $PlannedFees
    [object[]] $PlannedCost
    [object[]] $Budget
}

class WrikeFolderHistory {
    [string] $Id
    [WrikeProjectHistory] $Project
}
function ConvertFrom-WrikeErrorDescription {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [WrikeErrorDescription]
        $WrikeErrorDescription
    )

    process {
        $categoryMap = @{
            invalid_request = 'ProtocolError'
            invalid_parameter = 'InvalidArgument'
            parameter_required = 'InvalidData'
            not_authorized = 'SecurityError'
            access_forbidden = 'PermissionDenied'
            not_allowed = 'PermissionDenied'
            resource_not_found = 'ObjectNotFound'
            method_not_found = 'InvalidOperation'
            too_many_requests = 'LimitsExceeded'
            rate_limit_exceeded = 'LimitsExceeded'
            server_error = 'NotSpecified'
        }
        $e = [exception]::new($WrikeErrorDescription.ErrorDescription)
        $category = [System.Management.Automation.ErrorCategory]::($categoryMap.($WrikeErrorDescription.Error))
        $record = [System.Management.Automation.ErrorRecord]::new($e, $WrikeErrorDescription.Error, $category, $null)
        Write-Output $record
    }
}
function ConvertTo-PlainText {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [securestring]
        $Secret
    )

    process {
        Write-Output ([pscredential]::new('a', $Secret).GetNetworkCredential().Password)
    }
}
function ConvertTo-TimestampInterval {
    [CmdletBinding()]
    param (
        [Parameter()]
        [Nullable[datetime]]
        $Start,

        [Parameter()]
        [Nullable[datetime]]
        $End,

        [Parameter()]
        [switch]
        $AsJson
    )

    process {
        if ($null -ne $Start -and $null -ne $End -and $End -le $Start) {
            Write-Error 'Timestamp for end cannot be less than or equal to timestamp for start.'
            return
        }
        $interval = @{}
        if ($MyInvocation.BoundParameters.ContainsKey('Start') -and $null -ne $Start) {
            $interval.start = $Start.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
        }
        if ($MyInvocation.BoundParameters.ContainsKey('End') -and $null -ne $End) {
            $interval.end = $End.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
        }
        if ($AsJson) {
            $interval | ConvertTo-Json -Compress
        }
        else {
            Write-Output $interval
        }
    }
}
function Get-StandardHeaders {
    [CmdletBinding()]
    param ()

    process {
        $headers = @{
            Authorization = "Bearer $($script:Config.Wrike.AccessToken | ConvertTo-PlainText)"
        }
        Write-Output $headers
    }
}
function Import-AccessToken {
    [CmdletBinding()]
    param ()

    process {
        $path = Join-Path $script:Config.AppDataPath 'accesstoken.txt'
        if (-not (Test-Path -Path $path)) {
            return
        }
        try {
            $token = Get-Content -Path $path | ConvertTo-SecureString
            $script:Config.Wrike.AccessToken = $token
        }
        catch {
            Write-Error "Failed to import Wrike API App access token from '$path'. Consider deleting the file and using Set-WrikeAccessToken again."
        }
    }
}
function Remove-AccessToken {
    [CmdletBinding()]
    param (
    )

    process {
        $script:Config.Wrike.AccessToken = $null
        $path = Join-Path $script:Config.AppDataPath 'accesstoken.txt'
        Remove-Item -Path $path -Force
    }
}
function Save-AccessToken {
    [CmdletBinding()]
    param (
        # Specifies the API App access token to use for authentication
        [Parameter(Mandatory)]
        [securestring]
        $AccessToken,

        # Specifies that the token should not be persisted to disk
        [Parameter()]
        [switch]
        $Ephemeral
    )

    process {
        $script:Config.Wrike.AccessToken = $AccessToken
        if (-not $Ephemeral) {
            $path = Join-Path $script:Config.AppDataPath 'accesstoken.txt'
            $AccessToken | ConvertFrom-SecureString | Set-Content -Path $path
        }
    }
}
function ValidateAccessToken {
    [CmdletBinding()]
    param ()

    process {
        if ([string]::IsNullOrWhiteSpace($script:Config.Wrike.AccessToken)) {
            throw "Wrike API App access token has not been set. Please use Set-WrikeAccessToken."
        }
    }
}
function Clear-WrikeAccessToken {
    [CmdletBinding(SupportsShouldProcess)]
    param (
    )

    process {
        if ($PSCmdlet.ShouldProcess("Wrike API Access Token", "Clear")) {
            Remove-AccessToken
        }
    }
}

function Get-WrikeApiVersion {
    [CmdletBinding()]
    param (
    )

    process {
        Invoke-WrikeApi -Path version -ResponseType version
    }
}
function Get-WrikeContact {
    [CmdletBinding()]
    [OutputType([WrikeContact])]
    param (
        # Specifies one or more optional ID values for known contacts
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateCount(0, 100)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Id,

        # Specifies that the Wrike Contact record for the current user should be returned
        [Parameter()]
        [Alias('CurrentUser')]
        [switch]
        $Me,

        # Specifies that only deleted Wrike Contact records should be returned
        [Parameter()]
        [switch]
        $Deleted,

        # Specifies a set of optional fields to be included in the Wrike Contact response model. Normally these values are empty.
        [Parameter()]
        [ValidateSet('metadata', 'workScheduleId', 'currentBillRate', 'currentCostRate', IgnoreCase = $false)]
        [string[]]
        $Include
    )

    process {
        $query = @{}
        if ($MyInvocation.BoundParameters.ContainsKey('Me')) {
            $query.me = $Me.ToString().ToLower()
        }
        if ($MyInvocation.BoundParameters.ContainsKey('Deleted')) {
            $query.deleted = $Deleted.ToString().ToLower()
        }
        if ($MyInvocation.BoundParameters.ContainsKey('Include')) {
            # De-duplicate the list of optional fields
            $Include = $Include | Group-Object | Select-Object -ExpandProperty Name
            $json = $Include | ConvertTo-Json -Compress
            if ($Include.Count -lt 2) {
                $json = "[$json]"
            }
            $query.fields = $json
        }
        $path = "contacts"
        if ($null -ne $Id -and $Id.Count -gt 0) {
            $path += '/' + [string]::Join(',', $Id)
        }
        Invoke-WrikeApi -Path $path -ResponseType contacts -Query $query
    }
}
function Get-WrikeContactHistory {
    [CmdletBinding()]
    [OutputType([WrikeContactHistory])]
    param (
        # Specifies one or more optional ID values for known contacts
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateCount(1, 100)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Id,

        # Specifies the inclusive start of the time range for the contact history request
        [Parameter()]
        [datetime]
        $UpdatedAfter,

        # Specifies the inclusive end of the time range for the contact history request
        [Parameter()]
        [datetime]
        $UpdatedBefore,

        # Specifies a set of optional fields to be included in the Wrike Contact response model. Normally these values are empty.
        [Parameter()]
        [ValidateSet('billRate', 'costRate', IgnoreCase = $false)]
        [string[]]
        $Include
    )

    process {
        if ($null -ne $UpdatedAfter -and $null -ne $UpdatedBefore -and $UpdatedBefore -le $UpdatedAfter) {
            Write-Error 'UpdatedBefore cannot be less than or equal to UpdatedAfter.'
            return
        }

        $query = @{}
        if ($MyInvocation.BoundParameters.ContainsKey('Include')) {
            # De-duplicate the list of optional fields
            $Include = $Include | Group-Object | Select-Object -ExpandProperty Name
            $json = $Include | ConvertTo-Json -Compress
            if ($Include.Count -lt 2) {
                $json = "[$json]"
            }
            $query.fields = $json
        }

        if ($MyInvocation.BoundParameters.ContainsKey('UpdatedAfter') -or $MyInvocation.BoundParameters.ContainsKey('UpdatedBefore')) {
            $query.updatedDate = ConvertTo-TimestampInterval -Start $UpdatedAfter -End $UpdatedBefore -AsJson
        }

        $path = 'contacts'
        if ($null -ne $Id -and $Id.Count -gt 0) {
            $path += '/' + [string]::Join(',', $Id)
        }
        $path += '/contacts_history'
        Invoke-WrikeApi -Path $path -ResponseType contactsHistory -Query $query
    }
}
function Get-WrikeCustomField {
    [CmdletBinding()]
    [OutputType([WrikeCustomField])]
    param (
        # Specifies one or more optional id's to limit the results.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateCount(0, 100)]
        [string[]]
        $Id
    )

    process {
        $path = 'customfields'
        if ($null -ne $Id -and $Id.Count -gt 0) {
            $path += '/' + [string]::Join(',', $Id)
        }
        Invoke-WrikeApi -Path $path -ResponseType 'customfields'
    }
}
function Get-WrikeDataExport {
    [CmdletBinding()]
    [OutputType([WrikeDataExport])]
    param()

    process {
        Invoke-WrikeApi -Path 'data_export' -ResponseType dataExport
    }
}
function Get-WrikeDataExportSchema {
    [CmdletBinding()]
    param ()

    process {
        Invoke-WrikeApi -Path 'data_export_schema' -ResponseType dataExportSchema
    }
}
function Get-WrikeExplorerConfig {
    [CmdletBinding()]
    param (

    )

    process {
        Write-Output $script:Config
    }
}
function Get-WrikeFolder {
    [CmdletBinding()]
    param (
        # Specifies one or more optional folder ID values for known folders.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Id,

        # Specifies the permalink url for the folder
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $Permalink,

        # Specifies a metadata filter key
        [Parameter()]
        [ValidateLength(1, 50)]
        [ValidatePattern('[A-Za-z0-9_-]+')]
        [string]
        $Key,

        # Specifies a metadata filter value
        [Parameter()]
        [ValidateLength(0,1000)]
        [string]
        $Value,

        # Specifies an optional custom field filter to limit the search scope. Use New-WrikeFieldFilter to compose a filter.
        [Parameter()]
        [WrikeFieldFilter]
        $FieldFilter,

        # Specifies which type of records to return, projects, folders, or all. Default is 'All'.
        [Parameter()]
        [ValidateSet('Projects', 'Folders', 'All')]
        [string]
        $Type = 'All',

        # Specifies the inclusive last-updated time. Only records last updated after this time will be included in the search scope.
        [Parameter()]
        [datetime]
        $UpdatedAfter,

        # Specifies the inclusive last-updated time. Only records last updated before this time will be included in the search scope.
        [Parameter()]
        [datetime]
        $UpdatedBefore,

        # Specifies that only deleted records should be returned.
        [Parameter()]
        [switch]
        $Deleted,

        # Specifies that descendant folders should not be included in the search scope.
        [Parameter()]
        [switch]
        $NoDescendants,

        # Specifies one or more contract types to include in the search scope.
        [Parameter()]
        [WrikeContractType[]]
        $ContractType,

        # Specifies a set of optional fields to be included in the response model.
        [Parameter()]
        [ValidateSet('metadata', 'hasAttachments', 'attachmentCount', 'description', 'briefDescription', 'customFields', 'customColumnIds', 'superParentIds', 'space', 'contractType', IgnoreCase = $false)]
        [string[]]
        $Include
    )

    process {
        if ($Id.Count -gt 100) {
            Write-Error 'The Wrike API imposes a limit of 100 IDs in a query.'
            return
        }
        if ($null -ne $UpdatedAfter -and $null -ne $UpdatedBefore -and $UpdatedBefore -le $UpdatedAfter) {
            Write-Error 'UpdatedBefore cannot be less than or equal to UpdatedAfter.'
            return
        }

        $query = @{}
        if ($MyInvocation.BoundParameters.ContainsKey('Permalink')) {
            $query.permalink = $Permalink
        }
        if ($MyInvocation.BoundParameters.ContainsKey('NoDescendants')) {
            $query.descendants = (!$NoDescendants).ToString().ToLower()
        }
        if ($MyInvocation.BoundParameters.ContainsKey('Key')) {
            $query.metadata = @{ key = $Key; value = $Value } | ConvertTo-Json -Compress
        }
        if ($MyInvocation.BoundParameters.ContainsKey('FieldFilter')) {
            $query.customField = $FieldFilter.ToString()
        }
        if ($MyInvocation.BoundParameters.ContainsKey('UpdatedAfter') -or $MyInvocation.BoundParameters.ContainsKey('UpdatedBefore')) {
            $query.updatedDate = ConvertTo-TimestampInterval -Start $UpdatedAfter -End $UpdatedBefore -AsJson
        }
        if ($Type -ne 'All') {
            $query.project = ($Type -eq 'Project').ToString().ToLower()
        }
        if ($MyInvocation.BoundParameters.ContainsKey('Deleted')) {
            $query.deleted = $Deleted.ToString().ToLower()
        }
        if ($MyInvocation.BoundParameters.ContainsKey('ContractType')) {
            $selectedTypes = $ContractType | Foreach-Object { $_.ToString() } | Group-Object | Select-Object -ExpandProperty Name
            $json = $selectedTypes | ConvertTo-Json -Compress
            if ($selectedTypes.Count -lt 2) {
                $json = "[$json]"
            }
            $query.contractTypes = $json
        }
        if ($MyInvocation.BoundParameters.ContainsKey('Include')) {
            $Include = $Include | Group-Object | Select-Object -ExpandProperty Name
            $json = $Include | ConvertTo-Json -Compress
            if ($Include.Count -lt 2) {
                $json = "[$json]"
            }
            $query.fields = $json
        }

        $path = 'folders'
        $responseType = 'folderTree'
        if ($null -ne $Id -and $Id.Count -gt 0) {
            $path += '/' + [string]::Join(',', $Id)
            $responseType = 'folders'
        }

        if (($query.Keys | Where-Object { $_ -in @('permalink', 'descendants', 'metadata', 'customField', 'updatedDate', 'project', 'contractTypes') })) {
            $responseType = 'folders'
        }

        Invoke-WrikeApi -Path $path -ResponseType $responseType -Query $query
    }
}
function Get-WrikeFolderHistory {
    [CmdletBinding()]
    [OutputType([WrikeFolderHistory])]
    param (
        # Specifies one or more ID's for known records records.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [ValidateCount(1, 100)]
        [string[]]
        $Id,

        # Specifies the inclusive start of the time range for the contact history request
        [Parameter()]
        [datetime]
        $UpdatedAfter,

        # Specifies the inclusive end of the time range for the contact history request
        [Parameter()]
        [datetime]
        $UpdatedBefore,

        # Specifies a set of optional fields to be included in the Wrike Contact response model. Normally these values are empty.
        [Parameter()]
        [ValidateSet('actualFees', 'actualCost', 'plannedFees', 'plannedCost', 'budget', IgnoreCase = $false)]
        [string[]]
        $Include
    )

    process {
        $query = @{}
        if ($MyInvocation.BoundParameters.ContainsKey('Include')) {
            $Include = $Include | Group-Object | Select-Object -ExpandProperty Name
            $json = $Include | ConvertTo-Json -Compress
            if ($Include.Count -lt 2) {
                $json = "[$json]"
            }
            $query.fields = $json
        }
        if ($MyInvocation.BoundParameters.ContainsKey('UpdatedAfter') -or $MyInvocation.BoundParameters.ContainsKey('UpdatedBefore')) {
            $query.updatedDate = ConvertTo-TimestampInterval -Start $UpdatedAfter -End $UpdatedBefore -AsJson
        }

        $path = 'folders'
        if ($null -ne $Id -and $Id.Count -gt 0) {
            $path += '/' + [string]::Join(',', $Id)
        }
        $path += '/folders_history'
        Invoke-WrikeApi -Path $path -ResponseType foldersHistory -Query $query
    }
}
function Invoke-WrikeApi {
    [CmdletBinding()]
    param (
        # Specifies the HTTP method to use for the API call
        [Parameter()]
        [ValidateSet('Get', 'Post', 'Delete', 'Put', 'Patch', 'Head', 'Options', 'Merge', 'Trace', 'Default')]
        [string]
        $Method = 'Get',

        # Specifies the full, or base uri to use for the Wrike API call. By default this will be the BaseUri from the module's configuration file.
        [Parameter()]
        [Uri]
        $Uri = [uri]$script:Config.Wrike.BaseUri,

        # Specifies the relative path of the Wrike API to call
        [Parameter()]
        [string]
        $Path = [string]::Empty,

        # Specifies the query string keys and values if applicable
        [Parameter()]
        [hashtable]
        $Query = @{},

        # Specifies any additional headers needed. The Authorization header will be added for you, and if you supply it manually it will be overwritten.
        [Parameter()]
        [hashtable]
        $Headers = @{},

        # Specifies the body for the API call if applicable
        [Parameter()]
        [object]
        $Body,

        # Specifies the path to save files for Wrike API calls that are intended to download data
        [Parameter()]
        [string]
        $OutFile,

        # Specifies the expected response type. This determines the class used to parse the results into a strong type.
        [Parameter()]
        [string]
        $ResponseType
    )

    begin {
        ValidateAccessToken
    }

    process {
        if ($MyInvocation.BoundParameters.ContainsKey('ResponseType') -and -not $script:ResponseType.ContainsKey($ResponseType)) {
            throw "Unexpected ResponseType passed to Invoke-WrikeApi: $ResponseType"
        }

        $Path = $Path.TrimStart('/')
        $uriBuilder = [uribuilder]([uri]::new($Uri, $Path))
        $queryStringCollection = [System.Web.HttpUtility]::ParseQueryString($uriBuilder.Query)
        $Query.Keys | ForEach-Object {
            $queryStringCollection.Add($_, $Query.$_)
        }
        $uriBuilder.Query = $queryStringCollection.ToString()
        $standardHeaders = Get-StandardHeaders
        $standardHeaders.Keys | ForEach-Object { $Headers.$_ = $standardHeaders.$_ }

        $requestParams = @{
            Method = $Method
            Uri = $uriBuilder.Uri
            Headers = $Headers
        }
        foreach ($p in 'Body', 'OutFile') {
            if ($MyInvocation.BoundParameters.ContainsKey($p)) {
                Write-Verbose "Adding parameter '$p' with value '$($MyInvocation.BoundParameters[$p])' to Invoke-RestMethod invocation "
                $requestParams.$p = $MyInvocation.BoundParameters[$p]
            }
        }
        try {
            $response = Invoke-RestMethod @requestParams
            Write-Verbose "Processing API response with kind -eq '$($response.kind)'"
            if ($MyInvocation.BoundParameters.ContainsKey('ResponseType')) {
                if ($response.kind -eq $ResponseType) {
                    $dataType = $script:ResponseType.$ResponseType
                    foreach ($data in $response.data) {
                        $obj = $dataType::new()
                        foreach ($property in $data | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) {
                            $obj.$property = $data.$property
                        }
                        Write-Output $obj
                    }
                }
                else {
                    Write-Error "Unexpected response from Wrike api: $($response | ConvertTo-Json -Depth 10)"
                }
            }
            elseif ($response) {
                Write-Output $response
            }
        }
        catch [System.Net.Http.HttpRequestException], [System.InvalidOperationException] {
            $errorRecord = $_
            Write-Verbose "Handling $($errorRecord.Exception.GetType()) exception"
            if ($errorDescription = ($errorRecord.ErrorDetails.Message | ConvertFrom-Json -Depth 10) -as [WrikeErrorDescription]) {
                Write-Verbose "Successfully parsed Wrike API error response"
                $errorRecord = $errorDescription | ConvertFrom-WrikeErrorDescription
            }
            Write-Error -ErrorRecord $errorRecord
        }
    }
}
function New-WrikeFieldFilter {
    [CmdletBinding()]
    [OutputType([WrikeFieldFilter])]
    param (
        # Specifies the custom field ID.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string]
        $Id,

        # Specifies the comparison operator.
        [Parameter()]
        [WrikeFieldComparator]
        $Comparator,

        # Specifies the filter value, or optionally a set of values.
        [Parameter()]
        [string[]]
        $Value,

        # Specifies the minimum value for comparable field types.
        [Parameter()]
        [string]
        $MinValue,

        # Specifies the maximum value for comparable field types.
        [Parameter()]
        [string]
        $MaxValue,

        # Specifies that the filter should be returned even if the ID is not recognized as an existing Wrike custom field ID.
        [Parameter()]
        [switch]
        $Force
    )

    process {
        if (-not $Force) {
            $customField = Get-WrikeCustomField -Id $Id -ErrorAction Stop
            Write-Verbose "Creating field filter for field named '$($customField.Title)'"
            $compatibleComparators = @{
                [WrikeCustomFieldType]::Text = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange, [WrikeFieldComparator]::Contains, [WrikeFieldComparator]::StartsWith, [WrikeFieldComparator]::EndsWith
                [WrikeCustomFieldType]::DropDown = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange, [WrikeFieldComparator]::Contains, [WrikeFieldComparator]::StartsWith, [WrikeFieldComparator]::EndsWith
                [WrikeCustomFieldType]::Numeric = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange
                [WrikeCustomFieldType]::Currency = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange
                [WrikeCustomFieldType]::Percentage = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange
                [WrikeCustomFieldType]::Date = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange
                [WrikeCustomFieldType]::Duration = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange
                [WrikeCustomFieldType]::Checkbox = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty
                [WrikeCustomFieldType]::Contacts = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::ContainsAll, [WrikeFieldComparator]::ContainsAny
                [WrikeCustomFieldType]::Multiple = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::ContainsAll, [WrikeFieldComparator]::ContainsAny
            }
            if ($Comparator -notin $compatibleComparators.($customField.Type)) {
                Write-Error "The chosen comparator '$Comparator' is not valid for use with the custom field '$($customField.Title)' of type '$($customField.Type)'"
                return
            }
        }
        [WrikeFieldFilter]@{
            Id = $Id
            Comparator = $Comparator
            Value = $Value
            MinValue = $MinValue
            MaxValue = $MaxValue
        }
    }
}
function Request-WrikeDataExport {
    [CmdletBinding()]
    [OutputType([WrikeDataExport])]
    param(
        # Specifies the maximum age of the BI Export data before a refresh request is issued.
        [Parameter()]
        [TimeSpan]
        $MaxAge = ( New-TimeSpan -Hours 12 ),

        # Specifies that if a new BI Export request is issued, the command should block until the request is completed and the results are returned
        [Parameter()]
        [switch]
        $Wait,

        # Specifies the maximum time to wait for a result.
        [Parameter()]
        [TimeSpan]
        $Timeout = [TimeSpan]::MaxValue
    )

    process {
        $minAge = New-TimeSpan -Hours 1
        if ($MaxAge -lt $minAge) {
            Write-Verbose "MaxAge is less than 1 hour. MaxAge will be set to 1 hour."
            $MaxAge = $minAge
        }

        $data = Get-WrikeDataExport
        $now = if ($data.CompletedDate.Kind -eq [System.DateTimeKind]::Utc) { (Get-Date).ToUniversalTime() } else { Get-Date }
        if ($null -ne $data -and $now - $data.CompletedDate -le $MaxAge) {
            # The last report is not older than MaxAge so we just return it
            Write-Output $data
            return
        }

        # Either there is no BI Export data available or it's older than MaxAge
        Write-Verbose "Requesting updated BI Export data"
        $now = Get-Date
        $expireTime = if ($Timeout -eq [TimeSpan]::MaxValue) { [datetime]::MaxValue } else { $now.Add($Timeout) }
        $data = Invoke-WrikeApi -Method Post -Path 'data_export' -ResponseType dataExport

        if (-not $Wait) {
            # Return early with a Status of Scheduled or InProgress probably
            Write-Output $data
            return
        }
        Write-Verbose "Waiting until $expireTime"
        $completedStates = @([WrikeStatus]::Completed, [WrikeStatus]::Cancelled, [WrikeStatus]::Failed)
        $requestInterval = New-TimeSpan -Seconds $script:Config.Wrike.RequestIntervalSeconds
        while ($data.Status -notin $completedStates -and (Get-Date) -lt $expireTime) {
            Write-Verbose "BI Export status is $($data.Status). Checking again in $requestInterval. . ."
            Start-Sleep -Seconds $requestInterval.TotalSeconds
            $data = Request-WrikeDataExport -MaxAge $MaxAge
        }
        Write-Output $data
    }
}
function Save-WrikeDataExport {
    [CmdletBinding()]
    param(
        # Specifies the WrikeDataExport data returned from Get-WrikeDataExport or Request-WrikeDataExport. If ommitted, a call will be made to Get-WrikeDataExport automatically.
        [Parameter(ValueFromPipeline)]
        [WrikeDataExport]
        $WrikeDataExport,

        # Specifies the directory where all Wrike BI Export resources will be written as CSV files. Path will be created if it doesn't exist, and existing files will be overwritten.
        [Parameter(Mandatory)]
        [string]
        $Destination
    )

    process {
        $null = New-Item -Path $Destination -ItemType Directory -Force -ErrorAction Stop
        if ($null -eq $WrikeDataExport) {
            $WrikeDataExport = Get-WrikeDataExport
            if ($null -eq $WrikeDataExport) {
                Write-Error "There is no Wrike BI Export data available. Please use Request-WrikeDataExport to refresh the BI Export data."
                return
            }
        }
        foreach ($resource in $WrikeDataExport.Resources) {
            $requestParams = @{
                Uri = $resource.Url
                OutFile = Join-Path $Destination "$($resource.Name).csv"
            }
            $ProgressPreference = 'SilentlyContinue'
            Invoke-WrikeApi @requestParams
        }
    }
}
function Set-WrikeAccessToken {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # Specifies the API App access token to use for authentication. It is recommended to provide a securestring, but a string can also be provided.
        [Parameter(Mandatory)]
        [object]
        $AccessToken,

        # Specifies that the token should not be persisted to disk
        [Parameter()]
        [switch]
        $Ephemeral
    )

    process {
        $valueType = $AccessToken.GetType().Name
        if ($valueType -eq 'String') {
            Write-Warning "The AccessToken value was received as a string. For better security it is recommended to use a securestring instead. Otherwise it's possible your PowerShell command history could be read, and secrets like these could be stolen. Try using 'Read-Host -AsSecureString' to collect tokens, passwords, and secrets in the future."
            Write-Verbose 'Converting the AccessToken string to a securestring'
            $AccessToken = $AccessToken | ConvertTo-SecureString -AsPlainText -Force
        }
        elseif ($valueType -ne 'SecureString') {
            Write-Error "AccessToken is expected to be of type SecureString or String. Received a $valueType instead."
        }

        if ($PSCmdlet.ShouldProcess("Wrike API Access Token", "Set")) {
            Write-Verbose "Setting the Wrike API access token. Ephemeral = $Ephemeral"
            Save-AccessToken -AccessToken $AccessToken -Ephemeral:$Ephemeral
        }
    }
}
$script:Config = Import-PowerShellDataFile $PSScriptRoot\config.psd1
if ($script:Config.AppDataPath -eq 'Default') {
    $script:Config.AppDataPath = Join-Path -Path ([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ApplicationData)) -ChildPath 'WrikeExplorer'
}
if (-not (Test-Path $script:Config.AppDataPath)) {
    New-Item -Path $script:Config.AppDataPath -ItemType Directory
}

$script:ResponseType = @{
    contacts = [WrikeContact]
    dataExport = [WrikeDataExport]
    dataExportSchema = [WrikeDataExportSchema]
    contactsHistory = [WrikeContactHistory]
    version = [WrikeApiVersion]
    folderTree = [WrikeFolderTree]
    folders = [WrikeFolder]
    customfields = [WrikeCustomField]
    foldersHistory = [WrikeFolderHistory]
}

Import-AccessToken