/*
 * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den
 * Ministerpräsidenten des Landes Schleswig-Holstein
 * Staatskanzlei
 * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
 *
 * Lizenziert unter der EUPL, Version 1.2 oder - sobald
 * diese von der Europäischen Kommission genehmigt wurden -
 * Folgeversionen der EUPL ("Lizenz");
 * Sie dürfen dieses Werk ausschließlich gemäß
 * dieser Lizenz nutzen.
 * Eine Kopie der Lizenz finden Sie hier:
 *
 * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
 *
 * Sofern nicht durch anwendbare Rechtsvorschriften
 * gefordert oder in schriftlicher Form vereinbart, wird
 * die unter der Lizenz verbreitete Software "so wie sie
 * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
 * ausdrücklich oder stillschweigend - verbreitet.
 * Die sprachspezifischen Genehmigungen und Beschränkungen
 * unter der Lizenz sind dem Lizenztext zu entnehmen.
 */
import {
  createErrorStateResource,
  createStateResource,
  isUnprocessableEntity,
  StateResource,
} from '@alfa-client/tech-shared';
import { SnackBarService } from '@alfa-client/ui';
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Resource } from '@ngxp/rest';
import { Observable, of, Subject, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { CommandEffects } from './+state/command.effects';
import { COMMAND_ERROR_MESSAGES } from './command.message';
import {
  CommandListResource,
  CommandResource,
  CreateCommand,
  CreateCommandProps,
} from './command.model';
import { CommandRepository } from './command.repository';
import { isConcurrentModification, isPending } from './command.util';

import * as Actions from './+state/command.actions';
import * as Selectors from './+state/command.selectors';

@Injectable({ providedIn: 'root' })
export class CommandService {
  intervalTimer: number = CommandEffects.POLL_DELAY;

  constructor(
    private repository: CommandRepository,
    private snackBarService: SnackBarService,
    private store: Store,
  ) {}

  /**
   * @deprecated Use createCommandByProps(createCommandProps: CreateCommandProps) instead.
   */
  public createCommand(
    resource: Resource,
    linkRel: string,
    command: CreateCommand,
  ): Observable<StateResource<CommandResource>> {
    return this.handleCommandResponse(this.repository.createCommand(resource, linkRel, command));
  }

  public revokeCommand(resource: CommandResource): Observable<StateResource<CommandResource>> {
    return this.handleCommandResponse(this.repository.revokeCommand(resource));
  }

  private handleCommandResponse(
    command$: Observable<CommandResource>,
  ): Observable<StateResource<CommandResource>> {
    return command$.pipe(
      mergeMap((command) => this.handleCommand(command)),
      catchError((errorResponse) => this.handleHttpError(errorResponse)),
    );
  }

  handleHttpError(errorResponse: HttpErrorResponse): Observable<StateResource<CommandResource>> {
    return of(this.handleErrorByStatus(errorResponse));
  }

  handleErrorByStatus(error: HttpErrorResponse): StateResource<CommandResource> {
    if (isUnprocessableEntity(error.status)) {
      return createErrorStateResource(error.error);
    }
    throwError({ error });
  }

  handleCommand(command: CommandResource): Observable<StateResource<CommandResource>> {
    return this.startPolling(command);
  }

  handleCommandError(command: CommandResource): Observable<StateResource<CommandResource>> {
    if (isConcurrentModification(command.errorMessage)) {
      this.snackBarService.showError(COMMAND_ERROR_MESSAGES[command.errorMessage]);
      this.store.dispatch(Actions.publishConcurrentModificationAction());
    }
    return of(createStateResource(command));
  }

  startPolling(commandResource: CommandResource): Observable<StateResource<CommandResource>> {
    return isPending(commandResource) ?
        this.pollCommand(commandResource)
      : of(createStateResource(commandResource));
  }

  public pollCommand(commandResource: CommandResource): Observable<StateResource<CommandResource>> {
    const interval: IntervallHandleWithTickObservable = startInterval(this.intervalTimer);
    return interval.tickObservable.pipe(
      mergeMap(() => this.getAndUpdate(commandResource)),
      tap((stateResource) => this.handleInterval(stateResource, interval)),
    );
  }

  handleInterval(
    stateResource: StateResource<CommandResource>,
    interval: IntervallHandleWithTickObservable,
  ): void {
    if (!stateResource.loading) this.clearInterval(interval.handle);
  }

  getAndUpdate(commandResource: CommandResource): Observable<StateResource<CommandResource>> {
    return this.getCommand(commandResource).pipe(
      map((res) => createStateResource(res, isPending(res))),
    );
  }

  getCommand(resource: CommandResource): Observable<CommandResource> {
    return this.repository.getCommand(resource);
  }

  clearInterval(handler: number): void {
    window.clearInterval(handler);
  }

  public getPendingCommands(resource: Resource, linkRel: string): Observable<CommandListResource> {
    return this.repository.getPendingCommands(resource, linkRel);
  }

  public getEffectedResource<T>(command: CommandResource): Observable<T> {
    return this.repository.getEffectedResource(command);
  }

  public createCommandByProps(
    createCommandProps: CreateCommandProps,
  ): Observable<StateResource<CommandResource>> {
    this.store.dispatch(Actions.createCommand(createCommandProps));
    return this.store.select(Selectors.commandByOrder(createCommandProps.command.order));
  }
}

export interface IntervallHandleWithTickObservable {
  handle: number;
  tickObservable: Observable<any>;
}

export function startInterval(interval: number): IntervallHandleWithTickObservable {
  const subj: Subject<any> = new Subject();
  // Workaround: cast return value of setInterval() to number because since TS 4.8 it thinks it must be "Timer"
  const handle = setInterval(() => subj.next('tick'), interval) as unknown as number;

  return { handle, tickObservable: subj.asObservable() };
}