import { AsyncPipe } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, inject, isDevMode, model, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { clamp } from '../../../shared/utils/math.util';
import { TuiLink } from '@taiga-ui/core/components/link';
import { TuiLoader } from '@taiga-ui/core/components/loader';
import { TuiTitle } from '@taiga-ui/core/components/title';
import { TuiTabs } from '@taiga-ui/kit/components/tabs';
import {
  catchError,
  combineLatest,
  distinctUntilChanged,
  map,
  of,
  startWith,
  switchMap,
  tap,
  timeout,
} from 'rxjs';

import { DevLogService } from '../../../core/devtools/dev-log.service';
import { isKeyboardTelemetryEvent } from '../../../core/keyboard/keyboard-payload.util';
import { UserErrorNotifyService } from '../../../core/notifications/user-error-notify.service';
import type { ParsedEvent } from '../../../core/models/api.types';
import { SessionsApiService } from '../../../core/services/sessions-api.service';
import { SessionViewTabComponent } from './session-view-tab/session-view-tab.component';
import { SessionInteractiveTabComponent } from './session-interactive-tab/session-interactive-tab.component';
import { SessionInfoTabComponent } from './session-info-tab/session-info-tab.component';
import { SessionFingerprintTabComponent } from './session-fingerprint-tab/session-fingerprint-tab.component';

@Component({
  selector: 'app-session-detail',
  imports: [
    AsyncPipe,
    RouterLink,
    TuiLink,
    TuiLoader,
    TuiTitle,
    ...TuiTabs,
    SessionViewTabComponent,
    SessionInteractiveTabComponent,
    SessionInfoTabComponent,
    SessionFingerprintTabComponent,
  ],
  templateUrl: './session-detail.html',
  styleUrl: './session-detail.css',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SessionDetailComponent {
  private readonly route = inject(ActivatedRoute);
  private readonly api = inject(SessionsApiService);
  private readonly userErrors = inject(UserErrorNotifyService);
  private readonly devLog = inject(DevLogService);

  protected readonly telemetryToMs = signal<number | null>(null);
  protected readonly recordingStartMs = signal<number | null>(null);
  protected readonly recordingEndMs = signal<number | null>(null);
  protected readonly excludeMouseMoves = model(true);
  protected readonly activeTabIndex = model(0);

  private readonly sessionId$ = this.route.paramMap.pipe(
    map((p) => Number(p.get('id'))),
    distinctUntilChanged(),
  );

  protected readonly vm$ = this.sessionId$.pipe(
    switchMap((id) => {
      if (!Number.isFinite(id)) {
        this.userErrors.notifyError(new Error('Некорректный идентификатор сессии'), 'Сессия');
        return of({ status: 'error' as const });
      }
      this.activeTabIndex.set(0);
      return this.api.getSession(id).pipe(
        map((detail) => ({ status: 'ok' as const, id, detail })),
        tap((state) => {
          if (state.status !== 'ok') {
            return;
          }
          const start = this.toUnixMs(state.detail.session.started_at);
          const end = this.toUnixMs(state.detail.session.ended_at);
          this.recordingStartMs.set(start);
          this.recordingEndMs.set(end);
          this.telemetryToMs.set(end ?? Date.now());
        }),
        catchError((e: HttpErrorResponse) => {
          this.userErrors.notifyError(e, 'Сессия');
          return of({ status: 'error' as const });
        }),
        startWith({ status: 'loading' as const }),
      );
    }),
  );

  protected readonly telemetry$ = combineLatest([
    this.sessionId$,
    toObservable(this.telemetryToMs),
    toObservable(this.recordingStartMs),
    toObservable(this.recordingEndMs),
  ]).pipe(
    switchMap(([id, toMs, recordingStartMs, recordingEndMs]) => {
      if (!Number.isFinite(id)) {
        return of({
          status: 'error' as const,
          toMs: 0,
          fromMs: 0,
          telemetry: [] as ParsedEvent[],
          parsedMeta: undefined,
        });
      }
      const now = Date.now();
      const lowerBound = recordingStartMs ?? 0;
      const upperBound = recordingEndMs ?? now;
      const normalizedTo = clamp(toMs ?? upperBound, lowerBound, upperBound);
      return this.api
        .getParsedEvents(id, lowerBound, normalizedTo)
        .pipe(
          timeout(12000),
          map((resp) => ({
            status: 'ok' as const,
            toMs: normalizedTo,
            fromMs: lowerBound,
            telemetry: Array.isArray(resp.events) ? resp.events : [],
            parsedMeta: {
              session_id: resp.session_id,
              count: resp.count,
            },
          })),
          tap((result) => {
            if (!isDevMode() || result.status !== 'ok') {
              return;
            }
            this.devLog.clearSource('telemetry');
            const keyboardEvents = result.telemetry.filter(isKeyboardTelemetryEvent);
            let rawEventJson: string;
            try {
              rawEventJson = JSON.stringify(keyboardEvents, null, 2);
            } catch {
              rawEventJson = '(не удалось сериализовать)';
            }
            this.devLog.add({
              level: 'info',
              source: 'telemetry',
              message: `События клавиатуры · ${keyboardEvents.length} шт.`,
              telemetryDetails: { rawEventJson },
            });
          }),
          catchError((e: unknown) => {
            this.userErrors.notifyError(e, 'Телеметрия');
            return of({
              status: 'error' as const,
              toMs: normalizedTo,
              fromMs: lowerBound,
              telemetry: [] as ParsedEvent[],
              parsedMeta: undefined,
            });
          }),
          startWith({
            status: 'loading' as const,
            toMs: normalizedTo,
            fromMs: lowerBound,
            telemetry: [] as ParsedEvent[],
            parsedMeta: undefined,
          }),
        );
    }),
  );

  private toUnixMs(value: string | null | undefined): number | null {
    if (!value) {
      return null;
    }
    const ms = new Date(value).getTime();
    return Number.isFinite(ms) ? ms : null;
  }
}
