import { get, isEqual, isArray } from 'lodash';

import { PageDSL, isValidStepDef, ValidMultiStepDef, StepDef } from '../types/index.js';
import { FlattenedWidgetProps } from '../utils/normalize.js';
import { refactorNameInString, refactorNameInValue, ScopedDataTree } from './refactor.js';
import { createTriggerStepRenamer } from './rename.js';
import { Extractor } from './types.js';

export type PropertyUpdate = { propertyName: string; propertyValue: unknown; entityId: string };

const PAGE_WIDGET_ID = '0';

export async function getUpdatesInWidgets<T extends Record<string, FlattenedWidgetProps>>({
  widgets,
  oldName,
  newName,
  dataTree,
  extractPairs,
  namespace
}: {
  widgets: T;
  oldName: string;
  newName: string;
  dataTree: ScopedDataTree;
  extractPairs: Extractor;
  namespace?: string;
}): Promise<PropertyUpdate[]> {
  let updates: PropertyUpdate[] = [];

  for (const [widgetId, widget] of Object.entries(widgets)) {
    if (widget.widgetName === oldName) {
      updates.push({
        entityId: widgetId,
        propertyName: 'widgetName',
        propertyValue: newName
      });

      // if it's a table, rename in all of the cached columns (which aren't dynamic)
      if (widget.type === 'TABLE_WIDGET') {
        const tableWidget = widget as typeof widget & { cachedColumnSettings: Record<string, Record<string, string>> };

        const cachedSettings = Object.keys(tableWidget.cachedColumnSettings ?? {}).reduce((accum, key) => {
          Object.entries(tableWidget.cachedColumnSettings[key]).forEach(([k, v]) => {
            if (typeof v === 'string') {
              accum.push(`cachedColumnSettings.${key}.${k}`);
            }
          });
          return accum;
        }, [] as string[]);
        await Promise.all(
          cachedSettings.map(async (key: string) => {
            const binding = get(widget, key) as string;
            const withNewName = await refactorNameInString({
              value: binding,
              oldName,
              newName,
              dataTree,
              namespace,
              extractPairs
            });
            if (binding !== withNewName) {
              updates.push({
                entityId: widgetId,
                propertyName: key,
                propertyValue: withNewName
              });
            }
          })
        );
      }
    }

    if (widget.dynamicBindingPathList) {
      await Promise.all(
        widget.dynamicBindingPathList.map(async ({ key }) => {
          const binding = get(widget, key);
          const withNewName = await refactorNameInValue({
            oldValue: binding,
            oldName,
            newName,
            dataTree,
            namespace,
            extractPairs
          });
          if (!isEqual(binding, withNewName)) {
            updates.push({
              entityId: widgetId,
              propertyName: key,
              propertyValue: withNewName
            });
          }
        })
      );
    }

    if (widget.dynamicTriggerPathList) {
      const renameInTriggerStep = createTriggerStepRenamer({
        oldName,
        newName,
        dataTree,
        addUpdate: (payload) => {
          updates.push({
            ...payload,
            entityId: widgetId
          });
        },
        extractEvaluationPairs: extractPairs,
        namespace
      });

      await Promise.all(
        widget.dynamicTriggerPathList.map(async ({ key }) => {
          const trigger: StepDef | undefined = get(widget, key) as StepDef | undefined;
          if (!isArray(trigger)) {
            return;
          }
          await Promise.all(
            trigger.map(async (step: StepDef, index) => {
              if (!isValidStepDef(step)) return;
              await renameInTriggerStep(step, key, index);
            })
          );
        })
      );
    }

    // Canvas widgets also contain API response info
    if (widgetId === PAGE_WIDGET_ID && 'apis' in widget) {
      const apiUpdates = await updateNameInEntityMap({
        entityMap: widget.apis as PageDSL['apis'],
        parentId: widgetId,
        parentKey: 'apis',
        oldName,
        newName,
        dataTree,
        extractPairs,
        namespace
      });

      updates = [...updates, ...apiUpdates];
    }
  }

  return updates;
}

export async function updateNameInEntityMap({
  entityMap,
  parentKey,
  parentId = '',
  oldName,
  newName,
  dataTree,
  extractPairs,
  namespace
}: {
  entityMap: PageDSL['events'] | PageDSL['apis'] | PageDSL['timers'] | PageDSL['stateVars'] | undefined;
  parentId?: string;
  parentKey: string;
  oldName: string;
  newName: string;
  dataTree: ScopedDataTree;
  extractPairs: Extractor;
  namespace?: string;
}): Promise<PropertyUpdate[]> {
  if (!entityMap) return [];

  const updates: PropertyUpdate[] = [];

  const renameInTriggerStep = createTriggerStepRenamer({
    oldName,
    newName,
    dataTree,
    addUpdate: (payload) => {
      updates.push({
        ...payload,
        entityId: parentId
      });
    },
    extractEvaluationPairs: extractPairs,
    namespace
  });

  if ('timerMap' in entityMap) {
    const timers = entityMap.timerMap;
    await Promise.all(
      Object.values(timers).map(async (timer) => {
        if (timer.name === oldName) {
          updates.push({
            propertyName: `${parentKey}.timerMap.${timer.id}.name`,
            propertyValue: newName,
            entityId: timer.id
          });
        }
        await Promise.all(
          timer?.steps?.map(async (step, index) => {
            if (!isValidStepDef(step)) return;
            const mappedKey = `${parentKey}.timerMap.${timer.id}.steps`;
            await renameInTriggerStep(step, mappedKey, index);
          })
        );
      })
    );
  } else if ('apiMap' in entityMap) {
    const apis = entityMap.apiMap;
    for (const api of Object.values(apis)) {
      await Promise.all(
        (api.dynamicTriggerPathList ?? [])?.map(async ({ key }) => {
          const mappedKey = `${parentKey}.apiMap.${api.id}.${key}`;
          const triggers = get(api, key, []) as ValidMultiStepDef;
          await Promise.all(
            triggers.map(async (step: StepDef, index) => {
              if (!isValidStepDef(step)) return;
              await renameInTriggerStep(step, mappedKey, index);
            })
          );
        })
      );
    }
  } else if ('stateVarMap' in entityMap) {
    const stateVars = entityMap.stateVarMap;
    for (const stateVar of Object.values(stateVars)) {
      if (stateVar.name === oldName) {
        updates.push({
          propertyName: `${parentKey}.stateVarMap.${stateVar.id}.name`,
          propertyValue: newName,
          entityId: stateVar.id
        });
      }
      if (stateVar.dynamicBindingPathList) {
        await Promise.all(
          stateVar.dynamicBindingPathList.map(async ({ key }) => {
            const value = get(stateVar, key);
            const newValue = await refactorNameInValue({
              oldValue: value,
              oldName,
              newName,
              dataTree,
              namespace,
              extractPairs
            });
            if (!isEqual(value, newValue)) {
              updates.push({
                entityId: parentId ?? stateVar.id,
                propertyName: `${parentKey}.stateVarMap.${stateVar.id}.defaultValue`,
                propertyValue: newValue
              });
            }
          })
        );
      }
    }
  }

  return updates;
}
