<script setup lang="ts">
/**
 * Design explorer — the gallery (milestone #251 step 3).
 *
 * Renders the design system against the tokens that are actually live, read at
 * runtime rather than parsed from source, so what you see here is what the app
 * is using right now.
 *
 * HONESTY RULE, and the reason parts of this page say "not implemented":
 * a gallery of hand-written look-alikes drifts from the app within a month and
 * then lies — which is the same failure this whole surface exists to catch. So
 * every specimen below is either a REAL component imported from the app, or a
 * real token read from the browser, or it is explicitly marked as missing.
 *
 * Buttons are the case where that bites. Rule 65 specifies four variants, but
 * `.btn-primary` is defined four separate times in four `<style scoped>` blocks
 * and 30 of 54 SFCs carry their own button CSS (#2273). There is no shared
 * button to import, so rendering one here would just make this a fifth copy.
 * It is reported as a gap instead.
 */
import { computed, onMounted, ref } from "vue";

import { fetchDesignExpectations } from "@/api/design";
import DesignTabs from "@/components/DesignTabs.vue";
import PriorityBadge from "@/components/PriorityBadge.vue";
import StatusBadge from "@/components/StatusBadge.vue";
import TagPill from "@/components/TagPill.vue";
import {
  compareToTokens,
  rankFindings,
  summarise,
  type Expectation,
  type Finding,
} from "@/utils/designDrift";
import { groupTokens, readTokens, type DesignToken, type TokenGroup } from "@/utils/designTokens";

const tokens = ref<DesignToken[]>([]);
const expectations = ref<Expectation[]>([]);
const designRulebookId = ref<number | null>(null);
const driftLoaded = ref(false);
const showCleanRows = ref(false);

/**
 * Read on mount, not at module scope: the values depend on the live cascade,
 * which needs the app's stylesheets applied and the theme attribute set.
 */
onMounted(async () => {
  tokens.value = readTokens();
  try {
    const response = await fetchDesignExpectations();
    designRulebookId.value = response.rulebook_id;
    expectations.value = response.expectations;
  } catch {
    // The gallery is useful without the panel, so a failed fetch degrades to
    // "no drift data" rather than taking the page down with it.
    designRulebookId.value = null;
  } finally {
    driftLoaded.value = true;
  }
});

const findings = computed<Finding[]>(() =>
  rankFindings(compareToTokens(expectations.value, tokens.value)),
);
const driftSummary = computed(() => summarise(findings.value));
const visibleFindings = computed(() =>
  showCleanRows.value ? findings.value : findings.value.filter((f) => f.status !== "ok"),
);

const grouped = computed(() => groupTokens(tokens.value));

const GROUP_ORDER: TokenGroup[] = ["color", "radius", "glow", "gradient", "focus", "layout", "other"];
const orderedGroups = computed(() =>
  GROUP_ORDER.filter((g) => grouped.value.has(g)).map((g) => ({ group: g, tokens: grouped.value.get(g)! })),
);

/** A token whose value reads as a colour is worth showing as a swatch. */
function isColourish(value: string): boolean {
  return /^(#|rgba?\(|hsla?\(|color-mix\()/.test(value.trim());
}

/** Rule 65's four variants — none of which exists as a shared artifact (#2273). */
const RULEBOOK_BUTTONS = [
  { name: "Primary", spec: "Moss #4A5D3F bg, Parchment text, no border" },
  { name: "Secondary", spec: "Bronze #8B7355 bg, Parchment text, no border" },
  { name: "Ghost", spec: "transparent, Parchment text, 0.5px Pewter border" },
  { name: "Destructive", spec: "Oxblood #6B2118 bg, Parchment text, pair with icon" },
];

const TYPE_SPECIMENS = [
  { token: "Display", spec: "40 / 500 / Fraunces" },
  { token: "H1", spec: "32 / 500 / Fraunces" },
  { token: "H2", spec: "24 / 500 / Fraunces" },
  { token: "H3", spec: "18 / 500 / Inter" },
  { token: "Body", spec: "15 / 400 / Inter" },
  { token: "Body small", spec: "13 / 400 / Inter" },
  { token: "Label", spec: "12 / 500 / Inter" },
  { token: "Code", spec: "13 / 400 / JetBrains Mono" },
  { token: "Tiny", spec: "11 / 500 / Inter, uppercase +0.08em" },
];
</script>

<template>
  <div class="design-view">
    <DesignTabs />

    <header class="design-header">
      <h1>Live tokens</h1>
      <p class="lede">
        The system as it actually is. Token values are read from the browser at
        runtime, so this page reflects the live cascade rather than what the
        stylesheet says. Components shown are the real ones — where a piece of
        the system has no shared implementation, it is marked missing rather
        than mocked up.
      </p>
    </header>

    <!-- Drift: what the rulebook claims vs what the tokens do. -->
    <section class="design-section">
      <h2>Rulebook drift</h2>

      <p v-if="!driftLoaded" class="muted">Checking against the design rulebook…</p>

      <div v-else-if="designRulebookId === null" class="gap-notice">
        <strong>No design rulebook designated.</strong>
        <p>
          This install hasn't said which rulebook describes its design system, so
          there is nothing to check the tokens against. Designate one in
          <router-link to="/settings">Settings</router-link> and this panel will
          compare every colour and token the rulebook names against what the
          stylesheet actually resolves to.
        </p>
      </div>

      <template v-else>
        <p class="section-note">
          <strong>{{ driftSummary.violated }}</strong> violated ·
          <strong>{{ driftSummary.missing }}</strong> missing ·
          {{ driftSummary.ok }} matching, from {{ driftSummary.total }} checkable
          claims in rulebook #{{ designRulebookId }}.
        </p>

        <div class="gap-notice">
          <strong>This compares the rulebook against the TOKENS only.</strong>
          <p>
            A value hardcoded in a component — where a token should have been
            referenced — is invisible here, because the drift isn't in the tokens
            at all. Reading it would mean bundling every component's source into
            the app. That check belongs in CI and is tracked separately, so treat
            a clean panel as "the tokens agree", not "the app agrees".
          </p>
        </div>

        <p v-if="!findings.length" class="muted">
          The rulebook names nothing this panel can check. Rules that state values
          — colours, token names — produce claims; rules that state judgement
          don't, by design.
        </p>

        <ul v-else class="spec-list">
          <li v-for="finding in visibleFindings" :key="`${finding.expectation.kind}:${finding.expectation.value}`">
            <span class="spec-name">
              <span
                v-if="finding.expectation.kind !== 'token'"
                class="swatch"
                :style="{ background: finding.expectation.value }"
                aria-hidden="true"
              />
              <code>{{ finding.expectation.value }}</code>
            </span>
            <span class="spec-detail">
              rule #{{ finding.expectation.rule_id }} — {{ finding.expectation.rule_title }}
              <span v-if="finding.matches.length" class="matches">
                · {{ finding.matches.join(", ") }}
              </span>
            </span>
            <span class="spec-status" :class="finding.status">
              {{ finding.status === "violated" ? "forbidden, but present"
                 : finding.status === "missing" ? "not in the stylesheet" : "ok" }}
            </span>
          </li>
        </ul>

        <button
          v-if="findings.length && driftSummary.ok"
          class="reveal-toggle"
          @click="showCleanRows = !showCleanRows"
        >
          {{ showCleanRows ? "Hide" : "Show" }} the {{ driftSummary.ok }} matching claims
        </button>
      </template>
    </section>

    <!-- Real components: these are imported, not recreated. -->
    <section class="design-section">
      <h2>Components</h2>
      <p class="section-note">Imported from the app. What you see is what ships.</p>

      <div class="specimen">
        <span class="specimen-label">Status badge</span>
        <div class="specimen-row">
          <StatusBadge status="todo" />
          <StatusBadge status="in_progress" />
          <StatusBadge status="done" />
          <StatusBadge status="cancelled" />
        </div>
      </div>

      <div class="specimen">
        <span class="specimen-label">Priority badge</span>
        <div class="specimen-row">
          <PriorityBadge priority="low" />
          <PriorityBadge priority="medium" />
          <PriorityBadge priority="high" />
          <span class="muted">(<code>none</code> renders nothing, by design)</span>
        </div>
      </div>

      <div class="specimen">
        <span class="specimen-label">Tag pill</span>
        <div class="specimen-row">
          <TagPill tag="design-system" />
          <TagPill tag="dismissible" dismissible />
        </div>
      </div>
    </section>

    <!-- The honest gap. -->
    <section class="design-section">
      <h2>Buttons</h2>
      <div class="gap-notice">
        <strong>Not implemented as a shared component.</strong>
        <p>
          Rule 65 specifies four variants. In practice <code>.btn-primary</code> is
          defined four separate times in four <code>&lt;style scoped&gt;</code>
          blocks, and 30 of 54 single-file components carry their own button CSS.
          There is no shared button to render here, and drawing one would make
          this page a fifth copy of it — the exact drift this surface exists to
          catch. Tracked as issue #2273.
        </p>
      </div>
      <ul class="spec-list">
        <li v-for="b in RULEBOOK_BUTTONS" :key="b.name">
          <span class="spec-name">{{ b.name }}</span>
          <span class="spec-detail">{{ b.spec }}</span>
          <span class="spec-status missing">missing</span>
        </li>
      </ul>
    </section>

    <!-- Typography: the families load, the scale does not exist as tokens. -->
    <section class="design-section">
      <h2>Type scale</h2>
      <div class="gap-notice">
        <strong>Families load; the scale has no tokens.</strong>
        <p>
          Fraunces, Inter and JetBrains Mono are imported (rule 59), but rule 60's
          scale is not expressed as custom properties, so sizes and weights are
          set ad hoc per component. Listed here as specification, not as a live
          specimen — there is nothing to read.
        </p>
      </div>
      <ul class="spec-list">
        <li v-for="t in TYPE_SPECIMENS" :key="t.token">
          <span class="spec-name">{{ t.token }}</span>
          <span class="spec-detail">{{ t.spec }}</span>
          <span class="spec-status missing">no token</span>
        </li>
      </ul>
    </section>

    <!-- Tokens: entirely real, read live. -->
    <section v-for="{ group, tokens: groupTokenList } in orderedGroups" :key="group" class="design-section">
      <h2 class="token-group-heading">{{ group }} <span class="count">{{ groupTokenList.length }}</span></h2>
      <ul class="token-list">
        <li v-for="token in groupTokenList" :key="token.name" class="token-row">
          <span
            v-if="isColourish(token.value)"
            class="swatch"
            :style="{ background: token.value }"
            aria-hidden="true"
          />
          <span v-else class="swatch swatch-none" aria-hidden="true" />
          <code class="token-name">{{ token.name }}</code>
          <code class="token-value">{{ token.value || "—" }}</code>
          <span v-if="token.overriddenInDark" class="token-flag" title="Re-declared in the dark block">
            mode-aware
          </span>
        </li>
      </ul>
    </section>

    <p v-if="!tokens.length" class="muted">Reading tokens…</p>
  </div>
</template>

<style scoped>
.design-view {
  max-width: var(--page-max-width);
  margin: 0 auto;
  padding: 1.5rem var(--page-padding-x) 4rem;
}

.design-header {
  margin-bottom: 2rem;
}

.lede {
  color: var(--color-text-secondary);
  max-width: 60ch;
  line-height: 1.7;
}

.design-section {
  margin-bottom: 2.5rem;
}

.design-section h2 {
  margin-bottom: 0.25rem;
}

.token-group-heading {
  text-transform: capitalize;
}

.count {
  color: var(--color-text-muted);
  font-size: 0.8rem;
  font-weight: 400;
}

.section-note,
.muted {
  color: var(--color-text-muted);
  font-size: 0.85rem;
  margin-bottom: 1rem;
}

/* Specimens -------------------------------------------------------------- */

.specimen {
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--color-border);
}

.specimen-label {
  display: block;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  margin-bottom: 0.5rem;
}

.specimen-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

/* Gaps ------------------------------------------------------------------- */

.gap-notice {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-warning);
  border-radius: var(--radius-sm);
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
}

.gap-notice p {
  margin: 0.5rem 0 0;
  color: var(--color-text-secondary);
  font-size: 0.9rem;
  line-height: 1.6;
  max-width: 70ch;
}

.spec-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.spec-list li {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.4rem 0;
  border-bottom: 1px solid var(--color-border);
  flex-wrap: wrap;
}

.spec-name {
  min-width: 8rem;
  font-weight: 500;
}

.spec-detail {
  color: var(--color-text-secondary);
  font-size: 0.85rem;
  flex: 1;
}

.spec-status {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0.1rem 0.45rem;
  border-radius: var(--radius-sm);
}

.spec-status.missing {
  background: var(--color-priority-medium-bg);
  color: var(--color-priority-medium);
}

.spec-status.violated {
  background: var(--color-priority-high-bg);
  color: var(--color-priority-high);
}

.spec-status.ok {
  background: var(--color-status-done-bg);
  color: var(--color-status-done);
}

.spec-name .swatch {
  vertical-align: middle;
  margin-right: 0.4rem;
}

.matches {
  color: var(--color-text-muted);
}

.reveal-toggle {
  margin-top: 0.75rem;
  padding: 0.35rem 0.75rem;
  background: transparent;
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-family: inherit;
  font-size: 0.8rem;
}

.reveal-toggle:hover {
  border-color: var(--color-text-muted);
}

/* Tokens ----------------------------------------------------------------- */

.token-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.token-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.3rem 0;
  border-bottom: 1px solid var(--color-border);
  flex-wrap: wrap;
}

.swatch {
  width: 1.25rem;
  height: 1.25rem;
  flex: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}

.swatch-none {
  background: repeating-linear-gradient(
    45deg,
    transparent,
    transparent 3px,
    var(--color-border) 3px,
    var(--color-border) 4px
  );
}

.token-name {
  min-width: 16rem;
  font-size: 0.8rem;
}

.token-value {
  color: var(--color-text-secondary);
  font-size: 0.8rem;
  flex: 1;
  word-break: break-all;
}

.token-flag {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0.05rem 0.35rem;
}

@media (max-width: 640px) {
  .token-name {
    min-width: 0;
  }
}
</style>
