#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.dev.yml"
BACKEND_ENV="${ROOT_DIR}/backend/.env"
BACKEND_ENV_EXAMPLE="${ROOT_DIR}/backend/.env.example"
FRONTEND_DIR="${ROOT_DIR}/frontend"
FRONTEND_PACKAGE_JSON="${FRONTEND_DIR}/package.json"
FRONTEND_PACKAGE_LOCK="${FRONTEND_DIR}/package-lock.json"
FRONTEND_NODE_MODULES="${FRONTEND_DIR}/node_modules"
FRONTEND_NODE_MODULES_LOCK="${FRONTEND_NODE_MODULES}/.package-lock.json"
MIGRATIONS_DIR="${ROOT_DIR}/backend/migrations"
MYSQL_DATABASE="${DEV_MYSQL_DATABASE:-hfb_sys}"
BACKEND_PORT="${DEV_BACKEND_PORT:-8080}"
FRONTEND_PORT="${DEV_FRONTEND_PORT:-5173}"
BACKEND_HEALTH_URL="${DEV_BACKEND_HEALTH_URL:-http://127.0.0.1:${BACKEND_PORT}/health}"
FRONTEND_HEALTH_URL="${DEV_FRONTEND_HEALTH_URL:-http://127.0.0.1:${FRONTEND_PORT}/}"
HTTP_READY_TIMEOUT="${DEV_HTTP_READY_TIMEOUT:-120}"
AUTO_KILL_PORTS="${DEV_AUTO_KILL_PORTS:-1}"
FORCE_NPM_CI="${DEV_FORCE_NPM_CI:-0}"
GOOSE_VERSION="${GOOSE_VERSION:-v3.27.1}"
GOOSE_BIN="${GOOSE_BIN:-}"

BACKEND_PID=""
FRONTEND_PID=""
CLEANED_UP=0

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[1;34m'
NC='\033[0m'

log() {
  printf "${BLUE}[dev]${NC} %s\n" "$*"
}

log_success() {
  printf "${GREEN}[dev]${NC} %s\n" "$*"
}

log_warn() {
  printf "${YELLOW}[dev]${NC} %s\n" "$*"
}

log_error() {
  printf "${RED}[dev]${NC} %s\n" "$*" >&2
}

need_cmd() {
  if ! command -v "$1" >/dev/null 2>&1; then
    log_error "缺少命令：$1"
    exit 1
  fi
}

show_help() {
  cat << EOF
用法: ./scripts/dev.sh [选项]

选项:
  --reset-db      重置数据库（删除并重建）
  --no-migrate    不执行数据库迁移
  --no-frontend   不启动前端
  --no-backend    不启动后端
  -h, --help      显示帮助信息

环境变量:
  DEV_RESET_DB=1  等同于 --reset-db
  DEV_MYSQL_DATABASE=hfb_sys
                  开发数据库名
  DEV_BACKEND_PORT=8080
                  后端启动端口
  DEV_FRONTEND_PORT=5173
                  前端启动端口
  DEV_BACKEND_HEALTH_URL=http://127.0.0.1:8080/health
                  后端健康检查地址
  DEV_FRONTEND_HEALTH_URL=http://127.0.0.1:5173/
                  前端健康检查地址
  DEV_HTTP_READY_TIMEOUT=120
                  HTTP 服务就绪等待秒数
  DEV_AUTO_KILL_PORTS=1
                  前后端端口被占用时自动停止占用的 Docker 容器或本机进程
  DEV_FORCE_NPM_CI=0
                  强制重新执行前端 npm ci

示例:
  ./scripts/dev.sh                 # 正常启动
  ./scripts/dev.sh --reset-db      # 重置数据库后启动（推荐迁移后使用）
EOF
  exit 0
}

# 参数解析
RESET_DB="${DEV_RESET_DB:-0}"
NO_MIGRATE=0
NO_FRONTEND=0
NO_BACKEND=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --reset-db)
      RESET_DB=1
      shift
      ;;
    --no-migrate)
      NO_MIGRATE=1
      shift
      ;;
    --no-frontend)
      NO_FRONTEND=1
      shift
      ;;
    --no-backend)
      NO_BACKEND=1
      shift
      ;;
    -h|--help)
      show_help
      ;;
    *)
      log_error "未知选项: $1"
      show_help
      ;;
  esac
done

cleanup() {
  if [[ "${CLEANED_UP}" == "1" ]]; then
    return
  fi
  CLEANED_UP=1

  if [[ -z "${FRONTEND_PID}" && -z "${BACKEND_PID}" ]]; then
    return
  fi

  log "正在停止前后端开发进程..."
  if [[ -n "${FRONTEND_PID}" ]] && kill -0 "${FRONTEND_PID}" >/dev/null 2>&1; then
    kill "${FRONTEND_PID}" >/dev/null 2>&1 || true
  fi
  if [[ -n "${BACKEND_PID}" ]] && kill -0 "${BACKEND_PID}" >/dev/null 2>&1; then
    kill "${BACKEND_PID}" >/dev/null 2>&1 || true
  fi
}

wait_container_healthy() {
  local container_name="$1"
  local label="$2"
  local attempt

  log "等待 ${label} 就绪..."
  for attempt in {1..60}; do
    local status
    status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${container_name}" 2>/dev/null || true)"
    if [[ "${status}" == "healthy" || "${status}" == "running" ]]; then
      log "${label} 已就绪"
      return 0
    fi
    sleep 2
  done

  printf "%s 启动超时，请检查：docker logs %s\n" "${label}" "${container_name}" >&2
  exit 1
}

wait_http_ready() {
  local url="$1"
  local label="$2"
  local pid="${3:-}"
  local attempt

  log "等待 ${label} HTTP 服务就绪..."
  for ((attempt = 1; attempt <= HTTP_READY_TIMEOUT; attempt++)); do
    if [[ -n "${pid}" ]] && ! kill -0 "${pid}" >/dev/null 2>&1; then
      wait "${pid}" || true
      log_error "${label} 进程已退出，请检查上方日志"
      exit 1
    fi

    if curl -fsS --max-time 2 "${url}" >/dev/null 2>&1; then
      log_success "${label} 已就绪：${url}"
      return 0
    fi

    sleep 1
  done

  log_error "${label} HTTP 服务启动超时：${url}"
  exit 1
}

port_in_use() {
  local port="$1"

  if command -v lsof >/dev/null 2>&1; then
    lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
    return $?
  fi

  if command -v nc >/dev/null 2>&1; then
    nc -z 127.0.0.1 "${port}" >/dev/null 2>&1
    return $?
  fi

  return 1
}

wait_port_released() {
  local port="$1"
  local timeout="${2:-5}"
  local attempt

  for ((attempt = 1; attempt <= timeout; attempt++)); do
    if ! port_in_use "${port}"; then
      return 0
    fi
    sleep 1
  done

  return 1
}

is_allowed_container() {
  local container_name="$1"
  shift

  local allowed_name
  for allowed_name in "$@"; do
    if [[ "${container_name}" == "${allowed_name}" ]]; then
      return 0
    fi
  done

  return 1
}

docker_containers_on_host_port() {
  local port="$1"

  docker ps --format '{{.ID}}\t{{.Names}}\t{{.Ports}}' 2>/dev/null |
    awk -F '\t' -v port="${port}" 'index($3, ":" port "->") || $3 ~ ("(^|[ ,])" port "->") { print $1 "\t" $2 }'
}

stop_docker_containers_on_port() {
  local port="$1"
  local label="$2"
  shift 2

  local containers container_id container_name allowed_found=0 stopped_found=0
  containers="$(docker_containers_on_host_port "${port}" || true)"
  if [[ -z "${containers}" ]]; then
    return 1
  fi

  while IFS=$'\t' read -r container_id container_name; do
    [[ -z "${container_id}" ]] && continue

    if is_allowed_container "${container_name}" "$@"; then
      allowed_found=1
      log "${label}端口 ${port} 已由本项目容器 ${container_name} 使用，保留运行"
      continue
    fi

    if [[ "${AUTO_KILL_PORTS}" != "1" ]]; then
      log_error "${label}端口 ${port} 被 Docker 容器 ${container_name} 占用"
      exit 1
    fi

    log_warn "${label}端口 ${port} 被 Docker 容器 ${container_name} 占用，正在停止..."
    docker stop "${container_id}" >/dev/null
    stopped_found=1
    log_success "已停止容器 ${container_name}"
  done <<< "${containers}"

  if [[ "${allowed_found}" == "1" ]]; then
    return 0
  fi

  if [[ "${stopped_found}" == "1" ]]; then
    wait_port_released "${port}" 5 || true
  fi

  return 1
}

process_args() {
  local pid="$1"
  ps -p "${pid}" -o args= 2>/dev/null || true
}

is_docker_proxy_process() {
  local args="$1"

  [[ "${args}" == *"com.docker"* || "${args}" == *"Docker Desktop"* || "${args}" == *"vpnkit"* || "${args}" == *"OrbStack"* ]]
}

ensure_port_free() {
  local port="$1"
  local label="$2"
  shift 2

  if stop_docker_containers_on_port "${port}" "${label}" "$@"; then
    return 0
  fi

  if command -v lsof >/dev/null 2>&1; then
    local pids pid args
    pids="$(lsof -nP -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | sort -u || true)"
    if [[ -z "${pids}" ]]; then
      return 0
    fi

    if [[ "${AUTO_KILL_PORTS}" != "1" ]]; then
      log_error "${label}端口 ${port} 已被占用，请先停止旧进程或调整端口"
      exit 1
    fi

    log_warn "${label}端口 ${port} 已被占用，正在停止占用进程..."
    while IFS= read -r pid; do
      [[ -z "${pid}" ]] && continue
      args="$(process_args "${pid}")"

      if is_docker_proxy_process "${args}"; then
        log_error "${label}端口 ${port} 仍由 Docker 代理进程占用，但未找到可自动停止的容器，请执行 docker ps 检查"
        exit 1
      fi

      log_warn "停止 PID ${pid}: ${args:-未知进程}"
      kill "${pid}" >/dev/null 2>&1 || true
    done <<< "${pids}"

    if ! wait_port_released "${port}" 5; then
      log_warn "${label}端口 ${port} 未及时释放，尝试强制停止..."
      pids="$(lsof -nP -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | sort -u || true)"
      while IFS= read -r pid; do
        [[ -z "${pid}" ]] && continue
        args="$(process_args "${pid}")"

        if is_docker_proxy_process "${args}"; then
          log_error "${label}端口 ${port} 仍由 Docker 代理进程占用，但未找到可自动停止的容器，请执行 docker ps 检查"
          exit 1
        fi

        log_warn "强制停止 PID ${pid}: ${args:-未知进程}"
        kill -9 "${pid}" >/dev/null 2>&1 || true
      done <<< "${pids}"

      if ! wait_port_released "${port}" 5; then
        log_error "${label}端口 ${port} 释放失败，请手动检查占用进程"
        exit 1
      fi
    fi

    log_success "${label}端口 ${port} 已释放"
    return 0
  fi

  if command -v nc >/dev/null 2>&1; then
    if nc -z 127.0.0.1 "${port}" >/dev/null 2>&1; then
      log_error "${label}端口 ${port} 已被占用，但未找到 lsof，无法自动定位并停止占用进程"
      exit 1
    fi
    return 0
  fi

  log_warn "未找到 lsof 或 nc，跳过 ${label}端口 ${port} 占用检查"
}

mysql_root() {
  docker exec -i -e MYSQL_PWD=rootsecret hfb-mysql mysql -uroot "$@"
}

validate_database_name() {
  if [[ ! "${MYSQL_DATABASE}" =~ ^[A-Za-z0-9_]+$ ]]; then
    log_error "DEV_MYSQL_DATABASE 只能包含字母、数字和下划线，当前值：${MYSQL_DATABASE}"
    exit 1
  fi
}

reset_database() {
  if [[ "${RESET_DB}" != "1" ]]; then
    return 0
  fi

  log_warn "重置数据库 ${MYSQL_DATABASE}..."
  mysql_root -e \
    "DROP DATABASE IF EXISTS \`${MYSQL_DATABASE}\`; CREATE DATABASE \`${MYSQL_DATABASE}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
  log_success "数据库已重建"
}

ensure_goose() {
  if [[ -z "${GOOSE_BIN}" ]]; then
    GOOSE_BIN="$(command -v goose || true)"
  fi

  if [[ -z "${GOOSE_BIN}" ]]; then
    need_cmd go
    log "未找到 goose，正在安装 ${GOOSE_VERSION}..."
    (cd "${ROOT_DIR}/backend" && go install "github.com/pressly/goose/v3/cmd/goose@${GOOSE_VERSION}")
    GOOSE_BIN="$(go env GOPATH)/bin/goose"
  fi

  if [[ ! -x "${GOOSE_BIN}" ]]; then
    log_error "goose 不可执行：${GOOSE_BIN}"
    exit 1
  fi
}

goose_dsn() {
  local dsn="$1"
  if [[ "${dsn}" != *"multiStatements="* ]]; then
    if [[ "${dsn}" == *"?"* ]]; then
      dsn="${dsn}&multiStatements=true"
    else
      dsn="${dsn}?multiStatements=true"
    fi
  fi
  printf "%s" "${dsn}"
}

dev_goose_dsn() {
  goose_dsn "hfb:secret@tcp(127.0.0.1:3306)/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local"
}

goose_cmd() {
  "${GOOSE_BIN}" -dir "${MIGRATIONS_DIR}" mysql "$(dev_goose_dsn)" "$@"
}

run_migrations() {
  if [[ "${NO_MIGRATE}" == "1" ]]; then
    log_warn "已跳过数据库迁移"
    return 0
  fi

  ensure_goose
  log "使用 goose 执行数据库迁移..."
  goose_cmd up
  log_success "数据库迁移完成"
}

load_env_file() {
  local env_file="$1"
  local line

  while IFS= read -r line || [[ -n "${line}" ]]; do
    [[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue
    line="${line#export }"
    export "${line}"
  done < "${env_file}"
}

start_backend() {
  if [[ ! -f "${BACKEND_ENV}" ]]; then
    log "创建 backend/.env"
    cp "${BACKEND_ENV_EXAMPLE}" "${BACKEND_ENV}"
  fi

  log "启动后端：http://localhost:8080"
  (
    cd "${ROOT_DIR}/backend"
    load_env_file "${BACKEND_ENV}"
    go run ./cmd/api
  ) &
  BACKEND_PID="$!"
}

frontend_dependencies_need_ci() {
  if [[ "${FORCE_NPM_CI}" == "1" ]]; then
    return 0
  fi

  if [[ ! -d "${FRONTEND_NODE_MODULES}" ]]; then
    return 0
  fi

  if [[ ! -f "${FRONTEND_NODE_MODULES_LOCK}" ]]; then
    return 0
  fi

  if [[ "${FRONTEND_PACKAGE_JSON}" -nt "${FRONTEND_NODE_MODULES_LOCK}" || "${FRONTEND_PACKAGE_LOCK}" -nt "${FRONTEND_NODE_MODULES_LOCK}" ]]; then
    return 0
  fi

  (
    cd "${FRONTEND_DIR}"
    npm ls --depth=0 >/dev/null 2>&1
  ) || return 0

  return 1
}

ensure_frontend_dependencies() {
  if [[ ! -f "${FRONTEND_PACKAGE_LOCK}" ]]; then
    log_error "缺少 frontend/package-lock.json，无法执行 npm ci"
    exit 1
  fi

  if frontend_dependencies_need_ci; then
    log "同步前端依赖（npm ci）..."
    (
      cd "${FRONTEND_DIR}"
      npm ci
    )
    log_success "前端依赖已同步"
  else
    log "前端依赖已同步，跳过 npm ci"
  fi
}

start_frontend() {
  ensure_frontend_dependencies

  log "启动前端：http://localhost:5173"
  (
    cd "${FRONTEND_DIR}"
    npm run dev
  ) &
  FRONTEND_PID="$!"
}

watch_processes() {
  while true; do
    if [[ -n "${BACKEND_PID}" ]] && ! kill -0 "${BACKEND_PID}" >/dev/null 2>&1; then
      wait "${BACKEND_PID}" || true
      printf "后端进程已退出\n" >&2
      exit 1
    fi
    if [[ -n "${FRONTEND_PID}" ]] && ! kill -0 "${FRONTEND_PID}" >/dev/null 2>&1; then
      wait "${FRONTEND_PID}" || true
      printf "前端进程已退出\n" >&2
      exit 1
    fi
    sleep 2
  done
}

main() {
  need_cmd docker
  if [[ "${NO_BACKEND}" == "0" ]]; then
    need_cmd go
  fi
  if [[ "${NO_FRONTEND}" == "0" ]]; then
    need_cmd npm
  fi
  if [[ "${NO_BACKEND}" == "0" || "${NO_FRONTEND}" == "0" ]]; then
    need_cmd curl
  fi

  trap cleanup EXIT
  trap 'cleanup; exit 130' INT TERM

  validate_database_name
  if [[ "${RESET_DB}" == "1" && "${NO_MIGRATE}" == "1" ]]; then
    log_error "--reset-db 会清空数据库，不能和 --no-migrate 同时使用"
    exit 1
  fi

  log "启动开发依赖：MySQL、Redis、MinIO"
  docker compose -f "${COMPOSE_FILE}" up -d

  wait_container_healthy hfb-mysql "MySQL"
  wait_container_healthy hfb-redis "Redis"
  wait_container_healthy hfb-minio "MinIO"

  reset_database
  run_migrations

  if [[ "${NO_BACKEND}" == "0" ]]; then
    ensure_port_free "${BACKEND_PORT}" "后端"
    start_backend
    wait_http_ready "${BACKEND_HEALTH_URL}" "后端" "${BACKEND_PID}"
  fi

  if [[ "${NO_FRONTEND}" == "0" ]]; then
    ensure_port_free "${FRONTEND_PORT}" "前端"
    start_frontend
    wait_http_ready "${FRONTEND_HEALTH_URL}" "前端" "${FRONTEND_PID}"
  fi

  if [[ -z "${BACKEND_PID}" && -z "${FRONTEND_PID}" ]]; then
    log "仅启动 Docker 开发依赖，前后端未启动"
    return 0
  fi

  log_success "开发环境已启动"
  if [[ "${NO_FRONTEND}" == "0" ]]; then
    local admin_base_path="${VITE_ADMIN_BASE_PATH:-/admin}"
    log "前台：http://localhost:5173"
    log "后台：http://localhost:5173${admin_base_path%/}/login"
  fi

  log "按 Ctrl+C 停止前后端；Docker 依赖会保留运行"

  watch_processes
}

main "$@"
