import { prisma } from "./prisma";
import { Prisma } from "../../prisma/generated/client";
import dns from "node:dns";
import { request as httpsRequest } from "node:https";
import { isIP } from "node:net";

const TELEGRAM_BOT_ID_FROM_TOKEN = (() => {
  if (!process.env.TELEGRAM_BOT_TOKEN) return undefined;
  const [candidate] = process.env.TELEGRAM_BOT_TOKEN.split(":", 1);
  return candidate?.trim() || undefined;
})();

export type TrafficAlertEvent = {
  level: "info" | "warning" | "error";
  action: string;
  surface: string;
  actorType: "SYSTEM" | "AGENT" | "USER";
  actorId: string;
  actorClass?: "a2a" | "external_ai_agent" | "likely_ai_agent" | "other_external";
  taskId?: string;
  message: string;
  sourceIp?: string;
  userAgent?: string;
  metadata?: Record<string, unknown>;
};

const TRAFFIC_WEBHOOK_URL = process.env.VIBEWORK_TRAFFIC_WEBHOOK_URL?.trim();
const TELEGRAM_BOT_TOKEN = (
  process.env.TELEGRAM_BOT_TOKEN ||
  process.env.VIBEWORKAIAGENTBOT_TOKEN ||
  process.env.VIBEWORK_AI_BOT_TOKEN
)?.trim();
const TELEGRAM_CHAT_ID = (
  process.env.TELEGRAM_CHAT_ID ||
  process.env.VIBEWORKAIAGENTBOT_CHAT_ID ||
  process.env.VIBEWORK_AI_BOT_CHAT_ID
)?.trim();
const TELEGRAM_CHAT_HANDLE = (
  process.env.TELEGRAM_CHAT_HANDLE ||
  process.env.TELEGRAM_CHAT_USERNAME ||
  process.env.TELEGRAM_CHAT_ALIAS
)?.trim();
const TELEGRAM_FALLBACK_FROM_UPDATES =
  process.env.TELEGRAM_FALLBACK_FROM_UPDATES?.trim().toLowerCase() === "true";
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL?.trim();
const TELEGRAM_NOTIFY_TIMEOUT_MS = Math.max(
  1,
  Number.parseInt(process.env.TELEGRAM_NOTIFY_TIMEOUT_MS?.trim() || "3000", 10) || 3000
);
const TELEGRAM_NOTIFY_MAX_ATTEMPTS = Math.max(
  1,
  Number.parseInt(process.env.TELEGRAM_NOTIFY_MAX_ATTEMPTS?.trim() || "2", 10) || 2
);
const TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS = Math.max(
  100,
  Number.parseInt(process.env.TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS?.trim() || "400", 10) || 400
);
const TELEGRAM_IP_FAMILY = Number.parseInt(
  process.env.TELEGRAM_IP_FAMILY?.trim() || "4",
  10
) as 4 | 6;
const TELEGRAM_NOTIFY_EXTERNAL_ONLY =
  process.env.TELEGRAM_NOTIFY_EXTERNAL_ONLY?.trim().toLowerCase() === "true";
const TELEGRAM_NOTIFY_SYSTEM_WARN_INTERNAL_SUPPRESS =
  process.env.TELEGRAM_NOTIFY_SYSTEM_WARN_INTERNAL_SUPPRESS?.trim().toLowerCase() !== "false";

const AI_USER_AGENT_HINTS = [
  "gpt",
  "chatgpt",
  "openai",
  "anthropic",
  "claude",
  "perplexity",
  "llm",
  "mcp",
  "autogpt",
  "agent",
  "assistant",
  "gemini",
  "cursor",
  "copilot",
];

function normalizeTrafficMetadata(event: TrafficAlertEvent): Record<string, unknown> {
  if (!event.metadata || typeof event.metadata !== "object") {
    return {};
  }
  return event.metadata as Record<string, unknown>;
}

function resolveActorClass(event: TrafficAlertEvent) {
  if (event.actorClass) {
    return event.actorClass;
  }

  const normalizedSurface = event.surface.trim().toLowerCase();
  if (normalizedSurface.startsWith("mcp/")) {
    return "a2a";
  }

  if (event.actorType === "AGENT" || event.actorId.toLowerCase().startsWith("agent:")) {
    return "external_ai_agent";
  }

  const metadata = normalizeTrafficMetadata(event);
  const userAgent = String(metadata.user_agent || "").toLowerCase();
  if (AI_USER_AGENT_HINTS.some((token) => userAgent.includes(token))) {
    return "likely_ai_agent";
  }

  const headers = metadata.request_actor_headers;
  if (typeof headers === "object" && headers !== null) {
    const headerText = Object.values(headers)
      .filter((item): item is string => typeof item === "string")
      .join(" ")
      .toLowerCase();

    if (AI_USER_AGENT_HINTS.some((token) => headerText.includes(token))) {
      return "likely_ai_agent";
    }
  }

  return event.action.startsWith("EXTERNAL_") ? "other_external" : "other_external";
}

function formatActorClass(actorClass: ReturnType<typeof resolveActorClass>) {
  if (actorClass === "a2a") return "A2A";
  if (actorClass === "external_ai_agent") return "外部 AI Agent";
  if (actorClass === "likely_ai_agent") return "疑似 AI 流量";
  return "其他外部流量";
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const INVALID_CHAT_ID_MARKERS = new Set(["", "n/a", "na", "null", "undefined"]);
const cachedTelegramFallbackChatIdByTarget = new Map<string | undefined, string | null>();

function normalizeChatId(rawChatId: string | undefined) {
  if (!rawChatId) return undefined;
  const normalized = rawChatId.trim();
  if (!normalized) return undefined;

  const lowered = normalized.toLowerCase();
  if (INVALID_CHAT_ID_MARKERS.has(lowered)) {
    return undefined;
  }

  if (TELEGRAM_BOT_ID_FROM_TOKEN && normalized === TELEGRAM_BOT_ID_FROM_TOKEN) {
    console.warn(
      "[Traffic alert] TELEGRAM_CHAT_ID looks like bot id, skip as chat target to avoid sending to bot itself."
    );
    return undefined;
  }

  return normalized;
}

function normalizeTelegramHandle(rawHandle: string | undefined) {
  if (!rawHandle) return undefined;
  const normalized = rawHandle.trim();
  if (!normalized) return undefined;
  return normalized.startsWith("@") ? normalized.slice(1).toLowerCase() : normalized.toLowerCase();
}

type TelegramChatRecord = {
  id: number | string;
  username?: string;
  title?: string;
};

function extractChatCandidates(update: Record<string, unknown>): TelegramChatRecord[] {
  const candidates: TelegramChatRecord[] = [];
  const messageContainer = (update.message as { chat?: TelegramChatRecord } | undefined)?.chat;
  const editedMessageContainer = (update.edited_message as { chat?: TelegramChatRecord } | undefined)?.chat;
  const callbackMessageContainer = (update.callback_query as { message?: { chat?: TelegramChatRecord } } | undefined)?.message
    ?.chat;
  const channelPostContainer = (update.channel_post as { chat?: TelegramChatRecord } | undefined)?.chat;
  const myChatMemberContainer = (update.my_chat_member as { chat?: TelegramChatRecord } | undefined)?.chat;

  for (const container of [
    messageContainer,
    editedMessageContainer,
    callbackMessageContainer,
    channelPostContainer,
    myChatMemberContainer,
  ]) {
    if (!container || typeof container !== "object") continue;
    const chat = container as TelegramChatRecord;
    if (typeof chat.id === "number" || typeof chat.id === "string") {
      candidates.push(chat);
    }
  }

  return candidates;
}

function extractChatIdFromCandidates(
  candidates: TelegramChatRecord[],
  preferredHandle?: string
): string | undefined {
  const handle = normalizeTelegramHandle(preferredHandle);
  for (const chat of candidates) {
    const chatId = normalizeChatId(String(chat.id));
    if (!chatId) continue;

    if (!handle) {
      return chatId;
    }

    const username = normalizeTelegramHandle(chat.username);
    const title = normalizeTelegramHandle(chat.title);
    if (username === handle || title === handle) {
      return chatId;
    }
  }
  return undefined;
}

async function resolveTelegramFallbackChatId(
  preferredHandle?: string
): Promise<string | undefined> {
  if (!TELEGRAM_BOT_TOKEN) {
    return undefined;
  }

  const cacheKey = normalizeTelegramHandle(preferredHandle);
  if (cachedTelegramFallbackChatIdByTarget.has(cacheKey)) {
    const cached = cachedTelegramFallbackChatIdByTarget.get(cacheKey);
    return cached || undefined;
  }

  try {
    const response = await requestTelegram(
      `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates?limit=20`,
      "GET"
    );

    if (response.status < 200 || response.status >= 300) {
      return undefined;
    }

    const data = (JSON.parse(response.text) as {
      result?: Array<Record<string, unknown>>;
    }) || {};

    const updates = Array.isArray(data.result) ? data.result : [];
    for (let index = updates.length - 1; index >= 0; index -= 1) {
      const update = updates[index];
      if (!update || typeof update !== "object") continue;

      const chatCandidates = extractChatCandidates(update as Record<string, unknown>);
      const candidateId = extractChatIdFromCandidates(chatCandidates, cacheKey);
      if (candidateId) {
        cachedTelegramFallbackChatIdByTarget.set(cacheKey, candidateId);
        const selector = cacheKey ? ` for ${cacheKey}` : "";
        console.log(
          `[Traffic alert] Resolved Telegram chat_id from updates${selector}: ${candidateId}`
        );
        return candidateId;
      }
    }

    cachedTelegramFallbackChatIdByTarget.set(cacheKey, null);
    if (cacheKey) {
      console.warn(
        `[Traffic alert] No Telegram chat match for handle=${cacheKey}; set a numeric TELEGRAM_CHAT_ID or message the bot first.`
      );
    }
    return undefined;
  } catch (error) {
    console.warn("[Traffic alert] resolveTelegramFallbackChatId failed", error);
    cachedTelegramFallbackChatIdByTarget.set(cacheKey, null);
    return undefined;
  }
}

async function resolveTelegramChatId(): Promise<string | undefined> {
  const explicitChatId = normalizeChatId(TELEGRAM_CHAT_ID);
  if (explicitChatId) return explicitChatId;

  const configuredHandle = normalizeTelegramHandle(TELEGRAM_CHAT_HANDLE);
  if (configuredHandle) {
    const resolvedChatId = await resolveTelegramFallbackChatId(configuredHandle);
    if (resolvedChatId) {
      return resolvedChatId;
    }
  }

  if (!TELEGRAM_FALLBACK_FROM_UPDATES) {
    return undefined;
  }

  return resolveTelegramFallbackChatId();
}

function escapeHtml(value: unknown) {
  if (value === null || value === undefined) return "";
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function extractIpFromActor(actorType: TrafficAlertEvent["actorType"], actorId: string) {
  if (actorType !== "USER") {
    return undefined;
  }

  const marker = actorId.lastIndexOf(":");
  if (marker < 0) return undefined;

  const maybeIp = actorId.slice(marker + 1).trim();
  if (isIP(maybeIp) || maybeIp.includes(".") || maybeIp.includes(":")) {
    return maybeIp;
  }

  return undefined;
}

function resolveDisplaySourceIp(event: TrafficAlertEvent) {
  const metadataIp = typeof event.metadata?.source_ip === "string" ? event.metadata.source_ip : undefined;
  return (
    event.sourceIp ||
    metadataIp ||
    extractIpFromActor(event.actorType, event.actorId) ||
    (event.actorType === "SYSTEM" ? "system" : undefined)
  );
}

function resolveDisplayUserAgent(event: TrafficAlertEvent) {
  return (
    event.userAgent ||
    (typeof event.metadata?.user_agent === "string" ? event.metadata.user_agent : undefined) ||
    (event.actorType === "SYSTEM" ? "system" : undefined)
  );
}

function buildTelegramMessage(event: TrafficAlertEvent) {
  const actorClass = resolveActorClass(event);
  return (
    `<b>VibeWork 流量告警</b>` +
    `\n- 平台: <code>agent-bounty-protocol</code>` +
    `\n- 等級: <code>${escapeHtml(event.level)}</code>` +
    `\n- 行為: <code>${escapeHtml(event.action)}</code>` +
    `\n- 通道: <code>${escapeHtml(event.surface)}</code>` +
    `\n- Actor: <code>${escapeHtml(`${event.actorType}/${event.actorId}`)}</code>` +
    `\n- 類別: <code>${escapeHtml(formatActorClass(actorClass))}</code>` +
    `\n- Source IP: <code>${escapeHtml(resolveDisplaySourceIp(event) || "n/a")}</code>` +
    `\n- User-Agent: <code>${escapeHtml(resolveDisplayUserAgent(event) || "n/a")}</code>` +
    `\n- 回應: <code>${escapeHtml(typeof event.metadata?.response_status === "number" ? event.metadata.response_status : "n/a")}</code>` +
    `\n- 任務: <code>${escapeHtml(event.taskId || "n/a")}</code>` +
    `\n- request_id: <code>${escapeHtml(typeof event.metadata?.request_id === "string" ? event.metadata.request_id : "n/a")}</code>` +
    `\n- 訊息: <code>${escapeHtml(event.message)}</code>`
  );
}

function isExternalAction(action: string) {
  return action.startsWith("EXTERNAL_");
}

function shouldNotifyViaTelegram(event: TrafficAlertEvent) {
  if (!TELEGRAM_NOTIFY_EXTERNAL_ONLY) {
    return true;
  }

  if (isExternalAction(event.action)) {
    return true;
  }

  if (event.level === "error") {
    return true;
  }

  if (TELEGRAM_NOTIFY_SYSTEM_WARN_INTERNAL_SUPPRESS && event.level === "warning") {
    return false;
  }

  return false;
}

async function sendViaHttps(url: string, body: Record<string, unknown>) {
  return new Promise<{ messageId?: string }>((resolve, reject) => {
    try {
      const payload = JSON.stringify(body);

      requestTelegram(url, "POST", payload)
        .then(async (response) => {
          const responseBody = response.text;
          if (response.status >= 200 && response.status < 300) {
            let messageId: string | undefined;
            try {
              const parsed = JSON.parse(responseBody) as { result?: { message_id?: number } };
              const parsedMessageId = parsed?.result?.message_id;
              if (typeof parsedMessageId === "number") {
                messageId = String(parsedMessageId);
              }
            } catch {
              // ignore parse errors for non-JSON success responses
            }

            resolve({ messageId });
            return;
          }

          reject(new Error(`Telegram API ${response.status}: ${responseBody.slice(0, 200)}`));
        })
        .catch((error) => {
          if (error && error.name === "TimeoutError") {
            reject(new Error(`Telegram API timeout ${TELEGRAM_NOTIFY_TIMEOUT_MS}ms`));
            return;
          }
          reject(error);
        });
    } catch (error) {
      reject(error);
    }
  });
}

type TelegramRequestResult = {
  status: number;
  text: string;
};

function requestTelegram(
  url: string,
  method: "GET" | "POST",
  payload?: string
): Promise<TelegramRequestResult> {
  return new Promise<TelegramRequestResult>((resolve, reject) => {
    try {
      const requestUrl = new URL(url);
      const request = httpsRequest(
        {
          protocol: requestUrl.protocol,
          hostname: requestUrl.hostname,
          port: requestUrl.port,
          path: `${requestUrl.pathname}${requestUrl.search}`,
          method,
          headers: {
            ...(payload
              ? {
                  "content-type": "application/json",
                  "content-length": String(Buffer.byteLength(payload)),
                }
              : {}),
          },
          timeout: TELEGRAM_NOTIFY_TIMEOUT_MS,
          family: TELEGRAM_IP_FAMILY,
          lookup: (hostname, options, callback) => {
            dns.lookup(hostname, { ...options, family: TELEGRAM_IP_FAMILY }, callback);
          },
        },
        (response) => {
          let responseText = "";
          response.setEncoding("utf8");
          response.on("data", (chunk) => {
            responseText += chunk;
          });
          response.on("end", () => {
            resolve({
              status: response.statusCode || 0,
              text: responseText,
            });
          });
        }
      );

      request.on("timeout", () => {
        request.destroy(new Error(`Telegram API timeout ${TELEGRAM_NOTIFY_TIMEOUT_MS}ms`));
      });
      request.on("error", (error) => {
        reject(error);
      });

      if (payload) {
        request.write(payload);
      }
      request.end();
    } catch (error) {
      reject(error);
    }
  });
}

function resolveEntityFromTrafficEvent(event: TrafficAlertEvent) {
  if (event.taskId) {
    return { entityType: "TASK", entityId: event.taskId };
  }

  return {
    entityType: "TASK",
    entityId: `surface:${event.surface}`,
  };
}

async function writeTrafficAuditEvent(event: TrafficAlertEvent) {
  const actorClass = resolveActorClass(event);
  const { entityType, entityId } = resolveEntityFromTrafficEvent(event);
  const sourceIp =
    event.sourceIp ||
    (typeof event.metadata?.source_ip === "string" ? event.metadata.source_ip : undefined) ||
    (event.actorType === "SYSTEM" ? "system" : undefined);
  const userAgent =
    event.userAgent ||
    (typeof event.metadata?.user_agent === "string" ? event.metadata.user_agent : undefined) ||
    (event.actorType === "SYSTEM" ? "system" : undefined);

  try {
    await prisma.auditEvent.create({
      data: {
        actorType: event.actorType,
        actorId: event.actorId,
        action: event.action,
        entityType,
        entityId,
        beforeState: Prisma.JsonNull,
        afterState: Prisma.JsonNull,
        reason: event.message,
        metadata: {
          ...event.metadata,
          actor_class: actorClass,
          level: event.level,
          surface: event.surface,
          source_ip: sourceIp,
          user_agent: userAgent,
          source: "traffic-alert",
        },
      },
    });
  } catch (error) {
    console.error("[Traffic alert persistence error]", error);
  }
}

export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void> {
  const actorClass = resolveActorClass(event);
  const enrichedEvent: TrafficAlertEvent = { ...event, actorClass };
  void writeTrafficAuditEvent(enrichedEvent);
  const eventSourceIp =
    enrichedEvent.sourceIp ??
    (typeof enrichedEvent.metadata?.source_ip === "string" ? enrichedEvent.metadata.source_ip : undefined);
  const eventUserAgent =
    enrichedEvent.userAgent ??
    (typeof enrichedEvent.metadata?.user_agent === "string" ? enrichedEvent.metadata.user_agent : undefined);

  const payload = {
    platform: "agent-bounty-protocol",
    created_at: new Date().toISOString(),
    actor_class: actorClass,
    source_ip: eventSourceIp,
    user_agent: eventUserAgent,
    ...enrichedEvent,
  };

  const resolvedTelegramChatId = await resolveTelegramChatId();

  const notifyTargets = [
    TRAFFIC_WEBHOOK_URL && {
      kind: "generic",
      url: TRAFFIC_WEBHOOK_URL,
      init: {
        method: "POST",
        headers: {
          "content-type": "application/json",
          "x-trace-source": "agent-bounty-protocol",
        },
        body: JSON.stringify(payload),
      },
    },
    DISCORD_WEBHOOK_URL && {
      kind: "discord",
      url: DISCORD_WEBHOOK_URL,
      init: {
        method: "POST",
        headers: {
          "content-type": "application/json",
        },
        body: JSON.stringify({
          content: `\`\`\`\n${JSON.stringify(payload, null, 2)}\n\`\`\``,
        }),
      },
    },
    TELEGRAM_BOT_TOKEN &&
    resolvedTelegramChatId &&
    shouldNotifyViaTelegram(event) && {
      kind: "telegram",
      url: `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
      init: {
        method: "POST",
        headers: {
          "content-type": "application/json",
        },
        body: JSON.stringify({
          chat_id: resolvedTelegramChatId,
          text: buildTelegramMessage(event),
          parse_mode: "HTML",
        }),
      },
    },
  ].filter(Boolean) as Array<{ kind: string; url: string; init: RequestInit }>;

  if (notifyTargets.length === 0) return;

  await Promise.allSettled(
    notifyTargets.map(async (target) => {
      try {
        if (target.kind === "telegram") {
          const payload = target.init.body
            ? JSON.parse(typeof target.init.body === "string" ? target.init.body : "{}")
            : {};

          for (let attempt = 1; ; attempt += 1) {
            try {
              const result = await sendViaHttps(target.url, payload);
              console.log(
                "[Traffic alert notify ok] telegram",
                `action=${event.action}`,
                `surface=${event.surface}`,
                `actor=${event.actorType}/${event.actorId}`,
                `request_id=${typeof event.metadata?.request_id === "string" ? event.metadata.request_id : "n/a"}`,
                `attempt=${attempt}`,
                `message_id=${result.messageId || "n/a"}`
              );
              return;
            } catch (error) {
              if (attempt >= TELEGRAM_NOTIFY_MAX_ATTEMPTS) {
                console.warn(
                  `[Traffic alert] telegram notify failed: ${event.action} attempt=${attempt}`,
                  error
                );
                throw error;
              }
              console.warn(
                `[Traffic alert] telegram notify retrying action=${event.action} attempt=${attempt}`,
                error
              );
              await sleep(TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS * attempt);
            }
          }
        } else {
          const response = await fetch(target.url, { ...target.init, signal: AbortSignal.timeout(3000) });
          if (!response.ok) {
            console.warn(
              `[Traffic alert notify failed] ${target.kind} ${response.status} ${await response.text()}`
            );
          }
        }
      } catch (error) {
        console.warn(`[Traffic alert notify failed] ${target.kind}`, error);
      }
    })
  );
}
