const DEFAULT_TIMEOUT_MS = 5000;
const DEFAULT_LOGIN_RETRY_ATTEMPTS = 2;
const DEFAULT_LOGIN_RETRY_DELAY_MS = 250;
const NON_RETRYABLE_USER_BILLING_ERROR_CODES = new Set(["user_billing_not_configured"]);

export function createUserBillingClient({ env = process.env, fetchImpl = fetch } = {}) {
  const baseUrl = normalizeBaseUrl(firstText(
    env.HWLAB_USER_BILLING_URL,
    env.HWLAB_USER_BILLING_BASE_URL,
    env.HWLAB_USER_BILLING_INTERNAL_URL
  ));
  const internalToken = text(env.HWLAB_USER_BILLING_INTERNAL_TOKEN);
  const timeoutMs = positiveInteger(env.HWLAB_USER_BILLING_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
  const loginRetryAttempts = nonNegativeInteger(env.HWLAB_USER_BILLING_LOGIN_RETRY_ATTEMPTS, DEFAULT_LOGIN_RETRY_ATTEMPTS);
  const loginRetryDelayMs = positiveInteger(env.HWLAB_USER_BILLING_LOGIN_RETRY_DELAY_MS, DEFAULT_LOGIN_RETRY_DELAY_MS);

  async function requestJson(path, { method = "GET", body = null, bearerToken = "", traceparent = "" } = {}) {
    return requestJsonWithRetry(path, { method, body, bearerToken, traceparent });
  }

  async function requestJsonWithRetry(path, { method = "GET", body = null, bearerToken = "", traceparent = "", retry = null } = {}) {
    const retryAttempts = nonNegativeInteger(retry?.attempts, 0);
    const retryDelayMs = positiveInteger(retry?.delayMs, 0);
    let retryCount = 0;
    let transientObserved = false;
    for (;;) {
      const result = await requestJsonOnce(path, { method, body, bearerToken, traceparent });
      const retryable = typeof retry?.shouldRetry === "function" ? retry.shouldRetry(result) : false;
      if (!retryable || retryCount >= retryAttempts) {
        if (retryCount > 0 || transientObserved || retryable) {
          return { ...result, retryable, retryCount, transientObserved: transientObserved || retryable, valuesRedacted: true };
        }
        return result;
      }
      retryCount += 1;
      transientObserved = true;
      if (retryDelayMs > 0) await sleep(retryDelayMs * retryCount);
    }
  }

  async function requestJsonOnce(path, { method = "GET", body = null, bearerToken = "", traceparent = "" } = {}) {
    if (!baseUrl) {
      return { ok: false, status: 503, error: { code: "user_billing_not_configured", message: "HWLAB user-billing URL is not configured" } };
    }
    const headers = {
      accept: "application/json"
    };
    if (body !== null) headers["content-type"] = "application/json";
    const token = text(bearerToken);
    if (token) headers.authorization = `Bearer ${token}`;
    const normalizedTraceparent = normalizeTraceparent(traceparent);
    if (normalizedTraceparent) headers.traceparent = normalizedTraceparent;
    if (internalToken) headers["x-hwlab-internal-token"] = internalToken;
    try {
      const init = {
        method,
        headers,
        signal: AbortSignal.timeout(timeoutMs),
        ...(body !== null ? { body: JSON.stringify(body) } : {})
      };
      const response = await fetchImpl(`${baseUrl}${path}`, {
        ...init
      });
      const raw = await response.text();
      const payload = parseJson(raw, null);
      if (!response.ok) {
        return {
          ok: false,
          status: response.status,
          body: payload,
          error: normalizeUserBillingError(payload, response.status)
        };
      }
      return { ok: true, status: response.status, body: payload ?? {} };
    } catch (error) {
      return {
        ok: false,
        status: 503,
        error: {
          code: "user_billing_unreachable",
          message: error instanceof Error ? error.message : String(error)
        }
      };
    }
  }

  async function requestText(path, { method = "GET" } = {}) {
    if (!baseUrl) {
      return { ok: false, status: 503, error: { code: "user_billing_not_configured", message: "HWLAB user-billing URL is not configured" } };
    }
    const headers = { accept: "text/csv" };
    if (internalToken) headers["x-hwlab-internal-token"] = internalToken;
    try {
      const response = await fetchImpl(`${baseUrl}${path}`, { method, headers, signal: AbortSignal.timeout(timeoutMs) });
      const body = await response.text();
      if (!response.ok) {
        return { ok: false, status: response.status, body, error: { code: "user_billing_request_failed", message: `user-billing request failed with HTTP ${response.status}` } };
      }
      return { ok: true, status: response.status, body, contentType: response.headers?.get?.("content-type") ?? "text/csv" };
    } catch (error) {
      return { ok: false, status: 503, error: { code: "user_billing_unreachable", message: error instanceof Error ? error.message : String(error) } };
    }
  }

  async function post(path, body = {}) {
    return requestJson(path, { method: "POST", body });
  }

  async function publicPost(path, body = {}) {
    return requestJson(path, { method: "POST", body });
  }

  async function patch(path, body = {}, options = {}) {
    return requestJson(path, { method: "PATCH", body, ...options });
  }

  async function deleteRequest(path, options = {}) {
    return requestJson(path, { method: "DELETE", body: {}, ...options });
  }

  return {
    configured: Boolean(baseUrl),
    baseUrl,
    async register(body = {}) {
      return publicPost("/v1/auth/register", body);
    },
    async login(body = {}, options = {}) {
      return requestJsonWithRetry("/v1/auth/login", {
        method: "POST",
        body,
        traceparent: options.traceparent,
        retry: { attempts: loginRetryAttempts, delayMs: loginRetryDelayMs, shouldRetry: isRetryableUserBillingResult }
      });
    },
    async logout(token) {
      return requestJson("/v1/auth/logout", { method: "POST", body: {}, bearerToken: token });
    },
    async introspect(token) {
      return post("/internal/auth/introspect", { token });
    },
    async billingPreflight(body) {
      return post("/internal/billing/preflight", body);
    },
    async billingRecord(body) {
      return post("/internal/billing/record", body);
    },
    async billingRelease(body) {
      return post("/internal/billing/release", body);
    },
    async billingSummary(token, { limit = 20 } = {}) {
      const query = new URLSearchParams({ limit: String(limit) });
      return requestJson(`/v1/billing/summary?${query.toString()}`, { method: "GET", bearerToken: token });
    },
    async redeem(token, body = {}) {
      return requestJson("/v1/redeem", { method: "POST", body, bearerToken: token });
    },
    async subscriptionSummary(token) {
      return requestJson("/v1/subscription/summary", { method: "GET", bearerToken: token });
    },
    async paymentSummary(token) {
      return requestJson("/v1/payments/summary", { method: "GET", bearerToken: token });
    },
    async updateProfile(token, body = {}) {
      return patch("/v1/me/profile", body, { bearerToken: token });
    },
    async changePassword(token, body = {}) {
      return requestJson("/v1/me/password", { method: "POST", body, bearerToken: token });
    },
    async listApiKeys(token) {
      return requestJson("/v1/api-keys", { method: "GET", bearerToken: token });
    },
    async createApiKey(token, body = {}) {
      return requestJson("/v1/api-keys", { method: "POST", body, bearerToken: token });
    },
    async updateApiKey(token, keyId, body = {}) {
      return patch(`/v1/api-keys/${encodeURIComponent(text(keyId))}`, body, { bearerToken: token });
    },
    async revokeApiKey(token, keyId) {
      return deleteRequest(`/v1/api-keys/${encodeURIComponent(text(keyId))}`, { bearerToken: token });
    },
    async adminBillingSummary({ limit = 50 } = {}) {
      const query = new URLSearchParams({ limit: String(limit) });
      return requestJson(`/internal/admin/billing/summary?${query.toString()}`, { method: "GET" });
    },
    async adminBillingPlans() {
      return requestJson("/internal/admin/billing/plans", { method: "GET" });
    },
    async adminUsage(params = {}) {
      return requestJson(adminAuditPath("/internal/admin/usage", params), { method: "GET" });
    },
    async adminUsageExport(params = {}) {
      return requestText(adminAuditPath("/internal/admin/usage/export", params), { method: "GET" });
    },
    async adminLedger(params = {}) {
      return requestJson(adminAuditPath("/internal/admin/ledger", params), { method: "GET" });
    },
    async adminLedgerExport(params = {}) {
      return requestText(adminAuditPath("/internal/admin/ledger/export", params), { method: "GET" });
    },
    async adminRedeemCodes(params = {}) {
      return requestJson(adminRedeemPath("/internal/admin/redeem-codes", params), { method: "GET" });
    },
    async createAdminRedeemCode(body = {}) {
      return post("/internal/admin/redeem-codes", body);
    },
    async updateAdminRedeemCode(codeId, body = {}) {
      return patch(`/internal/admin/redeem-codes/${encodeURIComponent(text(codeId))}`, body);
    },
    async adminRedeemRedemptions(params = {}) {
      return requestJson(adminRedeemPath("/internal/admin/redeem-redemptions", params), { method: "GET" });
    },
    async adminUsers({ page = 1, pageSize = 50, search = "", status = "", role = "" } = {}) {
      const query = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
      if (text(search)) query.set("search", text(search));
      if (text(status)) query.set("status", text(status));
      if (text(role)) query.set("role", text(role));
      return requestJson(`/internal/admin/users?${query.toString()}`, { method: "GET" });
    },
    async adminUser(userId) {
      return requestJson(`/internal/admin/users/${encodeURIComponent(text(userId))}`, { method: "GET" });
    },
    async createAdminUser(body = {}) {
      return post("/internal/admin/users", body);
    },
    async updateAdminUser(userId, body = {}) {
      return patch(`/internal/admin/users/${encodeURIComponent(text(userId))}`, body);
    },
    async adjustAdminCredits(body = {}) {
      return post("/internal/admin/credits/adjust", body);
    },
    async updateAdminUserStatus(userId, status) {
      return post("/internal/admin/users/status", { userId, status });
    }
  };
}

function adminAuditPath(path, params = {}) {
  const query = new URLSearchParams();
  for (const key of ["page", "pageSize", "limit", "userId", "search", "serviceId", "resourceType", "apiKeyId", "status", "kind", "source", "idempotencyKey", "from", "to"]) {
    const value = text(params?.[key]);
    if (value) query.set(key, value);
  }
  const suffix = query.toString();
  return suffix ? `${path}?${suffix}` : path;
}

function adminRedeemPath(path, params = {}) {
  const query = new URLSearchParams();
  for (const key of ["limit", "status", "userId", "codeId"]) {
    const value = text(params?.[key]);
    if (value) query.set(key, value);
  }
  const suffix = query.toString();
  return suffix ? `${path}?${suffix}` : path;
}

function normalizeBaseUrl(value) {
  const input = text(value);
  if (!input) return "";
  try {
    const url = new URL(input);
    url.pathname = url.pathname.replace(/\/+$/u, "");
    url.search = "";
    url.hash = "";
    return url.toString().replace(/\/+$/u, "");
  } catch {
    return "";
  }
}

function normalizeUserBillingError(payload, status) {
  const error = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.error : null;
  return {
    code: text(error?.code) || (status === 402 ? "insufficient_credits" : "user_billing_request_failed"),
    message: text(error?.message) || `user-billing request failed with HTTP ${status}`
  };
}

function positiveInteger(value, fallback) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

function nonNegativeInteger(value, fallback) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
}

function isRetryableUserBillingResult(result) {
  if (!result || result.ok) return false;
  const code = text(result.error?.code);
  if (NON_RETRYABLE_USER_BILLING_ERROR_CODES.has(code)) return false;
  if (code === "user_billing_unreachable") return true;
  const status = Number(result.status ?? 0);
  if ([500, 502, 503, 504].includes(status)) return true;
  return false;
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
}

function firstText(...values) {
  for (const value of values) {
    const current = text(value);
    if (current) return current;
  }
  return "";
}

function text(value) {
  return String(value ?? "").trim();
}

function normalizeTraceparent(value) {
  const textValue = text(value).toLowerCase();
  return /^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/u.test(textValue) ? textValue : "";
}

function parseJson(value, fallback) {
  if (!value) return fallback;
  try {
    return JSON.parse(String(value));
  } catch {
    return fallback;
  }
}
