import { z } from "zod";
import {
  Credential,
  CredentialType,
  Company,
  SecureValue,
} from "../../generated/prisma/client";
import { prisma } from "../constants";
import { fieldValidator } from "../modules/credentials/fieldValidator";
import {
  CredentialField,
  CredentialTypeField,
  ValueType,
} from "../modules/credentials/credentialTypeDefs";
import { generateSecureValue } from "../modules/credentials/generateSecureValue";
import { readSecureValue } from "../modules/credentials/readSecureValue";
import GenericError from "../Errors/GenericError";
import { CompanyController } from "./CompanyController";

/**
 * Credential Controller
 *
 * This class manages credential data, including field validation,
 * secure value storage/retrieval, and credential updates.
 */
export class CredentialController {
  public readonly id: string;
  public name: string;
  public notes: string | null;
  public readonly typeId: string;
  public readonly companyId: string;
  public readonly subCredentialOfId: string | null;
  public fields: any;

  private _type: CredentialType;
  private _company: Company;
  private _secureValues: SecureValue[];
  private _subCredentials: CredentialController[];

  public readonly createdAt: Date;
  public updatedAt: Date;

  constructor(
    credentialData: Credential & {
      type: CredentialType;
      company: Company;
      securevalues: SecureValue[];
      subCredentials?: (Credential & {
        type: CredentialType;
        company: Company;
        securevalues: SecureValue[];
      })[];
    },
  ) {
    this.id = credentialData.id;
    this.name = credentialData.name;
    this.notes = credentialData.notes;
    this.typeId = credentialData.typeId;
    this.companyId = credentialData.companyId;
    this.subCredentialOfId = credentialData.subCredentialOfId;
    this._type = credentialData.type;
    this._company = credentialData.company;
    this._secureValues = credentialData.securevalues;
    this._subCredentials = (credentialData.subCredentials ?? []).map(
      (sc) => new CredentialController(sc),
    );
    this.fields = this._buildFields(credentialData);
    this.createdAt = credentialData.createdAt;
    this.updatedAt = credentialData.updatedAt;
  }

  /**
   * Build Fields
   *
   * Maps raw credential data into a structured fields array.
   * - Regular credentials: maps through the type's field definitions.
   * - Multi-credential fields: returns sub-credential references and subField definitions.
   * - Sub-credentials: returns raw field data (validated against subFields, not the type's top-level fields).
   */
  private _buildFields(credentialData: Credential) {
    const raw = credentialData.fields as Record<string, any>;
    const typeFields = this._type.fields as any as CredentialTypeField[];

    // Sub-credentials: their fields don't match the type's top-level definitions,
    // so we return a simple id/value list built from raw JSON + secure values.
    if (credentialData.subCredentialOfId) {
      const result: any[] = [];

      // Collect field IDs that have secure values
      const secureFieldIds = new Set(this._secureValues.map((sv) => sv.name));

      // Non-secure fields from JSON
      Object.entries(raw).forEach(([fieldId, value]) => {
        if (!secureFieldIds.has(fieldId)) {
          result.push({ id: fieldId, value, secure: false });
        }
      });

      // Secure value references
      this._secureValues.forEach((sv) => {
        result.push({ id: sv.name, value: `secure-${sv.id}`, secure: true });
      });

      return result;
    }

    // Regular (parent) credential: map through type field definitions
    return typeFields.map((f: any) => {
      if (f.valueType === ValueType.MULTI_CREDENTIAL) {
        const subCredIds: string[] = raw[f.id] ?? [];
        return {
          id: f.id,
          name: f.name,
          secure: false,
          required: f.required,
          valueType: f.valueType,
          subFields: f.subFields ?? [],
          subCredentialIds: subCredIds,
        };
      }

      return {
        id: f.id,
        name: f.name,
        secure: f.secure,
        required: f.required,
        valueType: f.valueType as ValueType,
        value: f.secure
          ? `secure-${this._secureValues.find((sv) => sv.name === f.id)?.id}`
          : raw[f.id],
      };
    });
  }

  /**
   * Update Internal Values
   *
   * Internal method to update all internal values when we query the database.
   * This keeps everything up-to-date even when we pass around the credential controller.
   *
   * @param credentialData - Credential object from Prisma
   */
  private _updateInternalValues(credentialData: Credential) {
    this.name = credentialData.name;
    this.notes = credentialData.notes;
    this.fields = credentialData.fields;
    this.updatedAt = credentialData.updatedAt;
  }

  /**
   * Validate and Update Fields
   *
   * This method validates the submitted fields against the credential type's
   * acceptable fields, then updates the credential in the database.
   * Secure fields are encrypted and stored in the SecureValue table.
   *
   * @param fields - The fields to validate and update
   * @returns {Promise<CredentialController>} - The updated credential controller
   */
  async validateAndUpdateFields(
    fields: CredentialField[],
  ): Promise<CredentialController> {
    // Get acceptable fields from the credential type
    const acceptableFields = this._type.fields as any as CredentialTypeField[];
    // Validate the fields
    const validatedFields = await fieldValidator(fields, acceptableFields);

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

    // Process secure fields - encrypt and store in SecureValue table
    await Promise.all(
      secureFields.map(async (field) => {
        const { encrypted, hash } = generateSecureValue(field.value);

        // Check if a secure value already exists for this field
        const existingSecureValue = await prisma.secureValue.findFirst({
          where: {
            credentialId: this.id,
            name: field.fieldId,
          },
        });

        if (existingSecureValue) {
          // Update existing secure value
          await prisma.secureValue.update({
            where: { id: existingSecureValue.id },
            data: {
              content: encrypted,
              hash,
            },
          });
        } else {
          // Create new secure value
          await prisma.secureValue.create({
            data: {
              name: field.fieldId,
              content: encrypted,
              hash,
              credentialId: this.id,
            },
          });
        }
      }),
    );

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

    // Preserve multi-credential field values (sub-credential ID arrays)
    const currentFields = (await prisma.credential.findFirst({
      where: { id: this.id },
      select: { fields: true },
    }))!.fields as Record<string, any>;

    const typeFields = this._type.fields as any as CredentialTypeField[];
    typeFields.forEach((f) => {
      if (f.valueType === ValueType.MULTI_CREDENTIAL && currentFields[f.id]) {
        fieldsObject[f.id] = currentFields[f.id];
      }
    });

    // Update the credential with non-secure fields
    const updatedCredential = await prisma.credential.update({
      where: { id: this.id },
      data: {
        fields: fieldsObject,
      },
    });

    this._updateInternalValues(updatedCredential);

    // Refresh secure values
    const secureValues = await prisma.secureValue.findMany({
      where: { credentialId: this.id },
    });
    this._secureValues = secureValues;

    return this;
  }

  /**
   * Fetch All Field Values
   *
   * Returns all field values (both secure and non-secure) for this credential.
   * Secure field values are NOT decrypted - use `readSecureFieldValue` for that.
   *
   * @returns {Promise<CredentialField[]>} - Array of all fields with their encrypted values
   */
  async fetchAllFieldValues(): Promise<CredentialField[]> {
    const fields: CredentialField[] = [];

    // Add non-secure fields from the fields JSON
    const nonSecureFields = this.fields as Record<string, any>;
    Object.entries(nonSecureFields || {}).forEach(([fieldId, value]) => {
      fields.push({
        fieldId,
        value: value as string,
      });
    });

    // Add secure fields from SecureValue table (encrypted)
    this._secureValues.forEach((secureValue) => {
      fields.push({
        fieldId: secureValue.name,
        value: secureValue.content, // Encrypted value
      });
    });

    return fields;
  }

  /**
   * Read Secure Field Value
   *
   * Decrypts and returns the value of a specific secure field.
   *
   * @param fieldId - The field ID to read
   * @returns {Promise<string>} - The decrypted field value
   */
  async readSecureFieldValue(fieldId: string): Promise<string> {
    const secureValue = this._secureValues.find((sv) => sv.name === fieldId);

    if (!secureValue) {
      throw new GenericError({
        message: `Secure field not found: ${fieldId}`,
        name: "SecureFieldNotFound",
        cause: `No secure value exists for field '${fieldId}' in credential '${this.id}'`,
        status: 404,
      });
    }

    // Decrypt the value
    const decryptedValue = readSecureValue(
      secureValue.content,
      secureValue.hash,
    );

    return decryptedValue;
  }

  /**
   * Read All Secure Values
   *
   * Decrypts and returns all secure field values for this credential.
   *
   * @returns {Promise<Record<string, string>>} - Object mapping field IDs to decrypted values
   */
  async readAllSecureValues(): Promise<Record<string, string>> {
    const secureValues: Record<string, string> = {};

    await Promise.all(
      this._secureValues.map(async (secureValue) => {
        const decryptedValue = readSecureValue(
          secureValue.content,
          secureValue.hash,
        );
        secureValues[secureValue.name] = decryptedValue;
      }),
    );

    return secureValues;
  }

  /**
   * Update Credential
   *
   * Update the credential name or other basic properties.
   *
   * @param data - Partial credential data to update
   * @returns {Promise<CredentialController>} - The updated credential controller
   */
  async update(
    data: Partial<Pick<Credential, "name" | "notes">>,
  ): Promise<CredentialController> {
    const pData = z
      .object({
        name: z.string().optional(),
        notes: z.string().nullable().optional(),
      })
      .strict()
      .parse(data);

    const updatedCredential = await prisma.credential.update({
      where: { id: this.id },
      data: pData,
    });

    this._updateInternalValues(updatedCredential);

    return this;
  }

  /**
   * Get Credential Type
   *
   * Returns the credential type information.
   *
   * @returns {CredentialType} - The credential type
   */
  getType(): CredentialType {
    return this._type;
  }

  /**
   * Get Company
   *
   * Returns the company this credential belongs to.
   *
   * @returns {Company} - The company
   */
  getCompany(): CompanyController {
    return new CompanyController(this._company as any);
  }

  /**
   * To JSON
   *
   * Create an object that can be safely returned to the user.
   * Secure values are not included by default.
   *
   * @param opts - Options to change the output
   * @returns - An object that is JSON friendly
   */
  toJson(opts?: { includeSecureValues?: boolean }): Record<string, any> {
    return {
      id: this.id,
      name: this.name,
      notes: this.notes,
      typeId: this.typeId,
      companyId: this.companyId,
      subCredentialOfId: this.subCredentialOfId ?? undefined,
      fields: this.fields,
      type: {
        id: this._type.id,
        name: this._type.name,
        fields: this._type.fields,
        permissionScope: this._type.permissionScope,
      },
      company: {
        id: (this._company as any).uid ?? this._company.id,
        name: this._company.name,
      },
      subCredentials:
        this._subCredentials.length > 0
          ? this._subCredentials.map((sc) => sc.toJson(opts))
          : undefined,
      secureFieldIds: opts?.includeSecureValues
        ? this._secureValues.map((sv) => sv.name)
        : undefined,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}
