Public/Jobs.ps1

<#
.SYNOPSIS
    Gets backup and restore jobs for a Keepit connector
.DESCRIPTION
    Retrieves jobs (backup and restore) for a specified Keepit connector.
    By default, shows active jobs (currently running) and future jobs (scheduled to run).
    Completed jobs from the past are excluded unless an explicit date range is specified. If a date range
    is specified, the cmdlet will return the job history for the specified time range and connector.
    Results can be optionally filtered by job type and date range.
.PARAMETER Connector
    The connector name or GUID to retrieve jobs for.
    Can be piped from Get-KeepitConnector.
.PARAMETER Type
    Optional job type filter. Valid values: 'backup', 'restore'.
    If not specified, returns all job types.
.PARAMETER StartTime
    Optional start date for filtering jobs. If specified, EndTime must also be provided.
    When StartTime and EndTime are provided, the default future-only filter is disabled
    and all jobs within the date range are returned.
    Jobs are filtered based on their Start time (or Scheduled time if Start is not available).
    Only jobs with dates on or after StartTime are included.
.PARAMETER EndTime
    Optional end date for filtering jobs. If specified, StartTime must also be provided.
    When StartTime and EndTime are provided, the default future-only filter is disabled
    and all jobs within the date range are returned.
    Jobs are filtered based on their Start time (or Scheduled time if Start is not available).
    Only jobs with dates on or before EndTime are included.
.PARAMETER Completed
    Shows only jobs that have finished (End time has a value).
    Can be combined with -Scheduled using OR logic.
.PARAMETER Scheduled
    Shows only pending scheduled jobs (Scheduled has a value, Active = False, and no End value).
    Can be combined with -Completed using OR logic.
.PARAMETER Raw
    Returns the raw XML response from the API instead of parsed PowerShell objects.
    Useful for debugging or when you need the complete API response.
.PARAMETER ActiveOnly
    Adds the active-only filter to the API request, causing the server to return only
    jobs that are currently active (running). This is more efficient than client-side
    filtering when you only need active jobs, as the filtering is done server-side.
.EXAMPLE
    Get-KeepitJobs -Connector "Production M365"

    Gets active and future jobs for the connector named "Production M365"
.EXAMPLE
    Get-KeepitJobs -Connector "abc123-def456" -Type backup

    Gets only active and future backup jobs for the connector
.EXAMPLE
    Get-KeepitJobs -Connector "abc123" -StartTime (Get-Date).AddDays(-7) -EndTime (Get-Date)

    Gets all jobs from the last 7 days
.EXAMPLE
    Get-KeepitJobs -Connector "abc123" -Type restore -StartTime "2025-12-01" -EndTime "2025-12-21"

    Gets only restore jobs between December 1 and December 21, 2025
.EXAMPLE
    Get-KeepitJobs -Connector "abc123" -StartTime "2025-12-15" -EndTime "2025-12-15"

    Gets all jobs for a single day (December 15, 2025). The range is automatically expanded to cover 00:00:00 to 23:59:59.
.EXAMPLE
    Get-KeepitConnector | Get-KeepitJobs

    Gets all jobs for all connectors
.EXAMPLE
    Get-KeepitJobs -Connector "Production M365" -Completed

    Gets only completed jobs for the connector
.EXAMPLE
    Get-KeepitJobs -Connector "Production M365" -Scheduled

    Gets only scheduled (pending) jobs for the connector
.EXAMPLE
    Get-KeepitJobs -Connector "Production M365" -Raw

    Returns the raw XML response from the API for debugging purposes
.EXAMPLE
    Get-KeepitJobs -Connector "Production M365" -ActiveOnly

    Gets only active jobs using server-side filtering.
.OUTPUTS
    PSCustomObject containing job details with properties:
        - JobGuid: The job GUID
        - ConnectorGuid: The connector GUID
        - Type: Job type (backup or restore)
        - Description: Job description
        - Active: Boolean indicating if job is active
        - Priority: Job priority
        - Scheduled: Scheduled start time
        - Start: Actual start time
        - End: Completion time (if completed)

    When -Raw is specified, returns the raw XML response as a string.
.NOTES
    Requires an active connection via Connect-KeepitService.
    Accepts connector objects from Get-KeepitConnector via pipeline.
    DEFAULT BEHAVIOR: Shows active jobs (currently running) and future jobs (scheduled to run).
    - Active jobs are always included regardless of when they started
    - Future jobs are included if Start or Scheduled time is greater than current time
    - Completed jobs from the past are excluded
    To see all jobs including completed past jobs, specify a date range using StartTime and EndTime.
    Date filtering uses Start time if available, falls back to Scheduled time.
    Jobs without Start or Scheduled times are excluded by the default filter.
    StartTime and EndTime must be provided together - one cannot be specified without the other.
    If StartTime and EndTime are the same day, the range is expanded to cover the full day (00:00:00 to 23:59:59).
#>

function Get-KeepitJobs {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Public API name; renaming would be a breaking change')]
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('ConnectorGuid', 'Name')]
        [string]$Connector,

        [ValidateSet('backup', 'restore')]
        [string]$Type,

        [DateTime]$StartTime,

        [DateTime]$EndTime,

        [switch]$Completed,

        [switch]$Scheduled,

        [switch]$Raw,

        [switch]$ActiveOnly
    )

    begin {
        Write-Verbose "Get-KeepitJobs"
        $totalJobsReturned = 0

        # Check if any status filters are specified
        $hasStatusFilter = $Completed -or $Scheduled
        if ($hasStatusFilter) {
            Write-Verbose "Status filter(s) specified: Completed=$Completed, Scheduled=$Scheduled"
        }

        # Validate date parameters
        $hasStartTime = $PSBoundParameters.ContainsKey('StartTime')
        $hasEndTime = $PSBoundParameters.ContainsKey('EndTime')

        if ($hasStartTime -and -not $hasEndTime) {
            throw "StartTime specified without EndTime. Both StartTime and EndTime must be provided together."
        }

        if ($hasEndTime -and -not $hasStartTime) {
            throw "EndTime specified without StartTime. Both StartTime and EndTime must be provided together."
        }

        if ($hasStartTime -and $hasEndTime) {
            # Validate dates first - catch inverted ranges before same-day expansion
            if ($StartTime -ge $EndTime -and $StartTime.Date -ne $EndTime.Date) {
                throw "StartTime must be less than EndTime. StartTime: $StartTime, EndTime: $EndTime"
            }

            # Handle same-day search - expand to full day
            if ($StartTime.Date -eq $EndTime.Date) {
                Write-Verbose "StartTime and EndTime are the same date - expanding to full day"
                $StartTime = $StartTime.Date  # Midnight start
                $EndTime = $EndTime.Date.AddDays(1).AddSeconds(-1)  # 23:59:59
                Write-Verbose "Expanded range: $($StartTime.ToString('yyyy-MM-ddTHH:mm:ss', [System.Globalization.CultureInfo]::InvariantCulture)) to $($EndTime.ToString('yyyy-MM-ddTHH:mm:ss', [System.Globalization.CultureInfo]::InvariantCulture))"
            }

            # Normalize times to UTC for consistent comparison with API times (which are UTC)
            # If Kind is Unspecified, treat as UTC (consistent with API behavior)
            # If Kind is Local, convert to UTC
            if ($StartTime.Kind -eq [DateTimeKind]::Unspecified) {
                $StartTime = [DateTime]::SpecifyKind($StartTime, [DateTimeKind]::Utc)
            }
            elseif ($StartTime.Kind -eq [DateTimeKind]::Local) {
                $StartTime = $StartTime.ToUniversalTime()
            }
            if ($EndTime.Kind -eq [DateTimeKind]::Unspecified) {
                $EndTime = [DateTime]::SpecifyKind($EndTime, [DateTimeKind]::Utc)
            }
            elseif ($EndTime.Kind -eq [DateTimeKind]::Local) {
                $EndTime = $EndTime.ToUniversalTime()
            }

            Write-Verbose "Date range filter (UTC): $($StartTime.ToString('yyyy-MM-ddTHH:mm:ssZ')) to $($EndTime.ToString('yyyy-MM-ddTHH:mm:ssZ'))"
            $useFutureFilter = $false
        }
        else {
            # No dates specified - default to showing active jobs and future jobs
            $useFutureFilter = $true
            Write-Verbose "No date range specified - will filter for active jobs and jobs scheduled in the future"
        }

        # Get authentication header and base URL once for all pipeline items
        try {
            $authHeader = Get-AuthHeader
            $baseUrl = Get-KeepitBaseUrl
            Write-Verbose "Base URL: $baseUrl"

            $userId = Get-KeepitUserId -AuthHeader $authHeader -BaseUrl $baseUrl
            Write-Verbose "User ID: $userId"
            Write-Verbose "Initialization completed successfully"
        }
        catch {
            throw
        }
    }

    process {
        try {
            # Capture current time fresh for each pipeline item
            if ($useFutureFilter) {
                $currentTime = [DateTime]::UtcNow
                Write-Verbose "Current UTC time for future filter: $($currentTime.ToString('yyyy-MM-ddTHH:mm:ssZ'))"
            }

            # Resolve connector identity to GUID
            $resolved = Resolve-KeepitConnectorIdentity -Identity $Connector
            $connectorGuid = $resolved.ConnectorGuid
            Write-Verbose "=== Get-KeepitJobs: Processing Connector ==="
            Write-Verbose "Connector: $($resolved.Name) ($connectorGuid)"
            if ($PSBoundParameters.ContainsKey('Type')) {
                Write-Verbose "Job Type Filter: $Type"
            }
            else {
                Write-Verbose "Job Type Filter: None (all types)"
            }
            if ($hasStartTime -and $hasEndTime) {
                Write-Verbose "Date Range Filter: $StartTime to $EndTime"
            }

            # Build request
            $uri = "$baseUrl/users/$userId/devices/$connectorGuid/jobs"
            $activeJobsOnlyValue = if ($ActiveOnly) { 'true' } else { 'false' }
            $headers = @{
                'Authorization' = $authHeader
                'Content-Type' = 'application/xml'
                'Accept' = 'application/vnd.keepit.v4+xml'
                'active-jobs-only' = $activeJobsOnlyValue
            }

            Write-Verbose "=== API Request Details ==="
            Write-Verbose "Method: GET"
            Write-Verbose "URI: $uri"
            Write-Verbose "active-jobs-only: $activeJobsOnlyValue"

            Write-Verbose "=== Sending API Request ==="

            # If -Raw is specified, return the raw XML response
            if ($Raw) {
                Write-Verbose "Returning raw XML response"
                $webResponse = Invoke-WebRequest -Uri $uri -Method Get -Headers $headers -ErrorAction Stop
                $webResponse.Content
                return
            }

            # Make API call
            $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -ErrorAction Stop

            Write-Verbose "=== API Response Received ==="
            Write-Verbose "Response Type: $($response.GetType().FullName)"

            # Parse response
            Write-Verbose "Parsing jobs from response..."
            $jobs = @()

            if ($response.jobs.job) {
                # Normalize to array
                if ($response.jobs.job -is [System.Array]) {
                    $jobs = $response.jobs.job
                    Write-Verbose "Found $($jobs.Count) jobs"
                }
                else {
                    $jobs = @($response.jobs.job)
                    Write-Verbose "Found 1 job"
                }
            }
            else {
                Write-Verbose "No jobs found in response"
            }

            # Process and output each job
            foreach ($job in $jobs) {
                # Apply type filter if specified
                if ($PSBoundParameters.ContainsKey('Type') -and $job.type -ne $Type) {
                    Write-Verbose "Skipping job $($job.guid) - type '$($job.type)' does not match filter '$Type'"
                    continue
                }

                # Apply future filter (default behavior when no dates specified)
                if ($useFutureFilter) {
                    # Always include active jobs (currently running)
                    $isActive = $job.active -eq 'true' -or $job.active -eq $true
                    if ($isActive) {
                        Write-Verbose "Including job $($job.guid) - job is active (currently running)"
                    }
                    else {
                        # For non-active jobs, only include if scheduled for the future
                        # Use Start time if available, fall back to Scheduled time
                        $jobDateTime = $null
                        if ($job.start) {
                            $jobDateTime = [DateTime]::Parse($job.start, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind)
                        }
                        elseif ($job.scheduled) {
                            $jobDateTime = [DateTime]::Parse($job.scheduled, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind)
                        }

                        if ($jobDateTime) {
                            # Ensure job time is UTC for comparison
                            if ($jobDateTime.Kind -ne [DateTimeKind]::Utc) {
                                $jobDateTime = $jobDateTime.ToUniversalTime()
                            }
                            # Only include jobs scheduled for the future
                            if ($jobDateTime -le $currentTime) {
                                Write-Verbose "Skipping job $($job.guid) - date '$($jobDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ'))' is not in the future (current time: $($currentTime.ToString('yyyy-MM-ddTHH:mm:ssZ')))"
                                continue
                            }
                        }
                        else {
                            # No date available - skip jobs without scheduled times when using future filter
                            Write-Verbose "Skipping job $($job.guid) - no Start or Scheduled date available for future filtering"
                            continue
                        }
                    }
                }
                # Apply date range filter if specified
                elseif ($hasStartTime -and $hasEndTime) {
                    # Use Start time if available, fall back to Scheduled time
                    $jobDateTime = $null
                    if ($job.start) {
                        $jobDateTime = [DateTime]::Parse($job.start, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind)
                    }
                    elseif ($job.scheduled) {
                        $jobDateTime = [DateTime]::Parse($job.scheduled, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind)
                    }

                    if ($jobDateTime) {
                        # Ensure job time is UTC for comparison
                        if ($jobDateTime.Kind -ne [DateTimeKind]::Utc) {
                            $jobDateTime = $jobDateTime.ToUniversalTime()
                        }
                        if ($jobDateTime -lt $StartTime -or $jobDateTime -gt $EndTime) {
                            Write-Verbose "Skipping job $($job.guid) - date '$($jobDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ'))' is outside range $($StartTime.ToString('yyyy-MM-ddTHH:mm:ssZ')) to $($EndTime.ToString('yyyy-MM-ddTHH:mm:ssZ'))"
                            continue
                        }
                    }
                    else {
                        # No date available to filter on, skip this job
                        Write-Verbose "Skipping job $($job.guid) - no Start or Scheduled date available for filtering"
                        continue
                    }
                }

                $jobObject = [PSCustomObject]@{
                    JobGuid = if ($job.guid) { $job.guid } else { $null }
                    ConnectorGuid = $connectorGuid
                    ConnectorName = $resolved.Name
                    Type = if ($job.type) { $job.type } else { 'unknown' }
                    Description = if ($job.description) { $job.description } else { '' }
                    Active = if ($job.active -eq $true -or $job.active -eq 'true') { $true } else { $false }
                    Priority = if ($job.priority) { $job.priority } else { $null }
                    Scheduled = if ($job.scheduled) { $job.scheduled } else { $null }
                    Start = if ($job.start) { $job.start } else { $null }
                    End = if ($job.end) { $job.end } else { $null }
                }

                # Apply status filters if specified (OR logic - match any specified filter)
                if ($hasStatusFilter) {
                    $matchesFilter = $false

                    # -Completed: jobs where End has a value
                    if ($Completed -and $null -ne $jobObject.End -and $jobObject.End -ne '') {
                        $matchesFilter = $true
                        Write-Verbose "Job $($jobObject.JobGuid) matches -Completed filter"
                    }

                    # -Scheduled: jobs where Scheduled has a value AND Active is False AND End is empty (pending, not completed)
                    if ($Scheduled -and ($null -ne $jobObject.Scheduled -and $jobObject.Scheduled -ne '') -and $jobObject.Active -eq $false -and ($null -eq $jobObject.End -or $jobObject.End -eq '')) {
                        $matchesFilter = $true
                        Write-Verbose "Job $($jobObject.JobGuid) matches -Scheduled filter"
                    }

                    if (-not $matchesFilter) {
                        Write-Verbose "Skipping job $($jobObject.JobGuid) - does not match any status filter"
                        continue
                    }
                }

                Write-Verbose "Outputting job: $($jobObject.JobGuid) (Type: $($jobObject.Type), Active: $($jobObject.Active))"
                $totalJobsReturned++
                $jobObject
            }

            Write-Verbose "=== End Get-KeepitJobs ==="
        }
        catch {
            $PSCmdlet.ThrowTerminatingError(
                [System.Management.Automation.ErrorRecord]::new(
                    [System.Exception]::new("Failed to retrieve jobs for connector $connectorGuid : $($_.Exception.Message)", $_.Exception),
                    'KeepitJobError',
                    [System.Management.Automation.ErrorCategory]::ConnectionError,
                    $connectorGuid
                )
            )
        }
    }

    end {
        Write-Verbose "Total jobs returned: $totalJobsReturned"
    }
}

<#
.SYNOPSIS
    Retrieves historical job records for a Keepit connector
.DESCRIPTION
    Retrieves completed and past backup/restore jobs for a specified Keepit connector
    using the Keepit job history API (PUT /jobs). Unlike Get-KeepitJobs which returns
    active and future jobs, this cmdlet is designed for querying the historical record.

    Results can be filtered by type, limited in count, or restricted to failed jobs only.
.PARAMETER Connector
    The connector name or GUID to retrieve job history for.
    Can be piped from Get-KeepitConnector.
.PARAMETER StartTime
    Start of the time range for job history. Required by the API.
.PARAMETER EndTime
    Optional end of the time range. If omitted, defaults to the current time.
.PARAMETER Type
    Optional job type filter. Valid values: 'backup', 'restore'.
    If not specified, returns all job types.
.PARAMETER Limit
    Maximum number of records to return. Valid range: 1-10000.
    If not specified, the API returns its default limit.
.PARAMETER FailedOnly
    Returns only failed jobs. Cannot be combined with other status filters.
.PARAMETER Raw
    Returns the raw XML response from the API instead of parsed PowerShell objects.
    Useful for debugging or when you need the complete API response.
.EXAMPLE
    Get-KeepitJobHistory -Connector "Production M365" -StartTime (Get-Date).AddDays(-30)

    Gets all job history for the last 30 days.
.EXAMPLE
    Get-KeepitJobHistory -Connector "Production M365" -StartTime "2026-01-01" -EndTime "2026-06-01" -Type backup

    Gets only backup jobs between January 1 and June 1, 2026.
.EXAMPLE
    Get-KeepitJobHistory -Connector "abc123" -StartTime (Get-Date).AddDays(-7) -FailedOnly

    Gets only failed jobs from the last 7 days.
.EXAMPLE
    Get-KeepitJobHistory -Connector "abc123" -StartTime (Get-Date).AddDays(-30) -Limit 50

    Gets the most recent 50 job history records for the last 30 days.
.EXAMPLE
    Get-KeepitConnector | Get-KeepitJobHistory -StartTime (Get-Date).AddDays(-7)

    Gets job history for all connectors over the last 7 days.
.OUTPUTS
    PSCustomObject containing job details with properties:
        - JobGuid: The job GUID
        - ConnectorGuid: The connector GUID
        - ConnectorName: The connector name
        - Type: Job type (backup or restore)
        - Description: Job description
        - Active: Boolean indicating if the job is still active
        - Priority: Job priority
        - Scheduled: Scheduled start time
        - Start: Actual start time
        - Succeeded: Completion time when the job succeeded (null if failed or in progress)
        - Failed: Completion time when the job failed (null if succeeded or in progress)
        - Status: Human-readable status: Succeeded, Failed, Active, or Pending
        - Progress: Job progress as a float (0.0-1.0)

    When -Raw is specified, returns the raw XML response as a string.
.NOTES
    Requires an active connection via Connect-KeepitService.
    Accepts connector objects from Get-KeepitConnector via pipeline.
    The API requires StartTime (from-time). EndTime defaults to current time if omitted.
#>

function Get-KeepitJobHistory {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('ConnectorGuid', 'Name')]
        [string]$Connector,

        [Parameter(Mandatory = $true)]
        [DateTime]$StartTime,

        [DateTime]$EndTime,

        [ValidateSet('backup', 'restore')]
        [string]$Type,

        [ValidateRange(1, 10000)]
        [int]$Limit,

        [switch]$FailedOnly,

        [switch]$Raw
    )

    begin {
        Write-Verbose "Get-KeepitJobHistory: Initializing"

        # Normalize StartTime to UTC
        if ($StartTime.Kind -eq [DateTimeKind]::Unspecified) {
            $StartTime = [DateTime]::SpecifyKind($StartTime, [DateTimeKind]::Utc)
        }
        elseif ($StartTime.Kind -eq [DateTimeKind]::Local) {
            $StartTime = $StartTime.ToUniversalTime()
        }

        # Normalize EndTime to UTC (default to now if not specified)
        $hasEndTime = $PSBoundParameters.ContainsKey('EndTime')
        if ($hasEndTime) {
            if ($EndTime.Kind -eq [DateTimeKind]::Unspecified) {
                $EndTime = [DateTime]::SpecifyKind($EndTime, [DateTimeKind]::Utc)
            }
            elseif ($EndTime.Kind -eq [DateTimeKind]::Local) {
                $EndTime = $EndTime.ToUniversalTime()
            }
            if ($EndTime -le $StartTime) {
                throw "EndTime must be greater than StartTime. StartTime: $StartTime, EndTime: $EndTime"
            }
        }

        try {
            $authHeader = Get-AuthHeader
            $baseUrl = Get-KeepitBaseUrl
            $userId = Get-KeepitUserId -AuthHeader $authHeader -BaseUrl $baseUrl
            Write-Verbose "Base URL: $baseUrl, User ID: $userId"
        }
        catch {
            throw
        }
    }

    process {
        try {
            $resolved = Resolve-KeepitConnectorIdentity -Identity $Connector
            $connectorGuid = $resolved.ConnectorGuid
            Write-Verbose "Connector: $($resolved.Name) ($connectorGuid)"

            # Build filter XML body
            $fromTimeStr = $StartTime.ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
            $filterXml = "<filter><from-time>$fromTimeStr</from-time>"

            if ($hasEndTime) {
                $toTimeStr = $EndTime.ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
                $filterXml += "<to-time>$toTimeStr</to-time>"
            }
            if ($PSBoundParameters.ContainsKey('Type')) {
                $filterXml += "<type>$Type</type>"
            }
            if ($PSBoundParameters.ContainsKey('Limit')) {
                $filterXml += "<limit>$Limit</limit>"
            }
            if ($FailedOnly) {
                $filterXml += "<failed-only>true</failed-only>"
            }
            $filterXml += "</filter>"

            $uri = "$baseUrl/users/$userId/devices/$connectorGuid/jobs"
            $headers = @{
                'Authorization' = $authHeader
                'Content-Type'  = 'application/xml'
                'Accept'        = 'application/vnd.keepit.v4+xml'
            }

            Write-Verbose "Method: PUT, URI: $uri"
            Write-Verbose "Filter XML: $filterXml"

            if ($Raw) {
                $webResponse = Invoke-WebRequest -Uri $uri -Method Put -Headers $headers -Body $filterXml -ErrorAction Stop
                $webResponse.Content
                return
            }

            $response = Invoke-RestMethod -Uri $uri -Method Put -Headers $headers -Body $filterXml -ErrorAction Stop

            $jobs = @()
            if ($response.jobs.job) {
                $jobs = if ($response.jobs.job -is [System.Array]) { $response.jobs.job } else { @($response.jobs.job) }
            }
            Write-Verbose "Found $($jobs.Count) job(s)"

            foreach ($job in $jobs) {
                $isActive = $job.active -eq 'true' -or $job.active -eq $true
                $status = if ($job.succeeded) { 'Succeeded' }
                           elseif ($job.failed) { 'Failed' }
                           elseif ($isActive) { 'Active' }
                           else { 'Pending' }

                [PSCustomObject]@{
                    JobGuid       = if ($job.guid) { $job.guid } else { $null }
                    ConnectorGuid = $connectorGuid
                    ConnectorName = $resolved.Name
                    Type          = if ($job.type) { $job.type } else { 'unknown' }
                    Description   = if ($job.description) { $job.description } else { '' }
                    Active        = $isActive
                    Priority      = if ($job.priority) { $job.priority } else { $null }
                    Scheduled     = if ($job.scheduled) { $job.scheduled } else { $null }
                    Start         = if ($job.start) { $job.start } else { $null }
                    Succeeded     = if ($job.succeeded) { $job.succeeded } else { $null }
                    Failed        = if ($job.failed) { $job.failed } else { $null }
                    Status        = $status
                    Progress      = if ($job.progress) { [double]$job.progress } else { $null }
                }
            }
        }
        catch {
            $errorGuid = if ($connectorGuid) { $connectorGuid } else { $Connector }
            $PSCmdlet.ThrowTerminatingError(
                [System.Management.Automation.ErrorRecord]::new(
                    [System.Exception]::new("Failed to retrieve job history for connector '$errorGuid': $($_.Exception.Message)", $_.Exception),
                    'KeepitJobHistoryError',
                    [System.Management.Automation.ErrorCategory]::ConnectionError,
                    $errorGuid
                )
            )
        }
    }
}

function Invoke-JobCancellation {
    <#
    .SYNOPSIS
        Sends a cancellation request for a single job. Used internally by Stop-KeepitJob.
    #>

    param(
        [string]$Uri,
        [hashtable]$Headers,
        [string]$ConnectorGuid,
        [string]$ConnectorName,
        [string]$JobGuid
    )

    $timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
    $cancelXml = "<job><cancelled>$timestamp</cancelled></job>"

    try {
        Invoke-RestMethod -Uri $Uri -Method Put -Headers $Headers -Body $cancelXml -ErrorAction Stop | Out-Null
        [PSCustomObject]@{
            ConnectorGuid = $ConnectorGuid
            ConnectorName = $ConnectorName
            JobGuid       = $JobGuid
            Status        = 'Cancelled'
            CancelledAt   = $timestamp
        }
    }
    catch {
        $errorMessage = $_.Exception.Message
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            try {
                $errorXml = [xml]$_.ErrorDetails.Message
                $errorMessage = "$($errorXml.error.code): $($errorXml.error.description)"
            }
            catch { Write-Verbose "Could not parse error response XML; using raw exception message" }
        }
        # Return error info; let the caller decide whether to Write-Error or throw
        [PSCustomObject]@{
            ConnectorGuid = $ConnectorGuid
            ConnectorName = $ConnectorName
            JobGuid       = $JobGuid
            Status        = "Error: $errorMessage"
            CancelledAt   = $null
            _Exception    = $_.Exception
            _ErrorMessage = $errorMessage
        }
    }
}

<#
.SYNOPSIS
    Cancels one or more running or scheduled jobs on a Keepit connector
.DESCRIPTION
    Cancels backup or restore jobs by sending a cancellation timestamp to the Keepit API.
    Can cancel a single job by GUID, or all active and scheduled jobs on a connector
    using the -All switch.

    Supports pipeline input from Get-KeepitJobs for targeted cancellation.
.PARAMETER Connector
    The connector name or GUID. Can be piped from Get-KeepitConnector or Get-KeepitJobs.
    Aliases: ConnectorGuid, Name
.PARAMETER JobGuid
    The GUID of the specific job to cancel. Accepts pipeline input from Get-KeepitJobs.
.PARAMETER All
    Cancel all active and scheduled jobs on the connector.
.EXAMPLE
    Stop-KeepitJob -Connector "Production M365" -JobGuid "abc123-def456-ghi789"

    Cancels a specific job on the connector.
.EXAMPLE
    Stop-KeepitJob -Connector "Production M365" -All

    Cancels all active and scheduled jobs on the connector.
.EXAMPLE
    Get-KeepitJobs -Connector "Production M365" -ActiveOnly | Stop-KeepitJob

    Cancels all active jobs via pipeline.
.EXAMPLE
    Stop-KeepitJob -Connector "Production M365" -All -WhatIf

    Shows what jobs would be cancelled without actually cancelling them.
.OUTPUTS
    PSCustomObject with properties:
        - ConnectorGuid: The connector GUID
        - ConnectorName: The connector name
        - JobGuid: The job GUID
        - Status: "Cancelled" or error message
        - CancelledAt: UTC timestamp of the cancellation
.NOTES
    Requires an active connection via Connect-KeepitService.
    Supports -WhatIf and -Confirm via ShouldProcess.
#>

function Stop-KeepitJob {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Single')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
                   ParameterSetName = 'Single')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
                   ParameterSetName = 'All')]
        [ValidateNotNullOrEmpty()]
        [Alias('ConnectorGuid', 'Name')]
        [string]$Connector,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
                   ParameterSetName = 'Single')]
        [ValidateNotNullOrEmpty()]
        [string]$JobGuid,

        [Parameter(Mandatory = $true, ParameterSetName = 'All')]
        [switch]$All
    )

    begin {
        Write-Verbose "Stop-KeepitJob: Initializing"

        try {
            $authHeader = Get-AuthHeader
            $baseUrl = Get-KeepitBaseUrl
            $userId = Get-KeepitUserId -AuthHeader $authHeader -BaseUrl $baseUrl
            Write-Verbose "Base URL: $baseUrl, User ID: $userId"
        }
        catch {
            throw
        }
    }

    process {
        try {
            # Resolve connector identity to GUID
            $resolved = Resolve-KeepitConnectorIdentity -Identity $Connector
            $connectorGuid = $resolved.ConnectorGuid
            $connectorName = $resolved.Name
            Write-Verbose "Connector: $connectorName ($connectorGuid)"

            if ($All) {
                # Fetch active + scheduled jobs
                $jobsToCancel = @()
                try { $jobsToCancel += @(Get-KeepitJobs -Connector $connectorGuid -ActiveOnly) } catch {
                    Write-Warning "Failed to retrieve active jobs for connector '$connectorName': $($_.Exception.Message)"
                }
                try { $jobsToCancel += @(Get-KeepitJobs -Connector $connectorGuid -Scheduled) } catch {
                    Write-Warning "Failed to retrieve scheduled jobs for connector '$connectorName': $($_.Exception.Message)"
                }
                $jobsToCancel = $jobsToCancel | Where-Object { $_ -and $_.JobGuid }

                if ($jobsToCancel.Count -eq 0) {
                    Write-Verbose "No active or scheduled jobs found for connector '$connectorName'"
                    return
                }

                Write-Verbose "Found $($jobsToCancel.Count) job(s) to cancel"

                foreach ($job in $jobsToCancel) {
                    $uri = "$baseUrl/users/$userId/devices/$connectorGuid/jobs/$($job.JobGuid)"

                    if ($PSCmdlet.ShouldProcess("$connectorName job $($job.JobGuid) ($($job.Type))", "Cancel")) {
                        $headers = @{
                            'Authorization' = $authHeader
                            'Content-Type'  = 'application/xml'
                            'Accept'        = 'application/vnd.keepit.v4+xml'
                        }
                        $result = Invoke-JobCancellation -Uri $uri -Headers $headers `
                            -ConnectorGuid $connectorGuid -ConnectorName $connectorName `
                            -JobGuid $job.JobGuid
                        if ($result._Exception) {
                            Write-Error "Failed to cancel job $($job.JobGuid): $($result._ErrorMessage)"
                            # Remove internal properties before outputting
                            $result.PSObject.Properties.Remove('_Exception')
                            $result.PSObject.Properties.Remove('_ErrorMessage')
                        }
                        $result
                    }
                }
            }
            else {
                # Single job cancellation
                $uri = "$baseUrl/users/$userId/devices/$connectorGuid/jobs/$JobGuid"

                if ($PSCmdlet.ShouldProcess("$connectorName job $JobGuid", "Cancel")) {
                    $headers = @{
                        'Authorization' = $authHeader
                        'Content-Type'  = 'application/xml'
                        'Accept'        = 'application/vnd.keepit.v4+xml'
                    }
                    $result = Invoke-JobCancellation -Uri $uri -Headers $headers `
                        -ConnectorGuid $connectorGuid -ConnectorName $connectorName `
                        -JobGuid $JobGuid
                    if ($result._Exception) {
                        $ex = $result._Exception
                        $msg = $result._ErrorMessage
                        $result.PSObject.Properties.Remove('_Exception')
                        $result.PSObject.Properties.Remove('_ErrorMessage')
                        $PSCmdlet.ThrowTerminatingError(
                            [System.Management.Automation.ErrorRecord]::new(
                                [System.Exception]::new("Failed to cancel job $JobGuid on connector '$connectorName': $msg", $ex),
                                'KeepitJobError',
                                [System.Management.Automation.ErrorCategory]::ConnectionError,
                                $JobGuid
                            )
                        )
                    }
                    $result
                }
            }
        }
        catch {
            $errorGuid = if ($connectorGuid) { $connectorGuid } else { $Connector }
            $PSCmdlet.ThrowTerminatingError(
                [System.Management.Automation.ErrorRecord]::new(
                    [System.Exception]::new("Failed to cancel job(s) on connector '$errorGuid': $($_.Exception.Message)", $_.Exception),
                    'KeepitJobError',
                    [System.Management.Automation.ErrorCategory]::ConnectionError,
                    $errorGuid
                )
            )
        }
    }
}

function New-AlreadyQueuedResult {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Internal helper; creates an in-memory result object only')]
    <#
    .SYNOPSIS
        Creates a status object for WAITING_JOB_TO_START or RUNNING_JOB API errors.
        Used internally by Start-KeepitBackup.
    #>

    param(
        [string]$ConnectorGuid,
        [string]$ConnectorName,
        [string]$Status,
        [string]$ErrorCode,
        [string]$ErrorDescription,
        [string]$StartTime
    )

    [PSCustomObject]@{
        ConnectorGuid        = $ConnectorGuid
        ConnectorName        = $ConnectorName
        Type                 = 'backup'
        Description          = 'Job creation skipped'
        Status               = $Status
        CreatedAt            = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
        ErrorCode            = $ErrorCode
        ErrorMessage         = $ErrorDescription
        ExistingJobStartTime = $StartTime
    }
}

<#
.SYNOPSIS
    Starts a backup job on a Keepit connector
.DESCRIPTION
    Initiates a backup job for a specified Keepit connector. The backup can be started
    immediately or scheduled for a future time using the -ScheduledTime parameter.
.PARAMETER Connector
    The connector name or GUID to back up.
    Can be piped from Get-KeepitConnector.
.PARAMETER ScheduledTime
    Optional DateTime for scheduling the backup at a future time.
    Must be in the future. Times are converted to UTC for the API.
    If not specified, the backup starts immediately.
.EXAMPLE
    Start-KeepitBackup -Connector "Production M365"

    Starts an immediate backup for the connector named "Production M365"
.EXAMPLE
    Start-KeepitBackup -Connector "abc123-def456"

    Starts an immediate backup for the specified connector GUID
.EXAMPLE
    Get-KeepitConnector | Start-KeepitBackup

    Starts immediate backups for all connectors
.EXAMPLE
    Start-KeepitBackup -Connector "Production M365" -ScheduledTime (Get-Date).AddMinutes(30)

    Schedules a backup to run 30 minutes from now
.EXAMPLE
    Start-KeepitBackup -Connector "Production M365" -ScheduledTime "2026-06-15T14:00:00"

    Schedules a backup for a specific date and time
.OUTPUTS
    PSCustomObject containing backup job details with properties:
        - ConnectorGuid: The connector GUID
        - Type: Job type (backup)
        - Description: Job description
        - Status: Job status ("Active", "Pending", "Scheduled", "AlreadyQueued", or "AlreadyRunning")
        - CreatedAt: Timestamp when the job was created
        - ScheduledTime: Scheduled start time (when -ScheduledTime is used)

    When Status is "AlreadyQueued" or "AlreadyRunning", additional properties are included:
        - ErrorCode: The API error code ("WAITING_JOB_TO_START" or "RUNNING_JOB")
        - ErrorMessage: The API error description
        - ExistingJobStartTime: When the existing job started or is scheduled to start
.NOTES
    Requires an active connection via Connect-KeepitService.
    Accepts connector objects from Get-KeepitConnector via pipeline.

    ERROR HANDLING: If another backup job is already queued or running on the connector,
    the cmdlet displays a warning and returns a status object with Status = 'AlreadyQueued'
    or 'AlreadyRunning' instead of throwing an error. This allows graceful handling when
    automating backup operations across multiple connectors.
#>

function Start-KeepitBackup {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('ConnectorGuid', 'Name')]
        [string]$Connector,

        [DateTime]$ScheduledTime
    )

    begin {
        Write-Verbose "=== Start-KeepitBackup: Initialization ==="
        Write-Verbose "Initializing backup job operation"

        # Get authentication header and base URL once for all pipeline items
        try {
            $authHeader = Get-AuthHeader
            $baseUrl = Get-KeepitBaseUrl
            Write-Verbose "Base URL: $baseUrl"

            $userId = Get-KeepitUserId -AuthHeader $authHeader -BaseUrl $baseUrl
            Write-Verbose "User ID: $userId"
            Write-Verbose "Initialization completed successfully"
        }
        catch {
            throw
        }
    }

    process {
        try {
            # Resolve connector identity to GUID
            $resolved = Resolve-KeepitConnectorIdentity -Identity $Connector
            $connectorGuid = $resolved.ConnectorGuid
            Write-Verbose "=== Start-KeepitBackup: Processing Connector ==="
            Write-Verbose "Connector: $($resolved.Name) ($connectorGuid)"

            # Validate ScheduledTime if provided
            if ($PSBoundParameters.ContainsKey('ScheduledTime')) {
                if ($ScheduledTime.ToUniversalTime() -lt (Get-Date).ToUniversalTime()) {
                    Write-Error "ScheduledTime must be in the future."
                    return
                }
            }

            # Build XML request body
            if ($PSBoundParameters.ContainsKey('ScheduledTime')) {
                $timeStr = $ScheduledTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
                Write-Verbose "Scheduling backup for $timeStr"
                $xmlBody = "<job><start>$timeStr</start><description>User-requested backup</description><type>backup</type><commands><backup /></commands></job>"
            }
            else {
                $xmlBody = '<job><description>User-requested backup</description><type>backup</type><immediate /><commands><backup /></commands></job>'
            }

            Write-Verbose "=== API Request Details ==="
            Write-Verbose "Method: POST"
            Write-Verbose "URI: $baseUrl/users/$userId/devices/$connectorGuid/jobs/"
            Write-Verbose "Content-Type: application/xml"
            Write-Verbose "Request Body:`n$xmlBody"

            # Build request
            $uri = "$baseUrl/users/$userId/devices/$connectorGuid/jobs/"
            $headers = @{
                'Authorization' = $authHeader
                'Content-Type' = 'application/xml'
                'Accept' = 'application/vnd.keepit.v4+xml'
            }

            Write-Verbose "=== Sending API Request ==="

            if ($PSCmdlet.ShouldProcess("connector $($resolved.Name) ($connectorGuid)", 'Start backup')) {
                # Make API call with error handling
                try {
                    $response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $xmlBody -ErrorAction Stop
                }
                catch {
                    # Handle HTTP error responses
                    $errorMessage = $_.Exception.Message
                    $statusCode = $null
                    $apiError = $null
                    $errorBody = $null

                    # Try to extract the error response body
                    if ($_.Exception -is [Microsoft.PowerShell.Commands.HttpResponseException]) {
                        $statusCode = $_.Exception.Response.StatusCode.value__
                        Write-Verbose "HTTP Status Code: $statusCode"
                    }
                    elseif ($_.Exception.Response) {
                        $statusCode = $_.Exception.Response.StatusCode.value__
                        Write-Verbose "HTTP Status Code: $statusCode"
                    }

                    # Try to get error response body from ErrorDetails
                    if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
                        $errorBody = $_.ErrorDetails.Message
                        Write-Verbose "Error response body from ErrorDetails:`n$errorBody"
                    }
                    elseif ($errorMessage -match '(?s)<error>.*?</error>') {
                        # Fall back to extracting from error message with single-line mode
                        $errorBody = $matches[0]
                        Write-Verbose "Error response body from exception message:`n$errorBody"
                    }

                    # Try to parse error response as XML
                    if ($errorBody) {
                        Write-Verbose "Attempting to parse error response as XML"
                        try {
                            $errorXml = [xml]$errorBody
                            $apiError = [PSCustomObject]@{
                                Code = $errorXml.error.code
                                Description = $errorXml.error.description
                                StartTime = $errorXml.error.'start-time'
                            }
                            Write-Verbose "Parsed API Error - Code: $($apiError.Code), Description: $($apiError.Description)"
                        }
                        catch {
                            Write-Verbose "Could not parse error XML: $($_.Exception.Message)"
                        }
                    }

                    # Handle specific error cases gracefully
                    if ($apiError -and ($apiError.Code -eq 'WAITING_JOB_TO_START' -or $apiError.Code -eq 'RUNNING_JOB')) {
                        $status = if ($apiError.Code -eq 'WAITING_JOB_TO_START') { 'AlreadyQueued' } else { 'AlreadyRunning' }
                        $reason = if ($apiError.Code -eq 'WAITING_JOB_TO_START') { 'another backup job is already queued' } else { 'a backup job is already running' }
                        Write-Verbose "Handling $($apiError.Code) gracefully"
                        Write-Warning "Cannot start backup for connector $connectorGuid - $reason (start time: $($apiError.StartTime))"

                        $statusObject = New-AlreadyQueuedResult `
                            -ConnectorGuid $connectorGuid -ConnectorName $resolved.Name `
                            -Status $status -ErrorCode $apiError.Code `
                            -ErrorDescription $apiError.Description -StartTime $apiError.StartTime

                        Write-Verbose "=== Job Creation Skipped ==="
                        Write-Verbose "Reason: $reason"
                        Write-Verbose "Existing job start time: $($apiError.StartTime)"
                        Write-Verbose "=== End Start-KeepitBackup ==="

                        return $statusObject
                    }
                    elseif ($apiError) {
                        # Build user-friendly error message for other API errors
                        $friendlyMessage = "API Error [$($apiError.Code)]: $($apiError.Description)"
                        if ($apiError.StartTime) {
                            $friendlyMessage += " (Start time: $($apiError.StartTime))"
                        }
                        throw $friendlyMessage
                    }
                    else {
                        # Re-throw original error if we couldn't parse it
                        throw
                    }
                }

                # Debug: Show raw response
                Write-Verbose "=== API Response Received ==="

                # Handle empty response (some connector types return 201 Created with no body)
                if ($null -eq $response -or ($response -is [string] -and [string]::IsNullOrWhiteSpace($response))) {
                    Write-Verbose "API returned empty response - treating as successful job submission"

                    $jobObject = [PSCustomObject]@{
                        ConnectorGuid = $connectorGuid
                        Type = 'backup'
                        Description = 'User-requested backup'
                        Status = 'Submitted'
                        CreatedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
                    }

                    Write-Verbose "=== Backup Job Submitted ==="
                    Write-Verbose "Connector: $connectorGuid"
                    Write-Verbose "Note: API did not return job details"
                    Write-Verbose "=== End Start-KeepitBackup ==="

                    return $jobObject
                }

                Write-Verbose "Response Type: $($response.GetType().FullName)"

                if ($response -is [System.Xml.XmlDocument]) {
                    Write-Verbose "Response is XML Document"
                    Write-Verbose "Response XML Content:"
                    Write-Verbose $response.OuterXml
                }
                elseif ($response -is [System.Xml.XmlElement]) {
                    Write-Verbose "Response is XML Element"
                    Write-Verbose "Root Element Name: $($response.LocalName)"
                    Write-Verbose "Response XML Content:"
                    Write-Verbose $response.OuterXml
                }
                else {
                    Write-Verbose "Response is Object (not XML)"
                    try {
                        Write-Verbose "Response Object (JSON):"
                        Write-Verbose ($response | ConvertTo-Json -Depth 5)
                    }
                    catch {
                        Write-Verbose "Could not convert response to JSON: $($_.Exception.Message)"
                        Write-Verbose "Response String: $($response | Out-String)"
                    }
                }

                # Check if response is an error (in case Invoke-RestMethod didn't throw)
                if ($response.error) {
                    Write-Verbose "Response contains error element - handling as API error"
                    $apiError = [PSCustomObject]@{
                        Code = $response.error.code
                        Description = $response.error.description
                        StartTime = $response.error.'start-time'
                    }
                    Write-Verbose "API Error - Code: $($apiError.Code), Description: $($apiError.Description)"

                    # Handle specific error cases gracefully
                    if ($apiError.Code -eq 'WAITING_JOB_TO_START' -or $apiError.Code -eq 'RUNNING_JOB') {
                        $status = if ($apiError.Code -eq 'WAITING_JOB_TO_START') { 'AlreadyQueued' } else { 'AlreadyRunning' }
                        $reason = if ($apiError.Code -eq 'WAITING_JOB_TO_START') { 'another backup job is already queued' } else { 'a backup job is already running' }
                        Write-Verbose "Handling $($apiError.Code) gracefully"
                        Write-Warning "Cannot start backup for connector $connectorGuid - $reason (start time: $($apiError.StartTime))"

                        $statusObject = New-AlreadyQueuedResult `
                            -ConnectorGuid $connectorGuid -ConnectorName $resolved.Name `
                            -Status $status -ErrorCode $apiError.Code `
                            -ErrorDescription $apiError.Description -StartTime $apiError.StartTime

                        Write-Verbose "=== Job Creation Skipped ==="
                        Write-Verbose "Reason: $reason"
                        Write-Verbose "Existing job start time: $($apiError.StartTime)"
                        Write-Verbose "=== End Start-KeepitBackup ==="

                        return $statusObject
                    }
                    else {
                        # Build user-friendly error message for other API errors
                        $friendlyMessage = "API Error [$($apiError.Code)]: $($apiError.Description)"
                        if ($apiError.StartTime) {
                            $friendlyMessage += " (Start time: $($apiError.StartTime))"
                        }
                        throw $friendlyMessage
                    }
                }

                # Parse response
                Write-Verbose "Attempting to parse response structure..."
                $job = if ($response.job) {
                    Write-Verbose "Found response.job element"
                    $response.job
                }
                elseif ($response.jobs.job) {
                    Write-Verbose "Found response.jobs.job element"
                    if ($response.jobs.job -is [System.Array]) {
                        Write-Verbose "response.jobs.job is an array, taking first element"
                        $response.jobs.job[0]
                    }
                    else {
                        Write-Verbose "response.jobs.job is a single object"
                        $response.jobs.job
                    }
                }
                else {
                    Write-Verbose "ERROR: Could not find job in response. Available properties:"
                    $response | Get-Member -MemberType Properties | ForEach-Object {
                        Write-Verbose " - $($_.Name): $($response.($_.Name))"
                    }
                    throw "Unexpected response structure from API. Expected 'job' or 'jobs.job' element. See verbose output for details."
                }

                Write-Verbose "Successfully parsed job. JobGuid: $($job.guid)"

                # Create and output job object
                $statusValue = if ($PSBoundParameters.ContainsKey('ScheduledTime')) {
                    'Scheduled'
                } elseif ($job.active -eq $true -or $job.active -eq 'true') {
                    'Active'
                } else {
                    'Pending'
                }
                $scheduledTimeValue = if ($PSBoundParameters.ContainsKey('ScheduledTime')) { $timeStr } else { $null }

                $jobObject = [PSCustomObject]@{
                    ConnectorGuid = $connectorGuid
                    Type = if ($job.type) { $job.type } else { 'backup' }
                    Description = if ($job.description) { $job.description } else { 'User-requested backup' }
                    Status = $statusValue
                    CreatedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
                    ScheduledTime = $scheduledTimeValue
                }

                Write-Verbose "=== Job Created Successfully ==="
                Write-Verbose "Type: $($jobObject.Type)"
                Write-Verbose "Status: $($jobObject.Status)"
                if ($scheduledTimeValue) { Write-Verbose "ScheduledTime: $scheduledTimeValue" }
                Write-Verbose "CreatedAt: $($jobObject.CreatedAt)"
                Write-Verbose "=== End Start-KeepitBackup ==="

                $jobObject
            }
        }
        catch {
            $errorIdentifier = if ($connectorGuid) { $connectorGuid } else { $Connector }
            $PSCmdlet.ThrowTerminatingError(
                [System.Management.Automation.ErrorRecord]::new(
                    [System.Exception]::new("Failed to start backup job for connector $errorIdentifier : $($_.Exception.Message)", $_.Exception),
                    'KeepitJobError',
                    [System.Management.Automation.ErrorCategory]::ConnectionError,
                    $errorIdentifier
                )
            )
        }
    }

    end {
        Write-Verbose "Start-KeepitBackup completed"
    }
}