import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
import { getRedisService } from "@calcom/features/di/containers/Redis";
import { checkIfFreeEmailDomain } from "@calcom/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain";
import { getLocation } from "@calcom/lib/CalEventParser";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { RetryableError } from "@calcom/lib/crmManager/errors";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import type { CalEventResponses, CalendarEvent } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { Contact, CRM, CrmEvent } from "@calcom/types/CrmService";
import type { Connection, Field, TokenResponse } from "@jsforce/jsforce-node";
import jsforce from "@jsforce/jsforce-node";
import { RRule } from "rrule";
import { z } from "zod";
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
import { default as appMeta } from "../config.json";
import type { appDataSchema, RRSkipFieldRule, writeToBookingEntry, writeToRecordDataSchema } from "../zod";
import { RRSkipFieldRuleActionEnum } from "../zod";
import {
  DateFieldTypeData,
  RoutingReasons,
  SalesforceFieldType,
  SalesforceRecordEnum,
  WhenToWriteToRecord,
} from "./enums";
import { PrismaAssignmentReasonRepository } from "./repositories/PrismaAssignmentReasonRepository";

/**
 * Extended CRM interface with Salesforce-specific methods.
 */
export interface SalesforceCRM extends CRM {
  findUserEmailFromLookupField(
    attendeeEmail: string,
    fieldName: string,
    salesforceObject: SalesforceRecordEnum
  ): Promise<{ email: string; recordType: RoutingReasons } | undefined>;

  incompleteBookingWriteToRecord(
    email: string,
    writeToRecordObject: z.infer<typeof writeToRecordDataSchema>
  ): Promise<void>;

  getAllPossibleAccountWebsiteFromEmailDomain(emailDomain: string): string;
}

import { getSalesforceAppKeys } from "./getSalesforceAppKeys";
import { getSalesforceTokenLifetime } from "./getSalesforceTokenLifetime";
import { SalesforceGraphQLClient } from "./graphql/SalesforceGraphQLClient";
import getAllPossibleWebsiteValuesFromEmailDomain from "./utils/getAllPossibleWebsiteValuesFromEmailDomain";
import type { GetDominantAccountIdInput } from "./utils/getDominantAccountId";
import getDominantAccountId from "./utils/getDominantAccountId";

class SFObjectToUpdateNotFoundError extends RetryableError {
  constructor(message: string) {
    super(message);
  }
}

type ExtendedTokenResponse = TokenResponse & {
  instance_url: string;
};

const sfApiErrors = {
  INVALID_EVENTWHOIDS: "INVALID_FIELD: No such column 'EventWhoIds' on sobject of type Event",
};

type ContactRecord = {
  Id?: string;
  Email?: string;
  OwnerId?: string;
  AccountId?: string;
  attributes?: {
    type?: string;
  };
  Account?: {
    Owner?: {
      Email?: string;
    };
    Website?: string;
  };
  Owner?: {
    Email?: string;
    Name?: string;
  };
};

type SalesforceDuplicateError = {
  name?: string;
  duplicateResult?: {
    matchResults?: Array<{
      matchRecords?: Array<{
        record: { Id: string };
      }>;
    }>;
  };
};

type Attendee = { email: string; name: string };

const salesforceTokenSchema = z.object({
  id: z.string(),
  issued_at: z.string(), // Salesforce returns this in milliseconds as a string
  instance_url: z.string(),
  signature: z.string(),
  access_token: z.string(),
  scope: z.string(),
  token_type: z.string(),
  token_lifetime: z.number().optional(), // Token lifetime in seconds (from introspection)
});

class SalesforceCRMService implements CRM {
  private integrationName = "";
  private conn!: Promise<Connection>;
  private log: typeof logger;
  private calWarnings: string[] = [];
  private appOptions: z.infer<typeof appDataSchema>;
  private doNotCreateEvent = false;
  private fallbackToContact = false;
  private accessToken: string;
  private instanceUrl: string;
  private hasAttemptedRefresh = false;
  private credentialId: number;
  private describeCache = new Map<string, Set<string>>();

  /**
   * Escapes a string value for safe interpolation into SOQL queries.
   * Prevents SOQL injection by escaping backslashes and single quotes.
   */
  private sanitizeSoqlValue(value: string): string {
    return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
  }

  constructor(credential: CredentialPayload, appOptions: z.infer<typeof appDataSchema>, testMode = false) {
    this.integrationName = "salesforce_other_calendar";
    this.credentialId = credential.id;
    if (!testMode) {
      this.conn = this.getClient(credential).then((c) => c);
    }
    this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
    this.appOptions = appOptions;
    const credentialKey = credential.key as unknown as ExtendedTokenResponse;
    this.accessToken = credentialKey.access_token;
    this.instanceUrl = credentialKey.instance_url;
  }

  public getAppOptions() {
    return this.appOptions;
  }

  /**
   * Refreshes the Salesforce access token and optionally introspects to get/update token_lifetime.
   * @param forceIntrospection - If true, always introspect to recalibrate token_lifetime (e.g., after unexpected expiry)
   * @param existingTokenLifetime - The current token_lifetime to reuse if not forcing introspection
   */
  private refreshAccessToken = async ({
    refreshToken,
    forceIntrospection,
    existingTokenLifetime,
  }: {
    refreshToken: string;
    forceIntrospection: boolean;
    existingTokenLifetime?: number;
  }) => {
    const { consumer_key, consumer_secret } = await getSalesforceAppKeys();

    const response = await fetch("https://login.salesforce.com/services/oauth2/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: new URLSearchParams({
        grant_type: "refresh_token",
        client_id: consumer_key,
        client_secret: consumer_secret,
        refresh_token: refreshToken,
      }),
    });

    if (!response.ok) {
      const message = `${response.statusText}: ${JSON.stringify(await response.json())}`;
      throw new Error(message);
    }

    const accessTokenJson = await response.json();
    const accessTokenParsed = parseRefreshTokenResponse(accessTokenJson, salesforceTokenSchema);

    // Introspect if forced or if we don't have a token_lifetime yet
    let tokenLifetime = existingTokenLifetime;
    if (forceIntrospection || !tokenLifetime) {
      tokenLifetime = await getSalesforceTokenLifetime({
        accessToken: accessTokenParsed.access_token,
        instanceUrl: accessTokenParsed.instance_url,
      });
    }

    // Update credential in database
    const updatedKey = {
      ...accessTokenParsed,
      refresh_token: refreshToken,
      token_lifetime: tokenLifetime,
    };

    await CredentialRepository.updateWhereId({
      id: this.credentialId,
      data: { key: updatedKey },
    });

    return {
      accessToken: accessTokenParsed.access_token,
      instanceUrl: accessTokenParsed.instance_url,
      issuedAt: accessTokenParsed.issued_at,
      tokenLifetime,
    };
  };

  private getClient = async (credential: CredentialPayload) => {
    const { consumer_key, consumer_secret } = await getSalesforceAppKeys();
    const credentialKey = credential.key as unknown as ExtendedTokenResponse & { token_lifetime?: number };

    if (!credentialKey.refresh_token)
      throw new Error(`Refresh token is missing for credential ${credential.id}`);

    const refreshToken = credentialKey.refresh_token;

    // Check if token is still valid
    // issued_at is in milliseconds (string), token_lifetime is in seconds
    const BUFFER_MS = 5 * 60 * 1000; // 5 minutes buffer before expiry
    const issuedAt = parseInt(credentialKey.issued_at, 10);
    const tokenLifetimeMs = (credentialKey.token_lifetime || 0) * 1000;
    const expiryTime = issuedAt + tokenLifetimeMs;
    const isTokenValid = credentialKey.token_lifetime && Date.now() < expiryTime - BUFFER_MS;

    if (!isTokenValid) {
      try {
        const result = await this.refreshAccessToken({
          refreshToken,
          forceIntrospection: false,
          existingTokenLifetime: credentialKey.token_lifetime,
        });

        // Update instance variables and credentialKey for the connection
        this.accessToken = result.accessToken;
        this.instanceUrl = result.instanceUrl;
        credentialKey.access_token = result.accessToken;
        credentialKey.issued_at = result.issuedAt;
        credentialKey.token_lifetime = result.tokenLifetime;
      } catch (err: unknown) {
        console.error(err); // log but proceed
      }
    }

    return new jsforce.Connection({
      oauth2: {
        clientId: consumer_key,
        clientSecret: consumer_secret,
        redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
      },
      instanceUrl: credentialKey.instance_url,
      accessToken: credentialKey.access_token,
      refreshToken: credentialKey.refresh_token,
      refreshFn: async (conn, callback) => {
        // Only attempt refresh once to avoid infinite loops
        if (this.hasAttemptedRefresh) {
          return callback(new Error("Token refresh already attempted"));
        }
        this.hasAttemptedRefresh = true;

        try {
          // Force introspection to recalibrate token_lifetime after unexpected expiry
          const result = await this.refreshAccessToken({
            refreshToken,
            forceIntrospection: true,
          });

          this.accessToken = result.accessToken;
          this.instanceUrl = result.instanceUrl;

          callback(null, result.accessToken);
        } catch (err) {
          callback(err instanceof Error ? err : new Error(String(err)));
        }
      },
    });
  };

  private getSalesforceUserIdFromEmail = async (email: string) => {
    const conn = await this.conn;
    const query = await conn.query(
      `SELECT Id, Email FROM User WHERE Email = '${this.sanitizeSoqlValue(email)}' AND IsActive = true LIMIT 1`
    );
    if (query.records.length > 0) {
      return (query.records[0] as { Email: string; Id: string }).Id;
    }
  };

  private getSalesforceUserFromUserId = async (userId: string) => {
    const conn = await this.conn;

    return await conn.query(`SELECT Id, Email, Name FROM User WHERE Id = '${this.sanitizeSoqlValue(userId)}' AND IsActive = true`);
  };

  private getSalesforceEventBody = (event: CalendarEvent): string => {
    return `${event.organizer.language.translate("invitee_timezone")}: ${
      event.attendees[0].timeZone
    } \r\n\r\n ${event.organizer.language.translate("share_additional_notes")}\r\n${
      event.additionalNotes || "-"
    }`;
  };

  private salesforceCreateEventApiCall = async (
    event: CalendarEvent,
    options: { [key: string]: unknown }
  ) => {
    const conn = await this.conn;

    return await conn.sobject("Event").create({
      StartDateTime: new Date(event.startTime).toISOString(),
      EndDateTime: new Date(event.endTime).toISOString(),
      Subject: event.title,
      Description: this.getSalesforceEventBody(event),
      Location: getLocation({
        videoCallData: event.videoCallData,
        additionalInformation: event.additionalInformation,
        location: event.location,
        uid: event.uid,
      }),
      ...options,
      ...(event.recurringEvent && {
        IsRecurrence2: true,
        Recurrence2PatternText: new RRule(event.recurringEvent).toString(),
      }),
    });
  };

  private salesforceCreateEvent = async (event: CalendarEvent, contacts: Contact[]) => {
    const log = logger.getSubLogger({ prefix: [`[salesforceCreateEvent]:${event.uid}`] });

    const appOptions = this.getAppOptions();

    const writeToEventRecord = await this.generateWriteToEventBody(event);
    log.info(`Writing to event fields: ${Object.keys(writeToEventRecord)} `);

    let ownerId: string | undefined;
    if (event?.organizer?.email) {
      ownerId = await this.getSalesforceUserIdFromEmail(event.organizer.email);
    } else {
      log.warn("No organizer email found for event", event?.organizer);
    }
    log.info(`Organizer found with Salesforce id ${ownerId}`);

    /**
     * Current code assume that contacts is not empty.
     **/
    const [firstContact] = contacts;

    if (!firstContact?.id) {
      log.error("No contacts found for event", { contacts });
      throw new SFObjectToUpdateNotFoundError("No contacts found for event");
    }

    const eventWhoIds = contacts.reduce((contactIds, contact) => {
      if (contact?.id) {
        contactIds.push(contact.id);
      }

      return contactIds;
    }, [] as string[]);

    if (eventWhoIds.length !== contacts.length) {
      log.warn(`Not all contacts contain ids ${contacts}`);
    }

    const createdEvent = await this.salesforceCreateEventApiCall(event, {
      EventWhoIds: eventWhoIds,
      ...writeToEventRecord,
      ...(ownerId && { OwnerId: ownerId }),
    }).catch(async (reason) => {
      if (reason === sfApiErrors.INVALID_EVENTWHOIDS) {
        this.calWarnings.push(
          `Please enable option "Allow Users to Relate Multiple Contacts to Tasks and Events" under
           "Setup > Feature Settings > Sales > Activity Settings" to be able to create events with
           multiple contact attendees.`
        );
        // User has not configured "Allow Users to Relate Multiple Contacts to Tasks and Events"
        // proceeding to create the event using just the first attendee as the primary WhoId
        return await this.salesforceCreateEventApiCall(event, {
          WhoId: firstContact.id,
        }).catch((reason) => Promise.reject(reason));
      }
      log.error(`Error creating event: ${JSON.stringify(reason)}`);

      // Try creating a simple object without additional records
      return await this.salesforceCreateEventApiCall(event, {
        WhoId: firstContact.id,
        ...(ownerId && { OwnerId: ownerId }),
      }).catch((reason) => {
        log.error(`Error creating simple event: ${JSON.stringify(reason)}`);
        return Promise.reject(reason);
      });
    });
    // Check to see if we also need to change the record owner
    if (appOptions?.onBookingChangeRecordOwner && appOptions?.onBookingChangeRecordOwnerName) {
      if (ownerId) {
        // TODO: firstContact id is assumed to not be undefined. But current code doesn't check for it.
        await this.checkRecordOwnerNameFromRecordId(firstContact.id, ownerId);
      } else {
        log.warn(
          `Could not find owner with email ${event.organizer.email} to change record ${firstContact.id} ownership to`
        );
      }
    }
    if (appOptions?.onBookingWriteToRecord && appOptions?.onBookingWriteToRecordFields) {
      await this.writeToRecord({
        // TODO: firstContact id is assumed to not be undefined. But current code doesn't check for it.
        recordId: firstContact.id,
        fieldsToWriteTo: appOptions.onBookingWriteToRecordFields,
        startTime: event.startTime,
        organizerEmail: event.organizer?.email,
        calEventResponses: event.responses,
        bookingUid: event?.uid,
      });
    }
    return createdEvent;
  };

  private salesforceUpdateEvent = async (uid: string, event: CalendarEvent) => {
    const conn = await this.conn;
    return await conn.sobject("Event").update({
      Id: uid,
      StartDateTime: new Date(event.startTime).toISOString(),
      EndDateTime: new Date(event.endTime).toISOString(),
      Subject: event.title,
      Description: this.getSalesforceEventBody(event),
      Location: getLocation({
        videoCallData: event.videoCallData,
        additionalInformation: event.additionalInformation,
        location: event.location,
        uid: event.uid,
      }),
      ...(event.recurringEvent && {
        IsRecurrence2: true,
        Recurrence2PatternText: new RRule(event.recurringEvent).toString(),
      }),
    });
  };

  private salesforceDeleteEvent = async (uid: string, event: CalendarEvent) => {
    const appOptions = this.getAppOptions();
    const conn = await this.conn;

    if (appOptions?.onCancelWriteToEventRecord) {
      const fieldsToWriteTo = appOptions?.onCancelWriteToEventRecordFields;

      // If the option is enabled then don't delete the event record
      if (!fieldsToWriteTo || !Object.keys(fieldsToWriteTo)) {
        return Promise.resolve();
      }

      return await this.writeToRecord({
        recordId: uid,
        fieldsToWriteTo,
        startTime: event.startTime,
        organizerEmail: event.organizer?.email,
        calEventResponses: event.responses,
        bookingUid: event?.uid,
      });
    }
    return await conn.sobject("Event").delete(uid);
  };

  async handleEventCreation(event: CalendarEvent, contacts: Contact[]) {
    const sfEvent = await this.salesforceCreateEvent(event, contacts);
    if (sfEvent.success) {
      this.log.info("event:creation:ok", { sfEvent });
      return {
        uid: sfEvent.id,
        id: sfEvent.id,
        type: "salesforce_other_calendar",
        password: "",
        url: "",
        additionalInfo: { contacts, sfEvent, calWarnings: this.calWarnings },
      };
    }
    this.log.info("event:creation:notOk", { event, sfEvent, contacts });
    return Promise.reject({
      calError: "Something went wrong when creating an event in Salesforce",
    });
  }

  async createEvent(event: CalendarEvent, contacts: Contact[]): Promise<CrmEvent | undefined> {
    const skipEventCreation = this.getDoNotCreateEvent();
    if (skipEventCreation) return undefined;

    const sfEvent = await this.salesforceCreateEvent(event, contacts);
    if (sfEvent.success) {
      return {
        uid: sfEvent.id,
        id: sfEvent.id,
        type: "salesforce_other_calendar",
        password: "",
        url: "",
        additionalInfo: { contacts, sfEvent, calWarnings: this.calWarnings },
      };
    }
    this.log.debug("event:creation:notOk", { event, sfEvent, contacts });
    return Promise.reject("Something went wrong when creating an event in Salesforce");
  }

  async updateEvent(uid: string, event: CalendarEvent): Promise<CrmEvent> {
    const updatedEvent = await this.salesforceUpdateEvent(uid, event);
    if (updatedEvent.success) {
      return {
        uid: updatedEvent.id,
        id: updatedEvent.id,
        type: "salesforce_other_calendar",
        password: "",
        url: "",
        additionalInfo: { calWarnings: this.calWarnings },
      };
    } else {
      return Promise.reject({ calError: "Something went wrong when updating the event in Salesforce" });
    }
  }

  public async deleteEvent(uid: string, event: CalendarEvent) {
    await this.salesforceDeleteEvent(uid, event)
      .then(() => {
        return Promise.resolve();
      })
      .catch((error) => {
        this.log.error(`Error canceling event ${uid} with error ${error}`);
        return Promise.reject({ calError: "Something went wrong when deleting the event in Salesforce" });
      });
  }

  async getContacts({
    emails,
    includeOwner,
    forRoundRobinSkip,
  }: {
    emails: string | string[];
    includeOwner?: boolean;
    forRoundRobinSkip?: boolean;
  }) {
    const log = logger.getSubLogger({ prefix: [`[getContacts]:${emails}`] });

    try {
      const conn = await this.conn;
      const emailArray = Array.isArray(emails) ? emails : [emails];

      const appOptions = this.getAppOptions();
      const recordToSearch =
        (forRoundRobinSkip ? appOptions?.roundRobinSkipCheckRecordOn : appOptions?.createEventOn) ??
        SalesforceRecordEnum.CONTACT;

      let soql: string;

      log.info(
        "Getting contacts for emails",
        safeStringify({
          emailArray,
          includeOwner,
          forRoundRobinSkip,
          recordToSearch,
          appOptions,
        })
      );

      // Validate field rules early, before branching to GraphQL or SOQL paths
      let validatedFieldRules: RRSkipFieldRule[] | undefined;
      if (forRoundRobinSkip && appOptions?.rrSkipFieldRules?.length) {
        log.info(
          "Validating field rules",
          safeStringify({
            rules: appOptions.rrSkipFieldRules,
            recordToSearch,
          })
        );
        const existingFieldNames = await this.getObjectFieldNames(recordToSearch);
        log.info(`Found ${existingFieldNames.size} fields on ${recordToSearch}`);
        if (existingFieldNames.size > 0) {
          const filtered = appOptions.rrSkipFieldRules.filter((r) => existingFieldNames.has(r.field));
          if (filtered.length > 0) {
            validatedFieldRules = filtered;
            log.info("Validated field rules", safeStringify({ validatedFieldRules }));
          } else {
            log.warn(
              "No field rules matched existing fields",
              safeStringify({
                configuredFields: appOptions.rrSkipFieldRules.map((r) => r.field),
              })
            );
          }
        }
      }

      if (recordToSearch === SalesforceRecordEnum.ACCOUNT && forRoundRobinSkip) {
        try {
          const client = new SalesforceGraphQLClient({
            accessToken: this.accessToken,
            instanceUrl: this.instanceUrl,
          });
          return await client.GetAccountRecordsForRRSkip(emailArray[0], validatedFieldRules);
        } catch (error) {
          log.error("Error getting account records for round robin skip", safeStringify({ error }));
          return [];
        }
      }

      let records: ContactRecord[] = [];

      // This combination is for searching for ownership via contacts
      if (
        recordToSearch === SalesforceRecordEnum.CONTACT &&
        appOptions?.roundRobinSkipFallbackToLeadOwner &&
        forRoundRobinSkip
      ) {
        const record = await this.getContactOrLeadFromEmail({ email: emailArray[0], conn });
        if (record) {
          records.push(record);
        }
      }

      // If falling back to contacts, check for the contact before returning the leads or empty array
      if (
        appOptions.createEventOn === SalesforceRecordEnum.LEAD &&
        appOptions.createEventOnLeadCheckForContact &&
        !forRoundRobinSkip
      ) {
        const record = await this.getContactOrLeadFromEmail({
          email: emailArray[0],
          setFallbackToContact: true,
          conn,
        });
        if (record) {
          records.push(record);
        }
      }

      if (records.length === 0) {
        // Build extra SELECT fields from validated field rules so we don't need a second query
        const extraFields = validatedFieldRules?.length
          ? ", " + validatedFieldRules.map((r) => r.field).join(", ")
          : "";

        // Handle Account record type
        if (recordToSearch === SalesforceRecordEnum.ACCOUNT) {
          // For an account let's assume that the first email is the one we should be querying against
          const attendeeEmail = emailArray[0];
          log.info("[recordToSearch=ACCOUNT] Searching contact for email", safeStringify({ attendeeEmail }));
          soql = `SELECT Id, Email, OwnerId, AccountId, Account.OwnerId, Account.Owner.Email, Account.Website${extraFields} FROM ${SalesforceRecordEnum.CONTACT} WHERE Email = '${this.sanitizeSoqlValue(attendeeEmail)}' AND AccountId != null`;
        } else {
          // Handle Contact/Lead record types
          soql = `SELECT Id, Email, OwnerId, Owner.Email${extraFields} FROM ${recordToSearch} WHERE Email IN ('${emailArray.map((e) => this.sanitizeSoqlValue(e)).join(
            "','"
          )}')`;
        }

        const results = await conn.query(soql);

        log.info("Query results", safeStringify({ recordCount: results.records?.length }));

        if (results.records.length === 0) {
          log.info("No records found");
          return [];
        }

        if (!records.length && results?.records?.length) {
          records = results.records as ContactRecord[];
        }
      }

      // Apply field rules if configured and this is for round robin skip
      if (forRoundRobinSkip && validatedFieldRules?.length && records.length > 0) {
        records = this.applyFieldRules(records, validatedFieldRules);
        if (records.length === 0) {
          log.info("All records filtered out by field rules");
          return [];
        }
      }

      const isFreeEmailDomain = await this.shouldSkipAttendeeIfFreeEmailDomain(emailArray[0]);
      const includeOwnerData = (includeOwner || forRoundRobinSkip) && !isFreeEmailDomain;

      const includeAccountRecordType = forRoundRobinSkip && recordToSearch === SalesforceRecordEnum.ACCOUNT;
      return records.map((record) => {
        // Handle if Account is nested
        const ownerEmail =
          recordToSearch === SalesforceRecordEnum.ACCOUNT &&
          record?.attributes?.type !== SalesforceRecordEnum.ACCOUNT
            ? record?.Account?.Owner?.Email
            : record?.Owner?.Email;

        return {
          id: includeAccountRecordType ? record?.AccountId || "" : record?.Id || "",
          email: record?.Email || "",
          recordType: includeAccountRecordType ? SalesforceRecordEnum.ACCOUNT : record?.attributes?.type,
          ...(includeOwnerData && {
            ownerId: record?.OwnerId,
            ownerEmail: ownerEmail,
          }),
        };
      });
    } catch (error) {
      log.error("Error in getContacts", safeStringify(error));
      return [];
    }
  }

  private async getContactOrLeadFromEmail({
    email,
    setFallbackToContact = false,
    conn,
  }: {
    email: string;
    setFallbackToContact?: boolean;
    conn: Connection;
  }) {
    // Escape SOSL reserved characters: ? & | ! { } [ ] ( ) ^ ~ * : \ " ' + -
    // eslint-disable-next-line no-useless-escape
    const escapedEmail = email.replace(/([?&|!{}[\]()^~*:\\"'+-])/g, "\\$1");
    const searchResult = await conn.search(
      `FIND {${escapedEmail}} IN EMAIL FIELDS RETURNING Lead(Id, Email, OwnerId, Owner.Email), Contact(Id, Email, OwnerId, Owner.Email)`
    );

    if (searchResult.searchRecords.length === 0) {
      return null;
    }

    // See if a contact was found first
    const contactQuery = searchResult.searchRecords.filter(
      (record) => record.attributes?.type === SalesforceRecordEnum.CONTACT
    );

    if (contactQuery.length > 0) {
      this.setFallbackToContact(setFallbackToContact);
      return contactQuery[0] as ContactRecord;
    } else {
      // If not fallback to lead
      const leadQuery = searchResult.searchRecords.filter(
        (record) => record.attributes?.type === SalesforceRecordEnum.LEAD
      );
      if (leadQuery.length > 0) {
        return leadQuery[0] as ContactRecord;
      }
    }

    return null;
  }

  async createContacts(
    contactsToCreate: Attendee[],
    organizerEmail?: string,
    calEventResponses?: CalEventResponses | null
  ) {
    const log = logger.getSubLogger({ prefix: [`[createContacts]`] });
    const conn = await this.conn;
    const appOptions = this.getAppOptions();
    const createEventOn = appOptions.createEventOn ?? SalesforceRecordEnum.CONTACT;
    // See if the organizer exists in the CRM
    const organizerId = organizerEmail ? await this.getSalesforceUserIdFromEmail(organizerEmail) : undefined;
    const createdContacts: { id: string; email: string }[] = [];

    log.info("createContacts", safeStringify({ createEventOn, organizerId, contactsToCreate }));

    if (createEventOn === SalesforceRecordEnum.CONTACT) {
      await Promise.all(
        contactsToCreate.map(async (attendee) => {
          return await this.createAttendeeRecord({
            attendee,
            recordType: SalesforceRecordEnum.CONTACT,
            organizerId,
          }).then((result) => {
            createdContacts.push(...result);
          });
        })
      );
    }

    if (!contactsToCreate[0]?.email) {
      this.log.warn(`createContact: no attendee email found `, contactsToCreate);
      return [];
    }

    if (createEventOn === SalesforceRecordEnum.LEAD) {
      // Base this off of the first contact
      const attendee = contactsToCreate[0];

      if (appOptions.createNewContactUnderAccount) {
        // Check for an account
        const accountId = await this.getAccountIdBasedOnEmailDomainOfContacts(attendee.email);

        if (accountId) {
          const createdAccountContacts = await this.createNewContactUnderAnAccount({
            attendee,
            accountId,
            organizerId,
          });
          if (createdAccountContacts.length > 0) {
            createdContacts.push(...createdAccountContacts);
          }
        } else {
          await this.createAttendeeRecord({
            attendee,
            recordType: SalesforceRecordEnum.LEAD,
            organizerId,
            calEventResponses,
          }).then((result) => {
            createdContacts.push(...result);
          });
        }
      } else {
        await this.createAttendeeRecord({
          attendee,
          recordType: SalesforceRecordEnum.LEAD,
          organizerId,
          calEventResponses,
        }).then((result) => {
          createdContacts.push(...result);
        });
      }
    }

    if (createEventOn === SalesforceRecordEnum.ACCOUNT) {
      if (!appOptions.createNewContactUnderAccount && !appOptions.createLeadIfAccountNull) {
        this.setDoNotCreateEvent(true);
        return [{ id: "Do not create event", email: "placeholder" }];
      }

      // Base this off of the first contact
      const attendee = contactsToCreate[0];

      const accountId = await this.getAccountIdBasedOnEmailDomainOfContacts(attendee.email);
      let contactCreated = false;

      if (accountId && appOptions.createNewContactUnderAccount) {
        const createdAccountContacts = await this.createNewContactUnderAnAccount({
          attendee,
          accountId,
          organizerId,
        });
        if (createdAccountContacts.length > 0) {
          createdContacts.push(...createdAccountContacts);
          contactCreated = true;
        }
      }

      if (!accountId && appOptions.createLeadIfAccountNull && !contactCreated) {
        // Check to see if the lead exists already
        const leadQuery = await conn.query(
          `SELECT Id, Email FROM Lead WHERE Email = '${this.sanitizeSoqlValue(attendee.email)}' LIMIT 1`
        );
        if (leadQuery.records.length > 0) {
          const contact = leadQuery.records[0] as { Id: string; Email: string };
          return [{ id: contact.Id, email: contact.Email }];
        }

        for (const attendee of contactsToCreate) {
          try {
            const createBody = await this.generateCreateRecordBody({
              attendee,
              recordType: SalesforceRecordEnum.LEAD,
              organizerId,
              calEventResponses,
            });

            const result = await conn.sobject(SalesforceRecordEnum.LEAD).create(createBody);
            if (result.success) {
              createdContacts.push({ id: result.id, email: attendee.email });
            }
          } catch (error: unknown) {
            if (error instanceof Error && error.name === "DUPLICATES_DETECTED") {
              // we know it's a DuplicateError now (DUPLICATES_DETECTED)
              const existingId = this.getExistingIdFromDuplicateError(error as SalesforceDuplicateError);
              if (existingId) {
                log.info("Using existing record:", existingId);
                createdContacts.push({ id: existingId, email: attendee.email });
              }
            } else {
              log.error("Error creating lead:", error);
            }
          }
        }
      }
    }

    if (createdContacts.length === 0) {
      // This should never happen
      log.error(`No contacts created for these app options ${safeStringify(appOptions)}`);
    }
    return createdContacts;
  }

  async handleAttendeeNoShow(bookingUid: string, attendees: { email: string; noShow: boolean }[]) {
    const appOptions = this.getAppOptions();
    const { sendNoShowAttendeeData = false, sendNoShowAttendeeDataField = {} } = appOptions;
    const conn = await this.conn;
    // Check that no show is enabled
    if (!sendNoShowAttendeeData && !sendNoShowAttendeeDataField) {
      this.log.warn(`No show settings not set for bookingUid ${bookingUid}`);
      return;
    }
    // Get all Salesforce events associated with the booking
    const salesforceEvents = await prisma.bookingReference.findMany({
      where: {
        type: appMeta.type,
        booking: {
          uid: bookingUid,
        },
        deleted: null,
      },
    });

    const salesforceEntity = await conn.describe("Event");
    const fields = salesforceEntity.fields;
    const noShowField = fields.find((field) => field.name === sendNoShowAttendeeDataField);

    if (!noShowField || (noShowField.type as unknown as string) !== "boolean") {
      this.log.warn(
        `No show field on Salesforce doesn't exist or is not of type boolean for bookingUid ${bookingUid}`
      );
      return;
    }

    for (const event of salesforceEvents) {
      const salesforceEvent = (await conn.query(`SELECT WhoId FROM Event WHERE Id = '${this.sanitizeSoqlValue(event.uid)}'`)) as {
        records: { WhoId: string }[];
      };

      let salesforceAttendeeEmail: string | undefined;
      // Figure out if the attendee is a contact or lead
      const contactQuery = (await conn.query(
        `SELECT Email FROM Contact WHERE Id = '${this.sanitizeSoqlValue(salesforceEvent.records[0].WhoId)}'`
      )) as { records: { Email: string }[] };
      const leadQuery = (await conn.query(
        `SELECT Email FROM Lead WHERE Id = '${this.sanitizeSoqlValue(salesforceEvent.records[0].WhoId)}'`
      )) as { records: { Email: string }[] };

      // Prioritize contacts over leads
      if (contactQuery.records.length > 0) {
        salesforceAttendeeEmail = contactQuery.records[0].Email;
      } else if (leadQuery.records.length > 0) {
        salesforceAttendeeEmail = leadQuery.records[0].Email;
      } else {
        this.log.warn(
          `Could not find attendee for bookingUid ${bookingUid} and salesforce event id ${event.uid}`
        );
      }

      if (salesforceAttendeeEmail) {
        // Find the attendee no show data
        const noShowData = attendees.find((attendee) => attendee.email === salesforceAttendeeEmail);

        if (!noShowData) {
          this.log.warn(
            `No show data could not be found for ${salesforceAttendeeEmail} and bookingUid ${bookingUid}`
          );
        } else {
          // Update the event with the no show data
          await conn.sobject("Event").update({
            Id: event.uid,
            [sendNoShowAttendeeDataField as string]: noShowData.noShow,
          });
        }
      }
    }
  }

  private getExistingIdFromDuplicateError(error: SalesforceDuplicateError): string | null {
    if (error.duplicateResult && error.duplicateResult.matchResults) {
      for (const matchResult of error.duplicateResult.matchResults) {
        if (matchResult.matchRecords && matchResult.matchRecords.length > 0) {
          return matchResult.matchRecords[0].record.Id;
        }
      }
    }
    return null;
  }

  private setDoNotCreateEvent(boolean: boolean) {
    this.doNotCreateEvent = boolean;
  }

  private getDoNotCreateEvent() {
    return this.doNotCreateEvent;
  }

  private getDominantAccountId(contacts: GetDominantAccountIdInput) {
    return getDominantAccountId(contacts);
  }

  private async createAttendeeRecord({
    attendee,
    recordType,
    organizerId,
    accountId,
    calEventResponses,
  }: {
    attendee: Attendee;
    recordType: SalesforceRecordEnum;
    organizerId?: string;
    accountId?: string;
    calEventResponses?: CalEventResponses | null;
  }) {
    const log = logger.getSubLogger({ prefix: [`[createAttendeeRecord]:${attendee.email}`] });
    log.info("createAttendeeRecord", safeStringify({ attendee, recordType, organizerId, accountId }));
    const conn = await this.conn;

    const createBody = await this.generateCreateRecordBody({
      attendee,
      recordType: recordType,
      organizerId,
      calEventResponses,
    });

    return await conn
      .sobject(recordType)
      .create({
        ...createBody,
        AccountId: accountId,
      })
      .then((result) => {
        if (result.success) {
          return [{ id: result.id, email: attendee.email }];
        } else {
          return [];
        }
      })
      .catch((error) => {
        this.log.error(`Error creating Salesforce contact for ${attendee.email} with error ${error}`);
        return [];
      });
  }

  private async generateCreateRecordBody({
    attendee,
    recordType,
    organizerId,
    calEventResponses,
  }: {
    attendee: { email: string; name: string };
    recordType: SalesforceRecordEnum;
    organizerId?: string;
    /**Only Leads have the default company field */
    calEventResponses?: CalEventResponses | null;
  }) {
    const [FirstName, LastName] = attendee.name ? attendee.name.split(" ") : [attendee.email, ""];

    // Assume that the first part of the email domain is the company title
    const company =
      (await this.getCompanyNameFromBookingResponse(calEventResponses)) ??
      attendee.email.split("@")[1].split(".")[0];
    return {
      LastName: LastName || "-",
      FirstName,
      Email: attendee.email,
      ...(organizerId && { OwnerId: organizerId }),
      ...(recordType === SalesforceRecordEnum.LEAD && { Company: company }),
    };
  }

  /**
   * Gets all field names for a Salesforce object.
   * Uses a two-level cache: in-memory (per request) and Redis (cross-request, 1hr TTL).
   */
  private async getObjectFieldNames(sobject: string): Promise<Set<string>> {
    const log = logger.getSubLogger({ prefix: [`[getObjectFieldNames]`] });

    // Level 1: in-memory cache (same request)
    const memoryCached = this.describeCache.get(sobject);
    if (memoryCached) {
      return memoryCached;
    }

    // Level 2: Redis cache (cross-request)
    const cacheKey = `salesforce:describe:${this.instanceUrl}:${sobject}`;
    const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour

    try {
      const redis = getRedisService();
      const redisCached = await redis.get<string[]>(cacheKey);
      if (redisCached) {
        log.info(`Redis cache hit for ${sobject} field names`);
        const fieldNames = new Set(redisCached);
        this.describeCache.set(sobject, fieldNames);
        return fieldNames;
      }
    } catch (cacheError) {
      log.warn(`Redis cache unavailable, falling back to API`, safeStringify({ error: cacheError }));
    }

    // Level 3: Salesforce API
    const conn = await this.conn;

    try {
      const salesforceEntity = await conn.describe(sobject);
      const fieldNamesList = salesforceEntity.fields.map((f) => f.name);
      const fieldNames = new Set(fieldNamesList);
      this.describeCache.set(sobject, fieldNames);

      // Persist to Redis for other requests
      try {
        const redis = getRedisService();
        await redis.set(cacheKey, fieldNamesList, { ttl: CACHE_TTL_MS });
        log.info(`Cached ${sobject} field names in Redis (${fieldNamesList.length} fields)`);
      } catch (cacheError) {
        log.warn(`Failed to cache field names in Redis`, safeStringify({ error: cacheError }));
      }

      return fieldNames;
    } catch (e) {
      log.error(`Error fetching field names for ${sobject}`, safeStringify({ error: e }));
      return new Set();
    }
  }

  private async ensureFieldsExistOnObject(fieldsToTest: string[], sobject: string): Promise<Field[]> {
    const log = logger.getSubLogger({ prefix: [`[ensureFieldsExistOnObject]`] });
    const conn = await this.conn;

    const fieldSet = new Set(fieldsToTest);
    const foundFields: Field[] = [];

    try {
      const salesforceEntity = await conn.describe(sobject);
      const fields = salesforceEntity.fields;

      for (const field of fields) {
        if (foundFields.length === fieldSet.size) break;

        if (fieldSet.has(field.name)) {
          foundFields.push(field);
        }
      }

      return foundFields;
    } catch (e) {
      log.error(`Error ensuring fields ${fieldsToTest} exist on object ${sobject} with error ${e}`);
      return [];
    }
  }

  /**
   * Filters records in-memory based on pre-validated field rules.
   * Field rule values are expected to already be present on the records
   * (included in the original SOQL SELECT).
   * Records without the field value (e.g. from SOSL path) are kept.
   *
   * @param records - The records to filter (must already contain field rule field values)
   * @param validRules - Array of pre-validated rules (fields already confirmed to exist on the object)
   * @returns Filtered records that pass all rules (AND logic)
   */
  private applyFieldRules(records: ContactRecord[], validRules: RRSkipFieldRule[]): ContactRecord[] {
    const log = logger.getSubLogger({ prefix: [`[applyFieldRules]`] });

    if (!validRules.length || !records.length) {
      return records;
    }

    log.info("Applying field rules", {
      ruleCount: validRules.length,
      recordCount: records.length,
    });

    return records.filter((record) => {
      for (const rule of validRules) {
        // Access dynamic field via bracket notation — value is on the record from SOQL
        const rawRecord = record as unknown as Record<string, unknown>;
        const actualValue = String(rawRecord[rule.field] ?? "").toLowerCase();
        const ruleValue = rule.value.toLowerCase();
        const matches = actualValue === ruleValue;

        // If field value is missing (empty string from nullish coalescing), skip the rule.
        // This handles records from SOSL path that don't have field rule fields.
        if (rawRecord[rule.field] === undefined || rawRecord[rule.field] === null) {
          log.info(`Record ${record.Id} missing field "${rule.field}", skipping rule`);
          continue;
        }

        if (rule.action === RRSkipFieldRuleActionEnum.IGNORE && matches) {
          log.info(`Record ${record.Id} filtered out by ignore rule`, {
            field: rule.field,
            value: rule.value,
          });
          return false;
        }

        if (rule.action === RRSkipFieldRuleActionEnum.MUST_INCLUDE && !matches) {
          log.info(`Record ${record.Id} filtered out by must_include rule`, {
            field: rule.field,
            expectedValue: rule.value,
            actualValue,
          });
          return false;
        }
      }

      return true;
    });
  }

  private async checkRecordOwnerNameFromRecordId(id: string, newOwnerId: string) {
    const conn = await this.conn;
    const appOptions = this.getAppOptions();

    this.log.info(`Changing owner for record ${id}`);

    const recordType = this.determineRecordTypeById(id);

    if (!appOptions?.onBookingChangeRecordOwnerName) {
      this.log.warn(
        `No appOptions.onBookingChangeRecordOwnerName found for ${this.integrationName} on checkRecordOwnerNameFromRecordId`
      );
      return;
    }

    // Get the associated record that the event was created on
    const recordQuery = (await conn.query(
      `SELECT OwnerId, Owner.Name FROM ${recordType} WHERE Id = '${this.sanitizeSoqlValue(id)}'`
    )) as { records: { OwnerId: string; Owner: { Name: string } }[] };

    if (!recordQuery || !recordQuery.records.length) {
      this.log.warn(`Could not find record for id ${id} and type ${recordType}`);
    }

    const owner = recordQuery.records[0].Owner;

    if (!appOptions.onBookingChangeRecordOwnerName.includes(owner?.Name)) {
      this.log.warn(
        `Current owner name ${owner?.Name} for record ${id} does not match the option ${appOptions.onBookingChangeRecordOwnerName}`
      );
    }

    await conn
      .sobject(recordType)
      .update({
        // First field is there WHERE statement
        Id: id,
        OwnerId: newOwnerId,
      })
      .catch((error) => {
        this.log.warn(
          `Error changing record ${id} of type ${recordType} owner to ${newOwnerId} with error ${JSON.stringify(
            error
          )}`
        );
      });
  }

  public getAllPossibleAccountWebsiteFromEmailDomain(emailDomain: string) {
    const websites = getAllPossibleWebsiteValuesFromEmailDomain(emailDomain);
    // Format for SOQL query
    return websites.map((website) => `'${this.sanitizeSoqlValue(website)}'`).join(", ");
  }

  private async getAccountIdBasedOnEmailDomainOfContacts(email: string) {
    const conn = await this.conn;
    const emailDomain = email.split("@")[1];
    const log = logger.getSubLogger({ prefix: [`[getAccountIdBasedOnEmailDomainOfContacts]:${email}`] });
    log.info("getAccountIdBasedOnEmailDomainOfContacts", safeStringify({ email, emailDomain }));

    // First check if an account has the same website as the email domain of the attendee
    const accountQuery = await conn.query(
      `SELECT Id, Website FROM Account WHERE Website IN (${this.getAllPossibleAccountWebsiteFromEmailDomain(
        emailDomain
      )}) LIMIT 1`
    );
    if (accountQuery.records.length > 0) {
      const account = accountQuery.records[0] as { Id: string; Website: string };
      log.info(
        "Found account based on email domain",
        safeStringify({ emailDomain, accountWebsite: account.Website, accountId: account.Id })
      );
      return account.Id;
    }

    // Fallback to querying which account the majority of contacts are under
    const response = await conn.query(
      `SELECT Id, Email, AccountId FROM Contact WHERE Email LIKE '%@${this.sanitizeSoqlValue(emailDomain)}' AND AccountId != null`
    );

    const accountId = this.getDominantAccountId(response.records as { AccountId: string }[]);

    if (accountId) {
      log.info("Found account based on other contacts", safeStringify({ accountId }));
    } else {
      log.info("No account found");
    }

    return accountId;
  }

  private setFallbackToContact(boolean: boolean) {
    this.fallbackToContact = boolean;
  }

  private getFallbackToContact() {
    return this.fallbackToContact;
  }

  private async writeToRecord({
    recordId,
    startTime,
    fieldsToWriteTo,
    organizerEmail,
    calEventResponses,
    bookingUid,
  }: {
    recordId: string;
    startTime: string;
    fieldsToWriteTo: Record<string, z.infer<typeof writeToBookingEntry>>;
    organizerEmail?: string;
    calEventResponses?: CalEventResponses | null;
    bookingUid?: string | null;
  }) {
    const conn = await this.conn;
    // Determine record type (Contact, Lead, etc)
    const personRecordType = this.determineRecordTypeById(recordId);
    // Search the fields and ensure 1. they exist 2. they're the right type
    const fieldsToWriteOn = Object.keys(fieldsToWriteTo);
    const existingFields = await this.ensureFieldsExistOnObject(fieldsToWriteOn, personRecordType);
    if (!existingFields.length) {
      this.log.warn(`No fields found for record type ${personRecordType}`);
      return;
    }

    const personRecord = await this.fetchPersonRecord(recordId, existingFields, personRecordType);
    if (!personRecord) {
      this.log.warn(`No personRecord found for contactId ${recordId}`);
      return;
    }

    this.log.info(`Writing to recordId ${recordId} on fields ${fieldsToWriteOn}`);

    const writeOnRecordBody = await this.buildRecordUpdatePayload({
      existingFields,
      personRecord,
      fieldsToWriteTo,
      startTime,
      bookingUid,
      organizerEmail,
      calEventResponses,
      recordId,
    });

    this.log.info(
      `Final writeOnRecordBody contains fields ${Object.keys(writeOnRecordBody)} for record ${recordId}`
    );

    // Update the person record
    await conn
      .sobject(personRecordType)
      .update({
        Id: recordId,
        ...writeOnRecordBody,
      })
      .catch((e) => {
        this.log.error(`Error updating person record for contactId ${recordId}`, e);
      });
  }

  private async buildRecordUpdatePayload({
    existingFields,
    personRecord,
    fieldsToWriteTo,
    startTime,
    bookingUid,
    organizerEmail,
    calEventResponses,
    recordId,
  }: {
    existingFields: Field[];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    personRecord: Record<string, any>;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    fieldsToWriteTo: Record<string, any>;
    startTime?: string;
    bookingUid?: string | null;
    organizerEmail?: string;
    calEventResponses?: CalEventResponses | null;
    recordId: string;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  }): Promise<Record<string, any>> {
    const log = logger.getSubLogger({ prefix: [`[buildRecordUpdatePayload] ${recordId}`] });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const writeOnRecordBody: Record<string, any> = {};
    let fieldTypeHandled = false;

    for (const field of existingFields) {
      const fieldConfig = fieldsToWriteTo[field.name];

      if (!fieldConfig) {
        log.error(`No field config found for field ${field.name}`);
        continue;
      }

      log.info(
        `Processing field ${field.name} with type ${field.type} and config ${JSON.stringify(fieldConfig)}`
      );

      // Skip if field should only be written when empty and already has a value
      if (fieldConfig.whenToWrite === WhenToWriteToRecord.FIELD_EMPTY && personRecord[field.name]) {
        log.info(
          `Field ${field.name} on contactId ${personRecord?.Id} already exists with value ${
            personRecord[field.name]
          }`
        );
        continue;
      }

      if (fieldConfig.fieldType === SalesforceFieldType.CUSTOM) {
        fieldTypeHandled = true;
        const extractedValue = await this.getTextFieldValue({
          fieldValue: fieldConfig.value,
          fieldLength: field.length,
          calEventResponses,
          bookingUid,
          recordId,
          fieldName: field.name,
        });
        if (extractedValue) {
          writeOnRecordBody[field.name] = extractedValue;
          continue;
        }
      }

      // Handle different field types
      if (
        field.type === SalesforceFieldType.TEXT ||
        field.type === SalesforceFieldType.TEXTAREA ||
        field.type === SalesforceFieldType.PHONE
      ) {
        fieldTypeHandled = true;
        const extractedText = await this.getTextFieldValue({
          fieldValue: fieldConfig.value,
          fieldLength: field.length,
          calEventResponses,
          bookingUid,
          recordId,
          fieldName: field.name,
        });
        if (extractedText) {
          writeOnRecordBody[field.name] = extractedText;
          continue;
        }
      } else if (
        (field.type === SalesforceFieldType.DATE || field.type === SalesforceFieldType.DATETIME) &&
        startTime &&
        organizerEmail
      ) {
        fieldTypeHandled = true;
        const dateValue = await this.getDateFieldValue(
          fieldConfig.value,
          startTime,
          bookingUid,
          organizerEmail
        );
        if (dateValue) {
          writeOnRecordBody[field.name] = dateValue;
          continue;
        }
      } else if (field.type === SalesforceFieldType.PICKLIST) {
        fieldTypeHandled = true;
        const picklistValue = await this.getPicklistFieldValue({
          fieldConfigValue: fieldConfig.value,
          salesforceField: field,
          calEventResponses,
          bookingUid,
          recordId,
        });
        if (picklistValue) {
          writeOnRecordBody[field.name] = picklistValue;
          continue;
        }
      } else if (field.type === SalesforceFieldType.CHECKBOX) {
        fieldTypeHandled = true;
        // If the checkbox field value is not a boolean for some reason, default to if it's a falsely value
        const checkboxValue = !!fieldConfig.value;
        writeOnRecordBody[field.name] = checkboxValue;
        continue;
      }

      if (!fieldTypeHandled) {
        log.error(`Salesforce field type ${field.type} not handled for fieldConfig ${fieldConfig}`);
      }
      log.error(
        `No value found for field ${field.name} with value ${
          personRecord[field.name]
        }, field config ${JSON.stringify(fieldConfig)} and Salesforce config ${JSON.stringify(field)}`
      );
    }
    return writeOnRecordBody;
  }

  private async generateWriteToEventBody(event: CalendarEvent) {
    const appOptions = this.getAppOptions();

    const customFieldInputsEnabled =
      appOptions?.onBookingWriteToEventObject && appOptions?.onBookingWriteToEventObjectMap;

    if (!customFieldInputsEnabled) return {};

    if (!appOptions?.onBookingWriteToEventObjectMap) return {};

    const customFieldInputs = customFieldInputsEnabled
      ? await this.ensureFieldsExistOnObject(Object.keys(appOptions.onBookingWriteToEventObjectMap), "Event")
      : [];

    const confirmedCustomFieldInputs: {
      // This is unique to each instance so we don't know the type
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      [key: string]: any;
    } = {};

    for (const field of customFieldInputs) {
      confirmedCustomFieldInputs[field.name] = await this.getTextFieldValue({
        fieldValue: appOptions.onBookingWriteToEventObjectMap[field.name],
        fieldLength: field.length,
        calEventResponses: event.responses,
        bookingUid: event?.uid,
        recordId: event?.uid ?? "Cal booking",
        fieldName: field.name,
      });
    }

    return confirmedCustomFieldInputs;
  }

  private async getTextFieldValue({
    fieldValue,
    fieldLength,
    calEventResponses,
    bookingUid,
    recordId,
    fieldName,
  }: {
    fieldValue: string;
    fieldLength?: number;
    calEventResponses?: CalEventResponses | null;
    bookingUid?: string | null;
    recordId: string;
    fieldName: string;
  }) {
    const log = logger.getSubLogger({ prefix: [`[getTextFieldValue]: ${recordId} - ${fieldName}`] });

    // If no {} then indicates we're passing a static value
    if (!fieldValue.startsWith("{") && !fieldValue.endsWith("}")) {
      log.info("Returning static value");
      return fieldValue;
    }

    let valueToWrite = fieldValue;
    if (fieldValue.startsWith("{utm:")) {
      if (!bookingUid) {
        log.error(`BookingUid not passed. Cannot get tracking values without it`);
        return;
      }
      valueToWrite = await this.getTextValueFromBookingTracking(fieldValue, bookingUid);
    } else if (fieldValue === "{assignmentReason}") {
      if (!bookingUid) {
        log.error(`BookingUid not passed. Cannot get assignment reason without it`);
        return;
      }
      valueToWrite = await this.getAssignmentReason(bookingUid);
      if (!valueToWrite) {
        log.error(`No assignment reason found for bookingUid ${bookingUid}`);
        return;
      }
    } else {
      // Get the value from the booking response
      if (!calEventResponses) {
        log.error(`CalEventResponses not passed. Cannot get booking form responses`);
        return;
      }
      valueToWrite = this.getTextValueFromBookingResponse(fieldValue, calEventResponses);
    }

    // If a value wasn't found in the responses. Don't return the field name
    if (valueToWrite === fieldValue) {
      log.error("No responses found returning nothing");
      return;
    }

    // Trim incase the replacement values increased the length
    return fieldLength ? valueToWrite.substring(0, fieldLength) : valueToWrite;
  }

  private async getTextValueFromBookingTracking(fieldValue: string, bookingUid: string) {
    const log = logger.getSubLogger({
      prefix: [`[getTextValueFromBookingTracking]: ${bookingUid}`],
    });
    const tracking = await prisma.tracking.findFirst({
      where: {
        booking: {
          uid: bookingUid,
        },
      },
    });
    if (!tracking) {
      log.warn(`No tracking found for bookingUid ${bookingUid}`);
      return "";
    }

    // Remove the {utm: and trailing } from the field value
    const utmParam = fieldValue.split(":")[1].slice(0, -1);
    return tracking[`utm_${utmParam}` as keyof typeof tracking]?.toString() ?? "";
  }

  private async getAssignmentReason(bookingId: string) {
    const assignmentReason = await PrismaAssignmentReasonRepository.findLatestReasonFromBookingUid(bookingId);
    return assignmentReason?.reasonString ?? "";
  }

  private getTextValueFromBookingResponse(fieldValue: string, calEventResponses: CalEventResponses) {
    const regexValueToReplace = /\{(.*?)\}/g;
    return fieldValue.replace(regexValueToReplace, (match, captured) => {
      return calEventResponses[captured]?.value ? calEventResponses[captured].value.toString() : match;
    });
  }

  private async getDateFieldValue(
    fieldValue: string,
    startTime: string,
    bookingUid?: string | null,
    organizerEmail?: string
  ): Promise<string | null> {
    if (fieldValue === DateFieldTypeData.BOOKING_START_DATE) {
      return new Date(startTime).toISOString();
    }
    if (fieldValue === DateFieldTypeData.BOOKING_CREATED_DATE && bookingUid) {
      if (!organizerEmail) {
        this.log.warn(`No organizer email found for bookingUid ${bookingUid}`);
      }
      const booking = await prisma.booking.findUnique({
        where: { uid: bookingUid },
        select: { createdAt: true },
      });

      if (!booking) {
        this.log.warn(`No booking found for ${bookingUid}`);
        return null;
      }

      return new Date(booking.createdAt).toISOString();
    }

    if (!bookingUid) {
      this.log.warn(`No uid for booking with organizer ${organizerEmail}`);
    }

    if (fieldValue === DateFieldTypeData.BOOKING_CANCEL_DATE) {
      return new Date().toISOString();
    }

    return null;
  }

  private async getPicklistFieldValue({
    fieldConfigValue,
    salesforceField,
    calEventResponses,
    bookingUid,
    recordId,
  }: {
    fieldConfigValue: string;
    salesforceField: Field;
    calEventResponses?: CalEventResponses | null;
    bookingUid?: string | null;
    recordId: string;
  }) {
    const log = logger.getSubLogger({ prefix: [`[getPicklistFieldValue] ${recordId}`] });

    const picklistOptions = salesforceField.picklistValues;
    if (!picklistOptions || !picklistOptions.length) {
      log.warn(`No picklist values found for field ${salesforceField.name}`);
      return null;
    }

    // Get the text value from the field
    const fieldTextValue = await this.getTextFieldValue({
      fieldValue: fieldConfigValue,
      fieldLength: salesforceField.length,
      calEventResponses,
      bookingUid,
      recordId,
      fieldName: salesforceField.name,
    });

    if (!fieldTextValue) {
      log.warn(`No text value found for field ${salesforceField.name}`);
      return null;
    }
    // Get the picklist value from the field
    const picklistValue = picklistOptions.find((option) => option.active && option.value === fieldTextValue);
    if (!picklistValue) {
      log.warn(`No picklist value found for field ${salesforceField.name} and value ${fieldTextValue}`);
      return null;
    }

    return picklistValue.value;
  }

  private async fetchPersonRecord(
    contactId: string,
    existingFields: Field[],
    personRecordType: SalesforceRecordEnum
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  ): Promise<Record<string, any> | null> {
    const conn = await this.conn;
    const existingFieldNames = existingFields.map((field) => field.name);

    const query = await conn.query(
      `SELECT Id, ${existingFieldNames.join(", ")} FROM ${personRecordType} WHERE Id = '${this.sanitizeSoqlValue(contactId)}'`
    );

    if (!query.records.length) {
      this.log.warn(`Could not find person record with id ${contactId}`);
      return null;
    }

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return query.records[0] as Record<string, any>;
  }

  private async createNewContactUnderAnAccount({
    attendee,
    accountId,
    organizerId,
  }: {
    attendee: Attendee;
    accountId: string;
    organizerId?: string;
  }) {
    const log = logger.getSubLogger({ prefix: [`[createNewContactUnderAnAccount]:${attendee.email}`] });
    log.info("createNewContactUnderAnAccount", safeStringify({ attendee, accountId, organizerId }));
    const conn = await this.conn;

    // First see if the contact already exists and connect it to the account
    const userQuery = await conn.query(
      `SELECT Id, Email FROM Contact WHERE Email = '${this.sanitizeSoqlValue(attendee.email)}' LIMIT 1`
    );
    if (userQuery.records.length > 0) {
      const contact = userQuery.records[0] as { Id: string; Email: string };
      await conn.sobject(SalesforceRecordEnum.CONTACT).update({
        // The first argument is the WHERE clause
        Id: contact.Id,
        AccountId: accountId,
      });
      return [{ id: contact.Id, email: contact.Email }];
    }

    return await this.createAttendeeRecord({
      attendee,
      recordType: SalesforceRecordEnum.CONTACT,
      accountId,
      organizerId,
    });
  }

  async findUserEmailFromLookupField(
    attendeeEmail: string,
    fieldName: string,
    salesforceObject: SalesforceRecordEnum
  ) {
    const conn = await this.conn;

    // Ensure the field exists on the record
    const existingFields = await this.ensureFieldsExistOnObject([fieldName], salesforceObject);

    if (!existingFields.length) return;

    const lookupField = existingFields[0];

    if (salesforceObject === SalesforceRecordEnum.ACCOUNT) {
      const accountId = await this.getAccountIdBasedOnEmailDomainOfContacts(attendeeEmail);

      if (!accountId) return;

      const accountQuery = (await conn.query(
        `SELECT ${lookupField.name} FROM ${SalesforceRecordEnum.ACCOUNT} WHERE Id = '${this.sanitizeSoqlValue(accountId)}'`
      )) as {
        // We do not know what fields are included in the account since it's unqiue to each instance
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        records: { [key: string]: any };
      };

      if (!accountQuery.records.length) return;

      const lookupFieldUserId = accountQuery.records[0][lookupField.name];

      if (!lookupFieldUserId) return;

      const userQuery = await this.getSalesforceUserFromUserId(lookupFieldUserId);

      if (!userQuery.records.length) return;

      const user = userQuery.records[0] as { Email: string };

      return { email: user.Email, recordType: RoutingReasons.ACCOUNT_LOOKUP_FIELD };
    }
  }

  /** Search the booking questions for the Company field value rather than relying on the email domain  */
  private async getCompanyNameFromBookingResponse(calEventResponses?: CalEventResponses | null) {
    const appOptions = this.getAppOptions();
    const companyFieldName = "Company";
    const defaultTextValueLength = 225;

    const { onBookingWriteToRecordFields = undefined } = appOptions;

    if (!onBookingWriteToRecordFields || !calEventResponses) return;

    // Check that we're writing to the Company field
    if (!(companyFieldName in onBookingWriteToRecordFields)) return;

    const companyValue = await this.getTextFieldValue({
      fieldValue: onBookingWriteToRecordFields[companyFieldName].value as string,
      fieldLength: defaultTextValueLength,
      calEventResponses,
      recordId: "New lead",
      fieldName: companyFieldName,
    });

    if (companyValue && companyValue === onBookingWriteToRecordFields[companyFieldName].value) return;
    return companyValue;
  }

  private async shouldSkipAttendeeIfFreeEmailDomain(attendeeEmail: string) {
    const appOptions = this.getAppOptions();
    if (!appOptions.ifFreeEmailDomainSkipOwnerCheck) return false;

    const response = await checkIfFreeEmailDomain({ email: attendeeEmail });
    return response;
  }

  async incompleteBookingWriteToRecord(
    email: string,
    writeToRecordObject: z.infer<typeof writeToRecordDataSchema>
  ) {
    const conn = await this.conn;

    // Prioritize contacts over leads
    const personRecord = await this.findProspectByEmail(email);

    if (!personRecord) {
      this.log.info(`No contact or lead found for email ${email}`);
      // No salesforce entity to update, skip and report success (unrecoverable)
      return;
    }

    const recordType = this.determineRecordTypeById(personRecord.Id);

    // Ensure the fields exist on the record
    const existingFields = await this.ensureFieldsExistOnObject(Object.keys(writeToRecordObject), recordType);

    const writeOnRecordBody = await this.buildRecordUpdatePayload({
      existingFields,
      personRecord,
      fieldsToWriteTo: writeToRecordObject,
      recordId: personRecord.Id,
    });
    await conn
      .sobject(recordType)
      .update({
        Id: personRecord.Id,
        ...writeOnRecordBody,
      })
      .catch((e) => {
        const contactId = personRecord?.Id || "unknown";
        // catch the error and throw a new one with a more descriptive message
        const errorMessage = `Error updating person record for contactId '${contactId}': ${
          e instanceof Error ? e.message : String(e)
        }`;
        throw new Error(errorMessage);
      });
  }

  /** All salesforce ids have a 3 character prefix associated with the record type
   * https://help.salesforce.com/s/articleView?id=000385203&type=1
   */
  private determineRecordTypeById(id: string) {
    switch (id.substring(0, 3)) {
      case "003":
        return SalesforceRecordEnum.CONTACT;
      case "001":
        return SalesforceRecordEnum.ACCOUNT;
      case "00Q":
        return SalesforceRecordEnum.LEAD;
      case "00U":
        return SalesforceRecordEnum.EVENT;
      default:
        this.log.warn(`Unhandled record id type ${id}`);
        return SalesforceRecordEnum.CONTACT;
    }
  }

  /** Prioritizes contacts over leads */
  private async findProspectByEmail(email: string) {
    const contact = await this.findContactByEmail(email);
    if (contact) return contact;
    const lead = await this.findLeadByEmail(email);
    if (lead) return lead;
    return null;
  }

  private async findContactByEmail(email: string) {
    const conn = await this.conn;
    const contactsQuery = await conn.query(
      `SELECT Id, Email FROM ${SalesforceRecordEnum.CONTACT} WHERE Email = '${this.sanitizeSoqlValue(email)}' LIMIT 1`
    );

    if (contactsQuery.records.length > 0) {
      return contactsQuery.records[0] as { Id: string; Email: string };
    }
  }

  private async findLeadByEmail(email: string) {
    const conn = await this.conn;
    const leadsQuery = await conn.query(
      `SELECT Id, Email FROM ${SalesforceRecordEnum.LEAD} WHERE Email = '${this.sanitizeSoqlValue(email)}' LIMIT 1`
    );

    if (leadsQuery.records.length > 0) {
      return leadsQuery.records[0] as { Id: string; Email: string };
    }
  }
}

/**
 * Factory function that creates a Salesforce CRM service instance.
 * This is exported instead of the class to prevent SDK types (like jsforce.Connection)
 * from leaking into the emitted .d.ts file, which would cause TypeScript to load
 * all jsforce SDK declaration files when type-checking dependent packages.
 */
export default function BuildCrmService(
  credential: CredentialPayload,
  appOptions?: Record<string, unknown>
): CRM {
  return new SalesforceCRMService(credential, (appOptions ?? {}) as z.infer<typeof appDataSchema>);
}

/**
 * Factory function that creates a Salesforce CRM service instance with the extended SalesforceCRM type.
 * This is used by internal Salesforce modules (routing forms, etc.) that need access to
 * Salesforce-specific methods beyond the generic CRM interface.
 */
export function createSalesforceCrmServiceWithSalesforceType(
  credential: CredentialPayload,
  appOptions?: Record<string, unknown>,
  testMode = false
): SalesforceCRM {
  return new SalesforceCRMService(credential, (appOptions ?? {}) as z.infer<typeof appDataSchema>, testMode);
}
