import {Component, OnInit} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {Pentest} from '@shared/models/pentest.model';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import {NotificationService, PopupType} from '@shared/services/notification.service';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators';
import {
  Comment,
  CommentDialogBody,
  CommentEntry, RelatedFindingOption,
  transformCommentsToObjectiveEntries,
  transformCommentToRequestBody
} from '@shared/models/comment.model';
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {Store} from '@ngxs/store';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {CommentService} from '@shared/services/comment.service';
import {UpdatePentestComments} from '@shared/stores/project-state/project-state.actions';
import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component';
import {Finding} from '@shared/models/finding.model';
import {FindingService} from '@shared/services/finding.service';

@UntilDestroy()
@Component({
  selector: 'app-pentest-comments',
  templateUrl: './pentest-comments.component.html',
  styleUrls: ['./pentest-comments.component.scss']
})
export class PentestCommentsComponent implements OnInit {

  // HTML only
  readonly fa = FA;
  notStartedStatus: PentestStatus = PentestStatus.NOT_STARTED;

  pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
  objectiveFindings: RelatedFindingOption[] = [];
  // comments$: BehaviorSubject<Comment[]> = new BehaviorSubject<Comment[]>(null);
  loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);

  columns: Array<CommentColumns> = [
    CommentColumns.COMMENT_ID, CommentColumns.TITLE, CommentColumns.DESCRIPTION, CommentColumns.RELATED_FINDINGS, CommentColumns.ACTIONS
  ];
  dataSource: NbTreeGridDataSource<CommentEntry>;
  data: CommentEntry[] = [];

  getters: NbGetters<CommentEntry, CommentEntry> = {
    dataGetter: (node: CommentEntry) => node,
    childrenGetter: (node: CommentEntry) => node.childEntries || undefined,
    expandedGetter: (node: CommentEntry) => !!node.expanded,
  };

  constructor(private readonly commentService: CommentService,
              private readonly findingService: FindingService,
              private dataSourceBuilder: NbTreeGridDataSourceBuilder<CommentEntry>,
              private notificationService: NotificationService,
              private dialogService: DialogService,
              private commentDialogService: CommentDialogService,
              private store: Store) {
    this.dataSource = dataSourceBuilder.create(this.data, this.getters);
  }

  ngOnInit(): void {
    this.store.select(ProjectState.pentest).pipe(
      untilDestroyed(this)
    ).subscribe({
      next: (selectedPentest: Pentest) => {
        this.pentestInfo$.next(selectedPentest);
        this.loadCommentsData();
        this.requestFindingsData(selectedPentest.id);
      },
      error: err => {
        console.error(err);
      }
    });
  }

  loadCommentsData(): void {
    this.commentService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
      .pipe(
        untilDestroyed(this),
        /*filter(isNotNullOrUndefined),*/
        tap(() => this.loading$.next(true))
      )
      .subscribe({
        next: (comments: Comment[]) => {
          if (comments) {
            this.data = transformCommentsToObjectiveEntries(comments);
          } else {
            this.data = [];
          }
          this.dataSource.setData(this.data, this.getters);
          this.loading$.next(false);
        },
        error: err => {
          console.error(err);
          // ToDo: Implement again after proper lazy loading and routing
          // this.notificationService.showPopup('comment.popup.not.found', PopupType.FAILURE);
          this.loading$.next(false);
        }
      });
  }

  onClickAddComment(): void {
    this.commentDialogService.openCommentDialog(
      CommentDialogComponent,
      this.pentestInfo$.getValue().findingIds,
      this.objectiveFindings,
      null,
      {
        closeOnEsc: false,
        hasScroll: false,
        autoFocus: true,
        closeOnBackdropClick: false
      }
    ).pipe(
      filter(value => !!value),
      mergeMap((value: CommentDialogBody) =>
        this.commentService.saveComment(
          this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
          transformCommentToRequestBody(value)
        )
      ),
      untilDestroyed(this)
    ).subscribe({
      next: (newComment: Comment) => {
        this.store.dispatch(new UpdatePentestComments(newComment.id));
        this.loadCommentsData();
        this.notificationService.showPopup('comment.popup.save.success', PopupType.SUCCESS);
      },
      error: err => {
        console.error(err);
        this.notificationService.showPopup('comment.popup.save.failed', PopupType.FAILURE);
      }
    });
  }

  onClickEditComment(commentEntry): void {
    this.commentService.getCommentById(commentEntry.data.commentId).pipe(
      filter(isNotNullOrUndefined),
      untilDestroyed(this)
    ).subscribe({
      next: (existingComment: Comment) => {
        if (existingComment) {
          this.commentDialogService.openCommentDialog(
            CommentDialogComponent,
            this.pentestInfo$.getValue().findingIds,
            this.objectiveFindings,
            existingComment,
            {
              closeOnEsc: false,
              hasScroll: false,
              autoFocus: true,
              closeOnBackdropClick: false
            }
          ).pipe(
            filter(value => !!value),
            mergeMap((value: CommentDialogBody) =>
              this.commentService.updateComment(
                commentEntry.data.commentId,
                transformCommentToRequestBody(value)
              )
            ),
            untilDestroyed(this)
          ).subscribe({
            next: (updatedComment: Comment) => {
              this.loadCommentsData();
              this.notificationService.showPopup('comment.popup.update.success', PopupType.SUCCESS);
            },
            error: err => {
              console.error(err);
              this.notificationService.showPopup('comment.popup.update.failed', PopupType.FAILURE);
            }
          });
        } else {
          this.notificationService.showPopup('comment.popup.not.available', PopupType.INFO);
        }
      },
      error: err => {
        console.error(err);
      }
    });
  }

  requestFindingsData(pentestId: string): void {
    this.objectiveFindings = [];
    this.findingService.getFindingsByPentestId(pentestId).pipe(
      untilDestroyed(this)
    ).subscribe({
      next: (findings: Finding[]) => {
        findings.forEach(finding => this.objectiveFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption));
      },
      error: err => {
        console.error(err);
      }
    });
  }

  onClickDeleteComment(commentEntry): void {
    const message = {
      title: 'comment.delete.title',
      key: 'comment.delete.key',
      data: {name: commentEntry.data.title},
    };
    this.dialogService.openConfirmDialog(
      message
    ).onClose.pipe(
      filter((confirm) => !!confirm),
      switchMap(() => this.commentService.deleteCommentByPentestAndCommentId(
        this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
        commentEntry.data.commentId)
      ),
      catchError(() => {
        this.notificationService.showPopup('comment.popup.delete.failed', PopupType.FAILURE);
        return [];
      }),
      untilDestroyed(this)
    ).subscribe({
      next: (deletedComment: any) => {
        this.store.dispatch(new UpdatePentestComments(deletedComment.id));
        this.loadCommentsData();
        this.notificationService.showPopup('comment.popup.delete.success', PopupType.SUCCESS);
      }, error: error => {
        console.error(error);
      }
    });
  }

  // HTML only
  isLoading(): Observable<boolean> {
    return this.loading$.asObservable();
  }
}

enum CommentColumns {
  COMMENT_ID = 'commentId',
  TITLE = 'title',
  DESCRIPTION = 'description',
  RELATED_FINDINGS = 'relatedFindings',
  ACTIONS = 'actions'
}
