Public/Import-FsKnowledgeBase.ps1

Function Import-FsKnowledgeBase {
<#
.SYNOPSIS
    Imports changes from FreshService to the local knowledge base repository
.DESCRIPTION
    The Import-FsKnowledgeBase function retrieves the latest content from FreshService
    and updates local HTML and JSON files. This is useful for pulling changes made
    directly in FreshService back to your git repository.
.EXAMPLE
    Import-FsKnowledgeBase -OutputPath "C:\MyRepo\Articles"
    Imports all changes from FreshService to the local repository
.EXAMPLE
    Import-FsKnowledgeBase -OutputPath "C:\MyRepo\Articles" -ArticleID 123
    Imports only the specified article
.PARAMETER OutputPath
    The local folder path containing the knowledge base files
.PARAMETER ArticleID
    Optional: Import only a specific article
.PARAMETER CategoryID
    Optional: Import only articles from a specific category
.PARAMETER Force
    Overwrites local files even if they appear newer than FreshService
.INPUTS
    None
.OUTPUTS
    [PSCustomObject] - Returns import summary with counts of items updated
.NOTES
    Requires FreshService API authentication
    Expects existing folder structure created by Export-FsKnowledgeBase
.LINK
    https://api.freshservice.com/v2/#solution_articles
#>

    [CmdletBinding(SupportsShouldProcess)]
    Param(
        [Parameter(Mandatory=$true,
            Position=0)]
            [string]$OutputPath,
            
        [Parameter(Mandatory=$false)]
            [Int64]$ArticleID,
            
        [Parameter(Mandatory=$false)]
            [Int64]$CategoryID,
            
        [Parameter(Mandatory=$false)]
            [switch]$Force
    )
    
    Begin {
        Write-Verbose -Message "Starting $($MyInvocation.InvocationName)..."
        Write-Verbose -Message "Parameters are $($PSBoundParameters | Select-Object -Property *)"
        Connect-FreshServiceAPI
        
        # Initialize counters
        $importSummary = @{
            CategoriesProcessed = 0
            FoldersProcessed = 0
            ArticlesUpdated = 0
            ArticlesSkipped = 0
            Errors = @()
            StartTime = Get-Date
        }
    } 
    
    Process {
        try {
            if (-not (Test-Path $OutputPath)) {
                throw "Output path does not exist: $OutputPath. Run Export-FsKnowledgeBase first to create the initial structure."
            }
            
            if ($ArticleID) {
                # Import specific article
                Write-Host "Importing specific article ID: $ArticleID" -ForegroundColor Green
                
                try {
                    $article = Get-FsArticle -ID $ArticleID
                    $result = Import-SingleArticle -Article $article -OutputPath $OutputPath -Force:$Force
                    if ($result) {
                        $importSummary.ArticlesUpdated++
                    } else {
                        $importSummary.ArticlesSkipped++
                    }
                }
                catch {
                    $errorMsg = "Failed to import article ID ${ArticleID}: $_"
                    Write-Warning $errorMsg
                    $importSummary.Errors += $errorMsg
                }
            }
            else {
                # Import all articles or articles from specific category
                if ($CategoryID) {
                    Write-Host "Importing articles from category ID: $CategoryID" -ForegroundColor Green
                    $articles = Get-FsArticle -All | Where-Object { $_.CategoryID -eq $CategoryID }
                }
                else {
                    Write-Host "Importing all articles from FreshService..." -ForegroundColor Green
                    $articles = Get-FsArticle -All
                }
                
                # Group articles by category and folder for organized processing
                $groupedArticles = $articles | Group-Object CategoryID, FolderID
                
                foreach ($group in $groupedArticles) {
                    $categoryID = $group.Group[0].CategoryID
                    $categoryName = $group.Group[0].CategoryName
                    $folderName = $group.Group[0].FolderName
                    
                    Write-Host "Processing Category: $categoryName, Folder: $folderName" -ForegroundColor Yellow
                    $importSummary.FoldersProcessed++
                    
                    foreach ($article in $group.Group) {
                        Write-Host " Importing article: $($article.title)" -ForegroundColor Cyan
                        
                        try {
                            $result = Import-SingleArticle -Article $article -OutputPath $OutputPath -Force:$Force
                            if ($result) {
                                $importSummary.ArticlesUpdated++
                                Write-Host " Updated: $($article.title)" -ForegroundColor Green
                            } else {
                                $importSummary.ArticlesSkipped++
                                Write-Verbose " Skipped: $($article.title) (no changes)"
                            }
                        }
                        catch {
                            $errorMsg = "Failed to import article '$($article.title)' (ID: $($article.id)): $_"
                            Write-Warning $errorMsg
                            $importSummary.Errors += $errorMsg
                        }
                    }
                }
                
                # Count unique categories processed
                $importSummary.CategoriesProcessed = ($articles | Select-Object CategoryID -Unique).Count
            }
            
            # Update final summary
            $importSummary.EndTime = Get-Date
            $importSummary.Duration = $importSummary.EndTime - $importSummary.StartTime
            
            Write-Host "`nImport completed!" -ForegroundColor Green
            Write-Host "Categories processed: $($importSummary.CategoriesProcessed)" -ForegroundColor White
            Write-Host "Folders processed: $($importSummary.FoldersProcessed)" -ForegroundColor White  
            Write-Host "Articles updated: $($importSummary.ArticlesUpdated)" -ForegroundColor White
            Write-Host "Articles skipped: $($importSummary.ArticlesSkipped)" -ForegroundColor White
            Write-Host "Duration: $($importSummary.Duration.ToString('mm\:ss'))" -ForegroundColor White
            
            if ($importSummary.Errors.Count -gt 0) {
                Write-Host "Errors: $($importSummary.Errors.Count)" -ForegroundColor Red
                $importSummary.Errors | ForEach-Object { Write-Host " $_" -ForegroundColor Red }
            }
            
            return [PSCustomObject]$importSummary
        }
        catch {
            Write-Error "Import failed: $_"
            throw
        }
    } 
    
    End {
        Write-Verbose -Message "Ending $($MyInvocation.InvocationName)..."
    }
}

Function Import-SingleArticle {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Article,
        
        [Parameter(Mandatory=$true)]
        [string]$OutputPath,
        
        [Parameter(Mandatory=$false)]
        [switch]$Force
    )
    
    try {
        # Build the expected file paths
        $categoryFolderName = ConvertTo-SafeFolderName -Name $Article.CategoryName
        $folderName = ConvertTo-SafeFolderName -Name $Article.FolderName
        $articleFileName = ConvertTo-SafeFileName -Name $Article.title
        
        $categoryPath = Join-Path $OutputPath $categoryFolderName
        $folderPath = Join-Path $categoryPath $folderName
        $htmlPath = Join-Path $folderPath "$articleFileName.html"
        $jsonPath = Join-Path $folderPath "$articleFileName.json"
        
        # Ensure directory structure exists
        if (-not (Test-Path $folderPath)) {
            Write-Verbose "Creating folder structure: $folderPath"
            New-Item -ItemType Directory -Path $folderPath -Force | Out-Null
        }
        
        # Check if update is needed
        $needsUpdate = $Force
        if (-not $needsUpdate -and (Test-Path $jsonPath)) {
            try {
                $existingMetadata = Get-Content $jsonPath | ConvertFrom-Json
                $fsModified = [datetime]$Article.updated_at
                $localModified = [datetime]$existingMetadata.updated_at
                $needsUpdate = $fsModified -gt $localModified
            }
            catch {
                Write-Verbose "Could not compare timestamps, forcing update"
                $needsUpdate = $true
            }
        }
        elseif (-not (Test-Path $jsonPath)) {
            $needsUpdate = $true
        }
        
        if ($needsUpdate) {
            # Update HTML content
            if ($Article.description) {
                $Article.description | Out-File -FilePath $htmlPath -Encoding UTF8
                Write-Verbose "Updated HTML content: $htmlPath"
            }
            
            # Update JSON metadata (exclude content and computed properties)
            $articleMetadata = $Article | Select-Object * -ExcludeProperty description, CategoryName, FolderName
            $articleMetadata | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonPath -Encoding UTF8
            Write-Verbose "Updated metadata: $jsonPath"
            
            return $true
        }
        else {
            Write-Verbose "Article is up to date: $($Article.title)"
            return $false
        }
    }
    catch {
        Write-Error "Failed to import article '$($Article.title)': $_"
        throw
    }
}