#!/bin/bash
# process-airtable-dual.sh — Provisiona proyectos desde Airtable con distribución automática entre 2 VPS
# Uso: ./process-airtable-dual.sh [--dry-run]
#
# Requiere: /root/scripts/.airtable.env con:
#   AIRTABLE_TOKEN=pat...
#   AIRTABLE_BASE_ID=app...
#   AIRTABLE_TABLE_ID=tbl...
#
# Campos usados desde Airtable (tabla Projects):
#   Name              → título del proyecto
#   Cpanel_User       → base_name cPanel (si vacío, se genera desde Name)
#   Email             → email del admin (si vacío: admin@{base_name}.bewpro.com)
#   Slug (from Product) → product slug para bewpro:new
#   Domain            → URL del sitio (si vacío: https://{base_name}.bewpro.com)
#   Notes             → texto: contiene CLEAN=true y/o COLORS=#HEX,... (opciones de provisión)
#
# Escribe de vuelta en Airtable:
#   Pipeline_Status      → "On Development"
#   Cpanel_User          → base_name usado
#   Provisioned_DB       → {base_name}_bp
#   Provisioned_Password → password generado
#   Server               → "vps1" o "vps2" (nuevo campo)

set -uo pipefail

########################################
# LOCK — evitar ejecuciones concurrentes
########################################

LOCK_FILE="/tmp/process-airtable.lock"
MAX_RUNTIME=3600  # 1 hora máximo (ajustar según cantidad de proyectos)

if [[ -f "$LOCK_FILE" ]]; then
  PID=$(cat "$LOCK_FILE")
  if kill -0 "$PID" 2>/dev/null; then
    LOCK_AGE=$(( $(date +%s) - $(stat -c %Y "$LOCK_FILE") ))
    if [[ "$LOCK_AGE" -gt "$MAX_RUNTIME" ]]; then
      echo "WARN: Proceso ${PID} lleva ${LOCK_AGE}s — matando por timeout..." >&2
      kill -9 "$PID" 2>/dev/null || true
      rm -f "$LOCK_FILE"
    else
      echo "Ya hay una instancia corriendo (PID: ${PID}, hace ${LOCK_AGE}s). Saliendo."
      exit 0
    fi
  else
    echo "Lock obsoleto (PID ${PID} ya no existe), limpiando..."
    rm -f "$LOCK_FILE"
  fi
fi

echo $$ > "$LOCK_FILE"
trap "rm -f ${LOCK_FILE}" EXIT INT TERM


SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SETUP_SCRIPT="${SCRIPT_DIR}/setup_cd_project4.sh"
CONFIG_FILE="/root/scripts/.airtable.env"

########################################
# CONFIGURACIÓN DUAL VPS
########################################

VPS1_IP="72.61.45.136"
VPS2_IP="179.43.124.219"
VPS2_PORT="5633"
VPS1_NAME="VPS Hostinger 1"
VPS2_NAME="VPS Donweb 1"

########################################
# FLAGS
########################################

DRY_RUN=false
for arg in "$@"; do
  [[ "$arg" == "--dry-run" ]] && DRY_RUN=true
done

########################################
# CHEQUEO DE ROOT
########################################

if [[ "$(id -u)" -ne 0 ]]; then
  echo "Este script debe ejecutarse como root." >&2
  exit 1
fi

########################################
# DEPENDENCIAS
########################################

for cmd in curl python3 jq openssl ssh; do
  if ! command -v "$cmd" &>/dev/null; then
    echo "ERROR: '$cmd' no está instalado." >&2
    exit 1
  fi
done

if [[ ! -x "$SETUP_SCRIPT" ]]; then
  echo "ERROR: setup_cd_project4.sh no encontrado o no ejecutable: ${SETUP_SCRIPT}" >&2
  exit 1
fi

########################################
# CARGAR CONFIG DE AIRTABLE
########################################

if [[ -f "$CONFIG_FILE" ]]; then
  source "$CONFIG_FILE"
fi

AIRTABLE_TOKEN="${AIRTABLE_TOKEN:-}"
AIRTABLE_BASE_ID="${AIRTABLE_BASE_ID:-}"
AIRTABLE_TABLE_ID="${AIRTABLE_TABLE_ID:-}"

if [[ -z "$AIRTABLE_TOKEN" || -z "$AIRTABLE_BASE_ID" || -z "$AIRTABLE_TABLE_ID" ]]; then
  echo "ERROR: Faltan variables de Airtable en ${CONFIG_FILE}" >&2
  exit 1
fi

AIRTABLE_API_URL="https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${AIRTABLE_TABLE_ID}"
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"

########################################
# FUNCIONES SLACK
########################################

slack_notify() {
  local msg="$1"
  if [[ -n "$SLACK_WEBHOOK_URL" ]]; then
    curl -s -X POST "$SLACK_WEBHOOK_URL" \
      -H 'Content-type: application/json' \
      -d '{"text": "'"$msg"'"}' > /dev/null 2>&1 &
  fi
}

########################################
# FUNCIONES DUAL VPS
########################################

get_vps1_count() {
  # Usamos `grep | wc -l` en lugar de `grep -c` porque grep -c devuelve
  # exit 1 cuando count=0, y eso dispara el fallback exterior creando
  # un valor concatenado tipo "0999". wc -l siempre devuelve exit 0.
  /usr/local/cpanel/bin/whmapi1 listaccts 2>/dev/null | grep "^      user:" | wc -l
}

get_vps2_count() {
  ssh -p "${VPS2_PORT}" -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
    root@${VPS2_IP} \
    "/usr/local/cpanel/bin/whmapi1 listaccts 2>/dev/null | grep '^      user:' | wc -l" \
    2>/dev/null || echo 999
}

select_target_server() {
  local vps1_count vps2_count

  # Verificar conectividad VPS2
  if ! ssh -p "${VPS2_PORT}" -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
    root@${VPS2_IP} "echo ok" >/dev/null 2>&1; then
    echo "WARN: VPS2 no disponible — usando VPS1" >&2
    echo "${VPS1_NAME}|${VPS1_IP}"
    return
  fi

  vps1_count=$(get_vps1_count)
  vps2_count=$(get_vps2_count)

  # Limpiar: dejar solo dígitos
  vps1_count="${vps1_count//[^0-9]/}"
  vps2_count="${vps2_count//[^0-9]/}"

  # Defaults si quedaron vacíos
  vps1_count="${vps1_count:-0}"
  vps2_count="${vps2_count:-999}"

  echo "  Cuentas VPS1: ${vps1_count} | Cuentas VPS2: ${vps2_count}" >&2

  if [[ "$vps2_count" -le "$vps1_count" ]]; then
    echo "${VPS2_NAME}|${VPS2_IP}"
  else
    echo "${VPS1_NAME}|${VPS1_IP}"
  fi
}
run_setup_on_server() {
  local target_ip="$1"
  local base_name="$2"
  local email="$3"
  local title="$4"
  local slug="$5"
  local password="$6"
  local domain="$7"

  if [[ "$target_ip" == "$VPS1_IP" ]]; then
    # Ejecutar local — pasar IP del server para DNS
    TARGET_SERVER_IP="${VPS1_IP}" \
    BEWPRO_VENDOR_CACHE="${BEWPRO_VENDOR_CACHE:-}" \
    bash "$SETUP_SCRIPT" "$base_name" "$email" "$title" "$slug" "$password" "$domain"
  else
    # Ejecutar remoto via SSH — propagar feature flags como env vars.
    # VPS2 también lee /root/scripts/.airtable.env via setup script, pero
    # pasar inline garantiza que la flag llegue aún si el archivo de VPS2
    # estuviera desincronizado.
    ssh -p "${VPS2_PORT}" -o StrictHostKeyChecking=no root@${target_ip} \
      "TARGET_SERVER_IP='${target_ip}' CLEAN_PROVISION='${CLEAN_PROVISION:-false}' BRAND_COLORS='${BRAND_COLORS:-}' \
       BEWPRO_VENDOR_CACHE='${BEWPRO_VENDOR_CACHE:-}' \
       bash /root/scripts/setup_cd_project4.sh \
       '$base_name' '$email' '$title' '$slug' '$password' '$domain'"
  fi
}

########################################
# FUNCIONES AIRTABLE
########################################

airtable_get_required() {
  local filter
  filter=$(python3 -c "import urllib.parse; print(urllib.parse.quote('{Pipeline_Status}=\"Required\"'))")
  curl -s \
    -H "Authorization: Bearer ${AIRTABLE_TOKEN}" \
    -H "Content-Type: application/json" \
    "${AIRTABLE_API_URL}?filterByFormula=${filter}&sort%5B0%5D%5Bfield%5D=Name&sort%5B0%5D%5Bdirection%5D=asc"
}

airtable_patch() {
  local record_id="$1"
  local payload="$2"
  curl -s -X PATCH \
    -H "Authorization: Bearer ${AIRTABLE_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "$payload" \
    "${AIRTABLE_API_URL}/${record_id}"
}

airtable_mark_processing() {
  local record_id="$1"
  airtable_patch "$record_id" '{"fields":{"Pipeline_Status":"Processing"}}'
}

airtable_mark_failed() {
  local record_id="$1"
  airtable_patch "$record_id" '{"fields":{"Pipeline_Status":"Failed"}}'
}

# Retry policy: marca el record con un counter en Notes para que el próximo
# run lo retomé. Después de MAX_RETRIES vuelve al "Failed" definitivo.
# Se usa cuando el setup script falla pero podría ser transitorio (ej: DNS no propagó).
airtable_mark_retry() {
  local record_id="$1"
  local current_notes="$2"   # texto actual del campo Notes (puede tener "RETRY=N")
  local current_count
  if echo "$current_notes" | grep -qE 'RETRY=[0-9]+'; then
    current_count=$(echo "$current_notes" | grep -oE 'RETRY=[0-9]+' | tail -1 | cut -d= -f2)
  else
    current_count=0
  fi
  local new_count=$((current_count + 1))
  local new_notes
  if [[ -n "$current_notes" ]]; then
    new_notes=$(echo "$current_notes" | sed 's/RETRY=[0-9]*//g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    new_notes="${new_notes}\nRETRY=${new_count}"
  else
    new_notes="RETRY=${new_count}"
  fi
  local payload
  payload=$(python3 -c "
import json, sys
data = {'fields': {'Pipeline_Status': 'Required', 'Notes': sys.argv[1]}}
print(json.dumps(data))
" "$new_notes")
  airtable_patch "$record_id" "$payload"
  echo "$new_count"  # devuelve el counter
}

# Constantes de retry
MAX_RETRIES=2

airtable_mark_on_development() {
  local record_id="$1"
  local db_name="$2"
  local password="$3"
  local cpanel_user="$4"
  local app_url="$5"
  local server_name="$6"   # vps1 o vps2

  local grace_end
  grace_end=$(date -d "+15 days" '+%Y-%m-%d' 2>/dev/null || date -v+15d '+%Y-%m-%d' 2>/dev/null || "")

  local payload
  payload=$(python3 -c "
import json, sys
data = {
    'fields': {
        'Pipeline_Status': 'On Development',
        'Cpanel_User': sys.argv[1],
        'Provisioned_DB': sys.argv[2],
        'Provisioned_Password': sys.argv[3],
        'Domain': sys.argv[4],
        'SERVER': sys.argv[6]
    }
}
if sys.argv[5]:
    data['fields']['Grace_Period_End'] = sys.argv[5]
print(json.dumps(data))
" "$cpanel_user" "$db_name" "$password" "$app_url" "$grace_end" "$server_name")

  airtable_patch "$record_id" "$payload"
}

airtable_create_subscription() {
  # Upsert idempotente:
  # - Si ya existe Subscription con este Stripe_Subscription_ID (creada por el
  #   webhook checkout.session.completed que captura sub_id, amount, grace,
  #   next_payment_date), la patcheamos con los datos del post-provision
  #   (Cpanel_User, App_URL, Provisioned_At, Status=Active, Payment_Status=Paid).
  # - Fallback a Stripe_Customer_ID solo si aún no tenemos sub_id.
  # - Si no existe nada, creamos nueva (caso form-reseller sin Stripe checkout).
  # Búsqueda por sub_id (no customer_id) porque Stripe REUSA customer cuando el
  # mismo email compra otro producto, y eso colapsaba múltiples projects en 1 sub.
  local project_record_id="$1"
  local project_name="$2"
  local cpanel_user="$3"
  local app_url="$4"
  local stripe_customer_id="${5:-}"
  local amount_usd="${6:-}"
  local grace_end="${7:-}"
  local stripe_subscription_id="${8:-}"

  # Buscar sub existente preferentemente por Sub_ID, fallback Customer_ID
  local existing_record_id=""
  local search_url="https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/tblnpr52JhFBBi2Mg"
  if [[ -n "$stripe_subscription_id" ]]; then
    local search_response
    search_response=$(curl -s -G \
      -H "Authorization: Bearer ${AIRTABLE_TOKEN}" \
      --data-urlencode "filterByFormula={Stripe_Subscription_ID}=\"${stripe_subscription_id}\"" \
      --data-urlencode "maxRecords=1" \
      "${search_url}")
    existing_record_id=$(echo "$search_response" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    recs = d.get('records', [])
    print(recs[0]['id'] if recs else '')
except: print('')
" 2>/dev/null)
  fi
  if [[ -z "$existing_record_id" && -n "$stripe_customer_id" ]]; then
    local search_response
    search_response=$(curl -s -G \
      -H "Authorization: Bearer ${AIRTABLE_TOKEN}" \
      --data-urlencode "filterByFormula={Stripe_Customer_ID}=\"${stripe_customer_id}\"" \
      --data-urlencode "maxRecords=1" \
      "${search_url}")
    existing_record_id=$(echo "$search_response" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    recs = d.get('records', [])
    # Solo aceptar si NO tiene sub_id ya asignado (sino, puede ser sub vieja huérfana)
    if recs and not recs[0].get('fields', {}).get('Stripe_Subscription_ID'):
        print(recs[0]['id'])
    else:
        print('')
except: print('')
" 2>/dev/null)
  fi

  if [[ -n "$existing_record_id" ]]; then
    # PATCH: enriquecer la sub creada por el webhook con datos del provisioning.
    # IMPORTANTE: incluir Project link, Amount_USD y Grace_Period_End. El webhook
    # original puede haber creado el record sin estos campos (race entre eventos
    # Stripe), entonces el PATCH garantiza que queden completos para que
    # bewpro:check-renewals y bewpro:check-grace tengan toda la data.
    local payload
    payload=$(python3 -c "
import json, sys
data = {
    'fields': {
        'Status': 'Active',
        'Payment_Status': 'Paid',
        'Payment_Method': 'Stripe',
        'Plan': 'Monthly',
        'Provisioned_At': __import__('datetime').date.today().isoformat(),
        'Cpanel_User': sys.argv[1],
        'App_URL': sys.argv[2],
        'Project': sys.argv[3],
        'Copia de Project': [sys.argv[4]],
    }
}
if sys.argv[5]:
    try:
        data['fields']['Amount_USD'] = float(sys.argv[5])
    except: pass
if sys.argv[6]:
    data['fields']['Grace_Period_End'] = sys.argv[6]
print(json.dumps(data))
" "$cpanel_user" "$app_url" "$project_name" "$project_record_id" "$amount_usd" "$grace_end")

    curl -s -X PATCH \
      -H "Authorization: Bearer ${AIRTABLE_TOKEN}" \
      -H "Content-Type: application/json" \
      -d "$payload" \
      "https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/tblnpr52JhFBBi2Mg/${existing_record_id}"
  else
    # CREATE: no existe sub previa (caso form-reseller sin Stripe)
    local payload
    payload=$(python3 -c "
import json, sys
data = {
    'fields': {
        'Project': sys.argv[1],
        'Copia de Project': [sys.argv[2]],
        'Billing_Model': 'Stripe',
        'Status': 'Active',
        'Payment_Status': 'Paid',
        'Payment_Method': 'Stripe',
        'Plan': 'Monthly',
        'Provisioned_At': __import__('datetime').date.today().isoformat(),
        'Cpanel_User': sys.argv[3],
        'App_URL': sys.argv[4],
    }
}
if sys.argv[5]:
    data['fields']['Stripe_Customer_ID'] = sys.argv[5]
if sys.argv[6]:
    try:
        data['fields']['Amount_USD'] = float(sys.argv[6])
    except: pass
if sys.argv[7]:
    data['fields']['Grace_Period_End'] = sys.argv[7]
print(json.dumps(data))
" "$project_name" "$project_record_id" "$cpanel_user" "$app_url" \
  "$stripe_customer_id" "$amount_usd" "$grace_end")

    curl -s -X POST \
      -H "Authorization: Bearer ${AIRTABLE_TOKEN}" \
      -H "Content-Type: application/json" \
      -d "$payload" \
      "https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/tblnpr52JhFBBi2Mg"
  fi
}

########################################
# PARSEAR REGISTROS DE AIRTABLE
########################################

parse_records() {
  local json="$1"

  python3 - "$json" <<'PYEOF'
import sys, json, re

raw = sys.argv[1]
try:
    data = json.loads(raw)
except Exception as e:
    print(f"ERROR parsing JSON: {e}", file=sys.stderr)
    sys.exit(1)

records = data.get('records', [])
if not records:
    print("NO_RECORDS")
    sys.exit(0)

def make_base_name(title, record_id):
    base = re.sub(r'[^a-z0-9]', '', title.lower())[:11]
    suffix = re.sub(r'[^a-z0-9]', '', record_id.lower())[-3:]
    return (base + suffix)[:14]

def make_subdomain(title):
    slug = re.sub(r'[^a-z0-9]+', '-', title.lower().strip()).strip('-')
    return slug[:63]

def normalize_domain(domain, slug):
    d = domain.split('\n')[0].strip()
    if not d or d in ('---', '-', 'N/A', 'n/a'):
        return f"https://{slug}.bewpro.com"
    if not re.match(r'^https?://[a-zA-Z0-9]', d) and not re.match(r'^[a-zA-Z0-9][\w.-]+\.[a-zA-Z]{2,}$', d):
        return f"https://{slug}.bewpro.com"
    if not d.startswith('http'):
        return f"https://{d}"
    return d


for r in records:
    fields = r.get('fields', {})
    record_id   = r.get('id', '')
    name        = fields.get('Name', '').strip()
    cpanel_user = fields.get('Cpanel_User', '').strip()
    email       = fields.get('Email', '').strip()

    slug_raw = fields.get('Slug (from Product)', [])
    if isinstance(slug_raw, list):
        slug = slug_raw[0].strip() if slug_raw else ''
    else:
        slug = str(slug_raw).strip()
    if not slug:
        slug = fields.get('Slug', '').strip()

    domain_raw = fields.get('Domain', '').strip()
    notes      = fields.get('Notes', '').strip()

    clean  = 'false'
    colors = ''
    for line in notes.split('\n'):
        line = line.strip()
        if line.startswith('CLEAN='):
            clean = line.split('=', 1)[1].strip().lower()
        elif line.startswith('COLORS='):
            colors = line.split('=', 1)[1].strip()

    if not name or not slug:
        print(f"SKIP|{record_id}|{name}|missing name or slug", file=sys.stderr)
        continue

    base_name = cpanel_user if cpanel_user else make_base_name(name, record_id)
    if not email:
        email = f"admin@{base_name}.bewpro.com"

    # Subdominio legible desde el nombre del proyecto
    subdomain = make_subdomain(name) if not domain_raw.strip() or domain_raw.strip() in ('---', '-', 'N/A', 'n/a') else None
    domain = normalize_domain(domain_raw, subdomain or make_subdomain(name))

    print(f"{record_id}|{name}|{base_name}|{email}|{slug}|{domain}|{clean}|{colors}")
PYEOF
}

########################################
# MAIN
########################################

echo "============================================="
echo "  PROCESS AIRTABLE DUAL VPS — Provisioning"
if $DRY_RUN; then
  echo "  MODE: DRY RUN (sin cambios reales)"
fi
echo "============================================="
echo ""

echo "Consultando Airtable (Pipeline_Status = Required)..."
RESPONSE=$(airtable_get_required)

if echo "$RESPONSE" | python3 -c \
  "import sys,json; d=json.load(sys.stdin); sys.exit(0 if 'records' in d else 1)" 2>/dev/null; then
  :
else
  echo "ERROR: Respuesta inesperada de Airtable:" >&2
  echo "$RESPONSE" >&2
  exit 1
fi

RECORDS=$(parse_records "$RESPONSE")

if [[ "$RECORDS" == "NO_RECORDS" || -z "$RECORDS" ]]; then
  echo "No hay proyectos con Pipeline_Status = 'Required'. Nada que hacer."
  exit 0
fi

COUNT=$(echo "$RECORDS" | grep -c '.' || true)
echo "Proyectos a provisionar: ${COUNT}"
echo ""

# ── FEATURE: Paralelización opt-in ─────────────────────────────────────────
# MAX_PARALLEL controla cuántos provisions corren simultáneamente.
# Default 1 = comportamiento clásico (secuencial dentro del lock).
# Activar con BEWPRO_MAX_PARALLEL=N en /root/scripts/.airtable.env (sugerido: 3-4).
# Riesgos al subir el N: WHM rate limits en createacct, contención SSH a VPS2.
MAX_PARALLEL="${BEWPRO_MAX_PARALLEL:-1}"

# Tempfile compartido entre subshells para tracking de resultados.
# Cada provision exitoso/fallido escribe 1 línea: "SUCCESS:NAME" o "FAILED:NAME".
RESULTS_FILE=$(mktemp /tmp/provision-results-XXXXXX)
trap "rm -f ${RESULTS_FILE} ${LOCK_FILE}" EXIT INT TERM

# ── Función que encapsula el provision de UN record ─────────────────────────
# Diseñada para ser llamable directo (modo serial) o en subshell (modo paralelo).
# Lee variables locales por argumentos para que funcione en background sin
# heredar valores de iteraciones siguientes del while.
provision_record() {
  local RECORD_ID="$1" NAME="$2" BASE_NAME="$3" EMAIL="$4"
  local SLUG="$5" DOMAIN="$6" CLEAN_FLAG="$7" COLORS_VAL="$8"
  local PREFIX="[${NAME}]"

  # Seleccionar servidor por cada proyecto (re-evalúa cuentas en tiempo real)
  local SERVER_SELECTION TARGET_SERVER TARGET_IP
  SERVER_SELECTION=$(select_target_server)
  TARGET_SERVER=$(echo "$SERVER_SELECTION" | cut -d'|' -f1)
  TARGET_IP=$(echo "$SERVER_SELECTION" | cut -d'|' -f2)

  echo "---------------------------------------------"
  echo "${PREFIX} Proyecto:  ${NAME}"
  echo "${PREFIX} base_name: ${BASE_NAME}"
  echo "${PREFIX} Email:     ${EMAIL}"
  echo "${PREFIX} Slug:      ${SLUG}"
  echo "${PREFIX} Domain:    ${DOMAIN}"
  echo "${PREFIX} Servidor:  ${TARGET_SERVER} (${TARGET_IP})"
  echo "${PREFIX} Clean:     ${CLEAN_FLAG:-false}"
  echo "${PREFIX} Colors:    ${COLORS_VAL:-default}"
  echo ""

  local PASSWORD NAME_SAFE
  PASSWORD=$(openssl rand -base64 12 | tr -d '/+=')
  NAME_SAFE=$(echo "$NAME" | sed "s/'/'\\\\''/g")

  if $DRY_RUN; then
    echo "${PREFIX} [DRY RUN] setup en ${TARGET_SERVER}: ${BASE_NAME} ${EMAIL} \"${NAME}\" ${SLUG} ${DOMAIN}"
    echo "DRY:${NAME}" >> "$RESULTS_FILE"
    return 0
  fi

  # Marcar como Processing — evita race condition con crons paralelos
  echo "${PREFIX} Marcando como Processing en Airtable..."
  airtable_mark_processing "$RECORD_ID" > /dev/null

  export CLEAN_PROVISION="${CLEAN_FLAG:-false}"
  export BRAND_COLORS="${COLORS_VAL:-}"

  # Ejecutar setup en el servidor elegido
  local APP_URL=""
  if APP_URL=$(run_setup_on_server \
    "$TARGET_IP" "$BASE_NAME" "$EMAIL" "$NAME_SAFE" "$SLUG" "$PASSWORD" "$DOMAIN"); then

    echo "${PREFIX} OK — ${APP_URL}"

    # 1. Actualizar Project en Airtable (con campo Server)
    echo "${PREFIX} Actualizando Airtable..."
    PATCH_RESULT=$(airtable_mark_on_development \
      "$RECORD_ID" "${BASE_NAME}_bp" "$PASSWORD" "$BASE_NAME" "$APP_URL" "$TARGET_SERVER")

    if echo "$PATCH_RESULT" | python3 -c \
      "import sys,json; d=json.load(sys.stdin); sys.exit(0 if 'id' in d else 1)" 2>/dev/null; then
      echo "${PREFIX} Project: Pipeline_Status=On Development, Server=${TARGET_SERVER}"
    else
      echo "${PREFIX} WARN: Project PATCH falló:" >&2
      echo "$PATCH_RESULT" >&2
    fi

    # 2. Crear Subscription record — leer todos los Stripe IDs del Project
    local STRIPE_FIELDS STRIPE_CID AMOUNT GRACE_END STRIPE_SUB_ID SUB_RESULT
    STRIPE_FIELDS=$(echo "$RESPONSE" | python3 -c "
import sys,json
data = json.load(sys.stdin)
for r in data.get('records',[]):
    if r['id'] == sys.argv[1]:
        f = r.get('fields',{})
        # 4 campos pipe-separated: customer | amount | grace | sub_id
        print('|'.join([
            f.get('Stripe_Customer_ID',''),
            str(f.get('Amount_USD','')),
            f.get('Grace_Period_End',''),
            f.get('Stripe_Subscription_ID',''),
        ]))
        break
" "$RECORD_ID" 2>/dev/null || echo "|||")

    STRIPE_CID=$(echo "$STRIPE_FIELDS" | cut -d'|' -f1)
    AMOUNT=$(echo "$STRIPE_FIELDS" | cut -d'|' -f2)
    GRACE_END=$(echo "$STRIPE_FIELDS" | cut -d'|' -f3)
    STRIPE_SUB_ID=$(echo "$STRIPE_FIELDS" | cut -d'|' -f4)

    # Fallback: 15 dias desde hoy (form-reseller sin trial de Stripe)
    if [[ -z "$GRACE_END" ]]; then
      GRACE_END=$(date -d '+15 days' '+%Y-%m-%d' 2>/dev/null || date -v+15d '+%Y-%m-%d' 2>/dev/null || '')
    fi

    SUB_RESULT=$(airtable_create_subscription \
      "$RECORD_ID" "$NAME" "$BASE_NAME" "$APP_URL" "$STRIPE_CID" "$AMOUNT" "$GRACE_END" "$STRIPE_SUB_ID")

    if echo "$SUB_RESULT" | python3 -c \
      "import sys,json; d=json.load(sys.stdin); sys.exit(0 if 'id' in d else 1)" 2>/dev/null; then
      echo "${PREFIX} Subscription: creada (Stripe/Active)"
    else
      echo "${PREFIX} WARN: Subscription no creada:" >&2
      echo "$SUB_RESULT" >&2
    fi

    # 3. Enviar emails
    GRACE_END=$(date -d "+15 days" '+%Y-%m-%d' 2>/dev/null || date -v+15d '+%Y-%m-%d' 2>/dev/null || "")

    echo "${PREFIX} Enviando emails (cliente: ${EMAIL})..."

    local ARTISAN_CMD="php artisan bewpro:send-welcome '${EMAIL}' '${NAME_SAFE}' '${APP_URL}' '${PASSWORD}' --product='${SLUG}'"
    if [[ -n "${GRACE_END}" ]]; then
      ARTISAN_CMD="${ARTISAN_CMD} --grace-end='${GRACE_END}'"
    fi

    if [[ "$TARGET_IP" == "$VPS1_IP" ]]; then
      su - "${BASE_NAME}" -c "
        cd public_html/git-files/${BASE_NAME} && ${ARTISAN_CMD}
      " >&2 && echo "${PREFIX} Emails: enviados via artisan" || {
        echo "${PREFIX} WARN: artisan falló, intentando fallback..." >&2
        local PHP_BIN_GLOBAL EMAIL_SCRIPT
        PHP_BIN_GLOBAL=$(which php 2>/dev/null || echo "php")
        EMAIL_SCRIPT="${SCRIPT_DIR}/send-welcome-email.php"
        if [[ -f "$EMAIL_SCRIPT" ]]; then
          $PHP_BIN_GLOBAL "$EMAIL_SCRIPT" \
            "$EMAIL" "$NAME" "$APP_URL" "$PASSWORD" "$EMAIL" 2>/dev/null \
            && echo "${PREFIX} Email: enviado via fallback" \
            || echo "${PREFIX} WARN: email no enviado" >&2
        fi
      }
    else
      ssh -p "${VPS2_PORT}" -o StrictHostKeyChecking=no root@${TARGET_IP} \
        "su - '${BASE_NAME}' -c \"cd public_html/git-files/${BASE_NAME} && ${ARTISAN_CMD}\"" >&2 \
        && echo "${PREFIX} Emails: enviados via artisan en VPS2" \
        || echo "${PREFIX} WARN: email no enviado en VPS2" >&2
    fi

    slack_notify "✅ *Provisioning completado*\nProyecto: ${NAME}\nURL: ${APP_URL}\nServidor: ${TARGET_SERVER}\nProducto: ${SLUG}"
    echo "SUCCESS:${NAME}" >> "$RESULTS_FILE"
    return 0

  else
    local SETUP_EXIT=$?
    echo "${PREFIX} ERROR: Provisioning falló (exit ${SETUP_EXIT}) en ${TARGET_SERVER}." >&2

    # Retry policy: si el setup completó pero la validación post falló (exit 2),
    # o si fue un error transitorio bajo MAX_RETRIES, devolver a Required para
    # próximo cron. Después de MAX_RETRIES, marcar Failed definitivo.
    local RETRY_COUNT
    RETRY_COUNT=$(airtable_mark_retry "$RECORD_ID" "${NOTES_VAL:-}" 2>/dev/null | tail -1)
    RETRY_COUNT="${RETRY_COUNT:-99}"

    if [[ "$RETRY_COUNT" -le "$MAX_RETRIES" ]] 2>/dev/null; then
      echo "${PREFIX} Retry ${RETRY_COUNT}/${MAX_RETRIES} programado — Pipeline_Status=Required" >&2
      slack_notify "🔁 *Provisioning retry ${RETRY_COUNT}/${MAX_RETRIES}*\nProyecto: ${NAME}\nServidor: ${TARGET_SERVER}\nExit: ${SETUP_EXIT}"
    else
      echo "${PREFIX} Max retries alcanzado — Pipeline_Status=Failed" >&2
      airtable_mark_failed "$RECORD_ID" > /dev/null
      slack_notify "❌ *Provisioning FALLÓ definitivo (${MAX_RETRIES}+1 intentos)*\nProyecto: ${NAME}\nServidor: ${TARGET_SERVER}\nProducto: ${SLUG}\nExit: ${SETUP_EXIT}"
    fi

    echo "FAILED:${NAME}" >> "$RESULTS_FILE"
    return 1
  fi
}
# ──────────────────────────────────────────────────────────────────────────

# Loop principal — modo serial (MAX_PARALLEL=1) o paralelo (MAX_PARALLEL>1).
RUNNING_JOBS=0
while IFS='|' read -r RECORD_ID NAME BASE_NAME EMAIL SLUG DOMAIN CLEAN_FLAG COLORS_VAL; do
  if [[ "$MAX_PARALLEL" -le 1 ]]; then
    # SERIAL: comportamiento clásico, llamada directa
    provision_record "$RECORD_ID" "$NAME" "$BASE_NAME" "$EMAIL" "$SLUG" "$DOMAIN" "$CLEAN_FLAG" "$COLORS_VAL" || true
  else
    # PARALELO: subshell en background con concurrency limit
    provision_record "$RECORD_ID" "$NAME" "$BASE_NAME" "$EMAIL" "$SLUG" "$DOMAIN" "$CLEAN_FLAG" "$COLORS_VAL" &
    RUNNING_JOBS=$((RUNNING_JOBS + 1))
    if [[ "$RUNNING_JOBS" -ge "$MAX_PARALLEL" ]]; then
      wait -n 2>/dev/null || true   # espera a que UNO termine
      RUNNING_JOBS=$((RUNNING_JOBS - 1))
    fi
  fi
  echo ""
done <<< "$RECORDS"

# Esperar todos los jobs en background restantes (si hubo paralelismo)
if [[ "$MAX_PARALLEL" -gt 1 ]]; then
  wait
fi

# Contar resultados desde RESULTS_FILE
SUCCEEDED=$(grep -c "^SUCCESS:" "$RESULTS_FILE" 2>/dev/null || echo 0)
FAILED=$(grep -c "^FAILED:" "$RESULTS_FILE" 2>/dev/null || echo 0)
FAILED_NAMES=()
while IFS=: read -r status name; do
  [[ "$status" == "FAILED" ]] && FAILED_NAMES+=("$name")
done < "$RESULTS_FILE"

echo "============================================="
if $DRY_RUN; then
  echo "  DRY RUN completado — ${COUNT} proyecto(s) en cola"
else
  echo "  Resultado: ${SUCCEEDED} OK, ${FAILED} fallidos"
  if [[ ${#FAILED_NAMES[@]} -gt 0 ]]; then
    echo "  Fallidos:"
    for n in "${FAILED_NAMES[@]}"; do
      echo "    - ${n}"
    done
  fi
fi
echo "============================================="

[[ $FAILED -gt 0 ]] && exit 1
exit 0
