Public/Get-CIHistory.ps1

function Get-CIHistory {
    <#
    .SYNOPSIS
        Pulls all tickets for a Configuration Item and generates an AI-powered summary.
    .DESCRIPTION
        Retrieves incident, change, and problem tickets associated with a server, application,
        or CI from ServiceNow, Jira Service Management, or a CSV/JSON export file.
        Uses AI to summarize the full history into an actionable briefing with timeline,
        recurring themes, and risk assessment.
    .EXAMPLE
        Get-CIHistory -CIName 'SQL-PROD-01' -Provider ServiceNow -Instance 'company.service-now.com' -Credential $cred
    .EXAMPLE
        Get-CIHistory -CIName 'SQL-PROD-01' -Provider File -FilePath '.\tickets.json' -SkipAI
    .EXAMPLE
        Get-CIHistory -CIName 'WEB-DMZ-03' -Provider Jira -BaseUrl 'https://company.atlassian.net' -Email 'admin@co.com' -OutputPath '.\report.html'
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$CIName,

        [Parameter()]
        [ValidateSet('ServiceNow', 'Jira', 'File')]
        [string]$Provider = 'ServiceNow',

        [Parameter()]
        [string]$Instance,

        [Parameter()]
        [PSCredential]$Credential,

        [Parameter()]
        [string]$ApiKey,

        [Parameter()]
        [string]$BaseUrl,

        [Parameter()]
        [string]$Email,

        [Parameter()]
        [string]$FilePath,

        [Parameter()]
        [ValidateRange(1, 60)]
        [int]$MonthsBack = 18,

        [Parameter()]
        [ValidateSet('Anthropic', 'OpenAI', 'Ollama', 'Custom')]
        [string]$AIProvider = 'Anthropic',

        [Parameter()]
        [string]$AIApiKey,

        [Parameter()]
        [string]$AIModel,

        [Parameter()]
        [string]$AIEndpoint,

        [Parameter()]
        [string]$OutputPath,

        [Parameter()]
        [switch]$SkipAI
    )

    Write-Verbose "Getting CI history for '$CIName' from $Provider (last $MonthsBack months)"

    # ── Retrieve tickets based on provider ──
    $allTickets = @()

    switch ($Provider) {
        'ServiceNow' {
            if (-not $Instance) {
                throw 'ServiceNow provider requires -Instance parameter (e.g., "company.service-now.com").'
            }

            $authParams = @{ Instance = $Instance }
            if ($Credential) { $authParams['Credential'] = $Credential }
            if ($ApiKey) { $authParams['ApiKey'] = $ApiKey }

            $fields = 'number,short_description,description,state,priority,category,subcategory,opened_at,closed_at,resolved_at,assigned_to,close_notes,work_notes'
            $dateFilter = "sys_created_on>javascript:gs.monthsAgo($MonthsBack)"

            # Incidents
            Write-Verbose 'Querying ServiceNow incidents...'
            $incidentQuery = "cmdb_ci.name=$CIName^$dateFilter"
            $incidents = Connect-ServiceNow @authParams -Method GET -Endpoint 'api/now/table/incident' `
                -QueryParameters @{
                    sysparm_query  = $incidentQuery
                    sysparm_fields = $fields
                    sysparm_limit  = '10000'
                } -Paginate

            foreach ($inc in @($incidents)) {
                $allTickets += [PSCustomObject]@{
                    Number           = $inc.number
                    Type             = 'Incident'
                    ShortDescription = $inc.short_description
                    Description      = $inc.description
                    State            = $inc.state
                    Priority         = $inc.priority
                    Category         = $inc.category
                    Subcategory      = $inc.subcategory
                    OpenedAt         = $inc.opened_at
                    ClosedAt         = $inc.closed_at
                    ResolvedAt       = $inc.resolved_at
                    AssignedTo       = if ($inc.assigned_to -is [string]) { $inc.assigned_to } else { $inc.assigned_to.display_value }
                    CloseNotes       = $inc.close_notes
                    WorkNotes        = $inc.work_notes
                    CIName           = $CIName
                    Source           = 'ServiceNow'
                }
            }
            Write-Verbose "Found $(@($incidents).Count) incidents"

            # Change Requests
            Write-Verbose 'Querying ServiceNow change requests...'
            $changeQuery = "cmdb_ci.name=$CIName^$dateFilter"
            $changes = Connect-ServiceNow @authParams -Method GET -Endpoint 'api/now/table/change_request' `
                -QueryParameters @{
                    sysparm_query  = $changeQuery
                    sysparm_fields = $fields
                    sysparm_limit  = '10000'
                } -Paginate

            foreach ($chg in @($changes)) {
                $allTickets += [PSCustomObject]@{
                    Number           = $chg.number
                    Type             = 'Change Request'
                    ShortDescription = $chg.short_description
                    Description      = $chg.description
                    State            = $chg.state
                    Priority         = $chg.priority
                    Category         = $chg.category
                    Subcategory      = $chg.subcategory
                    OpenedAt         = $chg.opened_at
                    ClosedAt         = $chg.closed_at
                    ResolvedAt       = $chg.resolved_at
                    AssignedTo       = if ($chg.assigned_to -is [string]) { $chg.assigned_to } else { $chg.assigned_to.display_value }
                    CloseNotes       = $chg.close_notes
                    WorkNotes        = $chg.work_notes
                    CIName           = $CIName
                    Source           = 'ServiceNow'
                }
            }
            Write-Verbose "Found $(@($changes).Count) change requests"

            # Problems
            Write-Verbose 'Querying ServiceNow problems...'
            $problemQuery = "cmdb_ci.name=$CIName^$dateFilter"
            $problems = Connect-ServiceNow @authParams -Method GET -Endpoint 'api/now/table/problem' `
                -QueryParameters @{
                    sysparm_query  = $problemQuery
                    sysparm_fields = $fields
                    sysparm_limit  = '10000'
                } -Paginate

            foreach ($prb in @($problems)) {
                $allTickets += [PSCustomObject]@{
                    Number           = $prb.number
                    Type             = 'Problem'
                    ShortDescription = $prb.short_description
                    Description      = $prb.description
                    State            = $prb.state
                    Priority         = $prb.priority
                    Category         = $prb.category
                    Subcategory      = $prb.subcategory
                    OpenedAt         = $prb.opened_at
                    ClosedAt         = $prb.closed_at
                    ResolvedAt       = $prb.resolved_at
                    AssignedTo       = if ($prb.assigned_to -is [string]) { $prb.assigned_to } else { $prb.assigned_to.display_value }
                    CloseNotes       = $prb.close_notes
                    WorkNotes        = $prb.work_notes
                    CIName           = $CIName
                    Source           = 'ServiceNow'
                }
            }
            Write-Verbose "Found $(@($problems).Count) problems"
        }

        'Jira' {
            if (-not $BaseUrl) {
                throw 'Jira provider requires -BaseUrl parameter (e.g., "https://company.atlassian.net").'
            }
            if (-not $Email) {
                throw 'Jira provider requires -Email parameter for authentication.'
            }

            $jql = "(""Affected CI"" = ""$CIName"" OR summary ~ ""$CIName"" OR description ~ ""$CIName"") AND created >= ""-${MonthsBack}m"""

            Write-Verbose "Querying Jira with JQL: $jql"

            $jiraParams = @{
                BaseUrl = $BaseUrl
                Email   = $Email
                Method  = 'GET'
                Endpoint = 'rest/api/3/search'
                QueryParameters = @{
                    jql        = $jql
                    maxResults = '100'
                    fields     = 'summary,description,status,priority,issuetype,created,resolutiondate,assignee,reporter,comment,components'
                }
                Paginate = $true
            }
            if ($ApiKey) { $jiraParams['ApiToken'] = $ApiKey }

            $jiraIssues = Connect-JiraSM @jiraParams

            foreach ($issue in @($jiraIssues)) {
                $allTickets += ConvertFrom-JiraIssue -Issue $issue
            }
            Write-Verbose "Found $(@($jiraIssues).Count) Jira issues"
        }

        'File' {
            if (-not $FilePath) {
                throw 'File provider requires -FilePath parameter pointing to a CSV or JSON file.'
            }

            $allTickets = @(Import-TicketExport -Path $FilePath -CIFilter $CIName -MonthsBack $MonthsBack)
            Write-Verbose "Imported $($allTickets.Count) tickets from file"
        }
    }

    # Sort by date
    $allTickets = @($allTickets | Sort-Object {
        try { [datetime]$_.OpenedAt } catch { [datetime]::MinValue }
    })

    # Count by type
    $incidentCount = @($allTickets | Where-Object { $_.Type -eq 'Incident' }).Count
    $changeCount   = @($allTickets | Where-Object { $_.Type -like '*Change*' }).Count
    $problemCount  = @($allTickets | Where-Object { $_.Type -eq 'Problem' }).Count

    # Identify open items
    $openItems = @($allTickets | Where-Object {
        $_.State -notmatch '(?i)(closed|resolved|cancelled|completed|done)'
    })

    # Build timeline
    $timeline = @($allTickets | ForEach-Object {
        $dateStr = $_.OpenedAt
        try {
            $parsedDate = [datetime]::Parse($dateStr)
            $formattedDate = $parsedDate.ToString('yyyy-MM-dd')
        }
        catch {
            $formattedDate = $dateStr
        }
        [PSCustomObject]@{
            Date        = $formattedDate
            Number      = $_.Number
            Type        = $_.Type
            Description = $_.ShortDescription
            State       = $_.State
        }
    })

    # ── AI Summary ──
    $summary = ''
    if (-not $SkipAI -and $allTickets.Count -gt 0) {
        Write-Verbose 'Generating AI summary...'

        # Load prompt template
        $templatePath = Join-Path $PSScriptRoot '..\Templates\ci-history-prompt.txt'
        if (Test-Path $templatePath) {
            $promptTemplate = Get-Content -Path $templatePath -Raw -Encoding UTF8
        }
        else {
            $promptTemplate = @"
You are an IT service management analyst. Given the following ticket history for configuration item "{ci_name}", provide a comprehensive summary.
 
Analyze ALL tickets and provide:
1. Executive Summary (2-3 sentences)
2. Timeline of Major Events (chronological)
3. Recurring Themes
4. Current State
5. Risk Assessment
 
TICKET DATA:
{ticket_data}
"@

        }

        # Build ticket data text
        $ticketDataText = ($allTickets | ForEach-Object {
            "[$($_.Type)] $($_.Number) | $($_.OpenedAt) | $($_.ShortDescription) | State: $($_.State) | Priority: $($_.Priority) | Assigned: $($_.AssignedTo) | Close Notes: $($_.CloseNotes)"
        }) -join "`n"

        $prompt = $promptTemplate -replace '\{ci_name\}', $CIName -replace '\{ticket_data\}', $ticketDataText

        $aiParams = @{
            Prompt   = $prompt
            Provider = $AIProvider
        }
        if ($AIApiKey)    { $aiParams['ApiKey']   = $AIApiKey }
        if ($AIModel)     { $aiParams['Model']    = $AIModel }
        if ($AIEndpoint)  { $aiParams['Endpoint'] = $AIEndpoint }

        try {
            $summary = Invoke-AICompletion @aiParams
        }
        catch {
            Write-Warning "AI summary generation failed: $($_.Exception.Message). Returning raw data only."
            $summary = "[AI summary unavailable: $($_.Exception.Message)]"
        }
    }
    elseif ($allTickets.Count -eq 0) {
        $summary = "No tickets found for CI '$CIName' in the last $MonthsBack months."
    }

    # ── Build result object ──
    $result = [PSCustomObject]@{
        CIName        = $CIName
        TicketCount   = $allTickets.Count
        IncidentCount = $incidentCount
        ChangeCount   = $changeCount
        ProblemCount  = $problemCount
        Summary       = $summary
        Timeline      = $timeline
        OpenItems     = $openItems
        RawTickets    = $allTickets
        Provider      = $Provider
        MonthsBack    = $MonthsBack
        GeneratedAt   = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    }

    # ── Generate HTML report if requested ──
    if ($OutputPath) {
        Write-Verbose "Generating HTML report: $OutputPath"
        New-HtmlDashboard -ReportType 'CIHistory' -Data $result -OutputPath $OutputPath
        Write-Host "HTML report saved to: $OutputPath" -ForegroundColor Cyan
    }

    return $result
}