<#
.SYNOPSIS
Update unified ToC file for publishing reference docs on docs.microsoft.com

.DESCRIPTION
Given a doc repo location and a location to output the ToC generate a Unified
Table of Contents:

* Get list of packages onboarded to docs.microsoft.com (domain specific)
* Get metadata for onboarded packages from metadata CSV
* Build a sorted list of services
* Add ToC nodes for the service
* Add "Core" packages to the bottom of the ToC under "Other"

ToC node layout:
* Service (service level overview page)
  * Client Package 1 (package level overview page)
  * Client Package 2 (package level overview page)
  ...
  * Management
    * Management Package 1
    * Management Package 2
    ...

.PARAMETER DocRepoLocation
Location of the documentation repo. This repo may be sparsely checked out
depending on the requirements for the domain

.PARAMETER OutputLocation
Output location for unified reference yml file

#>

param(
  [Parameter(Mandatory = $true)]
  [string] $DocRepoLocation,

  [Parameter(Mandatory = $true)]
  [string] $OutputLocation
)
. $PSScriptRoot/common.ps1
. $PSScriptRoot/Helpers/PSModule-Helpers.ps1

Install-ModuleIfNotInstalled "powershell-yaml" "0.4.1" | Import-Module

Set-StrictMode -Version 3

function GetClientPackageNode($clientPackage) {
  $packageInfo = &$GetDocsMsTocDataFn `
    -packageMetadata $clientPackage `
    -docRepoLocation $DocRepoLocation

  return [PSCustomObject]@{
    name     = $packageInfo.PackageTocHeader
    href     = $packageInfo.PackageLevelReadmeHref
    # This is always one package and it must be an array
    children = $packageInfo.TocChildren
  };
}

function GetPackageKey($pkg) {
  $pkgKey = $pkg.Package
  $groupId = $null

  if ($pkg.PSObject.Members.Name -contains "GroupId") {
    $groupId = $pkg.GroupId
  }

  if ($groupId) {
    $pkgKey = "${groupId}:${pkgKey}"
  }

  return $pkgKey
}

function GetPackageLookup($packageList) {
  $packageLookup = @{}

  foreach ($pkg in $packageList) {
    $pkgKey = GetPackageKey $pkg

    # We want to prefer updating non-hidden packages but if there is only
    # a hidden entry then we will return that
    if (!$packageLookup.ContainsKey($pkgKey) -or $packageLookup[$pkgKey].Hide -eq "true") {
      $packageLookup[$pkgKey] = $pkg
    }
    else {
      # Warn if there are more then one non-hidden package
      if ($pkg.Hide -ne "true") {
        Write-Host "Found more than one package entry for $($pkg.Package) selecting the first non-hidden one."
      }
    }
  }

  return $packageLookup
}

$onboardedPackages = &$GetOnboardedDocsMsPackagesFn `
  -DocRepoLocation $DocRepoLocation

# This criteria is different from criteria used in `Update-DocsMsPackages.ps1`
# because we need to generate ToCs for packages which are not necessarily "New"
# in the metadata AND onboard legacy packages (which `Update-DocsMsPackages.ps1`
# does not do)
$fullMetadata = Get-CSVMetadata
$metadata = @()
foreach($metadataEntry in $fullMetadata) {
  if ($metadataEntry.Package -and $metadataEntry.Hide -ne 'true') {
    $pkgKey = GetPackageKey $metadataEntry
    if($onboardedPackages.ContainsKey($pkgKey)) {
      $metadata += $metadataEntry
    }
  }
}

$fileMetadata = @()
foreach ($metadataFile in Get-ChildItem "$DocRepoLocation/metadata/*/*.json" -Recurse) {
  $fileContent = Get-Content $metadataFile -Raw
  $metadataEntry = ConvertFrom-Json $fileContent

  if ($metadataEntry) {
    $fileMetadata += $metadataEntry
  }
}

# Add file metadata information to package metadata from metadata CSV. Because
# metadata can exist for packages in both preview and GA there may be more than
# one file metadata entry. If that is the case keep the first entry found. We
# only use the `DirectoryPath` property from the json file metadata at this time
for ($i = 0; $i -lt $metadata.Count; $i++) {
  foreach ($fileEntry in $fileMetadata) {
    if ($fileEntry.Name -eq $metadata[$i].Package) {
      if ($metadata[$i].PSObject.Members.Name -contains "FileMetadata") {
        Write-Host "File metadata already added for $($metadata[$i].Package). Keeping the first entry found."
        continue
      }
      if (!($metadata[$i].PSObject.Members.Name -contains "GroupId") -or ($fileEntry.Group -eq $metadata[$i].GroupId)) {
        Add-Member `
          -InputObject $metadata[$i] `
          -MemberType NoteProperty `
          -Name FileMetadata `
          -Value $fileEntry
      }
    }
  }
}

$packagesForToc = @{}
$allPackages = GetPackageLookup $metadata
foreach ($metadataKey in $allPackages.Keys) {
  $metadataEntry = $allPackages[$metadataKey]
  if (!$metadataEntry.ServiceName) {
    LogWarning "Empty ServiceName for package `"$metadataKey`". Skipping."
    continue
  }
  $packagesForToc[$metadataKey] = $metadataEntry
}

# Get unique service names and sort alphabetically to act as the service nodes
# in the ToC
$services = @{}
foreach ($package in $packagesForToc.Values) {
  if ($package.ServiceName -eq 'Other') {
    # Skip packages under the service category "Other". Those will be handled
    # later
    continue
  }
  if (!$services.ContainsKey($package.ServiceName)) {
    $services[$package.ServiceName] = $true
  }
}
$serviceNameList = $services.Keys | Sort-Object

$toc = @()
foreach ($service in $serviceNameList) {
  Write-Host "Building service: $service"

  $packageItems = @()

  # Client packages get individual entries
  $clientPackages = $packagesForToc.Values.Where({ $_.ServiceName -eq $service -and ('client' -eq $_.Type) })
  $clientPackages = $clientPackages | Sort-Object -Property Package
  foreach ($clientPackage in $clientPackages) {
    $packageItems += GetClientPackageNode -clientPackage $clientPackage
  }

  # All management packages go under a single `Management` header in the ToC
  $mgmtPackages = $packagesForToc.Values.Where({ $_.ServiceName -eq $service -and ('mgmt' -eq $_.Type) })
  $mgmtPackages = $mgmtPackages | Sort-Object -Property Package
  if ($mgmtPackages) {
    $children = &$GetDocsMsTocChildrenForManagementPackagesFn `
      -packageMetadata $mgmtPackages `
      -docRepoLocation $DocRepoLocation

    $packageItems += [PSCustomObject]@{
      name     = 'Management'
      # There could be multiple packages, ensure this is treated as an array
      # even if it is a single package
      children = @($children)
    }
  }

  $uncategorizedPackages = $packagesForToc.Values.Where({ $_.ServiceName -eq $service -and !(@('client', 'mgmt') -contains $_.Type) })
  if ($uncategorizedPackages) {
    foreach ($package in $uncategorizedPackages) {
      LogWarning "Uncategorized package for service: $service - $($package.Package). Package not onboarded."
    }
  }

  $serviceReadmeBaseName = $service.ToLower().Replace(' ', '-').Replace('/', '-')
  $serviceTocEntry = [PSCustomObject]@{
    name            = $service;
    href            = "~/docs-ref-services/{moniker}/$serviceReadmeBaseName.md"
    landingPageType = 'Service'
    items           = @($packageItems)
  }
  $toc += $serviceTocEntry
}

# Core packages belong under the "Other" node in the ToC
$otherPackageItems = New-Object -TypeName System.Collections.Generic.List[PSCustomObject]
$otherPackages = $packagesForToc.Values.Where({ $_.ServiceName -eq 'Other' })
$otherPackages = $otherPackages | Sort-Object -Property DisplayName

if ($otherPackages) {
  foreach ($otherPackage in $otherPackages) {
    $segments = $otherPackage.DisplayName.Split('-').ForEach({ $_.Trim() })


    if ($segments.Count -gt 1) {
      $currentNode = $otherPackageItems

      # Iterate up to the penultimate item in the array so that the final item
      # in the array can be added as a leaf node. Since the array always has at
      # least two elements this iteration will cover at least the first element.
      # e.g. @(0, 1)[0..0] => 0
      foreach ($segment in $segments[0..($segments.Count - 2)]) {
        $matchingNode = $currentNode.Where({ $_.name -eq $segment })

        # ToC nodes can be "branches" which contain 0 or more branch
        # or leaf nodes in an "items" field OR they can be leaf nodes which have
        # a "children" field which can only contain package names or namespaces.
        # A node cannot contain both "items" and "children". If a node already
        # has a "children" field then it is a leaf node and cannot take
        # additional branch nodes.
        # Children are added using the `GetClientPackageNode` function
        if ($matchingNode -and $matchingNode.PSObject.Members.Name -contains "children") {
          LogWarning "Cannot create nested entry for package $($otherPackage.Package) because Segment `"$segment`" in the DisplayName $($otherPackage.DisplayName) is already a leaf node. Excluding package: $($otherPackage.Package)"
          $currentNode = $null
          break
        }

        if ($matchingNode) {
          $currentNode = $matchingNode[0].items
        }
        else {
          $newNode = [PSCustomObject]@{
            name            = $segment
            landingPageType = 'Service'
            items           = New-Object -TypeName System.Collections.Generic.List[PSCustomObject]
          }
          $currentNode.Add($newNode)
          $currentNode = $newNode.items
        }
      }

      if ($null -ne $currentNode) {
        $otherPackage.DisplayName = $segments[$segments.Count - 1]
        $currentNode.Add((GetClientPackageNode $otherPackage))
      }

    }
    else {
      $otherPackageItems.Add((GetClientPackageNode $otherPackage))
    }
  }
}
$toc += [PSCustomObject]@{
  name            = 'Other';
  landingPageType = 'Service';
  items           = $otherPackageItems + @(
    [PSCustomObject]@{
      name            = "Uncategorized Packages";
      landingPageType = 'Service';
      # All onboarded packages which have not been placed in the ToC will be
      # handled by the docs system here. In this case the list would consist of
      # packages whose ServiceName field is empty in the metadata.
      children        = @('**');
    }
  )
}

$output = @([PSCustomObject]@{
    name            = 'Reference';
    landingPageType = 'Root';
    expanded        = $false;
    items           = $toc
  })

if (Test-Path "Function:$UpdateDocsMsTocFn") {
  $output = &$UpdateDocsMsTocFn -toc $output
}

$outputYaml = ConvertTo-Yaml $output
Set-Content -Path $OutputLocation -Value $outputYaml
