<script setup lang="ts">
/**
 * The staleness sweep for NOTES: notes that assert a fact, oldest first.
 *
 * Sibling of RuleSweepPane, not a shared component — the two read differently
 * enough that merging them would mean a prop for every difference (a rule has
 * a tier and a statement; a note has a project and opens at a route). What
 * they share is the SHAPE of the judgement, and that is worth copying
 * deliberately rather than abstracting: the ordering carries urgency, "never"
 * is categorically different from a date, and a failed check writes nothing.
 *
 * Lives in the Knowledge view rather than beside the rules sweep (operator's
 * call, milestone 317 step 4): notes stay where notes live. The cost, accepted
 * knowingly, is that there is no single screen showing every record anyone has
 * left unconfirmed — /rules keeps its own.
 */
import { onMounted, ref } from "vue";
import { apiGet, apiPost } from "@/api/client";
import { useToastStore } from "@/stores/toast";

interface DueNote {
  id: number;
  title: string;
  project_id: number | null;
  verify_with: string;
  expires_when: string;
  last_verified: string | null;
  days_since_verified: number | null;
}

const emit = defineEmits<{ "open-note": [id: number] }>();

const toast = useToastStore();
const rows = ref<DueNote[]>([]);
const loading = ref(false);
const neverOnly = ref(false);
const busyId = ref<number | null>(null);

async function reload() {
  loading.value = true;
  try {
    const p = new URLSearchParams();
    if (neverOnly.value) p.set("never_only", "1");
    const data = await apiGet<{ notes: DueNote[] }>(
      `/api/notes/due-for-verification?${p}`,
    );
    rows.value = data.notes;
  } catch {
    toast.show("Could not load the sweep", "error");
  } finally {
    loading.value = false;
  }
}

async function verify(id: number, stillTrue: boolean) {
  busyId.value = id;
  try {
    await apiPost(`/api/notes/${id}/verify`, { still_true: stillTrue });
    if (stillTrue) {
      // It has been confirmed, so it leaves the list — the sweep shows what
      // still needs looking at, and leaving it in place would invite a second
      // stamp nobody earned.
      rows.value = rows.value.filter((r) => r.id !== id);
      toast.show("Recorded — checked today");
    } else {
      // It stays. A failed check writes nothing on purpose: the note is wrong
      // rather than in a state worth recording, so it keeps its place until
      // someone corrects, supersedes, or unhooks it.
      toast.show("Recorded as no longer true — the note keeps its place here");
    }
  } catch {
    toast.show("Could not record that", "error");
  } finally {
    busyId.value = null;
  }
}

onMounted(reload);
defineExpose({ reload });
</script>

<template>
  <section class="sweep">
    <header>
      <h2>Due for verification</h2>
      <p class="lede">
        Notes that assert a fact about something outside your control — what a
        service does, how a tool behaves. Most notes are decisions and never
        appear here; they have no truth value to go stale.
      </p>
    </header>

    <div class="filters">
      <label class="filter">
        <input v-model="neverOnly" type="checkbox" @change="reload" />
        <span>Never checked only</span>
      </label>
    </div>

    <p v-if="loading" class="state">Loading…</p>

    <!-- An empty sweep is GOOD NEWS and must not read like a broken page. -->
    <p v-else-if="!rows.length" class="state empty">
      Nothing to check.
      {{ neverOnly
        ? "Every note that carries a check has been confirmed at least once."
        : "No note carries a check yet — add one to a note that asserts a fact." }}
    </p>

    <ol v-else class="rows">
      <li v-for="n in rows" :key="n.id" class="row">
        <div class="row-head">
          <button class="row-title" @click="emit('open-note', n.id)">{{ n.title }}</button>
          <span class="age" :class="{ unchecked: n.days_since_verified === null }">
            {{ n.days_since_verified === null
              ? "never checked"
              : `${n.days_since_verified}d ago` }}
          </span>
        </div>

        <dl class="check">
          <dt>Check</dt>
          <dd>{{ n.verify_with }}</dd>
          <template v-if="n.expires_when">
            <dt>Ends when</dt>
            <dd>{{ n.expires_when }}</dd>
          </template>
        </dl>

        <div class="actions">
          <button :disabled="busyId === n.id" @click="verify(n.id, true)">Still true</button>
          <button :disabled="busyId === n.id" @click="verify(n.id, false)">No longer true</button>
        </div>
      </li>
    </ol>

    <p v-if="rows.length" class="footnote">
      Record a result only after actually running the check. “No longer true” stores nothing
      on purpose — the note is wrong rather than in a state worth recording, so it keeps its
      place here until you correct it, supersede it, or remove its check.
    </p>
  </section>
</template>

<style scoped>
.sweep { display: flex; flex-direction: column; gap: var(--fs-space-3); }
h2 { margin: 0; font-size: 1.05rem; }
.lede {
  margin: 0.35rem 0 0;
  max-width: 62ch;
  font-size: 0.85rem;
  color: var(--fs-text-secondary);
  line-height: 1.5;
}

.filters { display: flex; gap: var(--fs-space-5); align-items: center; flex-wrap: wrap; }
.filter { display: flex; align-items: center; gap: var(--fs-space-2); font-size: 0.82rem; color: var(--fs-text-secondary); }
.filter input[type="checkbox"] { accent-color: var(--fs-accent); }

.state { margin: 0; font-size: 0.9rem; color: var(--fs-text-secondary); }
.state.empty { color: var(--fs-text-tertiary); }

.rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--fs-space-3); }
.row {
  background: var(--fs-surface-raised);
  border-radius: var(--fs-radius-md);
  padding: var(--fs-space-3);
}
.row-head { display: flex; align-items: baseline; gap: var(--fs-space-2); flex-wrap: wrap; }
.row-title {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: Fraunces, serif; font-style: italic; font-size: 1.02rem;
  color: var(--fs-text-primary); text-align: left;
}
.row-title:hover { text-decoration: underline; }
/* The ORDER carries urgency — the top of this list is the least-confirmed
   thing in the corpus. No red/amber ramp: it would restate the ordering and
   force an invented "stale after N days" threshold. "Never" is marked because
   it is categorically DIFFERENT from a date, not a worse one. */
.age { margin-left: auto; font-size: 0.78rem; color: var(--fs-text-secondary); font-variant-numeric: tabular-nums; }
.age.unchecked { font-style: italic; color: var(--fs-text-tertiary); }

.check { display: grid; grid-template-columns: auto 1fr; gap: 0.15rem var(--fs-space-3); margin: var(--fs-space-3) 0 0; }
.check dt { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fs-text-tertiary); }
.check dd { margin: 0; font-size: 0.82rem; color: var(--fs-text-primary); min-width: 0; overflow-wrap: anywhere; }

.actions { display: flex; gap: var(--fs-space-2); margin-top: var(--fs-space-3); }
.actions button {
  cursor: pointer; font: inherit; font-size: 0.78rem;
  background: var(--fs-surface-page); color: var(--fs-text-primary);
  border: 1px solid var(--fs-border-color); border-radius: var(--fs-radius-md);
  padding: 0.25rem 0.6rem;
}
.actions button:hover:not(:disabled) { background: var(--fs-surface-hover); }
.actions button:disabled { opacity: var(--fs-disabled-opacity); cursor: default; }

.footnote { margin: 0; max-width: 62ch; font-size: 0.78rem; color: var(--fs-text-tertiary); line-height: 1.45; }
</style>
