# Production: build images for https://nudentic.ir, push :v* to Gitea (not :latest),
# SCP compose to Linux, docker login wixur.ir:3000, compose pull + up.
#
# Triggers: git tag v1.0.1 (and v*.*.*), or Actions → this workflow → Run (input tag).
# Staging stays on master: .gitea/workflows/registry-build-deploy.yml
#
# Do NOT push :latest — staging already uses :latest with the wixur.ir frontend bake.
#
# Variables:
#   REGISTRY_HOST          Windows Docker push host, e.g. host.docker.internal:3000
#                          (not gitea.wixur.ir unless nginx client_max_body_size 0)
#   REGISTRY_OWNER         Gitea user/org for packages
#   CLONE_HOST             git clone, e.g. 127.0.0.1:3000
#   PROD_PUBLIC_BASE_URL   https://nudentic.ir  (no trailing slash)
#   NEXT_PUBLIC_SENTRY_DSN  GlitchTip frontend project DSN
#   PROD_REGISTRY_HOST     Linux pull host, e.g. wixur.ir:3000  (HTTP; insecure-registries on the VPS)
#   PROD_INFRA_DIR         /opt/dyolink/infrastructure
#
# Secrets (in addition to REGISTRY_USERNAME / REGISTRY_PASSWORD):
#   PROD_SSH_HOST          Linux public IP or hostname
#   PROD_SSH_USER          e.g. dyolink
#   PROD_SSH_KEY           OpenSSH private key (full PEM)
#   PROD_SSH_PORT          optional, default 22
#
# One-time on Linux: insecure-registries ["wixur.ir:3000"], .env REGISTRY_PREFIX + DOMAIN=nudentic.ir
# See infrastructure/DEPLOY.md

name: Production — tag build, push, deploy

on:
  push:
    tags:
      - 'v*.*.*'
  workflow_dispatch:
    inputs:
      tag:
        description: 'Release tag to build and deploy (e.g. v1.0.1)'
        required: true

defaults:
  run:
    shell: powershell

jobs:
  build-and-push:
    runs-on: windows
    steps:
      - name: Checkout (this Gitea)
        run: |
          $ErrorActionPreference = 'Stop'
          $cloneHost = '${{ vars.CLONE_HOST }}'.Trim()
          if ([string]::IsNullOrWhiteSpace($cloneHost)) {
            $Server = "${{ github.server_url }}".TrimEnd('/')
          } elseif ($cloneHost -match '^https?://') {
            $Server = $cloneHost.TrimEnd('/')
          } else {
            $Server = 'http://' + $cloneHost
          }
          $Repo = "${{ github.repository }}"
          $dispatchTag = '${{ github.event.inputs.tag }}'.Trim()
          if (-not [string]::IsNullOrWhiteSpace($dispatchTag)) {
            $Branch = $dispatchTag
          } else {
            $Branch = "${{ github.ref_name }}"
          }
          $Token = "${{ github.token }}"
          $Actor = "${{ github.actor }}"
          $hp = $Server -replace '^https?://', ''
          if ($Server.StartsWith('https')) {
            $cloneUrl = 'https://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git'
          } else {
            $cloneUrl = 'http://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git'
          }
          $env:GIT_TERMINAL_PROMPT = '0'
          git clone --depth 1 --branch $Branch $cloneUrl .
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Resolve image tag (v*.*.* only)
        id: meta
        run: |
          $ErrorActionPreference = 'Stop'
          $dispatchTag = '${{ github.event.inputs.tag }}'.Trim()
          if (-not [string]::IsNullOrWhiteSpace($dispatchTag)) {
            $tag = $dispatchTag
          } else {
            $tag = "${{ github.ref_name }}"
          }
          if ($tag -notmatch '^v\d+\.\d+\.\d+') {
            Write-Host "Production images must be tagged vMAJOR.MINOR.PATCH (got: $tag)"
            exit 1
          }
          $prefix = "${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}"
          Write-Host "image_tag=$tag REGISTRY_PREFIX=$prefix"

      - name: Log in to container registry
        run: |
          $ErrorActionPreference = 'Stop'
          $pass = @'
          ${{ secrets.REGISTRY_PASSWORD }}
          '@
          $pass.Trim() | docker login "${{ vars.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Resolve node:20-alpine (Gitea, then mirrors, Hub last)
        run: |
          $ErrorActionPreference = 'Stop'
          $prefix = "${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}".Trim()
          $extra = '${{ vars.NODE_IMAGE_SOURCE }}'.Trim()
          if ($extra -like '*NODE_IMAGE_SOURCE*') { $extra = '' }
          # Do not nest powershell -File: empty -ExtraSources "$extra" is dropped and PS5.1 errors MissingArgument.
          $scriptArgs = @{ RegistryPrefix = $prefix }
          if (-not [string]::IsNullOrWhiteSpace($extra)) { $scriptArgs['ExtraSources'] = $extra }
          & .\infrastructure\scripts\ci-resolve-node-image.ps1 @scriptArgs
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Build and push backend (tag only, not :latest)
        run: |
          $ErrorActionPreference = 'Stop'
          $dispatchTag = '${{ github.event.inputs.tag }}'.Trim()
          if (-not [string]::IsNullOrWhiteSpace($dispatchTag)) { $tag = $dispatchTag } else { $tag = "${{ github.ref_name }}" }
          $prefix = "${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}".Trim()
          $nodeImage = ([System.IO.File]::ReadAllText((Join-Path (Get-Location) '.ci-node-image'))).Trim()
          if ([string]::IsNullOrWhiteSpace($nodeImage)) {
            Write-Host "Missing .ci-node-image"
            exit 1
          }
          if (Test-Path '.ci-use-legacy-builder') { $env:DOCKER_BUILDKIT = '0' }
          Write-Host "Building $prefix/dyolink-backend:$tag (NODE_IMAGE=$nodeImage)"
          $ok = $false
          for ($i = 1; $i -le 3; $i++) {
            Write-Host "docker build attempt $i/3"
            docker build --build-arg "NODE_IMAGE=$nodeImage" -t "$prefix/dyolink-backend:$tag" ./backend
            if ($LASTEXITCODE -eq 0) { $ok = $true; break }
            if ($i -lt 3) { Start-Sleep -Seconds (20 * $i) }
          }
          if (-not $ok) { exit 1 }
          docker push "$prefix/dyolink-backend:$tag"
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Build and push frontend (nudentic.ir baked in)
        env:
          PROD_PUBLIC_BASE_URL: ${{ vars.PROD_PUBLIC_BASE_URL }}
          NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
        run: |
          $ErrorActionPreference = 'Stop'
          $dispatchTag = '${{ github.event.inputs.tag }}'.Trim()
          if (-not [string]::IsNullOrWhiteSpace($dispatchTag)) { $tag = $dispatchTag } else { $tag = "${{ github.ref_name }}" }
          $prefix = "${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}".Trim()
          $nodeImage = ([System.IO.File]::ReadAllText((Join-Path (Get-Location) '.ci-node-image'))).Trim()
          if ([string]::IsNullOrWhiteSpace($nodeImage)) {
            Write-Host "Missing .ci-node-image"
            exit 1
          }
          if (Test-Path '.ci-use-legacy-builder') { $env:DOCKER_BUILDKIT = '0' }
          $base = $env:PROD_PUBLIC_BASE_URL.Trim()
          if ([string]::IsNullOrWhiteSpace($base)) { $base = 'https://nudentic.ir' }
          $base = $base.TrimEnd('/')
          Write-Host "Building $prefix/dyolink-frontend:$tag (NODE_IMAGE=$nodeImage)"
          $ok = $false
          for ($i = 1; $i -le 3; $i++) {
            Write-Host "docker build attempt $i/3"
            docker build `
              --build-arg "NEXT_PUBLIC_API_URL=$base/api" `
              --build-arg "NEXT_PUBLIC_APP_URL=$base" `
              --build-arg "NEXT_PUBLIC_APP_NAME=Dyolink" `
              --build-arg "NEXT_PUBLIC_SENTRY_DSN=$env:NEXT_PUBLIC_SENTRY_DSN" `
              --build-arg "NEXT_PUBLIC_SENTRY_ENVIRONMENT=production" `
              --build-arg "NODE_IMAGE=$nodeImage" `
              -t "$prefix/dyolink-frontend:$tag" `
              ./frontend
            if ($LASTEXITCODE -eq 0) { $ok = $true; break }
            if ($i -lt 3) { Start-Sleep -Seconds (20 * $i) }
          }
          if (-not $ok) { exit 1 }
          docker push "$prefix/dyolink-frontend:$tag"
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

  deploy:
    needs: build-and-push
    runs-on: windows
    steps:
      - name: Checkout (tagged tree for compose file)
        run: |
          $ErrorActionPreference = 'Stop'
          $cloneHost = '${{ vars.CLONE_HOST }}'.Trim()
          if ([string]::IsNullOrWhiteSpace($cloneHost)) {
            $Server = "${{ github.server_url }}".TrimEnd('/')
          } elseif ($cloneHost -match '^https?://') {
            $Server = $cloneHost.TrimEnd('/')
          } else {
            $Server = 'http://' + $cloneHost
          }
          $Repo = "${{ github.repository }}"
          $dispatchTag = '${{ github.event.inputs.tag }}'.Trim()
          if (-not [string]::IsNullOrWhiteSpace($dispatchTag)) {
            $Branch = $dispatchTag
          } else {
            $Branch = "${{ github.ref_name }}"
          }
          $Token = "${{ github.token }}"
          $Actor = "${{ github.actor }}"
          $hp = $Server -replace '^https?://', ''
          if ($Server.StartsWith('https')) {
            $cloneUrl = 'https://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git'
          } else {
            $cloneUrl = 'http://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git'
          }
          $env:GIT_TERMINAL_PROMPT = '0'
          git clone --depth 1 --branch $Branch $cloneUrl .
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Write SSH key
        run: |
          $ErrorActionPreference = 'Stop'
          $raw = @'
          ${{ secrets.PROD_SSH_KEY }}
          '@
          $key = $raw.Trim()
          if ($key -notmatch 'BEGIN') {
            Write-Host "Set secret PROD_SSH_KEY to an OpenSSH private key"
            exit 1
          }
          $keyPath = Join-Path $env:RUNNER_TEMP 'prod_ssh'
          $normalized = $key.Replace("`r`n", "`n").TrimEnd() + "`n"
          [System.IO.File]::WriteAllText($keyPath, $normalized)
          icacls $keyPath /inheritance:r | Out-Null
          icacls $keyPath /grant:r "$($env:USERNAME):(R)" | Out-Null
          [System.IO.File]::AppendAllText($env:GITHUB_ENV, "PROD_SSH_KEY_PATH=$keyPath`n")

      - name: Copy compose + deploy script to Linux
        run: |
          $ErrorActionPreference = 'Stop'
          $hostName = '${{ secrets.PROD_SSH_HOST }}'.Trim()
          $user = '${{ secrets.PROD_SSH_USER }}'.Trim()
          $port = '${{ secrets.PROD_SSH_PORT }}'.Trim()
          if ([string]::IsNullOrWhiteSpace($port)) { $port = '22' }
          $infra = '${{ vars.PROD_INFRA_DIR }}'.Trim()
          if ([string]::IsNullOrWhiteSpace($infra)) { $infra = '/opt/dyolink/infrastructure' }
          $ssh = @('-i', $env:PROD_SSH_KEY_PATH, '-o', 'StrictHostKeyChecking=accept-new')
          ssh.exe @ssh -p $port "${user}@${hostName}" "mkdir -p $infra/scripts $infra/nginx"
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
          scp.exe @ssh -P $port `
            infrastructure/docker-compose.prod.yml `
            "${user}@${hostName}:${infra}/docker-compose.prod.yml"
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
          scp.exe @ssh -P $port `
            infrastructure/scripts/prod-remote-deploy.sh `
            "${user}@${hostName}:${infra}/scripts/prod-remote-deploy.sh"
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
          scp.exe @ssh -P $port `
            infrastructure/scripts/render-nginx-ssl.sh `
            "${user}@${hostName}:${infra}/scripts/render-nginx-ssl.sh"
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
          scp.exe @ssh -P $port `
            infrastructure/nginx/nginx.ssl.conf.template `
            "${user}@${hostName}:${infra}/nginx/nginx.ssl.conf.template"
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
          ssh.exe @ssh -p $port "${user}@${hostName}" "chmod +x $infra/scripts/prod-remote-deploy.sh $infra/scripts/render-nginx-ssl.sh"
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Login on Linux and deploy tag
        run: |
          $ErrorActionPreference = 'Stop'
          $hostName = '${{ secrets.PROD_SSH_HOST }}'.Trim()
          $user = '${{ secrets.PROD_SSH_USER }}'.Trim()
          $port = '${{ secrets.PROD_SSH_PORT }}'.Trim()
          if ([string]::IsNullOrWhiteSpace($port)) { $port = '22' }
          $infra = '${{ vars.PROD_INFRA_DIR }}'.Trim()
          if ([string]::IsNullOrWhiteSpace($infra)) { $infra = '/opt/dyolink/infrastructure' }
          $regHost = '${{ vars.PROD_REGISTRY_HOST }}'.Trim()
          if ([string]::IsNullOrWhiteSpace($regHost)) { $regHost = 'wixur.ir:3000' }
          $dispatchTag = '${{ github.event.inputs.tag }}'.Trim()
          if (-not [string]::IsNullOrWhiteSpace($dispatchTag)) { $tag = $dispatchTag } else { $tag = "${{ github.ref_name }}" }
          $pass = @'
          ${{ secrets.REGISTRY_PASSWORD }}
          '@
          $regUser = '${{ secrets.REGISTRY_USERNAME }}'
          $ssh = @('-i', $env:PROD_SSH_KEY_PATH, '-o', 'StrictHostKeyChecking=accept-new')
          $remote = "docker login $regHost -u $regUser --password-stdin && PROD_INFRA_DIR=$infra $infra/scripts/prod-remote-deploy.sh $tag"
          $pass.Trim() | ssh.exe @ssh -p $port "${user}@${hostName}" $remote
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
