import { Prisma, Webhook } from "@prisma/client";
import { v7 as uuidv7 } from "uuid";
import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { ZId } from "@formbricks/types/common";
import {
  DatabaseError,
  InvalidInputError,
  ResourceNotFoundError,
  UnknownError,
} from "@formbricks/types/errors";
import { DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS } from "@/lib/constants";
import { generateStandardWebhookSignature, generateWebhookSecret } from "@/lib/crypto";
import { validateInputs } from "@/lib/utils/validate";
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
import { getTranslate } from "@/lingodotdev/server";
import { isDiscordWebhook } from "@/modules/integrations/webhooks/lib/utils";
import { TWebhookInput } from "../types/webhooks";

const getWebhookTestErrorMessage = async (statusCode: number): Promise<string | null> => {
  switch (statusCode) {
    case 500: {
      const t = await getTranslate();
      return t("environments.integrations.webhooks.endpoint_internal_server_error");
    }
    case 404: {
      const t = await getTranslate();
      return t("environments.integrations.webhooks.endpoint_not_found_error");
    }
    case 405: {
      const t = await getTranslate();
      return t("environments.integrations.webhooks.endpoint_method_not_allowed_error");
    }
    case 502: {
      const t = await getTranslate();
      return t("environments.integrations.webhooks.endpoint_bad_gateway_error");
    }
    case 503: {
      const t = await getTranslate();
      return t("environments.integrations.webhooks.endpoint_service_unavailable_error");
    }
    case 504: {
      const t = await getTranslate();
      return t("environments.integrations.webhooks.endpoint_gateway_timeout_error");
    }
    default:
      return null;
  }
};

export const updateWebhook = async (
  webhookId: string,
  webhookInput: Partial<TWebhookInput>
): Promise<boolean> => {
  if (webhookInput.url) {
    await validateWebhookUrl(webhookInput.url);
  }

  try {
    await prisma.webhook.update({
      where: {
        id: webhookId,
      },
      data: {
        name: webhookInput.name,
        url: webhookInput.url,
        triggers: webhookInput.triggers,
        surveyIds: webhookInput.surveyIds || [],
      },
    });

    return true;
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      throw new DatabaseError(error.message);
    }

    throw error;
  }
};

export const deleteWebhook = async (id: string): Promise<boolean> => {
  try {
    await prisma.webhook.delete({
      where: {
        id,
      },
    });

    return true;
  } catch (error) {
    if (
      error instanceof Prisma.PrismaClientKnownRequestError &&
      error.code === PrismaErrorType.RelatedRecordDoesNotExist
    ) {
      throw new ResourceNotFoundError("Webhook", id);
    }
    throw new DatabaseError(`Database error when deleting webhook with ID ${id}`);
  }
};

export const createWebhook = async (
  environmentId: string,
  webhookInput: TWebhookInput,
  secret?: string
): Promise<Webhook> => {
  await validateWebhookUrl(webhookInput.url);

  try {
    if (isDiscordWebhook(webhookInput.url)) {
      throw new UnknownError("Discord webhooks are currently not supported.");
    }

    const signingSecret = secret ?? generateWebhookSecret();

    const webhook = await prisma.webhook.create({
      data: {
        ...webhookInput,
        surveyIds: webhookInput.surveyIds || [],
        secret: signingSecret,
        environment: {
          connect: {
            id: environmentId,
          },
        },
      },
    });

    return webhook;
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      throw new DatabaseError(error.message);
    }

    if (!(error instanceof InvalidInputError)) {
      throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`);
    }

    throw error;
  }
};

export const getWebhooks = async (environmentId: string): Promise<Webhook[]> => {
  validateInputs([environmentId, ZId]);

  try {
    const webhooks = await prisma.webhook.findMany({
      where: {
        environmentId: environmentId,
      },
      orderBy: {
        createdAt: "desc",
      },
    });
    return webhooks;
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      throw new DatabaseError(error.message);
    }

    throw error;
  }
};

export const testEndpoint = async (url: string, secret?: string): Promise<boolean> => {
  await validateWebhookUrl(url);

  if (isDiscordWebhook(url)) {
    throw new UnknownError("Discord webhooks are currently not supported.");
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);

  try {
    const webhookMessageId = uuidv7();
    const webhookTimestamp = Math.floor(Date.now() / 1000);
    const body = JSON.stringify({ event: "testEndpoint" });

    const requestHeaders: Record<string, string> = {
      "Content-Type": "application/json",
      "webhook-id": webhookMessageId,
      "webhook-timestamp": webhookTimestamp.toString(),
    };

    if (secret) {
      requestHeaders["webhook-signature"] = generateStandardWebhookSignature(
        webhookMessageId,
        webhookTimestamp,
        body,
        secret
      );
    }

    // `redirect: "manual"` prevents SSRF via redirect — validateWebhookUrl only checks the
    // initial URL, so following 30x to a private/internal host (e.g. cloud metadata) would bypass it.
    // Gated on the same env var as validateWebhookUrl: self-hosters who opted into trusting internal
    // URLs also get the pre-patch redirect-follow behavior for consistency.
    const redirectMode: RequestRedirect = DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS ? "follow" : "manual";
    const response = await fetch(url, {
      method: "POST",
      body,
      headers: requestHeaders,
      signal: controller.signal,
      redirect: redirectMode,
    });

    const statusCode = response.status;

    // With `redirect: "manual"`, Node's undici returns the actual 30x response (not the spec's
    // opaqueredirect filter). Treat any 30x as a redirect rejection so users get a clear error
    // instead of a misleading success. With `redirect: "follow"`, fetch returns the final 2xx/4xx/5xx
    // and this branch is unreachable.
    if (statusCode >= 300 && statusCode < 400) {
      throw new InvalidInputError("Webhook endpoint returned a redirect, which is not allowed");
    }

    const errorMessage = await getWebhookTestErrorMessage(statusCode);

    if (errorMessage) {
      throw new InvalidInputError(errorMessage);
    }

    return true;
  } catch (error) {
    if (error instanceof Error && error.name === "AbortError") {
      throw new UnknownError("Request timed out after 5 seconds");
    }

    if (error instanceof InvalidInputError || error instanceof UnknownError) {
      throw error;
    }

    throw new UnknownError(
      `Error while fetching the URL: ${error instanceof Error ? error.message : "Unknown error occurred"}`
    );
  } finally {
    clearTimeout(timeout);
  }
};
