import { useMemo } from 'react';
import Debug from 'debug';
import { cloneDeep, difference, isEqual } from 'lodash-es';

import type { ItemConfig } from '~/lib/songwhipApi/types';
import type { ItemContext } from '../types';

import ApiError from '~/lib/errors/ApiError';
import { useAppAlert, useAppToast } from '~/src/components/NextApp/lib/CoreUi';
import useBeforeUnload from '~/src/hooks/useBeforeUnload';
import useFetchSessionUser from '~/src/hooks/useFetchSessionUser';
import { useI18n } from '~/src/lib/i18n';
import { useAppRouter } from '~/src/lib/router2';
import { reportError } from '~/src/lib/sentry';
import { useTracker } from '~/src/lib/tracker/useTracker';
import onceIdle from '~/src/lib/utils/onceIdle';
import { NotificationType } from '../../Notification';
import { WriteConflict } from './components/WriteConflict';
import {
  deleteUnusedImages,
  diffItemConfig,
  removeUnusedCustomObjects,
} from './utils';

const IS_DEV = process.env.NODE_ENV === 'development';
const debug = Debug('songwhip/components/ItemPageEdit');

const useOnSave = ({
  itemContext,
  onSave,
  pagePath,
}: {
  itemContext: ItemContext;
  onSave: (params: ItemConfig) => Promise<void | string>;
  pagePath: string;
}) => {
  const { t } = useI18n();
  const originalItemContext = itemContext.originalItemContext;

  if (!originalItemContext) {
    throw new Error('originalItemContext is required for useOnSave');
  }

  const layoutState = itemContext.layout;
  const { trackEvent } = useTracker();
  const router = useAppRouter();
  const toast = useAppToast();
  const appAlert = useAppAlert();
  const { userAccountId } = useFetchSessionUser();

  // We can clean up any custom objects that are not being used in the layout,
  // this means we can trust that the union of links (default + custom) are accurate.
  // As this runs on each layout change we clone the itemContext to avoid cleaning
  // any unassigned custom object that the user may want to place into the layout.
  const itemContextCleaned = removeUnusedCustomObjects(cloneDeep(itemContext));

  const params = diffItemConfig(originalItemContext, itemContextCleaned);
  const hasChanges = !!Object.keys(params).length;

  // Show native unload dialog warning when user closes tab w/ unsaved changes.
  useBeforeUnload({
    // don't run in development conflicts with live-reload/refresh.
    enabled: !IS_DEV && hasChanges,
  });

  return useMemo(() => {
    debug('change', {
      hasChanges,
      params,
    });

    return {
      hasChanges,

      async save({
        dryRun,
      }: {
        /**
         * Use `dryRun: true` to see if the save routine will make changes.
         * We use this when the user presses the back button to check if
         * changes have been made and if we need them to confirm.
         */
        dryRun?: boolean;
      } = {}): Promise<{ hasChanges: boolean }> {
        debug('on save click', { layoutState, hasChanges });

        if (!dryRun && hasChanges) {
          // save if there are changes
          try {
            const featuresDiff = toFeaturesDiff(
              originalItemContext,
              itemContext
            );

            debug('saving layout state', params, {
              featuresDiff,
            });

            const customToastText = await onSave(params);

            toast({
              text: customToastText ?? t('itemEdit.events.changesPublished'),
              timeoutSecs: 5,
            });

            trackEvent({
              type: 'save',
              subType: 'itemPage:saveChanges',
            });

            featuresDiff.added.forEach((feature) => {
              trackEvent({
                type: 'add-page-feature',
                subType: feature,
              });
            });

            featuresDiff.edited.forEach((feature) => {
              trackEvent({
                type: 'edit-page-feature',
                subType: feature,
              });
            });

            featuresDiff.removed.forEach((feature) => {
              trackEvent({
                type: 'remove-page-feature',
                subType: feature,
              });
            });

            // We attempt to delete songwhip-images that have been removed from the page in the
            // edit session. There are still cases where we might not delete images that are
            // no longer in use. For example if the user adds a Merch Product and then closes
            // the browser tab without saving, there's not much we can do to cleanup. Redundant
            // images on S3 isn't a huge probably in the grand scheme of things. Erroneously
            // cleaning up images that are still in use is much worse.
            onceIdle(async () => {
              try {
                await deleteUnusedImages({
                  prevContext: originalItemContext,
                  nextContext: itemContextCleaned,
                  accountId: userAccountId,
                });
              } catch (error) {
                debug('error deleting unused images', error);
                reportError({ error });
              }
            });

            router.push(pagePath);
          } catch (error) {
            debug('error saving item page edits', error);
            reportError({ error });

            // When the database record has been changed since the client last fetched
            // it we cannot reliably save the users changes. Refreshing the app will
            // pull in the latest record and allow the user to try again.
            if (
              error instanceof ApiError &&
              error.code === 'ITEM_WRITE_CONFLICT'
            ) {
              appAlert({
                title: t('itemEdit.writeConflictError.title'),
                content: (
                  <WriteConflict
                    message={t('itemEdit.writeConflictError.message')}
                    ctaText={t('itemEdit.writeConflictError.action')}
                  />
                ),
              });
            }

            toast({
              text: t('itemEdit.events.failedToPublishChanges'),
              type: NotificationType.ERROR,
              timeoutSecs: 30,
            });
          }
        } else if (!dryRun) {
          router.push(pagePath);
        }

        return {
          hasChanges,
        };
      },
    };
  }, [itemContext, onSave]);
};

const toFeaturesDiff = (
  originalItemContext: ItemContext,
  nextItemContext: ItemContext
) => {
  const originalSections = originalItemContext.layout.main.map(
    ({ component, preset }) => (component || preset) as string
  );

  const originalAddons = Object.keys(originalItemContext.addons);
  const originalFeatures = [...originalSections, ...originalAddons];

  const nextSections = nextItemContext.layout.main.map(
    ({ component, preset }) => (component || preset) as string
  );

  const nextAddons = Object.keys(nextItemContext.addons);
  const nextFeatures = [...nextSections, ...nextAddons];

  // REVIEW: Currently we are not tracking updates for sections without preset
  // cause we cannot distinguish the changes between them without unique ids.
  // We might need to introduce unique ids for sections
  const editedSections = originalItemContext.layout.main.flatMap(
    (originalSection) => {
      const sectionPreset = originalSection.preset;

      const nextSection = nextItemContext.layout.main.find(
        ({ preset }) => sectionPreset === preset
      );

      if (
        !sectionPreset ||
        !nextSection ||
        isEqual(originalSection, nextSection)
      ) {
        return [];
      }

      return [sectionPreset as string];
    }
  );

  const editedAddons = Object.entries(originalItemContext.addons).flatMap(
    ([originalAddonKey, originalAddon]) => {
      const nextAddon = nextItemContext.addons[originalAddonKey];

      return isEqual(originalAddon, nextAddon)
        ? []
        : [originalAddonKey as string];
    }
  );

  return {
    added: difference(nextFeatures, originalFeatures),
    edited: [...editedSections, ...editedAddons],
    removed: difference(originalFeatures, nextFeatures),
  };
};

export default useOnSave;
