import { prisma } from "../constants";
import { CredentialController } from "../controllers/CredentialController";
import { fieldValidator } from "../modules/credentials/fieldValidator";
import {
  CredentialField,
  CredentialTypeField,
  ValueType,
} from "../modules/credentials/credentialTypeDefs";
import { generateSecureValue } from "../modules/credentials/generateSecureValue";
import GenericError from "../Errors/GenericError";
import { Prisma } from "@prisma/client";

/**
 * Standard include clause used by every credential query.
 * Includes the credential type, company, secure values, and one level of sub-credentials.
 */
const credentialInclude = {
  type: true,
  company: true,
  securevalues: true,
  subCredentials: {
    include: {
      type: true,
      company: true,
      securevalues: true,
    },
  },
} as const;

export const credentials = {
  /**
   * Fetch Credential
   *
   * Fetch a credential by its ID and return a CredentialController instance.
   *
   * @param id - The credential ID to fetch
   * @returns {Promise<CredentialController>} - The credential controller
   */
  async fetch(id: string): Promise<CredentialController> {
    const credential = await prisma.credential.findFirst({
      where: { id },
      include: credentialInclude,
    });

    if (!credential) {
      throw new GenericError({
        message: "Credential not found",
        name: "CredentialNotFound",
        cause: `No credential exists with ID '${id}'`,
        status: 404,
      });
    }

    return new CredentialController(credential);
  },

  /**
   * Fetch Credentials by Company
   *
   * Fetch all top-level credentials associated with a specific company.
   * Sub-credentials are excluded from the top-level list and instead
   * included nested under their parent credential.
   *
   * @param companyId - The company ID to fetch credentials for
   * @returns {Promise<CredentialController[]>} - Array of credential controllers
   */
  async fetchByCompany(companyId: string): Promise<CredentialController[]> {
    const credentialsList = await prisma.credential.findMany({
      where: { companyId, subCredentialOfId: null },
      include: credentialInclude,
    });

    return credentialsList.map((cred) => new CredentialController(cred));
  },

  /**
   * Create Credential
   *
   * Create a new credential with validated fields.
   * This method processes all incoming field values, validating them against
   * the credential type, encrypting secure fields, and inserting everything
   * into the database atomically.
   *
   * When the credential type contains multi-credential fields, pass
   * `subCredentials` keyed by the multi-credential field ID.  Each entry
   * is an array of sub-credential objects with their own name and fields.
   *
   * @param data - The credential data to create
   * @returns {Promise<CredentialController>} - The created credential controller
   */
  async create(data: {
    name: string;
    notes?: string;
    typeId: string;
    companyId: string;
    fields: {
      fieldId: string;
      value: string;
    }[];
    subCredentials?: Record<
      string,
      { name: string; fields: { fieldId: string; value: string }[] }[]
    >;
  }): Promise<CredentialController> {
    // Fetch the credential type to get acceptable fields
    const credentialType = await prisma.credentialType.findFirst({
      where: { id: data.typeId },
    });

    if (!credentialType) {
      throw new GenericError({
        message: "Credential type not found",
        name: "CredentialTypeNotFound",
        cause: `No credential type exists with ID '${data.typeId}'`,
        status: 404,
      });
    }

    const typeFields =
      credentialType.fields! as Prisma.JsonArray as CredentialTypeField[];

    // Validate the fields against acceptable fields (exclude multi-credential fields
    // from value validation since they don't carry a direct value).
    const acceptableFields = typeFields.map((f) => ({
      id: f.id,
      name: f.name,
      secure: f.secure,
      required: f.required,
      valueType: f.valueType,
      subFields: f.subFields,
    })) as CredentialTypeField[];

    const validatedFields = await fieldValidator(
      data.fields as Prisma.JsonArray as CredentialField[],
      acceptableFields
    );

    // Separate secure, non-secure, and multi-credential fields
    const secureFields = validatedFields.filter(
      (f) => f.secure && !f.isMultiCredential
    );
    const nonSecureFields = validatedFields.filter(
      (f) => !f.secure && !f.isMultiCredential
    );

    // Build fields object for non-secure fields
    const fieldsObject: Record<string, any> = {};
    nonSecureFields.forEach((field) => {
      fieldsObject[field.fieldId] = field.value;
    });

    // Initialise multi-credential field slots with empty arrays
    typeFields
      .filter((f) => f.valueType === ValueType.MULTI_CREDENTIAL)
      .forEach((f) => {
        fieldsObject[f.id] = [];
      });

    // Encrypt secure field values
    const secureValueData = secureFields.map((field) => {
      const { encrypted, hash } = generateSecureValue(field.value);
      return {
        name: field.fieldId,
        content: encrypted,
        hash,
      };
    });

    // Create the parent credential first
    const credential = await prisma.credential.create({
      data: {
        name: data.name,
        notes: data.notes,
        typeId: data.typeId,
        companyId: data.companyId,
        fields: fieldsObject,
        securevalues: {
          create: secureValueData,
        },
      },
    });

    // Create inline sub-credentials when provided
    if (data.subCredentials) {
      for (const [fieldId, subCredDataList] of Object.entries(
        data.subCredentials
      )) {
        const fieldDef = typeFields.find((f) => f.id === fieldId);

        if (!fieldDef || fieldDef.valueType !== ValueType.MULTI_CREDENTIAL) {
          throw new GenericError({
            message: `Field '${fieldId}' is not a multi-credential field`,
            name: "InvalidMultiCredentialField",
            cause: `Cannot create sub-credentials for field '${fieldId}' because it is not a multi-credential field.`,
            status: 400,
          });
        }

        const subFieldDefs = (fieldDef.subFields ??
          []) as CredentialTypeField[];
        const subCredIds: string[] = [];

        for (const subCredData of subCredDataList) {
          const validatedSubFields = await fieldValidator(
            subCredData.fields as Prisma.JsonArray as CredentialField[],
            subFieldDefs
          );

          const subSecure = validatedSubFields.filter((f) => f.secure);
          const subNonSecure = validatedSubFields.filter((f) => !f.secure);

          const subFieldsObject: Record<string, any> = {};
          subNonSecure.forEach((f) => {
            subFieldsObject[f.fieldId] = f.value;
          });

          const subSecureValueData = subSecure.map((f) => {
            const { encrypted, hash } = generateSecureValue(f.value);
            return { name: f.fieldId, content: encrypted, hash };
          });

          const subCred = await prisma.credential.create({
            data: {
              name: subCredData.name,
              typeId: data.typeId,
              companyId: data.companyId,
              subCredentialOfId: credential.id,
              fields: subFieldsObject,
              securevalues: { create: subSecureValueData },
            },
          });

          subCredIds.push(subCred.id);
        }

        fieldsObject[fieldId] = subCredIds;
      }

      // Persist the sub-credential ID arrays on the parent
      await prisma.credential.update({
        where: { id: credential.id },
        data: { fields: fieldsObject },
      });
    }

    // Re-fetch with full includes
    const completeCredential = await prisma.credential.findFirst({
      where: { id: credential.id },
      include: credentialInclude,
    });

    return new CredentialController(completeCredential!);
  },

  /**
   * Add Sub-Credential
   *
   * Create a new sub-credential under an existing parent credential
   * for a specific multi-credential field.
   *
   * @param parentId - The parent credential ID
   * @param fieldId  - The multi-credential field this sub-credential belongs to
   * @param data     - The sub-credential data (name and fields)
   * @returns {Promise<CredentialController>} - The created sub-credential controller
   */
  async addSubCredential(
    parentId: string,
    fieldId: string,
    data: {
      name: string;
      fields: { fieldId: string; value: string }[];
    }
  ): Promise<CredentialController> {
    const parent = await prisma.credential.findFirst({
      where: { id: parentId },
      include: { type: true },
    });

    if (!parent) {
      throw new GenericError({
        message: "Parent credential not found",
        name: "CredentialNotFound",
        cause: `No credential exists with ID '${parentId}'`,
        status: 404,
      });
    }

    const typeFields = parent.type.fields as any as CredentialTypeField[];
    const fieldDef = typeFields.find((f) => f.id === fieldId);

    if (!fieldDef || fieldDef.valueType !== ValueType.MULTI_CREDENTIAL) {
      throw new GenericError({
        message: `Field '${fieldId}' is not a multi-credential field`,
        name: "InvalidMultiCredentialField",
        cause: `Cannot create sub-credentials for field '${fieldId}' because it is not a multi-credential field.`,
        status: 400,
      });
    }

    const subFieldDefs = (fieldDef.subFields ?? []) as CredentialTypeField[];
    const validatedFields = await fieldValidator(
      data.fields as Prisma.JsonArray as CredentialField[],
      subFieldDefs
    );

    const secureFields = validatedFields.filter((f) => f.secure);
    const nonSecureFields = validatedFields.filter((f) => !f.secure);

    const subFieldsObject: Record<string, any> = {};
    nonSecureFields.forEach((f) => {
      subFieldsObject[f.fieldId] = f.value;
    });

    const secureValueData = secureFields.map((f) => {
      const { encrypted, hash } = generateSecureValue(f.value);
      return { name: f.fieldId, content: encrypted, hash };
    });

    const subCredential = await prisma.credential.create({
      data: {
        name: data.name,
        typeId: parent.typeId,
        companyId: parent.companyId,
        subCredentialOfId: parentId,
        fields: subFieldsObject,
        securevalues: { create: secureValueData },
      },
      include: credentialInclude,
    });

    // Update parent's fields JSON to include the new sub-credential ID
    const parentFields = parent.fields as Record<string, any>;
    const subCredIds: string[] = parentFields[fieldId] ?? [];
    subCredIds.push(subCredential.id);
    parentFields[fieldId] = subCredIds;

    await prisma.credential.update({
      where: { id: parentId },
      data: { fields: parentFields },
    });

    return new CredentialController(subCredential);
  },

  /**
   * Remove Sub-Credential
   *
   * Delete a sub-credential and remove its reference from the parent credential's
   * multi-credential field.
   *
   * @param parentId       - The parent credential ID
   * @param subCredentialId - The sub-credential ID to remove
   * @returns {Promise<void>}
   */
  async removeSubCredential(
    parentId: string,
    subCredentialId: string
  ): Promise<void> {
    const subCredential = await prisma.credential.findFirst({
      where: { id: subCredentialId, subCredentialOfId: parentId },
    });

    if (!subCredential) {
      throw new GenericError({
        message: "Sub-credential not found",
        name: "SubCredentialNotFound",
        cause: `No sub-credential with ID '${subCredentialId}' exists under credential '${parentId}'`,
        status: 404,
      });
    }

    // Delete the sub-credential (cascade removes its secure values)
    await prisma.credential.delete({
      where: { id: subCredentialId },
    });

    // Remove the sub-credential ID from the parent's fields JSON
    const parent = await prisma.credential.findFirst({
      where: { id: parentId },
    });

    if (parent) {
      const parentFields = parent.fields as Record<string, any>;
      for (const key of Object.keys(parentFields)) {
        if (Array.isArray(parentFields[key])) {
          parentFields[key] = parentFields[key].filter(
            (id: string) => id !== subCredentialId
          );
        }
      }

      await prisma.credential.update({
        where: { id: parentId },
        data: { fields: parentFields },
      });
    }
  },

  /**
   * Fetch Sub-Credentials
   *
   * Fetch all sub-credentials that belong to a specific parent credential.
   *
   * @param parentId - The parent credential ID
   * @returns {Promise<CredentialController[]>} - Array of sub-credential controllers
   */
  async fetchSubCredentials(parentId: string): Promise<CredentialController[]> {
    const subCredentials = await prisma.credential.findMany({
      where: { subCredentialOfId: parentId },
      include: credentialInclude,
    });

    return subCredentials.map((sc) => new CredentialController(sc));
  },

  /**
   * Delete Credential
   *
   * Delete a credential by its ID.
   * Sub-credentials are cascade-deleted automatically by the database.
   *
   * @param id - The credential ID to delete
   * @returns {Promise<void>}
   */
  async delete(id: string): Promise<void> {
    await prisma.credential.delete({
      where: { id },
    });
  },
};
