// =====================================================================
// BITFORUM Backend Jenkinsfile (IIS In-Process) — antooza 환경(CTL / 192.168.0.201) 배포
//   - Solution:    Backend.slnx (.NET 10)
//   - Components:   Web.Api (IIS bitforum-api), Admin (IIS bitforum-admin)
//                   ※ MailWorker 없음 — bitforum 은 IMailService 로 인라인 발송 (별도 워커 미포팅)
//   - Agent:        CTL-Window-Server (antooza 와 동일한 Jenkins inbound agent, VM100 / .201)
//   - Branches:     main / dev  -> dev 환경 배포
//                   others       -> build/test only
//   - Selective:    AUTO_DETECT (default) via git diff, or manual checkboxes
//   - Deploy model: IIS In-Process AppPool + 백업/롤백 (antooza Jenkinsfile 패턴 그대로)
// =====================================================================

pipeline {
  agent { label 'CTL-Window-Server' }

  options {
    timestamps()
    disableConcurrentBuilds()
    buildDiscarder(logRotator(numToKeepStr: '20'))
  }

  parameters {
    booleanParam(name: 'AUTO_DETECT', defaultValue: true,
        description: 'true: git diff로 변경된 컴포넌트만 자동 배포 (아래 체크박스 무시)')
    booleanParam(name: 'DEPLOY_API',   defaultValue: true)
    booleanParam(name: 'DEPLOY_ADMIN', defaultValue: true)
  }

  environment {
    DOTNET_CLI_TELEMETRY_OPTOUT = '1'
    DOTNET_NOLOGO               = '1'
    JAVA_TOOL_OPTIONS           = '-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8'

    // Deployment roots (CTL-Window-Server) — 서버 관례 C:\workspace\IIS\<site>
    IIS_ROOT     = 'C:\\workspace\\IIS'
    IIS_RELEASES = 'C:\\workspace\\IIS\\_releases'

    // Project paths (relative to Backend/ working dir on agent)
    SLN_FILE   = 'Backend.slnx'
    API_PROJ   = 'Web.Api\\Web.Api.csproj'
    ADMIN_PROJ = 'Admin\\Admin.csproj'

    // EF Core
    EF_CTX_PROJ   = 'Infrastructure'
    EF_START_PROJ = 'Admin'

    // Change detection paths
    SHARED_PATHS = 'Application/,Domain/,Infrastructure/,SharedKernel/,Directory.Build.props,Directory.Packages.props,global.json,Backend.slnx'
    API_PATHS    = 'Web.Api/'
    ADMIN_PATHS  = 'Admin/'

    // Health check (loopback + Host header to hit the right IIS site binding)
    API_HEALTH_HOST = 'api.bitforum.io'
  }

  stages {

    stage('Locate workdir') {
      steps {
        script {
          // Multibranch checkout drops repo root. Solution lives in Backend/.
          if (fileExists('Backend\\Backend.slnx')) {
            env.WORK_SUBDIR = 'Backend'
          } else if (fileExists('Backend.slnx')) {
            env.WORK_SUBDIR = '.'
          } else {
            error 'Cannot locate Backend.slnx in checkout.'
          }
          echo "Working subdirectory = '${env.WORK_SUBDIR}'"
        }
      }
    }

    stage('Detect changes') {
      steps {
        script {
          def doApi   = params.DEPLOY_API
          def doAdmin = params.DEPLOY_ADMIN

          if (params.AUTO_DETECT) {
            def lastSha = env.GIT_PREVIOUS_SUCCESSFUL_COMMIT
            if (!lastSha) {
              echo 'No previous successful build — deploying all components.'
              doApi = doAdmin = true
            } else {
              def diffOut = bat(returnStdout: true,
                  script: "@git diff --name-only ${lastSha} HEAD 2>nul").trim()
              if (!diffOut) {
                echo "No diff since last successful build (${lastSha}) — nothing to deploy."
                doApi = doAdmin = false
              } else {
                echo "Changed files since ${lastSha}:\n${diffOut}"
                def files = diffOut.split('\r?\n').collect { it.replace('\\', '/') }
                if (env.WORK_SUBDIR == 'Backend') {
                  files = files.findAll { it.startsWith('Backend/') }
                              .collect { it.substring('Backend/'.length()) }
                }

                def matchAny = { String csv ->
                  def patterns = csv.split(',').collect { it.trim() }
                  files.any { f -> patterns.any { p -> f.startsWith(p) || f == p } }
                }

                def sharedChanged = matchAny(env.SHARED_PATHS)
                doApi   = sharedChanged || matchAny(env.API_PATHS)
                doAdmin = sharedChanged || matchAny(env.ADMIN_PATHS)
              }
            }
          }

          env.DO_API   = doApi.toString()
          env.DO_ADMIN = doAdmin.toString()
          echo "Deploy plan -> API:${doApi}  ADMIN:${doAdmin}"

          if (!doApi && !doAdmin) {
            currentBuild.result = 'NOT_BUILT'
            error('No components to deploy.')
          }
        }
      }
    }

    stage('Clean') {
      steps {
        dir(env.WORK_SUBDIR) {
          bat 'if exist publish rmdir /s /q publish'
        }
      }
    }

    stage('Restore') {
      steps {
        dir(env.WORK_SUBDIR) {
          bat 'chcp 65001 >NUL && dotnet restore %SLN_FILE%'
        }
      }
    }

    stage('Build') {
      steps {
        dir(env.WORK_SUBDIR) {
          bat 'chcp 65001 >NUL && dotnet build %SLN_FILE% -c Release --no-restore'
        }
      }
    }

    stage('Publish') {
      parallel {
        stage('api') {
          when { expression { env.DO_API == 'true' } }
          steps {
            dir(env.WORK_SUBDIR) {
              bat 'chcp 65001 >NUL && dotnet publish %API_PROJ% -c Release -o publish\\api --no-build'
            }
          }
        }
        stage('admin') {
          when { expression { env.DO_ADMIN == 'true' } }
          steps {
            dir(env.WORK_SUBDIR) {
              bat 'chcp 65001 >NUL && dotnet publish %ADMIN_PROJ% -c Release -o publish\\admin --no-build'
            }
          }
        }
      }
    }

    stage('DB Migration') {
      when {
        expression { (!env.BRANCH_NAME || env.BRANCH_NAME in ['main', 'dev']) && (env.DO_API == 'true' || env.DO_ADMIN == 'true') }
      }
      steps {
        dir(env.WORK_SUBDIR) {
          withCredentials([string(credentialsId: 'bitforum-db-connection-dev', variable: 'CONN')]) {
            bat 'chcp 65001 >NUL && dotnet tool restore --tool-manifest dotnet-tools.json'
            withEnv([
              "ConnectionStrings__DefaultConnection=${CONN}",
              "ConnectionStrings__IdentityDbContextConnection=${CONN}"
            ]) {
              // EF Core CLI: `-c` = `--context` (NOT --configuration). --configuration Release 명시.
              bat "chcp 65001 >NUL && dotnet ef database update --project ${EF_CTX_PROJ} --startup-project ${EF_START_PROJ} --context AppDbContext --configuration Release --no-build"
              bat "chcp 65001 >NUL && dotnet ef database update --project ${EF_CTX_PROJ} --startup-project ${EF_START_PROJ} --context IdentityDbContext --configuration Release --no-build"
            }
          }
        }
      }
    }

    stage('Inject Config') {
      when { expression { !env.BRANCH_NAME || env.BRANCH_NAME in ['main', 'dev'] } }
      steps {
        dir(env.WORK_SUBDIR) {
          script {
            // ⚠️ Secret File 은 committed appsettings.Production.json 을 통째로 대체함.
            //    Encryption:Keys / ConnectionStrings / Redis / Jwt 등 필수 섹션을 secret 에 모두 포함할 것.
            if (env.DO_API == 'true') {
              withCredentials([file(credentialsId: 'DEV-BITFORUM-API', variable: 'CFG')]) {
                bat 'copy /Y "%CFG%" publish\\api\\appsettings.Production.json'
              }
            }
            if (env.DO_ADMIN == 'true') {
              withCredentials([file(credentialsId: 'DEV-BITFORUM-ADMIN', variable: 'CFG')]) {
                bat 'copy /Y "%CFG%" publish\\admin\\appsettings.Production.json'
              }
            }
          }
        }
      }
    }

    stage('Deploy') {
      when { expression { !env.BRANCH_NAME || env.BRANCH_NAME in ['main', 'dev'] } }
      parallel {
        stage('api') {
          when { expression { env.DO_API == 'true' } }
          steps {
            dir(env.WORK_SUBDIR) {
              script { deployIIS('bitforum-api', 'bitforum-api', 'api', 'bitforum-api', env.API_HEALTH_HOST, true) }
            }
          }
        }
        stage('admin') {
          when { expression { env.DO_ADMIN == 'true' } }
          steps {
            dir(env.WORK_SUBDIR) {
              // Admin 은 /health 엔드포인트가 없어서 AppPool Started 만 검증
              script { deployIIS('bitforum-admin', 'bitforum-admin', 'admin', 'bitforum-admin', null, false) }
            }
          }
        }
      }
    }
  }

  post {
    success { echo "Build & deploy completed (branch=${env.BRANCH_NAME ?: 'unknown'})" }
    failure { echo 'Build failed - check stage logs' }
  }
}

// ==== Helpers =================================================================

// IIS In-Process 배포.
//   siteName    : Jenkins 로그용 라벨 (예: bitforum-api)
//   appPoolName : IIS AppPool 이름 (예: bitforum-api) — Stop-WebAppPool / Start-WebAppPool 대상
//   srcSubdir   : publish 하위 디렉토리 (예: api, admin) — `publish\<srcSubdir>` 에서 복사
//   destFolder  : C:\workspace\IIS\<destFolder> (bitforum-api, bitforum-admin)
//   healthHost  : Host 헤더로 보낼 도메인 (null이면 health 생략)
//   checkHealth : true면 /health 200 확인, false면 AppPool Started 만 확인
def deployIIS(siteName, appPoolName, srcSubdir, destFolder, healthHost, checkHealth) {
  powershell """
    \$ErrorActionPreference = 'Stop'
    Import-Module WebAdministration

    \$ts     = Get-Date -Format 'yyyyMMdd-HHmmss'
    \$dest   = Join-Path '${env.IIS_ROOT}' '${destFolder}'
    \$backup = Join-Path '${env.IIS_RELEASES}' ('${siteName}-' + \$ts)

    if (-not (Test-Path '${env.IIS_RELEASES}')) { New-Item -ItemType Directory -Path '${env.IIS_RELEASES}' -Force | Out-Null }

    Write-Host "[${siteName}] Stopping AppPool '${appPoolName}'..."
    if ((Get-WebAppPoolState -Name '${appPoolName}').Value -eq 'Started') {
      Stop-WebAppPool -Name '${appPoolName}'
      \$t = 30
      while ((Get-WebAppPoolState -Name '${appPoolName}').Value -ne 'Stopped' -and \$t -gt 0) {
        Start-Sleep -Seconds 1; \$t--
      }
      if ((Get-WebAppPoolState -Name '${appPoolName}').Value -ne 'Stopped') {
        throw '[${siteName}] AppPool did not stop within 30s'
      }
    }
    Start-Sleep -Seconds 2

    if ((Test-Path \$dest) -and ((Get-ChildItem \$dest -Force | Measure-Object).Count -gt 0)) {
      Write-Host "[${siteName}] Backing up current to \$backup"
      New-Item -ItemType Directory -Path \$backup -Force | Out-Null
      Move-Item (Join-Path \$dest '*') \$backup -Force
    } elseif (-not (Test-Path \$dest)) {
      New-Item -ItemType Directory -Path \$dest -Force | Out-Null
    }

    Write-Host "[${siteName}] Copying new artifacts..."
    Copy-Item ("publish\\${srcSubdir}\\*") \$dest -Recurse -Force

    Write-Host "[${siteName}] Starting AppPool..."
    Start-WebAppPool -Name '${appPoolName}'

    \$t = 15
    while ((Get-WebAppPoolState -Name '${appPoolName}').Value -ne 'Started' -and \$t -gt 0) {
      Start-Sleep -Seconds 1; \$t--
    }

    \$ok = \$false
    if ('${checkHealth}' -eq 'true') {
      1..10 | ForEach-Object {
        Start-Sleep -Seconds 3
        try {
          \$r = Invoke-WebRequest -Uri 'http://127.0.0.1/health' -Headers @{ Host = '${healthHost}' } -UseBasicParsing -TimeoutSec 5
          if (\$r.StatusCode -eq 200) {
            Write-Host "[${siteName}] Health OK on attempt \$_"
            \$ok = \$true; return
          }
        } catch {
          Write-Host "[${siteName}] Health attempt \$_ failed: \$(\$_.Exception.Message)"
        }
      }
    } else {
      1..3 | ForEach-Object {
        Start-Sleep -Seconds 5
        \$state = (Get-WebAppPoolState -Name '${appPoolName}').Value
        Write-Host "[${siteName}] AppPool state: \$state"
        if (\$state -ne 'Started') { \$ok = \$false; return }
        \$ok = \$true
      }
    }

    if (-not \$ok) {
      Write-Warning "[${siteName}] Verification failed - rolling back"
      Stop-WebAppPool -Name '${appPoolName}' -ErrorAction SilentlyContinue
      Start-Sleep -Seconds 2
      if (Test-Path \$backup) {
        Get-ChildItem \$dest -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
        Copy-Item (Join-Path \$backup '*') \$dest -Recurse -Force
        Start-WebAppPool -Name '${appPoolName}'
      }
      throw '[${siteName}] deployment failed; rolled back.'
    }

    # 백업 5개만 보존
    Get-ChildItem '${env.IIS_RELEASES}' -Directory -ErrorAction SilentlyContinue |
      Where-Object { \$_.Name -like '${siteName}-*' } |
      Sort-Object LastWriteTime -Descending |
      Select-Object -Skip 5 |
      Remove-Item -Recurse -Force
  """
}
