import ts from 'typescript';
import {
  type AnyType, DeclarationUtil, LiteralUtil,
  DecoratorUtil, DocUtil, type ParamDocumentation, type TransformerState, transformCast,
} from '@travetto/transformer';

export type ComputeConfig = { type?: AnyType, root?: ts.Node, name?: string, index?: number };

export class SchemaTransformUtil {

  static SCHEMA_IMPORT = '@travetto/schema/src/decorator/schema.ts';
  static METHOD_IMPORT = '@travetto/schema/src/decorator/method.ts';
  static FIELD_IMPORT = '@travetto/schema/src/decorator/field.ts';
  static INPUT_IMPORT = '@travetto/schema/src/decorator/input.ts';
  static COMMON_IMPORT = '@travetto/schema/src/decorator/common.ts';
  static TYPES_IMPORT = '@travetto/schema/src/types.ts';

  /**
   * Produce concrete type given transformer type
   */
  static toConcreteType(state: TransformerState, type: AnyType, node: ts.Node, root: ts.Node = node): ts.Expression {
    switch (type.key) {
      case 'pointer': return this.toConcreteType(state, type.target, node, root);
      case 'managed': return state.getOrImport(type);
      case 'tuple': return state.fromLiteral(type.subTypes.map(subType => this.toConcreteType(state, subType, node, root)!));
      case 'template': return state.createIdentifier(type.ctor.name);
      case 'literal': {
        if ((type.ctor === Array) && type.typeArguments?.length) {
          return state.fromLiteral([this.toConcreteType(state, type.typeArguments[0], node, root)]);
        } else if (type.ctor) {
          return state.createIdentifier(type.ctor.name!);
        }
        break;
      }
      case 'mapped': {
        const base = state.getOrImport(type);
        const uniqueId = state.generateUniqueIdentifier(node, type, 'Δ');
        const [id, existing] = state.registerIdentifier(uniqueId);
        if (!existing) {
          const cls = state.factory.createClassDeclaration(
            [
              state.createDecorator(this.SCHEMA_IMPORT, 'Schema', state.fromLiteral({
                description: type.comment,
                mappedOperation: type.operation,
                mappedFields: type.fields,
              })),
            ],
            id, [], [state.factory.createHeritageClause(
              ts.SyntaxKind.ExtendsKeyword, [state.factory.createExpressionWithTypeArguments(base, [])]
            )], []
          );
          cls.getText = (): string => `
class ${uniqueId} extends ${type.mappedClassName} { 
  fields: ${type.fields?.join(', ')} 
  operation: ${type.operation}
}`;
          state.addStatements([cls], root || node);
        }
        return id;
      }
      case 'unknown': {
        const imp = state.importFile(this.TYPES_IMPORT);
        return state.createAccess(imp.identifier, 'UnknownType');
      }
      case 'shape': {
        const uniqueId = state.generateUniqueIdentifier(node, type, 'Δ');

        // Build class on the fly
        const [id, existing] = state.registerIdentifier(uniqueId);
        if (!existing) {
          const cls = state.factory.createClassDeclaration(
            [
              state.createDecorator(this.SCHEMA_IMPORT, 'Schema', state.fromLiteral({
                description: type.comment
              })),
            ],
            id, [], [],
            Object.entries(type.fieldTypes)
              .map(([key, value]) =>
                this.computeInput(state, state.factory.createPropertyDeclaration(
                  [], /\W/.test(key) ? state.factory.createComputedPropertyName(state.fromLiteral(key)) : key,
                  value.undefinable || value.nullable ? state.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
                  value.key === 'unknown' ? state.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) : undefined, undefined
                ), { type: value, root })
              )
          );
          cls.getText = (): string => [
            `class ${uniqueId} {`,
            ...Object.entries(type.fieldTypes)
              .map(([key, value]) => `  ${key}${value.nullable ? '?' : ''}: ${value.name};`),
            '}'
          ].join('\n');
          state.addStatements([cls], root || node);
        }
        return id;
      }
      case 'composition': {
        if (type.commonType) {
          return this.toConcreteType(state, type.commonType, node, root);
        }
        break;
      }
      case 'foreign':
      default: {
        // Object
      }
    }
    return state.createIdentifier('Object');
  }

  /**
   * Compute decorator params from property/parameter/getter/setter
   */
  static computeInputDecoratorParams<T extends ts.PropertyDeclaration | ts.ParameterDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration>(
    state: TransformerState,
    node: T,
    config?: ComputeConfig
  ): ts.Expression[] {
    const typeExpr = config?.type ?? state.resolveType(ts.isSetAccessor(node) ? node.parameters[0] : node);
    const attrs: Record<string, string | boolean | object | number | ts.Expression> = {};

    if (!ts.isGetAccessorDeclaration(node) && !ts.isSetAccessorDeclaration(node)) {
      // eslint-disable-next-line no-bitwise
      if ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Readonly) > 0) {
        attrs.access = 'readonly';
      } else if (!!node.questionToken || !!typeExpr.undefinable || !!node.initializer) {
        attrs.required = { active: false };
      }
      if (node.initializer !== undefined && (
        ts.isLiteralExpression(node.initializer) ||
        node.initializer.kind === ts.SyntaxKind.TrueKeyword ||
        node.initializer.kind === ts.SyntaxKind.FalseKeyword ||
        (ts.isArrayLiteralExpression(node.initializer) && node.initializer.elements.length === 0)
      )) {
        attrs.default = node.initializer;
      }
    } else {
      const pair = DeclarationUtil.getAccessorPair(node);
      attrs.accessor = true;
      if (!pair.setter) {
        attrs.access = 'readonly';
      }
      if (!pair.getter) {
        attrs.access = 'writeonly';
      } else if (!!typeExpr.undefinable) {
        attrs.required = { active: false };
      }
    }

    const rawName = node.getSourceFile()?.text ? node.name.getText() ?? undefined : undefined;
    const providedName = config?.name ?? rawName!;
    attrs.name = providedName;

    if (rawName !== providedName && rawName) {
      attrs.sourceText = rawName;
    }

    const primaryExpr = typeExpr.key === 'literal' && typeExpr.typeArguments?.[0] ? typeExpr.typeArguments[0] : typeExpr;

    // We need to ensure we aren't being tripped up by the wrapper for arrays, sets, etc.
    // If we have a composition type
    if (primaryExpr.key === 'composition') {
      const values = primaryExpr.subTypes
        .map(subType => subType.key === 'literal' ? subType.value : undefined)
        .filter(value => value !== undefined && value !== null);

      if (values.length === primaryExpr.subTypes.length) {
        attrs.enum = {
          values,
          message: `{path} is only allowed to be "${values.join('" or "')}"`
        };
      }
    } else if (primaryExpr.key === 'template' && primaryExpr.template) {
      const regex = LiteralUtil.templateLiteralToRegex(primaryExpr.template);
      attrs.match = {
        regex: new RegExp(regex),
        template: primaryExpr.template,
        message: `{path} must match "${regex}"`
      };
    }

    if (ts.isParameter(node)) {
      const parentComments = DocUtil.describeDocs(node.parent);
      const paramComments: Partial<ParamDocumentation> = (parentComments.params ?? [])
        .find(param => param.name === node.name.getText()) || {};

      if (paramComments.description) {
        attrs.description = paramComments.description;
      }
    } else {
      const comments = DocUtil.describeDocs(node);
      if (comments.description) {
        attrs.description = comments.description;
      }
    }

    const tags = ts.getJSDocTags(node);
    const aliases = tags.filter(tag => tag.tagName.getText() === 'alias');
    if (aliases.length) {
      attrs.aliases = aliases.map(alias => alias.comment).filter(alias => !!alias);
    }

    const params: ts.Expression[] = [];

    const existing =
      state.findDecorator('@travetto/schema', node, 'Field', this.FIELD_IMPORT) ??
      state.findDecorator('@travetto/schema', node, 'Input', this.INPUT_IMPORT);

    if (config?.index !== undefined) {
      attrs.index = config.index;
    }

    if (Object.keys(attrs).length) {
      params.push(state.fromLiteral(attrs));
    }

    const resolved = this.toConcreteType(state, typeExpr, node, config?.root ?? node);
    const type = typeExpr.key === 'foreign' ? state.getConcreteType(node) :
      ts.isArrayLiteralExpression(resolved) ? resolved.elements[0] : resolved;

    params.unshift(LiteralUtil.fromLiteral(state.factory, {
      array: ts.isArrayLiteralExpression(resolved),
      type
    }));

    if (existing) {
      const args = DecoratorUtil.getArguments(existing) ?? [];
      if (args.length > 0) {
        params[0] = args[0]; // Overwrite
      }
      if (args.length > 1) {
        params.push(...args.slice(1));
      }
    }

    return params;
  }

  /**
   * Compute property information from declaration
   */
  static computeInput<T extends ts.PropertyDeclaration | ts.ParameterDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration>(
    state: TransformerState, node: T, config?: ComputeConfig
  ): T {
    const existingField = state.findDecorator('@travetto/schema', node, 'Field', this.FIELD_IMPORT);
    const existingInput = state.findDecorator('@travetto/schema', node, 'Input', this.INPUT_IMPORT);
    const params = this.computeInputDecoratorParams(state, node, config);

    let modifiers: ts.ModifierLike[];
    if (existingField) {
      const decorator = state.createDecorator(this.FIELD_IMPORT, 'Field', ...params);
      modifiers = DecoratorUtil.spliceDecorators(node, existingField, [decorator]);
    } else {
      const decorator = state.createDecorator(this.INPUT_IMPORT, 'Input', ...params);
      modifiers = DecoratorUtil.spliceDecorators(node, existingInput, [decorator]);
    }

    let result: unknown;
    if (ts.isPropertyDeclaration(node)) {
      result = state.factory.updatePropertyDeclaration(node,
        modifiers, node.name, node.questionToken, node.type, node.initializer);
    } else if (ts.isParameter(node)) {
      result = state.factory.updateParameterDeclaration(node,
        modifiers, node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer);
    } else if (ts.isGetAccessorDeclaration(node)) {
      result = state.factory.updateGetAccessorDeclaration(node,
        modifiers, node.name, node.parameters, node.type, node.body);
    } else {
      result = state.factory.updateSetAccessorDeclaration(node,
        modifiers, node.name, node.parameters, node.body);
    }
    return transformCast(result);
  }

  /**
   * Unwrap type
   */
  static unwrapType(type: AnyType): { out: Record<string, unknown>, type: AnyType } {
    const out: Record<string, unknown> = {};

    while (type?.key === 'literal' && type.typeArguments?.length) {
      if (type.ctor === Array) {
        out.array = true;
      }
      type = type.typeArguments?.[0] ?? { key: 'literal', ctor: Object }; // We have a promise nested
    }
    return { out, type };
  }

  /**
   * Ensure type
   * @param state
   * @param node
   */
  static ensureType(state: TransformerState, anyType: AnyType, target: ts.Node): Record<string, unknown> {
    const { out, type } = this.unwrapType(anyType);
    switch (type?.key) {
      case 'foreign': {
        out.type = state.getForeignTarget(type);
        break;
      }
      case 'managed': out.type = state.typeToIdentifier(type); break;
      case 'mapped': out.type = this.toConcreteType(state, type, target); break;
      case 'shape': out.type = this.toConcreteType(state, type, target); break;
      case 'template': out.type = state.factory.createIdentifier(type.ctor.name); break;
      case 'literal': {
        if (type.ctor) {
          out.type = out.array ?
            this.toConcreteType(state, type, target) :
            state.factory.createIdentifier(type.ctor.name);
        }
      }
    }
    return out;
  }

  /**
   * Find inner return method
   * @param state
   * @param node
   * @param methodName
   * @returns
   */
  static findInnerReturnMethod(state: TransformerState, node: ts.MethodDeclaration, methodName: string): ts.MethodDeclaration | undefined {
    // Process returnType
    const { type } = this.unwrapType(state.resolveReturnType(node));
    let cls;
    switch (type?.key) {
      case 'managed': {
        const [decorator] = DeclarationUtil.getDeclarations(type.original!);
        cls = decorator && ts.isClassDeclaration(decorator) ? decorator : undefined;
        break;
      }
      case 'shape': cls = type.original; break;
    }
    if (cls) {
      return state.findMethodByName(cls, methodName);
    }
  }

  /**
   * Compute return type decorator params
   */
  static computeReturnTypeDecoratorParams(state: TransformerState, node: ts.MethodDeclaration): ts.Expression[] {
    // If we have a valid response type, declare it
    const returnType = state.resolveReturnType(node);
    let targetType = returnType;

    if (returnType.key === 'literal' && returnType.typeArguments?.length && returnType.name === 'Promise') {
      targetType = returnType.typeArguments[0];
    }

    // TODO: Standardize this using jsdoc
    let innerReturnType: AnyType | undefined;
    if (targetType.key === 'managed' && targetType.importName.startsWith('@travetto/')) {
      innerReturnType = state.getApparentTypeOfField(targetType.original!, 'body');
    }

    const finalReturnType = SchemaTransformUtil.ensureType(state, innerReturnType ?? returnType, node);
    return finalReturnType ? [state.fromLiteral({ returnType: finalReturnType })] : [];
  }
}