# Ensure MySQL + Redis are ready. Returns: docker | native
# Dot-sourced from start.ps1

function Get-DevInfraModeFromEnv {
    param([string]$Root)
    $envFile = Join-Path $Root ".env"
    if (-not (Test-Path $envFile)) {
        return "auto"
    }
    foreach ($line in Get-Content $envFile) {
        if ($line -match '^\s*DEV_INFRA_MODE\s*=\s*(\S+)\s*$') {
            return $Matches[1].Trim().ToLower()
        }
    }
    return "auto"
}

function Get-EnvDbEndpoints {
    param([string]$Root)
    $envFile = Join-Path $Root ".env"
    $mysqlHost = "localhost"
    $mysqlPort = 3307
    $redisHost = "localhost"
    $redisPort = 6379

    if (Test-Path $envFile) {
        foreach ($line in Get-Content $envFile) {
            if ($line -match '^\s*DATABASE_URL\s*=\s*mysql://[^@]+@([^:/]+):(\d+)/') {
                $mysqlHost = $Matches[1]
                $mysqlPort = [int]$Matches[2]
            }
            if ($line -match '^\s*REDIS_URL\s*=\s*redis://([^:/]+):(\d+)') {
                $redisHost = $Matches[1]
                $redisPort = [int]$Matches[2]
            }
            if ($line -match '^\s*MYSQL_HOST_PORT\s*=\s*(\d+)') {
                $mysqlPort = [int]$Matches[1]
            }
        }
    }

    return @{
        MysqlHost = $mysqlHost
        MysqlPort = $mysqlPort
        RedisHost = $redisHost
        RedisPort = $redisPort
    }
}

function Test-TcpPortOpen {
    param(
        [string]$HostName,
        [int]$Port,
        [int]$TimeoutMs = 2000
    )
    $client = $null
    try {
        $client = New-Object System.Net.Sockets.TcpClient
        $connect = $client.BeginConnect($HostName, $Port, $null, $null)
        $ok = $connect.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
        if (-not $ok) {
            return $false
        }
        $client.EndConnect($connect)
        return $true
    }
    catch {
        return $false
    }
    finally {
        if ($client) {
            $client.Close()
        }
    }
}

function Get-WslHostIp {
    $distro = Get-DevWslDistro
    if (-not $distro) { return $null }

    $prev = $ErrorActionPreference
    $ErrorActionPreference = "SilentlyContinue"
    $raw = & wsl.exe -d $distro -e hostname -I 2>&1 | Out-String
    $ErrorActionPreference = $prev

    $text = ($raw -replace "`0", "").Trim()
    $ip = ($text -split '\s+')[0]
    if ($ip -match '^\d+\.\d+\.\d+\.\d+$') { return $ip }
    return $null
}

function Resolve-InfraHost {
    param(
        [string]$Root,
        [int]$MysqlPort,
        [int]$RedisPort
    )
    $localRedisUrl = "redis://127.0.0.1:${RedisPort}"
    if ((Test-TcpPortOpen -HostName "127.0.0.1" -Port $MysqlPort) -and
        (Test-TcpPortOpen -HostName "127.0.0.1" -Port $RedisPort) -and
        (Test-RedisPingFromNode -RedisUrl $localRedisUrl)) {
        return "127.0.0.1"
    }

    $wslIp = Get-WslHostIp
    if ($wslIp) {
        $wslRedisUrl = "redis://${wslIp}:${RedisPort}"
        if ((Test-TcpPortOpen -HostName $wslIp -Port $MysqlPort) -and
            (Test-TcpPortOpen -HostName $wslIp -Port $RedisPort) -and
            (Test-RedisPingFromNode -RedisUrl $wslRedisUrl)) {
            return $wslIp
        }
    }

    if ((Test-TcpPortOpen -HostName "127.0.0.1" -Port $MysqlPort) -and
        (Test-TcpPortOpen -HostName "127.0.0.1" -Port $RedisPort)) {
        return "127.0.0.1"
    }

    if ($wslIp -and
        (Test-TcpPortOpen -HostName $wslIp -Port $MysqlPort) -and
        (Test-TcpPortOpen -HostName $wslIp -Port $RedisPort)) {
        return $wslIp
    }

    return "127.0.0.1"
}

function Apply-DevInfraEnv {
    param([string]$Root)

    $ep = Get-EnvDbEndpoints -Root $Root
    $infraHost = Resolve-InfraHost -Root $Root -MysqlPort $ep.MysqlPort -RedisPort $ep.RedisPort

    if ($infraHost -ne "127.0.0.1" -and $infraHost -ne "localhost") {
        Write-DevLog "WSL localhost forward unavailable, using $infraHost" "Warn"
    }

    $envFile = Join-Path $Root ".env"
    if (Test-Path $envFile) {
        foreach ($line in Get-Content $envFile) {
            if ($line -match '^\s*DATABASE_URL\s*=\s*(.+)$') {
                $env:DATABASE_URL = ($Matches[1] -replace '@([^:/]+):', "@${infraHost}:")
            }
            if ($line -match '^\s*REDIS_URL\s*=\s*(.+)$') {
                $env:REDIS_URL = ($Matches[1] -replace 'redis://([^:/]+):', "redis://${infraHost}:")
            }
        }
    }

    Ensure-DevDirs
    $infraEnv = Join-Path $script:DevConfig.DevDir "infra.env"
    $wslHostIp = Get-WslHostIp
    $lines = @(
        "DATABASE_URL=$($env:DATABASE_URL)"
        "REDIS_URL=$($env:REDIS_URL)"
    )
    if ($wslHostIp) {
        $lines += "WSL_HOST_IP=$wslHostIp"
    }
    $lines | Set-Content -Path $infraEnv -Encoding ascii
}

function Test-NativeInfraPorts {
    param([string]$Root)
    $ep = Get-EnvDbEndpoints -Root $Root
    $infraHost = Resolve-InfraHost -Root $Root -MysqlPort $ep.MysqlPort -RedisPort $ep.RedisPort
    $mysqlOk = Test-TcpPortOpen -HostName $infraHost -Port $ep.MysqlPort
    $redisOk = Test-TcpPortOpen -HostName $infraHost -Port $ep.RedisPort
    return @{
        Ok = ($mysqlOk -and $redisOk)
        Mysql = $mysqlOk
        Redis = $redisOk
        Host = $infraHost
        Endpoints = $ep
    }
}

function Test-NativeInfraReady {
    param([string]$Root)
    $ports = Test-NativeInfraPorts -Root $Root
    if (-not $ports.Ok) {
        return $false
    }
    Apply-DevInfraEnv -Root $Root
    if (-not $env:REDIS_URL) {
        return $false
    }
    return Test-RedisPingFromNode -RedisUrl $env:REDIS_URL
}

function Get-DockerComposeArgs {
    $files = $script:DevConfig.ComposeFiles
    $args = @()
    foreach ($f in $files) {
        $args += "-f"
        $args += (Join-Path $script:DevConfig.ProjectRoot $f)
    }
    return $args
}

function Invoke-DockerComposeProject {
    param(
        [Parameter(ValueFromRemainingArguments = $true)][string[]]$ComposeArgs,
        [int]$TimeoutSec = 90
    )
    $base = Get-DockerComposeArgs
    $argList = @("compose") + $base + $ComposeArgs

    $psi = New-Object System.Diagnostics.ProcessStartInfo
    $psi.FileName = "docker"
    # Windows: quote paths with spaces
    $psi.Arguments = ($argList | ForEach-Object {
        if ($_ -match '\s') { '"' + $_ + '"' } else { $_ }
    }) -join " "
    $psi.RedirectStandardOutput = $true
    $psi.RedirectStandardError = $true
    $psi.UseShellExecute = $false
    $psi.CreateNoWindow = $true

    $proc = New-Object System.Diagnostics.Process
    $proc.StartInfo = $psi
    [void]$proc.Start()
    $finished = $proc.WaitForExit($TimeoutSec * 1000)
    if (-not $finished) {
        try { $proc.Kill() } catch {}
        Write-DevLog "docker compose timeout after ${TimeoutSec}s: $($psi.Arguments)" "Warn"
        return 1
    }
    # drain pipes to avoid rare deadlocks
    [void]$proc.StandardOutput.ReadToEnd()
    [void]$proc.StandardError.ReadToEnd()
    return $proc.ExitCode
}

function Test-DockerServicesHealthy {
    param([string[]]$Services)
    Push-Location $script:DevConfig.ProjectRoot
    try {
        $composeBase = Get-DockerComposeArgs
        foreach ($svc in $Services) {
            $prev = $ErrorActionPreference
            $ErrorActionPreference = "SilentlyContinue"
            $cid = (docker compose @composeBase ps -q $svc 2>$null | Select-Object -First 1)
            if (-not $cid) {
                $ErrorActionPreference = $prev
                return $false
            }
            $health = (docker inspect -f "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}" $cid 2>$null)
            $ErrorActionPreference = $prev
            if ($health -ne "healthy" -and $health -ne "running") {
                return $false
            }
            # mysql/redis 有 healthcheck，须为 healthy
            if ($health -ne "healthy") {
                return $false
            }
        }
        return $true
    }
    finally {
        Pop-Location
    }
}

function Invoke-DockerInfoSafe {
    $psi = New-Object System.Diagnostics.ProcessStartInfo
    $psi.FileName = "docker"
    $psi.Arguments = "info"
    $psi.RedirectStandardOutput = $true
    $psi.RedirectStandardError = $true
    $psi.UseShellExecute = $false
    $psi.CreateNoWindow = $true

    $proc = New-Object System.Diagnostics.Process
    $proc.StartInfo = $psi
    [void]$proc.Start()

    $finished = $proc.WaitForExit(25000)
    if (-not $finished) {
        try { $proc.Kill() } catch {}
        return @{ Code = -1; Output = "docker info timeout (engine likely stuck)" }
    }

    $output = $proc.StandardOutput.ReadToEnd() + $proc.StandardError.ReadToEnd()
    return @{ Code = $proc.ExitCode; Output = $output }
}

function Get-DockerEngineStatus {
    $info = Invoke-DockerInfoSafe
    $output = $info.Output
    $code = $info.Code

    if ($code -eq 0 -and $output -match "(?m)^Server:") {
        return "ok"
    }
    if ($output -match "500|Internal Server Error|request returned 500|engine likely stuck") {
        return "engine_500"
    }
    if ($output -match "pipe|cannot find the file|connection refused|not running") {
        return "not_running"
    }
    return "error"
}

function Get-WslInfraScriptPath {
    param([string]$Root)
    $drive = $Root.Substring(0, 1).ToLower()
    $rest = $Root.Substring(2) -replace '\\', '/'
    return "/mnt/$drive$rest/scripts/dev/wsl-infra.sh"
}

function Start-WslInfra {
    param([string]$Root)
    $scriptPath = Get-WslInfraScriptPath -Root $Root

    $code = Invoke-DevWsl -BashCommand "bash '$scriptPath' status"
    if ($code -ne 0) {
        Write-DevLog "starting mysql+redis in WSL (no Docker Desktop)..." "Warn"
        $code = Invoke-DevWsl -BashCommand "export MYSQL_HOST_PORT=3307 MYSQL_ROOT_PASSWORD=changeme; bash '$scriptPath' start"
    }

    if ($code -eq 0) {
        $ep = Get-EnvDbEndpoints -Root $Root
        $infraHost = Resolve-InfraHost -Root $Root -MysqlPort $ep.MysqlPort -RedisPort $ep.RedisPort
        $redisUrl = "redis://${infraHost}:$($ep.RedisPort)"
        if (Wait-InfraReadyFromWindows -HostName $infraHost -MysqlPort $ep.MysqlPort -RedisPort $ep.RedisPort -RedisUrl $redisUrl) {
            Write-DevLog "wsl mysql:3307 + redis:6379 ready" "Ok"
        }
        else {
            Write-DevLog "wsl infra started; Windows probe still waiting (will retry)" "Warn"
        }
        $distro = $env:WSL_DISTRO
        if (-not $distro) { $distro = "Ubuntu-22.04" }
        Start-WslKeepAlive -Distro $distro
    }
    return ($code -eq 0)
}

function Repair-DockerEngine {
    param([int]$MaxAttempts = 1)

    Write-DevLog "docker repair: quit desktop + wsl shutdown + restart (NO SwitchDaemon)" "Warn"

    $desktopExe = Join-Path ${env:ProgramFiles} "Docker\Docker\Docker Desktop.exe"

    if (Test-Path $desktopExe) {
        $prev = $ErrorActionPreference
        $ErrorActionPreference = "SilentlyContinue"
        Start-Process -FilePath $desktopExe -ArgumentList "--quit" -ErrorAction SilentlyContinue | Out-Null
        $ErrorActionPreference = $prev
        Start-Sleep -Seconds 8
    }

    @(
        "Docker Desktop",
        "com.docker.backend",
        "com.docker.proxy",
        "vpnkit",
        "wslrelay"
    ) | ForEach-Object {
        Get-Process -Name $_ -ErrorAction SilentlyContinue |
            Stop-Process -Force -ErrorAction SilentlyContinue
    }

    $prev = $ErrorActionPreference
    $ErrorActionPreference = "SilentlyContinue"
    wsl --shutdown 2>&1 | Out-Null
    $ErrorActionPreference = $prev
    Start-Sleep -Seconds 8

    if (Test-Path $desktopExe) {
        Start-Process -FilePath $desktopExe | Out-Null
    }

    $deadline = (Get-Date).AddSeconds(120)
    while ((Get-Date) -lt $deadline) {
        Start-Sleep -Seconds 5
        if ((Get-DockerEngineStatus) -eq "ok") {
            Write-DevLog "docker engine recovered" "Ok"
            return $true
        }
    }

    Write-DevLog "docker repair failed - use WSL infra instead: npm run dev:infra:setup" "Err"
    return $false
}

function Wait-DockerDaemonQuick {
    param([int]$TimeoutSec = 25)

    $deadline = (Get-Date).AddSeconds($TimeoutSec)
    while ((Get-Date) -lt $deadline) {
        if ((Get-DockerEngineStatus) -eq "ok") {
            Write-DevLog "docker daemon ready" "Ok"
            return $true
        }
        Start-Sleep -Seconds 3
    }
    Write-DevLog "docker not ready in ${TimeoutSec}s (skip repair on start)" "Warn"
    return $false
}

function Wait-DockerHealthy {
    param(
        [string[]]$Services,
        [int]$TimeoutSec = 90
    )
    $deadline = (Get-Date).AddSeconds($TimeoutSec)
    while ((Get-Date) -lt $deadline) {
        if (Test-DockerServicesHealthy -Services $Services) {
            Write-DevLog "docker healthy: $($Services -join ', ')" "Ok"
            return $true
        }
        Start-Sleep -Seconds 2
    }
    throw "docker not healthy within ${TimeoutSec}s"
}

function Start-DockerInfra {
    param([string]$Root)

    Push-Location $Root
    try {
        if (-not (Wait-DockerDaemonQuick -TimeoutSec $script:DevConfig.DockerDaemonWaitSec)) {
            return $false
        }

        if (Test-DockerServicesHealthy -Services $script:DevConfig.DockerServices) {
            Write-DevLog "docker mysql/redis already healthy" "Ok"
            return $true
        }

        Write-DevLog "docker compose up mysql redis (dev profile)..."
        $code = Invoke-DockerComposeProject up -d mysql redis
        if ($code -ne 0) {
            return $false
        }

        Wait-DockerHealthy -Services $script:DevConfig.DockerServices -TimeoutSec $script:DevConfig.DockerWaitSec
        return $true
    }
    finally {
        Pop-Location
    }
}

function Test-RedisPingFromNode {
    param([string]$RedisUrl)
    if (-not $RedisUrl) { return $false }
    $escaped = $RedisUrl.Replace('"', '\"')
    $nodeScript = @"
const Redis=require('ioredis');
(async()=>{
  const r=new Redis('$escaped',{lazyConnect:true,connectTimeout:1500,maxRetriesPerRequest:1,retryStrategy:()=>null});
  try{
    await r.connect();
    const pong=await r.ping();
    await r.quit();
    process.exit(pong==='PONG'?0:1);
  }catch{
    await r.quit().catch(()=>{});
    process.exit(1);
  }
})();
"@
    $prev = $ErrorActionPreference
    $ErrorActionPreference = "SilentlyContinue"
    $null = node -e $nodeScript 2>&1
    $ok = ($LASTEXITCODE -eq 0)
    $ErrorActionPreference = $prev
    return $ok
}

function Wait-InfraReadyFromWindows {
    param(
        [string]$HostName,
        [int]$MysqlPort,
        [int]$RedisPort,
        [string]$RedisUrl,
        [int]$TimeoutSec = 30
    )
    $deadline = (Get-Date).AddSeconds($TimeoutSec)
    $attempt = 0
    Wake-WslRedisForward
    while ((Get-Date) -lt $deadline) {
        $attempt += 1
        $mysqlOk = Test-TcpPortOpen -HostName $HostName -Port $MysqlPort -TimeoutMs 800
        $redisOk = Test-RedisPingFromNode -RedisUrl $RedisUrl
        if ($mysqlOk -and $redisOk) {
            return $true
        }
        if ($attempt -eq 1 -or ($attempt % 2) -eq 0) {
            Wake-WslRedisForward
        }
        $sleepMs = if ($attempt -le 6) { 350 } elseif ($attempt -le 12) { 700 } else { 1200 }
        Start-Sleep -Milliseconds $sleepMs
    }
    return $false
}

function Ensure-DevInfra {
    param(
        [string]$Root,
        [string]$Mode = "auto"
    )

    $mode = $Mode.ToLower()
    if ($mode -eq "auto") {
        $mode = Get-DevInfraModeFromEnv -Root $Root
    }

    Write-DevLog "infra mode: $mode"

    if ($mode -eq "native") {
        if (Test-NativeInfraReady -Root $Root) {
            Apply-DevInfraEnv -Root $Root
            $distro = $env:WSL_DISTRO
            if (-not $distro) { $distro = "Ubuntu-22.04" }
            Start-WslKeepAlive -Distro $distro
            Write-DevLog "using native mysql/redis" "Ok"
            return "native"
        }
        Write-DevLog "native ports not ready, trying WSL mysql/redis..." "Warn"
        if (Start-WslInfra -Root $Root) {
            if (Test-NativeInfraReady -Root $Root) {
                Apply-DevInfraEnv -Root $Root
                Write-DevLog "using wsl mysql/redis" "Ok"
                return "wsl"
            }
        }
        $ports = Test-NativeInfraPorts -Root $Root
        throw @"
native mode: mysql/redis not ready.
  mysql $($ports.Endpoints.MysqlPort): $(if ($ports.Mysql) { 'open' } else { 'closed' })
  redis $($ports.Endpoints.RedisPort): $(if ($ports.Redis) { 'open' } else { 'closed' })

Run once: npm run dev:infra:setup
Then: npm run dev:start
"@
    }

    if ($mode -eq "docker") {
        if (-not (Start-DockerInfra -Root $Root)) {
            throw "docker mode: failed to start mysql/redis. Run: npm run dev:repair-docker"
        }
        Write-DevLog "using docker mysql/redis" "Ok"
        return "docker"
    }

    # auto: docker quick try -> wsl infra -> native ports
    if (Start-DockerInfra -Root $Root) {
        Write-DevLog "using docker mysql/redis" "Ok"
        return "docker"
    }

    Write-DevLog "docker unavailable, trying WSL mysql/redis..." "Warn"
    if (Start-WslInfra -Root $Root) {
        if (Test-NativeInfraReady -Root $Root) {
            Apply-DevInfraEnv -Root $Root
            Write-DevLog "using wsl mysql/redis" "Ok"
            return "wsl"
        }
    }

    Write-DevLog "wsl infra failed, trying native ports..." "Warn"
    if (Test-NativeInfraReady -Root $Root) {
        Apply-DevInfraEnv -Root $Root
        Write-DevLog "using native mysql/redis (fallback)" "Ok"
        return "native"
    }

    $ports = Test-NativeInfraPorts -Root $Root
    throw @"
Cannot start dev infrastructure.

Root cause: Docker Desktop WSL engine IPC deadlock (engine 500 / context deadline exceeded).
Do NOT use DockerCli -SwitchDaemon - it makes this worse.

Recommended permanent fix:
  1. npm run dev:infra:setup     # install mysql+redis in WSL (no Docker Desktop)
  2. Set DEV_INFRA_MODE=native in .env
  3. npm run dev:start

Or diagnose Docker: npm run dev:diagnose-docker

Current ports:
  mysql $($ports.Endpoints.MysqlPort): $(if ($ports.Mysql) { 'open' } else { 'closed' })
  redis $($ports.Endpoints.RedisPort): $(if ($ports.Redis) { 'open' } else { 'closed' })
"@
}
