const crypto = require("crypto");
const express = require("express");
const { createLogger } = require("./logger");

const webhookLog = createLogger("core:webhooks", { category: "integration" });

function createWebhookService({ limit = "256kb" } = {}) {
  const endpoints = new Map();
  const endpointKeysByPlugin = new Map();
  const router = express.Router();

  router.use(express.raw({ type: "*/*", limit }));
  router.all("/:namespace/:slug", async (req, res) => {
    const namespace = normalizeSegment(req.params.namespace);
    const slug = normalizeSegment(req.params.slug);
    const endpoint = namespace && slug
      ? endpoints.get(`${namespace}/${slug}`)
      : null;
    if (!endpoint) {
      return res.status(404).json({ error: "Webhook endpoint not found." });
    }
    if (!endpoint.methods.includes(req.method.toUpperCase())) {
      res.set("Allow", endpoint.methods.join(", "));
      return res.status(405).json({ error: "Method not allowed." });
    }

    const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || "");
    const rawBodyText = rawBody.toString("utf8");
    let parsedJson = null;
    let jsonError = null;
    if (rawBody.length) {
      try {
        parsedJson = JSON.parse(rawBodyText);
      } catch (error) {
        jsonError = error;
      }
    }
    const context = {
      req,
      headers: { ...req.headers },
      method: req.method,
      namespace,
      slug,
      rawBody,
      rawBodyText,
      parsedJson,
      jsonError,
      receivedAt: Date.now(),
      remoteAddress: req.ip || req.socket?.remoteAddress || null,
      userAgent: req.get("user-agent") || null
    };

    try {
      if (endpoint.verify) {
        const verification = await endpoint.verify(context);
        if (verification === false || verification?.ok === false) {
          return res.status(Number(verification?.status) || 401).json({
            error: verification?.message || "Webhook verification failed."
          });
        }
        context.verification = verification || { ok: true };
      }
      return sendHandlerResult(res, await endpoint.handler(context));
    } catch (error) {
      webhookLog.error("Webhook handler failed", {
        pluginId: endpoint.pluginId,
        endpointId: endpoint.endpointId,
        namespace,
        slug,
        message: error?.message || String(error),
        stack: error?.stack || ""
      }, { event: "inbound_webhook_failed" });
      if (!res.headersSent) {
        return res.status(500).json({ error: "Webhook processing failed." });
      }
      return null;
    }
  });
  router.use((error, req, res, _next) => {
    const status = error?.type === "entity.too.large" ? 413 : 400;
    webhookLog.warn("Webhook request rejected", {
      path: req.path,
      status,
      message: error?.message || String(error)
    }, { event: "inbound_webhook_rejected" });
    res.status(status).json({
      error: status === 413 ? "Webhook payload is too large." : "Invalid webhook request."
    });
  });

  function registerInbound({
    pluginId,
    namespace,
    endpointId,
    slug,
    description,
    handler,
    verify,
    options = {}
  }) {
    const safePluginId = requireValue(pluginId, "pluginId");
    const safeEndpointId = requireValue(endpointId, "endpointId");
    const safeNamespace = requireSegment(namespace, "namespace");
    const safeSlug = requireSegment(slug, "slug");
    if (typeof handler !== "function") {
      throw new Error("Webhook handler must be a function.");
    }
    const key = `${safeNamespace}/${safeSlug}`;
    if (endpoints.has(key)) {
      throw new Error(`Webhook endpoint already registered: ${key}`);
    }
    unregisterInbound({ pluginId: safePluginId, endpointId: safeEndpointId });
    const endpoint = {
      pluginId: safePluginId,
      endpointId: safeEndpointId,
      namespace: safeNamespace,
      slug: safeSlug,
      description: (description || "").toString(),
      handler,
      verify: typeof verify === "function" ? verify : null,
      methods: normalizeMethods(options.methods),
      options
    };
    endpoints.set(key, endpoint);
    if (!endpointKeysByPlugin.has(safePluginId)) {
      endpointKeysByPlugin.set(safePluginId, new Map());
    }
    endpointKeysByPlugin.get(safePluginId).set(safeEndpointId, key);
    webhookLog.info("Webhook endpoint registered", {
      pluginId: safePluginId,
      endpointId: safeEndpointId,
      path: `/webhooks/${key}`
    }, { event: "webhook_registered" });
    return { namespace: safeNamespace, slug: safeSlug, path: `/webhooks/${key}` };
  }

  function unregisterInbound({ pluginId, endpointId }) {
    const pluginEndpoints = endpointKeysByPlugin.get((pluginId || "").toString());
    const key = pluginEndpoints?.get((endpointId || "").toString());
    if (!key) {
      return false;
    }
    endpoints.delete(key);
    pluginEndpoints.delete((endpointId || "").toString());
    if (!pluginEndpoints.size) {
      endpointKeysByPlugin.delete((pluginId || "").toString());
    }
    webhookLog.debug("Webhook endpoint unregistered", { pluginId, endpointId }, { event: "webhook_unregistered" });
    return true;
  }

  function buildPublicUrl({ namespace, slug, req, baseUrl }) {
    const safeNamespace = requireSegment(namespace, "namespace");
    const safeSlug = requireSegment(slug, "slug");
    const origin = (baseUrl || requestOrigin(req) || "").replace(/\/+$/, "");
    const routePath = `/webhooks/${safeNamespace}/${safeSlug}`;
    return origin ? `${origin}${routePath}` : routePath;
  }

  return {
    router,
    registerInbound,
    unregisterInbound,
    buildPublicUrl,
    generateSlug,
    send: sendWebhook,
    sendJson: ({ payload, ...options }) => sendWebhook({ ...options, json: payload }),
    isTimestampWithinWindow,
    getRegisteredEndpoints: () =>
      Array.from(endpoints.values()).map(({ handler, verify, ...entry }) => entry)
  };
}

async function sendWebhook({
  pluginId,
  url,
  method = "POST",
  headers = {},
  json,
  body,
  timeoutMs = 10000,
  retries = 0,
  sign
}) {
  if (!url) {
    throw new Error("Webhook URL is required.");
  }
  if (json !== undefined && body !== undefined) {
    throw new Error("Provide either json or body, not both.");
  }
  const requestHeaders = { ...headers };
  let requestBody = body;
  if (json !== undefined) {
    requestBody = JSON.stringify(json);
    if (!hasHeader(requestHeaders, "content-type")) {
      requestHeaders["Content-Type"] = "application/json";
    }
  }
  if (typeof sign === "function") {
    const signedHeaders = await sign({
      method,
      url,
      headers: { ...requestHeaders },
      body: requestBody
    });
    if (signedHeaders && typeof signedHeaders === "object") {
      Object.assign(requestHeaders, signedHeaders);
    }
  }

  const attempts = Math.max(1, Number(retries) + 1);
  let lastError = null;
  let lastDurationMs = 0;
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    const startedAt = Date.now();
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), Math.max(1, Number(timeoutMs)));
    try {
      const response = await fetch(url, {
        method,
        headers: requestHeaders,
        body: requestBody,
        signal: controller.signal
      });
      const result = {
        status: response.status,
        headers: Object.fromEntries(response.headers.entries()),
        body: await response.text(),
        durationMs: Date.now() - startedAt,
        success: response.ok
      };
      if (response.ok || attempt === attempts) {
        if (!response.ok) {
          webhookLog.warn("Outbound webhook returned an error", {
            pluginId: pluginId || null,
            url: redactUrl(url),
            status: response.status,
            attempt
          }, { event: "outbound_webhook_error" });
        }
        return result;
      }
    } catch (error) {
      lastError = error;
      lastDurationMs = Date.now() - startedAt;
      if (attempt === attempts) {
        webhookLog.error("Outbound webhook failed", {
          pluginId: pluginId || null,
          url: redactUrl(url),
          attempt,
          message: error?.message || String(error)
        }, { event: "outbound_webhook_failed" });
      }
    } finally {
      clearTimeout(timer);
    }
  }
  return {
    status: 0,
    headers: {},
    body: "",
    durationMs: lastDurationMs,
    success: false,
    error: lastError?.message || "Webhook request failed."
  };
}

function generateSlug({ identifier, uuid = crypto.randomUUID() }) {
  return `${sanitizeIdentifier(identifier)}-${requireUuid(uuid)}`;
}

function sanitizeIdentifier(value) {
  const safe = (value || "")
    .toString()
    .trim()
    .toLowerCase()
    .replace(/\s+/g, "-")
    .replace(/[^a-z0-9_-]+/g, "")
    .replace(/-+/g, "-")
    .replace(/^[-_]+|[-_]+$/g, "");
  if (!safe) {
    throw new Error("Webhook identifier must contain URL-safe letters or numbers.");
  }
  return safe;
}

function requireUuid(value) {
  const uuid = (value || "").toString().toLowerCase();
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(uuid)) {
    throw new Error("Webhook UUID is invalid.");
  }
  return uuid;
}

function requireSegment(value, label) {
  const normalized = normalizeSegment(value);
  if (!normalized) {
    throw new Error(`Webhook ${label} is invalid.`);
  }
  return normalized;
}

function normalizeSegment(value) {
  const raw = (value || "").toString().trim().toLowerCase();
  if (!raw || raw.includes("/") || raw.includes("\\") || raw.includes("..")) {
    return null;
  }
  if (/%2f|%5c/i.test(raw)) {
    return null;
  }
  return /^[a-z0-9][a-z0-9_-]*$/.test(raw) ? raw : null;
}

function requireValue(value, label) {
  const safe = (value || "").toString().trim();
  if (!safe) {
    throw new Error(`Webhook ${label} is required.`);
  }
  return safe;
}

function normalizeMethods(methods) {
  const source = Array.isArray(methods) && methods.length ? methods : ["POST"];
  return Array.from(new Set(source.map((method) => method.toString().toUpperCase())));
}

function sendHandlerResult(res, result) {
  const status = Number(result?.status) || 204;
  if (result?.headers && typeof result.headers === "object") {
    res.set(result.headers);
  }
  if (result?.body === undefined || result?.body === null) {
    return res.status(status).end();
  }
  if (Buffer.isBuffer(result.body) || typeof result.body === "string") {
    return res.status(status).send(result.body);
  }
  return res.status(status).json(result.body);
}

function requestOrigin(req) {
  if (!req) {
    return "";
  }
  const forwardedProto = req.get?.("x-forwarded-proto");
  const protocol = forwardedProto ? forwardedProto.split(",")[0].trim() : req.protocol;
  const host = req.get?.("host");
  return protocol && host ? `${protocol}://${host}` : "";
}

function isTimestampWithinWindow(timestamp, windowSeconds = 300, nowMs = Date.now()) {
  if (!/^\d+$/.test((timestamp || "").toString())) {
    return false;
  }
  const timestampMs = Number(timestamp) * 1000;
  return Number.isFinite(timestampMs) &&
    Math.abs(nowMs - timestampMs) <= Math.max(0, Number(windowSeconds)) * 1000;
}

function hasHeader(headers, name) {
  return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
}

function redactUrl(value) {
  try {
    const url = new URL(value);
    return `${url.protocol}//${url.host}${url.pathname}`;
  } catch {
    return "[invalid-url]";
  }
}

module.exports = {
  createWebhookService,
  generateSlug,
  sanitizeIdentifier,
  isTimestampWithinWindow
};
