// @flow
const isProduction = process.env.NODE_ENV === 'production';
const channelNameMinLength = 1;
const claimIdMaxLength = 40;

// see https://spec.lbry.com/#urls
export const regexInvalidURI = /[ =&#:$@%?;/\\"<>%{}|^~[\]`\u{0000}-\u{0008}\u{000b}-\u{000c}\u{000e}-\u{001F}\u{D800}-\u{DFFF}\u{FFFE}-\u{FFFF}]/u;
export const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/;
const regexPartProtocol = '^((?:lbry://)?)';
const regexPartStreamOrChannelName = '([^:$#/]*)';
const regexPartModifierSeparator = '([:$#]?)([^/]*)';
const queryStringBreaker = '^([\\S]+)([?][\\S]*)';
const separateQuerystring = new RegExp(queryStringBreaker);

const MOD_SEQUENCE_SEPARATOR = '*';
const MOD_CLAIM_ID_SEPARATOR_OLD = '#';
const MOD_CLAIM_ID_SEPARATOR = ':';
const MOD_BID_POSITION_SEPARATOR = '$';

/**
 * Parses a LBRY name into its component parts. Throws errors with user-friendly
 * messages for invalid names.
 *
 * Returns a dictionary with keys:
 *   - path (string)
 *   - isChannel (boolean)
 *   - streamName (string, if present)
 *   - streamClaimId (string, if present)
 *   - channelName (string, if present)
 *   - channelClaimId (string, if present)
 *   - primaryClaimSequence (int, if present)
 *   - secondaryClaimSequence (int, if present)
 *   - primaryBidPosition (int, if present)
 *   - secondaryBidPosition (int, if present)
 */

export function parseURI(url: string, requireProto: boolean = false): LbryUrlObj {
  // Break into components. Empty sub-matches are converted to null

  const componentsRegex = new RegExp(
    regexPartProtocol + // protocol
      regexPartStreamOrChannelName + // stream or channel name (stops at the first separator or end)
      regexPartModifierSeparator + // modifier separator, modifier (stops at the first path separator or end)
      '(/?)' + // path separator, there should only be one (optional) slash to separate the stream and channel parts
      regexPartStreamOrChannelName +
      regexPartModifierSeparator
  );
  // chop off the querystring first
  let QSStrippedURL, qs;
  const qsRegexResult = separateQuerystring.exec(url);
  if (qsRegexResult) {
    [QSStrippedURL, qs] = qsRegexResult.slice(1).map((match) => match || null);
  }

  const cleanURL = QSStrippedURL || url;
  const regexMatch = componentsRegex.exec(cleanURL) || [];
  const [proto, ...rest] = regexMatch.slice(1).map((match) => match || null);
  const path = rest.join('');
  const [
    streamNameOrChannelName,
    primaryModSeparator,
    primaryModValue,
    pathSep, // eslint-disable-line no-unused-vars
    possibleStreamName,
    secondaryModSeparator,
    secondaryModValue,
  ] = rest;
  const searchParams = new URLSearchParams(qs || '');
  const startTime = searchParams.get('t');

  // Validate protocol
  if (requireProto && !proto) {
    throw new Error(__('LBRY URLs must include a protocol prefix (lbry://).'));
  }

  // Validate and process name
  if (!streamNameOrChannelName) {
    throw new Error(__('URL does not include name.'));
  }

  rest.forEach((urlPiece) => {
    if (urlPiece && urlPiece.includes(' ')) {
      throw new Error(__('URL can not include a space'));
    }
  });

  const includesChannel = streamNameOrChannelName.startsWith('@');
  const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName;
  const channelName = includesChannel && streamNameOrChannelName.slice(1);

  if (includesChannel) {
    if (!channelName) {
      throw new Error(__('No channel name after @.'));
    }

    if (channelName.length < channelNameMinLength) {
      throw new Error(
        __(`Channel names must be at least %channelNameMinLength% characters.`, {
          channelNameMinLength,
        })
      );
    }
  }

  // Validate and process modifier
  const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(
    primaryModSeparator,
    primaryModValue
  );
  const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier(
    secondaryModSeparator,
    secondaryModValue
  );
  const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName;
  const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId;
  const channelClaimId = includesChannel && primaryClaimId;

  return {
    isChannel,
    path,
    ...(streamName ? { streamName } : {}),
    ...(streamClaimId ? { streamClaimId } : {}),
    ...(channelName ? { channelName } : {}),
    ...(channelClaimId ? { channelClaimId } : {}),
    ...(primaryClaimSequence ? { primaryClaimSequence: parseInt(primaryClaimSequence, 10) } : {}),
    ...(secondaryClaimSequence ? { secondaryClaimSequence: parseInt(secondaryClaimSequence, 10) } : {}),
    ...(primaryBidPosition ? { primaryBidPosition: parseInt(primaryBidPosition, 10) } : {}),
    ...(secondaryBidPosition ? { secondaryBidPosition: parseInt(secondaryBidPosition, 10) } : {}),
    ...(startTime ? { startTime: parseInt(startTime, 10) } : {}),

    // The values below should not be used for new uses of parseURI
    // They will not work properly with canonical_urls
    claimName: streamNameOrChannelName,
    claimId: primaryClaimId,
    ...(streamName ? { contentName: streamName } : {}),
    ...(qs ? { queryString: qs } : {}),
  };
}

function parseURIModifier(modSeperator: ?string, modValue: ?string) {
  let claimId;
  let claimSequence;
  let bidPosition;

  if (modSeperator) {
    if (!modValue) {
      throw new Error(__(`No modifier provided after separator %modSeperator%.`, { modSeperator }));
    }

    if (modSeperator === MOD_CLAIM_ID_SEPARATOR || MOD_CLAIM_ID_SEPARATOR_OLD) {
      claimId = modValue;
    } else if (modSeperator === MOD_SEQUENCE_SEPARATOR) {
      claimSequence = modValue;
    } else if (modSeperator === MOD_BID_POSITION_SEPARATOR) {
      bidPosition = modValue;
    }
  }

  if (claimId && (claimId.length > claimIdMaxLength || !claimId.match(/^[0-9a-f]+$/))) {
    throw new Error(__(`Invalid claim ID %claimId%.`, { claimId }));
  }

  if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
    throw new Error(__('Claim sequence must be a number.'));
  }

  if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
    throw new Error(__('Bid position must be a number.'));
  }

  return [claimId, claimSequence, bidPosition];
}

const errorHistory = [];

function logErrorOnce(err: string) {
  if (!errorHistory.includes(err)) {
    errorHistory.push(err);
    console.error(err);
  }
}

/**
 * Takes an object in the same format returned by parse() and builds a URI.
 *
 * The channelName key will accept names with or without the @ prefix.
 */
export function buildURI(UrlObj: LbryUrlObj, includeProto: boolean = true, protoDefault: string = 'lbry://'): string {
  const {
    streamName,
    streamClaimId,
    channelName,
    channelClaimId,
    primaryClaimSequence,
    primaryBidPosition,
    secondaryClaimSequence,
    secondaryBidPosition,
    startTime,
    ...deprecatedParts
  } = UrlObj;
  const { claimId, claimName, contentName } = deprecatedParts;

  // @ifndef IGNORE_BUILD_URI_WARNINGS
  if (!isProduction) {
    if (claimId) {
      logErrorOnce("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead");
    }
    if (claimName) {
      logErrorOnce("'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead");
    }
    if (contentName) {
      logErrorOnce("'contentName' should no longer be used. Use 'streamName' instead");
    }
  }
  // @endif

  if (!claimName && !channelName && !streamName) {
    console.error("'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.");
  }

  const formattedChannelName = channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`);
  const primaryClaimName = claimName || contentName || formattedChannelName || streamName;
  const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId);
  const secondaryClaimName = (!claimName && contentName) || (formattedChannelName ? streamName : null);
  const secondaryClaimId = secondaryClaimName && streamClaimId;

  return (
    (includeProto ? protoDefault : '') +
    // primaryClaimName will always exist here because we throw above if there is no "name" value passed in
    // $FlowFixMe
    primaryClaimName +
    (primaryClaimId ? `#${primaryClaimId}` : '') +
    (primaryClaimSequence ? `:${primaryClaimSequence}` : '') +
    (primaryBidPosition ? `${primaryBidPosition}` : '') +
    (secondaryClaimName ? `/${secondaryClaimName}` : '') +
    (secondaryClaimId ? `#${secondaryClaimId}` : '') +
    (secondaryClaimSequence ? `:${secondaryClaimSequence}` : '') +
    (secondaryBidPosition ? `${secondaryBidPosition}` : '') +
    (startTime ? `?t=${startTime}` : '')
  );
}

/* Takes a parseable LBRY URL and converts it to standard, canonical format */
export function normalizeURI(URL: string) {
  const {
    streamName,
    streamClaimId,
    channelName,
    channelClaimId,
    primaryClaimSequence,
    primaryBidPosition,
    secondaryClaimSequence,
    secondaryBidPosition,
    startTime,
  } = parseURI(URL);

  return buildURI({
    streamName,
    streamClaimId,
    channelName,
    channelClaimId,
    primaryClaimSequence,
    primaryBidPosition,
    secondaryClaimSequence,
    secondaryBidPosition,
    startTime,
  });
}

export function isURIValid(URL: string, normalize: boolean = true): boolean {
  try {
    parseURI(normalize ? normalizeURI(URL) : URL);
  } catch (error) {
    return false;
  }

  return true;
}

export function isNameValid(claimName: string) {
  return !regexInvalidURI.test(claimName);
}

export function isURIClaimable(URL: string) {
  let parts;
  try {
    parts = parseURI(normalizeURI(URL));
  } catch (error) {
    return false;
  }

  return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
}

export function convertToShareLink(URL: string) {
  const {
    streamName,
    streamClaimId,
    channelName,
    channelClaimId,
    primaryBidPosition,
    primaryClaimSequence,
    secondaryBidPosition,
    secondaryClaimSequence,
  } = parseURI(URL);
  return buildURI(
    {
      streamName,
      streamClaimId,
      channelName,
      channelClaimId,
      primaryBidPosition,
      primaryClaimSequence,
      secondaryBidPosition,
      secondaryClaimSequence,
    },
    true,
    'https://open.lbry.com/'
  );
}

export function splitBySeparator(uri: string) {
  const protocolLength = 7;
  return uri.startsWith('lbry://') ? uri.slice(protocolLength).split(/[#:*]/) : uri.split(/#:\*\$/);
}

export function isURIEqual(uriA: string, uriB: string) {
  const a = uriA && uriA.replace(/:/g, '#');
  const b = uriB && uriB.replace(/:/g, '#');
  return a === b;
}