<template>
  <main class="home-view">
    <PullToRefresh :is-at-top="isAtTop" :on-refresh="refreshDevices">
      <!-- Loading -->
      <div v-if="devicesStore.loading" class="home-view__loading" aria-live="polite">
        Loading…
      </div>

      <!-- Empty state -->
      <div v-else-if="devicesStore.devices.length === 0" class="home-view__empty">
        <div class="home-view__empty-card">
          <svg class="home-view__empty-icon" width="48" height="48" viewBox="0 0 24 24" fill="none"
               stroke="currentColor" stroke-width="1.5" aria-hidden="true">
            <rect x="3" y="3" width="18" height="18" rx="2"/>
            <circle cx="8.5" cy="8.5" r="1.5"/>
            <polyline points="21,15 16,10 5,21"/>
          </svg>
          <p class="home-view__empty-title">Set up your first frame</p>
          <p class="home-view__empty-sub">
            Power on your pictureFrame device and scan the QR code it displays to get started.
          </p>
        </div>
      </div>

      <!-- Single device — large card -->
      <div v-else-if="devicesStore.devices.length === 1" class="home-view__single">
        <FrameCard
          :deviceId="devicesStore.devices[0].id"
          :name="devicesStore.devices[0].name"
          size="large"
          :status="deviceStatus(devicesStore.devices[0])"
          :orientation="devicesStore.devices[0].orientation"
          :thumbnailUrl="previewUrl(devicesStore.devices[0])"
          :lastSync="lastSyncLabel(devicesStore.devices[0])"
          :nextSync="nextSyncLabel(devicesStore.devices[0])"
          @add-photo="onAddPhoto"
          @edit="onEdit"
        />
      </div>

      <!-- Multiple devices — vertical scroll-snap stack of large cards -->
      <div
        v-else
        ref="stackEl"
        class="home-view__stack"
        role="list"
        aria-label="Frames"
      >
        <div
          v-for="device in devicesStore.devices"
          :key="device.id"
          class="home-view__slide"
          role="listitem"
          :aria-label="device.name"
        >
          <FrameCard
            :deviceId="device.id"
            :name="device.name"
            size="large"
            :status="deviceStatus(device)"
            :orientation="device.orientation"
            :thumbnailUrl="previewUrl(device)"
            :lastSync="lastSyncLabel(device)"
            :nextSync="nextSyncLabel(device)"
            @add-photo="onAddPhoto"
            @edit="onEdit"
          />
        </div>
      </div>
    </PullToRefresh>
  </main>

  <!-- Frame settings sheet -->
  <BaseBottomSheet v-model="sheetOpen" label="Frame settings">
    <h2 class="home-view__sheet-title">Frame settings</h2>

    <div class="home-view__sheet-field">
      <BaseInput
        v-model="editName"
        label="Frame name"
        maxlength="100"
      />
    </div>

    <div class="home-view__sheet-field">
      <p class="home-view__sheet-label">Orientation</p>
      <OrientationPicker v-model="editOrientation" />
    </div>

    <div class="home-view__sheet-field">
      <p class="home-view__sheet-label">Update frequency</p>

      <select
        class="home-view__mode-select"
        v-model="editFrequencyMode"
        aria-label="Update frequency mode"
      >
        <option value="times">At specific time(s)</option>
        <option value="interval">Every X minutes</option>
      </select>

      <div v-if="editFrequencyMode === 'times'" class="home-view__times-mode">
        <div class="home-view__times-list">
          <div
            v-for="(m, idx) in editWakeTimes"
            :key="idx"
            class="home-view__time-row"
          >
            <select
              class="home-view__time-part"
              :value="hmpFromMinutes(m).h"
              aria-label="Hour"
              @change="onTimePart(idx, 'h', ($event.target as HTMLSelectElement).value)"
            >
              <option v-for="h in HOUR_OPTIONS" :key="h" :value="h">{{ h }}</option>
            </select>
            <span class="home-view__time-sep">:</span>
            <select
              class="home-view__time-part"
              :value="hmpFromMinutes(m).mm"
              aria-label="Minutes"
              @change="onTimePart(idx, 'mm', ($event.target as HTMLSelectElement).value)"
            >
              <option v-for="mm in MINUTE_OPTIONS" :key="mm" :value="mm">
                {{ String(mm).padStart(2, '0') }}
              </option>
            </select>
            <select
              class="home-view__time-part home-view__time-part--ampm"
              :value="hmpFromMinutes(m).p"
              aria-label="AM or PM"
              @change="onTimePart(idx, 'p', ($event.target as HTMLSelectElement).value)"
            >
              <option value="AM">AM</option>
              <option value="PM">PM</option>
            </select>
            <button
              type="button"
              class="home-view__time-remove"
              :aria-label="`Remove ${formatTime(m)}`"
              @click="removeTime(idx)"
            >
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
                   stroke="currentColor" stroke-width="2" aria-hidden="true">
                <polyline points="3 6 5 6 21 6"/>
                <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
                <path d="M10 11v6"/>
                <path d="M14 11v6"/>
              </svg>
            </button>
          </div>

          <p
            v-if="editWakeTimes.length === 0"
            class="home-view__times-empty"
          >No update times yet — add one below.</p>
        </div>

        <button
          type="button"
          class="home-view__time-add"
          @click="addTime"
        >+ Add time</button>

        <select class="home-view__tz-select" v-model="editTimezone">
          <optgroup v-for="group in TIMEZONE_GROUPS" :key="group.label" :label="group.label">
            <option v-for="tz in group.zones" :key="tz.value" :value="tz.value">{{ tz.label }}</option>
          </optgroup>
        </select>
      </div>

      <div v-else class="home-view__interval-mode">
        <div class="home-view__interval-input-row">
          <span>Every</span>
          <input
            type="number"
            inputmode="numeric"
            pattern="[0-9]*"
            min="1"
            max="1440"
            class="home-view__interval-input"
            v-model.number="editIntervalMinutes"
            aria-label="Update interval in minutes"
          />
          <span>minutes</span>
        </div>
      </div>

      <p class="home-view__next-update" aria-live="polite">{{ nextUpdatePreview }}</p>
      <p class="home-view__propagation-note">
        Changes will only take effect at the next device update. To force
        an immediate refresh, briefly disconnect and reconnect the frame’s
        power.
      </p>
    </div>

    <div class="home-view__sheet-field">
      <p class="home-view__sheet-label">Image selection</p>

      <select
        class="home-view__mode-select"
        v-model="editRotationMode"
        aria-label="Image selection mode"
      >
        <option value="oldest_upload">Oldest upload first</option>
        <option value="newest_upload">Newest upload first</option>
        <option value="least_recently_shown">Least recently shown</option>
        <option value="random">Random</option>
      </select>

      <label class="home-view__rotation-checkbox">
        <input
          type="checkbox"
          v-model="editPrioritizeNeverShown"
        />
        <span>Show never-shown images first</span>
      </label>
    </div>

    <BaseButton
      variant="primary"
      class="home-view__sheet-save"
      :disabled="saving"
      @click="saveSettings"
    >
      {{ saving ? 'Saving…' : 'Save' }}
    </BaseButton>

    <button
      type="button"
      class="home-view__remove"
      @click="removeConfirmOpen = true"
    >Remove this frame</button>

    <a href="/logout" class="home-view__logout">Sign out of pictureFrame</a>
  </BaseBottomSheet>

  <Teleport to="body">
    <Transition name="home-view__remove-modal">
      <div
        v-if="removeConfirmOpen"
        class="home-view__remove-modal"
        role="alertdialog"
        aria-labelledby="remove-confirm-title"
        @click.self="removeConfirmOpen = false"
      >
        <div class="home-view__remove-modal-card">
          <p class="home-view__remove-confirm-title" id="remove-confirm-title">
            Remove this frame?
          </p>
          <p class="home-view__remove-confirm-body">
            Use this if you’re selling or giving away the frame. It deletes
            this frame from your account and unlinks it from your photos so
            the next owner can claim it fresh. This can’t be undone.
          </p>
          <p class="home-view__remove-confirm-aside">
            On the frame itself, the new owner can also do a factory reset
            by holding the small button on the back until the screen starts
            to flash. (If they only tap it briefly, the frame just refreshes
            its current image — keep holding until it flashes.)
          </p>
          <div class="home-view__remove-confirm-actions">
            <button
              type="button"
              class="home-view__remove-cancel"
              :disabled="removing"
              @click="removeConfirmOpen = false"
            >Cancel</button>
            <button
              type="button"
              class="home-view__remove-confirm-btn"
              :disabled="removing"
              @click="performRemove"
            >{{ removing ? 'Removing…' : 'Yes, remove' }}</button>
          </div>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useDevicesStore } from '@/stores/devices'
import { useUploadStore } from '@/stores/upload'
import { useDeviceMercure } from '@/composables/useDeviceMercure'
import type { Device } from '@/types'

// Sync interval for status comparisons. Devices configured with explicit wake
// times use a 24h window (the longest possible gap between slots); otherwise
// the rotation interval drives it.
function syncIntervalMs(device: Device): number {
  if (device.wakeTimes.length > 0) return 24 * 60 * 60 * 1000
  return device.rotationIntervalMinutes * 60_000
}

function deviceStatus(device: Device): 'ok' | 'sync-fail' | 'offline' {
  if (!device.lastSeenAt) return 'offline'
  const elapsed = Date.now() - new Date(device.lastSeenAt).getTime()
  const interval = syncIntervalMs(device)
  if (elapsed <= interval)     return 'ok'
  if (elapsed <= 2 * interval) return 'sync-fail'
  return 'offline'
}

function lastSyncLabel(device: Device): string | null {
  if (!device.lastSeenAt) return null
  const ago = Date.now() - new Date(device.lastSeenAt).getTime()
  if (ago < 60_000)        return 'just now'
  if (ago < 3_600_000)     return `${Math.round(ago / 60_000)}m ago`
  if (ago < 86_400_000)    return `${Math.round(ago / 3_600_000)}h ago`
  const days = Math.round(ago / 86_400_000)
  if (days === 1) return 'yesterday'
  return `${days} days ago`
}

function formatTime(minutes: number): string {
  const h24 = Math.floor(minutes / 60)
  const mm  = minutes % 60
  const p   = h24 >= 12 ? 'PM' : 'AM'
  let h     = h24 % 12
  if (h === 0) h = 12
  const mmStr = mm === 0 ? '' : `:${String(mm).padStart(2, '0')}`
  return `${h}${mmStr} ${p}`
}

function getMinuteOfDayInTz(date: Date, tz: string): number {
  const fmt = new Intl.DateTimeFormat('en-GB', {
    timeZone: tz,
    hour:   '2-digit',
    minute: '2-digit',
    hour12: false,
  })
  const parts = fmt.formatToParts(date)
  const h  = parseInt(parts.find(p => p.type === 'hour')?.value   ?? '0', 10)
  const mm = parseInt(parts.find(p => p.type === 'minute')?.value ?? '0', 10)
  return h * 60 + mm
}

// Find the next wake time after now (in tz). Returns null if `times` is empty.
function nextWakeMatch(times: number[], tz: string): { minutes: number; today: boolean } | null {
  if (times.length === 0) return null
  const nowMin = getMinuteOfDayInTz(new Date(), tz)
  let best: { minutes: number; today: boolean } | null = null
  let bestDelta = Infinity
  for (const m of times) {
    const delta = m > nowMin ? m - nowMin : (24 * 60) + (m - nowMin)
    if (delta < bestDelta) {
      bestDelta = delta
      best = { minutes: m, today: m > nowMin }
    }
  }
  return best
}

// Prefer the server-stamped nextPollExpectedAt — that's the schedule the
// device is *actually* on, set every poll. Falls back to a local computation
// for devices that haven't polled since the column was added.
function nextSyncLabel(device: Device): string | null {
  let nextMs: number | null = null
  if (device.nextPollExpectedAt) {
    nextMs = new Date(device.nextPollExpectedAt).getTime()
  } else if (device.wakeTimes.length > 0) {
    const next = nextWakeMatch(device.wakeTimes, device.timezone || 'UTC')
    if (!next) return null
    return `next sync ~${formatTime(next.minutes)} ${next.today ? 'today' : 'tomorrow'}`
  } else if (device.lastSeenAt) {
    nextMs = new Date(device.lastSeenAt).getTime() + device.rotationIntervalMinutes * 60_000
  } else {
    return null
  }

  const fromNow = nextMs - Date.now()
  if (fromNow <= 0) return null
  if (fromNow < 60_000)     return 'next sync in <1m'
  if (fromNow < 3_600_000)  return `next sync in ${Math.round(fromNow / 60_000)}m`
  if (fromNow < 86_400_000) {
    // Long horizons read better as a clock time than "in 14h".
    const tz = device.timezone || 'UTC'
    const minOfDay = getMinuteOfDayInTz(new Date(nextMs), tz)
    const dayDelta = daysFromTodayInTz(new Date(nextMs), tz)
    const dayLabel = dayDelta === 0 ? 'today'
                   : dayDelta === 1 ? 'tomorrow'
                   :                  `in ${dayDelta}d`
    return `next sync ~${formatTime(minOfDay)} ${dayLabel}`
  }
  return `next sync in ${Math.round(fromNow / 86_400_000)}d`
}

// Home shows what's actually on the frame right now — the last image the
// device pulled. Lock/queue state is intentionally ignored; the preview
// won't change until the frame next polls and switches to the locked image.
function previewUrl(device: Device): string | undefined {
  return device.currentImageId
    ? `/api/devices/${device.id}/preview?v=${device.currentImageId}`
    : undefined
}
import FrameCard from '@/components/FrameCard.vue'
import BaseBottomSheet from '@/components/BaseBottomSheet.vue'
import BaseButton from '@/components/BaseButton.vue'
import BaseInput from '@/components/BaseInput.vue'
import OrientationPicker from '@/components/OrientationPicker.vue'
import PullToRefresh from '@/components/PullToRefresh.vue'

const router       = useRouter()
const route        = useRoute()
const devicesStore = useDevicesStore()
const uploadStore  = useUploadStore()

// Live updates: server publishes a fresh device payload after every poll
// (and on PATCH/lock/unlock); the composable splats it into the store.
useDeviceMercure()

onMounted(async () => {
  await devicesStore.fetchDevices()
  document.addEventListener('visibilitychange', onVisibility)

  // First-time setup landing: SetupController redirects new users to
  // /?setup=<deviceId> after register/login, instead of a separate
  // Twig configure page. Auto-open the settings sheet for that device
  // so the user sees the same rich UI everyone else uses for live edits
  // — pre-populated, themed, with no duplicated form to maintain.
  const setupId = Number(route.query.setup)
  if (setupId) {
    onEdit(setupId)
    // Clean the query param off the URL so a refresh doesn't keep
    // re-opening the sheet, but keep the user on the home view.
    router.replace({ query: { ...route.query, setup: undefined } })
  }
})

onUnmounted(() => {
  document.removeEventListener('visibilitychange', onVisibility)
})

// Quietly re-fetch device state every time the PWA returns to the foreground,
// so the user doesn't have to pull-to-refresh just because a frame cycled
// while the app was backgrounded.
function onVisibility() {
  if (document.visibilityState === 'visible') {
    devicesStore.fetchDevices({ silent: true })
  }
}

// ── Pull-to-refresh ───────────────────────────────────────────────────────────

const stackEl = ref<HTMLElement | null>(null)

function isAtTop(): boolean {
  if (window.scrollY > 0) return false
  return (stackEl.value?.scrollTop ?? 0) === 0
}

async function refreshDevices() {
  await devicesStore.fetchDevices({ silent: true })
}

function onAddPhoto(deviceId: number) {
  // File picker must be triggered in the user-gesture context (the click handler)
  // before navigating, otherwise browsers block it as a popup.
  const input = document.createElement('input')
  input.type   = 'file'
  input.accept = 'image/jpeg,image/png,image/webp,image/gif'
  input.onchange = () => {
    const file = input.files?.[0]
    if (!file) return
    uploadStore.init(file, deviceId)
    router.push('/upload')
  }
  input.click()
}

// ── Settings sheet ────────────────────────────────────────────────────────────

const HOUR_OPTIONS   = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
const MINUTE_OPTIONS = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]

type FrequencyMode = 'times' | 'interval'
type AmPm = 'AM' | 'PM'

function hmpFromMinutes(m: number): { h: number; mm: number; p: AmPm } {
  const h24 = Math.floor(m / 60)
  const mm  = m % 60
  const p: AmPm = h24 >= 12 ? 'PM' : 'AM'
  let h = h24 % 12
  if (h === 0) h = 12
  return { h, mm, p }
}

function minutesFromHmp(h: number, mm: number, p: AmPm): number {
  let h24 = h % 12
  if (p === 'PM') h24 += 12
  return h24 * 60 + mm
}

const TIMEZONE_GROUPS = [
  { label: 'Americas', zones: [
    { value: 'America/New_York',    label: 'Eastern — New York, Toronto'       },
    { value: 'America/Chicago',     label: 'Central — Chicago, Mexico City'    },
    { value: 'America/Denver',      label: 'Mountain — Denver, Calgary'        },
    { value: 'America/Phoenix',     label: 'Mountain (no DST) — Phoenix'       },
    { value: 'America/Los_Angeles', label: 'Pacific — Los Angeles, Vancouver'  },
    { value: 'America/Anchorage',   label: 'Alaska — Anchorage'                },
    { value: 'Pacific/Honolulu',    label: 'Hawaii — Honolulu'                 },
    { value: 'America/Sao_Paulo',   label: 'Brasília — São Paulo'              },
    { value: 'America/Argentina/Buenos_Aires', label: 'Argentina — Buenos Aires' },
    { value: 'America/Bogota',      label: 'Colombia — Bogotá'                 },
  ]},
  { label: 'Europe', zones: [
    { value: 'Europe/London',    label: 'GMT/BST — London, Dublin'              },
    { value: 'Europe/Lisbon',    label: 'WET/WEST — Lisbon'                     },
    { value: 'Europe/Paris',     label: 'CET/CEST — Paris, Brussels, Amsterdam' },
    { value: 'Europe/Berlin',    label: 'CET/CEST — Berlin, Vienna, Zurich'     },
    { value: 'Europe/Stockholm', label: 'CET/CEST — Stockholm, Oslo, Copenhagen'},
    { value: 'Europe/Helsinki',  label: 'EET/EEST — Helsinki, Tallinn, Riga'    },
    { value: 'Europe/Warsaw',    label: 'CET/CEST — Warsaw, Prague, Budapest'   },
    { value: 'Europe/Rome',      label: 'CET/CEST — Rome, Madrid'               },
    { value: 'Europe/Athens',    label: 'EET/EEST — Athens, Bucharest'          },
    { value: 'Europe/Istanbul',  label: 'TRT — Istanbul'                        },
    { value: 'Europe/Moscow',    label: 'MSK — Moscow'                          },
  ]},
  { label: 'Asia & Pacific', zones: [
    { value: 'Asia/Dubai',        label: 'GST — Dubai, Abu Dhabi'               },
    { value: 'Asia/Karachi',      label: 'PKT — Karachi, Islamabad'             },
    { value: 'Asia/Kolkata',      label: 'IST — India'                          },
    { value: 'Asia/Dhaka',        label: 'BST — Dhaka, Bangladesh'              },
    { value: 'Asia/Bangkok',      label: 'ICT — Bangkok, Jakarta, Hanoi'        },
    { value: 'Asia/Singapore',    label: 'SGT — Singapore, Kuala Lumpur'        },
    { value: 'Asia/Shanghai',     label: 'CST — Beijing, Shanghai, Taipei'      },
    { value: 'Asia/Seoul',        label: 'KST — Seoul'                          },
    { value: 'Asia/Tokyo',        label: 'JST — Tokyo'                          },
    { value: 'Australia/Sydney',  label: 'AEDT/AEST — Sydney, Melbourne'        },
    { value: 'Australia/Brisbane',label: 'AEST (no DST) — Brisbane'             },
    { value: 'Australia/Perth',   label: 'AWST — Perth'                         },
    { value: 'Pacific/Auckland',  label: 'NZDT/NZST — Auckland'                 },
  ]},
  { label: 'Africa & Middle East', zones: [
    { value: 'Africa/Cairo',        label: 'EET — Cairo'               },
    { value: 'Africa/Nairobi',      label: 'EAT — Nairobi, East Africa'},
    { value: 'Africa/Johannesburg', label: 'SAST — Johannesburg'       },
    { value: 'Africa/Lagos',        label: 'WAT — Lagos, West Africa'  },
  ]},
  { label: 'UTC', zones: [
    { value: 'UTC', label: 'UTC — Coordinated Universal Time' },
  ]},
]

const sheetOpen = ref(false)
const saving = ref(false)
const removing = ref(false)
const removeConfirmOpen = ref(false)
const editingDevice = ref<Device | null>(null)
const editName = ref('')
const editOrientation = ref<Device['orientation']>('landscape')
const editFrequencyMode = ref<FrequencyMode>('interval')
const editWakeTimes = ref<number[]>([])
const editIntervalMinutes = ref<number>(60)
const editTimezone = ref('UTC')
const editRotationMode = ref<Device['rotationMode']>('oldest_upload')
const editPrioritizeNeverShown = ref<boolean>(false)

// Default candidates tried (in order) when adding a new time slot — picks the
// first one that isn't already in the list, so repeated +Add gives a sensible
// progression rather than stacking duplicates.
const DEFAULT_TIME_CANDIDATES = [
  9 * 60,        // 9:00 AM
  18 * 60,       // 6:00 PM
  12 * 60,       // 12:00 PM
  21 * 60,       // 9:00 PM
  6 * 60,        // 6:00 AM
  15 * 60,       // 3:00 PM
  7 * 60 + 30,   // 7:30 AM
  19 * 60 + 30,  // 7:30 PM
  0,             // 12:00 AM
]

// Appends to the end of the list — never sorts mid-edit. Sorting a row away
// from where the user just clicked is disorienting; they lose track of "the
// one I just added." Backend's setWakeTimes() sorts on save, so the persisted
// state stays canonical.
function addTime() {
  for (const c of DEFAULT_TIME_CANDIDATES) {
    if (!editWakeTimes.value.includes(c)) {
      editWakeTimes.value = [...editWakeTimes.value, c]
      return
    }
  }
  // Fallback: pick the next free 5-minute slot.
  for (let m = 0; m < 24 * 60; m += 5) {
    if (!editWakeTimes.value.includes(m)) {
      editWakeTimes.value = [...editWakeTimes.value, m]
      return
    }
  }
}

function removeTime(idx: number) {
  editWakeTimes.value = editWakeTimes.value.filter((_, i) => i !== idx)
}

function onTimePart(idx: number, part: 'h' | 'mm' | 'p', raw: string) {
  const cur = hmpFromMinutes(editWakeTimes.value[idx])
  const h  = part === 'h'  ? parseInt(raw, 10) : cur.h
  const mm = part === 'mm' ? parseInt(raw, 10) : cur.mm
  const p  = part === 'p'  ? (raw as AmPm)     : cur.p
  // Update in-place — don't reorder, don't dedupe. Reordering a row while
  // the user's mid-edit would yank their focus to the new position. Backend
  // setWakeTimes() sorts and dedupes on save.
  const arr = [...editWakeTimes.value]
  arr[idx] = minutesFromHmp(h, mm, p)
  editWakeTimes.value = arr
}

// "Next update" is when the device will *actually* next sync — and the new
// settings only reach the device on that sync, since it's currently asleep
// under whatever schedule was active at its last poll. So we compute this from
// the device's CURRENT (saved) settings, ignoring the in-progress edit state.
//
// Examples this gets right:
//   - Currently every 1 min, new = "at 4 AM" → "in ~1 min" (next existing
//     poll under current schedule, not 4 AM tomorrow).
//   - Currently "at 4 AM", new = "at 4 AM and 6 PM" → "~4 AM tomorrow"
//     (device is asleep until then; can't act on the 6 PM slot today because
//     it won't learn about it until the 4 AM check-in).
const nextUpdatePreview = computed<string>(() => {
  const device = editingDevice.value
  if (!device) return ''

  // The preview is about when the device will *next sync* — it does NOT
  // depend on the proposed new settings, only on the device's current saved
  // schedule. The "no update times yet" hint already lives in the time list.
  const tz = device.timezone || 'UTC'

  // Prefer the server-stamped expected-next-poll: that timestamp was set
  // under the schedule active at the device's last poll, and isn't disturbed
  // by the user's PATCHes — exactly what we want for "when will the new
  // settings reach the frame?"
  let nextPollMs: number
  if (device.nextPollExpectedAt) {
    nextPollMs = new Date(device.nextPollExpectedAt).getTime()
  } else if (!device.lastSeenAt) {
    return 'Next update: when the frame next connects'
  } else {
    // Legacy fallback for devices that haven't polled since the column was added.
    const lastSeen = new Date(device.lastSeenAt).getTime()
    nextPollMs = device.wakeTimes.length > 0
      ? nextWakeAfter(lastSeen, device.wakeTimes, tz)
      : lastSeen + device.rotationIntervalMinutes * 60_000
  }
  // Already-overdue device: it'll poll any moment now.
  if (nextPollMs < Date.now()) nextPollMs = Date.now()

  return formatNextUpdate(nextPollMs, tz)
})

// Next absolute timestamp (ms) at which any of `times` (minutes-of-day) occurs
// strictly AFTER `refMs` in `tz`. Approximates DST transitions away.
function nextWakeAfter(refMs: number, times: number[], tz: string): number {
  const refMin = getMinuteOfDayInTz(new Date(refMs), tz)
  let bestDelta = Infinity
  for (const m of times) {
    let delta = m - refMin
    if (delta <= 0) delta += 24 * 60
    if (delta < bestDelta) bestDelta = delta
  }
  return refMs + bestDelta * 60_000
}

// 0 = today, 1 = tomorrow, 2 = day after, ... in the given timezone.
function daysFromTodayInTz(date: Date, tz: string): number {
  const fmt = new Intl.DateTimeFormat('en-CA', {
    timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit',
  })
  const todayMs  = Date.parse(fmt.format(new Date()) + 'T00:00:00Z')
  const targetMs = Date.parse(fmt.format(date)        + 'T00:00:00Z')
  return Math.round((targetMs - todayMs) / 86_400_000)
}

function formatNextUpdate(tsMs: number, tz: string): string {
  const fromNow = tsMs - Date.now()
  if (fromNow <= 0)        return 'Next update: any moment'
  if (fromNow < 90_000)    return 'Next update: in <1 min'
  if (fromNow < 3_600_000) return `Next update: in ~${Math.round(fromNow / 60_000)} min`

  const minOfDay = getMinuteOfDayInTz(new Date(tsMs), tz)
  const dayDelta = daysFromTodayInTz(new Date(tsMs), tz)
  const dayLabel = dayDelta === 0 ? 'today'
                 : dayDelta === 1 ? 'tomorrow'
                 :                  `in ${dayDelta} days`
  return `Next update: ~${formatTime(minOfDay)} ${dayLabel}`
}

function onEdit(deviceId: number) {
  const device = devicesStore.devices.find(d => d.id === deviceId)
  if (!device) return
  editingDevice.value = device
  editName.value = device.name
  editOrientation.value = device.orientation
  editTimezone.value = device.timezone ?? 'UTC'
  editIntervalMinutes.value = device.rotationIntervalMinutes
  editWakeTimes.value = [...device.wakeTimes]
  editFrequencyMode.value = device.wakeTimes.length > 0 ? 'times' : 'interval'
  editRotationMode.value = device.rotationMode
  editPrioritizeNeverShown.value = device.prioritizeNeverShown
  removeConfirmOpen.value = false
  sheetOpen.value = true
}

async function performRemove() {
  if (!editingDevice.value) return
  removing.value = true
  try {
    await devicesStore.removeDevice(editingDevice.value.id)
    sheetOpen.value = false
    removeConfirmOpen.value = false
  } finally {
    removing.value = false
  }
}

async function saveSettings() {
  if (!editingDevice.value) return
  saving.value = true
  try {
    const patch: Parameters<typeof devicesStore.updateDevice>[1] = {
      name: editName.value.trim() || editingDevice.value.name,
      orientation: editOrientation.value,
      timezone: editTimezone.value,
      rotationMode: editRotationMode.value,
      prioritizeNeverShown: editPrioritizeNeverShown.value,
    }
    if (editFrequencyMode.value === 'times') {
      patch.wakeTimes = [...editWakeTimes.value]
    } else {
      patch.wakeTimes = []
      patch.rotationIntervalMinutes = Math.max(1, Math.min(1440, editIntervalMinutes.value || 1))
    }
    await devicesStore.updateDevice(editingDevice.value.id, patch)
    sheetOpen.value = false
  } finally {
    saving.value = false
  }
}
</script>

<style scoped lang="scss">
.home-view {
  flex: 1;
  padding: var(--space-4) var(--space-4) calc(var(--bottom-nav-height) + var(--space-4));
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  &__loading {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    padding: var(--space-4) 0;
    text-align: center;
  }

  &__empty {
    display: flex;
    justify-content: center;
    padding-top: var(--space-6);
  }

  &__empty-card {
    background: var(--color-surface);
    border: 2px dashed var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-6) var(--space-5);
    text-align: center;
    max-width: 320px;
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
  }

  &__empty-icon {
    color: var(--color-text-muted);
    opacity: 0.5;
  }

  &__empty-title {
    font-size: var(--text-md);
    font-weight: 700;
  }

  &__empty-sub {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    line-height: 1.5;
  }

  &__single {
    flex: 1;
    display: flex;
    flex-direction: column;
  }

  // Vertical scroll-snap stack of full-size cards. Each slide takes a full
  // screen-port height and snaps as the user scrolls, so flipping between
  // frames feels deliberate (like swiping pages) rather than continuous
  // scrolling through a feed.
  &__stack {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    overflow-y: auto;
    scroll-snap-type: y mandatory;
    scroll-behavior: smooth;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;

    &::-webkit-scrollbar { display: none; }
  }

  &__slide {
    flex: 0 0 auto;
    min-height: 100%;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }

  // Landscape phone: horizontal carousel of smaller cards. Vertical space is
  // tight (~240-280px usable) so we side-scroll instead of paging vertically,
  // and each card shrinks to ~320px wide so 2-3 are visible at a time.
  @media (orientation: landscape) and (max-height: 600px) {
    &__stack {
      flex-direction: row;
      overflow-x: auto;
      overflow-y: hidden;
      scroll-snap-type: x mandatory;
      gap: var(--space-3);
      margin: 0 calc(-1 * var(--space-4));
      padding: 0 var(--space-4);
    }

    &__slide {
      flex: 0 0 min(320px, 70vw);
      min-height: 0;
      height: 100%;
      scroll-snap-align: center;
    }
  }

  &__sheet-title {
    font-size: var(--text-md);
    font-weight: 700;
    margin-bottom: var(--space-4);
  }

  &__sheet-field {
    margin-bottom: var(--space-4);
  }

  &__sheet-label {
    display: block;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--color-text-muted);
    margin-bottom: var(--space-2);
  }

  &__mode-select,
  &__tz-select {
    width: 100%;
    min-height: var(--touch-min);
    padding: 0 var(--space-3);
    border: 1.5px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-family: inherit;
    cursor: pointer;
    appearance: auto;

    &:focus {
      outline: 2px solid var(--color-primary);
      outline-offset: 2px;
    }
  }

  &__tz-select {
    margin-top: var(--space-3);
  }

  &__times-mode,
  &__interval-mode {
    margin-top: var(--space-3);
  }

  &__times-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }

  &__times-empty {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    font-style: italic;
    padding: var(--space-2) 0;
  }

  &__time-row {
    display: flex;
    align-items: center;
    gap: var(--space-1);
  }

  &__time-part {
    min-height: var(--touch-min);
    padding: 0 var(--space-2);
    border: 1.5px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-family: inherit;
    cursor: pointer;
    appearance: auto;
    flex: 1 1 0;
    min-width: 0;

    &--ampm {
      flex: 0 0 auto;
      min-width: 64px;
    }

    &:focus {
      outline: 2px solid var(--color-primary);
      outline-offset: 2px;
    }
  }

  &__time-sep {
    font-weight: 700;
    color: var(--color-text-muted);
    padding: 0 2px;
  }

  &__time-remove {
    flex: 0 0 auto;
    width: var(--touch-min);
    height: var(--touch-min);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1.5px solid var(--color-border);
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-text-muted);
    cursor: pointer;
    transition: all var(--duration-fast);

    &:hover, &:focus-visible {
      border-color: var(--color-danger, #c0392b);
      color:        var(--color-danger, #c0392b);
      outline: none;
    }
  }

  &__time-add {
    margin-top: var(--space-3);
    width: 100%;
    min-height: var(--touch-min);
    border: 1.5px dashed var(--color-border);
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: 600;
    cursor: pointer;
    transition: all var(--duration-fast);

    &:hover, &:focus-visible {
      border-color: var(--color-primary);
      color:        var(--color-primary);
      outline: none;
    }
  }

  &__interval-input-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
  }

  &__interval-input {
    flex: 0 0 96px;
    min-height: var(--touch-min);
    padding: 0 var(--space-3);
    border: 1.5px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    // iOS Safari auto-zooms <input> when font-size < 16px. Use 16px+ to prevent
    // the page from zooming when the user taps the number field.
    font-size: 16px;
    font-family: inherit;
    text-align: center;

    &:focus {
      outline: 2px solid var(--color-primary);
      outline-offset: 2px;
    }
  }

  &__next-update {
    margin-top: var(--space-3);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--color-text);
  }

  &__rotation-checkbox {
    margin-top: var(--space-3);
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    cursor: pointer;
    min-height: var(--touch-min);

    input[type="checkbox"] {
      // Bigger than browser default so it remains a comfortable touch target.
      width: 22px;
      height: 22px;
      cursor: pointer;
      accent-color: var(--color-primary);
    }
  }

  &__propagation-note {
    margin-top: var(--space-1);
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    font-style: italic;
    line-height: 1.4;
  }

  &__sheet-save {
    width: 100%;
    margin-top: var(--space-2);
  }

  &__remove {
    width: 100%;
    margin-top: var(--space-5);
    padding: var(--space-2) var(--space-3);
    min-height: var(--touch-min);
    border: 1.5px solid transparent;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-danger, #c0392b);
    font-size: var(--text-sm);
    font-weight: 600;
    cursor: pointer;
    transition: all var(--duration-fast);

    &:hover, &:focus-visible {
      border-color: var(--color-danger, #c0392b);
      outline: none;
    }

    &:disabled {
      opacity: 0.6;
      cursor: not-allowed;
    }
  }

  &__remove-modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background: rgba(20, 14, 8, 0.55);
    backdrop-filter: blur(2px);
  }

  &__remove-modal-card {
    width: 100%;
    max-width: 360px;
    padding: var(--space-4);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
  }

  // Transition: card scales up subtly + fades; backdrop fades.
  &__remove-modal-enter-active,
  &__remove-modal-leave-active {
    transition: opacity var(--duration-fast) ease;
  }
  &__remove-modal-enter-active .home-view__remove-modal-card,
  &__remove-modal-leave-active .home-view__remove-modal-card {
    transition: transform var(--duration-fast) ease;
  }
  &__remove-modal-enter-from,
  &__remove-modal-leave-to { opacity: 0; }
  &__remove-modal-enter-from .home-view__remove-modal-card,
  &__remove-modal-leave-to   .home-view__remove-modal-card {
    transform: scale(0.96);
  }

  &__remove-confirm-title {
    font-size: var(--text-md);
    font-weight: 700;
    color: var(--color-danger, #c0392b);
    margin-bottom: var(--space-2);
  }

  &__remove-confirm-body {
    font-size: var(--text-sm);
    color: var(--color-text);
    line-height: 1.5;
    margin-bottom: var(--space-2);
  }

  &__remove-confirm-aside {
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    line-height: 1.5;
    margin-bottom: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--color-border);
  }

  &__remove-confirm-actions {
    display: flex;
    gap: var(--space-2);
  }

  &__remove-cancel,
  &__remove-confirm-btn {
    flex: 1 1 0;
    min-height: var(--touch-min);
    padding: 0 var(--space-3);
    border: 1.5px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 600;
    cursor: pointer;

    &:disabled { opacity: 0.6; cursor: not-allowed; }
  }

  &__remove-confirm-btn {
    background: var(--color-danger, #c0392b);
    border-color: var(--color-danger, #c0392b);
    color: #fff;
  }

  &__logout {
    display: block;
    margin-top: var(--space-3);
    text-align: center;
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    text-decoration: none;
    padding: var(--space-2);

    &:hover, &:focus-visible {
      color: var(--color-text);
      text-decoration: underline;
      outline: none;
    }
  }
}
</style>
