import { isPlainObject } from '@nestjs/common/utils/shared.utils';

import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'class-validator';
import { FieldMetadataType } from 'twenty-shared/types';

import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';

import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { isDate } from 'src/utils/date/isDate';
import { isValidDate } from 'src/utils/date/isValidDate';

export function formatResult<T>(
  data: any,
  objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
  objectMetadataMaps: ObjectMetadataMaps,
): T {
  if (!data) {
    return data;
  }

  if (Array.isArray(data)) {
    return data.map((item) =>
      formatResult(item, objectMetadataItemWithFieldMaps, objectMetadataMaps),
    ) as T;
  }

  if (!isPlainObject(data)) {
    return data;
  }

  if (!objectMetadataItemWithFieldMaps) {
    throw new Error('Object metadata is missing');
  }

  const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
    objectMetadataItemWithFieldMaps,
  );

  const relationMetadataMap = new Map(
    Object.values(objectMetadataItemWithFieldMaps.fieldsById)
      .filter(({ type }) => isRelationFieldMetadataType(type))
      .map((fieldMetadata) => [
        fieldMetadata.name,
        {
          relationMetadata:
            fieldMetadata.fromRelationMetadata ??
            fieldMetadata.toRelationMetadata,
          relationType: computeRelationType(
            fieldMetadata,
            fieldMetadata.fromRelationMetadata ??
              (fieldMetadata.toRelationMetadata as RelationMetadataEntity),
          ),
        },
      ]),
  );
  const newData: object = {};
  const objectMetadaItemFieldsByName =
    objectMetadataMaps.byId[objectMetadataItemWithFieldMaps.id]?.fieldsByName;

  for (const [key, value] of Object.entries(data)) {
    const compositePropertyArgs = compositeFieldMetadataMap.get(key);
    const { relationMetadata, relationType } =
      relationMetadataMap.get(key) ?? {};

    if (!compositePropertyArgs && !relationMetadata) {
      if (isPlainObject(value)) {
        newData[key] = formatResult(
          value,
          objectMetadataItemWithFieldMaps,
          objectMetadataMaps,
        );
      } else if (objectMetadaItemFieldsByName[key]) {
        newData[key] = formatFieldMetadataValue(
          value,
          objectMetadaItemFieldsByName[key],
        );
      } else {
        newData[key] = value;
      }

      continue;
    }

    if (relationMetadata) {
      const toObjectMetadata =
        objectMetadataMaps.byId[relationMetadata.toObjectMetadataId];

      const fromObjectMetadata =
        objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId];

      if (!toObjectMetadata) {
        throw new Error(
          `Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`,
        );
      }

      if (!fromObjectMetadata) {
        throw new Error(
          `Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`,
        );
      }

      newData[key] = formatResult(
        value,
        relationType === 'one-to-many' ? toObjectMetadata : fromObjectMetadata,
        objectMetadataMaps,
      );
      continue;
    }

    if (!compositePropertyArgs) {
      continue;
    }

    const { parentField, ...compositeProperty } = compositePropertyArgs;

    if (!newData[parentField]) {
      newData[parentField] = {};
    }

    newData[parentField][compositeProperty.name] = value;
  }

  const dateFieldMetadataCollection =
    objectMetadataItemWithFieldMaps.fields.filter(
      (field) => field.type === FieldMetadataType.DATE,
    );

  // This is a temporary fix to handle a bug in the frontend where the date gets returned in the wrong timezone,
  //   thus returning the wrong date.
  //
  // In short, for example :
  //   - DB stores `2025-01-01`
  //   - TypeORM .returning() returns `2024-12-31T23:00:00.000Z`
  //   - we shift +1h (or whatever the timezone offset is on the server)
  //   - we return `2025-01-01T00:00:00.000Z`
  //
  // See this PR for more details: https://github.com/twentyhq/twenty/pull/9700
  const serverOffsetInMillisecondsToCounterActTypeORMAutomaticTimezoneShift =
    new Date().getTimezoneOffset() * 60 * 1000;

  for (const dateFieldMetadata of dateFieldMetadataCollection) {
    const rawUpdatedDate = newData[dateFieldMetadata.name] as
      | string
      | null
      | undefined
      | Date;

    if (!isDefined(rawUpdatedDate)) {
      continue;
    }

    if (isDate(rawUpdatedDate)) {
      if (isValidDate(rawUpdatedDate)) {
        const shiftedDate = new Date(
          rawUpdatedDate.getTime() -
            serverOffsetInMillisecondsToCounterActTypeORMAutomaticTimezoneShift,
        );

        newData[dateFieldMetadata.name] = shiftedDate;
      }
    } else if (isNonEmptyString(rawUpdatedDate)) {
      const currentDate = new Date(newData[dateFieldMetadata.name]);

      const shiftedDate = new Date(
        new Date(currentDate).getTime() -
          serverOffsetInMillisecondsToCounterActTypeORMAutomaticTimezoneShift,
      );

      newData[dateFieldMetadata.name] = shiftedDate;
    }
  }

  return newData as T;
}

export function getCompositeFieldMetadataMap(
  objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
  const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
    objectMetadataItemWithFieldMaps,
  );

  return new Map(
    compositeFieldMetadataCollection.flatMap((fieldMetadata) => {
      const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);

      if (!compositeType) return [];

      // Map each composite property to a [key, value] pair
      return compositeType.properties.map((compositeProperty) => [
        computeCompositeColumnName(fieldMetadata.name, compositeProperty),
        {
          parentField: fieldMetadata.name,
          ...compositeProperty,
        },
      ]);
    }),
  );
}

function formatFieldMetadataValue(
  value: any,
  fieldMetadata: FieldMetadataInterface,
) {
  if (
    typeof value === 'string' &&
    (fieldMetadata.type === FieldMetadataType.MULTI_SELECT ||
      fieldMetadata.type === FieldMetadataType.ARRAY)
  ) {
    const cleanedValue = value.replace(/{|}/g, '').trim();

    return cleanedValue ? cleanedValue.split(',') : [];
  }

  return value;
}
