/**
 * Formulas for the metrics a queue dashboard usually shows, computed from the `stats` block of a service.
 *
 * Everything here is a pure function of the stats (and, where it makes sense, of "now"), with no dependency on
 * the Wildix SDK, so it can be copied into any project. Rates and averages are not sent by the API on purpose:
 * they depend on the lookback window and on which definition you choose, so each dashboard derives its own.
 */

/** Counters of one agent over the lookback window (`stats.agentStats[agentId]`). */
export interface AgentStatsLike {
  offered?: number;
  answered?: number;
  missed?: number;
  talkSumMs?: number;
  talkCount?: number;
  ringToAnswerSumMs?: number;
  ringToAnswerCount?: number;
  answeredRingSumMs?: number;
  answeredRingCount?: number;
  availableMs?: number;
  busyMs?: number;
  /** Paused time, wrap-up not included. */
  pausedMs?: number;
  wrapUpMs?: number;
  loggedInMs?: number;
}

/** The fields of `ServiceStats` used below. All of them can be missing: treat a missing counter as 0. */
export interface ServiceStatsLike {
  offered?: number;
  outbound?: number;
  answered?: number;
  unanswered?: number;
  unknown?: number;
  abandoned?: number;
  shortAbandoned?: number;
  answeredWaitSumMs?: number;
  abandonedWaitSumMs?: number;
  unansweredWaitSumMs?: number;
  waitCount?: number;
  ringSumMs?: number;
  ringCount?: number;
  talkSumMs?: number;
  talkCount?: number;
  answeredWithinSl?: number;
  maxWaitMs?: number;
  windowStartedAt?: number;
  asOf?: number;
  periodSeconds?: number;
  longestWaitStartedAt?: number;
  callsWaiting?: number;
  callsActive?: number;
  agentsTotal?: number;
  agentsAvailable?: number;
  agentsTalking?: number;
  agentsPaused?: number;
  agentsWrapUp?: number;
  agentsLoggedOff?: number;
  agentStats?: Record<string, AgentStatsLike>;
}

const orZero = (value: number | undefined): number => value ?? 0;
const ratio = (numerator: number, denominator: number): number | null =>
  denominator > 0 ? numerator / denominator : null;

/** Abandons that waited more than 5 seconds: `abandoned` includes `shortAbandoned`. */
const longAbandoned = (stats: ServiceStatsLike): number => Math.max(0, orZero(stats.abandoned) - orZero(stats.shortAbandoned));

/**
 * Service level, 0..1: calls answered within the service-level target out of the calls with a known outcome
 * (answered, abandoned, unanswered). `abandoned` includes the short abandons (the caller hung up within 5 seconds);
 * they are taken out of the base, so a misdial does not hurt the service level. Calls still waiting are not in the
 * base. The target is the `serviceLevelSeconds` you asked for.
 */
export function serviceLevel(stats: ServiceStatsLike): number | null {
  return ratio(
    orZero(stats.answeredWithinSl),
    orZero(stats.answered) + longAbandoned(stats) + orZero(stats.unanswered),
  );
}

/**
 * Abandon rate, 0..1: abandoned calls (short ones included) out of all offered calls. With `excludeShortAbandons` the
 * short abandons are removed from the numerator and the denominator, which matches how the service level treats them. Pick one definition and
 * use it everywhere on the dashboard.
 */
export function abandonRate(stats: ServiceStatsLike, options: {excludeShortAbandons?: boolean} = {}): number | null {
  if (options.excludeShortAbandons) {
    return ratio(longAbandoned(stats), orZero(stats.offered) - orZero(stats.shortAbandoned));
  }

  return ratio(orZero(stats.abandoned), orZero(stats.offered));
}

/**
 * Average wait in milliseconds of the calls whose wait has ended.
 *
 * `waitCount` counts a call when it enters the queue, but its wait is added to the wait sums only when the wait ends
 * (answer, hang-up, timeout). While calls are waiting `totalWaitMs / waitCount` is therefore too low. Subtracting
 * `callsWaiting` removes the calls that are still waiting. That is only valid when every waiting call entered the
 * queue inside the lookback window, which holds when the longest wait started at or after `windowStartedAt`;
 * otherwise this falls back to `waitCount`.
 */
export function averageWaitMs(stats: ServiceStatsLike): number | null {
  const waitingInWindow =
    stats.longestWaitStartedAt == null || stats.longestWaitStartedAt >= orZero(stats.windowStartedAt)
      ? orZero(stats.callsWaiting)
      : 0;

  const totalWaitMs =
    orZero(stats.answeredWaitSumMs) + orZero(stats.abandonedWaitSumMs) + orZero(stats.unansweredWaitSumMs);

  return ratio(totalWaitMs, Math.max(0, orZero(stats.waitCount) - waitingInWindow));
}

/** Average speed of answer (ASA) in milliseconds: the average wait of the answered calls only. */
export function averageSpeedOfAnswerMs(stats: ServiceStatsLike): number | null {
  return ratio(orZero(stats.answeredWaitSumMs), orZero(stats.answered));
}

/** Average talk time in milliseconds of the answered calls that have ended. */
export function averageTalkMs(stats: ServiceStatsLike): number | null {
  return ratio(orZero(stats.talkSumMs), orZero(stats.talkCount));
}

/**
 * Wait in milliseconds of the call that has been waiting the longest right now, or null when nobody waits.
 * `longestWaitStartedAt` is a point in time: compute the duration yourself and tick it locally between updates.
 */
export function longestWaitMs(stats: ServiceStatsLike, now: number = Date.now()): number | null {
  return stats.longestWaitStartedAt == null ? null : Math.max(0, now - stats.longestWaitStartedAt);
}

/**
 * Longest wait in the window in milliseconds, including a call that is still waiting. `maxWaitMs` only covers waits
 * that have ended, so a dashboard that shows it alone lags behind the caller who has waited the longest.
 */
export function maxWaitIncludingLiveMs(stats: ServiceStatsLike, now: number = Date.now()): number {
  return Math.max(orZero(stats.maxWaitMs), longestWaitMs(stats, now) ?? 0);
}

/**
 * Calls offered per hour over the span the counters really cover (`periodSeconds`). Counters are summed over whole
 * UTC buckets (15 minutes, or an hour from 2 hours), so `periodSeconds` can be longer than the `periodMinutes` you asked for: use it, not the
 * requested value, as the denominator of any rate.
 */
export function offeredPerHour(stats: ServiceStatsLike): number | null {
  const perSecond = ratio(orZero(stats.offered), orZero(stats.periodSeconds));

  return perSecond == null ? null : perSecond * 3600;
}

/** Average talk time in milliseconds of one agent. */
export function agentAverageTalkMs(agent: AgentStatsLike): number | null {
  return ratio(orZero(agent.talkSumMs), orZero(agent.talkCount));
}

/** Average time in milliseconds from the first ring to the answer, for the calls the agent answered. */
export function agentAverageRingToAnswerMs(agent: AgentStatsLike): number | null {
  return ratio(orZero(agent.ringToAnswerSumMs), orZero(agent.ringToAnswerCount));
}

/** Average time in milliseconds the agent's phone rang, for the calls the agent answered. */
export function agentAverageRingMs(agent: AgentStatsLike): number | null {
  return ratio(orZero(agent.answeredRingSumMs), orZero(agent.answeredRingCount));
}

/** Average ring time in milliseconds of the service's calls: first ring of any agent to the answer or the end. */
export function averageRingMs(stats: ServiceStatsLike): number | null {
  return ratio(orZero(stats.ringSumMs), orZero(stats.ringCount));
}

/**
 * Occupancy of one agent, 0..1: the share of working time spent on calls and wrap-up, working time being the time the
 * agent was ready or busy. Paused time is excluded, so a break does not lower the occupancy.
 */
export function agentOccupancy(agent: AgentStatsLike): number | null {
  const working = orZero(agent.busyMs) + orZero(agent.wrapUpMs);

  return ratio(working, working + orZero(agent.availableMs));
}

/** Share of the calls offered to the agent that the agent answered, 0..1. */
export function agentAnswerRate(agent: AgentStatsLike): number | null {
  return ratio(orZero(agent.answered), orZero(agent.offered));
}

/** How the agents of the service are split right now. The values add up to `total`. */
export interface AgentStateCounts {
  total: number;
  available: number;
  talking: number;
  paused: number;
  wrapUp: number;
  loggedOff: number;
}

/** The agent counts of the stats, with 0 for anything missing. Read them as they are, do not recompute them. */
export function agentStateCounts(stats: ServiceStatsLike): AgentStateCounts {
  return {
    total: orZero(stats.agentsTotal),
    available: orZero(stats.agentsAvailable),
    talking: orZero(stats.agentsTalking),
    paused: orZero(stats.agentsPaused),
    wrapUp: orZero(stats.agentsWrapUp),
    loggedOff: orZero(stats.agentsLoggedOff),
  };
}

/** Formats milliseconds as mm:ss, or h:mm:ss from an hour on. Null renders as a dash. */
export function formatDuration(ms: number | null): string {
  if (ms == null) {
    return '-';
  }

  const seconds = Math.max(0, Math.round(ms / 1000));
  const pad = (value: number) => String(value).padStart(2, '0');

  return seconds >= 3600
    ? `${Math.floor(seconds / 3600)}:${pad(Math.floor((seconds % 3600) / 60))}:${pad(seconds % 60)}`
    : `${pad(Math.floor(seconds / 60))}:${pad(seconds % 60)}`;
}

/** Formats a 0..1 ratio as a whole percent. Null renders as a dash. */
export function formatPercent(value: number | null): string {
  return value == null ? '-' : `${Math.round(value * 100)}%`;
}
