#requires -Modules powershell-yaml

param(
    [string]$StartDir = (Resolve-Path -LiteralPath (Join-Path -Path (Split-Path -LiteralPath $MyInvocation.MyCommand.Path) -ChildPath "../bruno")),
    [string]$OutputDir = (Resolve-Path -LiteralPath (Join-Path -Path (Split-Path -LiteralPath $MyInvocation.MyCommand.Path) -ChildPath ".."))
)

$DEFAULT_ENV_VAR_VALUE = 'EDIT_VALUE_HERE'
$VARIABLE_NAME_VALUE_SEPARATOR = '='

function Find-WorkspaceRoot($dir) {
    Write-Host "Processing workspace folder: '$dir'..."
    $current = $dir
    while ($true) {
        if (Test-Path -LiteralPath (Join-Path $current 'workspace.yml')) {
            return $current
        }
        $parent = Split-Path -LiteralPath $current
        if ($parent -eq $current) {
            throw "workspace.yml not found from the provided start directory"
        }
        $current = $parent
    }
}

function Parse-Yaml($text) {
    try {
        $parsedYaml = ConvertFrom-Yaml $text
        return $parsedYaml ?? @{}
    } catch {
        throw "Failed to parse YAML content : $_"
    }
}

function Parse-YamlFile($path) {
    return Parse-Yaml (Get-Content -LiteralPath $path -Raw)
}

function Strip-Quotes($value) {
    $trim = $value.Trim()
    if (($trim.StartsWith('"') -and $trim.EndsWith('"')) -or
        ($trim.StartsWith("'") -and $trim.EndsWith("'"))) {
        return $trim.Substring(1, $trim.Length - 2)
    }
    return $trim
}

function Parse-Workspace($workspacePath) {
    $lines = Get-Content -LiteralPath $workspacePath
    $collections = @()
    $inCollections = $false
    $current = $null

    foreach ($line in $lines) {
        $trim = $line.Trim()

        if (-not $inCollections -and $trim -eq 'collections:') {
            $inCollections = $true
            continue
        }

        if (-not $inCollections) { continue }

        if (-not ($line.StartsWith(' ') -or $line.StartsWith("`t"))) { # && trimmed?
            break
        }

        if ($line -match '^\s*-\s+name:\s*(.+)$') {
            $name = Strip-Quotes $Matches[1]
            $current = [ordered]@{ name = $name }
            $collections += $current
            continue
        }

        if ($line -match '^\s*path:\s*(.+)$' -and $current) {
            $current.path = Strip-Quotes $Matches[1]
        }
    }

    return @{ collections = $collections }
}

function Sanitize-VarName($name) {
    $sanitized = ([string]$name).Trim() -replace '[{}]', '' -replace '[^A-Za-z0-9_]', '_' -replace '^([0-9])', '_$1'
    if ([string]::IsNullOrWhiteSpace($sanitized)) {
        return 'value'
    }
    return $sanitized
}

function Parse-PlaceholderContent($content) {
    $trimmed = ([string]$content).Trim();
    $dotenvMatch = [regex]::Match($trimmed, '^\$dotenv\s+(.+)$', 'IgnoreCase')
    if ($dotenvMatch.Success) {
        return @{
            name = $dotenvMatch.Groups[1].value.Trim()
            isDotenv = $true
        }
    }
    return @{
        name = $trimmed
        isDotenv = $false
    }
}

function Collect-Placeholders($value) {
    # Se non è stringa → restituisci array vuoto
    if ($value -isnot [string]) {
        return @()
    }
    $placeholders = @()
    $regex = [regex]'\{\{([^{}]+)\}\}'
    $foundMatches = $regex.Matches($value)
    foreach ($m in $foundMatches) {
        $inner = $m.Groups[1].value
        $parsed = Parse-PlaceholderContent $inner
        $placeholders += $parsed.name
    }
    return $placeholders
}

function Find-Block ([string[]]$lines, $keyName) {
    for ($i = 0; $i -lt $lines.Count; $i++) {
        $trimmed = $lines[$i].Trim()
        if ($trimmed -ne $keyName -and -not $trimmed.StartsWith("$($keyName):")) {
            continue
        }

        $lineIndent = ([regex]::Match($lines[$i], '^\s*')).value.Length
        $block = @()
        for ($j = $i + 1; $j -lt $lines.Count; $j++) {
            $currentLine = $lines[$j]
            $currentTrimmed = $currentLine.Trim()
            if ([string]::IsNullOrWhiteSpace($currentTrimmed)) {
                $block += $currentLine
                continue
            }
            $currentIndent = ([regex]::Match($currentLine, '^\s*')).value.Length
            # Caso 1: indentazione <= indentazione della chiave e NON inizia con spazio → fine blocco
            if ($currentIndent -le $lineIndent -and -not $currentLine.StartsWith(' ')) {
                break
            }
            # Caso 2: indentazione <= indentazione della chiave e riga commento → includi
            if ($currentIndent -le $lineIndent -and $currentTrimmed.StartsWith('#')) {
                $block += $currentLine
                continue
            }
            # Caso 3: indentazione <= indentazione della chiave → fine blocco
            if ($currentIndent -le $lineIndent) {
                break
            }
            # Altrimenti la riga fa parte del blocco
            $block += $currentLine
        }
        return $block
    }
    return @()
}

function Parse-RequestInfo ($text) {
    $lines = $text -split '\r?\n'
    $infoLines = Find-Block -Lines $lines -KeyName 'info'
    $joined = ($infoLines -join "`n")
    $nameMatch = [regex]::Match($joined, '^\s*name:\s*(.+)$', 'Multiline')
    if ($nameMatch.Success) {
        return Strip-Quotes $nameMatch.Groups[1].value
    }
    return ''
}

function Parse-HttpBlock ($text) {
    # ConvertFrom-Yaml restituisce $null se il testo è vuoto o non valido
    $parsed = Parse-Yaml $text
    if (-not $parsed) { $parsed = @{} }

    $http = $parsed.http
    if (-not $http) { $http = @{} }

    $headers = @()
    $headerList = @()
    if ($http.headers -is [System.Collections.IEnumerable]) {
        $headerList = $http.headers
    }
    foreach ($header in $headerList) {
        if (-not $header -or $header -isnot [psobject] -and $header -isnot [hashtable]) {
            continue
        }
        $name  = ([string]($header.name  ?? '')).Trim()
        $value = ([string]($header.value ?? '')).Trim()
        $headers += @{
            name  = $name
            value = $value
        }
    }

    $params = @()
    if ($http.params -is [System.Collections.IEnumerable]) {
        $params = $http.params
    }

    $body = $null
    if ($http.body -and ($http.body -is [psobject] -or $http.body -is [hashtable])) {
        $body = $http.body
    }

    $preRequestScript = $parsed.runtime?.scripts | Where-Object { $_.type -eq 'before-request' } | Select-Object -First 1 -ExpandProperty 'code'
    # $preRequestScript = $null
    # if ($parsed.runtime -and ($parsed.runtime -is [psobject] -or $parsed.runtime -is [hashtable])) {
    # }

    $postResponseScript = $parsed.runtime?.scripts | Where-Object { $_.type -eq 'after-response' } | Select-Object -First 1 -ExpandProperty 'code'
    # $postResponseScript = $null
    # if ($parsed.runtime -and ($parsed.runtime -is [psobject] -or $parsed.runtime -is [hashtable])) {
    # }

    return @{
        method = $http.method ?? 'GET'
        url = $http.url    ?? ''
        params = $params
        headers = $headers
        body = $body
        preRequestScript = $preRequestScript
        postResponseScript = $postResponseScript
    }
}

function Format-VariableValue ($value, [hashtable]$RenderContext = @{}) {
    if ($null -eq $value) {
        return "''"
    }

    if ($value -is [string]) {
        if ($value.Trim() -eq '') {
            return "''"
        }

        $renderedValue = $value
        if ($RenderContext.ContainsKey('renderValue') -and $RenderContext.renderValue) {
            $renderedValue = $RenderContext.renderValue.Invoke($value)
        }
        if ($renderedValue -match '\s') {
            $escaped = $renderedValue -replace '"', '\"'
            return '"' + $escaped + '"'
        }
        return $renderedValue
    }

    return ($value | ConvertTo-Json -Depth 20 -Compress)
}

function Merge-RequestConfig ($base, $updates) {
    if (-not $updates -or ($updates -isnot [psobject] -and $updates -isnot [hashtable])) {
        return $base
    }

    # Clona base (shallow clone)
    $merged = @{}
    if ($base -is [psobject] -or $base -is [hashtable]) {
        foreach ($key in $base.Keys) {
            $merged[$key] = $base[$key]
        }
    }

    if ($updates.ContainsKey('auth')) {
        $auth = $updates.auth
        if ($auth -eq 'inherit') {
            if ($merged.ContainsKey('auth') -and ($merged.auth -is [psobject] -or $merged.auth -is [hashtable])) {
                $merged.auth = $merged.auth
            }
            else {
                $merged.Remove('auth')
            }
        }
        elseif ($auth -is [psobject] -or $auth -is [hashtable]) {
            $merged.auth = $auth
        }
        else {
            $merged.Remove('auth')
        }
    }
    else {
        $merged.Remove('auth')
    }

    if ($updates.headers -is [System.Collections.IEnumerable]) {
        $headers = @()
        if ($base.headers -is [System.Collections.IEnumerable]) {
            $headers = @($base.headers)
        }
        # Mappa per nome
        $byName = @{}
        foreach ($header in $headers) {
            if ($header -and $header.name) {
                $byName[[string]$header.name] = $header
            }
        }
        foreach ($header in $updates.headers) {
            if ($header -and $header.name) {
                $byName[[string]$header.name] = $header
            }
        }
        $merged.headers = $byName.Values
    }

    if ($updates.variables -is [System.Collections.IEnumerable]) {
        $variables = @()
        if ($base.variables -is [System.Collections.IEnumerable]) {
            $variables = @($base.variables)
        }
        # Mappa per nome
        $byName = @{}
        foreach ($variable in $variables) {
            if ($variable -and $variable.name) {
                $byName[[string]$variable.name] = $variable
            }
        }
        foreach ($variable in $updates.variables) {
            if ($variable -and $variable.name) {
                $byName[[string]$variable.name] = $variable
            }
        }
        $merged.variables = $byName.Values
    }

    return $merged
}

function Get-RequestConfigForFile ($yamlFile, $sourceDir) {
    $resolved = [ref] @()
    $seenFiles = [ref] (New-Object System.Collections.Generic.HashSet[string])

    $config = [ref] @{}

    function Add-File ($FilePath) {
        if (-not $FilePath -or $seenFiles.Value.Contains($FilePath)) {
            return
        }
        [void]$seenFiles.Value.Add($FilePath)
        if (-not (Test-Path $FilePath)) {
            return
        }

        try {
            $parsed = (Get-Content -Raw $FilePath | ConvertFrom-Yaml)
            if (-not $parsed) { $parsed = @{} }
            $requestConfig = $null

            # Caso 1: parsed.request esiste ed è un oggetto
            if ($parsed.request -and ($parsed.request -is [psobject] -or $parsed.request -is [hashtable])) {
                $requestConfig = $parsed.request
            }
            # Caso 2: parsed è un oggetto e contiene auth/variables
            elseif ($parsed -is [psobject] -or $parsed -is [hashtable]) {
                # $config = @{}
                # auth
                if ($parsed.ContainsKey('auth')) {
                    $config.Value.auth = $parsed.auth
                }
                elseif ($parsed.http -and ($parsed.http -is [psobject] -or $parsed.http -is [hashtable]) -and $parsed.http.ContainsKey('auth')) {
                    $config.Value.auth = $parsed.http.auth
                }
                # variables
                if ($parsed.variables -is [System.Collections.IEnumerable]) {
                    $config.Value.variables = $parsed.variables
                }
                if ($config.Value.Count -gt 0) {
                    $requestConfig = $config.Value
                }
            }

            if ($null -ne $requestConfig) {
                $resolved.Value += $requestConfig
            }
            elseif ((Resolve-Path $FilePath).Path -eq (Resolve-Path $yamlFile).Path) {
                $resolved.Value += @{}
            }
        }
        catch {
            # Ignora file YAML non validi
        }
    }

    # Costruisci la catena delle directory
    $dirChain = @()
    $currentDir = Split-Path -LiteralPath $yamlFile

    $result = @{}

    while ($true) {
        if ($currentDir -match [regex]::Escape("EFTL processing")) {
            $result.hasEftlProcessing = $true
        }

        $dirChain = ,$currentDir + $dirChain
        if ($currentDir -eq $sourceDir) {
            break
        }
        $parentDir = Split-Path -LiteralPath $currentDir
        if ($parentDir -eq $currentDir) {
            break
        }
        $currentDir = $parentDir
    }

    foreach ($dir in $dirChain) {
        Add-File (Join-Path $dir 'opencollection.yml')
        Add-File (Join-Path $dir 'folder.yml')
    }

    # Aggiungi il file principale
    Add-File $yamlFile

    foreach ($config in $resolved.Value) {
        $result = Merge-RequestConfig $result $config
    }

    return $result
}

function Build-RequestContent ($request, $requestName, $requestConfig = @{}, [ref][System.Collections.Generic.HashSet[string]]$dotenvVariables = $(New-Object System.Collections.Generic.HashSet[string])) {
    $lines = @()
    $variableDefinitions = [ref] @()
    $commentedVariableDefinitions = [ref] @()
    $parameterVariableDefinitions = [ref] @()
    $seenVariables = [ref] (New-Object System.Collections.Generic.HashSet[string])

    function Add-Variable ($Name, $Value) {
        if (-not $Name) { return }
        $normalized = ([string]$Name).Trim()
        if (-not $normalized) { return }
        if ($seenVariables.Value.Contains($normalized)) { return }
        if ($dotenvVariables.Value.Contains($normalized)) { return }
        [void]$seenVariables.Value.Add($normalized)
        $variableDefinitions.Value += @{ name=$normalized; value=$Value }
    }

    function Add-ParameterVariable ($Name, $Value) {
        if (-not $Name) { return }
        $normalized = ([string]$Name).Trim()
        if (-not $normalized) { return }
        if ($seenVariables.Value.Contains($normalized)) { return }
        if ($dotenvVariables.Value.Contains($normalized)) { return }
        [void]$seenVariables.Value.Add($normalized)
        $parameterVariableDefinitions.Value += @{ name=$normalized; value=$Value }
    }

    function Add-CommentedVariable ($Name, $Value) {
        if (-not $Name) { return }
        $normalized = ([string]$Name).Trim()
        if (-not $normalized) { return }
        $commentedVariableDefinitions.Value += @{ name=$normalized; value=$Value }
    }

    function Add-ReferencedVariables ($Value, $FallbackValue = $DEFAULT_ENV_VAR_VALUE) {
        foreach ($placeholder in Collect-Placeholders ([string]$Value)) {
            Add-Variable $placeholder $FallbackValue
        }
    }

    function Convert-JsonStructure {
        param(
            [Parameter(Mandatory)]
            $Value
        )

        # Caso 1: array
        if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
            $result = [System.Collections.IEnumerable]@()
            foreach ($item in $Value) {
                $innerValue = $null
                if ($null -ne $item) { $innerValue = Convert-JsonStructure $item }
                $result += $innerValue
            }
            return $result
        }

        # Caso 2: PSCustomObject (JSON convertito)
        if ($Value -is [PSObject]) {
            $dict = [System.Collections.Specialized.OrderedDictionary]::new()
            foreach ($prop in $Value.PSObject.Properties) {
                if ($null -ne $prop.Value) {
                    if ($prop.Value -is [System.Collections.IEnumerable] -and $prop.Value.Length -eq 0 -and $prop.Value -isnot [string]) {
                        $dict[$prop.Name] = [System.Collections.IEnumerable]@()
                    }
                    else
                    {
                        $dict[$prop.Name] = Convert-JsonStructure $prop.Value
                    }
                }
                else {
                    $dict[$prop.Name] = $null
                }
            }
            return $dict
        }

        # Caso 3: hashtable puro
        if ($Value -is [hashtable]) {
            $dict = [System.Collections.Specialized.OrderedDictionary]::new()
            foreach ($key in $Value.Keys) {
                $innerValue = $null
                if ($null -ne $key -and $null -ne $Value[$key]) { $innerValue = Convert-JsonStructure $Value[$key] }
                $dict[$key] = $innerValue
            }
            return $dict
        }

        # Caso 4: valore primitivo → applico la tua funzione
        $primitiveValue = $Value
        if ($null -ne $Value) { $primitiveValue = Render-Value $Value }
        return $primitiveValue
    }

    function Add-ParameterVariables ($Name, $Value) { Add-ParameterVariable $Name $Value }

    function Add-CommentedVariables ($Name, $Value) { Add-CommentedVariable $Name $Value }

    function Render-Value ($Value) {
        if ($Value -isnot [string]) { return $Value }

        $regex = [regex]'\{\{([^{}]+)\}\}'
        $regex.Replace($Value, { param($match)
            $inner = $match.Groups[1].value
            $placeholder = Parse-PlaceholderContent $inner
            if ($placeholder.isDotenv) {
                return $match.Value
            }
            if ($placeholder.name -and $dotenvVariables.Value.Contains($placeholder.name)) {
                return "{{`$dotenv $($placeholder.name)}}"
            }
            return $match.Value
        })
    }

    function Replace-PreRequestEftlScript ($inputText) {
        # Sostituisce tutto fino al primo [EFTL] con codice specifico
        $processedPreRequest = ($inputText -replace '^[\s\S]*?(?=\[EFTL\])', "exports.elixBase64EftlDocument = btoa(String.raw```n")
        # Sostituisce tutto fino al primo [EFTL] con codice specifico
        $processedPreRequest = ($processedPreRequest -replace '(?s)(.*\[/EFTL\]).*$', "`$1`n``);")
        return $processedPreRequest
    }

    function Add-DefaultPostResponseEftlScript {
        return @"
var status = response.statusCode;

if (status !== 200) {
  console.log("ERRORE HTTP! Response status code: " + status);
  return;
}

var decodedDocument = "";
var response = response.parsedBody;

if (response.value.code === "ERROR") {
  decodedDocument = "ERRORE EFTL!\n";
  decodedDocument += response.value.description.split("\n").slice(0, 3).join("\n");
}
else {
  decodedDocument = atob(response.value.processedDocument);
}

console.log(decodedDocument);
"@
    }

    # -------------------------
    # Variabili da requestConfig
    # -------------------------
    $configVariables = @()
    if ($requestConfig.variables -is [System.Collections.IEnumerable]) {
        $configVariables = $requestConfig.variables
    }
    foreach ($variable in $configVariables) {
        if ($null -ne $variable -and $variable.name) {
            Add-Variable $variable.name $variable.value
        }
    }

    # -------------------------
    # URL
    # -------------------------
    $url = $request.url ?? ''
    if ($url) {
        Add-ReferencedVariables $url
        $url = ($url -replace ':([A-Za-z0-9_]+)', '{{$1}}')
        $url = Render-Value $url
        $url = $url.Split('?')[0]
    }

    # -------------------------
    # Headers
    # -------------------------
    $queryParams = @()
    $headers = [ref] @()

    function Add-Header ($Name, $Value) {
        if (-not $Name) { return }
        Add-ReferencedVariables ($Value ?? '')
        $headers.Value += @{
            name = ([string]$Name).Trim()
            value = Render-Value ($Value ?? '')
        }
    }

    foreach ($header in ($request.headers ?? @())) {
        if ($header -and $header.name) {
            Add-Header $header.name ($header.value ?? '')
        }
    }

    foreach ($header in ($requestConfig.headers ?? @())) {
        if ($header -and $header.name) {
            Add-Header $header.name ($header.value ?? '')
        }
    }

    # -------------------------
    # Params
    # -------------------------
    foreach ($param in ($request.params ?? @())) {
        $name = $param.name ?? ''
        $value = $param.value ?? ''
        $type = ([string]($param.type ?? 'query')).ToLower()
        $disabled = ([string]$param.disabled).ToLower() -eq 'true'

        if ($disabled) {
            Add-CommentedVariables $name $value
            continue
        }

        Add-ReferencedVariables $value

        if ($type -eq 'header') {
            $headers.Value += @{ name=$name; value=(Render-Value $value) }
        }
        else {
            $queryParams += @{ name=$name; value="{{$name}}" }
            Add-ParameterVariables $name $value
        }
    }

    # -------------------------
    # Auth
    # -------------------------
    if ($requestConfig.auth) {
        switch ($requestConfig.auth.type) {
            'bearer' {
                Add-ReferencedVariables ($requestConfig.auth.token ?? '')
                Add-Header 'Authorization' ("Bearer " + (Render-Value ($requestConfig.auth.token ?? '')))
            }
            'basic' {
                $username = $requestConfig.auth.username ?? ''
                $password = $requestConfig.auth.password ?? ''
                Add-ReferencedVariables $username
                Add-ReferencedVariables $password
                Add-Header 'Authorization' ("Basic " + (Render-Value $username) + ":" + (Render-Value $password))
            }
            default {
                $headers.Value += @{
                    name = "UNKNOWN_$($requestConfig.auth.type)"
                    value = "Basic $($requestConfig.auth.token)"
                }
            }
        }
    }

    # -------------------------
    # Commented variables
    # -------------------------
    if ($commentedVariableDefinitions.Value.Count -gt 0) {
        $lines += "# Other variables for $requestName"
        foreach ($variable in $commentedVariableDefinitions.Value | Sort-Object -Property name, value) {
            $lines += "# @$(Sanitize-VarName $variable.name)$VARIABLE_NAME_VALUE_SEPARATOR$(Format-VariableValue $variable.value @{ renderValue = { param($v) Render-Value $v } })"
        }
    }

    # -------------------------
    # Parameter variables
    # -------------------------
    if ($parameterVariableDefinitions.Value.Count -gt 0) {
        $lines += "# Parameter variables for $requestName"
        foreach ($variable in $parameterVariableDefinitions.Value | Sort-Object -Property name, value) {
            $lines += "@$(Sanitize-VarName $variable.name)$VARIABLE_NAME_VALUE_SEPARATOR$(Format-VariableValue $variable.value @{ renderValue = { param($v) Render-Value $v } })"
        }
    }

    # -------------------------
    # Body
    # -------------------------
    $requestBody = ''

    if ($request.body -and ($request.body -is [psobject] -or $request.body -is [hashtable])) {
        $bodyType = ([string]($request.body.type ?? '')).ToLower()

        if ($bodyType -eq 'json') {
            $jsonData = ConvertFrom-Json -Depth 20 -InputObject ($request.body.data)

            if ($jsonData -is [string]) {
                $requestBody = Render-Value $jsonData
            }
            elseif ($jsonData -is [System.Collections.IEnumerable]) {
                $requestBody = (ConvertTo-Json (Convert-JsonStructure $jsonData) -Depth 20)
            }
            elseif ($jsonData -is [psobject] -or $jsonData -is [hashtable]) {
                $requestBody = (ConvertTo-Json (Convert-JsonStructure $jsonData) -Depth 20)
            }

            Add-Header 'Content-Type' 'application/json'
        }
        elseif ($bodyType -eq 'form-urlencoded') {
            $parts = @()
            foreach ($entry in ($request.body.data ?? @())) {
                if (-not $entry -or -not $entry.name) { continue }
                Add-ReferencedVariables ($entry.value ?? '')
                $parts += "$($entry.name)=$(Render-Value ($entry.value ?? ''))"
            }
            $requestBody = ($parts -join '&')
            Add-Header 'Content-Type' 'application/x-www-form-urlencoded'
        }
    }

    # -------------------------
    # Variables
    # -------------------------
    if ($variableDefinitions.Value.Count -gt 0) {
        $lines += "# Variables for $requestName"
        foreach ($variable in $variableDefinitions.Value | Sort-Object -Property name, value) {
            $lines += "@$(Sanitize-VarName $variable.name)$VARIABLE_NAME_VALUE_SEPARATOR$(Format-VariableValue $variable.value @{ renderValue = { param($v) Render-Value $v } })"
        }
    }

    # -------------------------
    # Super-special treatment for requests belonging to elixForms API v2\EFTL Processing folder
    # Here we blindly assume that ALL requests must be treated in the same way!
    # Pre-request script is taken from the original request and manipulated so it can be integrated into the generated .http file
    # -------------------------
    if ($request.preRequestScript) {
        $lines += ''
        $lines += "{{"
        $lines += "// Pre-request script"
        if ($requestConfig.hasEftlProcessing) {
            $lines += (Replace-PreRequestEftlScript $request.preRequestScript)
        }
        else {
            $lines += $request.preRequestScript
        }
        $lines += "}}"
    }

    # -------------------------
    # Final request line
    # -------------------------
    $method = ([string]($request.method ?? 'GET')).ToUpper()
    $requestUrl = $url

    foreach ($param in $queryParams) {
        if (-not $param.name) { continue }
        $separator = ($requestUrl.Contains('?') ? '&' : '?')
        $requestUrl = "$requestUrl$separator$($param.name)=$($param.value)"
    }

    $lines += ''
    $lines += "$method $requestUrl"

    foreach ($header in $headers.Value) {
        $lines += "$($header.name): $($header.value)"
    }

    if ($requestBody) {
        $lines += ''
        $lines += $requestBody
    }

    # -------------------------
    # Super-special treatment for requests belonging to elixForms API v2\EFTL Processing folder
    # Here we blindly assume that ALL requests must be treated in the same way!
    # Post-response script is overwritten by default so it should magically work
    # -------------------------
    if ($requestConfig.hasEftlProcessing) {
        $lines += ''
        $lines += "{{"
        $lines += "// Post-response script (EFTL)"
        $lines += Add-DefaultPostResponseEftlScript
        $lines += "}}"
    }
    elseif ($request.postResponseScript) {
        $lines += ''
        $lines += "{{"
        $lines += "// Post-response script"
        $lines += $request.postResponseScript
        $lines += "}}"
    }

    return ($lines -join "`n")
}

function Ensure-Dir($path) {
    if (-not (Test-Path -LiteralPath $path)) {
        New-Item -ItemType Directory -Path $path | Out-Null
    }
}

function Walk-YamlFiles($rootDir) {
    Get-ChildItem -LiteralPath $rootDir -Recurse -File -Include *.yml, *.yaml |
        Where-Object { $_.Name -notmatch '^\.|node_modules|folder\.yml' } |
        Select-Object -ExpandProperty FullName
}

function Clean-Folder($dir) {
    if (-not (Test-Path -LiteralPath $dir)) { return }

    foreach ($entry in Get-ChildItem -LiteralPath $dir) {
        if ($entry.PSIsContainer) {
            Clean-Folder $entry.FullName
            $children = Get-ChildItem -LiteralPath $entry.FullName
            if ($children.Count -eq 0) {
                Remove-Item -LiteralPath $entry.FullName -Force
            }
        } else {
            if (!$entry.PSIsContainer -and $entry.Name -match '\.js$|\.http$|^\.env\..*$') {
                Remove-Item -LiteralPath $entry.FullName -Force
            }
        }
    }
}

function Get-DotenvVariablesForTargetDir ($TargetDir, $OutputRoot, $DotEnvVarsByTarget) {
    $variables = $(New-Object System.Collections.Generic.HashSet[string])
    $variables.Clear()
    $currentDir = $TargetDir

    while ($true) {
        if ($DotEnvVarsByTarget.ContainsKey($currentDir)) {
            foreach ($variable in $DotEnvVarsByTarget[$currentDir]) {
                [void]$variables.Add($variable)
            }
        }

        if (-not $currentDir) {
            Write-warning "Current directory is null while searching for dotenv variables for target dir: $TargetDir"
        }
        $parentDir = Split-Path -LiteralPath $currentDir
        if ($currentDir -eq $OutputRoot -or $parentDir -eq $currentDir) {
            break
        }

        $currentDir = $parentDir
    }

    return $variables
}

function Write-EnvironmentTemplates ($sourceDir, $outputRoot) {
    $targets = [ref] @()
    $localDotEnvVarsByTarget = @{}

    function Visit ([string]$CurrentDir) {
        $entries = Get-ChildItem -LiteralPath $CurrentDir -Force
        $hasEnvironmentsDir = ($entries | Where-Object {
            $_.PSIsContainer -and $_.Name -eq 'environments' -and (Get-ChildItem -LiteralPath $_.FullName -Force | Where-Object { -not $_.PSIsContainer -and $_.Name -match '\.ya?ml$' }).Count -gt 0
        }).Count -gt 0

        if ($hasEnvironmentsDir) {
            $targets.Value += $CurrentDir
        }

        foreach ($entry in $entries) {
            if (-not $entry.PSIsContainer) { continue }
            if ($entry.Name.StartsWith('.')) { continue }
            if ($entry.Name -eq 'node_modules') { continue }

            Visit (Join-Path $CurrentDir $entry.Name)
        }
    }

    Visit $sourceDir

    foreach ($dir in $targets.Value) {
        $relativeDir = [System.IO.Path]::GetRelativePath($sourceDir, $dir)
        if ($relativeDir -and $relativeDir -ne '.') {
            $targetDir = Join-Path $outputRoot $relativeDir
        }
        else {
            $targetDir = $outputRoot
        }

        Ensure-Dir $targetDir

        $environmentsDir = Join-Path $dir 'environments'
        if (-not (Test-Path $environmentsDir)) {
            continue
        }

        $envFiles =
            Get-ChildItem -LiteralPath $environmentsDir -Force |
            Where-Object { -not $_.PSIsContainer -and $_.Name -match '\.ya?ml$' } |
            ForEach-Object { $_.FullName }

        $variableNames = @()
        $seenNames = New-Object System.Collections.Generic.HashSet[string]

        foreach ($envFile in $envFiles) {
            $parsed = Parse-YamlFile $envFile
            if (-not $parsed) { continue }

            $variables = @()
            if ($parsed.variables -is [System.Collections.IEnumerable]) {
                $variables = $parsed.variables
            }

            foreach ($variable in $variables) {
                if (-not $variable -or -not $variable.name) { continue }

                $name = ([string]$variable.name).Trim()
                if (-not $name) { continue }
                if ($seenNames.Contains($name)) { continue }

                [void]$seenNames.Add($name) # Ma porc
                $variableNames += $name
            }
        }

        if ($variableNames.Count -gt 0) {
            $templateContent = ($variableNames | ForEach-Object { "$_=$DEFAULT_ENV_VAR_VALUE" }) -join "`n"
            $templateContent += "`n"
        }
        else {
            $templateContent = ""
        }

        $templatePath = Join-Path $targetDir '.env.template'
        Set-Content -LiteralPath $templatePath -Value $templateContent -Encoding UTF8 -NoNewLine

        $localDotEnvVarsByTarget[$targetDir] = $seenNames
    }

    return $localDotEnvVarsByTarget
}

function Write-JsFiles ($sourceDir, $outputRoot) {
    $targets = [ref] @()

    function Visit ($CurrentDir) {
        $entries = Get-ChildItem -LiteralPath $CurrentDir -Force

        $hasJsFiles = $entries | Where-Object {
            -not $_.PSIsContainer -and $_.Name -match '\.js$'
        }

        if ($hasJsFiles) {
            $targets.Value += $CurrentDir
        }

        # Visita ricorsivamente le sottodirectory
        foreach ($entry in $entries) {
            if (-not $entry.PSIsContainer) { continue }
            if ($entry.Name.StartsWith('.')) { continue }
            if ($entry.Name -eq 'node_modules') { continue }

            Visit (Join-Path $CurrentDir $entry.Name)
        }
    }

    Visit $sourceDir

    foreach ($dir in $targets.Value) {
        $relativeDir = [System.IO.Path]::GetRelativePath($sourceDir, $dir)
        if ($relativeDir -and $relativeDir -ne '.') {
            $targetDir = Join-Path $outputRoot $relativeDir
        }
        else {
            $targetDir = $outputRoot
        }

        Ensure-Dir $targetDir

        if (-not (Test-Path $dir)) {
            continue
        }

        $jsFiles = 
            Get-ChildItem -LiteralPath $dir -Force |
            Where-Object { -not $_.PSIsContainer -and $_.Name -match '\.js$' } |
            ForEach-Object {
                @{
                    Source = $_.FullName
                    Target = Join-Path $targetDir $_.Name
                }
            }

        foreach ($jsFile in $jsFiles) {
            Copy-Item -LiteralPath $jsFile.Source -Destination $jsFile.Target -Force
        }
    }

    return $targets.Value
}

function Invoke-Main {
    # Workspace root
    $workspaceRoot = Find-WorkspaceRoot $StartDir
    $workspaceFile = Join-Path $workspaceRoot 'workspace.yml'
    $workspace = Parse-Workspace $workspaceFile
    $collections = @()
    if ($workspace.collections -is [System.Collections.IEnumerable]) {
        $collections = $workspace.collections
    }

    if ($collections.Count -eq 0) {
        throw "No collections found in workspace.yml"
    }

    $outputBaseRoot = Join-Path $OutputDir 'autodocs/httpyac'
    if (-not (Test-Path $outputBaseRoot)) {
        New-Item -ItemType Directory -Path $outputBaseRoot | Out-Null
    }
    $outputBaseRoot = Resolve-Path -LiteralPath $outputBaseRoot

    Clean-Folder $outputBaseRoot

    foreach ($collection in $collections) {
        if (-not $collection -or -not $collection.name -or -not $collection.path) {
            continue
        }

        $sourceDir = Join-Path $workspaceRoot $collection.path
        if (-not (Test-Path $sourceDir)) {
            Write-Warning "Skipping missing collection path: $($collection.path)"
            continue
        }

        $outputRoot = Join-Path $outputBaseRoot $collection.name
        Ensure-Dir $outputRoot

        # JS files
        $writtenJsFiles = Write-JsFiles $sourceDir $outputRoot

        # dotenv templates
        $myDotEnvVarsByTarget = @{}
        $myDotEnvVarsByTarget = (Write-EnvironmentTemplates $sourceDir $outputRoot)

        # YAML files
        $yamlFiles = Walk-YamlFiles $sourceDir
        $processed = 0

        foreach ($yamlFile in $yamlFiles) {

            $content = Get-Content -LiteralPath $yamlFile -Raw
            $requestName = Parse-RequestInfo $content
            if (-not $requestName) {
                $requestName = [System.IO.Path]::GetFileNameWithoutExtension($yamlFile)
            }

            $httpBlock = Parse-HttpBlock $content
            if (-not $httpBlock -or -not $httpBlock.url) {
                continue
            }

            $relativePath = [System.IO.Path]::GetRelativePath($sourceDir, $yamlFile)
            $parsedPath = [System.IO.Path]::GetFileNameWithoutExtension($relativePath)
            $parsedDir  = Split-Path -LiteralPath $relativePath

            if ($parsedDir -and $parsedDir -ne '') {
                $targetDir = Join-Path $outputRoot $parsedDir
            }
            else {
                $targetDir = $outputRoot
            }
            Ensure-Dir $targetDir

            $requestConfig = Get-RequestConfigForFile $yamlFile $sourceDir
            $outputFile = Join-Path $targetDir ("$parsedPath.http")

            $myDotenvVariables = Get-DotenvVariablesForTargetDir $targetDir $outputRoot $myDotEnvVarsByTarget
            if (-not $myDotenvVariables) {
                $myDotenvVariables = $(New-Object System.Collections.Generic.HashSet[string])
            }

            $requestContent = Build-RequestContent $httpBlock $requestName $requestConfig ([ref]$myDotenvVariables)

            Set-Content -LiteralPath $outputFile -Value $requestContent -Encoding UTF8

            $processed++
        }

        $namePadded = $collection.name.PadRight(33)
        $httpCount  = $processed.ToString().PadLeft(3)
        $jsCount    = $writtenJsFiles.Count.ToString().PadLeft(3)

        Write-Host "$namePadded => generated $httpCount .http file(s) and $jsCount .js file(s)"
    }
}

Invoke-Main
