/**
 * Copyright 2026 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// This is an implementation of autolinking pulled from Monorail and repurposed
// for use with text entries in WebStatus. Use this directly via './utils.js'
// See: https://chromium.googlesource.com/infra/infra/+/refs/heads/main/appengine/monorail/static_src/autolink.js

import {TemplateResult, html} from 'lit';
import {FeatureLink} from '../js-src/cs-client.js';
import {enhanceAutolink} from './chromedash-link.js';

interface TextRun {
  content: string;
  tag?: string;
  href?: string;
}

interface Component {
  refRegs: RegExp[];
  replacer: (match: any) => never[] | TextRun[];
}

const CRBUG_DEFAULT_PROJECT = 'chromium';
const CRBUG_URL = 'https://bugs.chromium.org';
const ISSUE_TRACKER_RE =
  /(\b(issues?|bugs?)[ \t]*(:|=|\b)|\bfixed[ \t]*:)([ \t]*((\b[-a-z0-9]+)[:\#])?(\#?)(\d+)\b(,?[ \t]*(and|or)?)?)+/gi;
const PROJECT_LOCALID_RE =
  /((\b(issue|bug)[ \t]*(:|=)?[ \t]*|\bfixed[ \t]*:[ \t]*)?((\b[-a-z0-9]+)[:\#])?(\#?)(\d+))/gi;
const PROJECT_COMMENT_BUG_RE =
  /(((\b(issue|bug)[ \t]*(:|=)?[ \t]*)(\#?)(\d+)[ \t*])?((\b((comment)[ \t]*(:|=)?[ \t]*(\#?))|(\B((\#))(c)))(\d+)))/gi;
const PROJECT_LOCALID_RE_PROJECT_GROUP = 6;
const PROJECT_LOCALID_RE_ID_GROUP = 8;
const SHORT_LINK_RE =
  /(^|[^-\/._])\b(https?:\/\/|ftp:\/\/|mailto:)?(go|g|shortn|who|teams)\/([^\s<]+)/gi;
const NUMERIC_SHORT_LINK_RE =
  /(^|[^-\/._])\b(https?:\/\/|ftp:\/\/)?(b|t|o|omg|cl|cr|fxr|fxrev|fxb|tqr)\/([0-9]+)/gi;
const IMPLIED_LINK_RE =
  /(?!@)(^|[^-\/._])\b[a-z]((-|\.)?[a-z0-9])+\.(com|net|org|edu|dev)\b(\/[^\s<]*)?/gi;
const IS_LINK_RE = /()\b(https?:\/\/|ftp:\/\/|mailto:)([^\s<]+)/gi;
const LINK_TRAILING_CHARS = [
  [null, ':'],
  [null, '.'],
  [null, ','],
  [null, '>'],
  ['(', ')'],
  ['[', ']'],
  ['{', '}'],
  ["'", "'"],
  ['"', '"'],
] as const;
const GOOG_SHORT_LINK_RE =
  /^(b|t|o|omg|cl|cr|go|g|shortn|who|teams|fxr|fxrev|fxb|tqr)\/.*/gi;

const Components = new Map<string, Component>();
Components.set('00-commentbug', {
  refRegs: [PROJECT_COMMENT_BUG_RE],
  replacer: replaceCommentBugRef,
});
Components.set('02-full-urls', {
  refRegs: [IS_LINK_RE],
  replacer: replaceLinkRef,
});
// 03-user-emails unused.
Components.set('04-tracker-regular', {
  refRegs: [ISSUE_TRACKER_RE],
  replacer: replaceTrackerIssueRef,
});
Components.set('05-linkify-shorthand', {
  refRegs: [SHORT_LINK_RE, NUMERIC_SHORT_LINK_RE, IMPLIED_LINK_RE],
  replacer: replaceLinkRef,
});
// 06-versioncontrol unused.

// Replace plain text references with links functions.
function replaceIssueRef(
  stringMatch: string,
  projectName: string,
  localId: string,
  commentId: string
) {
  return createIssueRefRun(projectName, localId, stringMatch, commentId);
}

function replaceTrackerIssueRef(
  match: RegExpExecArray,
  currentProjectName = CRBUG_DEFAULT_PROJECT
) {
  const issueRefRE = PROJECT_LOCALID_RE;
  const commentId = '';
  const textRuns: TextRun[] = [];
  let refMatch: RegExpExecArray | null;
  let pos = 0;
  while ((refMatch = issueRefRE.exec(match[0])) !== null) {
    if (refMatch.index > pos) {
      // Create textrun for content between previous and current match.
      textRuns.push({content: match[0].slice(pos, refMatch.index)});
    }
    if (refMatch[PROJECT_LOCALID_RE_PROJECT_GROUP]) {
      currentProjectName = refMatch[PROJECT_LOCALID_RE_PROJECT_GROUP];
    }
    textRuns.push(
      replaceIssueRef(
        refMatch[0],
        currentProjectName,
        refMatch[PROJECT_LOCALID_RE_ID_GROUP],
        commentId
      )
    );
    pos = refMatch.index + refMatch[0].length;
  }
  if (match[0].slice(pos) !== '') {
    textRuns.push({content: match[0].slice(pos)});
  }
  return textRuns;
}

function replaceCommentBugRef(match: RegExpExecArray) {
  let textRun: TextRun;
  const issueNum = match[7];
  const commentNum = match[18];
  if (issueNum && commentNum) {
    const href = `${CRBUG_URL}/p/${CRBUG_DEFAULT_PROJECT}/issues/detail?id=${issueNum}#c${commentNum}`;
    textRun = {content: match[0], tag: 'a', href};
  } else if (commentNum) {
    const href = `${CRBUG_URL}/p/${CRBUG_DEFAULT_PROJECT}/issues/detail#c${commentNum}`;
    textRun = {content: match[0], tag: 'a', href};
  } else {
    textRun = {content: match[0]};
  }
  return [textRun];
}

function replaceLinkRef(match: RegExpExecArray) {
  const textRuns: TextRun[] = [];
  let content = match[0];
  let trailing = '';
  if (match[1]) {
    textRuns.push({content: match[1]});
    content = content.slice(match[1].length);
  }
  LINK_TRAILING_CHARS.forEach(([begin, end]) => {
    if (content.endsWith(end)) {
      if (!begin || !content.slice(0, -end.length).includes(begin)) {
        trailing = end + trailing;
        content = content.slice(0, -end.length);
      }
    }
  });
  let href = content;
  const lowerHref = href.toLowerCase();
  if (
    !lowerHref.startsWith('http') &&
    !lowerHref.startsWith('ftp') &&
    !lowerHref.startsWith('mailto')
  ) {
    // Prepend google-internal short links with http to
    // prevent HTTPS error interstitial.
    // SHORT_LINK_RE should not be used here as it might be
    // in the middle of another match() process in an outer loop.
    if (GOOG_SHORT_LINK_RE.test(lowerHref)) {
      href = 'http://' + href;
    } else {
      href = 'https://' + href;
    }
    GOOG_SHORT_LINK_RE.lastIndex = 0;
  }
  textRuns.push({content: content, tag: 'a', href: href});
  if (trailing.length) {
    textRuns.push({content: trailing});
  }
  return textRuns;
}

// Create custom textrun functions.
function createIssueRefRun(projectName, localId, content, commentId) {
  return {
    tag: 'a',
    content: content,
    href: `${CRBUG_URL}/p/${projectName}/issues/detail?id=${localId}${commentId}`,
  };
}

export function markupAutolinks(
  plainString,
  featureLinks: FeatureLink[] = []
): TemplateResult[] {
  plainString = plainString || '';
  const chunks = [plainString.trim()];
  const textRuns: TextRun[] = [];
  chunks.filter(Boolean).forEach(chunk => {
    textRuns.push(...autolinkChunk(chunk));
  });
  const result: TemplateResult[] = textRuns.map(part => {
    if (part.tag === 'a') {
      // if the link is a feature link, enhance it to provide more information
      return enhanceAutolink(part, featureLinks);
    }
    return html`${part.content}`;
  });
  return result;
}

function autolinkChunk(chunk) {
  let textRuns = [{content: chunk}];
  Components.forEach(({refRegs, replacer}) => {
    refRegs.forEach(re => {
      textRuns = applyLinks(textRuns, replacer, re);
    });
  });
  return textRuns;
}

function applyLinks(textRuns, replacer, re) {
  const resultRuns: TextRun[] = [];
  textRuns.forEach(textRun => {
    if (textRun.tag) {
      resultRuns.push(textRun);
    } else {
      const content = textRun.content;
      let pos = 0;
      let match;
      while ((match = re.exec(content)) !== null) {
        if (match.index > pos) {
          // Create textrun for content between previous and current match.
          resultRuns.push({content: content.slice(pos, match.index)});
        }
        resultRuns.push(...replacer(match));
        pos = match.index + match[0].length;
      }
      if (content.slice(pos) !== '') {
        resultRuns.push({content: content.slice(pos)});
      }
    }
  });
  return resultRuns;
}
