/*
 * Copyright (C) 2018 Robert Bosch GmbH Copyright (C) 2018 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import { BehaviorSubject, Observable } from 'rxjs';
import { ResourceSet } from '../models/authorization.model';
import { PRIVILEGE } from '../models/privileges.model';
import { map } from 'rxjs/operators';

export abstract class AuthorizationService {
  protected roles: string[];
  protected resourceSets$ = new BehaviorSubject<ResourceSet[]>([]);

  private static resourceSetMatches(
    resourceSet: ResourceSet,
    resource: string,
    tenantId?: string,
    applicationId?,
    resourceType?: string
  ): boolean {
    return (
      resourceSet.ResourceId === resource &&
      (tenantId === null || resourceSet.TenantId === tenantId) &&
      (applicationId === null || resourceSet.ApplicationId === applicationId) &&
      (resourceType === null || resourceSet.ResourceType === resourceType)
    );
  }

  // eslint-disable-next-line no-empty-function
  protected constructor() {}

  /**
   * checks, whether the logged in user have read-right.
   * @param resource resource to check against
   * @param tenantId tenant to which the application belongs
   * @param applicationId application to which the resource belongs.
   * @param resourceType resources of different types can have same resourceId.
   * Therefore the resourceType might be required to distinguish.
   */
  public canRead(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPrivilege(resource, PRIVILEGE.READ, tenantId, applicationId, resourceType);
  }

  /**
   * checks, whether the logged in user roles have modify-right.
   * @param resource resource to check against
   * @param tenantId tenant to which the application belongs
   * @param applicationId application to which the resource belongs.
   * @param resourceType resources of different types can have same resourceId.
   * Therefore the resourceType might be required to distinguish.
   */
  public canModify(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPrivilege(resource, PRIVILEGE.MODIFY, tenantId, applicationId, resourceType);
  }

  /**
   * checks, whether the logged in user have add-right.
   * @param resource resource to check against
   * @param tenantId tenant to which the application belongs
   * @param applicationId application to which the resource belongs.
   * @param resourceType resources of different types can have same resourceId.
   * Therefore the resourceType might be required to distinguish.
   */
  public canAdd(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPrivilege(resource, PRIVILEGE.ADD, tenantId, applicationId, resourceType);
  }

  /**
   * checks, whether the logged in user have delete-right.
   * @param resource resource to check against
   * @param tenantId tenant to which the application belongs
   * @param applicationId application to which the resource belongs.
   * @param resourceType resources of different types can have same resourceId.
   * Therefore the resourceType might be required to distinguish.
   */
  public canDelete(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPrivilege(resource, PRIVILEGE.DELETE, tenantId, applicationId, resourceType);
  }

  /**
   * checks, whether the logged in user have execute-right.
   * @param resource resource to check against
   * @param tenantId tenant to which the application belongs
   * @param applicationId application to which the resource belongs.
   * @param resourceType resources of different types can have same resourceId.
   * Therefore the resourceType might be required to distinguish.
   */
  public canExecute(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPrivilege(resource, PRIVILEGE.EXECUTE, tenantId, applicationId, resourceType);
  }

  /**
   * Returns resourceSets when they are undefined.
   */
  public getResourceSets(): Observable<ResourceSet[]> {
    return this.resourceSets$.asObservable();
  }

  /**
   * Checks whether logged in user has given privilege for given resource .
   */
  private hasPrivilege(
    resource: string,
    privilege: PRIVILEGE,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ) {
    if (!this.roles) {
      console.error("User's roles are unknown!");
      return false;
    }

    for (const role of this.roles) {
      /* tslint:disable:no-bitwise */
      const check =
        (privilege & this._getPrivilege(resource, role, tenantId, applicationId, resourceType)) === privilege;
      /* tslint:enable:no-bitwise */
      if (check) {
        return true;
      }
    }

    return false;
  }

  /**
   * Extracts privilege for given resource and role.
   */
  private _getPrivilege(
    resource: string,
    role: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): number {
    if (!this.resourceSets$.getValue()) {
      return 0;
    }
    const resourceSet = this.resourceSets$
      .getValue()
      .find((rs) => AuthorizationService.resourceSetMatches(rs, resource, tenantId, applicationId, resourceType));
    if (!resourceSet || !resourceSet.Permissions) {
      return 0;
    }
    const permission = resourceSet.Permissions.find((p) => p.RoleName === role);
    if (!permission) {
      return 0;
    }
    return permission.PrivilegesGranted;
  }

  public canReadObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPrivilegeObservable(resource, PRIVILEGE.READ, tenantId, applicationId, resourceType);
  }

  public canAddObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPrivilegeObservable(resource, PRIVILEGE.ADD, tenantId, applicationId, resourceType);
  }

  public canModifyObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPrivilegeObservable(resource, PRIVILEGE.MODIFY, tenantId, applicationId, resourceType);
  }

  public canDeleteObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPrivilegeObservable(resource, PRIVILEGE.DELETE, tenantId, applicationId, resourceType);
  }

  public canExecuteObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPrivilegeObservable(resource, PRIVILEGE.EXECUTE, tenantId, applicationId, resourceType);
  }

  private hasPrivilegeObservable(
    resource: string,
    privilege: PRIVILEGE,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this._getPrivilegeObservable(resource, tenantId, applicationId, resourceType).pipe(
      /* tslint:disable:no-bitwise */
      map((privilegesOnResources) => (privilege & privilegesOnResources) === privilege)
      /* tslint:enable:no-bitwise */
    );
  }

  /**
   * Extracts privilege for given resource and role.
   */
  private _getPrivilegeObservable(
    resource: string,
    tenantId?: string,
    applicationId?,
    resourceType?: string
  ): Observable<number> {
    return this.resourceSets$.pipe(
      map((resourceSets) => {
        if (!resourceSets || !this.roles) {
          return 0;
        }
        const resourceSet = resourceSets.find((rs) =>
          AuthorizationService.resourceSetMatches(rs, resource, tenantId, applicationId, resourceType)
        );
        if (!resourceSet || !resourceSet.Permissions) {
          return 0;
        }
        const perm = resourceSet.Permissions.find((p) => this.roles.includes(p.RoleName));
        if (!perm) {
          return 0;
        }
        return perm.PrivilegesGranted;
      })
    );
  }
}
