name: TaxBaik CI/CD

on:
  workflow_dispatch:
  push:
    branches:
      - master

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0'

      - name: Restore dependencies
        run: dotnet restore src/TaxBaik.sln

      - name: Build solution
        run: dotnet build src/TaxBaik.sln -c Release --no-restore -p:ContinuousIntegrationBuild=true

      - name: Test solution
        run: dotnet test src/TaxBaik.sln -c Release --no-build

      - name: Publish Web (auto-includes WASM from referenced TaxBaik.Web.Client)
        run: |
          set -e
          mkdir -p ./publish-logs
          web_log="./publish-logs/publish-web.log"
          start=$(date +%s)
          # Web.Client needs a Release static-web-assets manifest for Web publish.
          # Build it explicitly so publish can reuse the prepared outputs.
          dotnet build src/TaxBaik.Web.Client/TaxBaik.Web.Client.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true
          # Build the Web host in Release as well so publish has the same inputs
          # the server uses in production.
          dotnet build src/TaxBaik.Web/TaxBaik.Web.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true
          echo "--- Web.Client Release artifacts ---"
          ls -la src/TaxBaik.Web.Client/bin/Release/net10.0 || true
          ls -la src/TaxBaik.Web.Client/obj/Release/net10.0 || true
          if ! dotnet publish src/TaxBaik.Web/ \
            -c Release \
            -o ./publish \
            --no-restore \
            -p:SelfContained=false \
            -p:PublishReadyToRun=false \
            -p:PerformanceSummary=true \
            -clp:Summary \
            -bl:"./publish-logs/publish-web.binlog" >"$web_log" 2>&1; then
            echo "=== Publish Web failed; tailing log ==="
            tail -n 120 "$web_log" || true
            exit 1
          fi
          end=$(date +%s)
          echo "✓ Publish Web elapsed: $((end - start))s"
          ls -lh ./publish-logs/publish-web.binlog

      - name: Publish Proxy
        run: |
          set -e
          mkdir -p ./publish-logs
          # Proxy is not part of the solution restore graph, so restore it once
          # here before publishing to avoid NETSDK1004 in CI.
          dotnet restore src/TaxBaik.Proxy/
          dotnet build src/TaxBaik.Proxy/TaxBaik.Proxy.csproj -c Release --no-restore
          start=$(date +%s)
          dotnet publish src/TaxBaik.Proxy/ \
            -c Release \
            -o ./publish/proxy \
            --no-restore \
            --no-build \
            -p:PublishReadyToRun=false \
            -p:PerformanceSummary=true \
            -clp:Summary \
            -bl:./publish-logs/publish-proxy.binlog
          end=$(date +%s)
          echo "✓ Publish Proxy elapsed: $((end - start))s"
          ls -lh ./publish-logs/publish-proxy.binlog

      - name: Write production secrets
        run: |
          set -e
          JWT_SECRET_KEY="${{ secrets.TAXBAIK_JWT_SECRET_KEY }}"
          TELEGRAM_BOT_TOKEN="${{ secrets.TAXBAIK_TELEGRAM_BOT_TOKEN }}"
          TELEGRAM_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_CHAT_ID }}"
          TELEGRAM_INQUIRY_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_INQUIRY_CHAT_ID }}"
          TELEGRAM_SYSTEM_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_SYSTEM_CHAT_ID }}"
          [ -z "$JWT_SECRET_KEY" ]        && { echo "Missing TAXBAIK_JWT_SECRET_KEY" >&2;        exit 1; }
          [ -z "$TELEGRAM_BOT_TOKEN" ]    && { echo "Missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2;    exit 1; }
          [ -z "$TELEGRAM_CHAT_ID" ]      && { echo "Missing TAXBAIK_TELEGRAM_CHAT_ID" >&2;      exit 1; }
          [ -z "$TELEGRAM_INQUIRY_CHAT_ID" ] && TELEGRAM_INQUIRY_CHAT_ID="$TELEGRAM_CHAT_ID"
          [ -z "$TELEGRAM_SYSTEM_CHAT_ID" ]  && TELEGRAM_SYSTEM_CHAT_ID="-5585148480"
          JWT_SECRET_KEY="$JWT_SECRET_KEY" \
          TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \
          TELEGRAM_CHAT_ID="$TELEGRAM_CHAT_ID" \
          TELEGRAM_INQUIRY_CHAT_ID="$TELEGRAM_INQUIRY_CHAT_ID" \
          TELEGRAM_SYSTEM_CHAT_ID="$TELEGRAM_SYSTEM_CHAT_ID" \
          python3 -c '
          import json, os, pathlib
          pathlib.Path("./publish/appsettings.Production.json").write_text(
            json.dumps({
              "Jwt":      {"SecretKey": os.environ["JWT_SECRET_KEY"]},
              "Telegram": {
                "BotToken": os.environ["TELEGRAM_BOT_TOKEN"],
                "ChatId": os.environ["TELEGRAM_CHAT_ID"],
                "InquiryChatId": os.environ["TELEGRAM_INQUIRY_CHAT_ID"],
                "SystemChatId": os.environ["TELEGRAM_SYSTEM_CHAT_ID"]
              }
            }, ensure_ascii=False, indent=2),
            encoding="utf-8"
          )'
          test -s ./publish/appsettings.Production.json || { echo "appsettings.Production.json is empty" >&2; exit 1; }

      - name: Verify proxy artifact
        run: |
          test -s ./publish/proxy/TaxBaik.Proxy.dll || { echo "TaxBaik.Proxy.dll missing" >&2; exit 1; }
          test -s ./publish/proxy/TaxBaik.Proxy.runtimeconfig.json || { echo "TaxBaik.Proxy.runtimeconfig.json missing" >&2; exit 1; }

      - name: Copy migrations
        run: mkdir -p ./publish/db && cp -r db/migrations ./publish/db/ || true

      - name: Validate migration version uniqueness
        run: bash scripts/validate_migrations.sh db/migrations

      - name: Validate KST timestamps
        run: bash scripts/validate_kst_timestamps.sh

      - name: Generate build info
        run: |
          COMMIT_HASH=$(git rev-parse --short HEAD)
          BUILD_TIME=$(TZ=Asia/Seoul date +'%Y-%m-%d %H:%M:%S KST')
          mkdir -p ./publish/wwwroot
          printf '{\n  "version": "%s",\n  "built": "%s"\n}\n' "$COMMIT_HASH" "$BUILD_TIME" > ./publish/wwwroot/version.json
          echo "✓ Build: $COMMIT_HASH @ $BUILD_TIME"

      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}"
          SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}"
          if [ -n "$SSH_KEY_B64" ]; then
            printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
          elif [ -n "$SSH_KEY_RAW" ]; then
            if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then
              printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519
            else
              printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519
            fi
          else
            echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY" >&2; exit 1
          fi
          sed -i 's/\r$//' ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || true

      - name: Package artifact
        run: |
          cp deploy_gb.sh ./publish/deploy_gb.sh
          mkdir -p ./publish/scripts
          cp scripts/validate_migrations.sh ./publish/scripts/validate_migrations.sh
          chmod +x ./publish/scripts/validate_migrations.sh
          tar -czf taxbaik_deploy.tgz -C ./publish .
          echo "✓ Package: $(du -sh taxbaik_deploy.tgz | cut -f1)"

      - name: Deploy & verify on server
        run: |
          set -e
          export TAXBAIK_DEPLOY_FROM_CI=1
          TIMESTAMP=$(TZ=Asia/Seoul date +%Y%m%d_%H%M%S)
          COMMIT=$(git rev-parse --short HEAD)
          DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}"
          DEPLOY_USER="${{ secrets.DEPLOY_USER }}"
          TELEGRAM_BOT_TOKEN="${{ secrets.TAXBAIK_TELEGRAM_BOT_TOKEN }}"
          TELEGRAM_SYSTEM_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_SYSTEM_CHAT_ID }}"
          TELEGRAM_CHAT_ID="${TELEGRAM_SYSTEM_CHAT_ID:--5585148480}"

          send_telegram() {
            local text="$1"
            if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
              echo "Skipping Telegram notification: missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2
              return 0
            fi

            curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
              -d "chat_id=${TELEGRAM_CHAT_ID}" \
              --data-urlencode "text=${text}" \
              -d "parse_mode=HTML" >/dev/null || true
          }

          notify_failure() {
            local exit_code=$?
            send_telegram "❌ <b>TaxBaik 배포 실패</b>

            커밋: <code>${COMMIT}</code>
            시간: <code>${TIMESTAMP}</code>
            단계: CI/CD deploy"
            exit "$exit_code"
          }

          trap notify_failure ERR

          echo "=== Deploying TaxBaik $COMMIT ($TIMESTAMP) ==="

          # 1. 아티팩트 업로드
          scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
            taxbaik_deploy.tgz "$DEPLOY_USER@$DEPLOY_HOST:/tmp/taxbaik_${TIMESTAMP}.tgz"

          # 2. 서버에서 배포 + 헬스 체크 (SSH 1회 연결로 처리, Green-Blue 지원)
          ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
              -o ServerAliveInterval=10 \
              "$DEPLOY_USER@$DEPLOY_HOST" TAXBAIK_DEPLOY_FROM_CI=1 bash << REMOTE
          set -e
          DEPLOY_HOME="/home/kjh2064"
          DEPLOY_DIR="\$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}"
          TIMESTAMP="${TIMESTAMP}"
          COMMIT="${COMMIT}"

          echo "--- [1/5] 압축 해제 ---"
          mkdir -p "\$DEPLOY_DIR"
          tar -xzf "/tmp/taxbaik_\${TIMESTAMP}.tgz" -C "\$DEPLOY_DIR"
          rm -f "/tmp/taxbaik_\${TIMESTAMP}.tgz"

          echo "--- [2/5] 운영 설정 검증 ---"
          test -s "\$DEPLOY_DIR/appsettings.Production.json" \
            || { echo "FATAL: appsettings.Production.json 없음" >&2; exit 1; }
          test -s "\$DEPLOY_DIR/proxy/TaxBaik.Proxy.dll" \
            || { echo "FATAL: TaxBaik.Proxy.dll 없음" >&2; exit 1; }

          echo "--- [3/5] 마이그레이션 사전 검증 ---"
          test -x "\$DEPLOY_DIR/scripts/validate_migrations.sh" \
            || { echo "FATAL: validate_migrations.sh 없음" >&2; exit 1; }
          "\$DEPLOY_DIR/scripts/validate_migrations.sh" "\$DEPLOY_DIR/db/migrations" "postgresql://taxbaik:taxbaik123@localhost:5432/taxbaikdb"

          echo "--- [4/5] Green-Blue 배포 실행 ---"
          chmod +x "\$DEPLOY_DIR/deploy_gb.sh"
          "\$DEPLOY_DIR/deploy_gb.sh" "\$DEPLOY_DIR"

          echo "--- [4.5/5] Nginx 설정 검증 ---"
          # 실제 로드되는 파일은 sites-enabled/의 심볼릭 링크 대상만이다.
          # sites-available/에 다른 파일(예: default)이 있어도 sites-enabled에
          # 링크되어 있지 않으면 nginx는 그 내용을 절대 읽지 않는다.
          NGINX_CONF=""
          for f in /etc/nginx/sites-enabled/*; do
            if [ -e "\$f" ] && grep -q "location /taxbaik" "\$f" 2>/dev/null; then
              NGINX_CONF=\$(readlink -f "\$f")
              break
            fi
          done

          if [ -z "\$NGINX_CONF" ]; then
            echo "❌ FATAL: sites-enabled/ 안에서 'location /taxbaik'를 정의한 파일을 찾을 수 없음" >&2
            echo "   sites-available/에 파일을 수정해도 sites-enabled에 심볼릭 링크되어 있지 않으면 반영되지 않는다." >&2
            exit 1
          fi
          echo "실제 로드되는 설정 파일: \$NGINX_CONF"

          # 불변식: '/'와 '/taxbaik' location 모두 반드시 127.0.0.1:5001 (TaxBaik.Proxy)을
          # 가리켜야 한다. 5003/5004를 직접 하드코딩하면 Green-Blue 포트 전환 시
          # 죽은 포트를 가리키게 되어 502/404가 발생한다 (실제 발생했던 장애).
          if grep -E "proxy_pass\s+http://127\.0\.0\.1:500[34]" "\$NGINX_CONF" > /dev/null 2>&1; then
            echo "❌ FATAL: \$NGINX_CONF 가 포트 5003/5004를 직접 참조함 (Green-Blue 전환 시 502 발생)" >&2
            echo "   수정: sudo sed -i 's|127.0.0.1:500[34]|127.0.0.1:5001|g' \$NGINX_CONF && sudo nginx -t && sudo systemctl reload nginx" >&2
            exit 1
          fi

          # proxy_pass에 URI(끝 슬래시)가 있으면 nginx가 요청 경로를 재작성하며,
          # location 접두사와 슬래시 개수가 안 맞으면 백엔드로 이중 슬래시(//)가
          # 전달되어 404가 발생한다 (실제 발생했던 장애). 접두사 location에서는
          # proxy_pass에 URI를 붙이지 않는다.
          if grep -E "location\s+/taxbaik\s*\{" -A 1 "\$NGINX_CONF" | grep -qE "proxy_pass\s+http://127\.0\.0\.1:5001/;"; then
            echo "❌ FATAL: location /taxbaik 의 proxy_pass 에 불필요한 trailing slash가 있음 (이중 슬래시로 인한 404 위험)" >&2
            exit 1
          fi

          echo "✓ Nginx 설정 검증 통과 (실제 로드 파일 확인 + 포트 5001 고정 + trailing slash 없음)"

          echo "--- [5/5] 헬스 체크 (최대 60초) ---"
          ATTEMPTS=20
          for i in \$(seq 1 \$ATTEMPTS); do
            STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/healthz 2>/dev/null || echo "000")
            if [ "\$STATUS" = "200" ]; then
              echo "✓ [1/6] 헬스 체크 완료"

              # 검증 1: 메인 페이지 로드. curl -L + -w 는 리다이렉트 체인의 상태코드를
              # 이어붙이므로, 첫 응답 코드만 받아 200/3xx를 허용한다.
              MAIN_STATUS=\$(curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/ 2>/dev/null || echo "000")
              if ! printf '%s' "\$MAIN_STATUS" | grep -Eq '^(200|301|302|307|308)$'; then
                echo "❌ 메인 페이지 로드 실패 (상태: \$MAIN_STATUS)" >&2
                exit 1
              fi
              echo "✓ [2/6] 메인 페이지 로드 완료"

              # 검증 2: CSS 파일 로드
              CSS_STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/css/admin.css 2>/dev/null || echo "000")
              if [ "\$CSS_STATUS" != "200" ]; then
                echo "❌ CSS 파일 로드 실패 (상태: \$CSS_STATUS)" >&2
                exit 1
              fi
              echo "✓ [3/6] CSS 파일 로드 완료"

              # 검증 3: 버전 정보. 파일 존재만 보면 5001이 잘못된 구 프로세스를
              # 가리키는 장애를 놓치므로, HTTP 응답이 이번 커밋인지 확인한다.
              if [ ! -s "\$DEPLOY_DIR/wwwroot/version.json" ]; then
                echo "❌ version.json 누락" >&2
                exit 1
              fi
              VERSION_JSON=\$(curl -fsS http://127.0.0.1:5001/taxbaik/version.json 2>/dev/null || true)
              if ! printf '%s' "\$VERSION_JSON" | grep -q "\"version\": \"\$COMMIT\""; then
                echo "❌ 5001 프록시가 이번 배포 버전을 제공하지 않음" >&2
                echo "   expected: \$COMMIT" >&2
                echo "   actual: \$VERSION_JSON" >&2
                echo "   확인: 5001 포트가 TaxBaik.Proxy.dll인지, /home/kjh2064/taxbaik_port가 새 포트인지 점검" >&2
                exit 1
              fi
              echo "✓ [4/6] 버전 정보 확인 완료"

              # 검증 4: 5001 프록시 확인
              if ! ss -tlnp | grep -q ':5001 '; then
                echo "❌ 5001 프록시가 실행 중이 아님" >&2
                exit 1
              fi
              echo "✓ [5/6] 5001 프록시 확인 완료"

              # 검증 5: 관리자 로그인 페이지
              LOGIN_STATUS=\$(curl -fsSL -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/admin/login 2>/dev/null || echo "000")
              if [ "\$LOGIN_STATUS" != "200" ]; then
                echo "❌ 관리자 로그인 페이지 로드 실패 (상태: \$LOGIN_STATUS)" >&2
                exit 1
              fi
              echo "✓ [6/6] 관리자 페이지 로드 완료"

              echo "✓ 서비스 정상 (시도 \$i/\$ATTEMPTS)"
              # 구 배포 디렉토리 정리 (최근 5개 보존)
              ls -1dt \$DEPLOY_HOME/deployments/taxbaik_* 2>/dev/null \
                | tail -n +6 | xargs rm -rf 2>/dev/null || true
              exit 0
            fi
            if [ "\$i" -eq "\$ATTEMPTS" ]; then
              echo "=== FATAL: 서비스가 \$ATTEMPTS회 시도 후에도 응답하지 않음 ===" >&2
              echo "--- 5001 listener ---" >&2
              ss -tlnp 2>/dev/null | grep ':5001 ' >&2 || true
              echo "--- active port file ---" >&2
              cat "\$DEPLOY_HOME/taxbaik_port" >&2 || true
              echo "--- 신규 앱 로그 ---" >&2
              ACTIVE_PORT=\$(cat "\$DEPLOY_HOME/taxbaik_port" 2>/dev/null | tr -d '[:space:]' || true)
              if [ -n "\$ACTIVE_PORT" ] && [ -s "\$DEPLOY_DIR/web_\${ACTIVE_PORT}.log" ]; then
                tail -n 80 "\$DEPLOY_DIR/web_\${ACTIVE_PORT}.log" >&2
              else
                ls -la "\$DEPLOY_DIR" >&2 || true
              fi
              echo "--- proxy 로그 ---" >&2
              tail -n 80 "\$DEPLOY_HOME/taxbaik_proxy.log" >&2 || true
              exit 1
            fi
            echo "  대기 중... (\$i/\$ATTEMPTS, HTTP \$STATUS)"
            sleep 3
          done
          REMOTE

          echo "✓ 배포 완료: taxbaik_${TIMESTAMP} @ $DEPLOY_HOST"

          # 내부 127.0.0.1:5001 헬스 체크는 Nginx/Cloudflare를 거치지 않으므로
          # Nginx 설정 오류(잘못된 파일 수정, 죽은 포트 하드코딩 등)를 잡지 못한다.
          # 실제 사용자가 접속하는 경로 그대로 외부에서 검증해야 이런 장애를 CI가 스스로 잡는다.
          check_public() {
            local url="$1"
            local allow_redirect="${2:-0}"
            local status
            status=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$url" || echo "000")
            if [ "$allow_redirect" = "1" ]; then
              if ! printf '%s' "$status" | grep -Eq '^(200|301|302|303|307|308)$'; then
                echo "  ✗ $url → HTTP $status" >&2
                return 1
              fi
            elif [ "$status" != "200" ]; then
              echo "  ✗ $url → HTTP $status" >&2
              return 1
            fi
            echo "  ✓ $url → HTTP $status"
            return 0
          }

          echo "--- 실제 공개 도메인 종단 간 검증 (Nginx/Cloudflare 경유, 최대 3회 재시도) ---"
          PUBLIC_OK=false
          for i in 1 2 3; do
            if check_public "https://www.taxbaik.com/" 1 \
              && check_public "https://www.taxbaik.com/taxbaik/" 1 \
              && check_public "https://www.taxbaik.com/taxbaik/admin/login"; then
              PUBLIC_OK=true
              break
            fi
            echo "  재시도 대기 중... ($i/3)"
            sleep 5
          done

          if [ "$PUBLIC_OK" != "true" ]; then
            echo "❌ FATAL: 실제 공개 도메인 검증 실패. Nginx가 죽은 포트를 가리키거나 잘못된 파일을 수정했을 가능성이 높다." >&2
            echo "   확인: sites-enabled/의 실제 파일에서 location / 와 location /taxbaik 모두 127.0.0.1:5001을 가리키는지 점검" >&2
            exit 1
          fi
          echo "✓ 실제 공개 도메인 전체 정상"

          send_telegram "✅ <b>TaxBaik 배포 완료</b>

          커밋: <code>${COMMIT}</code>
          시간: <code>${TIMESTAMP}</code>
          대상: <code>${DEPLOY_HOST}</code>
          채널: <code>${TELEGRAM_CHAT_ID}</code>"
