#!/usr/bin/env sh

# DNS API plugin for acme.sh
# Environment variables:
#   MGDNS_HOST  - domain or full API URL
#   MGDNS_EMAIL - WHMCS client email (username header)
#   MGDNS_TOKEN - raw API key used to build hourly auth token
# Optional:
#   MGDNS_TTL      - TXT record TTL (default: 60)
#   MGDNS_INSECURE - set to 1 to skip TLS verification

MGDNS_TTL="${MGDNS_TTL:-60}"
dns_mgdns_info="MGDNS_HOST MGDNS_EMAIL MGDNS_TOKEN"

_mgdns_last_body=""
_mgdns_last_http_code=""
_mgdns_last_curl_exit=""
_mgdns_base_url=""

_dns_mgdns_add() {
  dns_mgdns_add "$@"
}

dns_mgdns_add() {
  fulldomain="$1"
  txtvalue="$2"

  _mgdns_load_env || return 1

  _mgdns_info "Using MGDNS DNS API"
  _mgdns_debug "Full domain" "$fulldomain"

  _mgdns_find_zone "$fulldomain" || {
    _mgdns_err "Unable to determine zone for: $fulldomain"
    return 1
  }

  _record_name="$_sub_domain"
  [ -z "$_record_name" ] && _record_name="@"

  # Check whether the desired TXT already exists.
  _mgdns_api_request GET "/zone/$_domain" || {
    _mgdns_err "Failed to fetch zone records before add"
    return 1
  }

  _list_response="$_mgdns_last_body"
  _mgdns_debug "Pre-add check HTTP code" "$_mgdns_last_http_code"
  _mgdns_debug "Pre-add check response" "$_list_response"

  if _mgdns_http_ok; then
    _existing_ids="$(_mgdns_extract_txt_ids "$_list_response" "$_record_name" "$_domain" "$txtvalue")"
    if [ -n "$_existing_ids" ]; then
      _mgdns_info "TXT record already exists: $_record_name.$_domain"
      return 0
    fi
  fi

  _payload="name=$(_mgdns_urlencode "$_record_name")&type=TXT&ttl=$(_mgdns_urlencode "$MGDNS_TTL")&rdata[txtdata]=$(_mgdns_urlencode "$txtvalue")"

  _mgdns_api_request POST "/zone/$_domain/record" "$_payload" || {
    _mgdns_err "Failed to add TXT record"
    return 1
  }

  _response="$_mgdns_last_body"
  _mgdns_debug "HTTP code" "$_mgdns_last_http_code"
  _mgdns_debug "Response" "$_response"

  if _mgdns_http_ok && _mgdns_response_ok "$_response"; then
    _mgdns_info "TXT record added: $_record_name.$_domain"
    return 0
  fi

  # Provider may return duplicate as an error even when the record already exists.
  if _mgdns_contains "$_response" 'Duplicate record in RRset'; then
    _mgdns_info "TXT record already exists (provider returned duplicate): $_record_name.$_domain"
    return 0
  fi

  _mgdns_err "Failed to add TXT record (HTTP: ${_mgdns_last_http_code:-unknown})"
  _mgdns_err "API response: $_response"
  return 1
}

_dns_mgdns_rm() {
  dns_mgdns_rm "$@"
}

dns_mgdns_rm() {
  fulldomain="$1"
  txtvalue="$2"

  _mgdns_load_env || return 1

  _mgdns_find_zone "$fulldomain" || {
    _mgdns_err "Unable to determine zone for: $fulldomain"
    return 1
  }

  _record_name="$_sub_domain"
  [ -z "$_record_name" ] && _record_name="@"

  _mgdns_api_request GET "/zone/$_domain" || {
    _mgdns_err "Failed to fetch zone records"
    return 1
  }

  _list_response="$_mgdns_last_body"
  _mgdns_debug "HTTP code" "$_mgdns_last_http_code"
  _mgdns_debug "List response" "$_list_response"

  if ! _mgdns_http_ok; then
    _mgdns_err "Failed to fetch zone records (HTTP: ${_mgdns_last_http_code:-unknown})"
    _mgdns_err "API response: $_list_response"
    return 1
  fi

  _ids="$(_mgdns_extract_txt_ids "$_list_response" "$_record_name" "$_domain" "$txtvalue")"

  if [ -z "$_ids" ]; then
    _mgdns_info "No matching TXT records to remove"
    return 0
  fi

  _removed=0
  for _id in $_ids; do
    _mgdns_api_request DELETE "/zone/$_domain/record/$_id" || {
      _mgdns_err "Failed removing record id: $_id"
      return 1
    }

    _delete_response="$_mgdns_last_body"
    _mgdns_debug "Delete HTTP code" "$_mgdns_last_http_code"
    _mgdns_debug "Delete response" "$_delete_response"

    if _mgdns_http_ok && _mgdns_response_ok "$_delete_response"; then
      _removed=$((_removed + 1))
    else
      _mgdns_err "Failed removing record id: $_id (HTTP: ${_mgdns_last_http_code:-unknown})"
      _mgdns_err "API response: $_delete_response"
      return 1
    fi
  done

  _mgdns_info "Removed $_removed TXT record(s)"
  return 0
}

_mgdns_check_deps() {
  if ! command -v curl >/dev/null 2>&1; then
    _mgdns_err "curl is required"
    return 1
  fi

  if ! command -v openssl >/dev/null 2>&1; then
    _mgdns_err "openssl is required"
    return 1
  fi

  if ! command -v sed >/dev/null 2>&1; then
    _mgdns_err "sed is required"
    return 1
  fi

  if ! command -v mktemp >/dev/null 2>&1; then
    _mgdns_err "mktemp is required"
    return 1
  fi

  if ! command -v python3 >/dev/null 2>&1 && ! command -v python >/dev/null 2>&1; then
    _mgdns_err "python3 or python is required"
    return 1
  fi

  return 0
}

_mgdns_load_env() {
  _mgdns_check_deps || return 1

  MGDNS_HOST="${MGDNS_HOST:-$(_readaccountconf_mutable MGDNS_HOST)}"
  MGDNS_EMAIL="${MGDNS_EMAIL:-$(_readaccountconf_mutable MGDNS_EMAIL)}"
  MGDNS_TOKEN="${MGDNS_TOKEN:-$(_readaccountconf_mutable MGDNS_TOKEN)}"

  if [ -z "$MGDNS_HOST" ] || [ -z "$MGDNS_EMAIL" ] || [ -z "$MGDNS_TOKEN" ]; then
    _mgdns_err "Missing required env vars: MGDNS_HOST, MGDNS_EMAIL, MGDNS_TOKEN"
    return 1
  fi

  case "$MGDNS_TTL" in
    ''|*[!0-9]*)
      _mgdns_err "MGDNS_TTL must be a positive integer"
      return 1
      ;;
    0)
      _mgdns_err "MGDNS_TTL must be greater than 0"
      return 1
      ;;
  esac

  _saveaccountconf_mutable MGDNS_HOST "$MGDNS_HOST"
  _saveaccountconf_mutable MGDNS_EMAIL "$MGDNS_EMAIL"
  _saveaccountconf_mutable MGDNS_TOKEN "$MGDNS_TOKEN"

  _mgdns_base_url="$(printf '%s' "$MGDNS_HOST" | sed 's:[/]*$::')"

  case "$_mgdns_base_url" in
    http://*|https://*) ;;
    *) _mgdns_base_url="https://$_mgdns_base_url" ;;
  esac

  case "$_mgdns_base_url" in
    */modules/addons/DNSManager3/api/index.php) ;;
    */api/index.php) ;;
    *)
      _mgdns_base_url="$_mgdns_base_url/modules/addons/DNSManager3/api/index.php"
      ;;
  esac

  return 0
}

_mgdns_api_token() {
  _stamp="$(_mgdns_system_hour_stamp)"
  _hmac_key="$MGDNS_EMAIL:$_stamp"

  _hex_digest="$(printf '%s' "$MGDNS_TOKEN" | openssl dgst -sha256 -hmac "$_hmac_key" | sed 's/^.*= //')" || return 1
  printf '%s' "$_hex_digest" | openssl base64 -A
}

_mgdns_system_hour_stamp() {
  date -u +'%y-%m-%d %H'
}

_mgdns_api_request() {
  _method="$1"
  _path="$2"
  _payload="$3"

  _token="$(_mgdns_api_token)" || {
    _mgdns_err "Failed to generate MGDNS auth token"
    return 1
  }

  _mgdns_do_request "$_mgdns_base_url" "$_method" "$_path" "$_payload" "$_token" || return 1
  return 0
}

_mgdns_do_request() {
  _base_url="$1"
  _method="$2"
  _path="$3"
  _payload="$4"
  _token="$5"
  _url="$_base_url$_path"

  _mgdns_last_body=""
  _mgdns_last_http_code=""
  _mgdns_last_curl_exit=""

  _mgdns_debug "Request" "$_method $_url"

  _tmp_body="$(mktemp 2>/dev/null)" || {
    _mgdns_err "Unable to create temporary file"
    return 1
  }

  _curl_insecure=""
  if [ "${MGDNS_INSECURE:-0}" = "1" ]; then
    _curl_insecure="-k"
  fi

  if [ "$_method" = "POST" ]; then
    _mgdns_last_http_code="$(
      curl -sS $_curl_insecure --connect-timeout 15 --max-time 60 \
        -o "$_tmp_body" -w "%{http_code}" \
        -X POST \
        -H "username: $MGDNS_EMAIL" \
        -H "token: $_token" \
        -H "Content-Type: application/x-www-form-urlencoded" \
        --data "$_payload" \
        "$_url"
    )"
    _mgdns_last_curl_exit=$?
  elif [ "$_method" = "DELETE" ]; then
    _mgdns_last_http_code="$(
      curl -sS $_curl_insecure --connect-timeout 15 --max-time 60 \
        -o "$_tmp_body" -w "%{http_code}" \
        -X DELETE \
        -H "username: $MGDNS_EMAIL" \
        -H "token: $_token" \
        "$_url"
    )"
    _mgdns_last_curl_exit=$?
  else
    _mgdns_last_http_code="$(
      curl -sS $_curl_insecure --connect-timeout 15 --max-time 60 \
        -o "$_tmp_body" -w "%{http_code}" \
        -X GET \
        -H "username: $MGDNS_EMAIL" \
        -H "token: $_token" \
        "$_url"
    )"
    _mgdns_last_curl_exit=$?
  fi

  _mgdns_last_body="$(cat "$_tmp_body" 2>/dev/null)"
  rm -f "$_tmp_body"

  if [ "${_mgdns_last_curl_exit:-1}" -ne 0 ]; then
    _mgdns_err "curl request failed with exit code: $_mgdns_last_curl_exit"
    return 1
  fi

  return 0
}

_mgdns_http_ok() {
  case "$_mgdns_last_http_code" in
    2??) return 0 ;;
    *) return 1 ;;
  esac
}

_mgdns_response_ok() {
  _resp="$1"
  _mgdns_contains "$_resp" '"result":"success"' && return 0
  _mgdns_contains "$_resp" '"result": "success"' && return 0
  return 1
}

_mgdns_find_zone() {
  _full_domain="$1"
  _full_domain="${_full_domain%.}"

  _candidate="$_full_domain"

  # Never query _acme-challenge.* as if it were a zone.
  case "$_candidate" in
    _acme-challenge.*)
      _candidate="${_candidate#_acme-challenge.}"
      ;;
  esac

  _last_error=""

  while [ -n "$_candidate" ]; do
    _mgdns_api_request GET "/zone/$_candidate" || return 1
    _zone_check="$_mgdns_last_body"

    _mgdns_debug "Trying zone candidate" "$_candidate"
    _mgdns_debug "Zone candidate HTTP code" "$_mgdns_last_http_code"
    _mgdns_debug "Zone candidate response" "$_zone_check"

    if _mgdns_http_ok && _mgdns_response_ok "$_zone_check"; then
      _domain="$_candidate"
      if [ "$_full_domain" = "$_candidate" ]; then
        _sub_domain=""
      else
        _sub_domain="${_full_domain%.$_candidate}"
      fi
      _mgdns_debug "Detected root zone" "$_domain"
      _mgdns_debug "Detected subdomain" "$_sub_domain"
      return 0
    fi

    if _mgdns_contains "$_zone_check" '"error"'; then
      _last_error="$_zone_check"
    fi

    case "$_candidate" in
      *.*) _candidate="${_candidate#*.}" ;;
      *) break ;;
    esac
  done

  if [ -n "$_last_error" ]; then
    _mgdns_debug "Last API error during zone detection" "$_last_error"
  fi

  return 1
}

_mgdns_extract_txt_ids() {
  _json="$1"
  _record_name="$2"
  _zone_name="$3"
  _txt_value="$4"

  _python_bin="$(command -v python3 || command -v python || true)"
  if [ -z "$_python_bin" ]; then
    _mgdns_err "python3 or python is required"
    return 1
  fi

  MGDNS_JSON="$_json" "$_python_bin" - "$_record_name" "$_zone_name" "$_txt_value" <<'PY'
import json
import os
import sys

record_name = sys.argv[1]
zone_name = sys.argv[2].rstrip(".")
txt_value = sys.argv[3]
raw = os.environ.get("MGDNS_JSON", "").strip()

if not raw:
    raise SystemExit(0)

try:
    data = json.loads(raw)
except Exception:
    raise SystemExit(0)

records = data.get("data") or []

if record_name == "@":
    accepted_names = {"@", zone_name, f"{zone_name}."}
else:
    abs_name = f"{record_name}.{zone_name}"
    accepted_names = {record_name, abs_name, f"{abs_name}."}

def matches_value(value: str, expected: str) -> bool:
    return value in {
        expected,
        f'"{expected}"',
        f"&quot;{expected}&quot;",
    }

for rec in records:
    if str(rec.get("type", "")).upper() != "TXT":
        continue

    name = str(rec.get("name", ""))
    if name not in accepted_names:
        continue

    rdata = rec.get("rdata")
    values = []

    if isinstance(rdata, dict):
        if "txtdata" in rdata:
            values.append(str(rdata.get("txtdata", "")))
        values.extend(str(v) for v in rdata.values() if isinstance(v, (str, int, float)))
    elif rdata is not None:
        values.append(str(rdata))

    if any(matches_value(v, txt_value) for v in values):
        rec_id = rec.get("id")
        if rec_id:
            print(rec_id)
PY
}

_mgdns_urlencode() {
  _raw="$1"
  _python_bin="$(command -v python3 || command -v python || true)"

  if [ -z "$_python_bin" ]; then
    _mgdns_err "python3 or python is required"
    return 1
  fi

  "$_python_bin" - "$_raw" <<'PY'
import sys
try:
    from urllib.parse import quote
except Exception:
    from urllib import quote
print(quote(sys.argv[1], safe=''))
PY
}

_mgdns_info() {
  if command -v _info >/dev/null 2>&1; then
    _info "$@"
  else
    printf '%s\n' "$*"
  fi
}

_mgdns_err() {
  if command -v _err >/dev/null 2>&1; then
    _err "$@"
  else
    printf '%s\n' "$*" >&2
  fi
}

_mgdns_debug() {
  if command -v _debug >/dev/null 2>&1; then
    _debug "$@"
  fi
}

_mgdns_contains() {
  _str="$1"
  _sub="$2"
  case "$_str" in
    *"$_sub"*) return 0 ;;
    *) return 1 ;;
  esac
}

