import {
  destructureNacelleEntryId,
  reconstructContentfulEntry,
  toUnixTimestamp,
  transformArticleLists,
  transformAuthor,
  transformMedia,
  transformRelatedArticles,
  transformSanityContent,
  transformSourceEntryId
} from '.';
import type { NacelleContent } from '@nacelle/types';
import type { Content } from 'storefrontSdkV1';
import type { SourceName } from './destructureNacelleEntryId';

export interface AnyObject {
  [key: string]: AnyObject | string | unknown;
}

export interface NacelleContentEntry {
  fields?: AnyObject;
  type?: string;
  nacelleEntryId?: string;
  tags?: string[];
}

export const isObject = (x: unknown): x is AnyObject =>
  Boolean(x) && typeof x === 'object' && !Array.isArray(x);

/**
 * Determine if an object is a Nacelle v2 `Content` entry.
 */
export const isNacelleContentEntry = (entry: {
  nacelleEntryId?: string;
  type?: string;
  fields?: object;
}): entry is NacelleContentEntry =>
  isObject(entry) &&
  typeof entry.nacelleEntryId === 'string' &&
  typeof entry.type === 'string' &&
  typeof entry.fields === 'object';

interface ContentTransformers<CmsContent> {
  /** A transformer for Nacelle v2 `NacelleReference`s to top-level content */
  entries?: (x: NacelleContentEntry) => CmsContent;

  /** A transformer for arbitrary content */
  arbitraryContent?: (x: AnyObject) => CmsContent;
}

/**
 * Apply content transformation functions to content. This takes care of the boilerplate
 * associated with applying transformations to arrays and objects, which allows the
 * CMS-specific transformer utilities to be less complex.
 * @param content A single content object or array of arbitrary content objects.
 * @param transformers Functions that transform top-level content entries and arbitrary content.
 * @returns Content that's been transformed into a Nacelle v1 shape.
 */
export function applyContentTransformation<CmsContent>(
  content: AnyObject | AnyObject[],
  transformers: ContentTransformers<CmsContent>
): NacelleContentEntry | NacelleContentEntry[] | CmsContent {
  if (Array.isArray(content)) {
    return content.map(
      (entry) =>
        applyContentTransformation(entry, transformers) as NacelleContentEntry
    );
  }

  if (isObject(content)) {
    for (const property of Object.keys(content)) {
      const key = property as keyof NacelleContentEntry;

      if (isObject(content[key]) || Array.isArray(content[key])) {
        content[key] = applyContentTransformation(
          content[key] as AnyObject,
          transformers
        );
      }
    }

    if (isNacelleContentEntry(content) && transformers.entries) {
      // `content` is a reference to a top-level content entry
      return transformers.entries(content);
    } else if (transformers.arbitraryContent) {
      // `content` is arbitrary content
      return transformers.arbitraryContent(content);
    }
  }

  return content;
}

/**
 * Apply CMS-specific transformations to content.
 */
function applyCmsTransforms(data: AnyObject | AnyObject[], cms: SourceName) {
  switch (cms) {
    case 'CONTENTFUL': {
      return applyContentTransformation(data, {
        // With Contentful content, we only want to reconstruct top-level
        // content entries (from `NacelleReference`s) with `sys` metadata.
        // Any other flattening and transformation is handled by other
        // (more specialized) transformers like `transformAuthor`.
        entries: reconstructContentfulEntry
      });
    }
    case 'SANITY': {
      return applyContentTransformation(data, {
        // With Sanity content, we can apply the same transformation to
        // top-level content entries (from `NacelleReference`s) and arbitrary
        // content data.
        arbitraryContent: transformSanityContent,
        entries: transformSanityContent
      });
    }
    default: {
      return data;
    }
  }
}

export function transformContent(
  entries: Content[],
  locale: string
): NacelleContent[] {
  return entries
    .filter((entry) => entry.handle)
    .map((entry) => {
      const {
        title,
        handle,
        createdAt,
        updatedAt,
        indexedAt,
        type,
        sourceEntryId,
        nacelleEntryId
      } = entry;
      const { sourceName: cms } = destructureNacelleEntryId(nacelleEntryId);

      const {
        articles = null,
        author = null,
        blogHandle = null,
        collectionHandle = null,
        content = null,
        contentHtml = null,
        description = null,
        excerpt = null,
        featuredMedia = null,
        publishDate = null,
        relatedArticles = null,
        sections = null,
        tags = null,
        ...fields
      } = entry.fields || {};

      const contentIsString = typeof content === 'string';

      if (content && !contentIsString && typeof content === 'object') {
        fields.content = content;
      }

      return {
        articleLists: transformArticleLists(articles, blogHandle, locale),
        author: transformAuthor(author, cms),
        blogHandle,
        cmsSyncSource: cms.toLowerCase(),
        cmsSyncSourceDomain: '',
        cmsSyncSourceContentId: transformSourceEntryId(sourceEntryId),
        collectionHandle,
        content: contentIsString ? content : null,
        contentHtml,
        createdAt,
        description,
        excerpt,
        featuredMedia: transformMedia(featuredMedia, cms),
        fields: applyCmsTransforms(fields, cms),
        globalHandle: `${handle}::${locale}`,
        handle: handle as string,
        id: nacelleEntryId,
        indexedAt: indexedAt || 0,
        locale,
        publishDate: toUnixTimestamp(publishDate) as number,
        relatedArticles: transformRelatedArticles(relatedArticles, cms),
        sections: applyCmsTransforms(sections, cms),
        tags,
        title,
        type: type || '',
        updatedAt
      };
    });
}
