#requires -Version 5.1
<#
.SYNOPSIS
    Recursively convert supported documents to markdown via marker-api.
.DESCRIPTION
    Scans a target folder for PDFs, DOCX, XLSX, PPTX, EPUB, images, and HTML.
    Sends each file to the marker-api /marker endpoint (multipart/form-data)
    and writes the resulting .md file beside the original.
.EXAMPLE
    .\marker-convert.ps1 -TargetFolder .\documents
    .\marker-convert.ps1 .\documents -Force -MaxConcurrency 8
    .\marker-convert.ps1 .\documents -ApiUrl http://10.0.0.5:8000/marker
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true, Position = 0)]
    [string]$TargetFolder,

    [Uri]$ApiUrl = "http://localhost:8000/marker",

    [ValidateSet("markdown", "json", "html", "chunks")]
    [string]$OutputFormat = "markdown",

    [switch]$Force,

    [ValidateRange(1, 32)]
    [int]$MaxConcurrency = 4,

    [ValidateRange(30, 1800)]
    [int]$Timeout = 300,

    [string]$PageRange = "",

    [switch]$ForceOcr,

    [switch]$DisableImageExtraction,

    [switch]$UseLlm,

    [string]$LlmService = "marker.services.ollama.OllamaService",

    [string]$Processors = "",

    [string]$ConfigJson = "",

    [string]$ConverterCls = ""
)

$ErrorActionPreference = "Stop"

# ════════════ 1. Validate ════════════
$TargetFolder = (Resolve-Path $TargetFolder).Path
if (-not (Test-Path $TargetFolder)) {
    Write-Error "Target folder does not exist: $TargetFolder"
    exit 1
}

Write-Host "====================================================" -ForegroundColor Cyan
Write-Host "  Force overwrite: $(if ($Force) { 'Yes' } else { 'No' })" -ForegroundColor White
Write-Host "  Force OCR      : $(if ($ForceOcr) { 'Yes' } else { 'No' })" -ForegroundColor White
Write-Host "  Use LLM        : $(if ($UseLlm) { 'Yes' } else { 'No' })" -ForegroundColor White
Write-Host "  LLM Service    : $LlmService" -ForegroundColor White
Write-Host "  Disable ImgExt : $(if ($DisableImageExtraction) { 'Yes' } else { 'No' })" -ForegroundColor White
if ($PageRange) { Write-Host "  Page range     : $PageRange" -ForegroundColor White }
if ($Processors) { Write-Host "  Processors     : $Processors" -ForegroundColor White }
if ($ConverterCls) { Write-Host "  Converter      : $ConverterCls" -ForegroundColor White }
Write-Host "====================================================" -ForegroundColor Cyan
Write-Host ""

# ════════════ 2. Discover files ════════════
$supportedExtensions = @('.pdf', '.docx', '.xlsx', '.pptx', '.epub',
                         '.png', '.jpg', '.jpeg', '.bmp', '.gif',
                         '.tiff', '.tif', '.webp', '.heic', '.html', '.htm')

$files = Get-ChildItem -Path $TargetFolder -Recurse -File |
    Where-Object { $_.Extension -in $supportedExtensions } |
    Sort-Object FullName

if ($files.Count -eq 0) {
    Write-Host "No supported files found in $TargetFolder." -ForegroundColor Yellow
    exit 0
}

Write-Host "Found $($files.Count) document(s) to convert." -ForegroundColor Green
Write-Host ""

# ════════════ 3. Worker function ════════════
# This script block runs inside Start-Job for each file.
$ScriptBlock = {
    param(
        [Parameter(Mandatory=$true)]
        [string]$FilePath,

        [Parameter(Mandatory=$true)]
        [Uri]$ApiUrl,

        [Parameter(Mandatory=$true)]
        [string]$Ofmt,

        [Parameter(Mandatory=$true)]
        [bool]$Force,

        [Parameter(Mandatory=$true)]
        [int]$Timeout,

        [string]$PageRange,

        [bool]$ForceOcr,

        [bool]$DisableImageExtraction,

        [bool]$UseLlm,

        [string]$LlmService,

        [string]$Processors,

        [string]$ConfigJson,

        [string]$ConverterCls
    )

    # Build output path (same directory, same base name with .md extension)
    $baseName = [System.IO.Path]::GetFileNameWithoutExtension($FilePath)
    $dirPath  = Split-Path $FilePath
    $mdPath   = Join-Path $dirPath "$baseName.md"

    $result = [ordered]@{}
    $result.File       = (Split-Path $FilePath -Leaf)
    $result.SizeBytes  = (Get-Item $FilePath).Length
    $result.Status     = "pending"
    $result.ElapsedMs  = 0
    $result.Error      = ""
    $result.OutputFile = ""

    # Skip if output already exists and -Force was not used
    if ((Test-Path $mdPath) -and ($Force -eq $false)) {
        $result.Status = "skipped"
        $result.OutputFile = $mdPath
        return $result
    }

    $sw = [System.Diagnostics.Stopwatch]::StartNew()

    try {
        $bin = [System.IO.File]::ReadAllBytes($FilePath)
        $name = [System.IO.Path]::GetFileName($FilePath)
        $boundary = "----marker-convert-$(Get-Random -Minimum 100000 -Maximum 999999)"
        $enc = [System.Text.Encoding]::UTF8

        # ── Build multipart body ──
        $contentParts = [System.Collections.ArrayList]::new()

        # Part 1: file
        $fileHeader = $enc.GetBytes(
            "--$boundary`r`n" +
            "Content-Disposition: form-data; name=`"file`"; filename=`"$name`"`r`n" +
            "Content-Type: application/octet-stream`r`n`r`n"
        )
        $contentParts.Add($fileHeader) | Out-Null
        $contentParts.Add($bin) | Out-Null
        $contentParts.Add($enc.GetBytes("`r`n")) | Out-Null

        # Part 2: output_format
        $fmtPart = $enc.GetBytes(
            "--$boundary`r`n" +
            "Content-Disposition: form-data; name=`"output_format`"`r`n`r`n" +
            "$Ofmt`r`n"
        )
        $contentParts.Add($fmtPart) | Out-Null

        # Part 3: force_ocr
        $ocrPart = $enc.GetBytes(
            "--$boundary`r`n" +
            "Content-Disposition: form-data; name=`"force_ocr`"`r`n`r`n" +
            "$(if ($ForceOcr) { 'true' } else { 'false' })`r`n"
        )
        $contentParts.Add($ocrPart) | Out-Null

        # Part 4: paginate_output (default false)
        $pagPart = $enc.GetBytes(
            "--$boundary`r`n" +
            "Content-Disposition: form-data; name=`"paginate_output`"`r`n`r`n" +
            "false`r`n"
        )
        $contentParts.Add($pagPart) | Out-Null

        # Part 5: use_llm
        $llmPart = $enc.GetBytes(
            "--$boundary`r`n" +
            "Content-Disposition: form-data; name=`"use_llm`"`r`n`r`n" +
            "$(if ($UseLlm) { 'true' } else { 'false' })`r`n"
        )
        $contentParts.Add($llmPart) | Out-Null

        # Part 6a: llm_service (only if UseLlm or custom service)
        if ($UseLlm -or $LlmService -ne "marker.services.ollama.OllamaService") {
            $svcPart = $enc.GetBytes(
                "--$boundary`r`n" +
                "Content-Disposition: form-data; name=`"llm_service`"`r`n`r`n" +
                "$LlmService`r`n"
            )
            $contentParts.Add($svcPart) | Out-Null
        }

        # Part 6b: page_range
        if ($PageRange) {
            $prPart = $enc.GetBytes(
                "--$boundary`r`n" +
                "Content-Disposition: form-data; name=`"page_range`"`r`n`r`n" +
                "$PageRange`r`n"
            )
            $contentParts.Add($prPart) | Out-Null
        }

        # Part 6c: disable_image_extraction
        if ($DisableImageExtraction) {
            $diePart = $enc.GetBytes(
                "--$boundary`r`n" +
                "Content-Disposition: form-data; name=`"disable_image_extraction`"`r`n`r`n" +
                "true`r`n"
            )
            $contentParts.Add($diePart) | Out-Null
        }

        # Part 6d: processors
        if ($Processors) {
            $prcPart = $enc.GetBytes(
                "--$boundary`r`n" +
                "Content-Disposition: form-data; name=`"processors`"`r`n`r`n" +
                "$Processors`r`n"
            )
            $contentParts.Add($prcPart) | Out-Null
        }

        # Part 6e: config_json
        if ($ConfigJson) {
            $cfgPart = $enc.GetBytes(
                "--$boundary`r`n" +
                "Content-Disposition: form-data; name=`"config_json`"`r`n`r`n" +
                "$ConfigJson`r`n"
            )
            $contentParts.Add($cfgPart) | Out-Null
        }

        # Part 6f: converter_cls
        if ($ConverterCls) {
            $ccPart = $enc.GetBytes(
                "--$boundary`r`n" +
                "Content-Disposition: form-data; name=`"converter_cls`"`r`n`r`n" +
                "$ConverterCls`r`n"
            )
            $contentParts.Add($ccPart) | Out-Null
        }

        # Closing boundary
        $closing = $enc.GetBytes("--$boundary--`r`n")

        # Calculate total content-length
        $contentLength = 0
        foreach ($p in $contentParts) {
            $contentLength += $p.Length
        }
        $contentLength += $closing.Length

        # ── Create HTTP request ──
        $request = [System.Net.HttpWebRequest]::Create($ApiUrl)
        $request.Method              = "POST"
        $request.ContentType         = "multipart/form-data; boundary=$boundary"
        $request.ContentLength       = $contentLength
        $request.Timeout             = $Timeout * 1000
        $request.KeepAlive           = $true
        $request.ProtocolVersion     = [System.Net.HttpVersion]::Version11

        $reqStream = $request.GetRequestStream()

        # Write content parts
        foreach ($p in $contentParts) {
            $reqStream.Write($p, 0, $p.Length)
        }
        # Write closing boundary
        $reqStream.Write($closing, 0, $closing.Length)
        $reqStream.Flush()
        $reqStream.Close()

        # ── Get response ──
        $response = $request.GetResponse()
        $resStream = $response.GetResponseStream()
        $reader = [System.IO.StreamReader]::new($resStream, $enc)
        $content = $reader.ReadToEnd()
        $reader.Close()
        $resStream.Close()
        $response.Close()

        # ── Write output file ──
        [System.IO.File]::WriteAllText($mdPath, $content, $enc)

        $sw.Stop()
        $result.Status   = "success"
        $result.ElapsedMs = $sw.ElapsedMilliseconds
        $result.OutputFile = $mdPath
        $result.ResponseSize = $content.Length

    } catch {
        $sw.Stop()
        $result.Status     = "failed"
        $result.ElapsedMs  = $sw.ElapsedMilliseconds
        $result.Error      = $_.Exception.Message
        if ($_.Exception.Response) {
            $result.Error += "`n  Status: $([int]$_.Exception.Response.StatusCode)"
        }
    }

    return $result
}

# ════════════ 4. Dispatch ════════════
Write-Host "Dispatching $($files.Count) conversion(s) with $($MaxConcurrency) worker(s)..." -ForegroundColor Yellow

$allResults = @()
$runningJobs = @()

for ($i = 0; $i -lt $files.Count; $i++) {
    # If we've reached concurrency limit, wait for at least one job to complete
    while ($runningJobs.Count -ge $MaxConcurrency) {
        $completed = $runningJobs | Where-Object { $_.State -in @('Completed','Failed') }
        if ($completed) {
            foreach ($job in $completed) {
                try {
                    $allResults += Receive-Job $job
                } catch {
                    $r = [ordered]@{}
                    $r.File       = "unknown"
                    $r.SizeBytes  = 0
                    $r.Status     = "failed"
                    $r.ElapsedMs  = 0
                    $r.Error      = $_.Exception.Message
                    $r.OutputFile = ""
                    $allResults += $r
                }
                $runningJobs = @($runningJobs | Where-Object { $_.Id -ne $job.Id })
                Remove-Job $job 2>&1 | Out-Null
            }
        } else {
            Start-Sleep -Milliseconds 200
        }
    }

    # Launch new job
    $f = $files[$i]
    $safeName = [System.Security.SecurityElement]::Escape($f.Name)
    $jobName = "mk_$safeName"

    $job = Start-Job `
        -Name $jobName `
        -ScriptBlock $ScriptBlock `
        -ArgumentList @(
            $f.FullName
            $ApiUrl
            $OutputFormat
            [bool]$Force
            $Timeout
            $PageRange
            [bool]$ForceOcr
            [bool]$DisableImageExtraction
            [bool]$UseLlm
            $LlmService
            $Processors
            $ConfigJson
            $ConverterCls
        )

    $runningJobs += $job
    Write-Host "  [$($i + 1)/$($files.Count)] Queued: $($f.Name)" -ForegroundColor Gray
}

# Wait for remaining jobs
while ($runningJobs.Count -gt 0) {
    $completed = $runningJobs | Where-Object { $_.State -in @('Completed','Failed') }
    if ($completed) {
        foreach ($job in $completed) {
            try {
                $allResults += Receive-Job $job
            } catch {
                $r = [ordered]@{}
                $r.File       = "unknown"
                $r.SizeBytes  = 0
                $r.Status     = "failed"
                $r.ElapsedMs  = 0
                $r.Error      = $_.Exception.Message
                $r.OutputFile = ""
                $allResults += $r
            }
            $runningJobs = @($runningJobs | Where-Object { $_.Id -ne $job.Id })
            Remove-Job $job 2>&1 | Out-Null
        }
    } else {
        Start-Sleep -Milliseconds 200
    }
}

# ════════════ 5. Report ════════════
$ok   = @($allResults | Where-Object { $_.Status -eq "success" })
$fail = @($allResults | Where-Object { $_.Status -eq "failed"   })
$skip = @($allResults | Where-Object { $_.Status -eq "skipped"  })

Write-Host ""
Write-Host "====== Conversion Results ======" -ForegroundColor Cyan
Write-Host "  Total files    : $($files.Count)" -ForegroundColor White
Write-Host "  Successful     : $($ok.Count)     " -ForegroundColor Green
Write-Host "  Failed         : $($fail.Count)    " -ForegroundColor Red
Write-Host "  Skipped        : $($skip.Count)    " -ForegroundColor Yellow

if ($ok.Count -gt 0) {
    $avgMs = [Math]::Round(($ok | Measure-Object -Property ElapsedMs -Sum).Sum / $ok.Count)
    Write-Host "  Avg time       : $avgMs ms" -ForegroundColor Green
}

if ($fail.Count -gt 0) {
    Write-Host ""
    Write-Host "Errors:" -ForegroundColor Red
    foreach ($r in $fail) {
        Write-Host "  [FAIL] $($r.File)" -ForegroundColor DarkRed
        Write-Host "         $($r.Error)" -ForegroundColor DarkGray
    }
}

# Save CSV report
$csvPath = Join-Path $TargetFolder "_marker_convert_results.csv"
$resultsForCsv = @($allResults | ForEach-Object {
    [PSCustomObject]@{
        FileName   = $_.File
        OutputFile = if ($_.OutputFile) { $_.OutputFile } else { "N/A" }
        Status     = $_.Status
        SizeBytes  = $_.SizeBytes
        ElapsedMs  = $_.ElapsedMs
        Error      = $_.Error
    }
})
$resultsForCsv | Sort-Object Status, File | Export-Csv $csvPath -NoTypeInformation -Encoding UTF8

Write-Host ""
Write-Host "CSV report: $csvPath" -ForegroundColor Magenta
Write-Host "====================================================" -ForegroundColor Cyan
