import * as Case from "case";
import * as path from "path";
import { UModel } from "../entities/model";
import { URenderer } from "../entities/renderer";
import { $attr } from "../shortcuts/queries";
import { RenderContent, RenderPath, RenderSelection } from "../types/renderer";
import { closeCursor, writeToCursor } from "../utils/rendering";
import {
  _array,
  _enum,
  _ref,
  _required,
  _rootModule,
} from "../shortcuts/attributes";
import { UModule } from "../entities/module";
import { UField } from "../entities/field";

export default class TSClassRenderer extends URenderer {
  private _entityDir = "src/entities";
  private _dtoDir = "src/dtos";
  private _enumDir = "src/types";
  private _includeModuleInDir = true;
  private _where?: (module: UModule, model: UModel) => boolean;

  constructor(options?: {
    modelDir?: string;
    dtoDir?: string;
    includeModuleInDir?: boolean;
    where?: (module: UModule, model: UModel) => boolean;
  }) {
    super("ts@classes");
    if (options?.modelDir) this._entityDir = options.modelDir;
    if (options?.dtoDir) this._dtoDir = options.dtoDir;
    if (options?.includeModuleInDir)
      this._includeModuleInDir = options.includeModuleInDir;
    if (options?.where) this._where = options.where;
  }

  $isDto(model: UModel) {
    const output = this.$output(model.$name());
    if (output) return !!output.meta?.isDto;
    return null;
  }

  $resolveImport(from: string, model: UModel): string {
    const modelPath = this.$path(this.$className(model));
    if (!modelPath?.path) return "";
    const importPath = this.$resolveRelativePath(from, modelPath.path);

    return `import { ${this.$className(model)} } from '${importPath}';\n`;
  }

  $key(model: UModel) {
    return this.$className(model);
  }

  $keys(models: UModel[]) {
    return models.map((model) => this.$key(model));
  }

  $paths(models?: UModel[]) {
    let paths = super.$paths();
    if (models)
      paths = paths.filter((p) => models.some((m) => p.key === this.$key(m)));
    return paths;
  }

  $className(model: UModel) {
    return Case.pascal(model.$name());
  }

  $fileName(model: UModel, extension = true) {
    return `${Case.kebab(model.$name())}${extension ? ".ts" : ""}`;
  }

  $fieldName(field: UField) {
    let nameParts = field.$name().match(/([^A-Za-z]+)(.+)/);
    if (nameParts) return nameParts[1] + Case.camel(nameParts[2]);
    return Case.camel(field.$name());
  }

  $fieldType(field: UField) {
    let type = field.$type() + "";
    if (type === "date") type = "Date";
    else if (type === "reference") type = "string";
    else if (["int", "float"].includes(type)) type = "number";
    return type;
  }

  $fieldSignature(field: UField) {
    let type = this.$fieldType(field);
    if (type === "nested") {
      const nestedModel = $attr(field, _ref());
      if (nestedModel) type = this.$className(nestedModel);
    }
    return `${this.$fieldName(field)}${
      !$attr(field, _required()) ? "?" : ""
    }: ${type}${$attr(field, _array()) ? "[]" : ""}`;
  }

  async select(): Promise<RenderSelection> {
    const models = this.$models(this._where);
    const paths: RenderPath[] = [];

    models.forEach((model) => {
      if (paths.some((p) => p.key === this.$key(model))) return;
      const mod = $attr(model, _rootModule());

      const isDto = !!model.$name().match(/dto$/i);
      const isEnum = !!$attr(model, _enum());
      paths.push({
        key: this.$key(model),
        meta: { isDto, isEnum },
        path: path.join(
          isDto ? this._dtoDir : isEnum ? this._enumDir : this._entityDir,
          this._includeModuleInDir ? Case.kebab(mod ? mod.$name() : "") : "",
          this.$fileName(model)
        ),
      });
    });

    return {
      paths,
      models,
    };
  }

  async render(): Promise<RenderContent[]> {
    const output: RenderContent[] = [];
    const models = this.$selection().models || [];

    models.forEach((model) => {
      const modelKey = this.$key(model);
      const modelPath = this.$path(modelKey);

      if (!modelPath) return;

      let content = "";

      const enumDefinition = $attr(model, _enum());
      if (enumDefinition) {
        const enumCursor = "#enum-cursor\n";

        content = `export enum ${this.$className(model)} {\n${enumCursor}}`;

        Object.keys(enumDefinition).forEach((key) => {
          content = writeToCursor(
            enumCursor,
            `  ${key} = ${JSON.stringify(enumDefinition[key])},\n`,
            content
          );
        });

        content = closeCursor(enumCursor, content);
      } else {
        const fieldCursor = "#field-cursor\n";
        const importCursor = "#import-cursor\n";
        const fromJsonCursor = "#from-json-cursor\n";
        const toJsonCursor = "#to-json-cursor\n";

        content = `${importCursor}export class ${this.$className(
          model
        )} {\n${fieldCursor}\n  constructor(data?: Omit<${this.$className(
          model
        )}, "toJson">) {
    if (data) ${this.$className(model)}.fromJson(data, this);
  }\n\n${fromJsonCursor}\n${toJsonCursor}}`;

        const fields = model.$fields();
        const importedModels: string[] = [];

        fields.forEach((field) => {
          const fieldSignature = this.$fieldSignature(field);

          if (field.$type() == "nested") {
            const nestedModel = $attr(field, _ref());
            if (nestedModel) {
              if (
                this.$className(model) != this.$className(nestedModel) &&
                !importedModels.includes(this.$className(nestedModel))
              ) {
                content = writeToCursor(
                  importCursor,
                  this.$resolveImport(modelPath.path ?? "", nestedModel),
                  content
                );
                importedModels.push(this.$className(nestedModel));
              }
            }
          }

          content = writeToCursor(
            fieldCursor,
            `  ${fieldSignature};\n`,
            content
          );
        });

        let fromJsonMethod = `  static fromJson(json: Record<string, any>, instance?: ${this.$className(
          model
        )}): ${this.$className(
          model
        )} {\n    instance = instance ?? new ${this.$className(model)}();\n`;
        let toJsonMethod = `  toJson(): Record<string, any> {\n    return {\n`;
        fields.forEach((field) => {
          const fieldName = this.$fieldName(field);
          const type = field.$type();

          if (type === "date") {
            // For Date fields, assume the JSON value is an ISO string.
            fromJsonMethod += `      if(json.${fieldName}) instance.${fieldName} =  new Date(json.${fieldName});\n`;
            toJsonMethod += `      ${fieldName}: this.${fieldName} ? this.${fieldName}.toISOString() : undefined,\n`;
          } else if (type === "nested") {
            // For nested objects, call the nested type's fromJson/toJson.
            const nestedModel = $attr(field, _ref());
            const isEnum = !nestedModel || !!$attr(nestedModel, _enum());
            if (nestedModel && !isEnum) {
              if ($attr(field, _array())) {
                // Nested array: map each item.
                fromJsonMethod += `      if(json.${fieldName}) instance.${fieldName} =json.${fieldName}.map((item: any) => ${this.$className(
                  nestedModel
                )}.fromJson(item));\n`;
                toJsonMethod += `      ${fieldName}: this.${fieldName} ? this.${fieldName}.map((item: any) => item.toJson()) : undefined,\n`;
              } else {
                // Single nested object.
                fromJsonMethod += `      if(json.${fieldName}) instance.${fieldName} = ${this.$className(
                  nestedModel
                )}.fromJson(json.${fieldName});\n`;
                toJsonMethod += `      ${fieldName}: this.${fieldName} ? this.${fieldName}.toJson() : undefined,\n`;
              }
            } else if (nestedModel) {
              fromJsonMethod += `      instance.${fieldName} = json.${fieldName};\n`;
              toJsonMethod += `      ${fieldName}: this.${fieldName},\n`;
            }
          } else {
            // For primitives and other types, assign directly.
            fromJsonMethod += `      instance.${fieldName} = json.${fieldName};\n`;
            toJsonMethod += `      ${fieldName}: this.${fieldName},\n`;
          }
        });

        fromJsonMethod += `      return instance;\n  }\n`;
        toJsonMethod += `    };\n  }\n`;

        content = writeToCursor(fromJsonCursor, fromJsonMethod, content);
        content = writeToCursor(toJsonCursor, toJsonMethod, content);

        if (importedModels.length > 0)
          content = writeToCursor(importCursor, "\n", content);

        content = closeCursor(fieldCursor, content);
        content = closeCursor(importCursor, content);
        content = closeCursor(fromJsonCursor, content);
        content = closeCursor(toJsonCursor, content);
      }

      if (content)
        output.push({
          key: modelKey,
          content,
          meta: modelPath.meta ?? {},
        });
    });

    return output;
  }
}
