import moment from 'moment-timezone';

import type {PromptData} from 'sentry/actionCreators/prompts';
import {DataCategory} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
import getDaysSinceDate from 'sentry/utils/getDaysSinceDate';

import {
  BILLION,
  DEFAULT_TRIAL_DAYS,
  GIGABYTE,
  MILLION,
  RESERVED_BUDGET_QUOTA,
  TRIAL_PLANS,
  UNLIMITED,
  UNLIMITED_RESERVED,
} from 'getsentry/constants';
import type {
  BillingConfig,
  BillingMetricHistory,
  EventBucket,
  Plan,
  ProductTrial,
  Subscription,
} from 'getsentry/types';
import {PlanName, PlanTier} from 'getsentry/types';
import titleCase from 'getsentry/utils/titleCase';
import {displayPriceWithCents} from 'getsentry/views/amCheckout/utils';

export const MILLISECONDS_IN_HOUR = 3600_000;

function isNum(val: unknown): val is number {
  return typeof val === 'number';
}

// TODO(brendan): remove condition for 0 once -1 is the value we use to represent unlimited reserved quota
export function isUnlimitedReserved(value: number | null | undefined): boolean {
  return value === UNLIMITED_RESERVED;
}

export const getSlot = (
  events?: number,
  price?: number,
  slots?: EventBucket[],
  shouldMinimize = false
) => {
  let s = 0;
  if (!slots?.length || (typeof events !== 'number' && typeof price !== 'number')) {
    return 0;
  }
  const byEvents = typeof events === 'number';

  const value = isNum(events) ? events : isNum(price) ? price : null;
  if (value === null) {
    return 0;
  }

  const slotKey = byEvents ? 'events' : 'price';

  while (value > slots[s]![slotKey]) {
    s++;
    if (s >= slots.length - 1) {
      if (shouldMinimize) {
        return Math.max(s - 1, 0);
      }
      return Math.min(s, slots.length - 1);
    }
  }

  // If the specified number of events does not match any of the slots we have,
  // we return the slot down if shouldMinimize is true, otherwise we always return
  // the next slot up (ie. 500 events when the slots are [50, 5000] would return 50
  // when shouldMinimize is true, and 5000 when it is false or unspecified)
  if (
    shouldMinimize &&
    ((byEvents && slots[s]![slotKey] !== events) ||
      (!byEvents && slots[s]![slotKey] !== price))
  ) {
    return Math.max(s - 1, 0);
  }

  return Math.min(s, slots.length - 1);
};

type ReservedSku =
  | Subscription['reservedErrors']
  | Subscription['reservedTransactions']
  | Subscription['reservedAttachments']
  | number
  | null;

/**
 * isAbbreviated: Shortens the number using K for thousand, M for million, etc
 *                Useful for Errors/Transactions but not recommended to be used
 *                with Attachments because "1K GB" is hard to read.
 * isGifted: For gifted data volumes, 0 is displayed as 0 instead of unlimited.
 * useUnitScaling: For Attachments only. Scale from KB -> MB -> GB -> TB -> etc
 */
type FormatOptions = {
  fractionDigits?: number;
  isAbbreviated?: boolean;
  isGifted?: boolean;
  useUnitScaling?: boolean;
};

/**
 * This expects values from CustomerSerializer, which contains quota/reserved
 * quantities for the data categories that we sell.
 *
 * Note: reservedQuantity for Attachments should be in GIGABYTES
 * If isReservedBudget is true, the reservedQuantity is in cents
 */
export function formatReservedWithUnits(
  reservedQuantity: ReservedSku,
  dataCategory: string,
  options: FormatOptions = {
    isAbbreviated: false,
    useUnitScaling: false,
    isGifted: false,
  },
  isReservedBudget = false
): string {
  if (isReservedBudget) {
    return displayPriceWithCents({cents: reservedQuantity ?? 0});
  }
  if (dataCategory !== DataCategory.ATTACHMENTS) {
    return formatReservedNumberToString(reservedQuantity, options);
  }
  // convert reservedQuantity to BYTES to check for unlimited
  const usageGb = reservedQuantity ? reservedQuantity * GIGABYTE : reservedQuantity;
  if (isUnlimitedReserved(usageGb)) {
    return !options.isGifted ? UNLIMITED : '0 GB';
  }
  if (!options.useUnitScaling) {
    const formatted = formatReservedNumberToString(reservedQuantity, options);
    return `${formatted} GB`;
  }

  return formatAttachmentUnits(reservedQuantity || 0, 3);
}

/**
 * This expects values from CustomerUsageEndpoint, which contains usage
 * quantities for the data categories that we sell.
 *
 * Note: usageQuantity for Attachments should be in BYTES
 */
export function formatUsageWithUnits(
  usageQuantity = 0,
  dataCategory: string,
  options: FormatOptions = {isAbbreviated: false, useUnitScaling: false}
) {
  if (dataCategory === DataCategory.ATTACHMENTS) {
    if (options.useUnitScaling) {
      return formatAttachmentUnits(usageQuantity);
    }

    const usageGb = usageQuantity / GIGABYTE;
    return options.isAbbreviated
      ? `${displayNumber(usageGb)} GB`
      : `${usageGb.toLocaleString(undefined, {maximumFractionDigits: 2})} GB`;
  }
  if (dataCategory === DataCategory.PROFILE_DURATION) {
    const usageProfileHours = usageQuantity / MILLISECONDS_IN_HOUR;
    if (usageProfileHours === 0) {
      return '0';
    }
    return options.isAbbreviated
      ? displayNumber(usageProfileHours, 1)
      : usageProfileHours.toLocaleString(undefined, {maximumFractionDigits: 1});
  }
  return options.isAbbreviated
    ? displayNumber(usageQuantity, 0)
    : usageQuantity.toLocaleString();
}

/**
 * Do not export.
 * Helper method for formatReservedWithUnits
 */
function formatReservedNumberToString(
  reservedQuantity: ReservedSku,
  options: FormatOptions = {
    isAbbreviated: false,
    isGifted: false,
    useUnitScaling: false,
    fractionDigits: 0,
  }
): string {
  // "null" indicates that there's no quota for it.
  if (!defined(reservedQuantity)) {
    return '0';
  }

  if (reservedQuantity === RESERVED_BUDGET_QUOTA) {
    return 'N/A';
  }

  if (isUnlimitedReserved(reservedQuantity) && !options.isGifted) {
    return UNLIMITED;
  }

  return options.isAbbreviated
    ? displayNumber(reservedQuantity, options.fractionDigits)
    : reservedQuantity.toLocaleString(undefined, {maximumFractionDigits: 1});
}

/**
 * Do not export.
 * Use formatReservedWithUnits or formatUsageWithUnits instead.
 *
 * This function is different from sentry/utils/formatBytes. Note the
 * difference between *a-bytes (base 10) vs *i-bytes (base 2), which means that:
 * - 1000 megabytes is equal to 1 gigabyte
 * - 1024 mebibytes is equal to 1024 gibibytes
 *
 * We will use base 10 throughout billing for attachments. This function formats
 * quota/usage values for display.
 *
 * For storage/memory/file sizes, please take a look at the function in
 * sentry/utils/formatBytes.
 */
function formatAttachmentUnits(bytes: number, u = 0) {
  const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  const threshold = 1000;

  while (bytes >= threshold) {
    bytes /= threshold;
    u += 1;
  }

  return bytes.toLocaleString(undefined, {maximumFractionDigits: 2}) + ' ' + units[u];
}

/**
 * Do not export.
 * Use formatReservedWithUnits or formatUsageWithUnits with options.isAbbreviated to true
 */
function displayNumber(n: number, fractionDigits = 0) {
  if (n >= BILLION) {
    return (n / BILLION).toLocaleString(undefined, {maximumFractionDigits: 2}) + 'B';
  }

  if (n >= MILLION) {
    return (n / MILLION).toLocaleString(undefined, {maximumFractionDigits: 1}) + 'M';
  }

  if (n >= 1000) {
    return (n / 1000).toFixed().toLocaleString() + 'K';
  }

  // Do not show decimals
  return n.toFixed(fractionDigits).toLocaleString();
}

/**
 * Utility functions for Pricing Plans
 */
export const isEnterprise = (subscription: Subscription) =>
  ['e1', 'enterprise'].some(p => subscription.plan.startsWith(p));

export const isTrialPlan = (plan: string) => TRIAL_PLANS.includes(plan);

export const hasPerformance = (plan?: Plan) => {
  return (
    // Older plans will have Transactions
    plan?.categories?.includes(DataCategory.TRANSACTIONS) ||
    // AM3 Onwards will have Spans
    plan?.categories?.includes(DataCategory.SPANS)
  );
};

export const hasPartnerMigrationFeature = (organization: Organization) =>
  organization.features.includes('partner-billing-migration');

export const hasActiveVCFeature = (organization: Organization) =>
  organization.features.includes('vc-marketplace-active-customer');

export const isDeveloperPlan = (plan?: Plan) => plan?.name === PlanName.DEVELOPER;

export const isBizPlanFamily = (plan?: Plan) =>
  plan?.name === PlanName.BUSINESS ||
  plan?.name === PlanName.BUSINESS_BUNDLE ||
  plan?.name === PlanName.BUSINESS_SPONSORED;

export const isTeamPlanFamily = (plan?: Plan) =>
  plan?.name === PlanName.TEAM ||
  plan?.name === PlanName.TEAM_BUNDLE ||
  plan?.name === PlanName.TEAM_SPONSORED;

export const isBusinessTrial = (subscription: Subscription) => {
  return (
    subscription.isTrial &&
    !subscription.isPerformancePlanTrial &&
    !subscription.isEnterpriseTrial
  );
};

export function isAmPlan(planId?: string) {
  return typeof planId === 'string' && planId.startsWith('am');
}

function isAm2Plan(planId?: string) {
  return typeof planId === 'string' && planId.startsWith('am2');
}

export function isAm3Plan(planId?: string) {
  return typeof planId === 'string' && planId.startsWith('am3');
}

export function isAm3DsPlan(planId?: string) {
  return typeof planId === 'string' && planId.startsWith('am3') && planId.includes('_ds');
}

export function isAmEnterprisePlan(planId?: string) {
  return (
    typeof planId === 'string' &&
    planId.startsWith('am') &&
    (planId.endsWith('_ent') ||
      planId.endsWith('_ent_auf') ||
      planId.endsWith('_ent_ds') ||
      planId.endsWith('_ent_ds_auf'))
  );
}

export function hasJustStartedPlanTrial(subscription: Subscription) {
  return subscription.isTrial && subscription.isTrialStarted;
}

export const displayPlanName = (plan?: Plan | null) => {
  return isAmEnterprisePlan(plan?.id) ? 'Enterprise' : (plan?.name ?? '[unavailable]');
};

export const getAmPlanTier = (plan: string) => {
  if (isAm3Plan(plan)) {
    return PlanTier.AM3;
  }
  if (isAm2Plan(plan)) {
    return PlanTier.AM2;
  }
  if (isAmPlan(plan)) {
    return PlanTier.AM1;
  }
  return null;
};

/**
 * Promotion utility functions that are based off of formData which has the plan as a string
 * instead of a Plan
 */

export const getBusinessPlanOfTier = (plan: string) =>
  plan.startsWith('am2_') ? 'am2_business' : 'am1_business';

export const isTeamPlan = (plan: string) => plan.includes('team');

/**
 * Get the number of days left on trial
 */
export function getTrialDaysLeft(subscription: Subscription): number {
  // trial end is in the future
  return -1 * getDaysSinceDate(subscription.trialEnd ?? '');
}

/**
 * Get the number of days left on contract
 */
export function getContractDaysLeft(subscription: Subscription): number {
  // contract period end is in the future
  return -1 * getDaysSinceDate(subscription.contractPeriodEnd ?? '');
}

/**
 * Return a sorted list of plans the user can upgrade to.
 * Used to find the best plan for an org to upgrade to
 * based on a particular feature to unlock.
 */
function sortPlansForUpgrade(billingConfig: BillingConfig, subscription: Subscription) {
  // Filter plans down to just user selectable plans types of the orgs current
  // contract interval. Sorted by price as features will become progressively
  // more available.
  let plans = billingConfig.planList
    .sort((a, b) => a.price - b.price)
    .filter(p => p.userSelectable && p.billingInterval === subscription.billingInterval);

  // If we're dealing with plans that are *not part of a tier* Then we can
  // assume special case that there is only one plan.
  if (billingConfig.id === null && plans.length === 0) {
    plans = billingConfig.planList;
  }
  return plans;
}

export function getBestPlanForUnlimitedMembers(
  billingConfig: BillingConfig,
  subscription: Subscription
) {
  const plans = sortPlansForUpgrade(billingConfig, subscription);
  // the best plan is the first one that has unlimited members
  return plans.find(p => p.maxMembers === null);
}

export function getTrialLength(_organization: Organization) {
  // currently only doing trials of 14 days
  return DEFAULT_TRIAL_DAYS;
}

export function formatBalance(value: number) {
  return value < 0
    ? `${displayPriceWithCents({cents: 0 - value})} credit`
    : `${displayPriceWithCents({cents: value})} owed`;
}

export enum UsageAction {
  START_TRIAL = 'start_trial',
  ADD_EVENTS = 'add_events',
  REQUEST_ADD_EVENTS = 'request_add_events',
  REQUEST_UPGRADE = 'request_upgrade',
  SEND_TO_CHECKOUT = 'send_to_checkout',
}

/**
 * Return the best action that user can take so that organization
 * can get more events.
 */
export function getBestActionToIncreaseEventLimits(
  organization: Organization,
  subscription: Subscription
) {
  const isPaidPlan = subscription.planDetails?.price > 0;
  const hasBillingPerms = organization.access?.includes('org:billing');

  // free orgs can increase event limits by trialing
  if (!isPaidPlan && subscription.canTrial) {
    return UsageAction.START_TRIAL;
  }
  // paid plans should add events without changing plans
  if (isPaidPlan && hasPerformance(subscription.planDetails)) {
    return hasBillingPerms ? UsageAction.ADD_EVENTS : UsageAction.REQUEST_ADD_EVENTS;
  }
  // otherwise, we want them to upgrade to a different plan
  return hasBillingPerms ? UsageAction.SEND_TO_CHECKOUT : UsageAction.REQUEST_UPGRADE;
}

/**
 * Returns a name for the plan that we can display to users
 */
export function getFriendlyPlanName(subscription: Subscription) {
  const {name} = subscription.planDetails;
  switch (name) {
    case 'Trial':
      return 'Business Trial';
    default:
      return name;
  }
}

export function hasAccessToSubscriptionOverview(
  subscription: Subscription,
  organization: Organization
) {
  return organization.access.includes('org:billing') || subscription.canSelfServe;
}

/**
 * Returns the soft cap type for the given metric history category that can be
 * displayed to users if applicable. Returns null for if no soft cap type.
 */
export function getSoftCapType(metricHistory: BillingMetricHistory): string | null {
  if (metricHistory.softCapType) {
    return titleCase(metricHistory.softCapType.replace(/_/g, ' '));
  }
  if (metricHistory.trueForward) {
    return 'True Forward';
  }
  return null;
}

/**
 * Returns:
 *    active trial with latest end date, if available, else
 *    available trial with most trial days, else
 *    most recently ended trial, else
 *    null,
 *    in that order.
 */
export function getProductTrial(
  productTrials: ProductTrial[] | null,
  category: DataCategory
): ProductTrial | null {
  const trialsForCategory =
    productTrials
      ?.filter(pt => pt.category === category)
      .sort((a, b) => b.endDate?.localeCompare(a.endDate ?? '') || 0) ?? [];

  const activeProductTrial = getActiveProductTrial(trialsForCategory, category);

  if (activeProductTrial) {
    return activeProductTrial;
  }

  const longestAvailableTrial = getPotentialProductTrial(trialsForCategory, category);

  if (longestAvailableTrial) {
    return longestAvailableTrial;
  }

  return trialsForCategory[0] ?? null;
}

/**
 * Returns the currently active product trial for the specified category if there is one,
 * otherwise, returns null.
 */
export function getActiveProductTrial(
  productTrials: ProductTrial[] | null,
  category: DataCategory
): ProductTrial | null {
  if (!productTrials) {
    return null;
  }
  const currentTrials = productTrials
    .filter(
      pt =>
        pt.category === category &&
        pt.isStarted &&
        getDaysSinceDate(pt.endDate ?? '') <= 0
    )
    .sort((a, b) => b.endDate?.localeCompare(a.endDate ?? '') || 0);

  return currentTrials[0] ?? null;
}

/**
 * Returns the longest available trial for the specified category if there is one,
 * otherwise, returns null.
 */
export function getPotentialProductTrial(
  productTrials: ProductTrial[] | null,
  category: DataCategory
): ProductTrial | null {
  if (!productTrials) {
    return null;
  }
  const potentialTrials = productTrials
    .filter(
      pt =>
        pt.category === category &&
        !pt.isStarted &&
        getDaysSinceDate(pt.endDate ?? '') <= 0
    )
    .sort((a, b) => (b.lengthDays ?? 0) - (a.lengthDays ?? 0));

  return potentialTrials[0] ?? null;
}

export function trialPromptIsDismissed(prompt: PromptData, subscription: Subscription) {
  const {snoozedTime, dismissedTime} = prompt || {};
  const time = snoozedTime || dismissedTime;
  if (!time) {
    return false;
  }
  const onDemandPeriodStart = new Date(subscription.onDemandPeriodStart);
  return time >= onDemandPeriodStart.getTime() / 1000;
}

export function partnerPlanEndingModalIsDismissed(
  prompt: PromptData,
  subscription: Subscription,
  timeframe: string
) {
  const {snoozedTime, dismissedTime} = prompt || {};
  const time = snoozedTime || dismissedTime;
  if (!time) {
    return false;
  }

  const lastDaysLeft = moment(subscription.contractPeriodEnd).diff(
    moment.unix(time),
    'days'
  );

  switch (timeframe) {
    case 'zero':
      return lastDaysLeft <= 0;
    case 'two':
      return lastDaysLeft <= 2 && lastDaysLeft > 0;
    case 'week':
      return lastDaysLeft <= 7 && lastDaysLeft > 2;
    case 'month':
      return lastDaysLeft <= 30 && lastDaysLeft > 7;
    default:
      return true;
  }
}
