/**
 * mnemory UI — Memory Check (fsck) Tab (Alpine.js component).
 *
 * Runs a memory consistency check, displays issues grouped by type
 * with full reasoning, Browse-tab-style memory cards, and clear
 * action cards with metadata change display.
 *
 * Usage:
 *   <div x-data="fsckTab()" x-init="init()"> ... </div>
 */

/* eslint-disable no-unused-vars */

// ── Utilities ──────────────────────────────────────────────────────

/**
 * Format a duration in seconds as a human-readable age string.
 * e.g. 45 → "45s ago", 130 → "2m ago", 7400 → "2h ago", 90000 → "1d ago"
 *
 * @param {number} seconds - Age in seconds (non-negative)
 * @returns {string}
 */
function formatAge(seconds) {
  if (seconds < 60) return `${seconds}s ago`;
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
  if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
  return `${Math.floor(seconds / 86400)}d ago`;
}

// ── Constants ──────────────────────────────────────────────────────

/** Issue type display order (most critical first). */
const FSCK_GROUP_ORDER = ['security', 'contradiction', 'duplicate', 'quality', 'split', 'reclassify'];

/** Severity sort weight (lower = higher priority). */
const FSCK_SEVERITY_WEIGHT = { high: 0, medium: 1, low: 2 };

/** Group metadata keyed by issue type. */
const FSCK_GROUP_META = {
  security:      { label: 'Security Issues',  icon: '\u26D4', borderCls: 'border-red-500/40',    headerCls: 'text-red-400' },
  contradiction: { label: 'Contradictions',    icon: '\u26A0', borderCls: 'border-red-500/30',    headerCls: 'text-red-300' },
  duplicate:     { label: 'Duplicates',        icon: '\u229C', borderCls: 'border-amber-500/30',  headerCls: 'text-amber-300' },
  quality:       { label: 'Quality Issues',    icon: '\u270E', borderCls: 'border-blue-500/30',   headerCls: 'text-blue-300' },
  split:         { label: 'Should Be Split',   icon: '\u2702', borderCls: 'border-purple-500/30', headerCls: 'text-purple-300' },
  reclassify:    { label: 'Misclassified',     icon: '\u2699', borderCls: 'border-teal-500/30',   headerCls: 'text-teal-300' },
};

// ── Auto-fsck Banner Component ────────────────────────────────────
// Self-contained Alpine component for the auto-fsck status banner.
// Reads all data from $store.metrics (set by metricsTab), so it has
// no dependency on fsckTab() scope — avoids Alpine scope errors when
// the store updates before fsckTab is initialized.

function autoFsckBanner() {
  return {
    runLoading: false,
    countdown: null,
    _timer: null,

    /** Start a 30-second countdown refresh interval. */
    _startCountdown() {
      this._refreshCountdown();
      this._timer = setInterval(() => this._refreshCountdown(), 30000);
    },

    /** Compute countdown string from $store.metrics.autofsck.next_run_at. */
    _refreshCountdown() {
      const af = Alpine.store('metrics')?.autofsck;
      if (af?.running) {
        this.countdown = 'Running now...';
        return;
      }
      if (!af?.next_run_at) {
        this.countdown = null;
        return;
      }
      const remaining = Math.max(0, Math.floor(af.next_run_at - Date.now() / 1000));
      if (remaining <= 0) {
        this.countdown = 'Starting soon...';
        return;
      }
      const h = Math.floor(remaining / 3600);
      const m = Math.floor((remaining % 3600) / 60);
      this.countdown = h > 0 ? `${h}h ${m}m` : `${m}m`;
    },

    /** Human-readable age of the most recent auto-fsck run. */
    get lastRunAge() {
      const byUser = Alpine.store('metrics')?.autofsck?.by_user || {};
      let latestTs = null;
      for (const uid of Object.keys(byUser)) {
        const ts = byUser[uid]?.last_run;
        if (ts && (latestTs === null || ts > latestTs)) latestTs = ts;
      }
      if (!latestTs) return null;
      return formatAge(Math.floor(Date.now() / 1000) - latestTs);
    },

    /** Number of fixes applied in the most recent auto-fsck run. */
    get lastRunApplied() {
      const byUser = Alpine.store('metrics')?.autofsck?.by_user || {};
      let latestTs = null;
      let applied = 0;
      for (const uid of Object.keys(byUser)) {
        const u = byUser[uid];
        if (u.last_run && (latestTs === null || u.last_run > latestTs)) {
          latestTs = u.last_run;
          applied = u.fixes_applied || 0;
        }
      }
      return applied;
    },

    /** Whether there are last check results available to view. */
    get hasLastResults() {
      const ids = Alpine.store('metrics')?.autofsck?.last_check_ids || {};
      return Object.keys(ids).length > 0;
    },

    /** Trigger an immediate auto-fsck run. Communicates with fsckTab via event. */
    async runNow() {
      this.runLoading = true;
      try {
        const data = await MnemoryAPI.autoRunFsck();
        sessionStorage.setItem('mnemory_fsck_check_id', data.check_id);
        Alpine.store('notify').success('Auto-check started');
        window.dispatchEvent(new CustomEvent('mnemory:autorun-started', {
          detail: { checkId: data.check_id },
        }));
      } catch (err) {
        Alpine.store('notify').error(`Auto-run failed: ${err.message}`);
      } finally {
        this.runLoading = false;
      }
    },

    /** Load the last auto-check results into fsckTab via event. */
    viewLastResults() {
      const ids = Alpine.store('metrics')?.autofsck?.last_check_ids || {};
      const userId = Alpine.store('auth')?.selectedUser || Alpine.store('auth')?.userId;
      const checkId = ids[userId];
      if (!checkId) {
        Alpine.store('notify').warning('No auto-check results available');
        return;
      }
      sessionStorage.setItem('mnemory_fsck_check_id', checkId);
      window.dispatchEvent(new CustomEvent('mnemory:autorun-started', {
        detail: { checkId },
      }));
    },

    destroy() {
      if (this._timer) {
        clearInterval(this._timer);
        this._timer = null;
      }
    },
  };
}

// ── Check Tab Component ───────────────────────────────────────────

function fsckTab() {
  return {
    // ── State ──────────────────────────────────────────────────
    initialized: false,
    loading: false,
    applying: false,

    // Check state
    checkId: null,
    status: null,       // null, 'running', 'completed', 'failed'
    progress: null,
    summary: null,
    issues: [],
    error: null,
    createdAt: null,
    expiresAt: null,

    // Polling
    pollTimer: null,

    // Filters for starting a check
    filters: {
      agent_id: '',
      memory_type: '',
      categories: [],
    },

    // Severity threshold filter for displayed issues
    severityFilter: 'all',  // 'all', 'medium', 'high'
    // Confidence threshold filter (0 = all, 0.5 = 50%+, etc.)
    confidenceFilter: 0,

    // Selection state
    selectedIssues: {},  // issue_id -> boolean

    // UI expansion state
    collapsedGroups: {},      // type -> boolean (true = collapsed)
    expandedMemories: {},     // memory id -> boolean (true = show detail grid)

    // Available filter options
    availableCategories: [],
    availableAgentIds: [],

    // Applied issues display
    showAppliedIssues: false,

    // ── Lifecycle ──────────────────────────────────────────────

    init() {
      window.addEventListener('mnemory:tab-changed', (e) => {
        if (e.detail.tab === 'check') {
          if (!this.initialized) {
            this.initialized = true;
            this.loadCategories();
            this._loadAgentIds();
          }
          // On every tab visit, try to restore a previous check if none is active
          if (!this.checkId) {
            this._restoreLastCheck();
          }
        }
      });

      window.addEventListener('mnemory:user-changed', () => {
        if (this.initialized) {
          this.reset();
          this._loadAgentIds();
          sessionStorage.removeItem('mnemory_fsck_check_id');
        }
      });

      // Listen for auto-run events from the banner component
      window.addEventListener('mnemory:autorun-started', (e) => {
        const { checkId } = e.detail;
        if (checkId) {
          this.checkId = checkId;
          this.status = 'running';
          this.loading = true;
          this.progress = null;
          this.summary = null;
          this.issues = [];
          this.error = null;
          this.selectedIssues = {};
          this.startPolling();
        }
      });
    },

    async loadCategories() {
      try {
        const data = await MnemoryAPI.categories();
        this.availableCategories = (data.categories || []).map((c) => c.name);
      } catch (err) {
        console.warn('Failed to load categories:', err);
      }
    },

    selectAllCategories() {
      this.filters.categories = [...this.availableCategories];
    },

    async _loadAgentIds() {
      try {
        const data = await MnemoryAPI.stats();
        const agents = data.agents || [];
        this.availableAgentIds = agents.map((a) => a.agent_id).filter(Boolean);
      } catch { /* ignore */ }
    },

    // ── Actions ───────────────────────────────────────────────

    async startCheck() {
      this.reset();
      this.loading = true;
      this.status = 'running';

      try {
        const params = {};
        if (this.filters.agent_id) params.agent_id = this.filters.agent_id;
        if (this.filters.memory_type) params.memory_type = this.filters.memory_type;
        if (this.filters.categories.length > 0) params.categories = this.filters.categories;

        const data = await MnemoryAPI.startFsck(params);
        this.checkId = data.check_id;
        sessionStorage.setItem('mnemory_fsck_check_id', this.checkId);
        this.startPolling();
      } catch (err) {
        this.status = 'failed';
        this.error = err.message;
        this.loading = false;
        Alpine.store('notify').error(`Failed to start check: ${err.message}`);
      }
    },

    /** Attempt to restore the last check from sessionStorage on tab visit. */
    async _restoreLastCheck() {
      const savedId = sessionStorage.getItem('mnemory_fsck_check_id');
      if (!savedId) return;

      try {
        const data = await MnemoryAPI.getFsckStatus(savedId);
        // Restore state
        this.checkId = savedId;
        this.status = data.status;
        this.progress = data.progress;
        this.summary = data.summary;
        this.error = data.error;
        this.createdAt = data.created_at;
        this.expiresAt = data.expires_at;

        if (data.status === 'completed') {
          this.issues = data.issues || [];
          // Select only unapplied issues
          for (const issue of this.issues) {
            if (!issue.applied) {
              this.selectedIssues[issue.issue_id] = true;
            }
          }
        } else if (data.status === 'running' || data.status === 'applying') {
          this.loading = true;
          this.startPolling();
        } else if (data.status === 'failed') {
          // Keep the error visible but don't auto-clear
        }
      } catch {
        // Check expired or not found — clear stored ID
        sessionStorage.removeItem('mnemory_fsck_check_id');
      }
    },

    startPolling() {
      this.stopPolling();
      this.pollTimer = setInterval(() => this.pollStatus(), 2000);
      this.pollStatus();
    },

    stopPolling() {
      if (this.pollTimer) {
        clearInterval(this.pollTimer);
        this.pollTimer = null;
      }
    },

    async pollStatus() {
      if (!this.checkId) return;

      try {
        const data = await MnemoryAPI.getFsckStatus(this.checkId);
        this.status = data.status;
        this.progress = data.progress;
        this.summary = data.summary;
        this.error = data.error;
        this.createdAt = data.created_at;
        this.expiresAt = data.expires_at;

        if (data.status === 'completed') {
          this.issues = data.issues || [];
          this.stopPolling();
          this.loading = false;
          // Select only unapplied issues
          for (const issue of this.issues) {
            if (!issue.applied) {
              this.selectedIssues[issue.issue_id] = true;
            }
          }
        } else if (data.status === 'applying') {
          // Auto-apply in progress — keep polling until "completed"
          this.issues = data.issues || [];
        } else if (data.status === 'failed') {
          this.stopPolling();
          this.loading = false;
        }
      } catch (err) {
        this.stopPolling();
        this.loading = false;
        this.status = 'failed';
        this.error = err.message;
      }
    },

    async applySelected() {
      const ids = Object.entries(this.selectedIssues)
        .filter(([_, v]) => v)
        .map(([k]) => k);

      if (ids.length === 0) {
        Alpine.store('notify').warning('No issues selected');
        return;
      }

      this.applying = true;
      try {
        const data = await MnemoryAPI.applyFsck(this.checkId, ids);
        const msg = `Applied ${data.applied} fixes` +
          (data.failed > 0 ? `, ${data.failed} failed` : '');
        Alpine.store('notify').success(msg);

        const appliedIds = new Set(
          (data.details || [])
            .filter((d) => d.status === 'applied')
            .map((d) => d.issue_id)
        );
        this.issues = this.issues.filter((i) => !appliedIds.has(i.issue_id));
        for (const id of appliedIds) {
          delete this.selectedIssues[id];
        }
        this._recalcSummary();
      } catch (err) {
        Alpine.store('notify').error(`Apply failed: ${err.message}`);
      } finally {
        this.applying = false;
      }
    },

    async applyAll() {
      this.selectAll();
      await this.applySelected();
    },

    // ── Per-Memory Actions (delete / edit) ────────────────────

    /**
     * Delete an affected memory directly (bypassing the LLM-suggested fix).
     * After deletion, removes the memory from the issue. If the issue has
     * no remaining affected memories, removes the entire issue.
     */
    async deleteAffectedMemory(memId, issueId) {
      if (!confirm('Delete this memory permanently?')) return;

      try {
        await MnemoryAPI.deleteMemory(memId);
        Alpine.store('notify').success('Memory deleted');

        // Remove the memory from the issue's affected_memories
        const issue = this.issues.find((i) => i.issue_id === issueId);
        if (issue) {
          issue.affected_memories = issue.affected_memories.filter((m) => m.id !== memId);
          // If no affected memories remain, remove the entire issue
          if (issue.affected_memories.length === 0) {
            this.issues = this.issues.filter((i) => i.issue_id !== issueId);
            delete this.selectedIssues[issueId];
            this._recalcSummary();
          }
        }
      } catch (err) {
        Alpine.store('notify').error(`Delete failed: ${err.message}`);
      }
    },

    /**
     * Open the global edit modal for an affected memory.
     * After saving, updates the memory content/metadata in-place within the
     * issue's affected_memories list.
     */
    openEditMemory(mem, issue) {
      // Build a memory-like object that the global memoryEdit store expects.
      // The store needs { id, memory, metadata }.
      const memoryObj = {
        id: mem.id,
        memory: mem.content,
        metadata: mem.metadata || {},
      };

      Alpine.store('memoryEdit').show(memoryObj, (updatedFields) => {
        // After save, update the affected memory in-place
        if (updatedFields.content !== undefined) {
          mem.content = updatedFields.content;
        }
        // Update metadata fields that were changed
        if (!mem.metadata) mem.metadata = {};
        for (const key of ['memory_type', 'categories', 'importance', 'pinned', 'event_date', 'agent_id']) {
          if (updatedFields[key] !== undefined) {
            mem.metadata[key] = updatedFields[key];
          }
        }
      });
    },

    reset() {
      this.stopPolling();
      this.checkId = null;
      this.status = null;
      this.progress = null;
      this.summary = null;
      this.issues = [];
      this.error = null;
      this.createdAt = null;
      this.expiresAt = null;
      this.selectedIssues = {};
      this.collapsedGroups = {};
      this.expandedMemories = {};
      this.loading = false;
      this.applying = false;
      this.showAppliedIssues = false;
      this.severityFilter = 'all';
      this.confidenceFilter = 0;
      sessionStorage.removeItem('mnemory_fsck_check_id');
    },

    // ── Selection ─────────────────────────────────────────────

    /** IDs of issues currently visible after applying the severity filter. */
    get _visibleIssueIds() {
      const ids = new Set();
      for (const group of this.groupedIssues) {
        for (const issue of group.issues) ids.add(issue.issue_id);
      }
      return ids;
    },

    /** Select all currently visible (filtered) issues. */
    selectAll() {
      for (const id of this._visibleIssueIds) {
        this.selectedIssues[id] = true;
      }
    },

    /** Deselect all currently visible (filtered) issues. */
    deselectAll() {
      for (const id of this._visibleIssueIds) {
        delete this.selectedIssues[id];
      }
    },

    get selectedCount() {
      return Object.values(this.selectedIssues).filter(Boolean).length;
    },

    toggleIssue(issueId) {
      this.selectedIssues[issueId] = !this.selectedIssues[issueId];
    },

    /** Visible issue IDs for a specific group type (respects filters). */
    _visibleGroupIssueIds(type) {
      const group = this.groupedIssues.find((g) => g.type === type);
      return group ? group.issues.map((i) => i.issue_id) : [];
    },

    /** Select/deselect all visible issues within a specific group type. */
    toggleGroupSelection(type, select) {
      for (const id of this._visibleGroupIssueIds(type)) {
        if (select) {
          this.selectedIssues[id] = true;
        } else {
          delete this.selectedIssues[id];
        }
      }
    },

    /** Count selected issues within a visible group type. */
    groupSelectedCount(type) {
      return this._visibleGroupIssueIds(type)
        .filter((id) => this.selectedIssues[id])
        .length;
    },

    /** True when every visible issue in the group is selected. */
    isGroupFullySelected(type) {
      const ids = this._visibleGroupIssueIds(type);
      return ids.length > 0 && ids.every((id) => this.selectedIssues[id]);
    },

    /** True when some (but not all) visible issues in the group are selected. */
    isGroupPartiallySelected(type) {
      const ids = this._visibleGroupIssueIds(type);
      const selectedCount = ids.filter((id) => this.selectedIssues[id]).length;
      return selectedCount > 0 && selectedCount < ids.length;
    },

    /** Toggle all visible issues in a group: select all if not fully selected, else deselect all. */
    toggleGroupAll(type) {
      const select = !this.isGroupFullySelected(type);
      this.toggleGroupSelection(type, select);
    },

    // ── Computed ──────────────────────────────────────────────

    get progressPercent() {
      return this.progress ? this.progress.percent : 0;
    },

    get phaseLabel() {
      if (!this.progress) return '';
      const labels = {
        starting: 'Starting...',
        security_scan: 'Phase 1/3 \u2014 Security scan (regex + LLM re-evaluation)',
        duplicate_search: 'Phase 2/3 \u2014 Duplicate detection (searching)',
        duplicate_eval: 'Phase 2/3 \u2014 Duplicate detection (evaluating clusters)',
        quality_check: 'Phase 3/3 \u2014 Quality check',
        done: 'Complete',
      };
      return labels[this.progress.phase] || this.progress.phase;
    },

    get expiresIn() {
      if (!this.expiresAt) return '';
      const exp = new Date(this.expiresAt);
      const now = new Date();
      const diff = Math.max(0, Math.floor((exp - now) / 1000 / 60));
      if (diff <= 0) return 'expired';
      return `${diff}m remaining`;
    },

    get hasIssues() {
      return this.issues.some((i) => !i.applied);
    },

    /**
     * Issues grouped by type, ordered by FSCK_GROUP_ORDER.
     * Each group: { type, label, icon, borderCls, headerCls, issues: [...] }
     * Issues within each group are sorted by severity (high > medium > low),
     * then by confidence (higher = more actionable) as a tiebreaker.
     * Only groups with at least one issue are included.
     * Excludes already-applied issues (shown separately in appliedIssues).
     */
    get groupedIssues() {
      const byType = {};
      for (const issue of this.issues) {
        // Skip applied issues — shown separately
        if (issue.applied) continue;
        // Apply severity threshold filter
        if (this.severityFilter === 'medium' && issue.severity === 'low') continue;
        if (this.severityFilter === 'high' && issue.severity !== 'high') continue;
        // Apply confidence threshold filter (null confidence treated as 0)
        if (this.confidenceFilter > 0 && (issue.confidence ?? 0) < this.confidenceFilter) continue;

        if (!byType[issue.type]) byType[issue.type] = [];
        byType[issue.type].push(issue);
      }

      const groups = [];
      for (const type of FSCK_GROUP_ORDER) {
        const issues = byType[type];
        if (!issues || issues.length === 0) continue;

        // Sort by severity first, then confidence (higher = more actionable)
        issues.sort((a, b) => {
          const sevA = FSCK_SEVERITY_WEIGHT[a.severity] ?? 1;
          const sevB = FSCK_SEVERITY_WEIGHT[b.severity] ?? 1;
          if (sevA !== sevB) return sevA - sevB;
          const confA = a.confidence ?? 0;
          const confB = b.confidence ?? 0;
          return confB - confA;
        });

        const meta = FSCK_GROUP_META[type] || {};
        groups.push({
          type,
          label: meta.label || type,
          icon: meta.icon || '\u2022',
          borderCls: meta.borderCls || 'border-brand-border',
          headerCls: meta.headerCls || 'text-secondary',
          issues,
        });
      }
      return groups;
    },

    /**
     * Issues that were already auto-applied. Shown in a separate
     * read-only collapsed section as an action log.
     */
    get appliedIssues() {
      return this.issues.filter((i) => i.applied);
    },

    get appliedIssueCount() {
      return this.appliedIssues.length;
    },

    // ── Group Collapse ────────────────────────────────────────

    toggleGroup(type) {
      this.collapsedGroups[type] = !this.collapsedGroups[type];
    },

    isGroupCollapsed(type) {
      return !!this.collapsedGroups[type];
    },

    // ── Memory Detail Expansion ───────────────────────────────

    toggleMemoryDetail(memId) {
      this.expandedMemories[memId] = !this.expandedMemories[memId];
    },

    isMemoryExpanded(memId) {
      return !!this.expandedMemories[memId];
    },

    // ── Issue Type Badge (for issue headers) ──────────────────

    issueTypeBadgeClass(type) {
      const classes = {
        duplicate: 'bg-amber-500/20 text-amber-300',
        quality: 'bg-blue-500/20 text-blue-300',
        split: 'bg-purple-500/20 text-purple-300',
        contradiction: 'bg-red-500/20 text-red-300',
        reclassify: 'bg-teal-500/20 text-teal-300',
        security: 'bg-red-600/20 text-red-400',
      };
      return classes[type] || 'bg-slate-500/20 text-slate-300';
    },

    severityBadgeClass(severity) {
      const classes = {
        low: 'bg-slate-500/20 text-slate-300',
        medium: 'bg-amber-500/20 text-amber-300',
        high: 'bg-red-500/20 text-red-300',
      };
      return classes[severity] || 'bg-slate-500/20 text-slate-300';
    },

    // ── Memory Card Badges (matching Browse tab) ──────────────

    memoryTypeBadgeClass(type) {
      const classes = {
        fact: 'badge-fact',
        preference: 'badge-preference',
        episodic: 'badge-episodic',
        procedural: 'badge-procedural',
        context: 'badge-context',
      };
      return classes[type] || 'badge-context';
    },

    memoryImportanceBadgeClass(importance) {
      const classes = {
        low: 'badge-low',
        normal: 'badge-normal',
        high: 'badge-high',
        critical: 'badge-critical',
      };
      return classes[importance] || 'badge-normal';
    },

    // ── Action Helpers ────────────────────────────────────────

    /** Check if an action is a no-op update (no content change, no metadata change). */
    isNoopAction(action) {
      if (action.action !== 'update') return false;
      if (action.new_content) return false;
      if (!action.new_metadata) return true;
      // Check if all metadata fields are null
      return Object.values(action.new_metadata).every((v) => v === null || v === undefined);
    },

    /** Return actions for an issue with no-op updates filtered out. */
    filteredActions(issue) {
      return (issue.actions || []).filter((a) => !this.isNoopAction(a));
    },

    /** Action card border class (colored left border). */
    actionBorderClass(action) {
      const classes = {
        delete: 'border-l-4 border-l-red-500/60',
        update: 'border-l-4 border-l-blue-500/60',
        add: 'border-l-4 border-l-green-500/60',
      };
      return classes[action] || '';
    },

    actionLabelClass(action) {
      const classes = {
        delete: 'text-red-400',
        update: 'text-blue-400',
        add: 'text-green-400',
      };
      return classes[action] || 'text-secondary';
    },

    actionLabelText(action) {
      const labels = {
        update: 'UPDATE',
        delete: 'DELETE',
        add: 'ADD',
      };
      return labels[action] || action.toUpperCase();
    },

    actionIcon(action) {
      const icons = {
        delete: '\u2716',  // heavy multiplication X
        update: '\u270E',  // pencil
        add: '\u002B',     // plus
      };
      return icons[action] || '\u2022';
    },

    /**
     * Extract non-null metadata changes from an action's new_metadata.
     * Returns array of { field, label, value, type } objects.
     * type is 'badge-type', 'badge-importance', 'categories', 'boolean'.
     */
    metadataChanges(newMetadata) {
      if (!newMetadata) return [];
      const changes = [];

      if (newMetadata.memory_type !== null && newMetadata.memory_type !== undefined) {
        changes.push({
          field: 'memory_type',
          label: 'Type',
          value: newMetadata.memory_type,
          type: 'badge-type',
        });
      }
      if (newMetadata.importance !== null && newMetadata.importance !== undefined) {
        changes.push({
          field: 'importance',
          label: 'Importance',
          value: newMetadata.importance,
          type: 'badge-importance',
        });
      }
      if (newMetadata.categories !== null && newMetadata.categories !== undefined) {
        changes.push({
          field: 'categories',
          label: 'Categories',
          value: newMetadata.categories,
          type: 'categories',
        });
      }
      if (newMetadata.pinned !== null && newMetadata.pinned !== undefined) {
        changes.push({
          field: 'pinned',
          label: 'Pinned',
          value: newMetadata.pinned,
          type: 'boolean',
        });
      }

      return changes;
    },

    // ── Display Helpers ───────────────────────────────────────

    typeIcon(type) {
      return (FSCK_GROUP_META[type] || {}).icon || '\u2022';
    },

    formatDate(dateStr) {
      if (!dateStr) return '';
      try {
        const d = new Date(dateStr);
        return d.toLocaleString(undefined, {
          year: 'numeric', month: 'short', day: 'numeric',
          hour: '2-digit', minute: '2-digit',
        });
      } catch { return dateStr; }
    },

    summaryCards() {
      if (!this.summary) return [];
      const cards = [];
      if (this.summary.security > 0) cards.push({ type: 'security', count: this.summary.security, label: 'Security', cls: 'border-red-500/30 bg-red-500/10' });
      if (this.summary.duplicate > 0) cards.push({ type: 'duplicate', count: this.summary.duplicate, label: 'Duplicates', cls: 'border-amber-500/30 bg-amber-500/10' });
      if (this.summary.quality > 0) cards.push({ type: 'quality', count: this.summary.quality, label: 'Quality', cls: 'border-blue-500/30 bg-blue-500/10' });
      if (this.summary.split > 0) cards.push({ type: 'split', count: this.summary.split, label: 'Split', cls: 'border-purple-500/30 bg-purple-500/10' });
      if (this.summary.contradiction > 0) cards.push({ type: 'contradiction', count: this.summary.contradiction, label: 'Contradictions', cls: 'border-red-500/30 bg-red-500/10' });
      if (this.summary.reclassify > 0) cards.push({ type: 'reclassify', count: this.summary.reclassify, label: 'Reclassify', cls: 'border-teal-500/30 bg-teal-500/10' });
      return cards;
    },

    /** Scroll to a specific issue group section and expand it if collapsed. */
    scrollToGroup(type) {
      // Expand the group if it's collapsed
      if (this.collapsedGroups[type]) {
        delete this.collapsedGroups[type];
      }
      // Wait for Alpine to render the expanded content, then scroll
      this.$nextTick(() => {
        const el = document.getElementById('fsck-group-' + type);
        if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
      });
    },

    _recalcSummary() {
      if (!this.summary) return;
      const counts = { duplicate: 0, quality: 0, split: 0, contradiction: 0, reclassify: 0, security: 0 };
      for (const issue of this.issues) {
        if (counts[issue.type] !== undefined) counts[issue.type]++;
      }
      this.summary = { ...counts, total: this.issues.length };
    },
  };
}
