// Copyright 2026 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 type * as Handlers from '../handlers/handlers.js';
import type * as Types from '../types/types.js';

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

export const UIStrings = {
  /**
   * @description Title of an insight that checks whether the page declares a character encoding early enough.
   */
  title: 'Declare a character encoding',
  /**
   * @description Description of an insight that checks whether the page has a proper character encoding declaration via HTTP header or early meta tag.
   */
  description:
      'A character encoding declaration is required. It can be done with a meta charset tag in the first 1024 bytes of the HTML or in the Content-Type HTTP response header. [Learn more about declaring the character encoding](https://developer.chrome.com/docs/insights/charset/).',
  /**
   * @description Text to tell the user that the charset is declared in the Content-Type HTTP response header.
   */
  passingHttpHeader: 'Declares charset in HTTP header',
  /**
   * @description Text to tell the user that the charset is NOT declared in the Content-Type HTTP response header.
   */
  failedHttpHeader: 'Does not declare charset in HTTP header',
  /**
   * @description Text to tell the user that a meta charset tag was found in the first 1024 bytes of the HTML.
   */
  passingMetaCharsetEarly: 'Declares charset using a meta tag in the first 1024 bytes',
  /**
   * @description Text to tell the user that a meta charset tag was found, but too late in the HTML.
   */
  failedMetaCharsetLate: 'Declares charset using a meta tag after the first 1024 bytes',
  /**
   * @description Text to tell the user that no meta charset tag was found in the HTML.
   */
  failedMetaCharsetMissing: 'Does not declare charset using a meta tag',
  /**
   * @description Text to tell the user that trace data did not include the Blink signal for meta charset.
   */
  failedMetaCharsetUnknown: 'Could not determine meta charset declaration from trace',
} as const;

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

const CHARSET_HTTP_REGEX = /charset\s*=\s*[a-zA-Z0-9\-_:.()]{2,}/i;

export type CharacterSetInsightModel = InsightModel<typeof UIStrings, {
  data?: {
    hasHttpCharset: boolean,
    checklist: Checklist<'httpCharset'|'metaCharset'>,
    metaCharsetDisposition?: Types.Events.MetaCharsetDisposition,
    documentRequest?: Types.Events.SyntheticNetworkRequest,
  },
}>;

export function isCharacterSetInsight(model: InsightModel): model is CharacterSetInsightModel {
  return model.insightKey === InsightKeys.CHARACTER_SET;
}

function finalize(partialModel: PartialInsightModel<CharacterSetInsightModel>): CharacterSetInsightModel {
  let hasFailure = false;
  if (partialModel.data) {
    hasFailure = !partialModel.data.checklist.httpCharset.value && !partialModel.data.checklist.metaCharset.value;
  }

  return {
    insightKey: InsightKeys.CHARACTER_SET,
    strings: UIStrings,
    title: i18nString(UIStrings.title),
    description: i18nString(UIStrings.description),
    docs: 'https://developer.chrome.com/docs/insights/charset/',
    category: InsightCategory.ALL,
    state: hasFailure ? 'fail' : 'pass',
    ...partialModel,
  };
}

function hasCharsetInContentType(request: Types.Events.SyntheticNetworkRequest): boolean {
  if (!request.args.data.responseHeaders) {
    return false;
  }
  for (const header of request.args.data.responseHeaders) {
    if (header.name.toLowerCase() === 'content-type') {
      return CHARSET_HTTP_REGEX.test(header.value);
    }
  }
  return false;
}

function findMetaCharsetDisposition(
    data: Handlers.Types.HandlerData,
    context: InsightSetContext,
    ): Types.Events.MetaCharsetDisposition|undefined {
  if (!context.navigation) {
    return undefined;
  }
  return data.PageLoadMetrics.metaCharsetCheckEventsByNavigation.get(context.navigation)
      ?.at(-1)
      ?.args.data?.disposition;
}

function metaCharsetLabel(disposition: Types.Events.MetaCharsetDisposition|undefined): ReturnType<typeof i18nString> {
  switch (disposition) {
    case 'found-in-first-1024-bytes':
      return i18nString(UIStrings.passingMetaCharsetEarly);
    case 'found-after-first-1024-bytes':
      return i18nString(UIStrings.failedMetaCharsetLate);
    case 'not-found':
      return i18nString(UIStrings.failedMetaCharsetMissing);
    default:
      return i18nString(UIStrings.failedMetaCharsetUnknown);
  }
}

export function generateInsight(
    data: Handlers.Types.HandlerData, context: InsightSetContext): CharacterSetInsightModel {
  if (!context.navigation) {
    return finalize({});
  }

  const documentRequest = data.NetworkRequests.byId.get(context.navigationId);
  if (!documentRequest) {
    return finalize({warnings: [InsightWarning.NO_DOCUMENT_REQUEST]});
  }

  const hasHttpCharset = hasCharsetInContentType(documentRequest);
  const metaCharsetDisposition = findMetaCharsetDisposition(data, context);
  const hasMetaCharsetInFirst1024Bytes = metaCharsetDisposition === 'found-in-first-1024-bytes';

  return finalize({
    relatedEvents: [documentRequest],
    data: {
      hasHttpCharset,
      metaCharsetDisposition,
      documentRequest,
      checklist: {
        httpCharset: {
          label: hasHttpCharset ? i18nString(UIStrings.passingHttpHeader) : i18nString(UIStrings.failedHttpHeader),
          value: hasHttpCharset,
        },
        metaCharset: {
          label: metaCharsetLabel(metaCharsetDisposition),
          value: hasMetaCharsetInFirst1024Bytes,
        },
      },
    },
  });
}

export function createOverlays(model: CharacterSetInsightModel): Types.Overlays.Overlay[] {
  if (!model.data?.documentRequest) {
    return [];
  }

  return [{
    type: 'ENTRY_SELECTED',
    entry: model.data.documentRequest,
  }];
}
