import { type InfiniteData } from 'react-query';

import {
  type BaseStats,
  type CallStatsReport,
  type ScreenShareSettings,
} from '@stream-io/video-react-sdk';
import { type AxiosResponse } from 'axios';

import { queryClient } from 'src/config/react-query';
import { type Comment, type PostCommentPayload } from 'src/types/comments';
import { type GroupCommentList } from 'src/types/groups';
import {
  type BytesSample,
  FullScreenElementType,
  type MetricsSnapshot,
  PollStatus,
  SourceType,
  StreamDestination,
  StreamSource,
  type StreamTimeTick,
} from 'src/types/stream';
import { type MeUser } from 'src/types/user';

import { eventsRoutes } from '../events/routes';
import { groupsRoutes } from '../groups/routes';

import { FULL_SCREEN_COMMENT_WIDTH, STREAM_METRICS } from './constants';

export const isLive = (currentTime?: number, liveWindow?: number) => {
  if (currentTime === undefined || liveWindow === undefined) return false;
  return Math.abs(Math.floor(currentTime) - Math.floor(liveWindow)) <= 10;
};

export const getIsRtmpConnection = (
  streamSource: StreamSource | null,
  customDataIsRtmpConnection?: boolean,
): boolean =>
  streamSource
    ? streamSource === StreamSource.EXTERNAL
    : Boolean(customDataIsRtmpConnection);

export const getLiveStreamDestination = (
  pathname: string,
): StreamDestination => {
  if (pathname.includes(groupsRoutes.groups())) {
    return StreamDestination.GROUPS;
  }

  if (pathname.includes(eventsRoutes.events())) {
    return StreamDestination.EVENTS;
  }

  return StreamDestination.FEED;
};

export const getContentHint = (screenShareContentHint: {
  default: boolean;
  motion: boolean;
  text: boolean;
  detail: boolean;
}): ScreenShareSettings['contentHint'] => {
  if (screenShareContentHint.motion) return 'motion';
  if (screenShareContentHint.text) return 'text';
  if (screenShareContentHint.detail) return 'detail';
  return '';
};

export const calculatePillsTopMargin = ({
  spacing,
  hasOngoingScreenshare,
  fullScreenElement,
  participantViewElement,
  containerDimensions,
  source,
  hasVideo,
}: {
  spacing: number;
  hasOngoingScreenshare: boolean;
  fullScreenElement: FullScreenElementType;
  participantViewElement: HTMLDivElement | null | undefined;
  containerDimensions: { width: number; height: number };
  source: SourceType;
  hasVideo: boolean;
}) => {
  if (hasOngoingScreenshare || !hasVideo) return spacing;

  if (
    fullScreenElement !== FullScreenElementType.NONE &&
    participantViewElement &&
    source === SourceType.WEB &&
    containerDimensions.height > 0
  ) {
    const topBlackBorder = getTopBlackBoder(
      participantViewElement,
      containerDimensions.height,
      fullScreenElement === FullScreenElementType.SEMI,
    );
    return topBlackBorder + spacing;
  }

  return spacing;
};

export const getTopBlackBoder = (
  container: HTMLDivElement,
  screenHeight: number,
  isSemiFullScreen: boolean,
) => {
  const containerRect = container.getBoundingClientRect();
  const containerWidth = isSemiFullScreen
    ? containerRect.width - Number(FULL_SCREEN_COMMENT_WIDTH.replace('px', ''))
    : containerRect.width;
  const containerHeight = containerRect.height;
  const containerAspectRatio = containerWidth / containerHeight;
  const videoAspectRatio = 16 / 9;

  if (containerAspectRatio <= videoAspectRatio) {
    const videoHeight = containerWidth / videoAspectRatio;
    const result = (screenHeight - videoHeight) / 2;
    return result;
  }

  return 0;
};

export const optimistCommentUpdate = async (
  user: MeUser | null,
  newComment: PostCommentPayload,
  queryKey: (string | number)[],
) => {
  await queryClient.cancelQueries(queryKey);

  const previousComments =
    queryClient.getQueryData<
      InfiniteData<AxiosResponse<Comment[] | GroupCommentList>>
    >(queryKey);

  const newPages = previousComments?.pages.map(page => {
    const newCommentData = {
      ...newComment,
      user,
      createdAt: new Date(),
      updatedAt: new Date(),
    };

    if (Array.isArray(page.data)) {
      return {
        ...page,
        data: [newCommentData, ...page.data],
      };
    }

    return {
      ...page,
      data: {
        items: [newCommentData, ...page.data.items],
      },
    };
  });

  queryClient.setQueryData(queryKey, {
    pages: newPages,
    pageParams: previousComments?.pageParams,
  });

  return { previousComments };
};

export const getPollStatus = (
  startsAt: string | null,
  endsAt: string | null,
) => {
  if (startsAt) {
    return endsAt ? PollStatus.ENDED : PollStatus.ACTIVE;
  }

  return PollStatus.IDLE;
};

export const getPollPercentage = (value: number, total: number) => {
  const result = Math.round((value / total) * 100);
  if (isNaN(result)) return 0;
  return result;
};

export const getSelectedPollOptionId = (values: Record<string, boolean>) => {
  let result: number | null = null;
  Object.entries(values).forEach(([key, value]) => {
    if (value) {
      result = Number(key);
    }
  });
  return result;
};

export const pushMetricSample = (history: number[], value: number) =>
  [...history, value].slice(-STREAM_METRICS.HISTORY_LENGTH);

// The SDK does not report bitrate directly: derive it from the delta of
// total bytes between two consecutive stats reports
export const computeBitrateBps = (
  previous: BytesSample | undefined,
  current: BytesSample,
) => {
  if (!previous) return null;

  const deltaSeconds = (current.timestamp - previous.timestamp) / 1000;
  const deltaBytes = current.totalBytes - previous.totalBytes;
  if (deltaSeconds <= 0 || deltaBytes < 0) return null;

  return (deltaBytes * 8) / deltaSeconds;
};

export const formatBitrate = (bps: number) => {
  if (bps >= 1_000_000) return `${Math.round(bps / 1_000_000)} Mbps`;

  return `${Math.round(bps / 1_000)} Kbps`;
};

export const formatFrameRate = (fps: number) => `${Math.round(fps)} fps`;

export const formatResolution = (frameHeight?: number) =>
  frameHeight ? `${frameHeight}p` : null;

// Codecs come as a mime type (e.g. `audio/opus`) → display `OPUS`
export const formatCodec = (codec?: string) => {
  if (!codec) return null;

  const [, subtype = codec] = codec.split('/');

  return subtype.toUpperCase();
};

// Minute ticks (every TICK_INTERVAL_MINUTES) across the sparkline window.
// Positions are anchored to the window start quantized down to a whole tick,
// so the labels sit on static slots and the whole axis shifts one slot only
// when a new minute enters the (full) buffer from the right.
// Labels are in live elapsed time: the minute grid is anchored to the live
// start, and while the live has not started every slot reads `0m`
export const getTimeTicks = (
  sampleTimes: number[],
  liveStartSeconds?: number,
): StreamTimeTick[] => {
  const first = sampleTimes[0] ?? 0;
  const windowSeconds =
    STREAM_METRICS.SAMPLE_INTERVAL_SECONDS *
    (STREAM_METRICS.HISTORY_LENGTH - 1);
  const tickSeconds =
    STREAM_METRICS.TICK_INTERVAL_MINUTES * STREAM_METRICS.SECONDS_PER_MINUTE;
  const originSeconds = liveStartSeconds ?? first;
  const windowStartSeconds =
    originSeconds +
    Math.floor((first - originSeconds) / tickSeconds) * tickSeconds;

  const ticks: StreamTimeTick[] = [];
  for (
    let tickTime = windowStartSeconds;
    tickTime <= windowStartSeconds + windowSeconds;
    tickTime += tickSeconds
  ) {
    const liveMinutes =
      liveStartSeconds !== undefined
        ? Math.round(
            Math.max(
              0,
              (tickTime - liveStartSeconds) / STREAM_METRICS.SECONDS_PER_MINUTE,
            ),
          )
        : 0;

    ticks.push({
      label: `${liveMinutes}m`,
      fraction: (tickTime - windowStartSeconds) / windowSeconds,
    });
  }

  return ticks;
};

// Keep edge labels inside the chart: the first hangs right, the last left
export const getTickTranslateX = (fraction: number) => {
  if (fraction === 0) return '0';
  if (fraction === 1) return '-100%';

  return '-50%';
};

// Maps the rolling buffer to SVG points: x spans the full history window so
// the line fills in left-to-right and scrolls once the buffer is full
export const getSparklinePoints = (values: number[]) => {
  const min = Math.min(...values);
  const max = Math.max(...values);
  const range = max - min;
  const { SPARKLINE } = STREAM_METRICS;
  const innerHeight = SPARKLINE.HEIGHT - SPARKLINE.STROKE_WIDTH * 2;
  const stepX = SPARKLINE.WIDTH / (STREAM_METRICS.HISTORY_LENGTH - 1);

  return values.map((value, index) => {
    const normalized = range > 0 ? (value - min) / range : 0.5;
    const x = index * stepX;
    const y = SPARKLINE.STROKE_WIDTH + (1 - normalized) * innerHeight;

    return { x, y };
  });
};

export const toRateMetric = (
  history: number[],
  format: (value: number) => string,
) => {
  const lastValue = history.at(-1);

  return {
    formattedValue: lastValue !== undefined ? format(lastValue) : null,
    history,
  };
};

// Webcam host → stats of the host's own published tracks
export const getPublisherSnapshot = (
  report: CallStatsReport,
): MetricsSnapshot => ({
  videoBytes: report.publisherStats.totalBytesSent,
  audioBytes: report.publisherAudioStats.totalBytesSent,
  timestamp: report.timestamp,
  frameRate: report.publisherStats.camera?.frameRate,
  frameHeight:
    report.publisherStats.camera?.frameHeight ||
    report.publisherStats.highestFrameHeight ||
    undefined,
  audioCodec: report.publisherAudioStats.codec,
});

// Software streaming / RTMP (OBS) → the source is a separate participant the
// host subscribes to via `call.startReportingStatsFor(sessionId)`
export const getRtmpSnapshot = (
  report: CallStatsReport,
  sessionId: string,
): MetricsSnapshot | null => {
  const trackReports = report.participants[sessionId];
  if (!trackReports?.length) return null;
  const streams = trackReports.flatMap(trackReport => trackReport.streams);
  const videoStreams = streams.filter(stream => stream.kind === 'video');
  const audioStreams = streams.filter(stream => stream.kind === 'audio');
  if (!videoStreams.length && !audioStreams.length) return null;

  const sumBytes = (streams: BaseStats[]) =>
    streams.reduce((total, stream) => total + (stream.bytesReceived ?? 0), 0);

  const maxOf = (streams: BaseStats[], pick: (stream: BaseStats) => number) => {
    const max = Math.max(...streams.map(pick));

    return max > 0 ? max : undefined;
  };

  return {
    videoBytes: sumBytes(videoStreams),
    audioBytes: sumBytes(audioStreams),
    timestamp: report.timestamp,
    frameRate: maxOf(videoStreams, stream => stream.framesPerSecond ?? 0),
    frameHeight: maxOf(videoStreams, stream => stream.frameHeight ?? 0),
    audioCodec: audioStreams.find(stream => stream.codec)?.codec,
  };
};
