import {
  CdkVirtualScrollViewport,
  ScrollingModule,
} from '@angular/cdk/scrolling';
import { AsyncPipe } from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  Input,
  ViewChild,
  inject,
} from '@angular/core';
import { DialogService } from '@ngneat/dialog';
import {
  BehaviorSubject,
  Observable,
  combineLatest,
  debounceTime,
  filter,
  map,
  mergeMap,
  scan,
  switchMap,
  tap,
} from 'rxjs';
import {
  AccountingFilterArgs,
  AccountingState,
  Ticket,
  Tour,
} from '../../../core/data-access/graphql/generated/generated';
import { AccountingTourViewComponent } from '../accounting-tour-view/accounting-tour-view.component';
import { DashboardAccountingService } from '../dashboard-accounting.service';
import { AccountingItemComponent } from './accounting-item.component';
import { AccountingLaneHeaderComponent } from './accounting-lane-header.component';

@Component({
  selector: 'dks-accounting-lane',
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './accounting-lane.component.html',
  host: {
    class: 'rounded h-full w-full grid',
  },
  styles: [
    `
      :host {
        box-shadow: var(--mat-sys-level1);
        background-color: var(--mat-sys-surface-container-low);
        grid-template-rows: auto 1fr auto;
      }
    `,
  ],
  imports: [
    AccountingLaneHeaderComponent,
    AccountingItemComponent,
    AsyncPipe,
    ScrollingModule
],
})
export class AccountingLaneComponent {
  private dialog = inject(DialogService);
  private accountingService = inject(DashboardAccountingService);

  private accountingState$ = new BehaviorSubject<AccountingState | undefined>(
    undefined
  );
  private accountingFilter$ = new BehaviorSubject<AccountingFilterArgs>({});

  @Input() set accountingState(accountingState: AccountingState) {
    this.accountingState$.next(accountingState);
  }
  get accountingState() {
    return this.accountingState$.getValue() as AccountingState;
  }

  @ViewChild(CdkVirtualScrollViewport)
  viewport!: CdkVirtualScrollViewport;

  AccountingState = AccountingState;

  take = 20;
  hasNextPage = true;
  nextPage = '';

  cursor$ = new BehaviorSubject<string>('');
  accountingStateMeta$ = this.accountingState$.pipe(
    filter(Boolean),
    switchMap((accountingState) =>
      this.accountingService.fetchAccountingStateMeta(accountingState)
    )
  );
  tours$: Observable<(Tour & { ticket: Ticket })[]>;

  constructor() {
    const batchMap: Observable<Record<string, Tour & { ticket: Ticket }>> =
      combineLatest([this.cursor$, this.accountingFilter$]).pipe(
        debounceTime(200),
        mergeMap(([cursor, filter]) =>
          this.fetchTours(cursor, filter).pipe(
            map((tours) => ({ tours, cursor }))
          )
        ),
        scan((acc, { tours, cursor }) => {
          const normalizedCursor = this.getNormalizedCursor(cursor);
          const existingTours = Object.entries(acc)
            .filter(
              ([id]) =>
                id.split('_').at(0) !== normalizedCursor ||
                Object.keys(tours).includes(id.split('_').at(1) ?? '')
            )
            .reduce(
              (acc, [id, tour]) => ({
                ...acc,
                [`${normalizedCursor}_${id}`]: tour,
              }),
              {} as Record<string, Tour & { ticket: Ticket }>
            );

          return { ...existingTours, ...tours };
        }, {} as Record<string, Tour & { ticket: Ticket }>)
      );
    this.tours$ = batchMap.pipe(
      map((tourNodes) =>
        Object.values(tourNodes).sort(
          ({ createdAt: aCreated }, { createdAt: bCreated }) =>
            new Date(bCreated).getTime() - new Date(aCreated).getTime()
        )
      )
    );
  }

  openTourView(tour: Tour & { ticket: Ticket }): void {
    const dialogRef = this.dialog.open(AccountingTourViewComponent, {
      data: tour,
      width: '60vw',
      minHeight: '400px',
    });
    dialogRef.afterClosed$
      .pipe(tap((result) => result && this.openTourView(result)))
      .subscribe();
  }

  trackByFn(index: number, tour: Tour): string {
    return tour.id;
  }

  fetchNextPage() {
    if (!this.hasNextPage) {
      return;
    }
    const start = this.viewport.getRenderedRange().start;
    const end = this.viewport.getRenderedRange().end;
    const total = this.viewport.getDataLength();

    if (this.cursor$.getValue() !== '' && start === 0) {
      this.cursor$.next('');
    } else if (total - end < 5) {
      this.cursor$.next(this.nextPage);
    }
  }

  private fetchTours(
    cursor: string,
    filter: AccountingFilterArgs
  ): Observable<Record<string, Tour & { ticket: Ticket }>> {
    const normalizedCursor = this.getNormalizedCursor(cursor);

    return this.accountingService
      .fetchToursByState(this.accountingState, cursor, filter, this.take)
      .pipe(
        tap(({ endCursor, hasNextPage }) => {
          this.nextPage = endCursor;
          this.hasNextPage = hasNextPage;
        }),
        map(({ nodes }) =>
          nodes?.reduce(
            (acc, cur) => ({
              ...acc,
              [`${normalizedCursor}_${cur.id}`]: cur as Tour & {
                ticket: Ticket;
              },
            }),
            {} as Record<string, Tour & { ticket: Ticket }>
          )
        )
      );
  }

  private getNormalizedCursor(cursor: string) {
    return cursor === '' ? 'MA==' : cursor;
  }

  filterChanged(filter: AccountingFilterArgs) {
    this.accountingFilter$.next(filter);
    this.cursor$.next('');
  }
}
