import {
  EventFilter,
  ExternalSource,
  ExternalSourceTrigger,
  HandlerEvent,
  IntegrationTaskKey,
  Logger,
} from "@trigger.dev/sdk";
import {
  Document,
  {{ identifier | capitalize }}Webhooks,
  WebhookPayload,
  DeletePayload,
  Webhook,
} from "{{ sdkPackage }}";
import { z } from "zod";
import * as events from "./events";
import { {{ identifier | capitalize }}, {{ identifier | capitalize }}RunTask, serialize{{ identifier | capitalize }}Output } from "./index";
import { WebhookPayloadSchema } from "./schemas";
import { {{ identifier | capitalize }}ReturnType } from "./types";
import { queryProperties } from "./utils";

export class Webhooks {
  runTask: {{ identifier | capitalize }}RunTask;

  constructor(runTask: {{ identifier | capitalize }}RunTask) {
    this.runTask = runTask;
  }

  webhook(key: IntegrationTaskKey, params: { id: string }): {{ identifier | capitalize }}ReturnType<Webhook> {
    return this.runTask(
      key,
      async (client, task, io) => {
        return serialize{{ identifier | capitalize }}Output(await client.webhook(params.id));
      },
      {
        name: "Get Webhook",
        params,
        properties: [{ label: "Webhook ID", text: params.id }],
      }
    );
  }

  webhooks(key: IntegrationTaskKey, params?: Document.WebhooksQueryVariables): {{ identifier | capitalize }}ReturnType<Webhook[]> {
    return this.runTask(
      key,
      async (client, task, io) => {
        let connections = await client.webhooks(params);
        const hooks = connections.nodes;
        while (connections.pageInfo.hasNextPage) {
          connections = await connections.fetchNext();
          hooks.push(...connections.nodes);
        }
        return serialize{{ identifier | capitalize }}Output(hooks);
      },
      {
        name: "List Webhooks",
        params,
        properties: queryProperties(params ?? {}),
      }
    );
  }

  createWebhook(
    key: IntegrationTaskKey,
    params: Document.WebhookCreateInput
  ): {{ identifier | capitalize }}ReturnType<Omit<WebhookPayload, "webhook"> & { webhook: Webhook | undefined }> {
    return this.runTask(
      key,
      async (client, task, io) => {
        const payload = await client.createWebhook({ ...params, allPublicTeams: !params.teamId });
        return serialize{{ identifier | capitalize }}Output({
          ...payload,
          webhook: await payload.webhook,
        });
      },
      {
        name: "Create Webhook",
        params,
        properties: [
          { label: "Webhook URL", text: params.url },
          { label: "Resource Types", text: params.resourceTypes.join(", ") },
        ],
      }
    );
  }

  deleteWebhook(key: IntegrationTaskKey, params: { id: string }): {{ identifier | capitalize }}ReturnType<DeletePayload> {
    return this.runTask(
      key,
      async (client, task, io) => {
        return serialize{{ identifier | capitalize }}Output(await client.deleteWebhook(params.id));
      },
      {
        name: "Delete Webhook",
        params,
        properties: [{ label: "Webhook ID", text: params.id }],
      }
    );
  }

  updateWebhook(
    key: IntegrationTaskKey,
    params: { id: string; input: Document.WebhookUpdateInput }
  ): {{ identifier | capitalize }}ReturnType<Omit<WebhookPayload, "webhook"> & { webhook: Webhook | undefined }> {
    return this.runTask(
      key,
      async (client, task) => {
        const payload = await client.updateWebhook(params.id, params.input);
        return serialize{{ identifier | capitalize }}Output({
          ...payload,
          webhook: await payload.webhook,
        });
      },
      {
        name: "Update Webhook",
        params,
        properties: [
          { label: "Webhook ID", text: params.id },
          ...(params.input.url ? [{ label: "Webhook URL", text: params.input.url }] : []),
          ...(params.input.resourceTypes
            ? [{ label: "Resource Types", text: params.input.resourceTypes.join(", ") }]
            : []),
        ],
      }
    );
  }
}

type {{ identifier | capitalize }}Events = (typeof events)[keyof typeof events];

export type TriggerParams = {
  teamId?: string;
};

type CreateTriggersResult<TEventSpecification extends {{ identifier | capitalize }}Events> = ExternalSourceTrigger<
  TEventSpecification,
  ReturnType<typeof createWebhookEventSource>
>;

export function createTrigger<TEventSpecification extends {{ identifier | capitalize }}Events>(
  source: ReturnType<typeof createWebhookEventSource>,
  event: TEventSpecification,
  params: TriggerParams
): CreateTriggersResult<TEventSpecification> {
  return new ExternalSourceTrigger({
    event,
    params,
    source,
    options: {},
  });
}

const WebhookRegistrationDataSchema = z.object({
  success: z.literal(true),
  webhook: z.object({
    id: z.string(),
    enabled: z.boolean(),
  }),
});

export function createWebhookEventSource(
  integration: {{ identifier | capitalize }}
): ExternalSource<{{ identifier | capitalize }}, TriggerParams, "HTTP", {}> {
  return new ExternalSource("HTTP", {
    id: "{{ identifier }}.webhook",
    schema: z.object({
      teamId: z.string().optional(),
    }),
    version: "0.1.0",
    integration,
    key: (params) => `${params.teamId ? params.teamId : "all"}`,
    handler: webhookHandler,
    register: async (event, io, ctx) => {
      const { params, source: httpSource, options } = event;

      // (key-specific) stored data, undefined if not registered yet
      const webhookData = WebhookRegistrationDataSchema.safeParse(httpSource.data);

      // set of events to register
      const allEvents = Array.from(new Set([...options.event.desired, ...options.event.missing]));
      const registeredOptions = {
        event: allEvents,
      };

      // easily identify webhooks on {{ identifier }}
      const label = `trigger.${params.teamId ? params.teamId : "all"}`;

      if (httpSource.active && webhookData.success) {
        const hasMissingOptions = Object.values(options).some(
          (option) => option.missing.length > 0
        );
        if (!hasMissingOptions) return;

        const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
          id: webhookData.data.webhook.id,
          input: {
            label,
            resourceTypes: allEvents,
            secret: httpSource.secret,
            url: httpSource.url,
          },
        });

        return {
          data: WebhookRegistrationDataSchema.parse(updatedWebhook),
          options: registeredOptions,
        };
      }

      // check for existing hooks that match url
      const listResponse = await io.integration.webhooks("list-webhooks");
      const existingWebhook = listResponse.find((w) => w.url === httpSource.url);

      if (existingWebhook) {
        const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
          id: existingWebhook.id,
          input: {
            label,
            resourceTypes: allEvents,
            secret: httpSource.secret,
            url: httpSource.url,
          },
        });

        return {
          data: WebhookRegistrationDataSchema.parse(updatedWebhook),
          options: registeredOptions,
        };
      }

      const createPayload = await io.integration.createWebhook("create-webhook", {
        label,
        resourceTypes: allEvents,
        secret: httpSource.secret,
        teamId: params.teamId,
        url: httpSource.url,
      });

      return {
        data: WebhookRegistrationDataSchema.parse(createPayload),
        secret: (await createPayload.webhook)?.secret,
        options: registeredOptions,
      };
    },
  });
}

async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integration: {{ identifier | capitalize }}) {
  logger.debug("[@trigger.dev/{{ identifier }}] Handling webhook payload");

  const { rawEvent: request, source } = event;

  const payloadUuid = request.headers.get("{{ identifier | capitalize }}-Delivery");
  const payloadEvent = request.headers.get("{{ identifier | capitalize }}-Event");

  if (!payloadUuid || !payloadEvent) {
    logger.debug("[@trigger.dev/{{ identifier }}] Missing required {{ identifier | capitalize }} headers");
    return { events: [] };
  }

  if (!request.body) {
    logger.debug("[@trigger.dev/{{ identifier }}] No body found");
    return { events: [] };
  }

  const signature = request.headers.get("WEBHOOK_SIGNATURE_HEADER");

  if (!signature) {
    logger.error("[@trigger.dev/{{ identifier }}] Error validating webhook signature, no signature found");
    throw Error("[@trigger.dev/{{ identifier }}] No signature found");
  }

  const rawBody = await request.text();
  const body = JSON.parse(rawBody);
  const webhookHelper = new {{ identifier | capitalize }}Webhooks(source.secret);

  if (!webhookHelper.verify(Buffer.from(rawBody), signature)) {
    logger.error("[@trigger.dev/{{ identifier }}] Error validating webhook signature, they don't match");
    throw Error("[@trigger.dev/{{ identifier }}] Invalid signature");
  }

  const webhookPayload = WebhookPayloadSchema.parse(body);

  return {
    events: [
      {
        id: payloadUuid,
        name: payloadEvent,
        source: "{{ identifier }}.app",
        payload: webhookPayload,
        context: {},
      },
    ],
  };
}
