const WECOM_API_ORIGIN = "https://qyapi.weixin.qq.com";
const DEFAULT_DUE_DAYS = 3;

export type WecomBindings = {
  WECOM_CORP_ID?: string;
  WECOM_AGENT_ID?: string;
  WECOM_SECRET?: string;
  WECOM_ROBOT_WEBHOOK?: string;
  WECOM_NOTIFY_DUE_DAYS?: string;
};

export type WecomConfig = {
  corpId: string;
  agentId: string;
  secret: string;
  robotWebhook: string;
  dueDays: number;
};

type FetchLike = typeof fetch;

type WecomEnvelope = {
  errcode?: number;
  errmsg?: string;
  access_token?: string;
  expires_in?: number;
  invaliduser?: string;
};

type CachedAccessToken = {
  corpId: string;
  secret: string;
  token: string;
  expiresAt: number;
};

let cachedAccessToken: CachedAccessToken | null = null;

export class WecomClientError extends Error {
  status: number;

  constructor(message: string, status = 502) {
    super(message);
    this.name = "WecomClientError";
    this.status = status;
  }
}

function bindingValue(value: unknown) {
  return String(value ?? "").trim();
}

function safeMessage(value: unknown) {
  return String(value ?? "").trim().slice(0, 240);
}

export function resolveWecomConfig(bindings: WecomBindings): WecomConfig {
  return {
    corpId: bindingValue(bindings.WECOM_CORP_ID),
    agentId: bindingValue(bindings.WECOM_AGENT_ID),
    secret: bindingValue(bindings.WECOM_SECRET),
    robotWebhook: bindingValue(bindings.WECOM_ROBOT_WEBHOOK),
    dueDays: parseDueDays(bindings.WECOM_NOTIFY_DUE_DAYS),
  };
}

function parseDueDays(value: string | undefined) {
  const parsed = Number(bindingValue(value));
  if (!Number.isFinite(parsed) || parsed < 1) return DEFAULT_DUE_DAYS;
  return Math.min(30, Math.floor(parsed));
}

function hasAppCredentials(config: WecomConfig) {
  return Boolean(config.corpId && config.agentId && config.secret);
}

async function readEnvelope(
  response: Response,
  fallbackMessage: string,
): Promise<WecomEnvelope> {
  const text = await response.text();
  try {
    return JSON.parse(text) as WecomEnvelope;
  } catch {
    throw new WecomClientError(
      `${fallbackMessage}（企业微信返回了非 JSON 响应）`,
      502,
    );
  }
}

function ensureOk(
  payload: WecomEnvelope,
  fallbackMessage: string,
) {
  const code = Number(payload.errcode ?? 0);
  if (code === 0) return;
  const message = safeMessage(payload.errmsg) || fallbackMessage;
  if (code === 40014 || code === 42001) {
    throw new WecomClientError(`企业微信 access_token 无效：${message}`, 401);
  }
  throw new WecomClientError(`${fallbackMessage}（${message}）`, 502);
}

async function fetchAccessToken(
  config: WecomConfig,
  fetchImpl: FetchLike,
) {
  if (
    cachedAccessToken?.corpId === config.corpId &&
    cachedAccessToken?.secret === config.secret &&
    cachedAccessToken.expiresAt > Date.now() + 60_000
  ) {
    return cachedAccessToken.token;
  }
  const url = new URL(`${WECOM_API_ORIGIN}/cgi-bin/gettoken`);
  url.searchParams.set("corpid", config.corpId);
  url.searchParams.set("corpsecret", config.secret);
  const response = await fetchImpl(url.toString(), {
    method: "GET",
    signal: AbortSignal.timeout(12_000),
  });
  const payload = await readEnvelope(response, "获取企业微信 access_token 失败");
  if (!response.ok) {
    throw new WecomClientError(
      `获取企业微信 access_token 失败（HTTP ${response.status}）`,
      502,
    );
  }
  ensureOk(payload, "获取企业微信 access_token 失败");
  const token = bindingValue(payload.access_token);
  if (!token) {
    throw new WecomClientError("企业微信未返回有效 access_token", 502);
  }
  cachedAccessToken = {
    corpId: config.corpId,
    secret: config.secret,
    token,
    expiresAt:
      Date.now() + Math.max(300, Number(payload.expires_in) || 7_200) * 1_000,
  };
  return token;
}

export async function sendWecomRobotMessage(
  content: string,
  config: WecomConfig,
  fetchImpl: FetchLike = fetch,
): Promise<void> {
  if (!config.robotWebhook) {
    console.warn("[KOC LOOP] wecom robot webhook not configured, skipping");
    return;
  }
  const response = await fetchImpl(config.robotWebhook, {
    method: "POST",
    headers: { "Content-Type": "application/json; charset=utf-8" },
    body: JSON.stringify({
      msgtype: "text",
      text: { content },
    }),
    signal: AbortSignal.timeout(10_000),
  });
  const payload = await readEnvelope(response, "企业微信群机器人推送失败");
  if (!response.ok) {
    throw new WecomClientError(
      `企业微信群机器人推送失败（HTTP ${response.status}）`,
      502,
    );
  }
  ensureOk(payload, "企业微信群机器人推送失败");
}

export async function sendWecomAppMessage(
  externalUserIds: string[],
  content: string,
  config: WecomConfig,
  fetchImpl: FetchLike = fetch,
): Promise<{ sent: number; failed: number; skipped: boolean }> {
  const normalized = externalUserIds
    .map((id) => bindingValue(id))
    .filter((id) => id.length > 0);
  if (normalized.length === 0) {
    return { sent: 0, failed: 0, skipped: true };
  }
  if (!hasAppCredentials(config)) {
    return { sent: 0, failed: 0, skipped: true };
  }
  const token = await fetchAccessToken(config, fetchImpl);
  const url = `${WECOM_API_ORIGIN}/cgi-bin/message/send?access_token=${encodeURIComponent(token)}`;
  const response = await fetchImpl(url, {
    method: "POST",
    headers: { "Content-Type": "application/json; charset=utf-8" },
    body: JSON.stringify({
      touser: normalized.join("|"),
      msgtype: "text",
      agentid: Number(config.agentId),
      text: { content },
    }),
    signal: AbortSignal.timeout(12_000),
  });
  const payload = await readEnvelope(response, "企业微信应用消息推送失败");
  if (!response.ok) {
    throw new WecomClientError(
      `企业微信应用消息推送失败（HTTP ${response.status}）`,
      502,
    );
  }
  ensureOk(payload, "企业微信应用消息推送失败");
  const invalid = bindingValue(payload.invaliduser).split("|").filter(Boolean);
  return {
    sent: Math.max(0, normalized.length - invalid.length),
    failed: invalid.length,
    skipped: false,
  };
}

export function clearWecomAccessTokenCacheForTests() {
  cachedAccessToken = null;
}
