param(
    [string]$TemplatePath = "C:\Users\niuniu\Desktop\Report.pptx",
    [string]$OutputPath = "C:\workspace\cursor\output\vip-report\generated-from-template.pptx",
    [string]$OperationsPath = ""
)

$ErrorActionPreference = "Stop"

function Get-ShapeChildren {
    param([Parameter(Mandatory = $true)]$Shape)
    $children = @()
    try {
        $count = $Shape.GroupItems.Count
        for ($i = 1; $i -le $count; $i++) {
            $child = $Shape.GroupItems.Item($i)
            $children += $child
            $children += Get-ShapeChildren -Shape $child
        }
    }
    catch {
    }
    return $children
}

function Find-Shape {
    param(
        [Parameter(Mandatory = $true)]$Slide,
        [int]$ShapeId,
        [string]$ShapeName,
        [string]$ExactText,
        [string]$ContainsText
    )

    $targetNumber = $null
    if ($ShapeName) {
        $numMatch = [regex]::Match($ShapeName, "(\d+)\s*$")
        if ($numMatch.Success) {
            $targetNumber = $numMatch.Groups[1].Value
        }
    }

    $candidates = @()
    foreach ($shape in $Slide.Shapes) {
        $candidates += $shape
        $candidates += Get-ShapeChildren -Shape $shape
    }

    foreach ($shape in $candidates) {
        if ($ShapeId -and $shape.Id -eq $ShapeId) {
            return $shape
        }

        if ($ShapeName) {
            if ($shape.Name -eq $ShapeName) {
                return $shape
            }
            if ($targetNumber) {
                $shapeNumMatch = [regex]::Match([string]$shape.Name, "(\d+)\s*$")
                if ($shapeNumMatch.Success -and $shapeNumMatch.Groups[1].Value -eq $targetNumber) {
                    $shapeNameLower = ([string]$shape.Name).ToLowerInvariant()
                    if ($shapeNameLower.Contains("picture") -or $shapeNameLower.Contains("group") -or $shapeNameLower.Contains("table")) {
                        return $shape
                    }
                }
            }
        }

        $text = $null
        try {
            if ($shape.HasTextFrame -eq -1 -and $shape.TextFrame.HasText -eq -1) {
                $text = $shape.TextFrame.TextRange.Text
            }
        }
        catch {
            $text = $null
        }

        if ($ExactText -and $text -eq $ExactText) {
            return $shape
        }

        if ($ContainsText -and $text -and $text.Contains($ContainsText)) {
            return $shape
        }
    }

    return $null
}

function Set-ShapeText {
    param(
        [Parameter(Mandatory = $true)]$Shape,
        [Parameter(Mandatory = $true)][string]$NewText
    )

    if ($Shape.HasTextFrame -ne -1) {
        throw "Shape '$($Shape.Name)' does not support text."
    }
    $Shape.TextFrame.TextRange.Text = $NewText
}

function Set-ShapeZOrderAndName {
    param(
        [Parameter(Mandatory = $true)]$Shape,
        [int]$TargetZ = 0,
        [string]$Name = ""
    )

    if ($TargetZ -gt 0) {
        while ($Shape.ZOrderPosition -lt $TargetZ) {
            $Shape.ZOrder(1) | Out-Null
        }
    }
    if ($Name) {
        try {
            $Shape.Name = $Name
        }
        catch {
        }
    }
}

function Replace-PictureShape {
    param(
        [Parameter(Mandatory = $true)]$Slide,
        [Parameter(Mandatory = $true)]$Shape,
        [Parameter(Mandatory = $true)][string]$ImagePath
    )

    if (-not (Test-Path -LiteralPath $ImagePath)) {
        throw "ImagePath not found: $ImagePath"
    }

    $left = $Shape.Left
    $top = $Shape.Top
    $width = $Shape.Width
    $height = $Shape.Height
    $z = $Shape.ZOrderPosition
    $name = $Shape.Name

    $Shape.Delete()
    $newShape = $Slide.Shapes.AddPicture($ImagePath, $false, $true, $left, $top, $width, $height)
    Set-ShapeZOrderAndName -Shape $newShape -TargetZ $z -Name $name
}

function Add-PictureAtBox {
    param(
        [Parameter(Mandatory = $true)]$Slide,
        [Parameter(Mandatory = $true)]$Item
    )

    $imagePath = [string]$Item.image_path
    if (-not (Test-Path -LiteralPath $imagePath)) {
        throw "ImagePath not found: $imagePath"
    }
    $box = $Item.target_box
    if ($null -eq $box) {
        throw "target_box is required when image op has no target shape."
    }
    $left = [double]$box.left / 12700.0
    $top = [double]$box.top / 12700.0
    $width = [double]$box.width / 12700.0
    $height = [double]$box.height / 12700.0
    $newShape = $Slide.Shapes.AddPicture($imagePath, $false, $true, $left, $top, $width, $height)
    if ($Item.shape_name) {
        try {
            $newShape.Name = [string]$Item.shape_name
        }
        catch {
        }
    }
}

function Try-UnblockFileSafe {
    param([string]$Path)
    if (-not $Path) {
        return
    }
    if (-not (Test-Path -LiteralPath $Path)) {
        return
    }
    try {
        Unblock-File -Path $Path -ErrorAction SilentlyContinue
    }
    catch {
    }
}

function New-PowerPointAppWithRetry {
    param(
        [int]$MaxAttempts = 3,
        [int]$DelayMs = 1000
    )

    $lastError = $null
    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        $app = $null
        try {
            $app = New-Object -ComObject PowerPoint.Application
            try { $app.DisplayAlerts = 1 } catch {}
            try { $app.Visible = -1 } catch {}
            $presentations = $app.Presentations
            if ($null -ne $presentations) {
                return $app
            }
            throw "PowerPoint Presentations collection is null."
        }
        catch {
            $lastError = $_
            if ($app -ne $null) {
                try { $app.Quit() | Out-Null } catch {}
            }
            Start-Sleep -Milliseconds $DelayMs
        }
    }

    if ($lastError) {
        throw $lastError
    }
    throw "Unable to initialize PowerPoint COM. Ensure desktop Office is installed and the session can create PowerPoint.Application."
}

function Try-OpenPresentation {
    param(
        [Parameter(Mandatory = $true)]$PptApp,
        [Parameter(Mandatory = $true)][string]$Path,
        [int]$MaxAttempts = 3,
        [int]$DelayMs = 1200
    )

    $lastError = $null
    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        try {
            Try-UnblockFileSafe -Path $Path
            $presentations = $PptApp.Presentations
            if ($null -eq $presentations) {
                throw "PowerPoint Presentations collection is null."
            }
            $pres = $presentations.Open($Path, 0, 0, 0)
            if ($null -ne $pres) {
                return $pres
            }
        }
        catch {
            $lastError = $_
            Start-Sleep -Milliseconds $DelayMs
        }
    }
    if ($lastError) {
        throw $lastError
    }
    throw "PowerPoint failed to open file: $Path"
}

function Open-PresentationWithFallback {
    param(
        [Parameter(Mandatory = $true)]$PptApp,
        [Parameter(Mandatory = $true)][string]$OutputPath,
        [Parameter(Mandatory = $true)][string]$TemplatePath
    )

    # 1) Primary: open output directly.
    try {
        $pres = Try-OpenPresentation -PptApp $PptApp -Path $OutputPath
        return [pscustomobject]@{
            Presentation = $pres
            OpenPath = $OutputPath
            UsingTempCopy = $false
            TempPath = ""
        }
    }
    catch {
    }

    # 2) Secondary: open template directly.
    if (
        $TemplatePath -and
        (Test-Path -LiteralPath $TemplatePath) -and
        (-not $TemplatePath.Equals($OutputPath, [System.StringComparison]::OrdinalIgnoreCase))
    ) {
        try {
            $pres = Try-OpenPresentation -PptApp $PptApp -Path $TemplatePath
            return [pscustomobject]@{
                Presentation = $pres
                OpenPath = $TemplatePath
                UsingTempCopy = $false
                TempPath = ""
            }
        }
        catch {
        }
    }

    # 3) Last resort: copy to temp and open temp copy.
    $sourcePath = $OutputPath
    if (-not (Test-Path -LiteralPath $sourcePath)) {
        $sourcePath = $TemplatePath
    }
    if (-not (Test-Path -LiteralPath $sourcePath)) {
        throw "Neither output nor template exists for fallback open. output=$OutputPath template=$TemplatePath"
    }

    $tempDir = Join-Path $env:TEMP "vip-report-render"
    if (-not (Test-Path -LiteralPath $tempDir)) {
        New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
    }
    $tempPath = Join-Path $tempDir ("render-work-" + [guid]::NewGuid().ToString("N") + ".pptx")
    Copy-Item -LiteralPath $sourcePath -Destination $tempPath -Force
    Try-UnblockFileSafe -Path $tempPath

    $tempPres = Try-OpenPresentation -PptApp $PptApp -Path $tempPath
    return [pscustomobject]@{
        Presentation = $tempPres
        OpenPath = $tempPath
        UsingTempCopy = $true
        TempPath = $tempPath
    }
}

function Convert-HexToRgbInt {
    param([string]$Hex)
    $clean = ([string]$Hex).Trim()
    if ($clean.StartsWith("#")) {
        $clean = $clean.Substring(1)
    }
    if ($clean.Length -ne 6) {
        return 0
    }
    $r = [Convert]::ToInt32($clean.Substring(0, 2), 16)
    $g = [Convert]::ToInt32($clean.Substring(2, 2), 16)
    $b = [Convert]::ToInt32($clean.Substring(4, 2), 16)
    return ($r + ($g * 256) + ($b * 65536))
}

function Set-ChartData {
    param(
        [Parameter(Mandatory = $true)]$Chart,
        [Parameter(Mandatory = $true)][string[]]$Headers,
        [Parameter(Mandatory = $true)][object[][]]$Rows
    )

    $Chart.ChartData.Activate()
    $wb = $Chart.ChartData.Workbook
    $ws = $wb.Worksheets.Item(1)
    try {
        $cols = $Headers.Count
        for ($c = 1; $c -le $cols; $c++) {
            $ws.Cells.Item(1, $c).Value2 = $Headers[$c - 1]
        }
        for ($r = 1; $r -le $Rows.Count; $r++) {
            $row = $Rows[$r - 1]
            for ($c = 1; $c -le $cols; $c++) {
                $value = $row[$c - 1]
                $ws.Cells.Item($r + 1, $c).Value2 = $value
            }
        }
        $endRow = $Rows.Count + 1
        $range = $ws.Range("A1", [string]([char](64 + $Headers.Count)) + $endRow)
        $Chart.SetSourceData($range)
    }
    finally {
        try {
            $wb.Application.Quit()
        }
        catch {
        }
    }
}

function Set-ComRgb {
    param(
        $Target,
        [Parameter(Mandatory = $true)][int]$Rgb
    )
    if ($null -eq $Target) {
        return
    }
    try {
        $Target.RGB = $Rgb
        return
    }
    catch {
    }
    try {
        $Target.ForeColor.RGB = $Rgb
    }
    catch {
    }
}

function Add-Slide12TrafficTable {
    param(
        [Parameter(Mandatory = $true)]$Slide,
        [Parameter(Mandatory = $true)]$OldShape,
        [Parameter(Mandatory = $true)]$Item
    )

    $left = $OldShape.Left
    $top = $OldShape.Top
    $width = $OldShape.Width
    $height = $OldShape.Height
    $z = $OldShape.ZOrderPosition
    $name = $OldShape.Name
    $OldShape.Delete()

    $headers = @($Item.headers)
    $rows = @($Item.rows)
    $rowCount = $rows.Count + 1
    $colCount = $headers.Count
    $tableShape = $Slide.Shapes.AddTable($rowCount, $colCount, $left, $top, $width, $height)
    Set-ShapeZOrderAndName -Shape $tableShape -TargetZ $z -Name $name

    $table = $tableShape.Table
    $baseWidths = @(92, 128, 144, 110, 144, 110, 122, 144, 110, 144, 110)
    if ($colCount -eq $baseWidths.Count) {
        $baseTotal = 0
        foreach ($bw in $baseWidths) { $baseTotal += $bw }
        for ($i = 1; $i -le $colCount; $i++) {
            $table.Columns.Item($i).Width = $width * ($baseWidths[$i - 1] / $baseTotal)
        }
    }

    $headerBg = Convert-HexToRgbInt "#5f6fce"
    $gridColor = Convert-HexToRgbInt "#d5d5d5"
    $black = Convert-HexToRgbInt "#111111"
    $white = Convert-HexToRgbInt "#ffffff"
    $lflPos = Convert-HexToRgbInt "#d11f1f"
    $lflNeg = Convert-HexToRgbInt "#0f9d58"

    for ($c = 1; $c -le $colCount; $c++) {
        $cell = $table.Cell(1, $c).Shape
        $cell.TextFrame.TextRange.Text = [string]$headers[$c - 1]
        Set-ComRgb -Target $cell.Fill.ForeColor -Rgb $headerBg
        Set-ComRgb -Target $cell.Line.ForeColor -Rgb $gridColor
        Set-ComRgb -Target $cell.TextFrame.TextRange.Font.Color -Rgb $white
        $cell.TextFrame.TextRange.Font.Bold = $true
        $cell.TextFrame.TextRange.Font.Size = 10
        $cell.TextFrame.TextRange.ParagraphFormat.Alignment = 2
        $cell.TextFrame.VerticalAnchor = 3
        $cell.TextFrame.MarginLeft = 2
        $cell.TextFrame.MarginRight = 2
        $cell.TextFrame.MarginTop = 1
        $cell.TextFrame.MarginBottom = 1
    }

    for ($r = 1; $r -le $rows.Count; $r++) {
        $row = @($rows[$r - 1])
        for ($c = 1; $c -le $colCount; $c++) {
            $value = [string]$row[$c - 1]
            $cell = $table.Cell($r + 1, $c).Shape
            $cell.TextFrame.TextRange.Text = $value
            Set-ComRgb -Target $cell.Fill.ForeColor -Rgb $white
            Set-ComRgb -Target $cell.Line.ForeColor -Rgb $gridColor
            $cell.TextFrame.TextRange.Font.Bold = ($r -eq $rows.Count)
            $cell.TextFrame.TextRange.Font.Size = 10
            $cell.TextFrame.TextRange.ParagraphFormat.Alignment = 2
            $cell.TextFrame.VerticalAnchor = 3
            $cell.TextFrame.MarginLeft = 2
            $cell.TextFrame.MarginRight = 2
            $cell.TextFrame.MarginTop = 1
            $cell.TextFrame.MarginBottom = 1

            $fontColor = $black
            if ($r -eq $rows.Count -and $c -gt 1) {
                if ($value.StartsWith("-")) {
                    $fontColor = $lflNeg
                }
                elseif (-not $value.StartsWith("0")) {
                    $fontColor = $lflPos
                }
            }
            Set-ComRgb -Target $cell.TextFrame.TextRange.Font.Color -Rgb $fontColor
        }
    }
}

function Add-Slide12TrendPanel {
    param(
        [Parameter(Mandatory = $true)]$Slide,
        [double]$Left,
        [double]$Top,
        [double]$Width,
        [double]$Height,
        [int]$Month,
        [Parameter(Mandatory = $true)]$Panel
    )

    $shape = $Slide.Shapes.AddChart2(201, 4, $Left, $Top, $Width, $Height)
    $chart = $shape.Chart
    $chart.HasTitle = $true
    $chart.ChartTitle.Text = [string]$Panel.title
    $chart.ChartTitle.Format.TextFrame2.TextRange.Font.Name = "Arial"
    $chart.ChartTitle.Format.TextFrame2.TextRange.Font.Size = 14
    $chart.HasLegend = $true
    $chart.Legend.Position = -4160
    $chart.PlotArea.Format.Fill.ForeColor.RGB = Convert-HexToRgbInt "#e7e7e7"
    $chart.ChartArea.Format.Fill.ForeColor.RGB = Convert-HexToRgbInt "#e7e7e7"

    $currentValues = @($Panel.current_values)
    $previousValues = @($Panel.previous_values)
    $lflValues = @($Panel.lfl_values)
    $count = $currentValues.Count

    $rows = @()
    for ($i = 0; $i -lt $count; $i++) {
        $day = $i + 1
        $label = ""
        if ($day -eq 1 -or $day -eq 8 -or $day -eq 16 -or $day -eq 23 -or $day -eq $count) {
            $label = "$($Month)-$($day)"
        }
        $rows += ,@($label, [double]$currentValues[$i], [double]$previousValues[$i], [double]$lflValues[$i])
    }
    Set-ChartData -Chart $chart -Headers @("Day", [string]$Panel.current_name, [string]$Panel.previous_name, "LFL") -Rows $rows

    $series1 = $chart.SeriesCollection(1)
    $series2 = $chart.SeriesCollection(2)
    $series3 = $chart.SeriesCollection(3)
    $series1.ChartType = 4
    $series2.ChartType = 4
    $series3.ChartType = 65
    $series3.AxisGroup = 2

    $colorCurrent = Convert-HexToRgbInt "#6f76ff"
    $colorPrevious = Convert-HexToRgbInt "#f08ad9"
    $colorLfl = Convert-HexToRgbInt "#b000c9"
    $series1.Format.Line.ForeColor.RGB = $colorCurrent
    $series2.Format.Line.ForeColor.RGB = $colorPrevious
    $series3.Format.Line.Visible = 0
    $series3.MarkerStyle = 8
    $series3.MarkerSize = 6
    $series3.MarkerForegroundColor = $colorLfl
    $series3.MarkerBackgroundColor = $colorLfl

    $valueAxis = $chart.Axes(2, 1)
    $valueAxis.MinimumScale = 0
    $valueAxis.MaximumScale = [double]$Panel.value_axis_max
    $valueAxis.MajorUnit = [double]$Panel.value_axis_major_unit

    $lflAxis = $chart.Axes(2, 2)
    $lflAxis.MinimumScale = [double]$Panel.lfl_axis_min
    $lflAxis.MaximumScale = [double]$Panel.lfl_axis_max
    $lflAxis.MajorUnit = [double]$Panel.lfl_axis_major_unit
    $lflAxis.TickLabels.NumberFormat = "0%"

    $categoryAxis = $chart.Axes(1, 1)
    $categoryAxis.TickLabelSpacing = 1
    $categoryAxis.TickMarkSpacing = 1
    $categoryAxis.HasTitle = $false

    return $shape
}

function Replace-Slide12DualLineLfl {
    param(
        [Parameter(Mandatory = $true)]$Slide,
        [Parameter(Mandatory = $true)]$OldShape,
        [Parameter(Mandatory = $true)]$Item
    )

    $left = $OldShape.Left
    $top = $OldShape.Top
    $width = $OldShape.Width
    $height = $OldShape.Height
    $z = $OldShape.ZOrderPosition
    $name = $OldShape.Name
    $OldShape.Delete()

    $panels = @($Item.panels)
    if ($panels.Count -lt 2) {
        throw "slide12_dual_line_lfl requires 2 panel configs."
    }
    $gap = 12.0
    $panelWidth = ($width - $gap) / 2.0
    $panelHeight = $height
    $month = [int]$Item.month

    $leftChartShape = Add-Slide12TrendPanel -Slide $Slide -Left $left -Top $top -Width $panelWidth -Height $panelHeight -Month $month -Panel $panels[0]
    $rightChartShape = Add-Slide12TrendPanel -Slide $Slide -Left ($left + $panelWidth + $gap) -Top $top -Width $panelWidth -Height $panelHeight -Month $month -Panel $panels[1]

    Set-ShapeZOrderAndName -Shape $leftChartShape -TargetZ $z -Name "$name-L"
    Set-ShapeZOrderAndName -Shape $rightChartShape -TargetZ ($z + 1) -Name "$name-R"
}

function Get-EntryDonutData {
    param([Parameter(Mandatory = $true)]$EntryRows, [Parameter(Mandatory = $true)][string]$Field)
    $pairs = @()
    foreach ($row in $EntryRows) {
        $pairs += ,@([string]$row.name, [double]$row.$Field)
    }
    $sorted = $pairs | Sort-Object { $_[1] } -Descending
    $top = @()
    $other = 0.0
    for ($i = 0; $i -lt $sorted.Count; $i++) {
        if ($i -lt 5) {
            $top += ,$sorted[$i]
        }
        else {
            $other += [double]$sorted[$i][1]
        }
    }
    if ($other -gt 0) {
        $top += ,@("Other", $other)
    }
    return $top
}

function Add-SimpleChartFromRows {
    param(
        [Parameter(Mandatory = $true)]$Slide,
        [int]$ChartType,
        [double]$Left,
        [double]$Top,
        [double]$Width,
        [double]$Height,
        [Parameter(Mandatory = $true)][string[]]$Headers,
        [Parameter(Mandatory = $true)][object[][]]$Rows,
        [string]$Title = ""
    )
    $shape = $Slide.Shapes.AddChart2(201, $ChartType, $Left, $Top, $Width, $Height)
    $chart = $shape.Chart
    if ($Title) {
        $chart.HasTitle = $true
        $chart.ChartTitle.Text = $Title
        $chart.ChartTitle.Format.TextFrame2.TextRange.Font.Name = "Arial"
        $chart.ChartTitle.Format.TextFrame2.TextRange.Font.Size = 14
    }
    else {
        $chart.HasTitle = $false
    }
    Set-ChartData -Chart $chart -Headers $Headers -Rows $Rows
    return $shape
}

function Replace-Slide12EntryMix {
    param(
        [Parameter(Mandatory = $true)]$Slide,
        [Parameter(Mandatory = $true)]$OldShape,
        [Parameter(Mandatory = $true)]$Item
    )

    $left = $OldShape.Left
    $top = $OldShape.Top
    $width = $OldShape.Width
    $height = $OldShape.Height
    $z = $OldShape.ZOrderPosition
    $name = $OldShape.Name
    $OldShape.Delete()

    $entries = @($Item.entries)
    $reportYear = [int]$Item.report_year
    $compareYear = [int]$Item.compare_year
    $reportLabel = ("{0}UV" -f ($reportYear.ToString().Substring($reportYear.ToString().Length - 2)))
    $compareLabel = ("{0}UV" -f ($compareYear.ToString().Substring($compareYear.ToString().Length - 2)))

    $leftWidth = [double]($width * 0.30)
    $rightWidth = $width - $leftWidth
    $donutGap = 10.0
    $donutWidth = ($rightWidth - $donutGap) / 2.0

    $barRows = @()
    foreach ($row in $entries) {
        $barRows += ,@([string]$row.name, [double]$row.report_uv, [double]$row.compare_uv)
    }
    $barShape = Add-SimpleChartFromRows -Slide $Slide -ChartType 51 -Left $left -Top $top -Width $leftWidth -Height $height -Headers @("Entry", $reportLabel, $compareLabel) -Rows $barRows -Title "Entry Traffic LFL"
    $barChart = $barShape.Chart
    $barChart.HasLegend = $true
    $barChart.Legend.Position = -4160
    $barChart.SeriesCollection(1).Format.Fill.ForeColor.RGB = Convert-HexToRgbInt "#6f76ff"
    $barChart.SeriesCollection(2).Format.Fill.ForeColor.RGB = Convert-HexToRgbInt "#f08ad9"
    $barChart.Axes(1, 1).TickLabels.Orientation = 35

    $reportDonutRows = Get-EntryDonutData -EntryRows $entries -Field "report_uv"
    $compareDonutRows = Get-EntryDonutData -EntryRows $entries -Field "compare_uv"
    $donutHeaders = @("Entry", "UV")

    $donut1Rows = @()
    foreach ($row in $reportDonutRows) {
        $donut1Rows += ,@($row[0], [double]$row[1])
    }
    $donut2Rows = @()
    foreach ($row in $compareDonutRows) {
        $donut2Rows += ,@($row[0], [double]$row[1])
    }

    $donutLeft = $left + $leftWidth
    $donutTop = [Math]::Max(0, $top - ($height * 0.08))
    $donutHeight = $height * 1.16
    $donut1Shape = Add-SimpleChartFromRows -Slide $Slide -ChartType -4120 -Left $donutLeft -Top $donutTop -Width $donutWidth -Height $donutHeight -Headers $donutHeaders -Rows $donut1Rows -Title ("{0} Entry Mix" -f ($reportYear.ToString().Substring($reportYear.ToString().Length - 2)))
    $donut2Shape = Add-SimpleChartFromRows -Slide $Slide -ChartType -4120 -Left ($donutLeft + $donutWidth + $donutGap) -Top $donutTop -Width $donutWidth -Height $donutHeight -Headers $donutHeaders -Rows $donut2Rows -Title ("{0} Entry Mix" -f ($compareYear.ToString().Substring($compareYear.ToString().Length - 2)))

    foreach ($donutShape in @($donut1Shape, $donut2Shape)) {
        $chart = $donutShape.Chart
        $chart.HasLegend = $false
        $series = $chart.SeriesCollection(1)
        $series.HasDataLabels = $true
        $series.DataLabels.ShowCategoryName = $true
        $series.DataLabels.ShowPercentage = $true
        $series.DataLabels.ShowValue = $false
        try {
            $series.DataLabels.Format.TextFrame2.TextRange.Font.Size = 10
            $series.DataLabels.Format.TextFrame2.TextRange.Font.Bold = -1
        } catch {}
        try {
            $series.DoughnutHoleSize = 65
        }
        catch {
        }
    }

    Set-ShapeZOrderAndName -Shape $barShape -TargetZ $z -Name "$name-Bar"
    Set-ShapeZOrderAndName -Shape $donut1Shape -TargetZ ($z + 1) -Name "$name-Donut-Report"
    Set-ShapeZOrderAndName -Shape $donut2Shape -TargetZ ($z + 2) -Name "$name-Donut-Compare"
}

function Apply-NativeChartOperation {
    param(
        [Parameter(Mandatory = $true)]$Presentation,
        [Parameter(Mandatory = $true)]$Item
    )

    $slide = $Presentation.Slides.Item([int]$Item.slide)
    $shape = Find-Shape -Slide $slide -ShapeId $item.shape_id -ShapeName $item.shape_name -ExactText $item.exact_text -ContainsText $item.contains_text
    if ($null -eq $shape) {
        throw "Native chart target not found on slide $($item.slide)"
    }

    $type = [string]$item.type
    switch ($type) {
        "slide12_traffic_table" {
            Add-Slide12TrafficTable -Slide $slide -OldShape $shape -Item $item
            break
        }
        "slide12_dual_line_lfl" {
            Replace-Slide12DualLineLfl -Slide $slide -OldShape $shape -Item $item
            break
        }
        "slide12_entry_mix" {
            Replace-Slide12EntryMix -Slide $slide -OldShape $shape -Item $item
            break
        }
        default {
            throw "Unsupported native chart operation type: $type"
        }
    }
}

if (-not (Test-Path -LiteralPath $TemplatePath)) {
    throw "TemplatePath not found: $TemplatePath"
}

$templateResolved = [System.IO.Path]::GetFullPath($TemplatePath)
$outputResolved = [System.IO.Path]::GetFullPath($OutputPath)
$isInPlaceUpdate = $templateResolved.Equals($outputResolved, [System.StringComparison]::OrdinalIgnoreCase)

$ops = [pscustomobject]@{
    replace_text = @()
    replace_tables = @()
    replace_charts = @()
    replace_images = @()
    replace_native_charts = @()
}

if ($OperationsPath) {
    if (-not (Test-Path -LiteralPath $OperationsPath)) {
        throw "OperationsPath not found: $OperationsPath"
    }
    $ops = Get-Content -LiteralPath $OperationsPath -Encoding UTF8 | ConvertFrom-Json
}

$replaceText = @()
$replaceTables = @()
$replaceCharts = @()
$replaceImages = @()
$replaceNativeCharts = @()
if ($ops.PSObject.Properties.Name -contains "replace_text") {
    $replaceText = @($ops.replace_text)
}
if ($ops.PSObject.Properties.Name -contains "replace_tables") {
    $replaceTables = @($ops.replace_tables)
}
if ($ops.PSObject.Properties.Name -contains "replace_charts") {
    $replaceCharts = @($ops.replace_charts)
}
if ($ops.PSObject.Properties.Name -contains "replace_images") {
    $replaceImages = @($ops.replace_images)
}
if ($ops.PSObject.Properties.Name -contains "replace_native_charts") {
    $replaceNativeCharts = @($ops.replace_native_charts)
}

$outDir = Split-Path -Parent $OutputPath
if ($outDir -and -not (Test-Path -LiteralPath $outDir)) {
    New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}

if (-not $isInPlaceUpdate) {
    Copy-Item -LiteralPath $TemplatePath -Destination $OutputPath -Force
}

if (
    $replaceText.Count -eq 0 -and
    $replaceTables.Count -eq 0 -and
    $replaceCharts.Count -eq 0 -and
    $replaceImages.Count -eq 0 -and
    $replaceNativeCharts.Count -eq 0
) {
    Write-Output $OutputPath
    return
}

$ppt = $null
$pres = $null
$openResult = $null
try {
    $ppt = New-PowerPointAppWithRetry
    $openResult = Open-PresentationWithFallback -PptApp $ppt -OutputPath $OutputPath -TemplatePath $TemplatePath
    $pres = $openResult.Presentation

    foreach ($item in $replaceText) {
        $slide = $pres.Slides.Item([int]$item.slide)
        $shape = Find-Shape -Slide $slide -ShapeId $item.shape_id -ShapeName $item.shape_name -ExactText $item.exact_text -ContainsText $item.contains_text
        if ($null -eq $shape) {
            throw "Text target not found on slide $($item.slide)"
        }
        Set-ShapeText -Shape $shape -NewText ([string]$item.new_text)
    }

    foreach ($item in $replaceImages) {
        $slide = $pres.Slides.Item([int]$item.slide)
        if ((-not $item.shape_id) -and $item.target_box) {
            Add-PictureAtBox -Slide $slide -Item $item
            continue
        }
        $shape = Find-Shape -Slide $slide -ShapeId $item.shape_id -ShapeName $item.shape_name -ExactText $item.exact_text -ContainsText $item.contains_text
        if ($null -eq $shape) {
            throw "Image target not found on slide $($item.slide)"
        }
        Replace-PictureShape -Slide $slide -Shape $shape -ImagePath ([string]$item.image_path)
    }

    foreach ($item in $replaceTables) {
        $slide = $pres.Slides.Item([int]$item.slide)
        $shape = Find-Shape -Slide $slide -ShapeId $item.shape_id -ShapeName $item.shape_name -ExactText $item.exact_text -ContainsText $item.contains_text
        if ($null -eq $shape) {
            throw "Table target not found on slide $($item.slide)"
        }
        if ($shape.HasTable -ne -1) {
            throw "Shape '$($shape.Name)' is not a table."
        }
        foreach ($cell in @($item.cells)) {
            $row = [int]$cell.row
            $col = [int]$cell.col
            $newText = [string]$cell.new_text
            $shape.Table.Cell($row, $col).Shape.TextFrame.TextRange.Text = $newText
        }
    }

    foreach ($item in $replaceCharts) {
        $slide = $pres.Slides.Item([int]$item.slide)
        $shape = Find-Shape -Slide $slide -ShapeId $item.shape_id -ShapeName $item.shape_name -ExactText $item.exact_text -ContainsText $item.contains_text
        if ($null -eq $shape) {
            throw "Chart target not found on slide $($item.slide)"
        }
        if ($shape.HasChart -ne -1) {
            throw "Shape '$($shape.Name)' is not a chart."
        }

        $chart = $shape.Chart
        foreach ($series in @($item.series)) {
            $index = [int]$series.index
            $chartSeries = $chart.SeriesCollection($index)
            if ($series.PSObject.Properties.Name -contains "name") {
                try { $chartSeries.Name = [string]$series.name } catch {}
            }
            if ($series.PSObject.Properties.Name -contains "x_values") {
                try { $chartSeries.XValues = @($series.x_values) } catch {}
            }
            if ($series.PSObject.Properties.Name -contains "values") {
                try { $chartSeries.Values = @($series.values) } catch {}
            }
        }
        try { $chart.Refresh() | Out-Null } catch {}
    }

    foreach ($item in $replaceNativeCharts) {
        Apply-NativeChartOperation -Presentation $pres -Item $item
    }

    if ($openResult -and $openResult.UsingTempCopy) {
        if (Test-Path -LiteralPath $OutputPath) {
            Remove-Item -LiteralPath $OutputPath -Force
        }
        # 24 = ppSaveAsOpenXMLPresentation (.pptx)
        $pres.SaveAs($OutputPath, 24)
    }
    elseif ($openResult -and (-not $openResult.OpenPath.Equals($OutputPath, [System.StringComparison]::OrdinalIgnoreCase))) {
        $pres.SaveAs($OutputPath, 24)
    }
    else {
        $pres.Save()
    }
    Write-Output $OutputPath
}
finally {
    if ($pres -ne $null) {
        try { $pres.Close() | Out-Null } catch {}
    }
    if ($ppt -ne $null) {
        try { $ppt.Quit() | Out-Null } catch {}
    }
    if ($openResult -and $openResult.UsingTempCopy -and $openResult.TempPath) {
        try {
            if (Test-Path -LiteralPath $openResult.TempPath) {
                Remove-Item -LiteralPath $openResult.TempPath -Force
            }
        }
        catch {
        }
    }
}

