// links is the frontend half deliberately deferred in Schritt 7 (see
// modules/field_types/README.md's "lookup/rollup frontends" section) -- unlike every
// other field_kind, links merges display and interaction into a single
// always-live cell with no separate edit-mode step, matching how
// boolean-cell has no edit_tag either: this ships only "links-cell",
// registered for both read and write.
//
// Unlike every other module frontend so far, this one makes its own network
// calls -- read-only ones only. It mirrors frontend/src/lib/components/
// LinksSelect.svelte's search-and-pick popover (same debounce, same
// display-column-or-primary-key fallback), duplicated here for the same
// reason every other guest/frontend duplication in this migration exists:
// this side of the module boundary can't import that file. Same-origin
// fetch() calls send the session cookie automatically (default `credentials:
// "same-origin"`), same as any other same-origin request the app already
// makes -- no separate auth handling needed.
//
// This module never writes anything itself: it only dispatches "addlink"/
// "removelink" CustomEvents (detail: {targetId}) that DataGrid.svelte wires
// straight to the SAME onAddLink/onRemoveLink props the native "+"/"×"
// buttons already call -- the actual mutation + refresh (invalidateAll())
// stays exactly where it already was, unchanged. A chip's label also
// dispatches "openrecord" (detail: {targetId}), wired to the same
// openRecordForm() the native component's equivalent chip already opens --
// originally shipped without this (a documented simplification at the
// time), added back after it turned out to be missed in practice.
const SEARCH_DEBOUNCE_MS = 250;
const RESULT_LIMIT = 20;

async function fetchJSON(url) {
	const res = await fetch(url);
	if (!res.ok) throw new Error('request failed: ' + res.status);
	return res.json();
}

// Per-target-table metadata cache -- several links-cell instances on the
// same column (one per row) all point at the same target table, no reason
// for each to refetch it separately.
const metadataCache = new Map();

function loadTargetMetadata(targetTableId) {
	let promise = metadataCache.get(targetTableId);
	if (!promise) {
		promise = fetchJSON(`/api/v1/tables/${targetTableId}`)
			.then((table) => {
				const displayColumn = (table.columns || []).find((c) => c.is_display_column);
				const displayColumnName =
					displayColumn &&
					(displayColumn.field_kind === 'physical' ||
						displayColumn.field_kind === 'auto_number' ||
						displayColumn.field_kind === 'formula')
						? displayColumn.effective_name
						: null;
				const pkColumnName = (table.columns || []).find((c) => c.is_primary_key)?.effective_name ?? null;
				return { searchColumnName: displayColumnName ?? pkColumnName, pkColumnName };
			})
			.catch(() => ({ searchColumnName: null, pkColumnName: null }));
		metadataCache.set(targetTableId, promise);
	}
	return promise;
}

customElements.define(
	'links-cell',
	class extends HTMLElement {
		static get observedAttributes() {
			return ['value', 'editable', 'target-table-id', 'aria-label'];
		}

		connectedCallback() {
			this._open = false;
			this._debounceHandle = null;
			this._searchToken = 0;
			this.render();
		}

		disconnectedCallback() {
			this._closeMenu();
		}

		attributeChangedCallback() {
			this.render();
		}

		_links() {
			try {
				const parsed = JSON.parse(this.getAttribute('value') ?? 'null');
				return Array.isArray(parsed) ? parsed : [];
			} catch {
				return [];
			}
		}

		render() {
			// A pick/remove dispatches addlink/removelink, which the host
			// resolves by re-fetching all grid data (invalidateAll()) rather
			// than patching this one cell -- the new "value" attribute that
			// results re-enters here. The open popover was appended to
			// document.body (see _toggleMenu), *outside* the subtree
			// this.innerHTML = '' below clears, so without this it would be
			// orphaned: still visible, but showing a now-stale results list
			// (an item just picked would still be listed, not yet excluded)
			// with no live connection to the trigger button this render is
			// about to replace. Closing it here instead trades away the
			// native LinksSelect's "stays open for several picks in a row"
			// convenience for a simple, always-consistent state -- clicking
			// "+" again reopens a fresh popover with the up-to-date exclude
			// list. Documented in modules/field_types/README.md, not a silent gap.
			if (this._open) this._closeMenu();

			this.innerHTML = '';
			this.className = 'flex flex-wrap items-center gap-1 overflow-hidden';
			const editable = this.getAttribute('editable') === 'true';
			const links = this._links();
			const excludeIds = new Set(links.map((l) => String(l.id)));

			for (const link of links) {
				const chip = document.createElement('span');
				chip.className = 'badge inline-flex shrink-0 items-center gap-1 bg-ink-100 text-ink-700';
				// A <button>, not a plain <span>: clicking it opens the linked
				// record's full edit form, same as the native component's
				// equivalent chip (DataGrid.svelte's openRecordForm). Originally
				// shipped as plain text ("wiring this through a module boundary
				// would need a second cross-boundary event contract for one
				// navigation shortcut") -- turned out to matter enough in
				// practice to be worth that one extra event.
				const label = document.createElement('button');
				label.type = 'button';
				label.className = 'truncate hover:underline';
				label.textContent = link.label == null ? '' : String(link.label);
				label.addEventListener('click', () => {
					this.dispatchEvent(new CustomEvent('openrecord', { detail: { targetId: link.id }, bubbles: true }));
				});
				chip.appendChild(label);
				if (editable) {
					const removeBtn = document.createElement('button');
					removeBtn.type = 'button';
					removeBtn.className = 'rounded-full text-ink-400 hover:text-red-600';
					removeBtn.setAttribute('aria-label', 'Remove ' + label.textContent);
					removeBtn.textContent = '×';
					removeBtn.addEventListener('click', () => {
						this.dispatchEvent(new CustomEvent('removelink', { detail: { targetId: link.id }, bubbles: true }));
					});
					chip.appendChild(removeBtn);
				}
				this.appendChild(chip);
			}

			if (editable && this.getAttribute('target-table-id')) {
				const trigger = document.createElement('button');
				trigger.type = 'button';
				trigger.className = 'badge shrink-0 bg-ink-100 text-ink-500 hover:bg-ink-200 hover:text-ink-700';
				trigger.setAttribute('aria-label', this.getAttribute('aria-label') || 'Add link');
				trigger.textContent = '+';
				trigger.addEventListener('click', () => this._toggleMenu(trigger, excludeIds));
				this._trigger = trigger;
				this.appendChild(trigger);
			}
		}

		async _toggleMenu(trigger, excludeIds) {
			if (this._open) {
				this._closeMenu();
				return;
			}
			this._open = true;
			this._excludeIds = excludeIds;
			const rect = trigger.getBoundingClientRect();
			const menu = document.createElement('div');
			menu.className = 'card fixed z-30 flex max-h-72 flex-col overflow-hidden py-1 shadow-lg';
			menu.style.minWidth = '220px';
			menu.style.top = rect.bottom + 2 + 'px';
			menu.style.left = rect.left + 'px';

			const search = document.createElement('input');
			search.type = 'text';
			search.placeholder = 'Search…';
			search.className = 'mx-2 mb-1 border-b border-ink-200 px-1 py-1 text-sm outline-none';
			menu.appendChild(search);

			const list = document.createElement('div');
			list.className = 'overflow-y-auto';
			menu.appendChild(list);

			document.body.appendChild(menu);
			this._menu = menu;
			this._list = list;
			search.focus();

			const targetTableId = this.getAttribute('target-table-id');
			const meta = await loadTargetMetadata(targetTableId);
			this._meta = meta;
			this._renderResults(list, [], meta, false, false);
			if (meta.searchColumnName && meta.pkColumnName) this._runSearch('');

			search.addEventListener('input', () => {
				if (this._debounceHandle) clearTimeout(this._debounceHandle);
				this._debounceHandle = setTimeout(() => this._runSearch(search.value), SEARCH_DEBOUNCE_MS);
			});

			const onOutsideClick = (e) => {
				if (menu.contains(e.target) || trigger.contains(e.target)) return;
				this._closeMenu();
			};
			const onKeydown = (e) => {
				if (e.key === 'Escape') this._closeMenu();
			};
			document.addEventListener('mousedown', onOutsideClick, true);
			document.addEventListener('keydown', onKeydown, true);
			this._cleanupMenu = () => {
				document.removeEventListener('mousedown', onOutsideClick, true);
				document.removeEventListener('keydown', onKeydown, true);
			};
		}

		async _runSearch(query) {
			if (!this._meta || !this._meta.searchColumnName || !this._meta.pkColumnName) return;
			const token = ++this._searchToken;
			this._renderResults(this._list, [], this._meta, true, false);
			const targetTableId = this.getAttribute('target-table-id');
			const params = new URLSearchParams();
			params.set('limit', String(RESULT_LIMIT));
			params.set('sort', this._meta.searchColumnName);
			if (query) {
				params.set('where', JSON.stringify({ col: this._meta.searchColumnName, op: 'like', val: query }));
			}
			try {
				const res = await fetchJSON(`/api/v1/tables/${targetTableId}/records?${params.toString()}`);
				if (token !== this._searchToken) return;
				const options = (res.records || [])
					.map((r) => ({ id: r[this._meta.pkColumnName], label: r[this._meta.searchColumnName] }))
					.filter((opt) => !this._excludeIds.has(String(opt.id)));
				this._renderResults(this._list, options, this._meta, false, false);
			} catch {
				if (token !== this._searchToken) return;
				this._renderResults(this._list, [], this._meta, false, true);
			}
		}

		_renderResults(list, options, meta, loading, loadError) {
			if (!list) return;
			list.innerHTML = '';
			if (!meta.pkColumnName) {
				list.innerHTML = '<div class="px-3 py-2 text-sm text-ink-500">Target table has no primary key.</div>';
				return;
			}
			if (loading) {
				list.innerHTML = '<div class="px-3 py-1.5 text-sm text-ink-400">Loading…</div>';
				return;
			}
			if (loadError) {
				list.innerHTML = '<div class="px-3 py-1.5 text-sm text-danger">Could not load results.</div>';
				return;
			}
			if (options.length === 0) {
				list.innerHTML = '<div class="px-3 py-1.5 text-sm text-ink-400">No results</div>';
				return;
			}
			for (const opt of options) {
				const btn = document.createElement('button');
				btn.type = 'button';
				btn.className = 'flex w-full items-center px-3 py-1.5 text-left text-sm hover:bg-ink-100';
				const span = document.createElement('span');
				span.className = 'truncate';
				span.textContent = opt.label == null ? '' : String(opt.label);
				btn.appendChild(span);
				btn.addEventListener('mousedown', (e) => e.preventDefault());
				btn.addEventListener('click', () => {
					this.dispatchEvent(new CustomEvent('addlink', { detail: { targetId: opt.id }, bubbles: true }));
				});
				list.appendChild(btn);
			}
		}

		_closeMenu() {
			this._open = false;
			if (this._debounceHandle) clearTimeout(this._debounceHandle);
			if (this._cleanupMenu) this._cleanupMenu();
			if (this._menu) this._menu.remove();
			this._menu = null;
			this._list = null;
		}
	}
);
