<script setup>
// The connected-service tab on a car: everything the manufacturer's own app
// knows about it, fetched live.
//
// Two things shape what you see below. The data is read under *this* user's
// account — so a car shared from someone else shows nothing here unless the
// vehicle is on this user's account too, and the panel says so rather than
// failing. And the sections are whatever the plugin exposes, rendered from the
// server's flattened key/value pairs plus the raw payload: nothing here knows a
// single upstream field name, so a provider adding a field surfaces it without a
// change to this file.
import { ref, computed, watch, onMounted } from "vue";
import { api } from "../api";
import { prefs } from "../prefs";
import { t } from "../i18n";
import { askConfirm } from "../lib/confirm.js";
import { formatDateTime, formatKm } from "../lib/format.js";

const props = defineProps({
  car: { type: Object, required: true },
  canWrite: { type: Boolean, default: false },
  // The provider's display name, already resolved by the parent for the tab
  // label. Passed in so the heading reads "MyToyota" from the first frame rather
  // than flashing the raw plugin name while the snapshot loads.
  providerLabel: { type: String, default: "" },
});
const emit = defineEmits(["car-updated"]);

const snap = ref(null);
const loading = ref(false);
const error = ref("");
const syncing = ref(false);

// Connect flow, for a car that has no link yet (a car added by hand, or one from
// before this feature existed).
const providers = ref([]);
const linkProvider = ref("");
const linkVehicles = ref([]);
const linkVehicleId = ref("");
const linkLoading = ref(false);
const linking = ref(false);

const linked = computed(() => !!props.car.provider);
const label = computed(
  () => snap.value?.label || props.providerLabel || currentProvider.value?.label || props.car.provider || ""
);
const currentProvider = computed(() => providers.value.find((p) => p.id === linkProvider.value) || null);
const connectable = computed(() => providers.value.filter((p) => p.connected));

// Sections that actually reported something come first; the empty and failed ones
// still render, below, so it is clear they were asked and what came back.
const sections = computed(() => snap.value?.sections || []);

async function load() {
  if (!linked.value) return;
  loading.value = true;
  error.value = "";
  try {
    snap.value = await api.getCarProvider(props.car.id);
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
}

async function loadProviders() {
  try {
    providers.value = await api.listVehicleProviders();
    linkProvider.value = connectable.value[0]?.id || "";
  } catch (e) {
    error.value = e.message;
  }
}

async function loadLinkVehicles() {
  linkVehicles.value = [];
  linkVehicleId.value = "";
  if (!linkProvider.value) return;
  linkLoading.value = true;
  try {
    const res = await api.listProviderVehicles(linkProvider.value);
    linkVehicles.value = res.vehicles || [];
    // Prefer the vehicle whose VIN matches the car — usually the only guess needed.
    const vin = (props.car.vin || "").trim().toUpperCase();
    const match = vin && linkVehicles.value.find((v) => (v.vin || "").toUpperCase() === vin);
    linkVehicleId.value = match?.id || linkVehicles.value[0]?.id || "";
  } catch (e) {
    error.value = e.message;
  } finally {
    linkLoading.value = false;
  }
}

watch(linkProvider, loadLinkVehicles);

async function connect() {
  if (!linkVehicleId.value) return;
  linking.value = true;
  error.value = "";
  try {
    const car = await api.linkCarProvider(props.car.id, {
      provider: linkProvider.value,
      vehicleId: linkVehicleId.value,
    });
    emit("car-updated", car);
    await load();
  } catch (e) {
    error.value = e.message;
  } finally {
    linking.value = false;
  }
}

async function disconnect() {
  if (!(await askConfirm(t("car.provider.unlinkConfirm", { label: label.value })))) return;
  error.value = "";
  try {
    const car = await api.linkCarProvider(props.car.id, { provider: "", vehicleId: "" });
    snap.value = null;
    emit("car-updated", car);
    await loadProviders();
    await loadLinkVehicles();
  } catch (e) {
    error.value = e.message;
  }
}

// Only the odometer is written back: it is the one provider reading the rest of
// the app computes from (service intervals, km-triggered reminders), and it is
// the one the user would otherwise retype after every drive.
async function applyOdometer() {
  syncing.value = true;
  error.value = "";
  try {
    const res = await api.syncCarProvider(props.car.id, {
      include: { identity: false, fuelType: false, dates: false, odometer: true },
    });
    emit("car-updated", res.car);
    await load();
  } catch (e) {
    error.value = e.message;
  } finally {
    syncing.value = false;
  }
}

// --- Collapsing the cards ---
//
// Every card below the headline readings folds away, so a long provider dump
// (Toyota reports ten sections) can be trimmed to the two or three worth
// watching. Which ones are folded is remembered in localStorage rather than on
// the profile: it is a per-device reading habit, not an account setting, and it
// should survive leaving the tab without a round trip. Keyed by section id, so
// collapsing "Notifications" keeps it collapsed on every car.
const COLLAPSED_KEY = "cc_provider_collapsed";

const collapsed = ref(readCollapsed());

function readCollapsed() {
  try {
    const raw = JSON.parse(localStorage.getItem(COLLAPSED_KEY) || "[]");
    return Array.isArray(raw) ? raw.filter((id) => typeof id === "string") : [];
  } catch {
    return []; // unreadable (hand-edited, or written by an older version)
  }
}

function isOpen(id) {
  return !collapsed.value.includes(id);
}

function toggleCard(id) {
  collapsed.value = isOpen(id)
    ? [...collapsed.value, id]
    : collapsed.value.filter((k) => k !== id);
  try {
    localStorage.setItem(COLLAPSED_KEY, JSON.stringify(collapsed.value));
  } catch {
    // A full or blocked store just means the choice lasts this visit only.
  }
}

// --- Arranging the headline readings ---
//
// The readings drag into any order, saved on drop. Stored on the car, like the
// arrangement of the Information rows, rather than per device the way the
// collapsed cards above are: it is a layout choice everyone the car is shared
// with sees, not a reading habit of this browser.
//
// Only the readings the provider actually reported can be arranged, so the
// stored list is a subset; one that turns up later — an EV range on a car that
// was parked unplugged — follows the arranged ones.
const metrics = ref([]);
watch(
  [() => snap.value?.metrics, () => props.car.metricOrder],
  ([reported, order]) => {
    const rank = new Map((order || []).map((key, i) => [key, i]));
    const list = [...(reported || [])];
    list.sort((a, b) => {
      const ra = rank.has(a.key) ? rank.get(a.key) : Infinity;
      const rb = rank.has(b.key) ? rank.get(b.key) : Infinity;
      return ra - rb; // equal ranks (both unarranged) keep the reported order
    });
    metrics.value = list;
  },
  { immediate: true }
);

// Same native drag events as the garage and the Information rows, so also
// pointer-only, and it needs write access like every other choice on the car.
// The rail's lock holds these still too — it covers every arrangement at once,
// which is the point of putting it in the rail rather than on each page.
const canArrangeMetrics = computed(
  () => props.canWrite && !prefs.dragLocked && metrics.value.length > 1
);
const dragMetric = ref(""); // reading being dragged
const dropMetric = ref(""); // reading it is currently hovering over
let metricsMoved = false; // the row changed during this drag and isn't saved yet

function onMetricDragStart(key, e) {
  dragMetric.value = key;
  metricsMoved = false;
  e.dataTransfer.effectAllowed = "move";
  // Firefox only starts a drag once something is on the transfer.
  e.dataTransfer.setData("text/plain", key);
}

// Reorder live as the pointer crosses readings, so the row shows the
// arrangement you are about to get. dragenter fires again for every child
// element inside the same reading, so only a genuinely new one moves anything.
function onMetricDragEnter(key) {
  if (!dragMetric.value || key === dragMetric.value || dropMetric.value === key) return;
  dropMetric.value = key;
  const list = metrics.value;
  const from = list.findIndex((m) => m.key === dragMetric.value);
  const to = list.findIndex((m) => m.key === key);
  if (from < 0 || to < 0) return;
  list.splice(to, 0, ...list.splice(from, 1));
  metricsMoved = true;
}

// Called from both drop and dragend: a reading released in the gap between two
// of them never produces a drop, and leaving that arrangement unsaved would
// quietly undo itself on the next load.
async function commitMetricOrder() {
  dragMetric.value = "";
  dropMetric.value = "";
  if (!metricsMoved) return;
  metricsMoved = false;
  error.value = "";
  try {
    const car = await api.updateCarView(props.car.id, {
      metricOrder: metrics.value.map((m) => m.key),
    });
    // Hand the car back up so the page holds one copy of it — the watch above
    // then rebuilds this row from what was actually stored.
    emit("car-updated", car);
  } catch (e) {
    // The arrangement didn't stick; say so and refetch rather than leaving the
    // panel showing an order the server doesn't have.
    error.value = e.message;
    await load();
  }
}

function metricLabel(key) {
  return t(`car.provider.metrics.${key}`);
}
function sectionLabel(id) {
  return t(`car.provider.sections.${id}`);
}
function prettyJSON(raw) {
  try {
    return JSON.stringify(raw, null, 2);
  } catch {
    return String(raw);
  }
}

onMounted(async () => {
  if (linked.value) {
    await load();
  } else {
    await loadProviders();
    await loadLinkVehicles();
  }
});
</script>

<template>
  <section>
    <p v-if="error" class="mb-4 rounded-control bg-danger-soft px-4 py-3 text-sm font-medium text-danger">{{ error }}</p>

    <!-- ── Linked: the live snapshot ─────────────────────────────────────── -->
    <template v-if="linked">
      <div class="mb-3 flex flex-wrap items-center justify-between gap-3">
        <div>
          <h2 class="text-lg font-bold tracking-[-0.02em] text-strong">{{ label }}</h2>
          <p class="text-sm text-muted">{{ t("car.provider.subtitle", { label }) }}</p>
        </div>
        <div class="flex items-center gap-2">
          <button class="dh-btn dh-btn-ghost !px-3 !py-1.5" :disabled="loading" @click="load">
            {{ loading ? t("car.provider.refreshing") : t("car.provider.refresh") }}
          </button>
          <button v-if="canWrite" class="dh-btn dh-btn-ghost !px-3 !py-1.5 text-danger" @click="disconnect">
            {{ t("car.provider.unlink") }}
          </button>
        </div>
      </div>

      <p v-if="loading && !snap" class="text-muted">{{ t("common.loading") }}</p>

      <template v-else-if="snap">
        <!-- The provider can't be reached for this car: say why, don't fail. -->
        <div v-if="snap.unavailable" class="rounded-card border border-dashed border-default p-8 text-center">
          <p class="text-sm text-warning">{{ snap.detail }}</p>
          <p class="mt-2 text-xs text-muted">{{ t("car.provider.own") }}</p>
        </div>

        <template v-else>
          <!-- Headline readings. They drag into any order with write access;
               the arrangement belongs to the car. -->
          <div v-if="metrics.length" class="dh-card mb-4 p-6">
            <p class="eyebrow mb-3">{{ t("car.provider.readings") }}</p>
            <dl class="grid grid-cols-2 gap-4 text-sm sm:grid-cols-4">
              <div
                v-for="m in metrics"
                :key="m.key"
                :draggable="canArrangeMetrics"
                :title="canArrangeMetrics ? t('car.provider.dragHint') : ''"
                class="-m-2 rounded-control p-2 transition-shadow duration-150"
                :class="[
                  canArrangeMetrics ? 'cursor-grab hover:bg-sunken active:cursor-grabbing' : '',
                  dragMetric === m.key ? 'opacity-50' : '',
                  dropMetric === m.key ? 'ring-2 ring-accent' : '',
                ]"
                @dragstart="onMetricDragStart(m.key, $event)"
                @dragenter.prevent="onMetricDragEnter(m.key)"
                @dragover.prevent
                @drop.prevent="commitMetricOrder"
                @dragend="commitMetricOrder"
              >
                <dt class="eyebrow">{{ metricLabel(m.key) }}</dt>
                <dd class="mt-0.5 data text-lg font-bold text-strong">
                  {{ m.value }}<span v-if="m.unit" class="ml-1 text-sm font-medium text-muted">{{ m.unit }}</span>
                </dd>
              </div>
            </dl>
            <p v-if="canArrangeMetrics" class="mt-3 text-xs text-muted">{{ t("car.provider.dragHint") }}</p>
          </div>

          <!-- The provider's odometer is ahead of the stored one: offer to take it. -->
          <div
            v-if="snap.suggestedCurrentKm && canWrite"
            class="mb-4 flex flex-wrap items-center justify-between gap-3 rounded-control bg-sunken px-4 py-3">
            <p class="text-sm text-body">
              {{ t("car.provider.odometerSuggest", { label, km: formatKm(snap.suggestedCurrentKm) }) }}
            </p>
            <button class="dh-btn dh-btn-primary !px-3 !py-1.5" :disabled="syncing" @click="applyOdometer">
              {{ syncing ? t("car.provider.updatingOdometer") : t("car.provider.updateOdometer") }}
            </button>
          </div>

          <!-- The vehicle record itself -->
          <div v-if="snap.vehicle" class="dh-card mb-4 p-6">
            <button
              type="button"
              class="flex w-full items-center justify-between gap-3 text-left"
              :aria-expanded="isOpen('vehicle')"
              @click="toggleCard('vehicle')"
            >
              <p class="eyebrow">{{ t("car.provider.vehicle") }}</p>
              <svg
                viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                class="h-4 w-4 shrink-0 text-muted transition-transform" :class="isOpen('vehicle') ? '' : '-rotate-90'"
              ><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" /></svg>
            </button>

            <div v-show="isOpen('vehicle')">
            <div class="flex flex-wrap items-start justify-between gap-4">
              <div>
                <p class="mt-0.5 font-bold text-strong">{{ snap.vehicle.name }}</p>
                <p class="text-sm text-muted">
                  {{ [snap.vehicle.make, snap.vehicle.model, snap.vehicle.year || ''].filter(Boolean).join(' ') }}
                </p>
                <p v-if="snap.vehicle.vin" class="data mt-0.5 text-xs text-muted">{{ snap.vehicle.vin }}</p>
              </div>
              <img
                v-if="snap.vehicle.imageUrl"
                :src="snap.vehicle.imageUrl"
                alt=""
                class="h-20 max-w-full rounded-control object-contain"
              />
            </div>
            <details v-if="snap.vehicle.fields?.length" class="mt-4">
              <summary class="cursor-pointer text-xs font-semibold text-brandtext hover:underline">
                {{ t("car.provider.allFields") }}
              </summary>
              <dl class="mt-3 divide-y divide-subtle text-sm">
                <div v-for="f in snap.vehicle.fields" :key="f.key" class="flex gap-4 py-1.5">
                  <dt class="data w-1/2 shrink-0 break-all text-xs text-muted">{{ f.key }}</dt>
                  <dd class="min-w-0 break-words text-body">{{ f.value }}</dd>
                </div>
              </dl>
            </details>
            </div>
          </div>

          <!-- One card per capability the plugin exposes -->
          <div class="space-y-4">
            <div v-for="sec in sections" :key="sec.id" class="dh-card p-6">
              <button
                type="button"
                class="flex w-full items-center justify-between gap-3 text-left"
                :aria-expanded="isOpen(sec.id)"
                @click="toggleCard(sec.id)"
              >
                <h3 class="font-semibold text-strong">{{ sectionLabel(sec.id) }}</h3>
                <div class="flex shrink-0 items-center gap-2">
                  <!-- A failed section says so in the collapsed header, but only
                       as a badge: the provider's own message can run to hundreds
                       of characters, which belongs in the body with the rest of
                       the detail rather than stretching the card. -->
                  <span v-if="sec.status === 'error'" class="dh-badge dh-badge-warning">
                    {{ t("car.provider.sectionFailed") }}
                  </span>
                  <svg
                    viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                    class="h-4 w-4 shrink-0 text-muted transition-transform" :class="isOpen(sec.id) ? '' : '-rotate-90'"
                  ><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" /></svg>
                </div>
              </button>

              <div v-show="isOpen(sec.id)" class="mt-3">
              <p
                v-if="sec.status === 'error'"
                class="overflow-x-auto whitespace-pre-wrap break-words rounded-control bg-warning-soft px-3 py-2 text-sm text-warning"
              >{{ sec.error }}</p>

              <p v-else-if="sec.status === 'empty'" class="text-sm text-muted">{{ t("car.provider.sectionEmpty") }}</p>

              <template v-else-if="sec.status === 'ok'">
                <dl class="divide-y divide-subtle text-sm">
                  <div v-for="f in sec.fields" :key="f.key" class="flex gap-4 py-1.5">
                    <dt class="data w-1/2 shrink-0 break-all text-xs text-muted">{{ f.key }}</dt>
                    <dd class="min-w-0 break-words text-body">{{ f.value }}</dd>
                  </div>
                </dl>
                <p v-if="sec.truncated" class="mt-2 text-xs text-warning">
                  {{ t("car.provider.truncated", { n: sec.fields.length }) }}
                </p>
                <details v-if="sec.raw" class="mt-3">
                  <summary class="cursor-pointer text-xs font-semibold text-brandtext hover:underline">
                    {{ t("car.provider.raw") }}
                  </summary>
                  <pre class="mt-2 max-h-96 overflow-auto rounded-control bg-sunken p-3 text-xs text-body">{{ prettyJSON(sec.raw) }}</pre>
                </details>
              </template>
              </div>
            </div>
          </div>

          <p class="mt-4 text-xs text-muted">
            {{ t("car.provider.updated", { time: formatDateTime(snap.fetchedAt) }) }} · {{ t("car.provider.own") }}
          </p>
        </template>
      </template>
    </template>

    <!-- ── Not linked: offer to connect this car to a vehicle ────────────── -->
    <template v-else>
      <div class="dh-card p-6">
        <h2 class="text-lg font-bold tracking-[-0.02em] text-strong">{{ t("car.provider.connectTitle") }}</h2>

        <p v-if="connectable.length === 0" class="mt-2 text-sm text-muted">{{ t("car.provider.noProviders") }}</p>
        <RouterLink
          v-if="connectable.length === 0"
          to="/settings"
          class="mt-3 inline-block text-sm font-medium text-brandtext hover:underline">
          {{ t("car.provider.settingsLink") }}
        </RouterLink>

        <template v-else>
          <p class="mt-1 text-sm text-muted">{{ t("car.provider.connectHint") }}</p>

          <div class="mt-4 grid gap-3 sm:grid-cols-2">
            <div v-if="connectable.length > 1">
              <label class="dh-label">{{ t("forms.import.service") }}</label>
              <select v-model="linkProvider" class="dh-input">
                <option v-for="p in connectable" :key="p.id" :value="p.id">{{ p.label }}</option>
              </select>
            </div>
            <div>
              <label class="dh-label">{{ t("forms.import.selectVehicle") }}</label>
              <p v-if="linkLoading" class="text-sm text-muted">{{ t("forms.import.loadingVehicles") }}</p>
              <p v-else-if="linkVehicles.length === 0" class="text-sm text-muted">{{ t("forms.import.noVehicles") }}</p>
              <select v-else v-model="linkVehicleId" class="dh-input">
                <option v-for="v in linkVehicles" :key="v.id" :value="v.id">
                  {{ v.name }}{{ v.vin ? " · " + v.vin : "" }}
                </option>
              </select>
            </div>
          </div>

          <div class="mt-4 flex justify-end">
            <button
              v-if="canWrite"
              class="dh-btn dh-btn-primary"
              :disabled="!linkVehicleId || linking"
              @click="connect">
              {{ linking ? t("car.provider.connecting") : t("car.provider.connectSubmit") }}
            </button>
          </div>
        </template>
      </div>
    </template>
  </section>
</template>
