import { PipelineTriggers, Webhook } from "@prisma/client";
import { headers } from "next/headers";
import { v7 as uuidv7 } from "uuid";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { sendTelemetryEvents } from "@/app/api/(internal)/pipeline/lib/telemetry";
import { ZPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { CRON_SECRET, DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS, POSTHOG_KEY } from "@/lib/constants";
import { generateStandardWebhookSignature } from "@/lib/crypto";
import { getIntegrations } from "@/lib/integration/service";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getResponseCountBySurveyId } from "@/lib/response/service";
import { getSurvey, updateSurvey } from "@/lib/survey/service";
import { convertDatesInObject } from "@/lib/time";
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
import { queueAuditEvent } from "@/modules/ee/audit-logs/lib/handler";
import { TAuditStatus, UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
import { recordResponseCreatedMeterEvent } from "@/modules/ee/billing/lib/metering";
import { sendResponseFinishedEmail } from "@/modules/email";
import { resolveStorageUrlsInObject } from "@/modules/storage/utils";
import { sendFollowUpsForResponse } from "@/modules/survey/follow-ups/lib/follow-ups";
import { FollowUpSendError } from "@/modules/survey/follow-ups/types/follow-up";
import { handleIntegrations } from "./lib/handleIntegrations";
import { captureSurveyResponsePostHogEvent } from "./lib/posthog";

export const POST = async (request: Request) => {
  const requestHeaders = await headers();
  // Check authentication
  if (requestHeaders.get("x-api-key") !== CRON_SECRET) {
    return responses.notAuthenticatedResponse();
  }

  const jsonInput = await request.json();
  const convertedJsonInput = convertDatesInObject(
    jsonInput,
    new Set(["contactAttributes", "variables", "data", "meta"])
  );

  const inputValidation = ZPipelineInput.safeParse(convertedJsonInput);

  if (!inputValidation.success) {
    logger.error(
      { error: inputValidation.error, url: request.url },
      "Error in POST /api/(internal)/pipeline"
    );
    return responses.badRequestResponse(
      "Fields are missing or incorrectly formatted",
      transformErrorToDetails(inputValidation.error),
      true
    );
  }

  const { environmentId, surveyId, event, response } = inputValidation.data;

  const organization = await getOrganizationByEnvironmentId(environmentId);
  if (!organization) {
    throw new ResourceNotFoundError("Organization", "Organization not found");
  }

  // Fetch survey for webhook payload
  const survey = await getSurvey(surveyId);
  if (!survey) {
    logger.error({ url: request.url, surveyId }, `Survey with id ${surveyId} not found`);

    return responses.notFoundResponse("Survey", surveyId, true);
  }

  if (survey.environmentId !== environmentId) {
    logger.error(
      { url: request.url, surveyId, environmentId, surveyEnvironmentId: survey.environmentId },
      `Survey ${surveyId} does not belong to environment ${environmentId}`
    );
    return responses.badRequestResponse("Survey not found in this environment");
  }

  // Fetch webhooks
  const getWebhooksForPipeline = async (environmentId: string, event: PipelineTriggers, surveyId: string) => {
    const webhooks = await prisma.webhook.findMany({
      where: {
        environmentId,
        triggers: { has: event },
        OR: [{ surveyIds: { has: surveyId } }, { surveyIds: { isEmpty: true } }],
      },
    });
    return webhooks;
  };

  const webhooks: Webhook[] = await getWebhooksForPipeline(environmentId, event, surveyId);
  // Prepare webhook and email promises

  // Fetch with timeout of 5 seconds to prevent hanging.
  // `redirect: "manual"` blocks SSRF via redirect — webhook URLs are validated against private/internal
  // ranges before delivery, but redirect targets would otherwise bypass that check. 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 fetchWithTimeout = (url: string, options: RequestInit, timeout: number = 5000): Promise<Response> => {
    return Promise.race([
      fetch(url, { ...options, redirect: redirectMode }),
      new Promise<never>((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
    ]);
  };

  const resolvedResponseData = resolveStorageUrlsInObject(response.data);

  const webhookPromises = webhooks.map((webhook) => {
    const body = JSON.stringify({
      webhookId: webhook.id,
      event,
      data: {
        ...response,
        data: resolvedResponseData,
        survey: {
          title: survey.name,
          type: survey.type,
          status: survey.status,
          createdAt: survey.createdAt,
          updatedAt: survey.updatedAt,
        },
      },
    });

    // Generate Standard Webhooks headers
    const webhookMessageId = uuidv7();
    const webhookTimestamp = Math.floor(Date.now() / 1000);

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

    // Add signature if webhook has a secret configured
    if (webhook.secret) {
      requestHeaders["webhook-signature"] = generateStandardWebhookSignature(
        webhookMessageId,
        webhookTimestamp,
        body,
        webhook.secret
      );
    }

    return validateWebhookUrl(webhook.url)
      .then(() =>
        fetchWithTimeout(webhook.url, {
          method: "POST",
          headers: requestHeaders,
          body,
        })
      )
      .catch((error) => {
        logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`);
      });
  });

  if (event === "responseFinished") {
    // Fetch integrations and responseCount in parallel
    const [integrations, responseCount] = await Promise.all([
      getIntegrations(environmentId),
      getResponseCountBySurveyId(surveyId),
    ]);

    if (integrations.length > 0) {
      await handleIntegrations(integrations, inputValidation.data, survey);
    }

    // Fetch users with notifications in a single query
    // TODO: add cache for this query. Not possible at the moment since we can't get the membership cache by environmentId
    const usersWithNotifications = await prisma.user.findMany({
      where: {
        memberships: {
          some: {
            organization: {
              projects: {
                some: {
                  environments: {
                    some: { id: environmentId },
                  },
                },
              },
            },
          },
        },
        OR: [
          {
            memberships: {
              every: {
                role: {
                  in: ["owner", "manager"],
                },
              },
            },
          },
          {
            teamUsers: {
              some: {
                team: {
                  projectTeams: {
                    some: {
                      project: {
                        environments: {
                          some: {
                            id: environmentId,
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
          },
        ],
        notificationSettings: {
          path: ["alert", surveyId],
          equals: true,
        },
      },
      select: { email: true, locale: true },
    });

    if (survey.followUps?.length > 0) {
      // send follow up emails
      const followUpsResult = await sendFollowUpsForResponse(response.id);
      if (!followUpsResult.ok) {
        const { error: followUpsError } = followUpsResult;
        if (followUpsError.code !== FollowUpSendError.FOLLOW_UP_NOT_ALLOWED) {
          logger.error({ error: followUpsError }, `Failed to send follow-up emails for survey ${surveyId}`);
        }
      }
    }

    const emailPromises = usersWithNotifications.map((user) =>
      sendResponseFinishedEmail(
        user.email,
        user.locale,
        environmentId,
        survey,
        response,
        responseCount
      ).catch((error) => {
        logger.error(
          { error, url: request.url, userEmail: user.email },
          `Failed to send email to ${user.email}`
        );
      })
    );

    // Update survey status if necessary
    if (survey.autoComplete && responseCount >= survey.autoComplete) {
      let logStatus: TAuditStatus = "success";

      try {
        await updateSurvey({
          ...survey,
          status: "completed",
        });
      } catch (error) {
        logStatus = "failure";
        logger.error(
          { error, url: request.url, surveyId },
          `Failed to update survey ${surveyId} status to completed`
        );
      } finally {
        await queueAuditEvent({
          status: logStatus,
          action: "updated",
          targetType: "survey",
          userId: UNKNOWN_DATA,
          userType: "system",
          targetId: survey.id,
          organizationId: organization.id,
          newObject: {
            status: "completed",
          },
        });
      }
    }

    // Await webhook and email promises with allSettled to prevent early rejection
    const results = await Promise.allSettled([...webhookPromises, ...emailPromises]);
    results.forEach((result) => {
      if (result.status === "rejected") {
        logger.error({ error: result.reason, url: request.url }, "Promise rejected");
      }
    });
  } else {
    // Await webhook promises if no emails are sent (with allSettled to prevent early rejection)
    const results = await Promise.allSettled(webhookPromises);
    results.forEach((result) => {
      if (result.status === "rejected") {
        logger.error({ error: result.reason, url: request.url }, "Promise rejected");
      }
    });
  }
  if (event === "responseCreated") {
    recordResponseCreatedMeterEvent({
      stripeCustomerId: organization.billing.stripeCustomerId,
      responseId: response.id,
      createdAt: response.createdAt,
    }).catch((error) => {
      logger.error({ error, responseId: response.id }, "Failed to record response meter event");
    });

    if (POSTHOG_KEY) {
      const responseCount = await getResponseCountBySurveyId(surveyId);

      captureSurveyResponsePostHogEvent({
        organizationId: organization.id,
        surveyId,
        surveyType: survey.type,
        environmentId,
        responseCount,
      });
    }

    // Send telemetry events
    await sendTelemetryEvents();
  }

  return Response.json({ data: {} });
};
