import {
  FieldType,
  Record as RecordModel,
} from '@airtable/blocks/models';
import ono from '@jsdevtools/ono';
import { FieldSet } from 'airtable';
import {
  isEqual,
  keys,
  omit
} from 'lodash';

import {
  AirtableFieldIdentifier,
  AirtableFieldLinks,
  AirtableFieldTypesToFieldValues
} from '../../mappers/airtable';
import { NonNullableObject } from '../../types';
import { isPresent } from '../../util';
import {
  ModelPropsKey,
  ShopifyObject,
  ShopifyObjectType,
  ShopifyObjects
} from '../shopify';

import {
  AirtableModelAttributes,
  SerializedModelProperties,
} from './AirtableModel';
import { SerializedAirtableTable } from './AirtableTable';

const SerializedRecordProperties = [...SerializedModelProperties,
  // 'url', not present in Record returned by REST API and not necessary - can be generated via table id and record id
  // 'createdTime' not present in Record returned by REST API
] as const;
type SerializedRecordProperties = typeof SerializedRecordProperties[number];

export type SerializedAirtableRecord = AirtableModelAttributes<RecordModel, SerializedRecordProperties> & {
  fields: SerializedRecordFields;
};

/**
 * Reduces the cell value contained in the Block API Record model to the cell value that the REST API will provide.
 * @param unknownCellValue 
 */
function getRestApiFieldValue(unknownCellValue: unknown, fieldType: FieldType) {
  switch(fieldType) {
    /**
     * In these cases, the cell value present in the Block Record has an equivalent type to the value returned by the REST API.
     */
    case FieldType.BUTTON:
    case FieldType.RATING:
    case FieldType.RICH_TEXT:
    case FieldType.DURATION:
    case FieldType.LAST_MODIFIED_TIME:
    case FieldType.BARCODE:
    case FieldType.AUTO_NUMBER:
    case FieldType.PHONE_NUMBER:
    case FieldType.MULTIPLE_ATTACHMENTS:
    case FieldType.CHECKBOX:
    case FieldType.FORMULA:
    case FieldType.CREATED_TIME:
    case FieldType.ROLLUP:
    case FieldType.COUNT:
    case FieldType.DATE:
    case FieldType.DATE_TIME:
    case FieldType.PERCENT:
    case FieldType.CURRENCY:
    case FieldType.NUMBER:
    case FieldType.EMAIL:
    case FieldType.URL:
    case FieldType.MULTILINE_TEXT:
    case FieldType.SINGLE_LINE_TEXT: {
      const returnAsIsCellValue = unknownCellValue as AirtableFieldTypesToFieldValues[typeof fieldType]['block'];

      return returnAsIsCellValue;
    }
    case FieldType.SINGLE_SELECT: {
      const singleSelectCellValue = unknownCellValue as AirtableFieldTypesToFieldValues[typeof fieldType]['block'];

      return singleSelectCellValue?.name ?? ABSENT_AIRTABLE_FIELD_VALUE;
    }
    case FieldType.MULTIPLE_SELECTS: {
      const multipleSelectsCellValue = unknownCellValue as AirtableFieldTypesToFieldValues[typeof fieldType]['block'];

      return multipleSelectsCellValue?.map(cv => cv.name) ?? [];
    }
    case FieldType.MULTIPLE_RECORD_LINKS: {
      const multipleRecordLinksCellValue = unknownCellValue as AirtableFieldTypesToFieldValues[typeof fieldType]['block'];

      return multipleRecordLinksCellValue?.map(cv => cv.id) ?? [];
    }

    case FieldType.MULTIPLE_LOOKUP_VALUES: {
      const lookupValuesCellValue = unknownCellValue as AirtableFieldTypesToFieldValues[typeof fieldType]['block'];

      return lookupValuesCellValue?.map(cv => cv.value) ?? [];
    }

    case FieldType.MULTIPLE_COLLABORATORS: {
      const multipleCollaboratorsCellValue = unknownCellValue as AirtableFieldTypesToFieldValues[typeof fieldType]['block'];

      return multipleCollaboratorsCellValue?.map(cv => omit(cv, 'profilePicUrl')) ?? [];
    }
    case FieldType.SINGLE_COLLABORATOR:
    case FieldType.CREATED_BY:
    case FieldType.LAST_MODIFIED_BY: {
      const singleCollaboratorCellValue = unknownCellValue as AirtableFieldTypesToFieldValues[typeof fieldType]['block'];

      return omit(singleCollaboratorCellValue, 'profilePicUrl');
    }
  }
}

interface BarcodeReadFormat {
  // the text value of the barcode
  text: string;
  // the type of barcode
  type?: string;
}

type FieldValue = FieldSet[string] | BarcodeReadFormat;
export type AirtableFieldValue = FieldValue | null;

export interface AirtableRecordFieldConfig {
  name: string;
  value: AirtableFieldValue;
}

export type AirtableFieldId = string;
export type SerializedRecordFields = Record<AirtableFieldId, AirtableRecordFieldConfig>;

export const ABSENT_AIRTABLE_FIELD_VALUE = null;
export type ABSENT_AIRTABLE_FIELD_VALUE = typeof ABSENT_AIRTABLE_FIELD_VALUE;

/**
 * Data structure that exists on Record models that stores the mapping between shopify object key and airtable field id.
 * Allows us to handle scenarios where a field is deleted from a table
 */
export type AirtableRecordFieldLinks<SO extends ShopifyObject> = {
  [K in keyof SO['props']]: AirtableFieldId;
};

export function getAirtableRecordFieldLinks<SO extends ShopifyObject>(
  airtableTableFieldLinks: AirtableFieldLinks<SO>
): AirtableRecordFieldLinks<SO> {
  return keys(airtableTableFieldLinks)
    .reduce((accum, key) => {
      const shopifyObjectKey = key as keyof typeof airtableTableFieldLinks;
      const airtableTableFieldLink = airtableTableFieldLinks[shopifyObjectKey];

      /**
       * We should only be syncing when we're not missing any fields so we can assume that `id` will exist on the
       * table.airtable_field_links
       */
      accum[shopifyObjectKey] = airtableTableFieldLink.id!;

      return accum;
    }, {} as AirtableRecordFieldLinks<SO>);
}

export function serializeRecord(
  airtableRecordModel: RecordModel,
  airtableTable: SerializedAirtableTable
): SerializedAirtableRecord {
  const fields = airtableTable.fields.reduce((accum, field) => {
    const {
      id,
      name,
    } = field;

    /**
     * Very important to fall back to null, since the REST API will simply not include fields that don't have values
     * (resulting in an undefined value when the value is accessed), while empty cells access via the Block API will return null.
     * We're going with `null` as the empty value to be consistent across the board. This way when checking for property differences
     * against empty values we won't mark them as different (`null` vs `undefined`) when they're practically the same.
     */
    const restApiFieldValue = getRestApiFieldValue(
      airtableRecordModel.getCellValue(field.id),
      field.type
    ) ?? ABSENT_AIRTABLE_FIELD_VALUE;

    accum[id] = {
      name,
      value: restApiFieldValue
    };

    return accum;
  }, {} as SerializedRecordFields);

  return {
    id: airtableRecordModel.id,
    fields,
  };
}

export type SerializedRecordsDelta<SOT extends ShopifyObjectType> = {
  shopifyObjectKey: ModelPropsKey[SOT];
  fieldLink: NonNullableObject<AirtableFieldIdentifier>;
  fieldName: string;
  oldValue: AirtableFieldValue;
  newValue: AirtableFieldValue;
};

type FieldLinkIndex = keyof AirtableFieldLinks<ShopifyObjects[ShopifyObjectType]>;

/**
 * Returns the differences between two records
 * @returns 
 */
export function getSerializedRecordsDelta({
  oldRecord,
  newRecord,
  shopifyObjectType,
  airtableFieldLinks,
}: {
  oldRecord: SerializedAirtableRecord;
  newRecord: SerializedAirtableRecord;
  shopifyObjectType: ShopifyObjectType;
  airtableFieldLinks: AirtableFieldLinks<ShopifyObjects[typeof shopifyObjectType]>;
}): SerializedRecordsDelta<typeof shopifyObjectType>[] {
  const syncableShopifyObjectKeys = Object.keys(airtableFieldLinks)
    .filter((key:FieldLinkIndex) => isPresent(airtableFieldLinks[key]?.id)) as ModelPropsKey[ShopifyObjectType][];
  return syncableShopifyObjectKeys
    .map((key:FieldLinkIndex) => {
      try {
        const fieldLink = airtableFieldLinks[key] as NonNullableObject<AirtableFieldIdentifier>;
        const fieldId = fieldLink.id!;
        const oldValue = oldRecord.fields[fieldId].value;
        const newFieldConfig = newRecord.fields[fieldId];
        const newValue = newFieldConfig.value;
        const isDifferent = getAreValuesDifferent({
          value1: oldValue,
          value2: newValue,
        });

        return isDifferent ? {
          shopifyObjectKey: key,
          fieldLink,
          fieldName: newFieldConfig.name,
          oldValue,
          newValue,
        } : null;
      } catch(err) {
        const error = ono(err, {
          record1: oldRecord,
          record2: newRecord,
          airtableFieldLinks,
          syncableShopifyObjectKey: key
        });

        throw error;
      }
    })
    .filter(isPresent);
}

/**
 * @param param0 
 * @returns 
 */
function getAreValuesDifferent({
  value1,
  value2,
}: {
  value1: AirtableFieldValue;
  value2: AirtableFieldValue;
}) {
  value1 = Array.isArray(value1) ?
    (value1.length ?
      value1 :
      null
    ) :
    value1 ?? null;
  value2 = Array.isArray(value2) ?
    (value2.length ?
      value2 :
      null
    ) :
    value2 ?? null;

  return !isEqual(value1 ?? null, value2 ?? null);
}