import { UITable } from './../../../ui/data/grid/ui-table';
import { KebabCase } from './../../../utils/kebab-case';
import { autoinject, BindingEngine, computedFrom, useView } from "aurelia-framework";
import { Router, NavModel } from "aurelia-router";
import { Actions, EntityTypeNames } from "../../../constants/constants";
import { ServiceBase } from "../../../data/service-base";
import { Permission as PermissionModel, Role, RolePermission, User, UserRole, Various } from "../../../model/common-model";
import { CustomLogger } from "../../../modules/custom-logger";
import { EntityDetailViewModelBase } from "../../detail-view-model-base";
import { EditingModeEnum } from "../../editing-mode-enum";
import { TranslatedPropertyRules } from '../../../utils/translated-property-rules';
import { Datacontext } from "../../../data/data-context";
import { Predicate, FilterQueryOp } from 'breeze-client';
import { RouteSettings } from '../../../config';

const CrudActions = [Actions.Read, Actions.Create, Actions.Update, Actions.Delete];

@autoinject
@useView("./../../../vm/security/roles/role-detail.html")
export class RoleDetail extends EntityDetailViewModelBase<Role> {
  public ressourceName: string = EntityTypeNames.Role;

  protected selectedLanguageCode = 'en';

  private rolePermissionService: ServiceBase<RolePermission>;
  private userRoleService: ServiceBase<UserRole>;
  private userService: ServiceBase<User>;

  private permissionGroups: PermissionGroup[];

  private canUpdateUser: boolean = false;

  private filter: {
    active: boolean;
    searchGroup: string;
    searchName: string;
    showScreens: boolean;
    showFunctionalities: boolean;
    showAssigned: boolean;
    showUnassigned: boolean;
  } = {
      active: false,
      searchGroup: null,
      searchName: null,
      showScreens: false,
      showFunctionalities: false,
      showAssigned: true,
      showUnassigned: false,
    };

  private isLoading = true;

  private permissionsTableElement: UITable;

  constructor(router: Router, logger: CustomLogger, datacontext: Datacontext, private bindingEngine: BindingEngine) {
    super(router, logger);
    super.initialize(new ServiceBase<Role>(EntityTypeNames.Role));
    this.selectedLanguageCode = datacontext.currentLanguage.code;
    this.rolePermissionService = new ServiceBase<RolePermission>(EntityTypeNames.RolePermission, null, EntityTypeNames.ResourceNames.RolePermission);
    this.userRoleService = new ServiceBase<UserRole>(EntityTypeNames.UserRole, null, EntityTypeNames.ResourceNames.UserRole);
    this.userService = new ServiceBase<User>(EntityTypeNames.User, null, EntityTypeNames.ResourceNames.User);
    this.userService.gridDataSource.customSelect2Predicates = () => {
      let predicate = new Predicate('disabled', FilterQueryOp.Equals, false);
      if (this.entity?.users?.length > 0) {
        return predicate.and(...this.entity.users.map(x => new Predicate('id', FilterQueryOp.NotEquals, x.userId)));
      }
      return predicate;
    }
  }

  protected getTranslationKeyEntityTypeName(): string {
    return 'common:' + super.getTranslationKeyEntityTypeName();
  }

  @computedFrom('editingMode', 'entity.name._translation')
  public get documentTitle() {
    if (this.editingMode === EditingModeEnum.Create) {
      return this.i18n.tr('common:role.role');
    } else if (this.editingMode === EditingModeEnum.Update) {
      return this.entity?.name?._translation;
    }
  }

  @computedFrom('ressourceName')
  private get crudActions(): string[] {
    return CrudActions;
  }

  public bind() {
    let userResource: string = (this.router?.currentInstruction?.config?.settings as RouteSettings)?.params?.userResource;
    this.canUpdateUser = this.authService.checkAccess(userResource ?? EntityTypeNames.User, Actions.Update);
  }

  public async activate(params: { param1?: number, duplicateFrom?: number, dialogType?: any }): Promise<void> {
    this.isLoading = true;
    await super.activate(params);

    await this.loadPermissions();
    await this.addObservableOnPermissions();

    const id = params.param1;
    if (id == Various.NewId) {
      this.editingMode = EditingModeEnum.Create;
      this.entity = await this.service.createEntity();
      this.entity.active = true;

      if (params.duplicateFrom != null) {
        await this.loadRole(params.duplicateFrom);
      }

      this.filter.showUnassigned = true;
    } else {
      this.editingMode = EditingModeEnum.Update;
      this.entity = await this.loadRole(id, ['users.user']);
    }
    this.controller.addObject(this.entity);
    TranslatedPropertyRules.anyRequired('name')
      .ensure('users').satisfies((userRoles: UserRole[]) => userRoles.every(x => x.userId > 0)).when(() => false == this.isCreationMode)
      .on(this.entity);

    this.isLoading = false;

    this.disposables.push(
      ...Object.keys(this.filter).map(prop =>
        this.bindingEngine.propertyObserver(this.filter, prop)
          .subscribe(() => this.permissionsTableElement?.filterChanged())
      )
    );
  }

  private async loadRole(id: number, expands: string[] = []): Promise<Role> {
    expands.push('permissions');
    let role = await this.service.getEntityById(id, ...expands);

    this.permissionGroups.forEach(group =>
      group.permissions.forEach(permission =>
        permission.isSelected = role.permissions.find(x => x.permissionId == permission.permissionId) != null
      )
    );

    return role;
  }

  private async loadPermissions() {
    let permissionModels = await new ServiceBase<PermissionModel>(EntityTypeNames.Permission, null, EntityTypeNames.ResourceNames.Permission)
      .getEntities(null, null, null, { propertyPaths: 'permissionId' });
    let permissions = permissionModels.map(x => new Permission(x));

    this.permissionGroups = [];

    permissions.groupBy('context').forEach((permissions: Permission[]) =>
      this.permissionGroups.push(...[
        ...permissions.filter(x => false == x.isCrud).map(x => new PermissionGroup(this, [x])),
        new CrudPermissionGroup(this, permissions.filter(x => x.isCrud))
      ].filter(group => group.permissions.length > 0))
    );

    let groupsUsingCustomConfig: PermissionGroup[] = [];
    this.permissionGroups.forEach(group => {
      let route = this.router.navigation.find(x => {
        // Match context
        let matchesContext = x.settings?.resource != null && KebabCase.fromCamelCase(x.settings.resource) == group.context;
        if (false == matchesContext) {
          return false;
        }
        // If crud, then action can be null.
        if (group.isCrud && x.settings?.action == null) {
          return true;
        }
        // Else action (or any action) must match an action in the group
        let routeActions: string[] = isArray(x.settings.action) ? x.settings.action : [x.settings.action];
        return group.permissions.some(p => routeActions.some(a => p.action == a));
      });

      let config = this.config.permissionsConfig?.perContext?.find(x =>
        (x.context != null || (route?.config?.name != null && x.routeName == null))
        && (x.context == null || KebabCase.fromCamelCase(x.context) == group.context)
        && (x.routeName == null || route?.config?.name == null || x.routeName == route.config.name)
      );

      if (config != null || route != null) {
        group.qualifiedName = Object.assign([], config?.qualifiedName ?? this.makeQualifiedName(route));
        group.href = config?.href ?? (route == null ? null : ('/' + route.relativeHref));
        group.hint = config?.hint;
        if (config != null) {
          // Config can override notion of if a group is a CRUD
          group.isCrud = config.isCrud ?? group.isCrud;
          group.forceType = config.forceType;
          if (false == group.isCrud) {
            group.qualifiedName.push(this.config.permissionsConfig.permissionNames[group.context][group.firstPermission.action]);
          }
          groupsUsingCustomConfig.push(group);
        } else if (route) {
          group.additionalScreens = this.router.navigation
            .filter(x => x.settings?.resource == route.settings.resource && x != route && (x.settings?.action == null || group.permissions.some(p => p.action == x.settings?.action)))
            .map(x => {
              let screen = new GroupBase();
              screen.qualifiedName = this.makeQualifiedName(x);
              screen.href = '/' + x.relativeHref;
              return screen;
            });
        }
      } else {
        console.warn('No menu found for ' + group.context);
        group.qualifiedName.push(group.context);
      }

    });

    // In case of a non-crud permission for an existing screen and the custom config is specified for crud permissions as well,
    // adapt the hierarchy (hierarchy points to crud screen, remove last element of crud qualified name)
    groupsUsingCustomConfig.filter(x => false == x.isCrud).forEach(group => {
      let customCrudGroup = groupsUsingCustomConfig.find(x => x.context == group.context && x.isCrud && x != group);
      if (customCrudGroup != null) {
        group.qualifiedName = [...customCrudGroup.hierarchy, group.name];
      }
    });

    this.sortGroups(this.permissionGroups);
  }

  private makeQualifiedName(route: NavModel): string[] {
    let qualifiedName: string[] = [route.title];
    // If subgroup defined, the menu item is on level 3
    if (route.settings.subGroup) {
      qualifiedName.push(route.settings.subGroup, route.settings.group);
    }
    // Else if there are multiple items in group, the menu item is on level 2
    else if (this.router.navigation.some(x => x.settings?.resource != route.settings.resource && x.settings.group == route.settings.group)) {
      qualifiedName.push(route.settings.group);
    }
    return qualifiedName.reverse();
  }

  private sortGroups(groups: PermissionGroup[]) {
    groups.sort((a, b) => {
      for (var i = 0; i < a.qualifiedName.length && i < b.qualifiedName.length; ++i) {
        let strA = this.i18n.tr(a.qualifiedName[i]) ?? a.qualifiedName[i];
        let strB = this.i18n.tr(b.qualifiedName[i]) ?? b.qualifiedName[i];
        let compare = strA.toLocaleLowerCase().localeCompare(strB.toLocaleLowerCase());
        if (compare != 0) {
          return compare;
        }
      }
      // Common parts are equivalent, shortest first
      return a.qualifiedName.length - b.qualifiedName.length;
    });
  }

  private addObservableOnPermissions() {
    this.permissionGroups.filter(x => x.isCrud).forEach(x => {
      let group = (x as CrudPermissionGroup);

      if (group.deletePermission && group.readPermission) {
        this.addObserver(group.deletePermission, group.readPermission);
      }
      if (group.updatePermission) {
        if (group.readPermission) {
          this.addObserver(group.updatePermission, group.readPermission);
        }
        if (group.createPermission) {
          this.addObserver(group.createPermission, group.updatePermission);
        }
      }
    }

    );
  }

  private addObserver = (observed, toSet) => {
    this.disposables.push(
      this.bindingEngine.propertyObserver(observed, "isSelected").subscribe(newVal => {
        if (this.isLoading == false && newVal) {
          toSet.isSelected = true;
        }
      })
    );
  };


  private toggleFilter() {
    this.filter.active = !this.filter.active;
    if (false == this.filter.active) {
      this.filter.searchGroup = null;
      this.filter.searchName = null;
      this.filter.showScreens = false;
      this.filter.showFunctionalities = false;
    }
  }

  private toggleTypeFilter(type: 'showScreens' | 'showFunctionalities') {
    this.filter[type] = !this.filter[type];
  }

  private filterFn = (group: PermissionGroup) => {
    let keep: boolean = true;
    if (this.filter.showFunctionalities != this.filter.showScreens) {
      // If both false, count as no filter active => show all
      // If both true, count as both selected in filter => show all
      if (this.filter.showFunctionalities) {
        keep = group.type == 'functionality';
      } else {
        keep = group.type == 'screen';
      }
    }
    // Show (un)assigned
    keep = keep && (
      (this.filter.showAssigned && group.isAnySelected)
      || (this.filter.showUnassigned && false == group.isAnySelected)
    );
    // Name search
    if (this.filter.searchName?.length > 0) {
      keep = keep && group.translatedName?.length > 0 && group.translatedName.toLocaleLowerCase().includes(this.filter.searchName.toLocaleLowerCase());
    }
    // Group/hierarchy search
    if (this.filter.searchGroup?.length > 0) {
      keep = keep && group.translatedHierarchy?.length > 0 && group.translatedHierarchy.toLocaleLowerCase().includes(this.filter.searchGroup.toLocaleLowerCase());
    }
    return keep;
  }

  @computedFrom('filter.showScreens', 'filter.showFunctionalities', 'filter.showAssigned', 'filter.showUnassigned')
  private get filteredPermissionGroups(): PermissionGroup[] {
    return this.permissionGroups
      .filter(group => {
        if (this.filter.showFunctionalities == this.filter.showScreens) {
          // If both false, count as no filter active => show all
          // If both true, count as both selected in filter => show all
          return true;
        } else if (this.filter.showFunctionalities) {
          return false == group.isCrud;
        } else {
          return group.isCrud;
        }
      })
      .filter(group => {
        if (this.filter.showAssigned && group.isAnySelected) {
          return true;
        }
        if (this.filter.showUnassigned && false == group.isAnySelected) {
          return true;
        }
        return false;
      });
  }

  public isSelectClicked(permission: Permission) {
    if (false == this.isLoading) {
      this.hasChanges = true;
    }
  }

  protected async beforeSave(): Promise<boolean | void> {
    let assignedPermissionsMap: { [key: string]: boolean } = this.permissionGroups.reduce((result: { [key: string]: boolean }, group) => {
      group.permissions.filter(p => p.isSelected).forEach(p => result[p.permissionId] = true);
      return result;
    }, {});
    // Create & delete RolePermissions
    for (var i = this.entity.permissions.length - 1; i >= 0; --i) {
      let rolePermission = this.entity.permissions[i];
      if (assignedPermissionsMap[rolePermission.permissionId] == null) {
        rolePermission.entityAspect.setDeleted();
      } else {
        // Don't need to create entity
        assignedPermissionsMap[rolePermission.permissionId] = false;
      }
    };
    Object.keys(assignedPermissionsMap)
      .filter(permissionId => assignedPermissionsMap[permissionId] === true)
      .forEach(permissionId => this.rolePermissionService.createEntity({ roleId: this.entity.id, permissionId }, true));
  }

  private newUserRole: UserRole;

  @computedFrom('newUserRole')
  private get canAddUser(): boolean {
    return this.newUserRole == null;
  }
  private async addUser() {
    if (this.canAddUser) {
      this.newUserRole = await this.userRoleService.createEntity({ roleId: -1 });
    }
  }

  private cancelAddUser() {
    this.newUserRole?.entityAspect.setDetached();
    this.newUserRole = null;
  }

  @computedFrom('newUserRole.userId')
  private get canApplyAddUser(): boolean {
    return this.newUserRole?.userId > 0;
  }

  private applyAddUser() {
    if (this.canApplyAddUser) {
      let newRole = this.newUserRole;
      this.newUserRole = null;

      // Handle re-creation of deleted user linking (deleted but not yet saved)
      let existing: UserRole = this.userRoleService.manager.getEntityByKey(EntityTypeNames.UserRole, [newRole.userId, this.entity.id]) as UserRole;
      if (existing?.entityAspect.entityState.isDeleted()) {
        newRole.entityAspect.rejectChanges();
        existing.entityAspect.rejectChanges();
      } else {
        newRole.roleId = this.entity.id;
      }
    }
  }

  private async removeUser(userRole: UserRole) {
    if (userRole.entityAspect.entityState.isAdded()) {
      userRole.entityAspect.setDetached();
    } else {
      userRole.entityAspect.setDeleted();
    }
  }
}

class Permission {
  context: string;
  action: string;
  group: PermissionGroup;
  private _isSelected: boolean;
  private _isLocked: boolean;

  constructor(permission: PermissionModel) {
    let split = permission.permissionId.split(':');
    this.action = split[0];
    this.context = split[1];
    this._isSelected = false;
    this._isLocked = permission.isLocked;
  }

  @computedFrom('_isSelected')
  public get isSelected() {
    return this._isSelected;
  }
  public set isSelected(value: boolean) {
    this._isSelected = value;
    this.group?.vm?.isSelectClicked(this);
  }
  @computedFrom('_isLocked')
  public get isLocked() {
    return this._isLocked;
  }

  @computedFrom('action', 'context')
  public get permissionId(): string {
    return `${this.action}:${this.context}`;
  }

  @computedFrom('action')
  public get isCrud(): boolean {
    return CrudActions.find(x => x == this.action) != null;
  }
}

class GroupBase {
  qualifiedName: string[] = [];
  href: string;

  @computedFrom('qualifiedName.length')
  public get name(): string {
    return this.qualifiedName?.last();
  }

  @computedFrom('qualifiedName.length')
  public get hierarchy(): string[] {
    return this.qualifiedName?.slice(0, -1);
  }
}
class PermissionGroup extends GroupBase {
  additionalScreens: GroupBase[] = [];
  hint?: string;
  isCrud: boolean;
  forceType: 'screen' | 'functionality' | null;

  constructor(public vm: RoleDetail, public permissions: Permission[]) {
    super();
    permissions.forEach(x => x.group = this);
    this.isCrud = false;
  }

  public get isAnySelected(): boolean {
    return this.permissions.find(x => x.isSelected) != null;
  }

  @computedFrom('forceType', 'isCrud')
  public get type(): 'screen' | 'functionality' {
    return this.forceType ?? (this.isCrud ? 'screen' : 'functionality');
  }

  @computedFrom('permissions[0]')
  public get firstPermission(): Permission {
    return this.permissions[0]
  }

  @computedFrom('firstPermission.context')
  public get context(): string {
    return this.firstPermission.context;
  }

  @computedFrom('name')
  public get translatedName(): string {
    return this.vm.i18n.tr(this.name);
  }

  @computedFrom('hierarchy.length')
  public get translatedHierarchy(): string {
    return this.hierarchy?.map(x => this.vm.i18n.tr(x)).join(' ');
  }

  public get isLocked(): boolean {
    return this.vm.entity?.isLocked || this.permissions.some(x => x.isLocked);
  }
}

class CrudPermissionGroup extends PermissionGroup {
  createPermission: Permission;
  readPermission: Permission;
  updatePermission: Permission;
  deletePermission: Permission;

  constructor(public vm: RoleDetail, permissions: Permission[]) {
    super(vm, permissions);
    permissions.forEach(p => this[p.action + "Permission"] = p);
    this.isCrud = true;
  }
}
