Set-StrictMode -Version 3.0
$ErrorActionPreference = "Stop"

function Get-RalphRoot {
    return (Split-Path -Parent $PSScriptRoot)
}

function Get-RepoRoot {
    return (Split-Path -Parent (Get-RalphRoot))
}

function Ensure-Directory {
    param([Parameter(Mandatory = $true)][string]$Path)
    if (-not (Test-Path $Path)) {
        New-Item -ItemType Directory -Force -Path $Path | Out-Null
    }
}

function Read-JsonFile {
    param([Parameter(Mandatory = $true)][string]$Path)
    if (-not (Test-Path $Path)) {
        throw "JSON file not found: $Path"
    }

    return Get-Content -Raw -Path $Path | ConvertFrom-Json
}

function Get-ProvidersConfig {
    $ralphRoot = Get-RalphRoot
    $localPath = Join-Path $ralphRoot "config\providers.local.json"
    $examplePath = Join-Path $ralphRoot "config\providers.example.json"

    if (Test-Path $localPath) {
        return Read-JsonFile -Path $localPath
    }

    return Read-JsonFile -Path $examplePath
}

function Get-ProviderConfig {
    param([Parameter(Mandatory = $true)][string]$Name)

    $config = Get-ProvidersConfig
    if (-not $config.providers.PSObject.Properties.Name.Contains($Name)) {
        throw "Provider '$Name' not found in providers config."
    }

    return $config.providers.$Name
}

function Get-DefaultImplementer {
    $config = Get-ProvidersConfig
    return [string]$config.default_implementer
}

function Get-DefaultReviewers {
    $config = Get-ProvidersConfig
    return @($config.default_reviewers)
}

function Get-CodexConfig {
    $ralphRoot = Get-RalphRoot
    $localPath = Join-Path $ralphRoot "config\codex.local.json"
    $examplePath = Join-Path $ralphRoot "config\codex.example.json"

    if (Test-Path $localPath) {
        return Read-JsonFile -Path $localPath
    }

    return Read-JsonFile -Path $examplePath
}

function Get-RalphAutomationConfig {
    $ralphRoot = Get-RalphRoot
    $localPath = Join-Path $ralphRoot "config\automation.local.json"
    $examplePath = Join-Path $ralphRoot "config\automation.example.json"

    if (Test-Path $localPath) {
        return Read-JsonFile -Path $localPath
    }

    if (Test-Path $examplePath) {
        return Read-JsonFile -Path $examplePath
    }

    return $null
}

function Get-TelegramConfig {
    $ralphRoot = Get-RalphRoot
    $localPath = Join-Path $ralphRoot "config\telegram.local.json"
    $examplePath = Join-Path $ralphRoot "config\telegram.example.json"

    if (Test-Path $localPath) {
        return Read-JsonFile -Path $localPath
    }

    if (Test-Path $examplePath) {
        return Read-JsonFile -Path $examplePath
    }

    return $null
}

function Get-TelegramChatIds {
    param($Config)

    if ($null -eq $Config) {
        return @()
    }

    if ($Config.PSObject.Properties.Name -contains "chat_ids") {
        return @($Config.chat_ids | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { [string]$_ })
    }

    if ($Config.PSObject.Properties.Name -contains "chat_id" -and -not [string]::IsNullOrWhiteSpace([string]$Config.chat_id)) {
        return @([string]$Config.chat_id)
    }

    return @()
}

function Test-TelegramEventEnabled {
    param(
        [Parameter(Mandatory = $true)]$Config,
        [Parameter(Mandatory = $true)][string]$EventName
    )

    $enabled = $false
    try { $enabled = [bool]$Config.enabled } catch { $enabled = $false }
    if (-not $enabled) {
        return $false
    }

    if ($Config.PSObject.Properties.Name -contains "events" -and $null -ne $Config.events) {
        $eventProperty = $Config.events.PSObject.Properties[$EventName]
        if ($null -ne $eventProperty) {
            try { return [bool]$eventProperty.Value } catch { return $false }
        }
    }

    return $true
}

function Send-TelegramNotification {
    param(
        [Parameter(Mandatory = $true)][string]$EventName,
        [Parameter(Mandatory = $true)][string]$Title,
        [Parameter(Mandatory = $true)][string]$Message,
        [string]$RunId = "",
        [string]$Stage = "",
        [string]$Status = ""
    )

    try {
        $config = Get-TelegramConfig
    }
    catch {
        return [ordered]@{
            sent = $false
            reason = $_.Exception.Message
            delivered = 0
            errors = @()
        }
    }

    if ($null -eq $config) {
        return [ordered]@{
            sent = $false
            reason = "telegram config missing"
            delivered = 0
            errors = @()
        }
    }

    if (-not (Test-TelegramEventEnabled -Config $config -EventName $EventName)) {
        return [ordered]@{
            sent = $false
            reason = "event disabled"
            delivered = 0
            errors = @()
        }
    }

    $botToken = ""
    if ($config.PSObject.Properties.Name -contains "bot_token") {
        $botToken = [string]$config.bot_token
    }
    if ([string]::IsNullOrWhiteSpace($botToken)) {
        return [ordered]@{
            sent = $false
            reason = "bot token missing"
            delivered = 0
            errors = @()
        }
    }

    $chatIds = @(Get-TelegramChatIds -Config $config)
    if ($chatIds.Count -eq 0) {
        return [ordered]@{
            sent = $false
            reason = "chat ids missing"
            delivered = 0
            errors = @()
        }
    }

    $timeoutSeconds = 8
    if ($config.PSObject.Properties.Name -contains "timeout_seconds") {
        try {
            $timeoutSeconds = [int]$config.timeout_seconds
        }
        catch {
            $timeoutSeconds = 8
        }
    }

    $prefix = "Ralph"
    if ($config.PSObject.Properties.Name -contains "prefix" -and -not [string]::IsNullOrWhiteSpace([string]$config.prefix)) {
        $prefix = [string]$config.prefix
    }

    $lines = @(
        ("{0} | {1}" -f $prefix, $Title.Trim())
        $Message.Trim()
    )
    if (-not [string]::IsNullOrWhiteSpace($RunId)) {
        $lines += ("run: " + $RunId)
    }
    if (-not [string]::IsNullOrWhiteSpace($Stage) -or -not [string]::IsNullOrWhiteSpace($Status)) {
        $statusLine = @()
        if (-not [string]::IsNullOrWhiteSpace($Stage)) {
            $statusLine += ("stage=" + $Stage)
        }
        if (-not [string]::IsNullOrWhiteSpace($Status)) {
            $statusLine += ("status=" + $Status)
        }
        if ($statusLine.Count -gt 0) {
            $lines += ($statusLine -join " ")
        }
    }
    $text = ($lines -join "`n").Trim()

    $uri = "https://api.telegram.org/bot{0}/sendMessage" -f $botToken
    $delivered = 0
    $errors = New-Object System.Collections.Generic.List[string]

    foreach ($chatId in $chatIds) {
        try {
            $body = @{
                chat_id = $chatId
                text = $text
                disable_web_page_preview = $true
            }
            Invoke-RestMethod -Method Post -Uri $uri -Body $body -ContentType "application/x-www-form-urlencoded" -TimeoutSec $timeoutSeconds | Out-Null
            $delivered += 1
        }
        catch {
            $errors.Add(("{0}: {1}" -f $chatId, $_.Exception.Message))
        }
    }

    return [ordered]@{
        sent = ($delivered -gt 0)
        reason = $(if ($delivered -gt 0) { "ok" } else { "delivery failed" })
        delivered = $delivered
        errors = @($errors)
    }
}

function New-RunId {
    param([string]$Label = "autopilot")
    return "{0}-{1}" -f (Get-Date -Format "yyyyMMdd-HHmmss"), $Label
}

function Get-TaskFiles {
    param([string]$TaskDirectory = $(Join-Path (Get-RalphRoot) "tasks\current"))

    return @{
        Task = Join-Path $TaskDirectory "TASK.md"
        Acceptance = Join-Path $TaskDirectory "ACCEPTANCE.md"
        Context = Join-Path $TaskDirectory "CONTEXT.md"
    }
}

function Get-RalphInboxDirectory {
    return (Join-Path (Get-RalphRoot) "tasks\inbox")
}

function Get-RalphProcessingDirectory {
    return (Join-Path (Get-RalphRoot) "tasks\processing")
}

function Get-RalphArchiveDirectory {
    return (Join-Path (Get-RalphRoot) "tasks\completed")
}

function Get-RalphFailedDirectory {
    return (Join-Path (Get-RalphRoot) "tasks\failed")
}

function Read-TaskPack {
    param([string]$TaskDirectory = $(Join-Path (Get-RalphRoot) "tasks\current"))

    $files = Get-TaskFiles -TaskDirectory $TaskDirectory
    foreach ($entry in $files.GetEnumerator()) {
        if (-not (Test-Path $entry.Value)) {
            throw "Task pack file missing: $($entry.Value)"
        }
    }

    return @{
        Files = $files
        Task = Get-Content -Raw -Path $files.Task
        Acceptance = Get-Content -Raw -Path $files.Acceptance
        Context = Get-Content -Raw -Path $files.Context
    }
}

function Convert-MarkdownToRalphTaskPack {
    param(
        [Parameter(Mandatory = $true)][string]$Path
    )

    if (-not (Test-Path $Path)) {
        throw "Task markdown not found: $Path"
    }

    $raw = Get-Content -Raw -Path $Path
    $lines = $raw -split "`r?`n"
    $taskLines = New-Object System.Collections.Generic.List[string]
    $acceptanceLines = New-Object System.Collections.Generic.List[string]
    $contextLines = New-Object System.Collections.Generic.List[string]
    $current = "task"
    $title = ""

    foreach ($line in $lines) {
        if ($line -match '^\s*#{1,3}\s+(.*\S)\s*$') {
            $heading = $matches[1].Trim()
            if (-not $title -and $line -match '^\s*#\s+') {
                $title = $heading
            }

            $headingLower = $heading.ToLowerInvariant()
            if ($headingLower -match '^(acceptance|acceptance criteria|criteria|done|definition of done)\b') {
                $current = "acceptance"
            }
            elseif ($headingLower -match '^(context|background|notes|references)\b') {
                $current = "context"
            }
            else {
                $current = "task"
                $taskLines.Add($line)
            }
            continue
        }

        switch ($current) {
            "acceptance" { $acceptanceLines.Add($line) }
            "context" { $contextLines.Add($line) }
            default { $taskLines.Add($line) }
        }
    }

    $taskText = (($taskLines -join "`n").Trim())
    $acceptanceText = (($acceptanceLines -join "`n").Trim())
    $contextText = (($contextLines -join "`n").Trim())

    if ([string]::IsNullOrWhiteSpace($taskText)) {
        $taskText = $raw.Trim()
    }
    if ([string]::IsNullOrWhiteSpace($acceptanceText)) {
        $acceptanceText = Get-Content -Raw -Path (Join-Path (Get-RalphRoot) "templates\ACCEPTANCE.md")
    }
    if ([string]::IsNullOrWhiteSpace($contextText)) {
        $contextText = Get-Content -Raw -Path (Join-Path (Get-RalphRoot) "templates\CONTEXT.md")
    }
    if ([string]::IsNullOrWhiteSpace($title)) {
        $title = [IO.Path]::GetFileNameWithoutExtension($Path)
    }

    return [ordered]@{
        Title = $title
        SourcePath = $Path
        RawContent = $raw
        Task = $taskText
        Acceptance = $acceptanceText
        Context = $contextText
    }
}

function Write-RalphTaskPackDirectory {
    param(
        [Parameter(Mandatory = $true)][hashtable]$TaskPack,
        [Parameter(Mandatory = $true)][string]$DestinationDirectory
    )

    Ensure-Directory -Path $DestinationDirectory
    Write-Utf8File -Path (Join-Path $DestinationDirectory "TASK.md") -Content (($TaskPack.Task.Trim()) + "`n")
    Write-Utf8File -Path (Join-Path $DestinationDirectory "ACCEPTANCE.md") -Content (($TaskPack.Acceptance.Trim()) + "`n")
    Write-Utf8File -Path (Join-Path $DestinationDirectory "CONTEXT.md") -Content (($TaskPack.Context.Trim()) + "`n")

    if ($TaskPack.ContainsKey("SourcePath") -and (Test-Path $TaskPack.SourcePath)) {
        Copy-Item -Path $TaskPack.SourcePath -Destination (Join-Path $DestinationDirectory "SOURCE.md") -Force
    }
}

function Write-Utf8File {
    param(
        [Parameter(Mandatory = $true)][string]$Path,
        [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content
    )

    $encoding = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($Path, $Content, $encoding)
}

function Write-JsonFile {
    param(
        [Parameter(Mandatory = $true)][string]$Path,
        [Parameter(Mandatory = $true)]$Object
    )

    $json = $Object | ConvertTo-Json -Depth 100
    Write-Utf8File -Path $Path -Content ($json + "`n")
}

function Get-RalphStateFile {
    param([Parameter(Mandatory = $true)][string]$Name)
    return Join-Path (Get-RalphRoot) ("state\" + $Name)
}

function Get-RalphTaskRoots {
    $ralphRoot = Get-RalphRoot
    return [ordered]@{
        TasksRoot = Join-Path $ralphRoot "tasks"
        Inbox = Join-Path $ralphRoot "tasks\inbox"
        Processing = Join-Path $ralphRoot "tasks\processing"
        Completed = Join-Path $ralphRoot "tasks\completed"
        Failed = Join-Path $ralphRoot "tasks\failed"
    }
}

function Convert-ToRalphSlug {
    param([Parameter(Mandatory = $true)][string]$Text)

    $slug = [string]$Text
    $slug = $slug.Trim().ToLowerInvariant()
    if ([string]::IsNullOrWhiteSpace($slug)) {
        return "task"
    }

    $slug = [regex]::Replace($slug, "[^a-z0-9]+", "-")
    $slug = $slug.Trim("-")
    if ([string]::IsNullOrWhiteSpace($slug)) {
        return "task"
    }
    if ($slug.Length -gt 48) {
        $slug = $slug.Substring(0, 48).Trim("-")
    }
    return $slug
}

function Get-RalphTaskTitleFromMarkdown {
    param(
        [string]$Markdown = "",
        [string]$Fallback = "task"
    )

    $lines = @($Markdown -split "`r?`n")
    foreach ($line in $lines) {
        $trimmed = [string]$line
        $trimmed = $trimmed.Trim()
        if ([string]::IsNullOrWhiteSpace($trimmed)) {
            continue
        }
        if ($trimmed.StartsWith("#")) {
            return ($trimmed.TrimStart("#").Trim())
        }
        return $trimmed
    }
    return $Fallback
}

function New-RalphTaskPackFromMarkdown {
    param(
        [Parameter(Mandatory = $true)][string]$MarkdownPath,
        [string]$TargetDirectory = "",
        [string]$TaskId = "",
        [string]$Title = "",
        [hashtable]$AdditionalMetadata = @{}
    )

    if (-not (Test-Path $MarkdownPath)) {
        throw "Markdown source not found: $MarkdownPath"
    }

    $roots = Get-RalphTaskRoots
    foreach ($path in $roots.Values) {
        Ensure-Directory -Path $path
    }

    $parsedTaskPack = Convert-MarkdownToRalphTaskPack -Path $MarkdownPath
    $sourceText = [string]$parsedTaskPack.RawContent
    $sourceName = [System.IO.Path]::GetFileNameWithoutExtension($MarkdownPath)
    if ([string]::IsNullOrWhiteSpace($Title)) {
        $Title = [string]$parsedTaskPack.Title
    }
    if ([string]::IsNullOrWhiteSpace($TaskId)) {
        $TaskId = "{0}-{1}" -f (Get-Date -Format "yyyyMMdd-HHmmss"), (Convert-ToRalphSlug -Text $Title)
    }
    if ([string]::IsNullOrWhiteSpace($TargetDirectory)) {
        $TargetDirectory = Join-Path $roots.Inbox $TaskId
    }

    Ensure-Directory -Path $TargetDirectory

    $taskPath = Join-Path $TargetDirectory "TASK.md"
    $acceptancePath = Join-Path $TargetDirectory "ACCEPTANCE.md"
    $contextPath = Join-Path $TargetDirectory "CONTEXT.md"
    $metadataPath = Join-Path $TargetDirectory "submission.json"
    $sourceCopyPath = Join-Path $TargetDirectory "SOURCE.md"

    $acceptance = [string]$parsedTaskPack.Acceptance
    $context = [string]$parsedTaskPack.Context
    $taskBody = [string]$parsedTaskPack.Task

    Write-Utf8File -Path $taskPath -Content ($taskBody.Trim() + "`n")
    Write-Utf8File -Path $sourceCopyPath -Content ($sourceText.Trim() + "`n")
    Write-Utf8File -Path $acceptancePath -Content ($acceptance + "`n")
    Write-Utf8File -Path $contextPath -Content ($context + "`n")
    $metadata = [ordered]@{
        id = $TaskId
        title = $Title
        submitted_at = (Get-Date).ToString("o")
        source_path = (Resolve-Path $MarkdownPath).Path
        task_directory = $TargetDirectory
        state = "queued"
    }
    foreach ($key in $AdditionalMetadata.Keys) {
        $metadata[$key] = $AdditionalMetadata[$key]
    }
    Write-JsonFile -Path $metadataPath -Object $metadata

    return [ordered]@{
        id = $TaskId
        title = $Title
        task_directory = $TargetDirectory
        task_file = $taskPath
        acceptance_file = $acceptancePath
        context_file = $contextPath
        metadata_file = $metadataPath
        source_copy = $sourceCopyPath
    }
}

function Read-CodexReviewVerdict {
    param([Parameter(Mandatory = $true)][string]$Path)

    if (-not (Test-Path $Path)) {
        throw "Codex review file not found: $Path"
    }

    $raw = (Get-Content -Raw -Path $Path).Trim()
    if ([string]::IsNullOrWhiteSpace($raw)) {
        throw "Codex review file is empty: $Path"
    }

    $jsonText = $raw
    if ($raw -match '(?s)```json\s*(\{.*?\})\s*```') {
        $jsonText = $matches[1]
    }
    elseif ($raw -match '(?s)(\{.*\})') {
        $jsonText = $matches[1]
    }

    try {
        $parsed = $jsonText | ConvertFrom-Json
    }
    catch {
        throw "Codex review output is not valid JSON: $Path"
    }

    $verdict = [string]$parsed.verdict
    if ([string]::IsNullOrWhiteSpace($verdict)) {
        throw "Codex review JSON missing 'verdict': $Path"
    }

    $normalizedVerdict = $verdict.Trim().ToLowerInvariant()
    if ($normalizedVerdict -notin @("pass", "needs_fix", "fail")) {
        throw "Codex review verdict '$verdict' is invalid. Expected: pass, needs_fix, fail."
    }

    $acceptancePassed = $false
    try {
        $acceptancePassed = [bool]$parsed.acceptance_passed
    }
    catch {
        $acceptancePassed = ($normalizedVerdict -eq "pass")
    }

    return [ordered]@{
        verdict = $normalizedVerdict
        acceptance_passed = $acceptancePassed
        fix_required = [bool]$parsed.fix_required
        summary = [string]$parsed.summary
        next_sprint_needed = [bool]$parsed.next_sprint_needed
        next_sprint_brief = [string]$parsed.next_sprint_brief
        highest_risk_issues = @($parsed.highest_risk_issues)
        raw = $parsed
    }
}

function Add-RalphEvent {
    param(
        [Parameter(Mandatory = $true)][string]$RunId,
        [Parameter(Mandatory = $true)][string]$Stage,
        [Parameter(Mandatory = $true)][string]$Status,
        [Parameter(Mandatory = $true)][string]$Message,
        [string]$Actor = "system",
        [hashtable]$Data = @{}
    )

    $eventsPath = Get-RalphStateFile -Name "events.jsonl"
    Ensure-Directory -Path (Split-Path -Parent $eventsPath)

    $event = [ordered]@{
        timestamp = (Get-Date).ToString("o")
        run_id = $RunId
        actor = $Actor
        stage = $Stage
        status = $Status
        message = $Message
        data = $Data
    }

    $encoding = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::AppendAllText($eventsPath, (($event | ConvertTo-Json -Depth 20 -Compress) + "`n"), $encoding)
}

function Set-RalphCurrentRunState {
    param([Parameter(Mandatory = $true)][hashtable]$State)
    $statePath = Get-RalphStateFile -Name "current_run.json"
    Write-JsonFile -Path $statePath -Object $State
}

function New-PromptDocument {
    param(
        [Parameter(Mandatory = $true)][string]$TemplatePath,
        [Parameter(Mandatory = $true)][hashtable]$TaskPack,
        [Parameter(Mandatory = $true)][string]$OutputPath,
        [string[]]$ExtraSections = @()
    )

    $template = Get-Content -Raw -Path $TemplatePath
    $sections = @(
        $template,
        "## TASK`n$($TaskPack.Task)",
        "## ACCEPTANCE`n$($TaskPack.Acceptance)",
        "## CONTEXT`n$($TaskPack.Context)"
    ) + $ExtraSections

    Write-Utf8File -Path $OutputPath -Content (($sections -join "`n`n").Trim() + "`n")
}
