// File has been copied to packages/editor/editor-plugin-ai/src/provider/markdown-transformer/utils/hyperlink.ts
// If changes are made to this file, please make the same update in the linked file.

import { isSafeUrl, linkify, normalizeUrl as normaliseLinkHref } from '@atlaskit/adf-schema';
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '../analytics/types';
import { shouldAutoLinkifyTld } from './should-auto-linkify-tld';
import { mapSlice } from './slice';

// Regular expression for a windows filepath in the format <DRIVE LETTER>:\<folder name>\
export const FILEPATH_REGEXP = /([a-zA-Z]:|\\)([^\/:*?<>"|]+\\)?([^\/:*?<>"| ]+(?=\s?))?/gim;

// Don't linkify if starts with $ or {
export const DONTLINKIFY_REGEXP = /^(\$|{)/;

/**
 * Instance of class LinkMatcher are used in autoformatting in place of Regex.
 * Hence it has been made similar to regex with an exec method.
 * Extending it directly from class Regex was introducing some issues, thus that has been avoided.
 */
export class LinkMatcher {
  static create() {
    class LinkMatcherRegex {
      exec(str) {
        const stringsBySpace = str.slice(0, str.length - 1).split(' ');
        const lastStringBeforeSpace = stringsBySpace[stringsBySpace.length - 1];
        const isLastStringValid = lastStringBeforeSpace.length > 0;
        if (!str.endsWith(' ') || !isLastStringValid) {
          return null;
        }
        if (DONTLINKIFY_REGEXP.test(lastStringBeforeSpace)) {
          return null;
        }
        const links = linkify.match(lastStringBeforeSpace);
        if (!links || links.length === 0) {
          return null;
        }
        const lastMatch = links[links.length - 1];
        const lastLink = links[links.length - 1];
        lastLink.input = str.substring(lastMatch.index);
        lastLink.length = lastLink.lastIndex - lastLink.index + 1;
        lastLink.index = str.lastIndexOf(lastStringBeforeSpace) + lastMatch.index;
        return lastLink;
      }
    }
    return new LinkMatcherRegex();
  }
}

/**
 * Adds protocol to url if needed.
 */
export function normalizeUrl(url) {
  if (!url) {
    return '';
  }
  if (isSafeUrl(url)) {
    return url;
  }
  return normaliseLinkHref(url);
}

/**
 * Linkify content in a slice (eg. after a rich text paste)
 */
export function linkifyContent(schema) {
  return slice => mapSlice(slice, (node, parent) => {
    const isAllowedInParent = !parent || parent.type !== schema.nodes.codeBlock;
    const link = node.type.schema.marks.link;
    if (link === undefined) {
      throw new Error('Link not in schema - unable to linkify content');
    }
    if (isAllowedInParent && node.isText && !link.isInSet(node.marks)) {
      const linkified = [];
      const text = node.text;
      const matches = getBooleanFF('platform.linking-platform.prevent-suspicious-linkification') ? findLinkMatches(text).filter(match => shouldAutoLinkifyTld(match.title)) : findLinkMatches(text);
      let pos = 0;
      const filepaths = findFilepaths(text);
      matches.forEach(match => {
        if (isLinkInMatches(match.start, filepaths)) {
          return;
        }
        if (match.start > 0) {
          linkified.push(node.cut(pos, match.start));
        }
        linkified.push(node.cut(match.start, match.end).mark(link.create({
          href: normalizeUrl(match.href)
        }).addToSet(node.marks)));
        pos = match.end;
      });
      if (pos < text.length) {
        linkified.push(node.cut(pos));
      }
      return linkified;
    }
    return node;
  });
}
export function getLinkDomain(url) {
  // Remove protocol and www., if either exists
  const withoutProtocol = url.toLowerCase().replace(/^(.*):\/\//, '');
  const withoutWWW = withoutProtocol.replace(/^(www\.)/, '');

  // Remove port, fragment, path, query string
  return withoutWWW.replace(/[:\/?#](.*)$/, '');
}
export function isFromCurrentDomain(url) {
  if (!window || !window.location) {
    return false;
  }
  const currentDomain = window.location.hostname;
  const linkDomain = getLinkDomain(url);
  return currentDomain === linkDomain;
}
function findLinkMatches(text) {
  const matches = [];
  let linkMatches = text && linkify.match(text);
  if (linkMatches && linkMatches.length > 0) {
    linkMatches.forEach(match => {
      matches.push({
        start: match.index,
        end: match.lastIndex,
        title: match.raw,
        href: match.url
      });
    });
  }
  return matches;
}
export const findFilepaths = (text, offset = 0) => {
  // Creation of a copy of the RegExp is necessary as lastIndex is stored on it when we run .exec()
  const localRegExp = new RegExp(FILEPATH_REGEXP);
  let match;
  const matchesList = [];
  const maxFilepathSize = 260;
  while ((match = localRegExp.exec(text)) !== null) {
    const start = match.index + offset;
    let end = localRegExp.lastIndex + offset;
    if (end - start > maxFilepathSize) {
      end = start + maxFilepathSize;
    } // We don't care about big strings of text that are pretending to be filepaths!!
    matchesList.push({
      startIndex: start,
      endIndex: end
    });
  }
  return matchesList;
};
export const isLinkInMatches = (linkStart, matchesList) => {
  for (let i = 0; i < matchesList.length; i++) {
    if (linkStart >= matchesList[i].startIndex && linkStart < matchesList[i].endIndex) {
      return true;
    }
  }
  return false;
};
export function getLinkCreationAnalyticsEvent(inputMethod, url) {
  return {
    action: ACTION.INSERTED,
    actionSubject: ACTION_SUBJECT.DOCUMENT,
    actionSubjectId: ACTION_SUBJECT_ID.LINK,
    attributes: {
      inputMethod,
      fromCurrentDomain: isFromCurrentDomain(url)
    },
    eventType: EVENT_TYPE.TRACK,
    nonPrivacySafeAttributes: {
      linkDomain: getLinkDomain(url)
    }
  };
}
export const canLinkBeCreatedInRange = (from, to) => state => {
  if (!state.doc.rangeHasMark(from, to, state.schema.marks.link)) {
    const $from = state.doc.resolve(from);
    const $to = state.doc.resolve(to);
    const link = state.schema.marks.link;
    if ($from.parent === $to.parent && $from.parent.isTextblock) {
      if ($from.parent.type.allowsMarkType(link)) {
        let allowed = true;
        state.doc.nodesBetween(from, to, node => {
          allowed = allowed && !node.marks.some(m => m.type.excludes(link));
          return allowed;
        });
        return allowed;
      }
    }
  }
  return false;
};