<script setup lang="ts">
/**
 * What a rule USED TO SAY — inside the slide-over, where a rule is read in
 * full. Not on the list row: a history entry point there would compete with
 * the row's actual job.
 *
 * A SIBLING OF HistoryPanel.vue, NOT A REUSE OF IT, and the reason is in its
 * props: `noteId` + `currentBody`, a `NoteVersion` carrying tags and pin
 * columns, a fetch of /api/notes/…, a `restore` emit, and pin/unpin buttons.
 * Every one of those is note-shaped. Rules have no tags, no pins, and
 * deliberately no restore, and a rule's text is EIGHT fields rather than one
 * body — which changes the central question from "what changed" to "which
 * fields moved".
 *
 * What was genuinely shared is shared: DiffView.vue takes DiffLine[] and
 * nothing note-shaped, and the LCS walk now lives in utils/diff.ts, which
 * this file uses rather than copying a fourth time (#3207).
 */
import { computed, onMounted, ref, watch } from "vue";
import DiffView from "@/components/DiffView.vue";
import { computeDiff } from "@/utils/diff";
import {
  listRuleVersions, getRuleVersion, type Rule, type RuleVersion,
} from "@/api/rulebooks";
import { useToastStore } from "@/stores/toast";

const props = defineProps<{ ruleId: number; current: Rule | null }>();

const toast = useToastStore();
const versions = ref<RuleVersion[]>([]);
const selected = ref<RuleVersion | null>(null);
const expanded = ref(false);
const loading = ref(false);
const loadingDetail = ref(false);

// The eight TEXT fields a version carries, in the order the editor shows
// them. Narrowed to its own type rather than `keyof RuleVersion`, which would
// also admit id/rule_id/user_id/created_at — none of which is text a reader
// compares, and all of which would widen every lookup below to `number`.
// Labels rather than column names: a reader is deciding whether to open a
// row, and "How to apply" reads where "how_to_apply" has to be decoded.
type TextField =
  | "title" | "statement" | "when_to_apply" | "tier"
  | "why" | "how_to_apply" | "verify_with" | "expires_when";

const FIELDS: Array<[TextField, string]> = [
  ["title", "Title"],
  ["statement", "Statement"],
  ["when_to_apply", "When to apply"],
  ["tier", "Tier"],
  ["why", "Why"],
  ["how_to_apply", "How to apply"],
  ["verify_with", "Check"],
  ["expires_when", "Ends when"],
];

/**
 * Which fields this edit moved.
 *
 * A version holds the text the edit REPLACED, so the edit is the step from
 * this row to the NEXT NEWER state — the version above it in the list, or,
 * for the newest row, the rule as it stands now. Comparing against the row
 * below instead would attribute every change to the wrong edit.
 */
function changedFields(index: number): string[] {
  const before = versions.value[index];
  // `Rule` carries all eight as required strings; a RuleVersion carries them
  // only once opened, which is what the undefined check below is about.
  const after: Pick<Rule, TextField> | RuleVersion | null =
    index === 0 ? props.current : versions.value[index - 1] ?? null;
  if (!before || !after) return [];
  return FIELDS
    .filter(([key]) => {
      // A listing row carries only the title; the rest arrive when opened.
      // Undefined means NOT LOADED, which is not the same as unchanged — so a
      // field nobody has fetched is claimed as neither.
      const a = before[key];
      const b = after[key];
      if (a === undefined || b === undefined) return false;
      return (a ?? "") !== (b ?? "");
    })
    .map(([, label]) => label);
}

/** True when this edit rewrote or removed the rule's check.
 *
 * Worth its own marker because editing `verify_with` silently drops
 * `verified_at` (milestone 312) — the moment a rule re-entered the staleness
 * sweep. That happens nowhere a reader can see it, and this row is the only
 * surface that can say when it happened. */
function checkChanged(index: number): boolean {
  return changedFields(index).includes("Check");
}

const diff = computed(() => {
  if (!selected.value || selected.value.statement === undefined) return [];
  const now = props.current?.statement ?? "";
  return computeDiff(now, selected.value.statement);
});

function stamp(iso: string): string {
  return iso.slice(0, 10);
}

async function load() {
  loading.value = true;
  try {
    versions.value = await listRuleVersions(props.ruleId);
  } catch {
    toast.show("Could not load this rule's history", "error");
  } finally {
    loading.value = false;
  }
}

async function open(v: RuleVersion) {
  if (selected.value?.id === v.id) {
    selected.value = null;
    return;
  }
  loadingDetail.value = true;
  try {
    const full = await getRuleVersion(props.ruleId, v.id);
    // Merged back into the list so `changedFields` can compare against real
    // text once a neighbour has been opened, instead of staying blind.
    const at = versions.value.findIndex((x) => x.id === v.id);
    if (at >= 0) versions.value[at] = { ...versions.value[at], ...full };
    selected.value = versions.value[at] ?? full;
  } catch {
    toast.show("Could not open that version", "error");
  } finally {
    loadingDetail.value = false;
  }
}

onMounted(load);
watch(() => props.ruleId, () => { selected.value = null; load(); });
</script>

<template>
  <section class="history">
    <button class="toggle" :aria-expanded="expanded" @click="expanded = !expanded">
      <span>Edit history</span>
      <span class="count">{{ versions.length || "none" }}</span>
    </button>

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

      <!-- Never reworded is the ordinary case, and must not read as a fault. -->
      <p v-else-if="!versions.length" class="state empty">
        This rule has never been reworded. Nothing was recorded before the history
        existed, so an older rule starts empty too.
      </p>

      <template v-else>
        <p class="lede">
          Each entry is what the rule said <em>before</em> that edit. The wording it
          was changed to is the rule as it stands above.
        </p>
        <ol class="rows">
          <li v-for="(v, i) in versions" :key="v.id" class="row">
            <button
              class="row-head"
              :class="{ open: selected?.id === v.id }"
              @click="open(v)"
            >
              <span class="when">{{ stamp(v.created_at) }}</span>
              <span class="fields">
                {{ changedFields(i).join(", ") || "opened to compare" }}
              </span>
              <span v-if="checkChanged(i)" class="check-moved">check reset</span>
            </button>

            <div v-if="selected?.id === v.id" class="detail">
              <p v-if="loadingDetail" class="state">Loading…</p>
              <template v-else>
                <p v-if="checkChanged(i)" class="warn">
                  This edit changed the rule's check, which cleared its verification
                  stamp — the rule went back to the top of the staleness sweep here.
                </p>
                <dl class="fields-list">
                  <template v-for="[key, label] in FIELDS" :key="key">
                    <template v-if="key !== 'statement' && v[key]">
                      <dt>{{ label }}</dt>
                      <dd>{{ v[key] }}</dd>
                    </template>
                  </template>
                </dl>
                <h4>Statement</h4>
                <DiffView v-if="diff.length" :diff="diff" />
                <p v-else class="state">The statement did not change in this edit.</p>
              </template>
            </div>
          </li>
        </ol>
      </template>
    </div>
  </section>
</template>

<style scoped>
.history { border-top: 1px solid var(--fs-border-color); padding-top: var(--fs-space-3); }

.toggle {
  display: flex; align-items: center; gap: var(--fs-space-2); width: 100%;
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; font-size: var(--fs-size-body-sm); color: var(--fs-text-secondary);
}
.toggle:hover { color: var(--fs-text-primary); }
.count {
  margin-left: auto; font-size: var(--fs-size-tiny); color: var(--fs-text-tertiary);
  font-variant-numeric: tabular-nums;
}

.body { margin-top: var(--fs-space-3); display: flex; flex-direction: column; gap: var(--fs-space-3); }
.state { margin: 0; font-size: var(--fs-size-body-sm); color: var(--fs-text-secondary); }
.state.empty { color: var(--fs-text-tertiary); }
.lede {
  margin: 0; max-width: 62ch; font-size: var(--fs-size-tiny);
  color: var(--fs-text-tertiary); line-height: var(--fs-leading-body);
}

.rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--fs-space-2); }
.row { background: var(--fs-surface-raised); border-radius: var(--fs-radius-md); }

.row-head {
  display: flex; align-items: baseline; gap: var(--fs-space-3); width: 100%;
  background: none; border: none; cursor: pointer; text-align: left;
  padding: var(--fs-space-2) var(--fs-space-3);
  font: inherit; font-size: var(--fs-size-body-sm); color: var(--fs-text-primary);
}
.row-head:hover { background: var(--fs-surface-hover); border-radius: var(--fs-radius-md); }
.when {
  font-variant-numeric: tabular-nums; color: var(--fs-text-secondary);
  font-size: var(--fs-size-tiny);
}
.fields { color: var(--fs-text-primary); min-width: 0; overflow-wrap: anywhere; }

/* A TINT, not the solid token. `--fs-warning-fg` is defined as "warning text
   ON A WARNING TINT" — painting it over solid `--fs-warning` is the same-hue
   contrast failure #3141 records. The 12% mix is how theme.css builds its own
   `-bg` pairs, and it keeps the value a resolvable var() rather than a raw hex
   that check_design_tokens.py cannot see at all. */
.check-moved {
  margin-left: auto; flex: none;
  background: color-mix(in srgb, var(--fs-warning) 12%, transparent);
  color: var(--fs-warning-fg);
  border-radius: var(--fs-radius-pill);
  padding: 0.1rem 0.5rem;
  font-size: var(--fs-size-tiny); letter-spacing: var(--fs-tracking-tiny);
}

.detail {
  padding: 0 var(--fs-space-3) var(--fs-space-3);
  display: flex; flex-direction: column; gap: var(--fs-space-2);
}
.warn {
  margin: 0; font-size: var(--fs-size-tiny); line-height: var(--fs-leading-body);
  color: var(--fs-warning-fg);
  background: color-mix(in srgb, var(--fs-warning) 12%, transparent);
  border-radius: var(--fs-radius-sm); padding: var(--fs-space-2);
}
.fields-list { display: grid; grid-template-columns: auto 1fr; gap: 0.15rem var(--fs-space-3); margin: 0; }
.fields-list dt {
  font-size: var(--fs-size-tiny); text-transform: uppercase;
  letter-spacing: var(--fs-tracking-tiny); color: var(--fs-text-tertiary);
}
.fields-list dd {
  margin: 0; font-size: var(--fs-size-body-sm);
  color: var(--fs-text-primary); min-width: 0; overflow-wrap: anywhere;
}
h4 { margin: var(--fs-space-2) 0 0; font-size: var(--fs-size-tiny); color: var(--fs-text-tertiary); }
</style>
