
Function Search-ISMBusinessObject {
<#
.SYNOPSIS
  Searches business objects for records matching the supplied criteria/filter.
.DESCRIPTION
  This function will fetch a business object(s) for records that equal the supplied criteria in the -Filter parameter.

  Only the following eight operators are supported when fetching business object records using the filter function:
    eq - Returns true if the first value is equal to the second value.
    ne - Returns true if the first value is not equal to the second value.
    lt - Returns true if the first value is less than the second value.
    le - Returns true if the first value is less than or equal to the second value.
    gt - Returns true if the first value is greater than the second value.
    ge - Returns true if the first value is greater than or equal to the second value.
    or - Returns true if either one of its left or right sub-expressions evaluates to true.
    and - Returns true if both its left and right sub-expressions evaluate to true.
.PARAMETER -BusinessObject
    The Business Object you are interacting with. (Incidents, CIs, CI__ActiveDirectorys, ServiceReqs, Locations, Employees, Manufacturers, StandardUserTeams, Problems, Changes, etc...)
.PARAMETER -Filter
    The filter containing your search string. This parameter allowed queries to be constructed using a more "friendly" syntax and additionally supports submitting the API call as entered explicitly. 
    
    The filter must be formatted properly to be parsed correctly by the API. Please see the examples in the help for more information. 
.PARAMETER -RawFilter
    An optional switch parameter that can be used to force the function to pass exactly the -filter string as supplied with no additional parsing. When this switch isn't used, the function will default to parsing the filter in an attempt to make it easier to format queries which includes requiring operators be preceded by hyphens. (-eq, -and, etc...)
.PARAMETER -Testing
    Hidden optional parameter that is useful for outputting additional debugging information when troubleshooting the function.
.PARAMETER -DetailedOutput
    Hidden optional parameter that will return the API URI submitted for outputting additional debugging information when troubleshooting the function.
.INPUTS
  Parent and Child RecIDs, Action and Relationship.
.OUTPUTS
  Returns the newly created Relationship Business Object on success. 
.NOTES
  Version:        1.0
  Author:         Sean Carlton
  Creation Date:  08/16/2022
  Purpose/Change: Initial script development  
.EXAMPLE
 Search-ISMBusinessObject -BusinessObject employees -Filter "primaryemail -eq johnsmith@email.com" 

 =======
 This example demonstrates searching the employees business object where the primaryemail property is equal to johnsmith@email.com.
 =======
.EXAMPLE
  Search-ISMBusinessObject -BusinessObject cis -Filter "createdby -eq jsmith -and name -eq 3M - File Extract Process"

 =======
 This example demonstrates searching the cis business object where the createdby user is equal to jsmith AND the name of the record is equal to 3M - File Extract Process.
 =======
.EXAMPLE
  Search-ISMBusinessObject -BusinessObject incidents -Filter "incidentnumber -eq 11504"

 =======
 This example demonstrates searching the incidents business object where the incident number is equal to 11504.
 =======
.EXAMPLE
 Search-ISMBusinessObject -BusinessObject locations -Filter "name -eq SAGH Campus - ''88 Bldg"

 =======
 This example demonstrates using the apostrophe as an escape character. The Location Name of SAGH Campus - '88 Bldg contains an apostrophe which is parsed by the REST API and so it must be escaped with an apostrophe (') so the search query executes successfully.
 =======
 .EXAMPLE
  Search-ISMBusinessObject -BusinessObject cis -Filter "name eq 'IL GEM 4000 2 SAGH'" -RawFilter

 =======
 This example demonstrates searching the cis business object where the name of the record is equal to IL GEM 4000 2 SAGH. The -RawFilter switch will submit the provided filter exactly as typed to the API. Notice that there is no hypen before the operator (eq) and the value is wrapped in single quotes.
 =======
.EXAMPLE
  Search-ISMBusinessObject -BusinessObject cis -Filter "CreatedDateTime -gt 2022-07-24T23:42:48Z"

 =======
 This example demonstrates searching the cis business object where the CreatedDateTime of the record is greater than 2022-07-24T23:42:48Z.
 =======
.EXAMPLE
  Search-ISMBusinessObject -BusinessObject cis -Filter "CreatedDateTime gt 2022-07-24T23:42:48Z" -RawFilter

 =======
 This example demonstrates searching the cis business object where the CreatedDateTime of the record is greater than 2022-07-24T23:42:48Z but using the -RawFilter switch parameter. Notice there is no hyphen before the operator (gt) and that in this particular instance, the value supplied is NOT wrapped in single quotes. Not all fetch calls to the API require the value be wrapped in single quotes depending on the type of field being queried for. 
 =======
.EXAMPLE
  Search-ISMBusinessObject -businessobject frs_data_workflow_historys -filter "BlockException ne '`$NULL'" -RawFilter

 =======
 This example demonstrates searching a business object and evaluating a NULL property value. The null property must be in all caps and be preceded with $ for the REST API to parse it properly. 
 =======
#>
    [CmdletBinding()]
    param (
    [Alias("BO")]
    [Parameter(
        HelpMessage = 'Business Object Type you are Querying Against (Incidents, ServiceReqs, cis, etc...)',
        Mandatory = $true)]
    [string]$BusinessObject,

    [Parameter(
        HelpMessage = 'Format: Property -eq ''Value'',
        Ex: displayname -eq ''John Batman''',
        Mandatory=$true)]
    [string]$Filter,

    [Parameter(Helpmessage = "
        Ex: Search-ISMBusinessObject -BusinessObject cis -Filter ""createdby eq 'scarlton' and name eq 'IL GEM 4000 2 SAGH'"" -RawFilter 
    ")]
    [switch]$RawFilter, #// Switch to pass the filter value literally. This accounts for scenarios where the default -filter parsing fails/doesn't work. Primary differences include no hyphen prefix for Operators and wrapping the value in single quotes for most values (but not all! If your query is failing, try wrapping the value in single quotes and try again without.)

    [Parameter(HelpMessage="Optional hidden parameter that will output additional information useful for debugging the function.",
        Mandatory=$false,
        DontShow)]
    [switch]$Testing,

    [Parameter(HelpMessage="Optional hidden parameter that can be used to ouput additional information.",
        Mandatory=$false,
        DontShow)]
    [switch]$DetailedOutput,
    
    ### API Connection Parameters ###
    [Parameter(HelpMessage = 'Tenant URL', DontShow)]
    [string]$Tenant = (Connect-ISM)[1],
    [Parameter(HelpMessage = 'Authorization Header', DontShow)]
    [hashtable]$Headers = (Connect-ISM)[0]

    )
   
    

BEGIN {

    #Presume the syntax is valid to start
    $ValidSyntax = $true

    ### Initialize an array for all errors/exceptions
    $ExceptionErrors = @()

    ### Query for BusinessObject Metadata Properties
    $BOMetaDataProperties = (Get-ISMBusinessObject -BusinessObject $BusinessObject -Metadata)

    ### If we have no BOMetaDataProperties then an invalid BusinessObject was supplied.
    if (!$BOMetaDataProperties){
        $ValidSyntax = $false
        Write-Host "Failed querying Spark!" -ForegroundColor yellow
    } elseif ($BOMetaDataProperties.EntityType.count){
        $BOMetaDataProperties = $BOMetaDataProperties.EntityType[-1].Property.Name | Sort-Object
    } else {
        $BOMetaDataProperties = $BOMetaDataProperties.EntityType.Property.Name
    }

    # Parse the filter accordingly based on whether the -RawFilter switch was provided or not.
    if ($RawFilter -and $ValidSyntax){
    
        $FilterEncodes = [ORDERED]@{
            RAW_Filter = $Filter #\\Filter literally as entered by the user.
            
            ## Commented out on 01/16/23. Caused false-positive errors getting logged in $Error
            ## HTTP_Parsed_RAW = [System.Web.HTTPUtility]::UrlEncode(($Filter))#\\URL-Encoded with single quotes around the query.
            ## Commented out on 01/16/23
        }

    } elseif ($ValidSyntax) {
        #Define the list of valid filter operators
        $ValidOperators = @("-eq","-ne","-lt","-gt","-ge","-or","-and")

        #Trim any whitespace from the beginning and end of the submitted filter query.
        $Filter = $Filter.trim()

        #Split out the provided filter on the spaces.
        $FilterSplit = ($Filter -split '(?= )').trim()

        #Parse the array. We can tell the Property Names and Operators apart from the values by querying for the metadata and comparing the operators to the supported operators.
        $i = 0
        $SplitArray = @()
        Foreach ($Split in $FilterSplit){

            $SplitObj = [PSCustomObject]@{
                Split = $Split
                Type = $null
                NextSplit = $null
                SplitQuoted = $null
                SplitOpenQuote = $null
                SplitCloseQuote = $null
                Count = $i
                ValidBOProperty = $false
            }

            if ($Split -in $ValidOperators){
                $SplitObj.Split = $SplitObj.Split.Replace("-","")
                $SplitObj.Type = "Operator"
                $SplitObj.NextSplit = "Value"
            } elseif ($SplitObj.Split -in $BOMetaDataProperties) {
                $SplitObj.ValidBOProperty = $true
                $SplitObj.Type = "Property"
                $SplitObj.NextSplit = "Operator"            
            }else {
                $SplitObj.Type = "Value"
                $SplitObj.NextSplit = "Unknown"
            }

            $SplitObj.SplitQuoted = "'" + $SplitObj.Split + "'"
            $SplitObj.SplitOpenQuote = "'" + $SplitObj.Split
            $SplitObj.SplitCloseQuote = $SplitObj.Split + "'"

            $SplitArray += $SplitObj

            $i++
        }

        ## Removed on 8/15/22 to ensure the fallback to the raw filter functionality works
        # if ("Operator" -notin $SplitArray.Type){
        #     Write-host "Your filter query appears to have no valid operators. Operators must be formatted with a preceding hyphen and be in the list of valid operators: $($ValidOperators -join ", ")`n`nExample: ""LoginID -eq $($ENV:Username)""`nExample: ""CreatedDateTime -lt 2022-01-24T23:42:48Z""`n`nIf your filter query is correct as formatted, please use the -RawFilter switch to query the API with your literal query string." -ForegroundColor yellow
        #     $ValidSyntax = $false
        # }

        # Build the filter string based on the Type derived.
        $FilterStringQuoted = @()
        $FilterStringUnquoted = @()
        $i = 0
        foreach ($Split in $SplitArray){

            #Add the raw value of the Split to the unquoted filter string.
            $FilterStringUnquoted += $Split.Split

            if ($Split.Type -eq "Value"){

                If ($SplitArray[$i-1].Type -eq "Operator" -and $SplitArray[$i+1].Type -eq "Operator") {
                    $FilterStringQuoted += $Split.SplitQuoted
                } elseif ($SplitArray[$i-1].Type -eq "Operator" -and (!$SplitArray[$i+1].Type)){
                    $FilterStringQuoted += $Split.SplitQuoted
                } elseif ($SplitArray[$i-1].Type -eq "Operator"){
                    $FilterStringQuoted += $Split.SplitOpenQuote
                } elseif ($SplitArray[$i+1].Type -eq "Operator") {
                    $FilterStringQuoted += $Split.SplitCloseQuote
                } elseif ($SplitArray[$i+1].Type -eq "Property"){
                    $FilterStringQuoted += $Split.SplitCloseQuote 
                } elseif ($SplitArray[$i+1].Type -eq "Property") {
                    $FilterStringQuoted += $Split.SplitCloseQuote 
                } elseif ((!$SplitArray[$i+1].Type)){
                    $FilterStringQuoted += $Split.SplitCloseQuote 
                } else {
                    $FilterStringQuoted += $Split.Split
                }

            } else {
                $FilterStringQuoted += $Split.Split
            }

            $i++
        }

        #Create an ordered hashtable with all of the parsed queries.
        $FilterEncodes = [ORDERED]@{
            HTTP_Parsed_With_Quotes = [System.Web.HTTPUtility]::UrlEncode(($FilterStringQuoted -join " "))#\\URL-Encoded with single quotes around the query.
            HTTP_Parsed_No_Quotes = [System.Web.HTTPUtility]::UrlEncode(($FilterStringUnquoted -join " ")) #\\URL-Encoded without single quotes around the query.
            RAW_With_Quotes = ($FilterStringQuoted -join " ") #\\Raw with single-quotes parsed around the query.
            RAW_No_Quotes = ($FilterStringUnquoted -join " ") #\\Raw except perators have had the preceding - removed.
            RAW_Filter = $Filter #\\Filter literally as entered by the user.
        }
              
        if ("Property" -notin $SplitArray.Type){
            $ValidSyntax = $false

            $PropertyHelper = Get-ISMBusinessObject -BusinessObject $BusinessObject -PropertyMatch $($FilterSplit[0])

            $PropertyHelperMsg = $null
            if ($PropertyHelper){
                $PropertyHelperMsg = "`nPerhaps one of these matching fields is what you're looking for:`n$($PropertyHelper.Name -join "`n")`n"
            }

            Write-Host "The ""$($FilterSplit[0])"" property provided does not appear to be a valid property name on the $BusinessObject BusinessObject!`n$PropertyHelperMsg`nTo see a list of ALL valid properties run the following command:`n`nGet-ISMBusinessObject -BusinessObject $BusinessObject -PropertyList" -ForegroundColor yellow

            # if ($DetailedOutput){
            #     # Write-Host "The ""$($FilterSplit[0])"" property provided does not appear to be a valid property name on the $BusinessObject BusinessObject!`n`n$BusinessObject has the following properties available:
            #     # $($BOMetaDataProperties -join ", ")" -ForegroundColor yellow


            # } else {
            #     Write-Host "The ""$($FilterSplit[0])"" property provided does not appear to be a valid property name on the $BusinessObject BusinessObject!`n`nTo see a list of valid properties run the following command:`n`nGet-ISMBusinessObject -BusinessObject $BusinessObject -PropertyList" -ForegroundColor yellow
            # }        
        }

    }#End Else-if No -RawFilter Switch
       
}#END BEGIN

    PROCESS {

        if ($ValidSyntax){
            #Initialize the results array.
            $Results = @()

            #Iterate through every encoded filter until we get results. This is to account for the various ways in which the API will fail to parse the filter query.
            Foreach ($FilterEncode in $FilterEncodes.Keys){
                
                if ($Testing){
                    Write-Host "Testing with $FilterEncode!" -foregroundcolor yellow
                }

                $uri = "$Tenant/api/odata/businessobject/$BusinessObject`?`$filter=$($FilterEncodes[$FilterEncode])&`$top=100&`$skip=0"

                try{
                    $Query = $null
                    $Query = Invoke-RestMethod -Method GET -uri $uri -headers $headers -TimeoutSec $SparkQueryTimeoutSec
                }
                
                catch {
                    $ExceptionErrors += $_.Exception.Message
                }

                If ($Query.'@odata.count'){

                    if ($Testing){
                        Write-Host "Success using $FilterEncode!`n`nFilter Passed: $($FilterEncodes[$FilterEncode])`n`nWorking URI: $URI
                        " -foregroundcolor green    
                    }
                    
                    $Results += $Query.Value

                    if ($Results.Count -lt $Query.'@odata.count'){

                        Do {

                            $Skip = $Results.Count

                            $uri = "$Tenant/api/odata/businessobject/$BusinessObject`?`$filter=$($FilterEncodes[$FilterEncode])&`$top=100&`$skip=$Skip"

                            $SkipQuery = Invoke-RestMethod -Method GET -uri $uri -headers $headers -TimeoutSec $SparkQueryTimeoutSec

                            if ($SkipQuery){
                                $Results += $SkipQuery.Value
                            } else {

                                if ($Skip -lt $Query.'@odata.count'){
                                    #If there's no SkipQuery but we have more results than the original queried @odata.count we won't bother with a warning. This can occur when looping through records when a new record is created during the loop.
                                    Write-Warning "Failed query for $BusinessObject! (Skip: $Skip | Total: $($Query.'@odata.count'))"
                                }

                                 $ExitLoop = $true
                               
                            }

                        } until (($Results.Count -ge $Query.'@odata.count') -or $ExitLoop)

                    }

                    #Friendly little helper loop to parse employee business object MemberOf HTML fields into a proper array.
                    if ($BusinessObject -eq "employees"){

                        Foreach ($Employee in $Results){

                            $Employee | Add-Member -MemberType NoteProperty -Value @() -Name "SHS_MemberOfParsed" -force
                            
                            if ($Employee.SHS_MemberOf){
                                $Employee.SHS_MemberOfParsed = $Employee.SHS_MemberOf.replace("<br>","%").split("%").trim()
                                $Employee.SHS_MemberOfParsed = $Employee.SHS_MemberOfParsed | Sort-Object
                            }

                        }
            
                    }

                    break
                    
                } else {

                    if ($DetailedOutput){
                        Write-Host "Query Error: $($ExceptionErrors[-1])" -ForegroundColor red
                    }
                    
                }

            }#END FOREACH
        }#END IF VALID-SYNTAX
    }#END PROCESS

    END {

        if ($DetailedOutput -and !$Results){
            
            Write-host "No results found querying $BusinessObject with the provided filter!" -ForegroundColor yellow

        } elseif ($DetailedOutput -and $Results){

            Write-Host "Successful query!`n`nAPI URI: $uri`n`n" -foregroundcolor green
            return $Results

        } else {
            return $Results
        }

    }#END END-BLOCK

}#END Search-ISMBusinessObject Function

New-Alias -Name Search-ISMBO -Value Search-ISMBusinessObject -Force