<script lang="ts">
  import { _ } from "svelte-i18n";
  import Meta from "$lib/components/meta/meta.svelte";
  import type * as ButtplugTypes from "@sexy/buttplug";
  import Button from "$lib/components/ui/button/button.svelte";
  import { onMount } from "svelte";
  import DeviceCard from "$lib/components/device-card/device-card.svelte";
  import RecordingSaveDialog from "../components/recording-save-dialog.svelte";
  import DeviceMappingDialog from "../components/device-mapping-dialog.svelte";
  import type { BluetoothDevice, RecordedEvent, DeviceInfo } from "$lib/types";
  import { toast } from "svelte-sonner";
  import { createRecording } from "$lib/services";
  import * as Empty from "$lib/components/ui/empty";

  // Runtime buttplug values — loaded dynamically from the buttplug nginx container
  let client: ButtplugTypes.ButtplugClient;
  let InputType: typeof ButtplugTypes.InputType;
  let DeviceOutputValueConstructor: typeof ButtplugTypes.DeviceOutputValueConstructor;
  let ButtplugWasmClientConnector: typeof ButtplugTypes.ButtplugWasmClientConnector;

  let connected = $state(false);
  let scanning = $state(false);
  let devices = $state<BluetoothDevice[]>([]);

  // Recording state
  let isRecording = $state(false);
  let recordingStartTime = $state<number | null>(null);
  let recordedEvents = $state<RecordedEvent[]>([]);
  let showSaveDialog = $state(false);
  let recordingDuration = $state(0);

  // Playback state
  let isPlaying = $state(false);
  let playbackProgress = $state(0);
  let playbackStartTime = $state<number | null>(null);
  let playbackTimeoutId = $state<number | null>(null);
  let currentEventIndex = $state(0);
  let showMappingDialog = $state(false);
  let deviceMappings = $state<Map<string, BluetoothDevice>>(new Map());

  async function init() {
    const connector = new ButtplugWasmClientConnector();
    await client.connect(connector);
    client.on("deviceadded", onDeviceAdded);
    client.on("deviceremoved", (dev: ButtplugTypes.ButtplugClientDevice) => {
      const idx = devices.findIndex((d) => d.info.index === dev.index);
      if (idx !== -1) devices.splice(idx, 1);
    });
    client.on("scanningfinished", () => (scanning = false));
    client.on("inputreading", handleInputReading);
    connected = client.connected;
  }

  async function startScanning() {
    await client.startScanning();
    scanning = true;
  }

  async function onDeviceAdded(dev: ButtplugTypes.ButtplugClientDevice) {
    const device = convertDevice(dev);
    devices.push(device);

    const idx = devices.length - 1;
    if (device.hasBattery) {
      try {
        devices[idx].batteryLevel = await dev.battery();
      } catch (e) {
        console.warn(`Failed to read battery for ${dev.name}:`, e);
      }
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  function handleInputReading(msg: any) {
    if (msg.InputReading === undefined) return;
    const reading = msg.InputReading;
    const device = devices.find((d) => d.info.index === reading.DeviceIndex);
    if (!device) return;

    if (reading.Reading[InputType.Battery] !== undefined) {
      device.batteryLevel = reading.Reading[InputType.Battery].Value;
    }
    device.lastSeen = new Date();
  }

  async function handleChange(device: BluetoothDevice, actuatorIdx: number, value: number) {
    const actuator = device.actuators[actuatorIdx];
    const feature = device.info.features.get(actuator.featureIndex);
    if (!feature) return;

    actuator.value = value;
    const outputType = actuator.outputType as typeof ButtplugTypes.OutputType;
    await feature.runOutput(new DeviceOutputValueConstructor(outputType).steps(value));

    if (isRecording && recordingStartTime) {
      captureEvent(device, actuatorIdx, value);
    }
  }

  function startRecording() {
    if (devices.length === 0) return;
    isRecording = true;
    recordingStartTime = performance.now();
    recordedEvents = [];
    recordingDuration = 0;
  }

  function stopRecording() {
    isRecording = false;
    if (recordedEvents.length > 0) {
      recordingDuration = recordedEvents[recordedEvents.length - 1].timestamp;
      showSaveDialog = true;
    }
  }

  function captureEvent(device: BluetoothDevice, actuatorIdx: number, value: number) {
    if (!recordingStartTime) return;

    const timestamp = performance.now() - recordingStartTime;
    const actuator = device.actuators[actuatorIdx];

    recordedEvents.push({
      timestamp,
      device_index: device.info.index,
      device_name: device.name,
      actuator_index: actuatorIdx,
      actuator_type: actuator.outputType,
      value: (value / actuator.maxSteps) * 100,
    });
  }

  async function handleStop(device: BluetoothDevice) {
    await device.info.stop();
    device.actuators.forEach((a) => (a.value = 0));
  }

  function convertDevice(device: ButtplugTypes.ButtplugClientDevice): BluetoothDevice {
    const actuators: import("$lib/types").DeviceActuator[] = []; // eslint-disable-line @typescript-eslint/consistent-type-imports
    for (const [, feature] of device.features) {
      for (const outputType of feature.outputTypes) {
        actuators.push({
          featureIndex: feature.featureIndex,
          outputType,
          maxSteps: feature.outputMaxValue(outputType),
          descriptor: feature.featureDescriptor,
          value: 0,
        });
      }
    }

    return {
      id: String(device.index),
      name: device.name,
      actuators,
      batteryLevel: 0,
      hasBattery: device.hasInput(InputType.Battery),
      isConnected: true,
      lastSeen: new Date(),
      info: device,
    };
  }

  async function handleSaveRecording(saveData: {
    title: string;
    description: string;
    tags: string[];
  }) {
    const deviceInfo: DeviceInfo[] = devices.map((d) => ({
      name: d.name,
      index: d.info.index,
      capabilities: d.actuators.map((a) => a.outputType),
    }));

    try {
      await createRecording({
        title: saveData.title,
        description: saveData.description,
        duration: Math.round(recordingDuration),
        events: recordedEvents,
        device_info: deviceInfo,
        tags: saveData.tags,
        status: "draft",
      });

      toast.success("Recording saved successfully!");
      showSaveDialog = false;
      recordedEvents = [];
      recordingDuration = 0;
    } catch (error) {
      console.error("Failed to save recording:", error);
      toast.error("Failed to save recording. Please try again.");
    }
  }

  function handleCancelSave() {
    showSaveDialog = false;
    recordedEvents = [];
    recordingDuration = 0;
  }

  function startPlayback() {
    if (!data.recording) return;

    if (devices.length === 0) {
      toast.error("Please connect devices before playing recording");
      return;
    }

    if (deviceMappings.size === 0 && (data.recording.device_info?.length ?? 0) > 0) {
      showMappingDialog = true;
      return;
    }

    beginPlayback();
  }

  function beginPlayback() {
    isPlaying = true;
    playbackStartTime = performance.now();
    playbackProgress = 0;
    currentEventIndex = 0;
    scheduleNextEvent();
  }

  function handleMappingConfirm(mappings: Map<string, BluetoothDevice>) {
    deviceMappings = mappings;
    showMappingDialog = false;
    beginPlayback();
  }

  function handleMappingCancel() {
    showMappingDialog = false;
  }

  function stopPlayback() {
    isPlaying = false;
    if (playbackTimeoutId !== null) {
      clearTimeout(playbackTimeoutId);
      playbackTimeoutId = null;
    }
    playbackProgress = 0;
    currentEventIndex = 0;
    devices.forEach((device) => handleStop(device));
  }

  function pausePlayback() {
    isPlaying = false;
    if (playbackTimeoutId !== null) {
      clearTimeout(playbackTimeoutId);
      playbackTimeoutId = null;
    }
  }

  function resumePlayback() {
    if (!data.recording) return;
    isPlaying = true;
    playbackStartTime = performance.now() - playbackProgress;
    scheduleNextEvent();
  }

  function scheduleNextEvent() {
    if (!data.recording || !isPlaying || !playbackStartTime) return;

    const events = (data.recording.events ?? []) as RecordedEvent[];
    if (currentEventIndex >= events.length) {
      stopPlayback();
      toast.success("Playback finished");
      return;
    }

    const event = events[currentEventIndex];
    const currentTime = performance.now() - playbackStartTime;
    const delay = event.timestamp - currentTime;

    if (delay <= 0) {
      executeEvent(event);
      currentEventIndex++;
      scheduleNextEvent();
    } else {
      playbackTimeoutId = setTimeout(() => {
        executeEvent(event);
        currentEventIndex++;
        playbackProgress = event.timestamp;
        scheduleNextEvent();
      }, delay) as unknown as number;
    }
  }

  function executeEvent(event: RecordedEvent) {
    const device = deviceMappings.get(event.device_name);
    if (!device) {
      console.warn(`No device mapping for: ${event.device_name}`);
      return;
    }

    const actuator = device.actuators.find((a) => a.outputType === event.actuator_type);
    if (!actuator) {
      console.warn(`Actuator type ${event.actuator_type} not found on ${device.name}`);
      return;
    }

    const deviceValue = Math.round((event.value / 100) * actuator.maxSteps);
    const feature = device.info.features.get(actuator.featureIndex);
    if (feature) {
      const outputType = actuator.outputType as typeof ButtplugTypes.OutputType;
      feature.runOutput(new DeviceOutputValueConstructor(outputType).steps(deviceValue));
    }

    actuator.value = deviceValue;
  }

  function seek(percentage: number) {
    if (!data.recording) return;

    const targetTime = (percentage / 100) * data.recording.duration;
    playbackProgress = targetTime;

    const seekEvents = (data.recording.events ?? []) as RecordedEvent[];
    currentEventIndex = seekEvents.findIndex((e) => e.timestamp >= targetTime);
    if (currentEventIndex === -1) {
      currentEventIndex = seekEvents.length;
    }

    if (isPlaying) {
      if (playbackTimeoutId !== null) {
        clearTimeout(playbackTimeoutId);
      }
      playbackStartTime = performance.now() - targetTime;
      scheduleNextEvent();
    }
  }

  const { data } = $props();

  onMount(async () => {
    // Concatenation prevents Rollup from statically resolving this URL at build time
    const buttplugUrl = "/buttplug/" + "dist/index.js";
    const bp = await import(/* @vite-ignore */ buttplugUrl);
    InputType = bp.InputType;
    DeviceOutputValueConstructor = bp.DeviceOutputValueConstructor;
    ButtplugWasmClientConnector = bp.ButtplugWasmClientConnector;
    client = new bp.ButtplugClient("Sexy.Art");
    connected = client.connected;
    await init();
  });
</script>

<Meta title={$_("play.title")} description={$_("play.description")} />

<div class="py-3 sm:py-6 lg:pl-6">
  <!-- Header -->
  <div class="mb-6">
    <h1 class="text-2xl font-bold">{$_("play.title")}</h1>
    <p class="text-sm text-muted-foreground mt-1">{$_("play.description")}</p>
  </div>

  <!-- Recording controls (only when devices are connected) -->
  {#if devices.length > 0 && !data.recording}
    <div class="flex flex-wrap items-center gap-3 mb-6">
      {#if !isRecording}
        <Button
          variant="outline"
          onclick={startRecording}
          class="cursor-pointer border-primary/30 hover:bg-primary/10"
        >
          <span class="icon-[ri--record-circle-line] w-4 h-4 mr-2"></span>
          Start Recording
        </Button>
      {:else}
        <Button
          onclick={stopRecording}
          class="cursor-pointer bg-red-500 hover:bg-red-600 text-white"
        >
          <span class="icon-[ri--stop-circle-fill] w-4 h-4 mr-2 animate-pulse"></span>
          Stop Recording ({recordedEvents.length} events)
        </Button>
      {/if}
    </div>
  {/if}

  <!-- Playback Controls (only shown when recording is loaded) -->
  {#if data.recording}
    <div class="bg-card/50 border border-primary/20 rounded-lg p-6 mb-6">
      <div class="mb-4">
        <h2 class="text-xl font-semibold text-card-foreground mb-1">
          {data.recording.title}
        </h2>
        {#if data.recording.description}
          <p class="text-sm text-muted-foreground">
            {data.recording.description}
          </p>
        {/if}
      </div>

      <!-- Progress Bar -->
      <div class="mb-4">
        <div class="flex items-center gap-3 mb-2">
          <span class="text-sm text-muted-foreground min-w-[50px]">
            {Math.floor(playbackProgress / 1000 / 60)}:{(Math.floor(playbackProgress / 1000) % 60)
              .toString()
              .padStart(2, "0")}
          </span>
          <div
            role="slider"
            tabindex="0"
            aria-label="Seek"
            aria-valuemin={0}
            aria-valuemax={data.recording.duration}
            aria-valuenow={playbackProgress}
            class="flex-1 h-2 bg-muted rounded-full overflow-hidden cursor-pointer relative"
            onclick={(e) => {
              const rect = e.currentTarget.getBoundingClientRect();
              const percentage = ((e.clientX - rect.left) / rect.width) * 100;
              seek(percentage);
            }}
            onkeydown={(e) => {
              if (e.key === "ArrowRight")
                seek(((playbackProgress + 1) / data.recording.duration) * 100);
              else if (e.key === "ArrowLeft")
                seek(((playbackProgress - 1) / data.recording.duration) * 100);
            }}
          >
            <div
              class="absolute inset-0 bg-gradient-to-r from-primary to-accent transition-all duration-150"
              style="width: {(playbackProgress / data.recording.duration) * 100}%"
            ></div>
          </div>
          <span class="text-sm text-muted-foreground min-w-[50px] text-right">
            {Math.floor(data.recording.duration / 1000 / 60)}:{(
              Math.floor(data.recording.duration / 1000) % 60
            )
              .toString()
              .padStart(2, "0")}
          </span>
        </div>
      </div>

      <!-- Playback Buttons -->
      <div class="flex gap-2 justify-center">
        <Button
          variant="outline"
          onclick={stopPlayback}
          disabled={!isPlaying && playbackProgress === 0}
          class="cursor-pointer border-primary/30 hover:bg-primary/10"
        >
          <span class="icon-[ri--stop-fill] w-5 h-5"></span>
        </Button>
        {#if !isPlaying}
          <Button
            onclick={playbackProgress > 0 ? resumePlayback : startPlayback}
            disabled={devices.length === 0}
            class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 min-w-[120px]"
          >
            <span class="icon-[ri--play-fill] w-5 h-5 mr-2"></span>
            {playbackProgress > 0 ? "Resume" : "Play"}
          </Button>
        {:else}
          <Button
            onclick={pausePlayback}
            class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 min-w-[120px]"
          >
            <span class="icon-[ri--pause-fill] w-5 h-5 mr-2"></span>
            Pause
          </Button>
        {/if}
      </div>

      <!-- Recording Info -->
      <div class="mt-4 pt-4 border-t border-border/50 grid grid-cols-3 gap-4 text-center">
        <div>
          <p class="text-xs text-muted-foreground">Events</p>
          <p class="text-sm font-medium">{data.recording.events?.length ?? 0}</p>
        </div>
        <div>
          <p class="text-xs text-muted-foreground">Devices</p>
          <p class="text-sm font-medium">{data.recording.device_info?.length ?? 0}</p>
        </div>
        <div>
          <p class="text-xs text-muted-foreground">Status</p>
          <p class="text-sm font-medium capitalize">{data.recording.status}</p>
        </div>
      </div>
    </div>
  {/if}

  <!-- Devices grid or empty state -->
  {#if devices.length > 0}
    <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
      {#each devices as device (device.name)}
        <DeviceCard
          {device}
          onChange={(scalarIndex, val) => handleChange(device, scalarIndex, val)}
          onStop={() => handleStop(device)}
        />
      {/each}
    </div>
  {:else}
    <Empty.Root>
      <Empty.Header>
        <Empty.Media>
          <span class="icon-[ri--rocket-line] w-8 h-8"></span>
        </Empty.Media>
        <Empty.Title>{$_("play.no_results")}</Empty.Title>
        <Empty.Description>{$_("play.no_results_description")}</Empty.Description>
      </Empty.Header>
      <Empty.Content>
        <Button
          disabled={!connected || scanning}
          onclick={startScanning}
          class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
        >
          {#if scanning}
            <div
              class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"
            ></div>
            {$_("play.scanning")}
          {:else}
            <span class="icon-[ri--rocket-line] w-4 h-4 mr-2"></span>
            {$_("play.scan")}
          {/if}
        </Button>
      </Empty.Content>
    </Empty.Root>
  {/if}
</div>

<!-- Recording Save Dialog -->
<RecordingSaveDialog
  open={showSaveDialog}
  events={recordedEvents}
  deviceInfo={devices.map((d) => ({
    name: d.name,
    index: d.info.index,
    capabilities: d.actuators.map((a) => a.outputType),
  }))}
  duration={recordingDuration}
  onSave={handleSaveRecording}
  onCancel={handleCancelSave}
/>

<!-- Device Mapping Dialog -->
{#if data.recording}
  <DeviceMappingDialog
    open={showMappingDialog}
    recordedDevices={(data.recording.device_info ?? []) as DeviceInfo[]}
    connectedDevices={devices}
    onConfirm={handleMappingConfirm}
    onCancel={handleMappingCancel}
  />
{/if}
