/** Display formatting only. No ride logic lives in the frontend (§4.3). */
import type { Units } from './types';

const EM_DASH = '—';

export function clock(seconds: number | null | undefined): string {
  if (seconds == null || !Number.isFinite(seconds)) return EM_DASH;
  const s = Math.max(0, Math.round(seconds));
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const sec = s % 60;
  const pad = (n: number) => String(n).padStart(2, '0');
  return h > 0 ? `${h}:${pad(m)}:${pad(sec)}` : `${m}:${pad(sec)}`;
}

/** Shorter form for an ETA: "1h 04" / "42 min" / "38 s". */
export function duration(seconds: number | null | undefined): string {
  if (seconds == null || !Number.isFinite(seconds)) return EM_DASH;
  const s = Math.max(0, Math.round(seconds));
  if (s >= 3600) {
    const h = Math.floor(s / 3600);
    const m = Math.floor((s % 3600) / 60);
    return `${h}h ${String(m).padStart(2, '0')}`;
  }
  if (s >= 60) return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
  return `${s}s`;
}

/** Wall-clock time of arrival, e.g. "14:37". */
export function finishAt(secondsFromNow: number | null | undefined): string {
  if (secondsFromNow == null || !Number.isFinite(secondsFromNow)) return EM_DASH;
  const t = new Date(Date.now() + secondsFromNow * 1000);
  return `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`;
}

export function num(v: number | null | undefined, digits = 0): string {
  if (v == null || !Number.isFinite(v)) return EM_DASH;
  return v.toFixed(digits);
}

export function signed(v: number | null | undefined, digits = 1): string {
  if (v == null || !Number.isFinite(v)) return EM_DASH;
  return `${v >= 0 ? '' : '−'}${Math.abs(v).toFixed(digits)}`;
}

export function axisLabel(unit: 'seconds' | 'metres'): string {
  return unit === 'metres' ? 'distance' : 'time';
}

export function axisValue(unit: 'seconds' | 'metres', x: number, units: Units): string {
  return unit === 'metres' ? `${dist(x, units, 1)} ${distUnit(units)}` : clock(x);
}

export function rssiBars(rssi: number): number {
  if (rssi >= -55) return 4;
  if (rssi >= -67) return 3;
  if (rssi >= -78) return 2;
  if (rssi >= -88) return 1;
  return 0;
}

export function targetText(
  target:
    | { Gradient: { percent: number } }
    | { Resistance: { level: number } }
    | { Power: { watts: number } }
    | null,
): string {
  if (!target) return EM_DASH;
  if ('Gradient' in target) return `${signed(target.Gradient.percent, 1)}%`;
  if ('Resistance' in target) return `L${target.Resistance.level}`;
  return `${target.Power.watts} W`;
}

export const MODE_LABEL: Record<string, string> = {
  ManualGrade: 'Manual grade',
  Resistance: 'Resistance',
  Profile: 'Profile',
  Erg: 'ERG',
};

export function connectionText(
  state: string | { Lost: { reason: string } },
): { label: string; tone: 'ok' | 'warn' | 'bad' | 'idle' } {
  if (typeof state !== 'string') return { label: `Lost — ${state.Lost.reason}`, tone: 'bad' };
  switch (state) {
    case 'Controlling':
      return { label: 'Connected', tone: 'ok' };
    case 'Connected':
      return { label: 'Connected', tone: 'warn' };
    case 'Connecting':
      return { label: 'Connecting', tone: 'warn' };
    case 'Reconnecting':
      return { label: 'Reconnecting', tone: 'warn' };
    case 'Scanning':
      return { label: 'Discovered', tone: 'idle' };
    default:
      return { label: 'Idle', tone: 'idle' };
  }
}

// ---------------------------------------------------------------------------
// Units (FR-7.4's neighbour)
//
// SI everywhere behind this line. The engine computes in metres and km/h, the
// FIT file records in metres and km/h, and a rider who prefers miles changes
// *only* what the glass says. Every function here therefore takes the unit
// system as an argument rather than reading a module-level setting: pure
// functions are what let the ride screen redraw the moment the preference
// changes, and what keeps a recorded activity independent of a display choice.
// ---------------------------------------------------------------------------

const KM_PER_MILE = 1.609344;
const M_PER_FOOT = 0.3048;
const KG_PER_LB = 0.45359237;

export function distUnit(units: Units): string {
  return units === 'imperial' ? 'mi' : 'km';
}

export function elevUnit(units: Units): string {
  return units === 'imperial' ? 'ft' : 'm';
}

export function speedUnit(units: Units): string {
  return units === 'imperial' ? 'mph' : 'km/h';
}

export function massUnit(units: Units): string {
  return units === 'imperial' ? 'lb' : 'kg';
}

/** Metres → the rider's long-distance unit. */
export function dist(metres: number | null | undefined, units: Units, digits = 2): string {
  if (metres == null || !Number.isFinite(metres)) return EM_DASH;
  const km = metres / 1000;
  return (units === 'imperial' ? km / KM_PER_MILE : km).toFixed(digits);
}

/** Metres of climbing → metres or feet. Always whole: nobody climbs 0.4 m. */
export function elev(metres: number | null | undefined, units: Units): string {
  if (metres == null || !Number.isFinite(metres)) return EM_DASH;
  return (units === 'imperial' ? metres / M_PER_FOOT : metres).toFixed(0);
}

export function speed(kph: number | null | undefined, units: Units, digits = 1): string {
  if (kph == null || !Number.isFinite(kph)) return EM_DASH;
  return (units === 'imperial' ? kph / KM_PER_MILE : kph).toFixed(digits);
}

/** Kilograms → kg or lb. For the settings form, which edits SI underneath. */
export function toMass(kg: number, units: Units): number {
  return units === 'imperial' ? kg / KG_PER_LB : kg;
}

export function fromMass(value: number, units: Units): number {
  return units === 'imperial' ? value * KG_PER_LB : value;
}

// ---------------------------------------------------------------------------
// Effort zones
//
// A watt is a fact; a zone is what it *costs you*, and it is the difference
// between a number and a number that means something. Every professional
// training app colours effort this way and this one did not, so the biggest
// figure on the ride screen was the same shade of white at 90 W and at 400 W.
//
// Two rules hold here:
//
//   1. **No reference, no zone.** `ftp` or `maxHr` of zero means the rider has
//      not told us, and an invented threshold would paint every ride with a
//      confident lie. `null` is the honest answer and the callers draw the
//      plain number.
//   2. **Colour never carries it alone.** Every zone has a short name that is
//      rendered beside the swatch, so the meaning survives a colour-blind
//      rider, a sun-washed phone and a black-and-white screenshot.
// ---------------------------------------------------------------------------

export interface Zone {
  /** One-based, as riders say them: "zone 4". */
  index: number;
  /** What it is called in a training plan. */
  name: string;
  /** Short form for a chip beside a number: "Z4". */
  short: string;
  /** A CSS custom property reference, so themes stay in the stylesheet. */
  colour: string;
}

/**
 * Coggan's seven power zones, as fractions of FTP. The boundaries are the
 * conventional ones — 55 / 75 / 90 / 105 / 120 / 150 % — which is what makes a
 * "zone 3 ride" here mean the same thing it means in the rider's plan.
 */
const POWER_ZONES: { upto: number; zone: Zone }[] = [
  { upto: 0.55, zone: { index: 1, name: 'Recovery', short: 'Z1', colour: 'var(--zone-1)' } },
  { upto: 0.75, zone: { index: 2, name: 'Endurance', short: 'Z2', colour: 'var(--zone-2)' } },
  { upto: 0.9, zone: { index: 3, name: 'Tempo', short: 'Z3', colour: 'var(--zone-3)' } },
  { upto: 1.05, zone: { index: 4, name: 'Threshold', short: 'Z4', colour: 'var(--zone-4)' } },
  { upto: 1.2, zone: { index: 5, name: 'VO₂ max', short: 'Z5', colour: 'var(--zone-5)' } },
  { upto: 1.5, zone: { index: 6, name: 'Anaerobic', short: 'Z6', colour: 'var(--zone-6)' } },
  { upto: Infinity, zone: { index: 7, name: 'Sprint', short: 'Z7', colour: 'var(--zone-7)' } },
];

/** Five heart-rate zones as fractions of maximum. */
const HR_ZONES: { upto: number; zone: Zone }[] = [
  { upto: 0.6, zone: { index: 1, name: 'Recovery', short: 'Z1', colour: 'var(--zone-1)' } },
  { upto: 0.7, zone: { index: 2, name: 'Endurance', short: 'Z2', colour: 'var(--zone-2)' } },
  { upto: 0.8, zone: { index: 3, name: 'Tempo', short: 'Z3', colour: 'var(--zone-3)' } },
  { upto: 0.9, zone: { index: 4, name: 'Threshold', short: 'Z4', colour: 'var(--zone-5)' } },
  { upto: Infinity, zone: { index: 5, name: 'Maximum', short: 'Z5', colour: 'var(--zone-6)' } },
];

function zoneFor(
  table: { upto: number; zone: Zone }[],
  value: number | null | undefined,
  reference: number,
): Zone | null {
  // Not pedalling is not zone 1. A stopped rider in "Recovery" green is the
  // screen claiming an effort that is not happening.
  if (!reference || value == null || !Number.isFinite(value) || value <= 0) return null;
  const ratio = value / reference;
  return table.find((row) => ratio <= row.upto)?.zone ?? null;
}

export function powerZone(watts: number | null | undefined, ftp: number): Zone | null {
  return zoneFor(POWER_ZONES, watts, ftp);
}

export function hrZone(bpm: number | null | undefined, maxHr: number): Zone | null {
  return zoneFor(HR_ZONES, bpm, maxHr);
}

/** Percentage of the reference, for the subtitle under a zoned number. */
export function percentOf(value: number | null | undefined, reference: number): string | null {
  if (!reference || value == null || !Number.isFinite(value) || value <= 0) return null;
  return `${Math.round((value / reference) * 100)}% of FTP`;
}
