<template>
  <div class="sf-field sf-relation" :class="{ errors: fieldErrors.length, 'sf-relation-multiple': isMultiple, 'sf-relation-open': dropdownVisible }" ref="root">
    <span class="sf-label" :class="{ required: isRequired }">{{ title }}</span>
    <div class="sf-relation-wrapper">
      <!-- Selected items -->
      <div v-if="selected.length" class="sf-relation-selected">
        <div v-for="item in selected" :key="itemKey(item)" class="sf-relation-tag">
          <img v-if="getItemImage(item)" class="sf-relation-tag-image" :src="getItemImage(item)" alt=""
            loading="lazy" />
          <span class="sf-relation-tag-text">{{ getDisplayName(item) }}</span>
          <button v-if="isMultiple || allowClear" type="button" class="sf-relation-tag-remove"
            @click.stop="removeItem(item)">
            <SfIcon name="times" />
          </button>
        </div>
      </div>

      <!-- Search box -->
      <div v-show="showSearch" class="sf-relation-search">
        <input ref="searchInput" type="text" class="sf-input sf-relation-input" :placeholder="placeholder"
          autocomplete="off" v-model="searchQuery" @input="onSearchInput" @focus="openDropdown"
          @keydown="handleKeyDown" />
        <!-- Dropdown -->
        <div v-show="dropdownVisible" class="sf-relation-dropdown" ref="dropdown" @scroll="onDropdownScroll">
          <div v-if="filteredResults.length === 0 && !loading" class="sf-relation-dropdown-empty">
            No results found
          </div>
          <div v-for="(item, i) in filteredResults" :key="itemKey(item)" class="sf-relation-dropdown-item"
            :class="{ highlighted: i === highlightIndex }" @click="selectItem(item)">
            <img v-if="getItemImage(item)" class="sf-relation-item-image" :src="getItemImage(item)" alt=""
              loading="lazy" />
            <span class="sf-relation-item-content">
              <span class="sf-relation-item-name">{{ getDisplayName(item) }}</span>
              <span v-if="getItemDescription(item)" class="sf-relation-item-description">{{ getItemDescription(item)
              }}</span>
            </span>
          </div>
          <div v-if="loading" class="sf-relation-dropdown-loading">Loading…</div>
        </div>
      </div>
    </div>
    <ul v-if="fieldErrors.length" class="errorlist">
      <li v-for="(err, i) in fieldErrors" :key="i">{{ err }}</li>
    </ul>
  </div>
</template>

<script>
import { debounce } from '../utils';
import SfIcon from './SfIcon.vue';

export default {
  name: 'RelationEditor',
  components: { SfIcon },
  inject: {
    language: { from: 'language', default: () => () => '' },
  },
  props: {
    schema: { type: Object, required: true },
    modelValue: { default: null },
    path: { type: Array, default: () => [] },
    form: { type: Object, default: null },
  },
  emits: ['update:modelValue'],
  data() {
    const isMultiple = !!this.schema.multiple;
    const value = this.modelValue;
    let selected;
    if (isMultiple) {
      selected = Array.isArray(value) ? [...value] : [];
    } else {
      selected = value ? [value] : [];
    }
    const options = this.schema.options || {};
    return {
      isMultiple,
      allowClear: options.select2?.allowClear ?? true,
      placeholder: options.select2?.placeholder || 'Search...',
      searchUrl: options.select2?.ajax?.url || '',
      // Generic, presentation-only rendering. An item may carry an image URL
      // and/or a secondary text line; the field name each lives under is
      // configurable (defaults: `image` / `description`) so the editor stays
      // agnostic of any particular backend model.
      imageKey: options.itemImage || 'image',
      descriptionKey: options.itemDescription || 'description',
      selected,
      dropdownVisible: false,
      searchResults: [],
      currentPage: 1,
      hasMore: false,
      loading: false,
      highlightIndex: -1,
      searchQuery: '',
    };
  },
  computed: {
    title() {
      return this.schema.title || this.humanize(this.path[this.path.length - 1]) || '';
    },
    isRequired() {
      if (this.path.length < 2 || !this.form) return false;
      const parentPath = this.path.slice(0, -1);
      const fieldName = this.path[this.path.length - 1];
      const parentSchema = this.form.getSchemaAtPath(parentPath);
      return parentSchema && Array.isArray(parentSchema.required) && parentSchema.required.includes(fieldName);
    },
    showSearch() {
      if (!this.isMultiple && this.selected.length > 0) return false;
      return true;
    },
    fieldErrors() {
      if (!this.form || !this.form.getErrorsForPath) return [];
      return this.form.getErrorsForPath(this.path);
    },
    filteredResults() {
      const selectedIds = new Set(this.selected.map(item => this.itemKey(item)));
      return this.searchResults.filter(item => !selectedIds.has(this.itemKey(item)));
    },
  },
  created() {
    this._doSearch = debounce((query) => this.fetchResults(query, 1), 300);
    this._onDocClick = (e) => {
      if (this.$refs.root && !this.$refs.root.contains(e.target)) {
        this.closeDropdown();
      }
    };
  },
  mounted() {
    document.addEventListener('click', this._onDocClick);
    this.hydrateSelectedDisplay();
  },
  beforeUnmount() {
    document.removeEventListener('click', this._onDocClick);
  },
  methods: {
    humanize(str) {
      if (!str) return '';
      return str.replace(/_/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, s => s.toUpperCase());
    },
    getDisplayName(item) {
      if (!item) return '';
      for (const key of ['__str__', 'name', 'title', 'label']) {
        if (item[key]) return String(item[key]);
      }
      return `#${item.id || '?'}`;
    },
    getItemImage(item) {
      const url = item && item[this.imageKey];
      return typeof url === 'string' && url.trim() ? url : '';
    },
    getItemDescription(item) {
      const desc = item && item[this.descriptionKey];
      return desc != null && desc !== '' ? String(desc) : '';
    },
    itemKey(item) {
      return `${item.id}-${item.model || ''}`;
    },
    onSearchInput() {
      this._doSearch(this.searchQuery);
    },
    openDropdown() {
      if (this.dropdownVisible) return;
      this.dropdownVisible = true;
      this.fetchResults(this.searchQuery, 1);
    },
    closeDropdown() {
      this.dropdownVisible = false;
      this.highlightIndex = -1;
    },
    async fetchResults(query, page) {
      if (!this.searchUrl) return;
      this.loading = true;
      this.currentPage = page;

      try {
        const url = new URL(this.searchUrl, window.location.origin);
        url.searchParams.set('_q', query || '');
        url.searchParams.set('page', String(page));
        const lang = typeof this.language === 'function' ? this.language() : this.language;
        if (lang) url.searchParams.set('_lang', lang);

        const response = await fetch(url, { credentials: 'same-origin' });
        if (!response.ok) return;
        const data = await response.json();

        if (page === 1) {
          this.searchResults = data.items || [];
        } else {
          this.searchResults = this.searchResults.concat(data.items || []);
        }
        this.hasMore = data.more;
      } finally {
        this.loading = false;
      }
    },
    onDropdownScroll() {
      // Infinite scroll: pull the next page once the user nears the bottom.
      // The `loading` flag is set synchronously at the top of fetchResults,
      // so rapid-fire scroll events can't trigger duplicate page loads.
      if (!this.hasMore || this.loading) return;
      const el = this.$refs.dropdown;
      if (!el) return;
      const remaining = el.scrollHeight - el.scrollTop - el.clientHeight;
      if (remaining <= 48) {
        this.fetchResults(this.searchQuery, this.currentPage + 1);
      }
    },
    async hydrateSelectedDisplay() {
      // Persisted relations only carry the minimal {id, name, model} dict, so
      // pre-selected items have no image/description after a reload. Re-fetch
      // their display data by pk from the same search endpoint and layer it
      // underneath (persisted identity always wins). Best-effort: any failure
      // simply leaves the items as text-only.
      if (!this.searchUrl || !this.selected.length) return;
      const needs = this.selected.filter(
        (it) => it && it.id != null && !this.getItemImage(it) && !this.getItemDescription(it)
      );
      if (!needs.length) return;
      try {
        const url = new URL(this.searchUrl, window.location.origin);
        url.searchParams.set('_q', `_pk__in=${needs.map((it) => it.id).join(',')}`);
        url.searchParams.set('page', '1');
        const lang = typeof this.language === 'function' ? this.language() : this.language;
        if (lang) url.searchParams.set('_lang', lang);

        const response = await fetch(url, { credentials: 'same-origin' });
        if (!response.ok) return;
        const data = await response.json();
        const byKey = {};
        for (const it of data.items || []) byKey[this.itemKey(it)] = it;
        this.selected = this.selected.map((it) => {
          const fresh = byKey[this.itemKey(it)];
          return fresh ? { ...fresh, ...it } : it;
        });
      } catch {
        /* display hydration is best-effort */
      }
    },
    handleKeyDown(e) {
      if (!this.filteredResults.length) return;
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        this.highlightIndex = Math.min(this.highlightIndex + 1, this.filteredResults.length - 1);
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        this.highlightIndex = Math.max(this.highlightIndex - 1, 0);
      } else if (e.key === 'Enter') {
        e.preventDefault();
        if (this.highlightIndex >= 0 && this.highlightIndex < this.filteredResults.length) {
          this.selectItem(this.filteredResults[this.highlightIndex]);
        }
      } else if (e.key === 'Escape') {
        this.closeDropdown();
      }
    },
    selectItem(item) {
      if (this.isMultiple) {
        this.selected.push(item);
      } else {
        this.selected = [item];
      }
      this.searchQuery = '';
      this.highlightIndex = -1;
      this.closeDropdown();
      this.emitValue();
    },
    removeItem(item) {
      this.selected = this.selected.filter(s => !(s.id === item.id && (s.model || '') === (item.model || '')));
      this.emitValue();
    },
    emitValue() {
      if (this.isMultiple) {
        this.$emit('update:modelValue', [...this.selected]);
      } else {
        this.$emit('update:modelValue', this.selected[0] || null);
      }
    },
  },
};
</script>
