param(
    [string]$ConfigPath = "",
    [Parameter(Mandatory = $true)]
    [string]$ReportMonth,
    [Parameter(Mandatory = $true)]
    [string]$Slides,
    [int]$ReportYear = 0,
    [int]$CompareYear = 0,
    [ValidateSet("single", "cumulative")]
    [string]$MonthlySalesMode = "single",
    [int]$Retries = 3,
    [int]$RetryDelaySeconds = 8,
    [string]$TemplatePath = "",
    [string]$OutputDir = "",
    [string]$OutputPath = "",
    [switch]$NoRender
)

$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$commandStartedAt = Get-Date

if (-not $ConfigPath) {
    $ConfigPath = Join-Path $root "config.yaml"
}
if (-not $TemplatePath) {
    $TemplatePath = Join-Path $root "Report.pptx"
}
if (-not $OutputPath) {
    $OutputPath = $TemplatePath
}

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

function Resolve-ConfiguredWorkdir {
    param([string]$ConfigPathValue)

    $text = Get-Content -LiteralPath $ConfigPathValue -Encoding UTF8 -Raw
    $match = [regex]::Match($text, "(?m)^\s{2}workdir:\s*[""']?(?<path>[^""'\r\n]+)[""']?\s*$")
    if ($match.Success) {
        return [System.IO.Path]::GetFullPath($match.Groups["path"].Value.Trim())
    }
    return Join-Path $root "output"
}

$outputDir = if ($OutputDir) { [System.IO.Path]::GetFullPath($OutputDir) } else { Resolve-ConfiguredWorkdir -ConfigPathValue $ConfigPath }
$mergedOpsPath = Join-Path $outputDir "render-ops.tableau-selection.live.json"

if (-not (Test-Path -LiteralPath $TemplatePath) -and -not $NoRender.IsPresent) {
    throw "TemplatePath not found: $TemplatePath"
}
if (-not (Test-Path -LiteralPath $outputDir)) {
    New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
}

function Resolve-PythonCommand {
    $venvPython = Join-Path $root ".venv\Scripts\python.exe"
    if (Test-Path -LiteralPath $venvPython) {
        return ,$venvPython
    }
    if (Get-Command py -ErrorAction SilentlyContinue) {
        return @("py", "-3")
    }
    if (Get-Command python -ErrorAction SilentlyContinue) {
        return @("python")
    }
    throw "Python launcher not found. Please install py or python."
}

$pythonCommand = @(Resolve-PythonCommand)

function Format-Elapsed {
    param([TimeSpan]$Duration)

    return "{0:N1}s" -f [Math]::Round($Duration.TotalSeconds, 1)
}

function Write-StageLog {
    param(
        [string]$Scope,
        [string]$Message
    )

    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    Write-Host ("[{0}] [{1}] {2}" -f $timestamp, $Scope, $Message)
}

function Get-SafeSessionToken {
    param([string]$Value)

    $token = [System.IO.Path]::GetFileName($Value)
    if (-not $token) {
        $token = "output"
    }
    $token = ($token -replace "[^A-Za-z0-9_-]", "-").Trim("-")
    if (-not $token) {
        $token = "output"
    }
    return $token
}

function Invoke-PythonScriptWithRetry {
    param(
        [string]$ScriptPath,
        [string[]]$ScriptArgs,
        [string]$StageName = "",
        [string[]]$SlideCodes = @(),
        [int]$MaxAttempts = 1,
        [int]$SleepSeconds = 8
    )
    $stageLabel = if ($StageName) { $StageName } else { [System.IO.Path]::GetFileNameWithoutExtension($ScriptPath) }
    $slideLabel = if ($SlideCodes -and $SlideCodes.Count -gt 0) { $SlideCodes -join "," } else { "-" }
    $stageStartedAt = Get-Date
    Write-StageLog -Scope "stage" -Message ("start {0} slides={1}" -f $stageLabel, $slideLabel)
    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        $attemptStartedAt = Get-Date
        Write-StageLog -Scope "stage" -Message ("run {0} attempt={1}/{2} slides={3}" -f $stageLabel, $attempt, $MaxAttempts, $slideLabel)
        if ($pythonCommand.Count -eq 2) {
            & $pythonCommand[0] $pythonCommand[1] $ScriptPath @ScriptArgs
        } else {
            & $pythonCommand[0] $ScriptPath @ScriptArgs
        }

        if ($LASTEXITCODE -eq 0) {
            $attemptElapsed = (Get-Date) - $attemptStartedAt
            $stageElapsed = (Get-Date) - $stageStartedAt
            Write-StageLog -Scope "timing" -Message ("attempt {0} {1}: {2}" -f $attempt, $stageLabel, (Format-Elapsed -Duration $attemptElapsed))
            Write-StageLog -Scope "timing" -Message ("stage {0} slides={1}: {2}" -f $stageLabel, $slideLabel, (Format-Elapsed -Duration $stageElapsed))
            return
        }

        if ($attempt -lt $MaxAttempts) {
            $attemptElapsed = (Get-Date) - $attemptStartedAt
            Write-StageLog -Scope "timing" -Message ("attempt {0} {1} failed after {2}" -f $attempt, $stageLabel, (Format-Elapsed -Duration $attemptElapsed))
            Write-StageLog -Scope "stage" -Message ("retry {0} after {1}s" -f $stageLabel, $SleepSeconds)
            Start-Sleep -Seconds $SleepSeconds
        }
    }

    throw "Script failed after $MaxAttempts attempts: $ScriptPath"
}

function Normalize-SlideCode {
    param([string]$SlideRaw)
    $text = $SlideRaw.Trim().ToUpper()
    if (-not $text) {
        return $null
    }

    if ($text -match "^S?(\d{1,2})$") {
        $number = [int]$Matches[1]
        if ($number -lt 1 -or $number -gt 99) {
            throw "Invalid slide number: $SlideRaw"
        }
        return ("S{0:d2}" -f $number)
    }

    throw "Invalid slide token: $SlideRaw"
}

function Get-CodePointKey {
    param([string]$Value)

    return (($Value.ToCharArray() | ForEach-Object { "{0:X4}" -f [int][char]$_ }) -join "-")
}

function Normalize-ReportMonthArgument {
    param([string]$RawMonth)

    $text = $RawMonth.Trim()
    if (-not $text) {
        throw "ReportMonth is required."
    }

    if ($text -match "^0?([1-9]|1[0-2])$") {
        return ([string]([int]$Matches[1]))
    }

    $monthCodeMap = @{
        "4E00-6708" = "1"
        "4E8C-6708" = "2"
        "4E09-6708" = "3"
        "56DB-6708" = "4"
        "4E94-6708" = "5"
        "516D-6708" = "6"
        "4E03-6708" = "7"
        "516B-6708" = "8"
        "4E5D-6708" = "9"
        "5341-6708" = "10"
        "5341-4E00-6708" = "11"
        "5341-4E8C-6708" = "12"
    }

    $codePointKey = Get-CodePointKey -Value $text
    if ($monthCodeMap.ContainsKey($codePointKey)) {
        return $monthCodeMap[$codePointKey]
    }

    throw "Invalid ReportMonth: $RawMonth. Use month labels or 1..12."
}

function Resolve-GroupSlides {
    param(
        [string[]]$RequestedSlides,
        [string[]]$GroupSlides
    )
    $resolved = @()
    foreach ($slide in $GroupSlides) {
        if ($RequestedSlides -contains $slide) {
            $resolved += $slide
        }
    }
    return $resolved
}

function Merge-Operations {
    param(
        [string[]]$OpsPaths,
        [string]$OutputOpsPath
    )
    $merged = @{
        replace_text = @()
        replace_tables = @()
        replace_charts = @()
        replace_images = @()
    }

    foreach ($opsPath in $OpsPaths) {
        if (-not (Test-Path -LiteralPath $opsPath)) {
            continue
        }
        $ops = Get-Content -LiteralPath $opsPath -Encoding UTF8 -Raw | ConvertFrom-Json
        if ($ops.PSObject.Properties.Name -contains "replace_text") {
            $merged.replace_text += @($ops.replace_text)
        }
        if ($ops.PSObject.Properties.Name -contains "replace_tables") {
            $merged.replace_tables += @($ops.replace_tables)
        }
        if ($ops.PSObject.Properties.Name -contains "replace_charts") {
            $merged.replace_charts += @($ops.replace_charts)
        }
        if ($ops.PSObject.Properties.Name -contains "replace_images") {
            $merged.replace_images += @($ops.replace_images)
        }
    }

    $json = $merged | ConvertTo-Json -Depth 100
    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($OutputOpsPath, $json, $utf8NoBom)
}

$reportMonthCliValue = Normalize-ReportMonthArgument -RawMonth $ReportMonth
$normalizedSlides = @()
$seenSlides = @{}
foreach ($raw in ($Slides -split ",")) {
    $slideCode = Normalize-SlideCode -SlideRaw $raw
    if ($slideCode -and -not $seenSlides.ContainsKey($slideCode)) {
        $normalizedSlides += $slideCode
        $seenSlides[$slideCode] = $true
    }
}

if (-not $normalizedSlides) {
    throw "No slides provided."
}

# Supported slide whitelist (including S05 Bags).
$allowedSlides = @("S02", "S03", "S04", "S05", "S06", "S07", "S08", "S09", "S10", "S11", "S13")
$invalidSlides = @($normalizedSlides | Where-Object { $allowedSlides -notcontains $_ })
if ($invalidSlides.Count -gt 0) {
    throw "Unsupported slides: $($invalidSlides -join ', '). Allowed: $($allowedSlides -join ', ')"
}

$monthlySlides = Resolve-GroupSlides -RequestedSlides $normalizedSlides -GroupSlides @("S02", "S03")
# S05 belongs to inventory group and uses sync_inventory_monthly_assets.py.
$inventorySlides = Resolve-GroupSlides -RequestedSlides $normalizedSlides -GroupSlides @("S04", "S05", "S06", "S07", "S08")
$topSlides = Resolve-GroupSlides -RequestedSlides $normalizedSlides -GroupSlides @("S09", "S10")
$campaignSlides = Resolve-GroupSlides -RequestedSlides $normalizedSlides -GroupSlides @("S11")
$warehouseSlides = Resolve-GroupSlides -RequestedSlides $normalizedSlides -GroupSlides @("S13")
$sharedSessionToken = Get-SafeSessionToken -Value $outputDir
$sharedPlaywrightSession = "vip-report-tableau-$sharedSessionToken"
$sharedTableauStatePath = Join-Path $outputDir ".playwright-cli\tableau-shared-state.json"
$sharedBrowserConfigPath = Join-Path $root ".playwright\cli.config.json"
$sharedTableauArgs = @(
    "--playwright-session", $sharedPlaywrightSession,
    "--tableau-state-path", $sharedTableauStatePath
)
if (Test-Path -LiteralPath $sharedBrowserConfigPath) {
    $sharedTableauArgs += @("--browser-config-path", $sharedBrowserConfigPath)
}

$opsPaths = @()
Push-Location $root
try {
    if ($monthlySlides.Count -gt 0) {
        $args = @(
            "--config", $ConfigPath,
            "--slides", ($monthlySlides -join ","),
            "--report-month", $reportMonthCliValue
        )
        if ($ReportYear -gt 0) {
            $args += @("--report-year", "$ReportYear")
        }
        if ($CompareYear -gt 0) {
            $args += @("--compare-year", "$CompareYear")
        }
        if ($MonthlySalesMode -eq "single") {
            $args += "--single-month"
        }
        $args += @("--output-dir", $outputDir)
        $args += $sharedTableauArgs
        Invoke-PythonScriptWithRetry -ScriptPath (Join-Path $root "scripts\sync_monthly_sales_assets.py") -ScriptArgs $args -StageName "monthly-sales" -SlideCodes $monthlySlides -MaxAttempts $Retries -SleepSeconds $RetryDelaySeconds
        $opsPaths += Join-Path $outputDir "render-ops.monthly-sales.live.json"
    }

    if ($inventorySlides.Count -gt 0) {
        $args = @(
            "--config", $ConfigPath,
            "--slides", ($inventorySlides -join ","),
            "--report-month", $reportMonthCliValue
        )
        if ($ReportYear -gt 0) {
            $args += @("--report-year", "$ReportYear")
        }
        if ($CompareYear -gt 0) {
            $args += @("--compare-year", "$CompareYear")
        }
        $args += @("--output-dir", $outputDir)
        $args += $sharedTableauArgs
        $inventorySlideList = @($inventorySlides)
        $inventoryRetries = if ($inventorySlideList.Count -eq 1) { 1 } else { $Retries }
        Invoke-PythonScriptWithRetry -ScriptPath (Join-Path $root "scripts\sync_inventory_monthly_assets.py") -ScriptArgs $args -StageName "inventory-monthly" -SlideCodes $inventorySlides -MaxAttempts $inventoryRetries -SleepSeconds $RetryDelaySeconds
        $opsPaths += Join-Path $outputDir "render-ops.inventory-monthly.live.json"
    }

    if ($topSlides.Count -gt 0) {
        $args = @(
            "--config", $ConfigPath,
            "--slides", ($topSlides -join ","),
            "--report-month", $reportMonthCliValue
        )
        if ($ReportYear -gt 0) {
            $args += @("--report-year", "$ReportYear")
        }
        if ($CompareYear -gt 0) {
            $args += @("--compare-year", "$CompareYear")
        }
        $args += @("--output-dir", $outputDir)
        $args += $sharedTableauArgs
        Invoke-PythonScriptWithRetry -ScriptPath (Join-Path $root "scripts\sync_top_products_assets.py") -ScriptArgs $args -StageName "top-products" -SlideCodes $topSlides -MaxAttempts $Retries -SleepSeconds $RetryDelaySeconds
        $opsPaths += Join-Path $outputDir "render-ops.top-products.live.json"
    }

    if ($campaignSlides.Count -gt 0) {
        $args = @(
            "--config", $ConfigPath,
            "--slides", ($campaignSlides -join ","),
            "--report-month", $reportMonthCliValue
        )
        if ($ReportYear -gt 0) {
            $args += @("--report-year", "$ReportYear")
        }
        if ($CompareYear -gt 0) {
            $args += @("--compare-year", "$CompareYear")
        }
        $args += @("--output-dir", $outputDir)
        Invoke-PythonScriptWithRetry -ScriptPath (Join-Path $root "scripts\sync_campaign_s11_assets.py") -ScriptArgs $args -StageName "campaign-s11" -SlideCodes $campaignSlides -MaxAttempts 1 -SleepSeconds $RetryDelaySeconds
        $opsPaths += Join-Path $outputDir "render-ops.campaign-s11.live.json"
    }

    if ($warehouseSlides.Count -gt 0) {
        $args = @(
            "--config", $ConfigPath,
            "--slides", ($warehouseSlides -join ","),
            "--report-month", $reportMonthCliValue
        )
        if ($ReportYear -gt 0) {
            $args += @("--report-year", "$ReportYear")
        }
        if ($CompareYear -gt 0) {
            $args += @("--compare-year", "$CompareYear")
        }
        $args += @("--output-dir", $outputDir)
        $args += $sharedTableauArgs
        Invoke-PythonScriptWithRetry -ScriptPath (Join-Path $root "scripts\sync_warehouse_100060_assets.py") -ScriptArgs $args -StageName "warehouse-100060" -SlideCodes $warehouseSlides -MaxAttempts $Retries -SleepSeconds $RetryDelaySeconds
        $opsPaths += Join-Path $outputDir "render-ops.warehouse-100060.live.json"
    }
}
finally {
    Pop-Location
}

if ($opsPaths.Count -eq 0) {
    throw "No operations generated for slides: $($normalizedSlides -join ',')"
}

Merge-Operations -OpsPaths $opsPaths -OutputOpsPath $mergedOpsPath
Write-StageLog -Scope "stage" -Message ("merged operations -> {0}" -f $mergedOpsPath)

if ($NoRender.IsPresent) {
    $totalElapsed = (Get-Date) - $commandStartedAt
    Write-StageLog -Scope "timing" -Message ("total tableau sync: {0}" -f (Format-Elapsed -Duration $totalElapsed))
    Write-Output $mergedOpsPath
    exit 0
}

$renderStartedAt = Get-Date
Write-StageLog -Scope "stage" -Message ("start render output={0}" -f $OutputPath)
powershell -ExecutionPolicy Bypass -File (Join-Path $root "bin\vip-report-render.ps1") -TemplatePath $TemplatePath -OutputPath $OutputPath -OperationsPath $mergedOpsPath | Out-Null
if ($LASTEXITCODE -ne 0) {
    throw "vip-report-render.ps1 failed with exit code $LASTEXITCODE"
}
$renderElapsed = (Get-Date) - $renderStartedAt
Write-StageLog -Scope "timing" -Message ("stage render: {0}" -f (Format-Elapsed -Duration $renderElapsed))
$totalElapsed = (Get-Date) - $commandStartedAt
Write-StageLog -Scope "timing" -Message ("total tableau sync: {0}" -f (Format-Elapsed -Duration $totalElapsed))
Write-Output $OutputPath
