import * as t from '@babel/types';
import R from 'ramda';

import type { NodePath } from '@babel/traverse';
import {
  SymbolResolver,
  TranspilerCubeResolver,
  TranspilerInterface,
  TranspilerSymbolResolver,
  TraverseObject
} from './transpiler.interface';

/* this list was generated by getTransformPatterns() with additional variants for snake_case */
export const transpiledFieldsPatterns: Array<RegExp> = [
  /\.sql$/,
  /sql$/,
  /(sqlTable|sql_table)$/,
  /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(drillMemberReferences|drillMembers|drill_members)$/,
  /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(orderBy|order_by)\.[0-9]+\.sql$/,
  /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeShift|time_shift)\.[0-9]+\.(timeDimension|time_dimension)$/,
  /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/,
  /^(measures|dimensions)\.[_a-zA-Z][_a-zA-Z0-9]*\.filter\.(exclude|keepOnly|keep_only)$/,
  /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.grain\.(exclude|keepOnly|keep_only|include)$/,
  /^(measures|dimensions)\.[_a-zA-Z][_a-zA-Z0-9]*\.case\.switch$/,
  /^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by|key)$/,
  /^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.indexes\.[_a-zA-Z][_a-zA-Z0-9]*\.columns$/,
  /^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeDimensionReference|timeDimension|time_dimension|segments|dimensions|measures|rollups|segmentReferences|dimensionReferences|measureReferences|rollupReferences)$/,
  /^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeDimensions|time_dimensions)\.\d+\.dimension$/,
  /^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.(outputColumnTypes|output_column_types)\.\d+\.member$/,
  /^contextMembers$/,
  /^views$/,
  /^(viewGroup|view_group)$/,
  /^(viewGroups|view_groups)$/,
  /^includes$/,
  /^excludes$/,
  /^hierarchies\.[_a-zA-Z][_a-zA-Z0-9]*\.levels$/,
  /^cubes\.[0-9]+\.(joinPath|join_path)$/,
  /^folders\.[0-9]+\.includes\.[0-9]+\.(joinPath|join_path)$/,
  /^(accessPolicy|access_policy)\.[0-9]+\.(rowLevel|row_level)\.filters\.[0-9]+.*\.member$/,
  /^(accessPolicy|access_policy)\.[0-9]+\.(rowLevel|row_level)\.filters\.[0-9]+.*\.values$/,
  /^(accessPolicy|access_policy)\.[0-9]+\.conditions.[0-9]+\.if$/,
  /^(defaultFilters|default_filters)\.[0-9]+\.member$/,
  /^(defaultFilters|default_filters)\.[0-9]+\.values$/,
  /^(defaultFilters|default_filters)\.[0-9]+\.unless$/,
  /^(measures|dimensions)\.[_a-zA-Z][_a-zA-Z0-9]*\.mask\.sql$/,
  /^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.links\.[0-9]+\.url$/,
  /^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.links\.[0-9]+\.params\.[0-9]+\.value$/,
];

export const transpiledFields: Set<String> = new Set<String>();

transpiledFieldsPatterns?.forEach((r) => {
  const fields = r.toString().replace(/.*?([_a-zA-Z|][_a-zA-Z0-9|]*)([^_a-zA-Z0-9|]*)$/, '$1').split('|');
  fields.forEach((f) => transpiledFields.add(f));
});

export class CubePropContextTranspiler implements TranspilerInterface {
  public constructor(
    protected readonly cubeSymbols: TranspilerSymbolResolver,
    protected readonly cubeDictionary: TranspilerCubeResolver,
    protected readonly viewCompiler: TranspilerSymbolResolver,
  ) {
  }

  public traverseObject(): TraverseObject {
    return {
      CallExpression: (path) => {
        if (t.isIdentifier(path.node.callee)) {
          const args = path.get('arguments');
          if (path.node.callee.name === 'cube' || path.node.callee.name === 'view') {
            if (args?.[args.length - 1]) {
              const cubeName = args[0].node.type === 'StringLiteral' && args[0].node.value ||
                args[0].node.type === 'TemplateLiteral' &&
                args[0].node.quasis.length &&
                args[0].node.quasis[0].value.cooked;
              args[args.length - 1].traverse(this.sqlAndReferencesFieldVisitor(cubeName as string));
              args[args.length - 1].traverse(
                this.knownIdentifiersInjectVisitor('extends', name => this.cubeDictionary.resolveCube(name))
              );
            }
          } else if (path.node.callee.name === 'context') {
            args[args.length - 1].traverse(this.sqlAndReferencesFieldVisitor(null));
          } else if (path.node.callee.name === 'view_group') {
            args[args.length - 1].traverse(this.viewGroupReferencesVisitor());
          }
        }
      }
    };
  }

  protected transformObjectProperty(path: NodePath<t.ObjectProperty>, resolveSymbol: SymbolResolver) {
    CubePropContextTranspiler.replaceValueWithArrowFunction(resolveSymbol, path.get('value'));
  }

  public static replaceValueWithArrowFunction(resolveSymbol: (name: string) => any, value: NodePath<any>) {
    // If the current value is already an arrow function, update its parameters and keep the body
    if (t.isArrowFunctionExpression(value.node)) {
      const bodyPath = value.get('body') as NodePath<any>;
      const knownIds = CubePropContextTranspiler.collectKnownIdentifiersAndTransform(
        resolveSymbol,
        bodyPath,
      );

      value.replaceWith(
        t.arrowFunctionExpression(
          knownIds.map(i => t.identifier(i)),
          value.node.body,
          false,
        ),
      );
    } else {
      const knownIds = CubePropContextTranspiler.collectKnownIdentifiersAndTransform(
        resolveSymbol,
        value,
      );

      value.replaceWith(
        t.arrowFunctionExpression(
          knownIds.map(i => t.identifier(i)),
          // @todo Replace any with assert expression
          <any>value.node,
          false,
        ),
      );
    }
  }

  /**
   * Visitor for the `view_group(...)` definition object. The legacy `views`
   * array is wrapped as a whole (flat references), while `includes` is wrapped
   * per-leaf so that nested view group definitions stay structurally intact and
   * only their view references become resolvable arrow functions. This mirrors
   * how nested folders preserve their structure.
   */
  protected viewGroupReferencesVisitor(): TraverseObject {
    const resolveSymbol = n => this.viewCompiler.resolveSymbol(null, n) ||
      this.cubeSymbols.resolveSymbol(null, n) ||
      this.cubeSymbols.isCurrentCube(n);

    const keyName = (node: t.ObjectProperty): string | undefined => {
      if (node.key.type === 'Identifier') return node.key.name;
      if (node.key.type === 'StringLiteral') return node.key.value;
      return undefined;
    };

    const wrapIncludes = (valuePath: NodePath<any>) => {
      if (!valuePath.isArrayExpression()) {
        return;
      }
      for (const element of valuePath.get('elements')) {
        if (element.isObjectExpression()) {
          // Nested view group: recurse into its own `includes` only.
          for (const prop of element.get('properties')) {
            if (prop.isObjectProperty() && keyName(prop.node) === 'includes') {
              wrapIncludes(prop.get('value') as NodePath<any>);
            }
          }
        } else if (element.isIdentifier() || element.isStringLiteral()) {
          CubePropContextTranspiler.replaceValueWithArrowFunction(resolveSymbol, element);
        }
      }
    };

    return {
      ObjectProperty: (path) => {
        const key = keyName(path.node);
        if (key !== 'includes' && key !== 'views') {
          return;
        }
        // Only handle the top-level definition properties here; nested
        // `includes` arrays are reached via manual recursion above.
        if (CubePropContextTranspiler.fullPath(path) !== key) {
          return;
        }
        if (key === 'views') {
          this.transformObjectProperty(path, resolveSymbol);
        } else {
          wrapIncludes(path.get('value') as NodePath<any>);
        }
      }
    };
  }

  protected sqlAndReferencesFieldVisitor(cubeName: string | null | undefined): TraverseObject {
    const resolveSymbol = n => this.viewCompiler.resolveSymbol(cubeName, n) ||
      this.cubeSymbols.resolveSymbol(cubeName, n) ||
      this.cubeSymbols.isCurrentCube(n);

    return {
      ObjectProperty: (path) => {
        if (((path.node.key.type === 'Identifier' && path.node.key.name === 'joins') ||
          (path.node.key.type === 'StringLiteral' && path.node.key.value === 'joins'))
          && t.isObjectExpression(path.node.value)) {
          const fullPath = CubePropContextTranspiler.fullPath(path);
          if (fullPath === 'joins') {
            this.convertJoinsObjectToArray(path);
          }
        }

        if ((path.node.key.type === 'Identifier' && transpiledFields.has(path.node.key.name)) ||
          (path.node.key.type === 'StringLiteral' && transpiledFields.has(path.node.key.value))) {
          const fullPath = CubePropContextTranspiler.fullPath(path);
          // eslint-disable-next-line no-restricted-syntax
          for (const p of transpiledFieldsPatterns) {
            if (fullPath.match(p)) {
              this.transformObjectProperty(path, resolveSymbol);
              return;
            }
          }
        }
      }
    };
  }

  protected convertJoinsObjectToArray(path: NodePath<t.ObjectProperty>) {
    const valuePath = path.get('value');
    if (!valuePath.isObjectExpression()) {
      return;
    }

    const elements: t.ObjectExpression[] = [];

    for (const prop of valuePath.get('properties')) {
      if (!prop.isObjectProperty()) {
        return;
      }
      const keyNode = prop.node.key;
      const valueNode = prop.node.value;

      if (
        (t.isIdentifier(keyNode) || t.isStringLiteral(keyNode)) &&
        t.isObjectExpression(valueNode)
      ) {
        const nameProp = t.objectProperty(
          t.identifier('name'),
          t.stringLiteral(t.isIdentifier(keyNode) ? keyNode.name : keyNode.value)
        );

        const newProps = [nameProp, ...valueNode.properties];
        const objectExpr = t.objectExpression(newProps);
        elements.push(objectExpr);
      }
    }

    valuePath.replaceWith(t.arrayExpression(elements));
  }

  protected static fullPath(path: NodePath): string {
    // @ts-ignore
    let fp = path?.node?.key?.name || path?.node?.key?.value || '';
    let pp: NodePath<t.Node> | null | undefined = path?.parentPath;
    while (pp) {
      if (pp?.parentPath?.node?.type === 'ArrayExpression') {
        fp = `0.${fp}`;
        pp = pp?.parentPath;
        // @ts-ignore
      } else if (pp?.parentPath?.node?.key?.type === 'Identifier') {
        // @ts-ignore
        fp = `${pp?.parentPath?.node?.key?.name || '0'}.${fp}`;
        pp = pp?.parentPath?.parentPath;
        // @ts-ignore
      } else if (pp?.parentPath?.node?.key?.type === 'StringLiteral') {
        // @ts-ignore
        fp = `${pp?.parentPath?.node?.key?.value || '0'}.${fp}`;
        pp = pp?.parentPath?.parentPath;
      } else break;
    }

    return fp;
  }

  protected knownIdentifiersInjectVisitor(field: RegExp | string, resolveSymbol: SymbolResolver): TraverseObject {
    return {
      ObjectProperty: (path) => {
        if ((path.node.key.type === 'Identifier' && path.node.key.name.match(field)) ||
          (path.node.key.type === 'StringLiteral' && path.node.key.value.match(field))) {
          this.transformObjectProperty(path, resolveSymbol);
        }
      }
    };
  }

  protected static collectKnownIdentifiersAndTransform(resolveSymbol: SymbolResolver, path: NodePath): string[] {
    const identifiers: string[] = [];
    const isAccessPolicy = this.isAccessPolicyPath(path);

    if (path.node.type === 'Identifier') {
      CubePropContextTranspiler.transformCubeCloudShorthandIdentifier(path as NodePath<t.Identifier>, identifiers, isAccessPolicy, resolveSymbol);
      if (path.node.type === 'Identifier') {
        CubePropContextTranspiler.matchAndTransformIdentifier(path, resolveSymbol, identifiers);
      }
    }

    path.traverse({
      Identifier: (p) => {
        CubePropContextTranspiler.transformCubeCloudShorthandIdentifier(p, identifiers, isAccessPolicy, resolveSymbol);
        CubePropContextTranspiler.matchAndTransformIdentifier(p, resolveSymbol, identifiers);
      },
      MemberExpression: (p) => {
        CubePropContextTranspiler.transformCubeCloudShorthandMemberExpression(p, isAccessPolicy, resolveSymbol);
      }
    });

    return R.uniq(identifiers);
  }

  protected static matchAndTransformIdentifier(path, resolveSymbol: SymbolResolver, identifiers: string[]) {
    if (
      (!path.parent ||
        (path.parent.type !== 'MemberExpression' || path.parent.type === 'MemberExpression' && path.key !== 'property')
      ) &&
      resolveSymbol(path.node.name)
    ) {
      identifiers.push(path.node.name);
    }
  }

  private static readonly CUBE_CLOUD_SHORTHAND_IDENTIFIERS = ['userAttributes', 'user_attributes', 'groups'];

  private static isAccessPolicyPath(path: NodePath): boolean {
    // @ts-ignore
    const target = (!path?.node?.key && path?.parentPath && t.isObjectProperty(path.parentPath.node))
      ? path.parentPath
      : path;
    const fp = this.fullPath(target);
    return fp.startsWith('accessPolicy') || fp.startsWith('access_policy');
  }

  private static securityContextIdentifier(isAccessPolicy: boolean): t.Identifier {
    return t.identifier(isAccessPolicy ? 'securityContext' : 'SECURITY_CONTEXT');
  }

  private static isShadowedByFunctionParam(name: string, path: NodePath): boolean {
    let current: NodePath | null = path.parentPath;
    while (current) {
      const { node } = current;
      if (
        (t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) &&
        node.params.some(p => t.isIdentifier(p) && p.name === name)
      ) {
        return true;
      }
      current = current.parentPath;
    }
    return false;
  }

  protected static transformCubeCloudShorthandIdentifier(path: NodePath<t.Identifier>, identifiers: string[], isAccessPolicy: boolean, resolveSymbol: SymbolResolver) {
    if (!this.CUBE_CLOUD_SHORTHAND_IDENTIFIERS.includes(path.node.name)) {
      return;
    }
    if (resolveSymbol(path.node.name)) {
      return;
    }
    if (
      path.parent &&
      (path.parent.type === 'MemberExpression' || path.parent.type === 'OptionalMemberExpression') &&
      path.key === 'property'
    ) {
      return;
    }
    if (this.isShadowedByFunctionParam(path.node.name, path)) {
      return;
    }
    const contextId = this.securityContextIdentifier(isAccessPolicy);
    const cubeCloud = t.memberExpression(contextId, t.identifier('cubeCloud'));
    const prop = path.node.name === 'user_attributes' ? 'userAttributes' : path.node.name;
    const newExpr = t.memberExpression(cubeCloud, t.identifier(prop));
    path.replaceWith(newExpr);
    identifiers.push(contextId.name);
  }

  protected static transformCubeCloudShorthandMemberExpression(path: NodePath<t.MemberExpression>, isAccessPolicy: boolean, resolveSymbol: SymbolResolver) {
    if (
      t.isIdentifier(path.node.object) &&
      this.CUBE_CLOUD_SHORTHAND_IDENTIFIERS.includes(path.node.object.name) &&
      !resolveSymbol(path.node.object.name) &&
      !this.isShadowedByFunctionParam(path.node.object.name, path)
    ) {
      const contextId = this.securityContextIdentifier(isAccessPolicy);
      const cubeCloud = t.memberExpression(contextId, t.identifier('cubeCloud'));
      const prop = path.node.object.name === 'user_attributes' ? 'userAttributes' : path.node.object.name;
      const shorthand = t.memberExpression(cubeCloud, t.identifier(prop));
      const newMemberExpression = t.memberExpression(shorthand, path.node.property, path.node.computed);
      path.replaceWith(newMemberExpression);
    } else if (
      (t.isMemberExpression(path.node.object) || t.isOptionalMemberExpression(path.node.object)) &&
      t.isIdentifier(path.node.object.property, { name: 'user_attributes' }) &&
      !path.node.object.computed
    ) {
      const newObject = t.memberExpression(
        path.node.object.object,
        t.identifier('userAttributes'),
        false
      );
      const newMemberExpression = t.memberExpression(newObject, path.node.property, path.node.computed);
      path.replaceWith(newMemberExpression);
    }
  }
}
