/**
 * This file is part of the NocoBase (R) project.
 * Copyright (c) 2020-2024 NocoBase Co., Ltd.
 * Authors: NocoBase Team.
 *
 * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
 * For more information, please refer to: https://www.nocobase.com/agreement.
 */

import { Context } from '@nocobase/actions';
import { Collection } from '@nocobase/database';

export async function dataTemplate(ctx: Context, next) {
  const { resourceName, actionName } = ctx.action;
  const { isTemplate, fields, appends } = ctx.action.params;

  await next();

  if (isTemplate && actionName === 'get') {
    ctx.body = traverseJSON(JSON.parse(JSON.stringify(ctx.body)), {
      collection: ctx.getCurrentRepository().collection,
      include: [...(fields || []), ...(appends || [])],
    });
  }
}

type TraverseOptions = {
  collection: Collection;
  exclude?: string[];
  include?: string[];
  through?: string;
  excludePk?: boolean;
  isHasManyField?: boolean;
};

const traverseHasMany = (arr: any[], { collection, exclude = [], include = [] }: TraverseOptions) => {
  if (!arr) {
    return arr;
  }
  return arr.map((item) => traverseJSON(item, { collection, exclude, include, isHasManyField: true }));
};

const traverseBelongsToMany = (arr: any[], { collection, exclude = [], through }: TraverseOptions) => {
  if (!arr) {
    return arr;
  }
  const throughCollection = collection.db.getCollection(through);
  return arr.map((item) => {
    const data = traverseJSON(item[through], { collection: throughCollection, exclude });
    if (data && Object.keys(data).length) {
      item[through] = data;
    } else {
      delete item[through];
    }
    return traverseJSON(item, {
      collection,
      excludePk: false,
    });
  });
};

const parseInclude = (keys: string[]) => {
  const map = {};
  for (const key of keys) {
    const args = key.split('.');
    const field = args.shift();
    map[field] = map[field] || [];
    if (args.length) {
      map[field].push(args.join('.'));
    }
  }
  return map;
};

const traverseJSON = (data, options: TraverseOptions) => {
  if (!data) {
    return data;
  }

  const { collection, exclude = [], include = [], excludePk = true } = options;
  const map = parseInclude(include);
  const result = {};
  for (const key of Object.keys(data || {})) {
    const subInclude = map[key];
    if (include.length > 0 && !subInclude) {
      continue;
    }
    if (exclude.includes(key)) {
      continue;
    }
    if (['createdAt', 'updatedAt', 'createdBy', 'createdById', 'updatedById', 'updatedBy'].includes(key)) {
      continue;
    }
    const field = collection.getField(key);
    if (!field) {
      result[key] = data[key];
      continue;
    }

    if (field.options.primaryKey && excludePk && !collection.isMultiFilterTargetKey()) {
      continue;
    }

    if (field.options.isForeignKey) {
      continue;
    }

    if (!options.isHasManyField && ['sort'].includes(field.type)) {
      continue;
    }

    if (['password', 'sequence'].includes(field.type)) {
      continue;
    }

    if (field.type === 'hasOne') {
      result[key] = traverseJSON(data[key], {
        collection: collection.db.getCollection(field.target),
        exclude: [field.foreignKey],
        include: subInclude,
      });
    } else if (field.type === 'hasMany') {
      result[key] = traverseHasMany(data[key], {
        collection: collection.db.getCollection(field.target),
        exclude: [field.foreignKey],
        include: subInclude,
      });
    } else if (field.type === 'belongsTo') {
      result[key] = traverseJSON(data[key], {
        collection: collection.db.getCollection(field.target),
        // exclude: [field.foreignKey],
        include: subInclude,
        excludePk: false,
      });
    } else if (field.type === 'belongsToMany') {
      result[key] = traverseBelongsToMany(data[key], {
        collection: collection.db.getCollection(field.target),
        exclude: [field.foreignKey, field.otherKey],
        through: field.through,
      });
    } else {
      result[key] = data[key];
    }
  }
  return result;
};
