import { isArray, isEqual, isObject, isString } from 'lodash';
import { StringSegment, getDynamicStringSegments, isExactBinding, joinStringWithBindings } from '../bindings/index.js';
import { LanguagePluginID } from '../types/plugin/language.js';
import { Extractor } from './types.js';

export type ScopedDataTree = { [entityName: string]: unknown };

const splitStringWithBindings = (value: string): StringSegment[] => {
  const stringSegments = getDynamicStringSegments(value.trim());

  return stringSegments.map((segment) => {
    const length = segment.length;

    if (isExactBinding(segment)) {
      return { dynamic: true, value: segment.substring(2, length - 2) };
    }
    return { dynamic: false, value: segment };
  });
};

export async function refactorNameInCode({
  value,
  oldName,
  newName,
  dataTree,
  extractPairs,
  suffixes,
  namespace
}: {
  value: string;
  oldName: string;
  newName: string;
  dataTree: ScopedDataTree;
  extractPairs: Extractor;
  suffixes?: Array<string>;
  namespace?: string;
}) {
  let pairs;

  if (namespace) {
    const namespacedEntitiesToExtract = {
      [namespace]: new Set([oldName])
    };
    pairs = await extractPairs(value, new Set(), dataTree, namespacedEntitiesToExtract);
  } else {
    pairs = await extractPairs(value, new Set([oldName]), dataTree);
  }
  let position = 0;
  let result = '';

  for (const { binding, range } of pairs) {
    if (suffixes) {
      // only refactor if the code after the binding matches a suffix
      const restOfCode = value.slice(range.start + binding.length);
      if (!suffixes.some((suffix) => restOfCode.startsWith(suffix))) {
        result += value.slice(position, range.end + 1);
        position = range.end + 1;
        continue;
      }
    }
    result += value.slice(position, range.start) + newName;
    position = range.start + binding.length;
  }

  return result + value.slice(position);
}

async function refactorNameInSegment({
  segment,
  oldName,
  newName,
  dataTree,
  extractPairs,
  namespace
}: {
  segment: StringSegment;
  oldName: string;
  newName: string;
  dataTree: ScopedDataTree;
  extractPairs: Extractor;
  namespace?: string;
}) {
  if (segment.dynamic) {
    const value = await refactorNameInCode({
      value: segment.value,
      oldName,
      newName,
      dataTree,
      extractPairs,
      namespace
    });

    if (value === null) {
      return null;
    }

    return { dynamic: true, value };
  }

  return segment;
}

export async function refactorNameInString({
  value,
  oldName,
  newName,
  dataTree,
  extractPairs,
  namespace
}: {
  value: string;
  oldName: string;
  newName: string;
  dataTree: ScopedDataTree;
  extractPairs: Extractor;
  namespace?: string;
}) {
  const segments = splitStringWithBindings(value);

  const newSegments = await Promise.all(
    segments.map((segment) => refactorNameInSegment({ segment, oldName, newName, dataTree, extractPairs, namespace }))
  );

  if (newSegments.filter((segment) => segment === null).length) {
    return value;
  }

  return joinStringWithBindings(newSegments.filter((segment): segment is StringSegment => segment !== null));
}

export async function refactorNameInValue({
  oldValue,
  oldName,
  newName,
  dataTree,
  extractPairs,
  namespace,
  includeNestedObjects
}: {
  oldValue: unknown;
  oldName: string;
  newName: string;
  dataTree: ScopedDataTree;
  extractPairs: Extractor;
  namespace?: string;
  includeNestedObjects?: boolean;
}) {
  if (isArray(oldValue)) {
    return Promise.all(
      oldValue.map(async (value) => {
        if (typeof value === 'string') {
          return refactorNameInString({
            value,
            oldName,
            newName,
            dataTree,
            extractPairs,
            namespace
          });
        } else if (includeNestedObjects && value !== null && typeof value === 'object') {
          return refactorNameInValue({
            oldValue: value,
            oldName,
            newName,
            dataTree,
            extractPairs,
            namespace,
            includeNestedObjects: true
          });
        } else {
          return value;
        }
      })
    );
  } else if (isString(oldValue)) {
    return refactorNameInString({
      value: oldValue,
      oldName,
      newName,
      dataTree,
      extractPairs,
      namespace
    });
  } else if (isObject(oldValue)) {
    const entries = await Promise.all(
      Object.entries(oldValue).map(async ([key, value]) => {
        const newValue: unknown = await refactorNameInValue({
          oldValue: value,
          oldName,
          newName,
          dataTree,
          extractPairs,
          namespace,
          includeNestedObjects
        });
        return [key, newValue];
      })
    );
    return Object.fromEntries(entries);
  }

  return oldValue;
}

export function createFakeDataTree(dsls: Record<string, { name?: string; widgetName?: string }>[]): Record<string, unknown> {
  const fakeDataTree = {};
  for (const dsl of dsls) {
    Object.values(dsl).forEach((entity) => {
      const name = entity.name || entity.widgetName;
      if (!name) {
        return;
      }
      fakeDataTree[name] = {};
    });
  }
  return fakeDataTree;
}

export async function refactorNameInStep({
  oldName,
  newName,
  dataTree,
  suffixes,
  namespace,
  configuration,
  pluginId,
  extractor
}: {
  oldName: string;
  newName: string;
  dataTree: ScopedDataTree;
  suffixes?: string[];
  namespace?: string;
  pluginId?: string;
  configuration: Record<string, unknown>;
  extractor: Extractor;
}) {
  for (const [key, value] of Object.entries(configuration)) {
    if (isString(value)) {
      let newValue: string | null = null;
      // Code body's vs plain text bodies (JS, Python vs HTTP, etc)
      if (key === 'body' && (pluginId === LanguagePluginID.JavaScript || pluginId === LanguagePluginID.Python)) {
        newValue = await refactorNameInCode({
          value,
          oldName,
          newName,
          dataTree,
          extractPairs: extractor,
          suffixes,
          namespace
        });
      } else {
        newValue = await refactorNameInString({
          value,
          oldName,
          newName,
          dataTree,
          extractPairs: extractor,
          namespace
        });
      }

      if (!isEqual(newValue, value)) {
        configuration[key] = newValue;
      }
    } else if (typeof value === 'object' && value !== null) {
      configuration[key] = await refactorNameInValue({
        oldValue: value,
        oldName,
        newName,
        dataTree,
        extractPairs: extractor,
        namespace,
        includeNestedObjects: true
      });
    }
  }
}
