import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TuiButton } from '@taiga-ui/core/components/button';
import { TuiTextarea } from '@taiga-ui/kit/components/textarea';

import { applyKeyboardPressToTranscriptBuffer } from '../../../../core/keyboard/keyboard-transcript.util';
import {
  isKeyboardTelemetryEvent,
  parseKeyboardAction,
  parseKeyboardHighlightKeyIds,
} from '../../../../core/keyboard/keyboard-payload.util';
import {
  isMouseMoveTelemetryEvent,
  isMouseTelemetryEvent,
  parseMouseHighlightTargets,
} from '../../../../core/mouse/mouse-payload.util';
import type { MouseHighlightTarget } from '../../../../core/mouse/mouse-payload.util';
import { SessionsApiService } from '../../../../core/services/sessions-api.service';
import type { ParsedEvent, SessionDetailResponse } from '../../../../core/models/api.types';
import { formatClockTime, formatUnixMs } from '../../../../shared/utils/date-time.util';
import { clamp } from '../../../../shared/utils/math.util';
import { resolveActiveStreamType, resolvePlaylistUrlForType } from '../../../../shared/utils/stream.util';
import { HlsPlayerComponent } from '../../hls-player/hls-player.component';
import { StreamSelectorComponent } from '../../stream-selector/stream-selector.component';
import { KeyboardViewComponent } from '../../keyboard-view/keyboard-view.component';
import { MouseViewComponent } from '../../mouse-view/mouse-view.component';
import { environment } from '../../../../../environments/environment';

const INTERACTIVE_PREROLL_MS = Number.isFinite(environment.interactivePrerollMs)
  ? Math.max(0, environment.interactivePrerollMs)
  : 4000;

/** Минимальная длительность подсветки нажатия (мс); при перекрытии побеждает более позднее нажатие. */
const KEY_HIGHLIGHT_MIN_MS = 500;

@Component({
  selector: 'app-session-interactive-tab',
  imports: [
    FormsModule,
    TuiButton,
    ...TuiTextarea,
    HlsPlayerComponent,
    StreamSelectorComponent,
    KeyboardViewComponent,
    MouseViewComponent,
  ],
  templateUrl: './session-interactive-tab.html',
  styleUrl: './session-interactive-tab.css',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SessionInteractiveTabComponent {
  private readonly api = inject(SessionsApiService);

  readonly detail = input.required<SessionDetailResponse>();
  readonly telemetryEvents = input.required<ParsedEvent[]>();
  readonly recordingStartMs = input.required<number | null>();
  readonly recordingEndMs = input.required<number | null>();
  readonly excludeMouseMoves = input<boolean>(true);

  protected readonly selectedStreamType = signal<string | null>(null);
  protected readonly timelineSec = signal(0);
  protected readonly durationSec = signal<number | null>(null);
  protected readonly isPlaying = signal(false);
  protected readonly timelineMaxSec = computed(() => {
    const videoDuration = this.durationSec();
    if (videoDuration != null && Number.isFinite(videoDuration) && videoDuration > 0) {
      return videoDuration;
    }
    const start = this.recordingStartMs();
    const end = this.recordingEndMs();
    if (start == null || end == null || end <= start) {
      return 0;
    }
    return (end - start) / 1000;
  });

  /**
   * Time anchor for timeline -> telemetry conversion.
   * `session.started_at` can be slightly shifted vs actual stream start,
   * so we additionally consider the earliest parsed telemetry timestamp.
   */
  private readonly telemetryAnchorStartMs = computed(() => {
    const sessionStart = this.recordingStartMs();
    const firstTelemetryTs = this.telemetryEvents().reduce<number | null>((minTs, e) => {
      const ts = e.timestamp;
      if (!Number.isFinite(ts)) {
        return minTs;
      }
      return minTs == null ? ts : Math.min(minTs, ts);
    }, null);
    if (sessionStart == null) {
      return firstTelemetryTs;
    }
    if (firstTelemetryTs == null) {
      return sessionStart;
    }
    return Math.min(sessionStart, firstTelemetryTs);
  });

  private readonly cursorMs = computed(() => {
    const start = this.telemetryAnchorStartMs();
    if (start == null) {
      return null;
    }
    return start + this.timelineSec() * 1000 - INTERACTIVE_PREROLL_MS;
  });

  /**
   * Pre-sorted keyboard events — recomputed only when telemetryEvents changes,
   * not on every slider tick.
   */
  private readonly sortedKeyboardEvents = computed(() =>
    this.telemetryEvents()
      .filter(isKeyboardTelemetryEvent)
      .sort((a, b) => a.timestamp - b.timestamp),
  );

  /**
   * Только `press`. Группировка по целой секунде unix-времени: floor(ts/1000)*1000.
   * Внутри секунды n нажатий в порядке телеметрии получают непересекающиеся интервалы
   * [T + i/n·1000, T + (i+1)/n·1000); фактическая подсветка держится минимум KEY_HIGHLIGHT_MIN_MS.
   */
  private readonly keyboardPressDisplaySlots = computed(
    (): { startMs: number; endMs: number; keyIds: string[]; data: unknown }[] => {
      const events = this.sortedKeyboardEvents();
      type RawPress = { ts: number; keyIds: string[]; data: unknown };
      const raw: RawPress[] = [];
      for (const event of events) {
        if (parseKeyboardAction(event.data) !== 'press') {
          continue;
        }
        const keyIds = parseKeyboardHighlightKeyIds(event.data);
        if (keyIds.length === 0) {
          continue;
        }
        const ts = event.timestamp;
        if (!Number.isFinite(ts)) {
          continue;
        }
        raw.push({ ts, keyIds, data: event.data });
      }
      const out: { startMs: number; endMs: number; keyIds: string[]; data: unknown }[] = [];
      let i = 0;
      while (i < raw.length) {
        const secStartMs = Math.floor(raw[i]!.ts / 1000) * 1000;
        let j = i + 1;
        while (j < raw.length && Math.floor(raw[j]!.ts / 1000) * 1000 === secStartMs) {
          j++;
        }
        const n = j - i;
        for (let k = 0; k < n; k++) {
          const startMs = secStartMs + (k / n) * 1000;
          const endMs = secStartMs + ((k + 1) / n) * 1000;
          out.push({ startMs, endMs, keyIds: raw[i + k]!.keyIds, data: raw[i + k]!.data });
        }
        i = j;
      }
      return out;
    },
  );

  /**
   * Текст, «набранный» к моменту позиции на timeline (порядок с тем же распределением по секунде).
   */
  protected readonly interactiveTypedText = computed((): string => {
    const cursorMs = this.cursorMs();
    if (cursorMs == null) {
      return '';
    }
    let buffer = '';
    for (const slot of this.keyboardPressDisplaySlots()) {
      if (slot.startMs > cursorMs) {
        break;
      }
      buffer = applyKeyboardPressToTranscriptBuffer(buffer, slot.data);
    }
    return buffer;
  });

  /**
   * Pre-sorted mouse events — recomputed only when telemetryEvents changes.
   */
  private readonly sortedMouseEvents = computed(() =>
    this.telemetryEvents()
      .filter((e) => {
        if (!isMouseTelemetryEvent(e)) {
          return false;
        }
        return !this.excludeMouseMoves() || !isMouseMoveTelemetryEvent(e);
      })
      .sort((a, b) => a.timestamp - b.timestamp),
  );

  /**
   * Клавиши слота с наибольшим `startMs`, для которого cursorMs попадает в
   * [startMs, max(endMs, startMs + KEY_HIGHLIGHT_MIN_MS)).
   */
  protected readonly pressedKeyIds = computed((): ReadonlySet<string> => {
    const cursorMs = this.cursorMs();
    if (cursorMs == null) {
      return new Set();
    }
    const slots = this.keyboardPressDisplaySlots();
    for (let idx = slots.length - 1; idx >= 0; idx--) {
      const s = slots[idx]!;
      if (s.startMs > cursorMs) {
        continue;
      }
      const visibleUntilMs = Math.max(s.endMs, s.startMs + KEY_HIGHLIGHT_MIN_MS);
      if (cursorMs < visibleUntilMs) {
        return new Set(s.keyIds);
      }
    }
    return new Set();
  });

  protected readonly mouseTargets = computed(() => {
    const cursorMs = this.cursorMs();
    if (cursorMs == null) {
      return [];
    }
    // Find the last mouse event at or before cursorMs.
    // Events are sorted ascending; iterate backwards for efficiency.
    const events = this.sortedMouseEvents();
    for (let i = events.length - 1; i >= 0; i--) {
      const event = events[i]!;
      if (event.timestamp <= cursorMs) {
        return parseMouseHighlightTargets(event.data);
      }
    }
    return [] as MouseHighlightTarget[];
  });

  protected readonly heatmapBuckets = computed(() => {
    const BUCKETS_COUNT = 150; // Более точная разбивка (150 баров)
    const events = this.telemetryEvents();
    if (!events?.length) {
      return [];
    }

    const startMs = this.telemetryAnchorStartMs();
    const durationSec = this.timelineMaxSec();

    if (startMs == null || durationSec <= 0) {
      return [];
    }

    const buckets = new Array(BUCKETS_COUNT).fill(0);
    const bucketDurationMs = (durationSec * 1000) / BUCKETS_COUNT;

    let maxCount = 0;
    for (const e of events) {
      if (!Number.isFinite(e.timestamp)) {
        continue;
      }
      // Относительное время от старта
      const offsetMs = e.timestamp - startMs;
      if (offsetMs < 0 || offsetMs > durationSec * 1000) {
        continue;
      }

      const bucketIdx = Math.min(Math.floor(offsetMs / bucketDurationMs), BUCKETS_COUNT - 1);
      buckets[bucketIdx]++;
      if (buckets[bucketIdx] > maxCount) {
        maxCount = buckets[bucketIdx];
      }
    }

    return buckets.map((count, idx) => {
      // Нормализуем opacity: фоновый цвет почти прозрачен, активные ярче.
      const rawOpacity = count > 0 ? Math.max(0.15, count / maxCount) : 0.03;
      return {
        index: idx,
        count,
        opacity: rawOpacity,
        startSec: (idx * bucketDurationMs) / 1000,
      };
    });
  });

  protected activeStreamType(): string | null {
    return resolveActiveStreamType(this.detail().streams, this.selectedStreamType());
  }

  protected playlistUrl(): string | null {
    return resolvePlaylistUrlForType(this.detail().streams, this.activeStreamType(), this.api);
  }

  protected pickStream(type: string): void {
    this.selectedStreamType.set(type);
  }

  protected setTimelineSec(value: string): void {
    const parsed = Number(value);
    if (!Number.isFinite(parsed)) {
      return;
    }
    this.timelineSec.set(clamp(parsed, 0, this.timelineMaxSec()));
  }

  protected onPlayerCurrentTimeChange(seconds: number): void {
    if (!Number.isFinite(seconds)) {
      return;
    }
    this.timelineSec.set(clamp(seconds, 0, this.timelineMaxSec()));
  }

  protected onPlayerDurationChange(seconds: number): void {
    if (!Number.isFinite(seconds) || seconds <= 0) {
      return;
    }
    this.durationSec.set(seconds);
    this.timelineSec.update((current) => clamp(current, 0, this.timelineMaxSec()));
  }

  protected shiftTimeline(deltaSec: number): void {
    this.timelineSec.update((current) => clamp(current + deltaSec, 0, this.timelineMaxSec()));
  }

  protected togglePlayback(): void {
    this.isPlaying.update((v) => !v);
  }

  protected onPlayerPlayingChange(playing: boolean): void {
    this.isPlaying.set(playing);
  }

  protected currentPositionLabel(): string {
    return `${Math.max(0, Math.round(this.timelineSec()))}с`;
  }

  protected endPositionLabel(): string {
    return `${Math.max(0, Math.round(this.timelineMaxSec()))}с`;
  }

  protected startTimeLabel(): string {
    return formatClockTime(this.recordingStartMs());
  }

  protected endTimeLabel(): string {
    const end = this.recordingEndMs();
    const start = this.recordingStartMs();
    if (end != null) {
      return formatClockTime(end);
    }
    const max = this.timelineMaxSec();
    if (start != null && Number.isFinite(max)) {
      return formatClockTime(start + max * 1000);
    }
    return '—';
  }

  protected cursorLabel(): string {
    return formatUnixMs(this.cursorMs());
  }
}
