import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { ZId, ZOptionalNumber, ZOptionalString } from "@formbricks/types/common";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import { ITEMS_PER_PAGE } from "@/lib/constants";
import { validateInputs } from "@/lib/utils/validate";
import { getContactSurveyLink } from "@/modules/ee/contacts/lib/contact-survey-link";
import { segmentFilterToPrismaQuery } from "@/modules/ee/contacts/segments/lib/filter/prisma-query";
import { getSegment } from "@/modules/ee/contacts/segments/lib/segments";
import {
  TContact,
  TContactWithAttributes,
  ZContactCSVAttributeMap,
  ZContactCSVDuplicateAction,
  ZContactCSVUploadResponse,
} from "../types/contact";
import { transformPrismaContact } from "./utils";

export const getContactsInSegment = reactCache(async (segmentId: string) => {
  try {
    const segment = await getSegment(segmentId);

    if (!segment) {
      return null;
    }

    const segmentFilterToPrismaQueryResult = await segmentFilterToPrismaQuery(
      segment.id,
      segment.filters,
      segment.environmentId
    );

    if (!segmentFilterToPrismaQueryResult.ok) {
      return null;
    }

    const { whereClause } = segmentFilterToPrismaQueryResult.data;

    const requiredAttributes = ["userId", "firstName", "lastName", "email"];

    const contacts = await prisma.contact.findMany({
      where: whereClause,
      select: {
        id: true,
        attributes: {
          where: {
            attributeKey: {
              key: {
                in: requiredAttributes,
              },
            },
          },
          select: {
            attributeKey: {
              select: {
                key: true,
              },
            },
            value: true,
          },
        },
      },
      orderBy: {
        createdAt: "desc",
      },
    });

    const contactsWithAttributes = contacts.map((contact) => {
      const attributes = contact.attributes.reduce(
        (acc, attr) => {
          acc[attr.attributeKey.key] = attr.value;
          return acc;
        },
        {} as Record<string, string>
      );
      return {
        contactId: contact.id,
        attributes,
      };
    });

    return contactsWithAttributes;
  } catch (error) {
    logger.error(error, "Failed to get contacts in segment");
    return null;
  }
});

const selectContact = {
  id: true,
  createdAt: true,
  updatedAt: true,
  environmentId: true,
  attributes: {
    select: {
      value: true,
      attributeKey: {
        select: {
          key: true,
          name: true,
        },
      },
    },
  },
} satisfies Prisma.ContactSelect;

export const buildContactWhereClause = (environmentId: string, search?: string): Prisma.ContactWhereInput => {
  const whereClause: Prisma.ContactWhereInput = { environmentId };

  if (search) {
    whereClause.OR = [
      {
        attributes: {
          some: {
            value: {
              contains: search,
              mode: "insensitive",
            },
          },
        },
      },
      {
        id: {
          contains: search,
          mode: "insensitive",
        },
      },
    ];
  }

  return whereClause;
};

export const getContacts = reactCache(
  async (environmentId: string, offset?: number, searchValue?: string): Promise<TContactWithAttributes[]> => {
    validateInputs([environmentId, ZId], [offset, ZOptionalNumber], [searchValue, ZOptionalString]);

    try {
      const contacts = await prisma.contact.findMany({
        where: buildContactWhereClause(environmentId, searchValue),
        select: selectContact,
        take: ITEMS_PER_PAGE,
        skip: offset,
        orderBy: {
          createdAt: "desc",
        },
      });

      return contacts.map((contact) => transformPrismaContact(contact));
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError) {
        throw new DatabaseError(error.message);
      }

      throw error;
    }
  }
);

export const getContact = reactCache(async (contactId: string): Promise<TContact | null> => {
  validateInputs([contactId, ZId]);

  try {
    return await prisma.contact.findUnique({
      where: {
        id: contactId,
      },
      select: selectContact,
    });
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      throw new DatabaseError(error.message);
    }

    throw error;
  }
});

export const deleteContact = async (contactId: string): Promise<TContact | null> => {
  validateInputs([contactId, ZId]);

  try {
    const contact = await prisma.contact.delete({
      where: {
        id: contactId,
      },
      select: selectContact,
    });

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

    throw error;
  }
};

// Shared include clause for contact queries
const contactAttributesInclude = {
  attributes: {
    select: {
      attributeKey: { select: { key: true } },
      value: true,
    },
  },
} satisfies Prisma.ContactInclude;

// Helper to create attribute objects for Prisma create operations
const createAttributeConnections = (record: Record<string, string>, environmentId: string) =>
  Object.entries(record).map(([key, value]) => ({
    attributeKey: {
      connect: { key_environmentId: { key, environmentId } },
    },
    value,
  }));

// Helper to handle userId conflicts when updating/overwriting contacts
const resolveUserIdConflict = (
  mappedRecord: Record<string, string>,
  existingContact: { id: string; attributes: { attributeKey: { key: string }; value: string }[] },
  existingUserIds: { value: string; contactId: string }[]
): Record<string, string> => {
  const existingUserId = existingUserIds.find(
    (attr) => attr.value === mappedRecord.userId && attr.contactId !== existingContact.id
  );

  if (!existingUserId) {
    return { ...mappedRecord };
  }

  const { userId: _userId, ...rest } = mappedRecord;
  const existingContactUserId = existingContact.attributes.find(
    (attr) => attr.attributeKey.key === "userId"
  )?.value;

  return {
    ...rest,
    ...(existingContactUserId && { userId: existingContactUserId }),
  };
};

export const createContactsFromCSV = async (
  csvData: Record<string, string>[],
  environmentId: string,
  duplicateContactsAction: "skip" | "update" | "overwrite",
  attributeMap: Record<string, string>
): Promise<TContact[]> => {
  validateInputs(
    [csvData, ZContactCSVUploadResponse],
    [environmentId, ZId],
    [duplicateContactsAction, ZContactCSVDuplicateAction],
    [attributeMap, ZContactCSVAttributeMap]
  );

  try {
    // Extract unique emails and userIds from CSV data
    const csvEmails = Array.from(new Set(csvData.map((r) => r.email).filter(Boolean)));
    const csvUserIds = Array.from(new Set(csvData.map((r) => r.userId).filter(Boolean)));

    // Fetch existing contacts by email
    const existingContactsByEmail = await prisma.contact.findMany({
      where: {
        environmentId,
        attributes: {
          some: {
            attributeKey: {
              key: "email",
            },
            value: {
              in: csvEmails,
            },
          },
        },
      },
      select: {
        id: true,
        attributes: {
          select: {
            attributeKey: { select: { key: true, id: true } },
            value: true,
          },
        },
      },
    });

    // Map emails to existing contacts
    const emailToContactMap = new Map<
      string,
      Prisma.ContactGetPayload<{
        select: {
          id: true;
          attributes: {
            select: {
              attributeKey: { select: { key: true; id: true } };
              value: true;
            };
          };
        };
      }>
    >();
    existingContactsByEmail.forEach((contact) => {
      const emailAttr = contact.attributes.find((attr) => attr.attributeKey.key === "email");
      if (emailAttr) {
        emailToContactMap.set(emailAttr.value, contact);
      }
    });

    // Check for duplicate userIds
    const existingUserIds = await prisma.contactAttribute.findMany({
      where: {
        attributeKey: {
          key: "userId",
          environmentId,
        },
        value: {
          in: csvUserIds,
        },
      },
      select: { value: true, contactId: true },
    });

    // Fetch existing attribute keys and cache them
    const existingAttributeKeys = await prisma.contactAttributeKey.findMany({
      where: { environmentId },
      select: { key: true, id: true },
    });

    const attributeKeyMap = new Map<string, string>();
    // Map from lowercase key to actual DB key (for case-insensitive lookup)
    const lowercaseToActualKeyMap = new Map<string, string>();

    existingAttributeKeys.forEach((attrKey) => {
      attributeKeyMap.set(attrKey.key, attrKey.id);
      lowercaseToActualKeyMap.set(attrKey.key.toLowerCase(), attrKey.key);
    });

    // Collect all unique CSV keys
    const csvKeys = new Set<string>();
    csvData.forEach((record) => {
      Object.keys(record).forEach((key) => csvKeys.add(key));
    });

    // Identify missing attribute keys (case-insensitive check)
    const missingKeys = Array.from(csvKeys).filter((key) => !lowercaseToActualKeyMap.has(key.toLowerCase()));

    // Create missing attribute keys (use original CSV casing for new keys)
    if (missingKeys.length > 0) {
      // Deduplicate by lowercase to avoid creating duplicates like "firstName" and "firstname"
      const uniqueMissingKeys = new Map<string, string>();
      missingKeys.forEach((key) => {
        const lowerKey = key.toLowerCase();
        if (!uniqueMissingKeys.has(lowerKey)) {
          uniqueMissingKeys.set(lowerKey, key);
        }
      });

      await prisma.contactAttributeKey.createMany({
        data: Array.from(uniqueMissingKeys.values()).map((key) => ({
          key,
          name: key,
          environmentId,
        })),
        skipDuplicates: true,
      });

      // Fetch and update the maps with new keys
      const newAttributeKeys = await prisma.contactAttributeKey.findMany({
        where: {
          key: { in: Array.from(uniqueMissingKeys.values()) },
          environmentId,
        },
        select: { key: true, id: true },
      });

      newAttributeKeys.forEach((attrKey) => {
        attributeKeyMap.set(attrKey.key, attrKey.id);
        lowercaseToActualKeyMap.set(attrKey.key.toLowerCase(), attrKey.key);
      });
    }

    const createdContacts: TContact[] = [];

    // Process contacts in parallel
    const contactPromises = csvData.map(async (record) => {
      // Map CSV keys to actual DB keys (case-insensitive matching, preserving DB key casing)
      const mappedRecord: Record<string, string> = {};
      Object.entries(record).forEach(([key, value]) => {
        const actualKey = lowercaseToActualKeyMap.get(key.toLowerCase());
        if (!actualKey) {
          // This should never happen since we create missing keys above
          throw new ValidationError(`Attribute key "${key}" not found in attribute key map`);
        }
        mappedRecord[actualKey] = value;
      });

      // Skip records without email
      if (!mappedRecord.email) {
        throw new ValidationError("Email is required for all contacts");
      }

      const existingContact = emailToContactMap.get(mappedRecord.email);

      if (existingContact) {
        // Handle duplicates based on duplicateContactsAction
        switch (duplicateContactsAction) {
          case "skip":
            return null;

          case "update": {
            const recordToProcess = resolveUserIdConflict(mappedRecord, existingContact, existingUserIds);

            const attributesToUpsert = Object.entries(recordToProcess).map(([key, value]) => ({
              where: {
                contactId_attributeKeyId: {
                  contactId: existingContact.id,
                  attributeKeyId: attributeKeyMap.get(key),
                },
              },
              update: { value },
              create: {
                attributeKeyId: attributeKeyMap.get(key),
                value,
              },
            }));

            // Update contact with upserted attributes
            return prisma.contact.update({
              where: { id: existingContact.id },
              data: {
                attributes: {
                  // @ts-expect-error
                  upsert: attributesToUpsert,
                },
              },
              include: contactAttributesInclude,
            });
          }

          case "overwrite": {
            const recordToProcess = resolveUserIdConflict(mappedRecord, existingContact, existingUserIds);

            // Overwrite by deleting existing attributes and creating new ones
            await prisma.contactAttribute.deleteMany({
              where: { contactId: existingContact.id },
            });

            return prisma.contact.update({
              where: { id: existingContact.id },
              data: {
                attributes: {
                  create: createAttributeConnections(recordToProcess, environmentId),
                },
              },
              include: contactAttributesInclude,
            });
          }
        }
      } else {
        // Create new contact - use mappedRecord with proper DB key casing
        return prisma.contact.create({
          data: {
            environmentId,
            attributes: {
              create: createAttributeConnections(mappedRecord, environmentId),
            },
          },
          include: contactAttributesInclude,
        });
      }
    });

    const results = await Promise.all(contactPromises);
    const createdContactsFiltered = results.filter((contact) => contact !== null) as TContact[];
    createdContacts.push(...createdContactsFiltered);

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

export const generatePersonalLinks = async (surveyId: string, segmentId: string, expirationDays?: number) => {
  const contactsResult = await getContactsInSegment(segmentId);

  if (!contactsResult) {
    return null;
  }

  // Generate survey links for each contact
  const contactLinks = await Promise.all(
    contactsResult.map(async (contact) => {
      const { contactId, attributes } = contact;

      const surveyUrlResult = await getContactSurveyLink(contactId, surveyId, expirationDays);

      if (!surveyUrlResult.ok) {
        logger.error(
          { error: surveyUrlResult.error, contactId: contactId, surveyId: surveyId },
          "Failed to generate survey URL for contact"
        );
        return null;
      }

      return {
        contactId,
        attributes,
        surveyUrl: surveyUrlResult.data,
        expirationDays,
      };
    })
  );

  const filteredContactLinks = contactLinks.filter(Boolean);
  return filteredContactLinks;
};
