import {service} from '@loopback/core';
import {indexOf} from 'lodash';
import {ApplicationError} from '../../helper/application-error';
import {EntityName} from '../../models/entity-name';
import {ObjectNode} from '../../models/object-node.model';
import {ObjectTree} from '../../models/object-tree.model';
import {ObjectType} from '../../models/object-type.model';
import {ActionEntityService} from '../../services/action-entity/action-entity.service';
import {
  CurrentContext,
  ExpectedValue,
} from '../../services/application.service';
import {
  ObjectNodeDefinitionService,
  OneOfTreeOption,
} from '../../services/entity-definition/object-node-definition.service';
import {InsideRestService} from '../../services/inside-rest/inside-rest.service';
import {ObjectNodeService} from '../../services/object-node/object-node.service';
import {ObjectTypeService} from '../../services/object-type.service';
import {TransientNodeService} from '../../services/transient-entity/transient-node.service';
import {UriCompleteService} from '../../services/uri-complete/uri-complete.service';
import {LocationService} from '../location/location.service';
import {
  CONTENT_FILE_PROVIDER,
  FILES_DIRECTORY_TYPE,
  FILE_DIRECTORIES_TYPE,
  FILE_DIRECTORY_REFERRER_TYPE,
  FILE_DIRECTORY_SELECTOR_TYPE,
  FILE_TYPE,
} from './content-file.const';
import {File} from './content-file.definition';
import {InterceptFileService} from './intercept-file.service';
import {TransientFileService} from './transient-file.service';

export class ActionFileService {
  constructor(
    @service(ActionEntityService)
    protected actionEntityService: ActionEntityService,
    @service(ObjectNodeService)
    private objectNodeService: ObjectNodeService,
    @service(ObjectTypeService)
    private objectTypeService: ObjectTypeService,
    @service(InsideRestService) private insideRestService: InsideRestService,
    @service(UriCompleteService) private uriCompleteService: UriCompleteService,
    @service(ObjectNodeDefinitionService)
    private objectNodeDefinitionService: ObjectNodeDefinitionService,
    @service(TransientFileService)
    private transientFileService: TransientFileService,
    @service(InterceptFileService)
    private interceptFileService: InterceptFileService,
    @service(TransientNodeService)
    private transientNodeService: TransientNodeService,
    @service(LocationService) private locationService: LocationService,
  ) {
    this.actionEntityService.registerNewMethodFunction(
      CONTENT_FILE_PROVIDER,
      ActionFileService.name,
      'Load and create several File entities in an File Directory',
      EntityName.objectNode,
      'load',
      FILES_DIRECTORY_TYPE.name,
      this.loadFileDirectoryNode.bind(this),
      'create',
    );
    this.actionEntityService.registerNewMethodFunction(
      CONTENT_FILE_PROVIDER,
      ActionFileService.name,
      'Load and create several File entities in a referenced File Directory',
      EntityName.objectNode,
      'load',
      FILE_DIRECTORY_REFERRER_TYPE.name,
      this.loadFileDirectoryReferrerNode.bind(this),
      'create',
    );
    this.actionEntityService.registerNewMethodFunction(
      CONTENT_FILE_PROVIDER,
      ActionFileService.name,
      'Create new File Directory for a directory referrer',
      EntityName.objectNode,
      'createDirectory',
      FILE_DIRECTORY_REFERRER_TYPE.name,
      this.createDirectoryFileDirectoryReferrerNode.bind(this),
      'create',
    );
    this.actionEntityService.registerNewMethodFunction(
      CONTENT_FILE_PROVIDER,
      ActionFileService.name,
      'Configure an File Directory for a directory referrer',
      EntityName.objectNode,
      'configureDirectory',
      FILE_DIRECTORY_REFERRER_TYPE.name,
      this.configureDirectoryFileDirectoryReferrerNode.bind(this),
      'update',
    );
  }

  protected async createFile(
    file: File,
    parent: ObjectNode,
    ctx: CurrentContext,
    parentType?: ObjectType,
    title?: string,
  ): Promise<ObjectNode> {
    const childCtx: CurrentContext = CurrentContext.get(ctx, {
      nodeContext: {
        parent: new ExpectedValue(parent),
        parentType: new ExpectedValue(parentType),
      },
    });
    const nameParts = file.name.split('.');
    if (1 < nameParts.length) {
      nameParts.pop();
    }

    return this.objectNodeService.add(
      {
        name: title ? title : nameParts.join('.'),
        parentNodeId: parent.id,
        objectTypeId: FILE_TYPE.name,
        contentFile: file,
      },
      childCtx,
      false,
      true,
      true,
    );
  }

  public async loadFileDirectoryNode(
    directory: ObjectNode,
    args: Object,
    ctx: CurrentContext,
  ): Promise<ObjectTree> {
    const files: {files: File[]; title?: string} = args as {
      files: File[];
      title?: string;
    };
    for (const file of files.files) {
      await this.createFile(
        file,
        ctx.nodeContext.node.value,
        ctx,
        ctx.nodeContext.objectType.value,
        files.title,
      );
    }
    return this.actionEntityService.getEntity<ObjectTree>(
      EntityName.objectTree,
      directory.id as string,
      CurrentContext.get(ctx, {}),
    );
  }

  public async configureDirectoryFileDirectoryReferrerNode(
    objectNode: ObjectNode,
    args: Object,
    ctx: CurrentContext,
  ): Promise<ObjectNode> {
    const fileDirectoryObjectTreeId = (args as {
      fileDirectoryObjectTreeId: string;
    }).fileDirectoryObjectTreeId;

    if (
      undefined === fileDirectoryObjectTreeId ||
      '' === fileDirectoryObjectTreeId
    ) {
      throw ApplicationError.missingParameter(fileDirectoryObjectTreeId);
    }

    await this.objectNodeService.modifyById(
      objectNode.id as string,
      {fileDirectoryObjectTreeId: fileDirectoryObjectTreeId},
      CurrentContext.get(ctx),
    );

    return this.actionEntityService.getEntity<ObjectNode>(
      EntityName.objectNode,
      objectNode.id as string,
      CurrentContext.get(ctx, {}),
    );
  }

  public async createDirectoryFileDirectoryReferrerNode(
    objectNode: ObjectNode,
    args: Object,
    ctx: CurrentContext,
  ): Promise<[ObjectTree, ObjectNode] | undefined> {
    const creationArguments: {
      name?: string;
      parentId?: string;
      childTypeId?: string;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } = args as any;
    let name: string | undefined = creationArguments.name;
    if (!name || '' === name) {
      name = objectNode.title || objectNode.name;
    }
    let parentId: string | undefined = creationArguments.parentId;
    if (!parentId || '' === parentId) {
      const directories: ObjectNode[] = await this.transientFileService.getFileDirectoriesParents(
        objectNode,
      );
      if (!directories || 0 === directories.length) {
        throw ApplicationError.missing({object: FILE_DIRECTORIES_TYPE.name});
      }
      parentId = directories[0].id as string;
    }

    let childTypeId: string | undefined = creationArguments.childTypeId;

    const refererType = await this.objectTypeService.searchById(
      objectNode.objectTypeId,
    );
    if (
      !refererType.definition.properties.fileDirectoryObjectTreeId.oneOfTree
    ) {
      throw ApplicationError.forbidden();
    }

    const oneOfTreeOptions: OneOfTreeOption[] = refererType.definition.properties.fileDirectoryObjectTreeId.oneOfTree.filter(
      (referencedTree: OneOfTreeOption) => !referencedTree.namespaceType,
    );
    const referredTypes: string[] = oneOfTreeOptions.map(
      (oneOfTree: {treeType: string}) => oneOfTree.treeType,
    );

    let directoryTypes: string[] = [];
    for (const referredType of referredTypes) {
      directoryTypes.push(
        ...(await this.objectTypeService.getImplementingTypes(referredType)),
      );
    }
    if (!childTypeId || '' === childTypeId) {
      const parentDirectoryTree: ObjectTree = (await this.insideRestService.read(
        this.uriCompleteService.getUri(EntityName.objectTree, parentId, ctx),
        ctx,
      )) as ObjectTree;
      if (!parentDirectoryTree.entityCtx?.actions?.creations) {
        throw ApplicationError.missing({
          object: FILE_DIRECTORIES_TYPE.name,
          parentId: parentId,
          childType: FILES_DIRECTORY_TYPE.name,
        });
      }
      const childTypeIds = Object.keys(
        parentDirectoryTree.entityCtx.actions.creations,
      );
      directoryTypes = directoryTypes.filter(
        (type) => -1 < childTypeIds.indexOf(type),
      );
      if (0 === childTypeIds.length) {
        throw ApplicationError.missing({
          object: FILE_DIRECTORIES_TYPE.name,
          parentId: parentId,
          childType: FILES_DIRECTORY_TYPE.name,
        });
      }
      childTypeId = directoryTypes[0];
    }
    if (-1 === directoryTypes.indexOf(childTypeId)) {
      throw ApplicationError.unauthorizedValue({childTypeId: childTypeId});
    }
    for (let index = oneOfTreeOptions.length - 1; index--; index >= 0) {
      if (
        -1 ===
        indexOf(
          await this.objectTypeService.getImplementingTypes(
            oneOfTreeOptions[index].treeType,
          ),
          childTypeId,
        )
      ) {
        oneOfTreeOptions.splice(index, 1);
      }
    }
    if (0 === oneOfTreeOptions.length) {
      throw ApplicationError.unauthorizedValue({childTypeId: childTypeId});
    }
    const oneOfTreeOption: OneOfTreeOption = oneOfTreeOptions[0];

    const newDirectory = await this.objectNodeService.add(
      {parentNodeId: parentId, objectTypeId: childTypeId, name: name},
      CurrentContext.get(ctx),
      false,
      true,
      true,
    );
    const owner = await this.objectNodeService.getNode(
      newDirectory.parentOwnerId,
      CurrentContext.get(ctx),
    );
    const namespace = await this.objectNodeService.getNode(
      newDirectory.parentNamespaceId,
      CurrentContext.get(ctx),
    );
    const fileDirectoryObjectTreeId = this.objectNodeDefinitionService.getTreeIdFromTreeOption(
      oneOfTreeOption,
      owner,
      namespace,
      newDirectory,
    );
    await this.objectNodeService.modifyById(
      objectNode.id as string,
      {fileDirectoryObjectTreeId: fileDirectoryObjectTreeId},
      CurrentContext.get(ctx),
    );

    return [
      await this.actionEntityService.getEntity<ObjectTree>(
        EntityName.objectTree,
        parentId,
        CurrentContext.get(ctx, {}),
      ),
      await this.actionEntityService.getEntity<ObjectNode>(
        EntityName.objectNode,
        objectNode.id as string,
        CurrentContext.get(ctx, {}),
      ),
    ];
  }

  public async loadFileDirectoryReferrerNode(
    objectNode: ObjectNode,
    args: Object,
    ctx: CurrentContext,
  ): Promise<[ObjectTree, ObjectNode] | undefined> {
    if (objectNode.fileDirectoryObjectTreeId) {
      const fileDirectoryParts = objectNode.fileDirectoryObjectTreeId.split(
        '/',
      );
      const fileDirectoryNode = await this.objectNodeService.searchTree(
        fileDirectoryParts[1],
        fileDirectoryParts[2],
        fileDirectoryParts[3],
        fileDirectoryParts[4],
        fileDirectoryParts[5],
        fileDirectoryParts[6],
      );

      if (fileDirectoryNode) {
        let selectedFiles: string[] | undefined = undefined;
        let initialSelectedFilesCount = 0;
        const implementedTypes = await this.objectTypeService.getImplementedTypes(
          objectNode.objectTypeId,
        );
        if (-1 < indexOf(implementedTypes, FILE_DIRECTORY_SELECTOR_TYPE.name)) {
          selectedFiles = objectNode.selectedFiles
            ? objectNode.selectedFiles
            : [];
          initialSelectedFilesCount = (selectedFiles as string[]).length;
        }

        const files: {files: File[]; title?: string} = args as {
          files: File[];
          title?: string;
        };
        for (const file of files.files) {
          const fileObject = await this.createFile(
            file,
            fileDirectoryNode,
            ctx,
            undefined,
            files.title,
          );
          if (fileObject && selectedFiles) {
            selectedFiles.push(fileObject.name);
          }
        }

        if (selectedFiles && selectedFiles.length > initialSelectedFilesCount) {
          await this.objectNodeService.modifyById(
            objectNode.id as string,
            {selectedFiles},
            ctx,
          );
        }

        return [
          await this.actionEntityService.getEntity<ObjectTree>(
            EntityName.objectTree,
            fileDirectoryNode.id as string,
            CurrentContext.get(ctx, {}),
          ),
          await this.actionEntityService.getEntity<ObjectNode>(
            EntityName.objectNode,
            objectNode.id as string,
            ctx,
          ),
        ];
      }
    }
  }
}
