import { type em as prototypeEm, FieldPrototype, make } from "data/prototype";
import { Entity } from "data/entities";
import type { EntityIndex, Field } from "data/fields";
import { entityTypes } from "data/entities/Entity";
import { isEqual } from "lodash-es";
import type { ModuleBuildContext, ModuleBuildContextMcpContext } from "./Module";
import type { EntityRelation } from "data/relations";
import type { Permission, PermissionContext } from "auth/authorize/Permission";
import { Exception } from "core/errors";
import { invariant, isPlainObject } from "bknd/utils";

export class ModuleHelper {
   constructor(protected ctx: Omit<ModuleBuildContext, "helper">) {}

   get em() {
      return this.ctx.em;
   }

   get flags() {
      return this.ctx.flags;
   }

   ensureEntity(entity: Entity) {
      const instance = this.em.entity(entity.name, true);

      // check fields
      if (!instance) {
         this.em.addEntity(entity);
         this.flags.sync_required = true;
         return;
      }

      // if exists, check all fields required are there
      // @todo: potentially identify system and generated entities and take that as instance
      // @todo: check if the field also equal
      for (const field of entity.fields) {
         const instanceField = instance.field(field.name);
         if (!instanceField) {
            instance.addField(field);
            this.flags.sync_required = true;
         } else {
            const changes = this.setEntityFieldConfigs(field, instanceField);
            if (changes > 0) {
               this.flags.sync_required = true;
            }
         }
      }

      // if type is different, keep the highest
      if (instance.type !== entity.type) {
         const instance_i = entityTypes.indexOf(instance.type);
         const entity_i = entityTypes.indexOf(entity.type);
         const type = entity_i > instance_i ? entity.type : instance.type;

         this.em.__replaceEntity(new Entity(instance.name, instance.fields, instance.config, type));
      }
   }

   ensureIndex(index: EntityIndex) {
      if (!this.em.hasIndex(index)) {
         this.em.addIndex(index);
         this.flags.sync_required = true;
      }
   }

   ensureRelation(relation: EntityRelation) {
      try {
         // most reliable way at the moment
         this.em.addRelation(relation);
         this.flags.sync_required = true;
      } catch (e) {}

      // @todo: improve this function, seems like it still doesn't catch all cases
      if (!this.em.relations.exists(relation)) {
      }
   }

   ensureSchema<Schema extends ReturnType<typeof prototypeEm>>(schema: Schema): Schema {
      Object.values(schema.entities ?? {}).forEach(this.ensureEntity.bind(this));
      schema.indices?.forEach(this.ensureIndex.bind(this));
      schema.relations?.forEach(this.ensureRelation.bind(this));

      return schema;
   }

   setEntityFieldConfigs(
      parent: Field,
      child: Field,
      props: string[] = ["hidden", "fillable", "required"],
   ) {
      let changes = 0;
      for (const prop of props) {
         if (!isEqual(child.config[prop], parent.config[prop])) {
            child.config[prop] = parent.config[prop];
            changes++;
         }
      }
      return changes;
   }

   replaceEntityField(
      _entity: string | Entity,
      field: Field | string,
      _newField: Field | FieldPrototype,
   ) {
      const entity = this.em.entity(_entity);
      const name = typeof field === "string" ? field : field.name;
      const newField =
         _newField instanceof FieldPrototype ? make(name, _newField as any) : _newField;

      // ensure keeping vital config
      this.setEntityFieldConfigs(entity.field(name)!, newField);

      entity.__replaceField(name, newField);
   }

   async granted<P extends Permission<any, any, any, any>>(
      c: { context: ModuleBuildContextMcpContext; raw?: unknown },
      permission: P,
      context: PermissionContext<P>,
   ): Promise<void>;
   async granted<P extends Permission<any, any, undefined, any>>(
      c: { context: ModuleBuildContextMcpContext; raw?: unknown },
      permission: P,
   ): Promise<void>;
   async granted<P extends Permission<any, any, any, any>>(
      c: { context: ModuleBuildContextMcpContext; raw?: unknown },
      permission: P,
      context?: PermissionContext<P>,
   ): Promise<void> {
      invariant(c.context.app, "app is not available in mcp context");
      const auth = c.context.app.module.auth;
      if (!auth.enabled) return;

      if (c.raw === undefined || c.raw === null) {
         throw new Exception("Request/Headers/Context is not available in mcp context", 400);
      }

      const user = await auth.authenticator?.resolveAuthFromRequest(c.raw as any);
      this.ctx.guard.granted(permission, user as any, context as any);
   }
}
