/**
 * The only place that talks to Rust.
 *
 * Commands are intents; they never mutate local state directly. Truth comes
 * back on the event channel (§4.3).
 */
import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import type {
  ControlMode,
  DeviceInfo,
  DeviceList,
  InputAck,
  LapSummary,
  Notice,
  ProfileView,
  Recovered,
  RideFrame,
  RideState,
  RideSummary,
  RiderConfig,
  SafetyLimits,
  SampleProfile,
} from './types';

export const EVENTS = {
  snapshot: 'ride://snapshot',
  rideState: 'ride://state',
  lap: 'ride://lap',
  summary: 'ride://summary',
  devices: 'devices://updated',
  connection: 'devices://connection',
  notice: 'app://notice',
  inputAck: 'app://input-ack',
  controllerInput: 'controller://input',
  controllerStatus: 'controller://status',
} as const;

/** Button names as sent by `controller::button_name`. */
export type ControllerButton =
  | 'left'
  | 'up'
  | 'right'
  | 'down'
  | 'a'
  | 'b'
  | 'y'
  | 'z'
  | 'minus'
  | 'plus';

/**
 * Which Click pod, named for the shift paddle it carries.
 *
 * Not left and right: nothing the pod advertises says which end of the bar it
 * is clamped to, so those labels would be a guess that a rider who mounts them
 * the other way round makes wrong. The paddle is printed on the pod.
 */
export type Pod = 'minus' | 'plus';

/** A press or release edge from a Zwift Click. Repeats while held are already
 *  filtered out in Rust, so every event here is a real edge. */
export type ControllerInput = {
  button: ControllerButton;
  pressed: boolean;
  /** Which pod sent it. Not used for routing — a `+` press means the same
   *  thing whichever pod it came from — but it proves a pod is alive. */
  pod: Pod;
};

/** Where one pod's link has got to. `idle` and `gaveUp` are both "not
 *  connected", but they call for different things from the rider. */
export type PodState = 'idle' | 'searching' | 'connected' | 'reconnecting' | 'gaveUp';

export type PodStatus = {
  pod: Pod;
  /** `+` or `−`, ready to render. */
  symbol: string;
  state: PodState;
  address: string | null;
  name: string | null;
  batteryPercent: number | null;
  error: string | null;
  lastButton: ControllerButton | null;
  buttonsSeen: number;
  /** This pod has sent its own paddle, so the label is proven, not assumed. */
  confirmed: boolean;
  /** It sent the *other* pod's paddle: the pair may be filed the wrong way
   *  round, or this pod reports for both. */
  contradicted: boolean;
};

/** A Click v2 is two peripherals, and each has its own link (FR-1.4). */
export type ControllerStatus = {
  minus: PodStatus;
  plus: PodStatus;
  /** The +/− assignment has been flipped by the rider. */
  swapped: boolean;
};

/** True when running inside the Tauri shell rather than a bare browser. */
export const inTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;

async function call<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
  return invoke<T>(cmd, args);
}

export const api = {
  // ride lifecycle
  rideState: () => call<RideState>('ride_state'),
  start: () => call<RideState>('start_ride'),
  pause: () => call<RideState>('pause_ride'),
  resume: () => call<RideState>('resume_ride'),
  togglePause: () => call<RideState>('toggle_pause'),
  stop: () => call<RideState>('stop_ride'),
  reset: () => call<RideState>('reset_ride'),

  // recording and export
  rideSummary: () => call<RideSummary | null>('ride_summary'),
  /** Copy the finished activity to `path`; resolves with the path written. */
  saveFit: (path: string) => call<string>('save_fit', { path }),
  /** Rides rebuilt from an interrupted session. Drains — call once at start. */
  recoveredRides: () => call<Recovered[]>('recovered_rides'),

  // control modes and targets
  setMode: (mode: ControlMode) => call<RideState>('set_control_mode', { mode }),
  cycleMode: () => call<RideState>('cycle_control_mode'),
  shiftGear: (delta: number) => call<RideState>('shift_gear', { delta }),
  setGear: (gear: number) => call<RideState>('set_gear', { gear }),
  nudgeGradient: (deltaPct: number) => call<RideState>('nudge_gradient', { deltaPct }),
  setGradient: (percent: number) => call<RideState>('set_gradient', { percent }),
  resetGradient: () => call<RideState>('reset_gradient'),
  setResistance: (level: number) => call<RideState>('set_target_resistance', { level }),
  setPower: (watts: number) => call<RideState>('set_target_power', { watts }),
  markLap: () => call<LapSummary>('mark_lap'),

  // configuration
  riderConfig: () => call<RiderConfig>('rider_config'),
  setRiderConfig: (config: RiderConfig) => call<RiderConfig>('set_rider_config', { config }),
  safetyLimits: () => call<SafetyLimits>('safety_limits'),
  setSafetyLimits: (limits: SafetyLimits) => call<SafetyLimits>('set_safety_limits', { limits }),

  // profiles
  loadProfilePath: (path: string) => call<ProfileView>('load_profile_from_path', { path }),
  loadProfileText: (name: string, text: string, isGpx: boolean) =>
    call<ProfileView>('load_profile_from_text', { name, text, isGpx }),
  previewYaml: (yaml: string) => call<ProfileView>('preview_profile_yaml', { yaml }),
  clearProfile: () => call<RideState>('clear_profile'),
  sampleProfiles: () => call<SampleProfile[]>('sample_profiles'),

  // devices
  deviceList: () => call<DeviceList>('device_list'),
  startScan: () => call<void>('start_scan'),
  stopScan: () => call<void>('stop_scan'),
  connect: (deviceId: string) => call<DeviceInfo>('connect_device', { deviceId }),
  disconnect: (deviceId: string) => call<DeviceInfo>('disconnect_device', { deviceId }),
  forget: (deviceId: string) => call<void>('forget_device', { deviceId }),
  trainerControllable: () => call<boolean>('trainer_controllable'),

  // controller (Zwift Click)
  controllerStatus: () => call<ControllerStatus>('controller_status'),
  /** One pod, or both when `pod` is omitted. `deviceId` names a specific pod
   *  the scanner has already listed, and needs `pod` alongside it. */
  connectController: (pod?: Pod, deviceId?: string) =>
    call<void>('connect_controller', { pod, deviceId }),
  disconnectController: (pod?: Pod) => call<void>('disconnect_controller', { pod }),
  /** Exchange + and −, for when the pods answer to the other name. */
  swapControllerPods: () => call<void>('swap_controller_pods'),
};

type Handlers = {
  onFrame?: (f: RideFrame) => void;
  onRideState?: (s: RideState) => void;
  onLap?: (l: LapSummary) => void;
  onSummary?: (s: RideSummary) => void;
  onDevices?: (d: DeviceList) => void;
  onNotice?: (n: Notice) => void;
  onInputAck?: (a: InputAck) => void;
  onControllerInput?: (i: ControllerInput) => void;
  onControllerStatus?: (s: ControllerStatus) => void;
};

/** Subscribe to the whole event channel. Returns a single unsubscribe. */
export async function subscribe(h: Handlers): Promise<UnlistenFn> {
  const offs: UnlistenFn[] = [];
  const add = async <T>(name: string, fn?: (p: T) => void) => {
    if (!fn) return;
    offs.push(await listen<T>(name, (e) => fn(e.payload)));
  };
  await add(EVENTS.snapshot, h.onFrame);
  await add(EVENTS.rideState, h.onRideState);
  await add(EVENTS.lap, h.onLap);
  await add(EVENTS.summary, h.onSummary);
  await add(EVENTS.devices, h.onDevices);
  await add(EVENTS.notice, h.onNotice);
  await add(EVENTS.inputAck, h.onInputAck);
  await add(EVENTS.controllerInput, h.onControllerInput);
  await add(EVENTS.controllerStatus, h.onControllerStatus);
  return () => offs.forEach((off) => off());
}
