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

import { HttpClient } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, EMPTY, Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { asObservable } from '../../as-observable';
import { AUTH_OPTIONS } from '../../auth-options.token';
import { AuthenticationService } from '../../services/authentication.service';
import { AuthorizationService } from '../../services/authorization.service';
import { IMacmaAuthModuleConfig } from '../macma-auth.options';
import { AclEntry } from '../models/aclentry.model';
import { PageResponse } from '../models/page-response.model';
import { periodicRepeat } from '../rxjs/periodic-repeat';

@Injectable()
export class MacmaAuthorizationService extends AuthorizationService {
  private static READ_PRIVILEGE = 'read';
  private static ADD_PRIVILEGE = 'add';
  private static DELETE_PRIVILEGE = 'delete';
  private static MODIFY_PRIVILEGE = 'modify';
  private static EXECUTE_PRIVILEGE = 'execute';

  private permissionKeys = new Set<string>();

  private aclLoadedSubject = new BehaviorSubject<boolean>(false);

  refreshSubject = new BehaviorSubject<void>(void 0);

  private userAclSource = combineLatest([
    this.authenticationService.isAuthenticated().pipe(distinctUntilChanged()),
    asObservable(this.config.authServerBaseUrl ?? this.config.accessControlEndpoint),
    asObservable(this.config.authServerACLBaseUrl),
    asObservable(this.config.aclPath ?? '/access-management/v1/user/acl'),
    this.refreshSubject,
  ]).pipe(
    periodicRepeat(this.config.aclReloadIntervalMs),
    switchMap(([isAuthenticated, authServerUrl, authServerAclUrl, aclPath]) => {
      if (isAuthenticated) {
        const userAclUrl = (authServerAclUrl ?? authServerUrl) + aclPath;
        return this.http.get<PageResponse<AclEntry>>(userAclUrl).pipe(
          map((response) => response.items),
          catchError((error) => {
            console.error('Failed to load userAcl: ' + JSON.stringify(error));
            return EMPTY;
          })
        );
      } else {
        return of([]);
      }
    }),
    tap(() => this.aclLoadedSubject.next(true)),
    shareReplay(1)
  );

  private static createKey(aclEntry: AclEntry, roleName: string, privilege: string): string {
    return JSON.stringify([
      aclEntry.resourceId,
      roleName,
      privilege,
      aclEntry.resourceOwningTenantId,
      aclEntry.applicationId,
      aclEntry.resourceType,
    ]);
  }

  private static resourceMatches(
    aclEntry: AclEntry,
    resourceId: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ) {
    // "aclEntry.resourceOwningTenantId === '0'" is necessary here to support MES, where tenant is set to '0'
    return (
      aclEntry.resourceId === resourceId &&
      (aclEntry.resourceOwningTenantId === null ||
        aclEntry.resourceOwningTenantId === '0' ||
        aclEntry.resourceOwningTenantId === tenantId) &&
      (aclEntry.applicationId === null || aclEntry.applicationId === applicationId) &&
      (aclEntry.resourceType === null || aclEntry.resourceType === resourceType)
    );
  }

  constructor(
    @Inject(AUTH_OPTIONS) private config: IMacmaAuthModuleConfig,
    private authenticationService: AuthenticationService,
    private http: HttpClient
  ) {
    super();
    this.authenticationService.getRoles().subscribe((roles) => (this.roles = roles));
    this.userAclSource.subscribe((aclEntries) => {
      this.permissionKeys = this.fillPermissionKeys(aclEntries);
    });
  }

  canRead(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPermission(
      resource,
      MacmaAuthorizationService.READ_PRIVILEGE,
      tenantId,
      applicationId,
      resourceType
    );
  }

  canReadObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPermissionObservable(
      resource,
      MacmaAuthorizationService.READ_PRIVILEGE,
      tenantId,
      applicationId,
      resourceType
    );
  }

  canAdd(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPermission(resource, MacmaAuthorizationService.ADD_PRIVILEGE, tenantId, applicationId, resourceType);
  }

  canAddObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPermissionObservable(
      resource,
      MacmaAuthorizationService.ADD_PRIVILEGE,
      tenantId,
      applicationId,
      resourceType
    );
  }

  canModify(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPermission(
      resource,
      MacmaAuthorizationService.MODIFY_PRIVILEGE,
      tenantId,
      applicationId,
      resourceType
    );
  }

  canModifyObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPermissionObservable(
      resource,
      MacmaAuthorizationService.MODIFY_PRIVILEGE,
      tenantId,
      applicationId,
      resourceType
    );
  }

  canDelete(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPermission(
      resource,
      MacmaAuthorizationService.DELETE_PRIVILEGE,
      tenantId,
      applicationId,
      resourceType
    );
  }

  canDeleteObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPermissionObservable(
      resource,
      MacmaAuthorizationService.DELETE_PRIVILEGE,
      tenantId,
      applicationId,
      resourceType
    );
  }

  canExecute(resource: string, tenantId?: string, applicationId?: string, resourceType?: string): boolean {
    return this.hasPermission(
      resource,
      MacmaAuthorizationService.EXECUTE_PRIVILEGE,
      tenantId,
      applicationId,
      resourceType
    );
  }

  canExecuteObservable(
    resource: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return this.hasPermissionObservable(
      resource,
      MacmaAuthorizationService.EXECUTE_PRIVILEGE,
      tenantId,
      applicationId,
      resourceType
    );
  }

  /**
   * Returns true as soon as the acl is loaded the first time
   */
  get aclLoaded$(): Observable<boolean> {
    return this.aclLoadedSubject.asObservable();
  }

  private hasPermission(
    resource: string,
    privilege: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ) {
    return this.roles.some((roleName) => {
      // key2 is used here to support MES with ACV3 where tenant is always '0'
      const key = JSON.stringify([resource, roleName, privilege, tenantId, applicationId, resourceType]);
      const key2 = JSON.stringify([resource, roleName, privilege, '0', applicationId, resourceType]);
      return this.permissionKeys.has(key) || this.permissionKeys.has(key2);
    });
  }

  private hasPermissionObservable(
    resource: string,
    privilege: string,
    tenantId?: string,
    applicationId?: string,
    resourceType?: string
  ): Observable<boolean> {
    return combineLatest([this.userAclSource, this.authenticationService.getRoles()]).pipe(
      map(([aclEntries, roles]) => {
        return aclEntries.some((aclEntry) =>
          this.roleAndPermissionMatches(aclEntry, resource, roles, privilege, tenantId, applicationId, resourceType)
        );
      })
    );
  }

  private roleAndPermissionMatches(
    aclEntry,
    resourceId,
    roles,
    privilege,
    tenantId?,
    applicationId?,
    resourceType?
  ): boolean {
    return (
      MacmaAuthorizationService.resourceMatches(aclEntry, resourceId, tenantId, applicationId, resourceType) &&
      aclEntry.grants.some((grant) => roles.includes(grant.roleName) && grant.privileges.includes(privilege))
    );
  }

  private fillPermissionKeys(aclEntries: AclEntry[]) {
    const permissionKeys = new Set<string>();
    aclEntries.forEach((aclEntry) => {
      aclEntry.grants.forEach((grant) => {
        // only if the current user has the required role
        if (this.roles.includes(grant.roleName)) {
          grant.privileges.forEach((privilege) => {
            const key = MacmaAuthorizationService.createKey(aclEntry, grant.roleName, privilege);
            permissionKeys.add(key);
          });
        }
      });
    });
    return permissionKeys;
  }
}
