/**
 * Synology Chat HTTP client.
 * Sends messages TO Synology Chat via the incoming webhook URL.
 */

import * as http from "node:http";
import * as https from "node:https";

const MIN_SEND_INTERVAL_MS = 500;
let lastSendTime = 0;

// --- Chat user_id resolution ---
// Synology Chat uses two different user_id spaces:
//   - Outgoing webhook user_id: per-integration sequential ID (e.g. 1)
//   - Chat API user_id: global internal ID (e.g. 4)
// The chatbot API (method=chatbot) requires the Chat API user_id in the
// user_ids array. We resolve via the user_list API and cache the result.

interface ChatUser {
  user_id: number;
  username: string;
  nickname: string;
}

type ChatUserCacheEntry = {
  users: ChatUser[];
  cachedAt: number;
};

type ChatWebhookPayload = {
  text?: string;
  file_url?: string;
  user_ids?: number[];
};

// Cache user lists per bot endpoint to avoid cross-account bleed.
const chatUserCache = new Map<string, ChatUserCacheEntry>();
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

/**
 * Send a text message to Synology Chat via the incoming webhook.
 *
 * @param incomingUrl - Synology Chat incoming webhook URL
 * @param text - Message text to send
 * @param userId - Optional user ID to mention with @
 * @returns true if sent successfully
 */
export async function sendMessage(
  incomingUrl: string,
  text: string,
  userId?: string | number,
  allowInsecureSsl = true,
): Promise<boolean> {
  // Synology Chat API requires user_ids (numeric) to specify the recipient
  // The @mention is optional but user_ids is mandatory
  const body = buildWebhookBody({ text }, userId);

  // Internal rate limit: min 500ms between sends
  const now = Date.now();
  const elapsed = now - lastSendTime;
  if (elapsed < MIN_SEND_INTERVAL_MS) {
    await sleep(MIN_SEND_INTERVAL_MS - elapsed);
  }

  // Retry with exponential backoff (3 attempts, 300ms base)
  const maxRetries = 3;
  const baseDelay = 300;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const ok = await doPost(incomingUrl, body, allowInsecureSsl);
      lastSendTime = Date.now();
      if (ok) return true;
    } catch {
      // will retry
    }

    if (attempt < maxRetries - 1) {
      await sleep(baseDelay * Math.pow(2, attempt));
    }
  }

  return false;
}

/**
 * Send a file URL to Synology Chat.
 */
export async function sendFileUrl(
  incomingUrl: string,
  fileUrl: string,
  userId?: string | number,
  allowInsecureSsl = true,
): Promise<boolean> {
  const body = buildWebhookBody({ file_url: fileUrl }, userId);

  try {
    const ok = await doPost(incomingUrl, body, allowInsecureSsl);
    lastSendTime = Date.now();
    return ok;
  } catch {
    return false;
  }
}

/**
 * Fetch the list of Chat users visible to this bot via the user_list API.
 * Results are cached for CACHE_TTL_MS to avoid excessive API calls.
 *
 * The user_list endpoint uses the same base URL as the chatbot API but
 * with method=user_list instead of method=chatbot.
 */
export async function fetchChatUsers(
  incomingUrl: string,
  allowInsecureSsl = true,
  log?: { warn: (...args: unknown[]) => void },
): Promise<ChatUser[]> {
  const now = Date.now();
  const listUrl = incomingUrl.replace(/method=\w+/, "method=user_list");
  const cached = chatUserCache.get(listUrl);
  if (cached && now - cached.cachedAt < CACHE_TTL_MS) {
    return cached.users;
  }

  return new Promise((resolve) => {
    let parsedUrl: URL;
    try {
      parsedUrl = new URL(listUrl);
    } catch {
      log?.warn("fetchChatUsers: invalid user_list URL, using cached data");
      resolve(cached?.users ?? []);
      return;
    }
    const transport = parsedUrl.protocol === "https:" ? https : http;

    transport
      .get(listUrl, { rejectUnauthorized: !allowInsecureSsl } as any, (res) => {
        let data = "";
        res.on("data", (c: Buffer) => {
          data += c.toString();
        });
        res.on("end", () => {
          try {
            const result = JSON.parse(data);
            if (result.success && result.data?.users) {
              const users = result.data.users.map((u: any) => ({
                user_id: u.user_id,
                username: u.username || "",
                nickname: u.nickname || "",
              }));
              chatUserCache.set(listUrl, {
                users,
                cachedAt: now,
              });
              resolve(users);
            } else {
              log?.warn(
                `fetchChatUsers: API returned success=${result.success}, using cached data`,
              );
              resolve(cached?.users ?? []);
            }
          } catch {
            log?.warn("fetchChatUsers: failed to parse user_list response");
            resolve(cached?.users ?? []);
          }
        });
      })
      .on("error", (err) => {
        log?.warn(`fetchChatUsers: HTTP error — ${err instanceof Error ? err.message : err}`);
        resolve(cached?.users ?? []);
      });
  });
}

/**
 * Resolve a webhook username to the correct Chat API user_id.
 *
 * Synology Chat outgoing webhooks send a user_id that may NOT match the
 * Chat-internal user_id needed by the chatbot API (method=chatbot).
 * The webhook's "username" field corresponds to the Chat user's "nickname".
 *
 * @param incomingUrl - Bot incoming webhook URL (used to derive user_list URL)
 * @param webhookUsername - The username from the outgoing webhook payload
 * @param allowInsecureSsl - Skip TLS verification
 * @returns The correct Chat user_id, or undefined if not found
 */
export async function resolveChatUserId(
  incomingUrl: string,
  webhookUsername: string,
  allowInsecureSsl = true,
  log?: { warn: (...args: unknown[]) => void },
): Promise<number | undefined> {
  const users = await fetchChatUsers(incomingUrl, allowInsecureSsl, log);
  const lower = webhookUsername.toLowerCase();

  // Match by nickname first (webhook "username" field = Chat "nickname")
  const byNickname = users.find((u) => u.nickname.toLowerCase() === lower);
  if (byNickname) return byNickname.user_id;

  // Then by username
  const byUsername = users.find((u) => u.username.toLowerCase() === lower);
  if (byUsername) return byUsername.user_id;

  return undefined;
}

function buildWebhookBody(payload: ChatWebhookPayload, userId?: string | number): string {
  const numericId = parseNumericUserId(userId);
  if (numericId !== undefined) {
    payload.user_ids = [numericId];
  }
  return `payload=${encodeURIComponent(JSON.stringify(payload))}`;
}

function parseNumericUserId(userId?: string | number): number | undefined {
  if (userId === undefined) {
    return undefined;
  }
  const numericId = typeof userId === "number" ? userId : parseInt(userId, 10);
  return Number.isNaN(numericId) ? undefined : numericId;
}

function doPost(url: string, body: string, allowInsecureSsl = true): Promise<boolean> {
  return new Promise((resolve, reject) => {
    let parsedUrl: URL;
    try {
      parsedUrl = new URL(url);
    } catch {
      reject(new Error(`Invalid URL: ${url}`));
      return;
    }
    const transport = parsedUrl.protocol === "https:" ? https : http;

    const req = transport.request(
      url,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          "Content-Length": Buffer.byteLength(body),
        },
        timeout: 30_000,
        // Synology NAS may use self-signed certs on local network.
        // Set allowInsecureSsl: true in channel config to skip verification.
        rejectUnauthorized: !allowInsecureSsl,
      },
      (res) => {
        let data = "";
        res.on("data", (chunk: Buffer) => {
          data += chunk.toString();
        });
        res.on("end", () => {
          resolve(res.statusCode === 200);
        });
      },
    );

    req.on("error", reject);
    req.on("timeout", () => {
      req.destroy();
      reject(new Error("Request timeout"));
    });
    req.write(body);
    req.end();
  });
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
