#!/bin/sh
set -u

content_root=${CONTENT_ROOT:-/lzcapp/pkg/content}
. "$content_root/lib/node.sh"

max_body_size=16384
mutation_lock=${MUTATION_LOCK:-/tmp/lazycat-tailscale-mutation.lock}
mutation_lock_dir=${mutation_lock}.test
using_test_lock=false

send_headers() {
  status=$1
  content_type=$2
  printf 'Status: %s\r\n' "$status"
  printf 'Content-Type: %s; charset=utf-8\r\n' "$content_type"
  printf 'Cache-Control: no-store\r\n'
  printf 'X-Content-Type-Options: nosniff\r\n'
  printf '\r\n'
}

fail() {
  status=$1
  message=$2
  send_headers "$status" text/plain
  printf '%s\n' "$message"
  exit 0
}

require_admin() {
  if [ "${ALLOW_LOCAL_ADMIN:-false}" = "true" ]; then
    return
  fi

  if [ "${HTTP_X_FORWARDED_BY:-}" != "lzc-ingress" ] || \
     [ "${HTTP_X_HC_USER_ROLE:-}" != "ADMIN" ] || \
     [ "${HTTP_X_HC_SOURCE:-}" != "client" ] || \
     [ -z "${HTTP_X_HC_USER_ID:-}" ]; then
    fail "403 Forbidden" "只有微服管理员可以设置 Tailscale 节点。"
  fi
}

reject_cross_site_request() {
  if [ "${HTTP_SEC_FETCH_SITE:-}" = "cross-site" ]; then
    fail "403 Forbidden" "不接受跨站设置请求。"
  fi
}

require_form_request() {
  reject_cross_site_request

  if [ "${HTTP_X_REQUESTED_WITH:-}" != "lazycat-tailscale-setup" ]; then
    fail "403 Forbidden" "缺少同源请求标记。"
  fi

  case "${CONTENT_TYPE:-}" in
    application/x-www-form-urlencoded*) ;;
    *) fail "415 Unsupported Media Type" "只接受表单格式的设置请求。" ;;
  esac

  case "${CONTENT_LENGTH:-}" in
    ''|*[!0-9]*) fail "400 Bad Request" "无效的请求长度。" ;;
  esac
  if [ "${#CONTENT_LENGTH}" -gt 5 ]; then
    fail "413 Content Too Large" "设置内容过长。"
  fi
  if [ "$CONTENT_LENGTH" -gt "$max_body_size" ]; then
    fail "413 Content Too Large" "设置内容过长。"
  fi

  form_body=$(dd bs=1 count="$CONTENT_LENGTH" 2>/dev/null)
}

decode_auth_key() {
  encoded=$1
  case "$encoded" in *+*) return 1 ;; esac
  decoded=$(printf '%s' "$encoded" | sed \
    -e 's/%2[Bb]/+/g' \
    -e 's/%2[Ff]/\//g' \
    -e 's/%3[Dd]/=/g')
  case "$decoded" in *%*) return 1 ;; esac
  printf '%s' "$decoded"
}

decode_unreserved() {
  encoded=$1
  case "$encoded" in *%*|*+*) return 1 ;; esac
  printf '%s' "$encoded"
}

decode_routes() {
  encoded=$1
  decoded=$(printf '%s' "$encoded" | sed \
    -e 's/%2[Cc]/,/g' \
    -e 's/%2[Ff]/\//g' \
    -e 's/%3[Aa]/:/g')

  case "$decoded" in *%*|*+*) return 1 ;; esac
  decoded=$(printf '%s' "$decoded" | tr -d '[:space:]')
  validate_routes "$decoded" || return 1
  printf '%s' "$decoded"
}

emit_current_node() {
  status=${1:-200 OK}
  if ! read_node_status; then
    fail "503 Service Unavailable" "Tailscale 守护进程尚未就绪，请稍后重试。"
  fi
  send_headers "$status" application/json
  emit_node_json
}

release_test_lock() {
  if [ "$using_test_lock" = true ]; then
    rmdir "$mutation_lock_dir" 2>/dev/null || true
  fi
}

acquire_mutation_lock() {
  if [ "${DISABLE_TAILSCALE_TIMEOUT:-false}" = "true" ]; then
    mkdir "$mutation_lock_dir" 2>/dev/null || return 1
    using_test_lock=true
    trap release_test_lock 0 HUP INT TERM
    return 0
  fi

  exec 9>"$mutation_lock" || return 1
  busybox flock -n 9
}

join_tailnet() {
  case "$form_body" in
    action=join\&auth_key=*\&hostname=*\&routes=*\&exit_node=*\&accept_dns=*\&web_client=*) ;;
    *) fail "400 Bad Request" "首次连接表单格式无效。" ;;
  esac

  remaining=${form_body#action=join&auth_key=}
  auth_key_encoded=${remaining%%&hostname=*}
  remaining=${remaining#*&hostname=}
  hostname_encoded=${remaining%%&routes=*}
  remaining=${remaining#*&routes=}
  routes_encoded=${remaining%%&exit_node=*}
  exit_node=${remaining##*&exit_node=}
  join_accept_dns=${exit_node##*&accept_dns=}
  exit_node=${exit_node%%&accept_dns=*}
  join_web_client=${join_accept_dns##*&web_client=}
  join_accept_dns=${join_accept_dns%%&web_client=*}

  if ! auth_key=$(decode_auth_key "$auth_key_encoded"); then
    fail "400 Bad Request" "Auth key 格式无效。"
  fi
  if ! validate_auth_key "$auth_key"; then
    fail "400 Bad Request" "请填写 tskey-auth- 开头的 Auth key，不要填 API key。"
  fi
  if ! join_hostname=$(decode_unreserved "$hostname_encoded") || ! validate_hostname "$join_hostname"; then
    fail "400 Bad Request" "节点名必须是有效主机名；每段最长 63 个字符，总长最多 253。"
  fi
  if ! join_routes=$(decode_routes "$routes_encoded"); then
    fail "400 Bad Request" "家庭网段格式无效，请填写以逗号分隔的 CIDR。"
  fi
  join_exit_node=$exit_node
  case "$join_exit_node" in true|false) ;; *) fail "400 Bad Request" "Exit Node 开关值无效。" ;; esac
  case "$join_accept_dns" in true|false) ;; *) fail "400 Bad Request" "DNS 开关值无效。" ;; esac
  case "$join_web_client" in true|false) ;; *) fail "400 Bad Request" "Device Web 开关值无效。" ;; esac

  if ! read_node_status; then
    fail "503 Service Unavailable" "Tailscale 守护进程尚未就绪，请稍后重试。"
  fi
  if [ "$node_joined" = true ]; then
    unset auth_key
    send_headers "200 OK" application/json
    emit_node_json
    exit 0
  fi
  if [ "$node_state" = NeedsMachineAuth ] || \
     { [ "$node_state" = Starting ] && [ "$node_have_node_key" = true ]; }; then
      unset auth_key
      send_headers "202 Accepted" application/json
      emit_node_json
      exit 0
  fi
  case "$node_state" in
    NeedsLogin) ;;
    Stopped)
      if [ "$node_have_node_key" = true ]; then
        unset auth_key
        fail "409 Conflict" "节点已注册但当前停用，请等待自动恢复。"
      fi
      ;;
    *)
      unset auth_key
      fail "503 Service Unavailable" "Tailscale 节点仍在启动，请稍后重试。"
      ;;
  esac

  if ! acquire_mutation_lock; then
    unset auth_key
    fail "409 Conflict" "另一个节点设置请求正在处理，请稍候。"
  fi

  if ! read_node_status; then
    unset auth_key
    fail "503 Service Unavailable" "Tailscale 守护进程尚未就绪，请稍后重试。"
  fi
  if [ "$node_joined" = true ]; then
    unset auth_key
    send_headers "200 OK" application/json
    emit_node_json
    exit 0
  fi
  if [ "$node_state" = NeedsMachineAuth ] || \
     { [ "$node_state" = Starting ] && [ "$node_have_node_key" = true ]; }; then
      unset auth_key
      send_headers "202 Accepted" application/json
      emit_node_json
      exit 0
  fi
  if [ "$node_state" = Stopped ] && [ "$node_have_node_key" = true ]; then
    unset auth_key
    fail "409 Conflict" "节点已注册但当前停用，请等待自动恢复。"
  fi

  start_tailnet_join "$auth_key" "$join_hostname" "$join_routes" "$join_exit_node" "$join_accept_dns" || true
  unset auth_key auth_key_encoded form_body remaining

  if ! read_node_status; then
    fail "503 Service Unavailable" "已提交连接请求，但暂时无法读取节点状态。"
  fi

  # `tailscale up --reset` resets RunWebClient, so this preference must be
  # applied after enrollment. A failure is reported separately: the node
  # identity is already valid and the one-off Auth key must never be reused.
  if [ "$node_have_node_key" = true ] && [ "$node_web_client_requested" != "$join_web_client" ]; then
    if ! set_device_web "$join_web_client" || ! read_node_status; then
      node_web_client_apply_failed=true
    elif [ "$node_web_client_requested" != "$join_web_client" ]; then
      node_web_client_apply_failed=true
    fi
  fi

  if [ "$node_joined" = true ]; then
    send_headers "200 OK" application/json
    emit_node_json
    exit 0
  fi

  case "$node_state" in
    NeedsMachineAuth|Starting)
      send_headers "202 Accepted" application/json
      emit_node_json
      ;;
    NeedsLogin)
      fail "422 Unprocessable Content" "加入 tailnet 失败。请检查 Auth key 是否有效、未过期，以及微服是否能访问公网。"
      ;;
    *) fail "503 Service Unavailable" "Tailscale 暂时未能进入可用状态。" ;;
  esac
}

resume_existing_node() {
  if [ "$form_body" != "action=resume" ]; then
    fail "400 Bad Request" "恢复已有节点的请求格式无效。"
  fi
  if ! read_node_status; then
    fail "503 Service Unavailable" "Tailscale 守护进程尚未就绪，请稍后重试。"
  fi
  if [ "$node_joined" = true ]; then
    send_headers "200 OK" application/json
    emit_node_json
    exit 0
  fi
  if [ "$node_have_node_key" != true ]; then
    fail "409 Conflict" "节点没有可恢复的已有身份。"
  fi
  case "$node_state" in
    NeedsMachineAuth|Starting)
      send_headers "202 Accepted" application/json
      emit_node_json
      exit 0
      ;;
    Stopped) ;;
    *) fail "409 Conflict" "节点需要重新鉴权。" ;;
  esac

  if ! acquire_mutation_lock; then
    fail "409 Conflict" "另一个节点设置请求正在处理，请稍候。"
  fi
  if ! read_node_status; then
    fail "503 Service Unavailable" "Tailscale 守护进程尚未就绪，请稍后重试。"
  fi
  if [ "$node_state" = Stopped ] && [ "$node_have_node_key" = true ]; then
    resume_tailnet || true
  fi
  if ! read_node_status; then
    fail "503 Service Unavailable" "已请求恢复节点，但暂时无法读取状态。"
  fi
  if [ "$node_joined" = true ]; then
    send_headers "200 OK" application/json
    emit_node_json
    exit 0
  fi
  case "$node_state" in
    NeedsMachineAuth|Starting)
      send_headers "202 Accepted" application/json
      emit_node_json
      ;;
    *) fail "409 Conflict" "节点需要重新鉴权。" ;;
  esac
}

enable_web_client() {
  if [ "$form_body" != "action=enable_web" ]; then
    fail "400 Bad Request" "启用官方设置页的请求格式无效。"
  fi
  if ! read_node_status; then
    fail "503 Service Unavailable" "Tailscale 守护进程尚未就绪，请稍后重试。"
  fi
  if [ "$node_have_node_key" != true ]; then
    fail "409 Conflict" "节点尚未获得可重用的 Tailscale 身份。"
  fi
  case "$node_state" in
    Running|NeedsMachineAuth|Starting) ;;
    *) fail "409 Conflict" "节点当前无法启用官方设置页。" ;;
  esac
  if [ "$node_web_client_requested" = true ]; then
    if [ "$node_joined" = true ]; then
      send_headers "200 OK" application/json
    else
      send_headers "202 Accepted" application/json
    fi
    emit_node_json
    exit 0
  fi
  if ! acquire_mutation_lock; then
    fail "409 Conflict" "另一个节点设置请求正在处理，请稍候。"
  fi
  if ! read_node_status || [ "$node_have_node_key" != true ]; then
    fail "409 Conflict" "节点状态已经变化，请刷新后重试。"
  fi
  case "$node_state" in
    Running|NeedsMachineAuth|Starting) ;;
    *) fail "409 Conflict" "节点当前无法启用官方设置页。" ;;
  esac
  if ! enable_device_web || ! read_node_status; then
    fail "503 Service Unavailable" "暂时无法启用 Tailscale 官方设置页。"
  fi
  if [ "$node_web_client_requested" != true ]; then
    fail "503 Service Unavailable" "Tailscale 尚未确认启用官方设置页，请重试。"
  fi
  if [ "$node_joined" = true ]; then
    send_headers "200 OK" application/json
  else
    send_headers "202 Accepted" application/json
  fi
  emit_node_json
}

rename_node() {
  case "$form_body" in
    action=rename\&hostname=*) ;;
    *) fail "400 Bad Request" "设备名称表单格式无效。" ;;
  esac

  hostname_encoded=${form_body#action=rename&hostname=}
  if ! renamed_hostname=$(decode_unreserved "$hostname_encoded") || \
     ! validate_hostname "$renamed_hostname"; then
    fail "400 Bad Request" "节点名必须是有效主机名；每段最长 63 个字符，总长最多 253。"
  fi

  if ! read_node_status; then
    fail "503 Service Unavailable" "Tailscale 守护进程尚未就绪，请稍后重试。"
  fi
  if [ "$node_joined" != true ]; then
    fail "409 Conflict" "节点尚未加入 tailnet，暂时不能修改设备名称。"
  fi
  if ! acquire_mutation_lock; then
    fail "409 Conflict" "另一个节点设置请求正在处理，请稍候。"
  fi
  if ! read_node_status || [ "$node_joined" != true ]; then
    fail "409 Conflict" "节点状态已经变化，请刷新后重试。"
  fi

  if ! set_node_hostname "$renamed_hostname"; then
    fail "422 Unprocessable Content" "Tailscale 拒绝了设备名称，请检查后重试。"
  fi
  if ! read_node_status; then
    fail "503 Service Unavailable" "设备名称已提交，但暂时无法读取更新后的节点状态。"
  fi

  send_headers "200 OK" application/json
  emit_node_json
}

save_node_settings() {
  case "$form_body" in
    action=save\&hostname=*\&routes=*\&exit_node=*\&accept_dns=*\&web_client=*) ;;
    *) fail "400 Bad Request" "节点设置表单格式无效。" ;;
  esac

  remaining=${form_body#action=save&hostname=}
  hostname_encoded=${remaining%%&routes=*}
  remaining=${remaining#*&routes=}
  routes_encoded=${remaining%%&exit_node=*}
  remaining=${remaining#*&exit_node=}
  exit_node=${remaining%%&accept_dns=*}
  remaining=${remaining#*&accept_dns=}
  accept_dns=${remaining%%&web_client=*}
  web_client=${remaining##*&web_client=}

  if ! saved_hostname=$(decode_unreserved "$hostname_encoded") || \
     ! validate_hostname "$saved_hostname"; then
    fail "400 Bad Request" "节点名必须是有效主机名；每段最长 63 个字符，总长最多 253。"
  fi
  if ! saved_routes=$(decode_routes "$routes_encoded"); then
    fail "400 Bad Request" "家庭网段格式无效，请填写以逗号分隔的 CIDR。"
  fi
  case "$exit_node" in
    true|false) ;;
    *) fail "400 Bad Request" "Exit Node 开关值无效。" ;;
  esac
  case "$web_client" in
    true|false) ;;
    *) fail "400 Bad Request" "Device Web 开关值无效。" ;;
  esac
  case "$accept_dns" in
    true|false) ;;
    *) fail "400 Bad Request" "DNS 开关值无效。" ;;
  esac

  if ! read_node_status; then
    fail "503 Service Unavailable" "Tailscale 守护进程尚未就绪，请稍后重试。"
  fi
  if [ "$node_joined" != true ]; then
    fail "409 Conflict" "节点尚未加入 tailnet，暂时不能修改运行设置。"
  fi
  if ! acquire_mutation_lock; then
    fail "409 Conflict" "另一个节点设置请求正在处理，请稍候。"
  fi
  if ! read_node_status || [ "$node_joined" != true ]; then
    fail "409 Conflict" "节点状态已经变化，请刷新后重试。"
  fi

  if ! apply_node_settings "$saved_hostname" "$saved_routes" "$exit_node" "$web_client" "$accept_dns"; then
    fail "422 Unprocessable Content" "Tailscale 拒绝了设置，请检查节点名与 CIDR 后重试。"
  fi
  if ! read_node_status; then
    fail "503 Service Unavailable" "设置已提交，但暂时无法读取更新后的节点状态。"
  fi

  send_headers "200 OK" application/json
  emit_node_json
}

set_network_mode() {
  case "$form_body" in
    action=mode\&network_mode=userspace) requested_network_mode=userspace ;;
    action=mode\&network_mode=kernel) requested_network_mode=kernel ;;
    *) fail "400 Bad Request" "网络模式请求格式无效。" ;;
  esac

  # Mode recovery must remain available even when the requested daemon mode
  # cannot start. A failed kernel launch otherwise makes LocalAPI unavailable
  # and would prevent the administrator from switching back to userspace.
  read_node_status || read_network_modes
  if ! acquire_mutation_lock; then
    fail "409 Conflict" "另一个节点设置请求正在处理，请稍候。"
  fi
  read_node_status || read_network_modes

  if [ "$requested_network_mode" = kernel ] && \
     [ "$node_kernel_mode_available" != true ]; then
    fail "409 Conflict" "Kernel TUN 所需权限或转发能力不可用。"
  fi

  if [ "$node_network_mode" = "$requested_network_mode" ] && \
     [ "$node_active_network_mode" = "$requested_network_mode" ]; then
    send_headers "200 OK" application/json
    emit_node_json
    exit 0
  fi

  if ! request_network_mode "$requested_network_mode"; then
    fail "503 Service Unavailable" "无法保存网络模式，请稍后重试。"
  fi

  send_headers "202 Accepted" application/json
  emit_node_json
}

require_admin

case "${REQUEST_METHOD:-}" in
  GET) emit_current_node ;;
  POST)
    require_form_request
    case "$form_body" in
      action=join\&*) join_tailnet ;;
      action=resume) resume_existing_node ;;
      action=enable_web) enable_web_client ;;
      action=rename\&*) rename_node ;;
      action=save\&*) save_node_settings ;;
      action=mode\&*) set_network_mode ;;
      *) fail "400 Bad Request" "未知的设置操作。" ;;
    esac
    ;;
  *) fail "405 Method Not Allowed" "只支持 GET 与 POST。" ;;
esac
