import { randomUUID } from "node:crypto";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { z } from "zod";

const DEFAULT_ENDPOINT_PREFIX = "/qa-credentials/v1";
const DEFAULT_HTTP_TIMEOUT_MS = 15_000;
const ALLOW_INSECURE_HTTP_ENV_KEY = "OPENCLAW_QA_ALLOW_INSECURE_HTTP";

const actorRoleSchema = z.union([z.literal("ci"), z.literal("maintainer")]);
const credentialStatusSchema = z.union([z.literal("active"), z.literal("disabled")]);
const listStatusSchema = z.union([z.literal("active"), z.literal("disabled"), z.literal("all")]);

const brokerErrorSchema = z.object({
  status: z.literal("error"),
  code: z.string().min(1),
  message: z.string().min(1),
});

const credentialLeaseSchema = z.object({
  ownerId: z.string().min(1),
  actorRole: actorRoleSchema,
  acquiredAtMs: z.number().int(),
  heartbeatAtMs: z.number().int(),
  expiresAtMs: z.number().int(),
});

const credentialRecordSchema = z.object({
  credentialId: z.string().min(1),
  kind: z.string().min(1),
  status: credentialStatusSchema,
  createdAtMs: z.number().int(),
  updatedAtMs: z.number().int(),
  lastLeasedAtMs: z.number().int(),
  note: z.string().optional(),
  lease: credentialLeaseSchema.optional(),
  payload: z.unknown().optional(),
});

const addCredentialResponseSchema = z.object({
  status: z.literal("ok"),
  credential: credentialRecordSchema,
});

const removeCredentialResponseSchema = z.object({
  status: z.literal("ok"),
  changed: z.boolean(),
  credential: credentialRecordSchema,
});

const listCredentialsResponseSchema = z.object({
  status: z.literal("ok"),
  credentials: z.array(credentialRecordSchema),
  count: z.number().int().nonnegative().optional(),
});

export type QaCredentialAdminListStatus = z.infer<typeof listStatusSchema>;
export type QaCredentialRecord = z.infer<typeof credentialRecordSchema>;
export type QaCredentialListResponse = z.infer<typeof listCredentialsResponseSchema>;

export class QaCredentialAdminError extends Error {
  code: string;
  httpStatus?: number;

  constructor(params: { code: string; message: string; httpStatus?: number }) {
    super(params.message);
    this.name = "QaCredentialAdminError";
    this.code = params.code;
    this.httpStatus = params.httpStatus;
  }
}

type AdminConfig = {
  actorId: string;
  authToken: string;
  addUrl: string;
  endpointPrefix: string;
  httpTimeoutMs: number;
  listUrl: string;
  removeUrl: string;
  siteUrl: string;
};

type AdminBaseOptions = {
  actorId?: string;
  endpointPrefix?: string;
  env?: NodeJS.ProcessEnv;
  fetchImpl?: typeof fetch;
  siteUrl?: string;
};

type AddQaCredentialSetOptions = AdminBaseOptions & {
  kind: string;
  note?: string;
  payload: Record<string, unknown>;
  status?: z.infer<typeof credentialStatusSchema>;
};

type RemoveQaCredentialSetOptions = AdminBaseOptions & {
  credentialId: string;
};

type ListQaCredentialSetsOptions = AdminBaseOptions & {
  includePayload?: boolean;
  kind?: string;
  limit?: number;
  status?: string;
};

function parsePositiveIntegerEnv(env: NodeJS.ProcessEnv, key: string, fallback: number): number {
  const raw = env[key]?.trim();
  if (!raw) {
    return fallback;
  }
  const value = Number(raw);
  if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
    throw new QaCredentialAdminError({
      code: "INVALID_ENV",
      message: `${key} must be a positive integer.`,
    });
  }
  return value;
}

function isTruthyOptIn(value: string | undefined) {
  const normalized = value?.trim().toLowerCase();
  return normalized === "1" || normalized === "true" || normalized === "yes";
}

function isLoopbackHostname(hostname: string) {
  return hostname === "localhost" || hostname === "::1" || hostname.startsWith("127.");
}

function normalizeConvexSiteUrl(raw: string, env: NodeJS.ProcessEnv): string {
  let parsed: URL;
  try {
    parsed = new URL(raw);
  } catch {
    throw new QaCredentialAdminError({
      code: "INVALID_SITE_URL",
      message: `OPENCLAW_QA_CONVEX_SITE_URL must be a valid URL, got "${raw || "<empty>"}".`,
    });
  }
  if (parsed.protocol === "https:") {
    const text = parsed.toString();
    return text.endsWith("/") ? text.slice(0, -1) : text;
  }
  if (parsed.protocol !== "http:") {
    throw new QaCredentialAdminError({
      code: "INVALID_SITE_URL",
      message: "OPENCLAW_QA_CONVEX_SITE_URL must use https://.",
    });
  }
  const allowInsecureHttp = isTruthyOptIn(env[ALLOW_INSECURE_HTTP_ENV_KEY]);
  if (!allowInsecureHttp || !isLoopbackHostname(parsed.hostname)) {
    throw new QaCredentialAdminError({
      code: "INVALID_SITE_URL",
      message: `OPENCLAW_QA_CONVEX_SITE_URL must use https://. http:// is only allowed for loopback hosts when ${ALLOW_INSECURE_HTTP_ENV_KEY}=1.`,
    });
  }
  const text = parsed.toString();
  return text.endsWith("/") ? text.slice(0, -1) : text;
}

function normalizeEndpointPrefix(value: string | undefined): string {
  const trimmed = value?.trim();
  if (!trimmed) {
    return DEFAULT_ENDPOINT_PREFIX;
  }
  const prefixed = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
  const normalized = prefixed.endsWith("/") ? prefixed.slice(0, -1) : prefixed;
  if (!normalized.startsWith("/") || normalized.startsWith("//")) {
    throw new QaCredentialAdminError({
      code: "INVALID_ARGUMENT",
      message: '--endpoint-prefix must be an absolute path like "/qa-credentials/v1" (not //host).',
    });
  }
  if (normalized.includes("\\") || normalized.split("/").some((segment) => segment === "..")) {
    throw new QaCredentialAdminError({
      code: "INVALID_ARGUMENT",
      message: '--endpoint-prefix must not contain backslashes or ".." path segments.',
    });
  }
  return normalized;
}

function joinEndpoint(baseUrl: string, prefix: string, suffix: string): string {
  const normalizedSuffix = suffix.startsWith("/") ? suffix : `/${suffix}`;
  const url = new URL(baseUrl);
  url.pathname = `${prefix}${normalizedSuffix}`.replace(/\/{2,}/gu, "/");
  url.search = "";
  url.hash = "";
  return url.toString();
}

function resolveAdminAuthToken(env: NodeJS.ProcessEnv): string {
  const token = env.OPENCLAW_QA_CONVEX_SECRET_MAINTAINER?.trim();
  if (token) {
    return token;
  }
  throw new QaCredentialAdminError({
    code: "MISSING_MAINTAINER_SECRET",
    message: "Missing OPENCLAW_QA_CONVEX_SECRET_MAINTAINER for qa credential admin commands.",
  });
}

function resolveAdminConfig(options: AdminBaseOptions): AdminConfig {
  const env = options.env ?? process.env;
  const siteUrl = options.siteUrl?.trim() || env.OPENCLAW_QA_CONVEX_SITE_URL?.trim();
  if (!siteUrl) {
    throw new QaCredentialAdminError({
      code: "MISSING_SITE_URL",
      message: "Missing OPENCLAW_QA_CONVEX_SITE_URL for qa credential admin commands.",
    });
  }
  const normalizedSiteUrl = normalizeConvexSiteUrl(siteUrl, env);
  const endpointPrefix = normalizeEndpointPrefix(
    options.endpointPrefix?.trim() || env.OPENCLAW_QA_CONVEX_ENDPOINT_PREFIX,
  );
  const actorId =
    options.actorId?.trim() ||
    env.OPENCLAW_QA_CREDENTIAL_OWNER_ID?.trim() ||
    `qa-lab-admin-${process.pid}-${randomUUID().slice(0, 8)}`;

  return {
    actorId,
    authToken: resolveAdminAuthToken(env),
    siteUrl: normalizedSiteUrl,
    endpointPrefix,
    httpTimeoutMs: parsePositiveIntegerEnv(
      env,
      "OPENCLAW_QA_CREDENTIAL_HTTP_TIMEOUT_MS",
      DEFAULT_HTTP_TIMEOUT_MS,
    ),
    addUrl: joinEndpoint(normalizedSiteUrl, endpointPrefix, "admin/add"),
    removeUrl: joinEndpoint(normalizedSiteUrl, endpointPrefix, "admin/remove"),
    listUrl: joinEndpoint(normalizedSiteUrl, endpointPrefix, "admin/list"),
  };
}

function parseJsonResponsePayload(text: string) {
  if (!text.trim()) {
    return undefined;
  }
  try {
    return JSON.parse(text) as unknown;
  } catch {
    return text;
  }
}

function toBrokerError(payload: unknown, httpStatus: number) {
  const parsed = brokerErrorSchema.safeParse(payload);
  if (!parsed.success) {
    return null;
  }
  return new QaCredentialAdminError({
    code: parsed.data.code,
    message: parsed.data.message,
    httpStatus,
  });
}

async function postJson<T>(params: {
  authToken: string;
  body: Record<string, unknown>;
  fetchImpl: typeof fetch;
  httpTimeoutMs: number;
  responseSchema: z.ZodType<T>;
  url: string;
}) {
  let response: Response;
  try {
    response = await params.fetchImpl(params.url, {
      method: "POST",
      headers: {
        authorization: `Bearer ${params.authToken}`,
        "content-type": "application/json",
      },
      body: JSON.stringify(params.body),
      signal: AbortSignal.timeout(params.httpTimeoutMs),
    });
  } catch (error) {
    throw new QaCredentialAdminError({
      code: "BROKER_REQUEST_FAILED",
      message: `Convex credential admin request failed: ${formatErrorMessage(error)}`,
    });
  }

  const text = await response.text();
  const payload = parseJsonResponsePayload(text);

  const brokerError = toBrokerError(payload, response.status);
  if (brokerError) {
    throw brokerError;
  }
  if (!response.ok) {
    throw new QaCredentialAdminError({
      code: "BROKER_HTTP_ERROR",
      message: `Convex credential admin request failed with HTTP ${response.status}.`,
      httpStatus: response.status,
    });
  }

  const parsed = params.responseSchema.safeParse(payload);
  if (!parsed.success) {
    throw new QaCredentialAdminError({
      code: "INVALID_RESPONSE",
      message: `Convex credential admin response did not match expected shape: ${parsed.error.message}`,
      httpStatus: response.status,
    });
  }

  return parsed.data;
}

function normalizeStatus(value: string | undefined): QaCredentialAdminListStatus | undefined {
  if (!value) {
    return undefined;
  }
  const normalized = value.trim().toLowerCase();
  const parsed = listStatusSchema.safeParse(normalized);
  if (!parsed.success) {
    throw new QaCredentialAdminError({
      code: "INVALID_ARGUMENT",
      message: '--status must be one of "active", "disabled", or "all".',
    });
  }
  return parsed.data;
}

function normalizeLimit(value: number | undefined) {
  if (value === undefined) {
    return undefined;
  }
  if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
    throw new QaCredentialAdminError({
      code: "INVALID_ARGUMENT",
      message: "--limit must be a positive integer.",
    });
  }
  return value;
}

export async function addQaCredentialSet(options: AddQaCredentialSetOptions) {
  const config = resolveAdminConfig(options);
  const fetchImpl = options.fetchImpl ?? fetch;
  return await postJson({
    fetchImpl,
    authToken: config.authToken,
    httpTimeoutMs: config.httpTimeoutMs,
    url: config.addUrl,
    responseSchema: addCredentialResponseSchema,
    body: {
      kind: options.kind,
      payload: options.payload,
      ...(options.note ? { note: options.note } : {}),
      ...(options.status ? { status: options.status } : {}),
      actorId: config.actorId,
    },
  });
}

export async function removeQaCredentialSet(options: RemoveQaCredentialSetOptions) {
  const config = resolveAdminConfig(options);
  const fetchImpl = options.fetchImpl ?? fetch;
  return await postJson({
    fetchImpl,
    authToken: config.authToken,
    httpTimeoutMs: config.httpTimeoutMs,
    url: config.removeUrl,
    responseSchema: removeCredentialResponseSchema,
    body: {
      credentialId: options.credentialId,
      actorId: config.actorId,
    },
  });
}

export async function listQaCredentialSets(options: ListQaCredentialSetsOptions) {
  const config = resolveAdminConfig(options);
  const fetchImpl = options.fetchImpl ?? fetch;
  const status = normalizeStatus(options.status);
  const limit = normalizeLimit(options.limit);
  return await postJson({
    fetchImpl,
    authToken: config.authToken,
    httpTimeoutMs: config.httpTimeoutMs,
    url: config.listUrl,
    responseSchema: listCredentialsResponseSchema,
    body: {
      ...(options.kind ? { kind: options.kind } : {}),
      ...(status ? { status } : {}),
      ...(options.includePayload === true ? { includePayload: true } : {}),
      ...(limit !== undefined ? { limit } : {}),
    },
  });
}

export const __testing = {
  DEFAULT_ENDPOINT_PREFIX,
  DEFAULT_HTTP_TIMEOUT_MS,
  normalizeConvexSiteUrl,
  normalizeEndpointPrefix,
  normalizeStatus,
  parsePositiveIntegerEnv,
  resolveAdminConfig,
};
