// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as i18n from '../../../core/i18n/i18n.js';
import * as Handlers from '../handlers/handlers.js';
import * as Helpers from '../helpers/helpers.js';
import type {SyntheticInteractionPair} from '../types/TraceEvents.js';
import type * as Types from '../types/types.js';

import {
  InsightCategory,
  InsightKeys,
  type InsightModel,
  type InsightSetContext,
  type PartialInsightModel,
} from './types.js';

export const UIStrings = {
  /**
   * @description Text to tell the user about the longest user interaction.
   */
  description:
      'Start investigating [how to improve INP](https://developer.chrome.com/docs/performance/insights/inp-breakdown) by looking at the longest subpart.',
  /**
   * @description Title for the performance insight "INP breakdown", which shows a breakdown of INP by subparts / sections.
   */
  title: 'INP breakdown',
  /**
   * @description Label used for the subpart/component/stage/section of a larger duration.
   */
  subpart: 'Subpart',
  /**
   * @description Label used for a time duration.
   */
  duration: 'Duration',

  // TODO: these are repeated in InteractionBreakdown. Add a place for common strings?
  /**
   * @description Text shown next to the interaction event's input delay time in the detail view.
   */
  inputDelay: 'Input delay',
  /**
   * @description Text shown next to the interaction event's thread processing duration in the detail view.
   */
  processingDuration: 'Processing duration',
  /**
   * @description Text shown next to the interaction event's presentation delay time in the detail view.
   */
  presentationDelay: 'Presentation delay',
  /**
   * @description Text status indicating that no user interactions were detected.
   */
  noInteractions: 'No interactions detected',
} as const;

const str_ = i18n.i18n.registerUIStrings('models/trace/insights/INPBreakdown.ts', UIStrings);
export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export type INPBreakdownInsightModel = InsightModel<typeof UIStrings, {
  longestInteractionEvent?: SyntheticInteractionPair,
  highPercentileInteractionEvent?: SyntheticInteractionPair,
}>;

export function isINPBreakdownInsight(insight: InsightModel): insight is INPBreakdownInsightModel {
  return insight.insightKey === InsightKeys.INP_BREAKDOWN;
}

function finalize(partialModel: PartialInsightModel<INPBreakdownInsightModel>): INPBreakdownInsightModel {
  let state: INPBreakdownInsightModel['state'] = 'pass';
  if (partialModel.longestInteractionEvent) {
    const classification = Handlers.ModelHandlers.UserInteractions.scoreClassificationForInteractionToNextPaint(
        partialModel.longestInteractionEvent.dur);
    if (classification === Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.GOOD) {
      state = 'informative';
    } else {
      state = 'fail';
    }
  }

  return {
    insightKey: InsightKeys.INP_BREAKDOWN,
    strings: UIStrings,
    title: i18nString(UIStrings.title),
    description: i18nString(UIStrings.description),
    docs: 'https://developer.chrome.com/docs/performance/insights/inp-breakdown',
    category: InsightCategory.INP,
    state,
    ...partialModel,
  };
}

export function generateInsight(
    data: Handlers.Types.HandlerData, context: InsightSetContext): INPBreakdownInsightModel {
  const interactionEvents = data.UserInteractions.interactionEventsWithNoNesting.filter(event => {
    return Helpers.Timing.eventIsInBounds(event, context.bounds);
  });

  if (!interactionEvents.length) {
    // A valid result, when there is no user interaction.
    return finalize({});
  }

  const longestByInteractionId = new Map<number, SyntheticInteractionPair>();
  for (const event of interactionEvents) {
    const key = event.interactionId;
    const longest = longestByInteractionId.get(key);
    if (!longest || event.dur > longest.dur) {
      longestByInteractionId.set(key, event);
    }
  }
  const normalizedInteractionEvents = [...longestByInteractionId.values()];
  normalizedInteractionEvents.sort((a, b) => b.dur - a.dur);

  // INP is the "nearest-rank"/inverted_cdf 98th percentile, except Chrome only
  // keeps the 10 worst events around, so it can never be more than the 10th from
  // last array element. To keep things simpler, sort desc and pick from front.
  // See https://source.chromium.org/chromium/chromium/src/+/main:components/page_load_metrics/browser/responsiveness_metrics_normalization.cc;l=45-59;drc=cb0f9c8b559d9c7c3cb4ca94fc1118cc015d38ad
  const highPercentileIndex = Math.min(9, Math.floor(normalizedInteractionEvents.length / 50));

  return finalize({
    relatedEvents: [normalizedInteractionEvents[0]],
    longestInteractionEvent: normalizedInteractionEvents[0],
    highPercentileInteractionEvent: normalizedInteractionEvents[highPercentileIndex],
  });
}

/**
 * If `subpart` is -1, then all subparts are included. Otherwise it's just that index.
 **/
export function createOverlaysForSubpart(
    event: Types.Events.SyntheticInteractionPair, subpartIndex = -1): Types.Overlays.Overlay[] {
  const p1 = Helpers.Timing.traceWindowFromMicroSeconds(
      event.ts,
      (event.ts + event.inputDelay) as Types.Timing.Micro,
  );
  const p2 = Helpers.Timing.traceWindowFromMicroSeconds(
      p1.max,
      (p1.max + event.mainThreadHandling) as Types.Timing.Micro,
  );
  const p3 = Helpers.Timing.traceWindowFromMicroSeconds(
      p2.max,
      (p2.max + event.presentationDelay) as Types.Timing.Micro,
  );
  let sections = [
    {bounds: p1, label: i18nString(UIStrings.inputDelay), showDuration: true},
    {bounds: p2, label: i18nString(UIStrings.processingDuration), showDuration: true},
    {bounds: p3, label: i18nString(UIStrings.presentationDelay), showDuration: true},
  ];
  if (subpartIndex !== -1) {
    sections = [sections[subpartIndex]];
  }

  return [
    {
      type: 'TIMESPAN_BREAKDOWN',
      sections,
      renderLocation: 'BELOW_EVENT',
      entry: event,
    },
  ];
}

export function createOverlays(model: INPBreakdownInsightModel): Types.Overlays.Overlay[] {
  const event = model.longestInteractionEvent;
  if (!event) {
    return [];
  }

  return createOverlaysForSubpart(event);
}
