<template>
  <div class="base-area">
    <el-popover
      v-model="popoverVisible"
      placement="bottom-start"
      trigger="click"
      popper-class="base-area-popover"
      :width="popoverWidth"
      :disabled="disabled || readonly"
      @show="handlePopoverShow"
      @hide="handlePopoverHide"
    >
      <div class="base-area-panel">
        <div class="base-area-search">
          <el-input
            ref="searchInputRef"
            v-model="searchKeyword"
            class="base-area-search-input"
            size="small"
            clearable
            :placeholder="searchPlaceholder"
            @input="handleSearchInput"
            @clear="handleSearchClear"
            @focus="searchDropdownVisible = true"
          />
          <transition name="base-area-fade">
            <div
              v-show="showSearchDropdown"
              class="base-area-search-dropdown"
              v-loading="searchLoading"
            >
              <div
                v-for="item in searchList"
                :key="'search-' + item.id"
                class="base-area-result-item"
                :class="{ on: isSearchItemActive(item) }"
                @mousedown.prevent
                @click="selectSearchItem(item)"
              >
                {{ item.fullName || item.name }}
              </div>
              <div
                v-if="!searchLoading && !searchList.length"
                class="base-area-dropdown-empty"
              >
                {{ emptyText }}
              </div>
            </div>
          </transition>
        </div>
        <div class="base-area-path">
          <div class="base-area-tabs">
            <template v-for="(tab, index) in tabs">
              <span
                v-if="index > 0"
                :key="'sep-' + index"
                class="base-area-path-sep"
                >/</span
              >
              <span
                :key="'tab-' + index"
                class="base-area-tab"
                :class="{
                  active: activeLevel === index + 1,
                  selected: !!tab.selected,
                  disabled: !isTabEnabled(index + 1),
                }"
                @click="switchLevel(index + 1)"
                >{{ getTabLabel(tab) }}</span
              >
            </template>
          </div>
        </div>
        <div class="base-area-grid-wrap">
          <div class="base-area-grid" v-loading="browseLoading">
            <div
              v-for="item in levelItems"
              :key="item.id"
              class="base-area-item"
              :class="{ on: isSelected(item) }"
              @click="selectItem(item)"
            >
              <span>{{ item.name }}</span>
            </div>
            <div
              v-if="!browseLoading && !levelItems.length"
              class="base-area-empty"
            >
              {{ emptyText }}
            </div>
          </div>
        </div>
        <div class="base-area-level-nav">
          <el-button
            size="mini"
            class="base-area-nav-btn"
            :disabled="!canPrevLevel"
            @mousedown.prevent
            @click="prevLevel"
          >
            <i class="el-icon-arrow-left"></i>
          </el-button>
          <span class="base-area-level-label">{{ currentLevelLabel }}</span>
          <el-button
            size="mini"
            class="base-area-nav-btn"
            :disabled="!canNextLevel"
            @mousedown.prevent
            @click="nextLevel"
          >
            <i class="el-icon-arrow-right"></i>
          </el-button>
        </div>
      </div>
      <div slot="reference" class="base-area-trigger">
        <el-input
          ref="inputRef"
          class="base-area-input"
          :class="{ 'is-focus': referenceFocused }"
          :value="displayText"
          :size="size"
          :disabled="disabled"
          :placeholder="placeholder"
          readonly
          @focus="referenceFocused = true"
          @blur="referenceFocused = false"
        >
          <i
            v-if="showClearIcon"
            slot="suffix"
            class="el-input__icon el-icon-circle-close base-area-clear"
            @mousedown.prevent
            @click.stop="handleClear"
          />
        </el-input>
      </div>
    </el-popover>
  </div>
</template>

<script>
const LEVEL_LABELS = ["省份", "城市", "区县", "乡镇"];
const AREA_LEVEL_KEYS = ["province", "city", "district", "town"];
const MAX_AREA_DEPTH = 4;
const SEARCH_DEBOUNCE = 300;

export default {
  name: "baseArea",
  props: {
    value: {
      type: [String, Number],
      default: null,
    },
    labelValue: {
      type: String,
      default: null,
    },
    maxLevel: {
      type: Number,
      default: 4,
    },
    locale: {
      type: String,
      default: null,
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    readonly: {
      type: Boolean,
      default: false,
    },
    clearable: {
      type: Boolean,
      default: true,
    },
    placeholder: {
      type: String,
      default: "请选择地区",
    },
    searchPlaceholder: {
      type: String,
      default: "请输入地区名称",
    },
    emptyText: {
      type: String,
      default: "暂无匹配地区",
    },
    size: {
      type: String,
      default: "small",
    },
    popoverWidth: {
      type: Number,
      default: 630,
    },
    /** leaf: 绑定末级 id；path: 兼容旧版逗号分隔路径 */
    valueFormat: {
      type: String,
      default: "leaf",
    },
  },
  data() {
    return {
      popoverVisible: false,
      searchKeyword: "",
      searchList: [],
      selectedPath: [],
      activeLevel: 1,
      levelItems: [],
      browseLoading: false,
      searchLoading: false,
      searchDropdownVisible: false,
      referenceFocused: false,
      searchTimer: null,
      syncToken: 0,
      skipValueSync: false,
      committedBrowseSnapshot: null,
      committedBoundValue: null,
      lastSelectionPayload: null,
    };
  },
  computed: {
    hasSearchKeyword() {
      return !!(this.searchKeyword || "").trim();
    },
    showSearchDropdown() {
      return this.searchDropdownVisible && this.hasSearchKeyword;
    },
    displayText() {
      if (this.labelValue) {
        return this.labelValue;
      }
      const pathLabel = this.selectedPath.length
        ? this.buildPathLabel(this.selectedPath)
        : "";
      if (this.popoverVisible && pathLabel) {
        return pathLabel;
      }
      return pathLabel || "";
    },
    tabs() {
      const tabs = [];
      for (let i = 0; i < this.browseLevelCount; i++) {
        tabs.push({
          level: i + 1,
          label: LEVEL_LABELS[i] || `第${i + 1}级`,
          selected: this.selectedPath[i] || null,
        });
      }
      return tabs;
    },
    showClearIcon() {
      return (
        this.clearable && !this.readonly && !this.disabled && !!this.displayText
      );
    },
    canPrevLevel() {
      return this.activeLevel > 1;
    },
    canNextLevel() {
      return (
        this.activeLevel < this.browseLevelCount &&
        this.isTabEnabled(this.activeLevel + 1)
      );
    },
    browseLevelCount() {
      return Math.max(this.maxLevel, this.selectedPath.length, 1);
    },
    currentLevelLabel() {
      return LEVEL_LABELS[this.activeLevel - 1] || `第${this.activeLevel}级`;
    },
  },
  watch: {
    value: {
      immediate: true,
      handler() {
        this.syncFromValue();
      },
    },
    labelValue(val) {
      if (!val && !this.value) {
        this.resetBrowseState(false);
      }
    },
  },
  beforeDestroy() {
    clearTimeout(this.searchTimer);
  },
  methods: {
    getResolveDepthLimit(item) {
      if (item && item.treePath) {
        const ids = this.parseTreePathIds(item.treePath);
        return Math.min(Math.max(ids.length, this.maxLevel, 1), MAX_AREA_DEPTH);
      }
      return MAX_AREA_DEPTH;
    },
    isPathResolvedForItem(path, item) {
      if (!path || !path.length || !item || item.id == null) {
        return false;
      }
      return this.getAreaId(path[path.length - 1]) == item.id;
    },
    isSearchItemActive(item) {
      if (!item || this.value == null || this.value === "") {
        return false;
      }
      const leafId = this.parseValueId(this.value);
      if (leafId == null) {
        return false;
      }
      return leafId == this.getAreaId(item);
    },
    parseValueId(val) {
      if (val == null || val === "") {
        return null;
      }
      const str = String(val).trim();
      if (this.isLegacyPathValue(str)) {
        const parts = str
          .split(",")
          .map((part) => part.trim())
          .filter(Boolean);
        return parts.length ? parts[parts.length - 1] : null;
      }
      return str;
    },
    parseLegacyPathIds(val) {
      if (val == null || val === "") {
        return [];
      }
      return String(val)
        .split(",")
        .map((part) => part.trim())
        .filter((part) => part !== "");
    },
    isLegacyPathValue(val) {
      return (
        this.valueFormat === "path" &&
        val != null &&
        String(val).includes(",") &&
        !String(val).startsWith(",")
      );
    },
    parseTreePathIds(treePath) {
      if (treePath == null || treePath === "") {
        return [];
      }
      return String(treePath)
        .split(",")
        .map((part) => part.trim())
        .filter((part) => part !== "");
    },
    formatBoundValue(path, item) {
      const last = item || (path && path.length ? path[path.length - 1] : null);
      const leafId = this.getAreaId(last);
      if (leafId == null) {
        return null;
      }
      if (this.valueFormat === "path") {
        const ids = (path || [])
          .map((node) => this.getAreaId(node))
          .filter((id) => id != null && id !== "");
        return ids.length ? ids.join(",") : String(leafId);
      }
      return leafId;
    },
    getAreaId(item) {
      if (!item) {
        return null;
      }
      if (item.id != null && item.id !== "") {
        return item.id;
      }
      if (item.value != null && item.value !== "") {
        return item.value;
      }
      if (item.areaId != null && item.areaId !== "") {
        return item.areaId;
      }
      return null;
    },
    isValueSynced(leafId) {
      if (leafId == null || leafId === "" || !this.selectedPath.length) {
        return false;
      }
      const currentLeaf = this.getAreaId(
        this.selectedPath[this.selectedPath.length - 1]
      );
      return currentLeaf == leafId;
    },
    buildPathLabel(path) {
      if (!path || !path.length) {
        return "";
      }
      const last = path[path.length - 1];
      if (last && last.fullName) {
        return last.fullName;
      }
      return path
        .map((item) => item.name || "")
        .filter((name) => !!name)
        .join("");
    },
    createEmptyAreaPathResult(value) {
      return {
        value: value != null && value !== "" ? value : null,
        label: null,
        path: [],
        province: null,
        city: null,
        district: null,
        town: null,
      };
    },
    formatAreaPathResult(value, path, label) {
      const result = this.createEmptyAreaPathResult(value);
      result.label =
        label != null && label !== ""
          ? label
          : this.buildPathLabel(path) || this.labelValue || null;
      result.path = path || [];
      (path || []).forEach((node, index) => {
        const key = AREA_LEVEL_KEYS[index];
        if (key && Object.prototype.hasOwnProperty.call(result, key)) {
          result[key] = node;
        }
      });
      return result;
    },
    async ensurePathDetail(path) {
      const result = [];
      for (let i = 0; i < path.length; i++) {
        const node = path[i];
        const id = this.getAreaId(node);
        if (id != null) {
          const detail = await this.fetchAreaById(id);
          result.push(detail || node);
        } else {
          result.push(node);
        }
      }
      return result;
    },
    async getAreaPath(value) {
      const val = value !== undefined ? value : this.value;
      if (val == null || val === "") {
        return this.createEmptyAreaPathResult(null);
      }
      const leafId = this.getLeafIdFromValue(val);
      let path = null;
      if (
        leafId != null &&
        this.isValueSynced(leafId) &&
        !this.shouldResyncBrowsePath() &&
        this.selectedPath.length
      ) {
        path = this.selectedPath.map((item) => ({ ...item }));
      }
      if (!path || !path.length) {
        if (this.isLegacyPathValue(val)) {
          path = await this.resolvePathByIds(this.parseLegacyPathIds(val));
        } else if (leafId != null) {
          const area = await this.fetchAreaById(leafId);
          if (area) {
            path = await this.resolveAreaPath(area);
          } else {
            path = [{ id: leafId, fullName: this.labelValue || undefined }];
          }
        }
      }
      if (!path || !path.length) {
        return this.createEmptyAreaPathResult(val);
      }
      path = await this.ensurePathDetail(path);
      return this.formatAreaPathResult(val, path);
    },
    hasDisplayLabel() {
      return !!(this.labelValue && String(this.labelValue).trim());
    },
    getLeafIdFromValue(val) {
      if (val == null || val === "") {
        return null;
      }
      if (this.isLegacyPathValue(val)) {
        const ids = this.parseLegacyPathIds(val);
        return ids.length ? ids[ids.length - 1] : null;
      }
      return this.parseValueId(val);
    },
    emitResolvedLabelIfNeeded() {
      if (this.popoverVisible || this.labelValue) {
        return;
      }
      const label = this.buildPathLabel(this.selectedPath);
      if (label) {
        this.$emit("update:labelValue", label);
      }
    },
    cancelPendingSync() {
      this.syncToken += 1;
    },
    getTabLabel(tab) {
      if (tab.selected) {
        return tab.selected.name || tab.label;
      }
      return tab.label;
    },
    isSelected(item) {
      const current = this.selectedPath[this.activeLevel - 1];
      return current && this.getAreaId(current) === this.getAreaId(item);
    },
    isTabEnabled(level) {
      return level === 1 || this.selectedPath.length >= level - 1;
    },
    handlePopoverShow() {
      this.saveCommittedBrowseSnapshot();
      this.$emit("popover-open");
      this.$nextTick(() => {
        this.$refs.searchInputRef && this.$refs.searchInputRef.focus();
      });
      this.initBrowseStateFromValue();
    },
    saveCommittedBrowseSnapshot() {
      this.committedBoundValue = this.value;
      this.committedBrowseSnapshot = {
        boundValue: this.value,
        leafId: this.getLeafIdFromValue(this.value),
        selectedPath: this.selectedPath.map((item) => ({ ...item })),
        activeLevel: this.activeLevel,
      };
    },
    isSameBoundValue(a, b) {
      const normalize = (val) => {
        if (val == null || val === "") {
          return null;
        }
        return String(val);
      };
      return normalize(a) === normalize(b);
    },
    emitChangeIfCommitted() {
      const snapshot = this.committedBrowseSnapshot;
      if (!snapshot) {
        return;
      }
      const oldVal = snapshot.boundValue;
      const newVal = this.value;
      if (!this.isSameBoundValue(oldVal, newVal)) {
        const payload = this.lastSelectionPayload || {
          value: newVal,
          row: null,
          label: this.labelValue,
        };
        this.$emit("change", {
          value: payload.value != null ? payload.value : newVal,
          row: payload.row || null,
          label: payload.label != null ? payload.label : this.labelValue,
          path: payload.path || [],
        });
      }
      this.committedBoundValue = null;
      this.lastSelectionPayload = null;
    },
    restoreBrowseStateIfUncommitted() {
      const snapshot = this.committedBrowseSnapshot;
      if (!snapshot) {
        return;
      }
      if (!this.isSameBoundValue(this.value, snapshot.boundValue)) {
        this.levelItems = [];
        this.committedBrowseSnapshot = null;
        return;
      }
      const committedLeafId = snapshot.leafId;
      if (this.isValueSynced(committedLeafId)) {
        this.levelItems = [];
        this.committedBrowseSnapshot = null;
        return;
      }
      if (snapshot.selectedPath && snapshot.selectedPath.length) {
        this.selectedPath = snapshot.selectedPath.map((item) => ({ ...item }));
        this.activeLevel = snapshot.activeLevel;
      } else if (committedLeafId) {
        this.selectedPath = [
          {
            id: committedLeafId,
            fullName: this.labelValue || undefined,
            name: this.labelValue || undefined,
          },
        ];
        this.activeLevel = 1;
      } else {
        this.selectedPath = [];
        this.activeLevel = 1;
      }
      this.levelItems = [];
      this.committedBrowseSnapshot = null;
    },
    syncFromValue() {
      if (this.skipValueSync) {
        return;
      }
      const val = this.value;
      if (!val) {
        this.resetBrowseState(false);
        return;
      }
      if (!this.hasDisplayLabel()) {
        this.resolveLabelFromValue();
      }
    },
    async resolveLabelFromValue() {
      const val = this.value;
      if (!val || this.hasDisplayLabel()) {
        return;
      }
      const leafId = this.getLeafIdFromValue(val);
      if (leafId == null) {
        return;
      }
      const token = ++this.syncToken;
      const area = await this.fetchAreaById(leafId);
      if (token !== this.syncToken || this.hasDisplayLabel()) {
        return;
      }
      const label = (area && area.fullName) || (area && area.name) || null;
      if (label) {
        this.$emit("update:labelValue", label);
      }
    },
    shouldResyncBrowsePath() {
      if (!this.labelValue || !this.selectedPath.length) {
        return false;
      }
      const pathLabel = this.buildPathLabel(this.selectedPath);
      return pathLabel !== String(this.labelValue).trim();
    },
    syncBrowseStateForPopover() {
      const val = this.value;
      if (!val) {
        if (!this.levelItems.length) {
          this.loadLevelItems(this.getParentId());
        }
        return;
      }
      const leafId = this.getLeafIdFromValue(val);
      if (this.isValueSynced(leafId) && !this.shouldResyncBrowsePath()) {
        const expectedActive = this.selectedPath.length || 1;
        if (this.activeLevel !== expectedActive) {
          this.activeLevel = expectedActive;
          this.loadLevelItems(this.getParentId());
          return;
        }
        if (!this.levelItems.length) {
          this.loadLevelItems(this.getParentId());
        }
        return;
      }
      if (this.isLegacyPathValue(val)) {
        this.syncBrowseStateFromPathIds(this.parseLegacyPathIds(val));
        return;
      }
      this.syncBrowseStateFromLeafId(leafId);
    },
    initBrowseStateFromValue() {
      this.syncBrowseStateForPopover();
    },
    getAreaRequestData(extra = {}) {
      return {
        enabled: true,
        locale: this.locale || null,
        ...extra,
      };
    },
    normalizeAreaRows(res) {
      const objx = res && res.objx;
      if (Array.isArray(objx)) {
        return objx;
      }
      if (objx && Array.isArray(objx.records)) {
        return objx.records;
      }
      return [];
    },
    requestAreaList(extra = {}) {
      return new Promise((resolve) => {
        this.$http({
          url: USER_PREFIX + "/area/listPage",
          method: "post",
          data: this.getAreaRequestData(extra),
          success: (res) => resolve(this.normalizeAreaRows(res)),
          error: () => resolve([]),
        });
      });
    },
    fetchChildren(parentId) {
      return new Promise((resolve) => {
        this.$http({
          url: USER_PREFIX + "/area/getChildren",
          method: "post",
          data: this.getAreaRequestData({ id: parentId }),
          success: (res) => resolve(this.normalizeAreaRows(res)),
          error: () => resolve([]),
        });
      });
    },
    getParentIdFromRow(row) {
      if (!row) return null;
      if (row.parentId != null && row.parentId !== "") {
        return row.parentId;
      }
      const parent = row.parent;
      if (parent == null || parent === undefined || parent === "") {
        return null;
      }
      if (typeof parent === "object") {
        return parent.id != null ? parent.id : null;
      }
      return parent;
    },
    fetchAreaById(id) {
      if (id == null || id === "") {
        return Promise.resolve(null);
      }
      return new Promise((resolve) => {
        this.$http({
          url: USER_PREFIX + "/area/get",
          method: "post",
          data: this.getAreaRequestData({ id }),
          success: (res) => {
            const objx = res && res.objx;
            if (objx && typeof objx === "object" && !Array.isArray(objx)) {
              resolve(objx);
              return;
            }
            resolve(null);
          },
          error: () => resolve(null),
        });
      });
    },
    isAreaObject(node) {
      return node && typeof node === "object" && node.id != null;
    },
    buildAreaPath(item, depthLimit) {
      const limit = depthLimit == null ? this.maxLevel : depthLimit;
      if (
        item.pathList &&
        Array.isArray(item.pathList) &&
        item.pathList.length > 1
      ) {
        if (this.isAreaObject(item.pathList[0])) {
          return item.pathList.slice(0, limit);
        }
      }
      if (
        item.areaPath &&
        Array.isArray(item.areaPath) &&
        item.areaPath.length > 1
      ) {
        if (this.isAreaObject(item.areaPath[0])) {
          return item.areaPath.slice(0, limit);
        }
      }
      return null;
    },
    matchAreaByPrefix(fullName, prefix, children) {
      const sorted = [...children].sort(
        (a, b) => (b.name || "").length - (a.name || "").length
      );
      for (let i = 0; i < sorted.length; i++) {
        const child = sorted[i];
        const name = child.name || "";
        if (!name) {
          continue;
        }
        if (fullName.startsWith(prefix + name)) {
          return child;
        }
      }
      return null;
    },
    async resolvePathByFullName(item, depthLimit) {
      const fullName = (item.fullName || "").trim();
      if (!fullName) {
        return null;
      }
      const path = [];
      let parentId = 0;
      let prefix = "";
      for (let level = 0; level < MAX_AREA_DEPTH; level++) {
        const children = await this.fetchChildren(parentId);
        if (!children.length) {
          break;
        }
        const matched = this.matchAreaByPrefix(fullName, prefix, children);
        if (!matched) {
          break;
        }
        path.push(matched);
        prefix += matched.name || "";
        if (matched.id == item.id) {
          return path;
        }
        parentId = matched.id;
      }
      if (path.length && path[path.length - 1].id != item.id) {
        const siblings = await this.fetchChildren(path[path.length - 1].id);
        const leaf = siblings.find((row) => row.id == item.id);
        if (leaf) {
          path.push(leaf);
        } else if (item.name && fullName.endsWith(item.name)) {
          path.push(item);
        }
      }
      return path.length ? path : null;
    },
    async resolvePathByParentChain(item, depthLimit) {
      const chain = [];
      let current = item;
      let safety = 0;
      while (current && safety < MAX_AREA_DEPTH + 2) {
        safety += 1;
        chain.unshift(current);
        const parentId = this.getParentIdFromRow(current);
        if (!parentId) {
          break;
        }
        const parent = await this.fetchAreaById(parentId);
        if (!parent || parent.id != parentId) {
          break;
        }
        current = parent;
      }
      return chain.length ? chain : null;
    },
    async resolvePathFromTreePath(area, depthLimit) {
      const ids = this.parseTreePathIds(area && area.treePath);
      if (!ids.length) {
        return area && area.id != null ? [area] : null;
      }
      const path = [];
      for (let i = 0; i < ids.length; i++) {
        const id = ids[i];
        let node = null;
        if (area && this.getAreaId(area) == id) {
          node = area;
        } else {
          node = await this.fetchAreaById(id);
        }
        path.push(node || { id });
      }
      if (area && area.id != null) {
        const exists = path.some((node) => this.getAreaId(node) == area.id);
        if (!exists) {
          path.push(area);
        }
      }
      return path.length ? path : null;
    },
    async resolveAreaPath(item, options = {}) {
      if (!item || item.id == null) {
        return item ? [item] : [];
      }
      const parentPath = await this.resolvePathByParentChain(item);
      if (this.isPathResolvedForItem(parentPath, item)) {
        return parentPath;
      }
      const depthLimit =
        options.depthLimit != null
          ? options.depthLimit
          : this.getResolveDepthLimit(item);
      if (item.treePath) {
        const treePath = await this.resolvePathFromTreePath(item, depthLimit);
        if (this.isPathResolvedForItem(treePath, item)) {
          return treePath;
        }
      }
      const presetPath = this.buildAreaPath(item, depthLimit);
      if (this.isPathResolvedForItem(presetPath, item)) {
        return presetPath;
      }
      const fullNamePath = await this.resolvePathByFullName(item, depthLimit);
      if (this.isPathResolvedForItem(fullNamePath, item)) {
        return fullNamePath;
      }
      if (parentPath && parentPath.length) {
        return parentPath;
      }
      if (item.treePath) {
        const treePath = await this.resolvePathFromTreePath(item, depthLimit);
        if (treePath && treePath.length) {
          return treePath;
        }
      }
      if (presetPath && presetPath.length) {
        return presetPath;
      }
      if (fullNamePath && fullNamePath.length) {
        return fullNamePath;
      }
      return [item];
    },
    async syncBrowseStateFromLeafId(id) {
      if (id == null || id === "") {
        return;
      }
      const token = ++this.syncToken;
      const area = await this.fetchAreaById(id);
      if (token !== this.syncToken) {
        return;
      }
      if (area) {
        await this.syncBrowseStateFromArea(area);
        return;
      }
      await this.syncBrowseStateFromArea({
        id,
        fullName: this.labelValue || undefined,
      });
    },
    async syncBrowseStateFromArea(item) {
      const token = ++this.syncToken;
      let seed = item || {};
      if (
        seed.id != null &&
        !seed.fullName &&
        !seed.name &&
        !this.getParentIdFromRow(seed)
      ) {
        const detail = await this.fetchAreaById(seed.id);
        if (detail) {
          seed = detail;
        }
      } else if (seed.id != null && !seed.fullName && this.labelValue) {
        seed = { ...seed, fullName: this.labelValue };
      }
      let path = await this.resolveAreaPath(seed);
      if (token !== this.syncToken) {
        return;
      }
      if (!path.length) {
        path = [seed];
      }
      const last = path[path.length - 1];
      if (last && last.id != seed.id && seed.id != null) {
        const siblings = await this.fetchChildren(
          path.length > 1 ? path[path.length - 2].id : 0
        );
        const exact = siblings.find((row) => row.id == seed.id);
        if (exact) {
          path = path.slice(0, -1).concat(exact);
        } else {
          path = path.concat(seed);
        }
      }
      if (token !== this.syncToken) {
        return;
      }
      this.selectedPath = path;
      this.activeLevel = path.length || 1;
      this.loadLevelItems(this.getParentId());
      this.emitResolvedLabelIfNeeded();
    },
    async resolvePathByIds(ids, depthLimit) {
      const limit = depthLimit == null ? ids.length : depthLimit;
      const path = [];
      let parentId = 0;
      for (let i = 0; i < ids.length && i < limit; i++) {
        const targetId = ids[i];
        const children = await this.fetchChildren(parentId);
        let matched = children.find((row) => this.getAreaId(row) == targetId);
        if (matched) {
          path.push(matched);
          parentId = matched.id;
          continue;
        }
        const fetched = await this.fetchAreaById(targetId);
        if (fetched) {
          path.push(fetched);
          parentId = fetched.id;
        } else {
          path.push({ id: targetId });
          break;
        }
      }
      return path;
    },
    async syncBrowseStateFromPathIds(ids) {
      const token = ++this.syncToken;
      const depthLimit = Math.min(
        Math.max(ids.length, this.maxLevel, 1),
        MAX_AREA_DEPTH
      );
      const path = await this.resolvePathByIds(ids, depthLimit);
      if (token !== this.syncToken) {
        return;
      }
      if (!path.length) {
        if (!this.levelItems.length) {
          this.loadLevelItems(0);
        }
        return;
      }
      this.selectedPath = path;
      this.activeLevel = path.length;
      this.loadLevelItems(this.getParentId());
      this.emitResolvedLabelIfNeeded();
    },
    handlePopoverHide() {
      this.searchKeyword = "";
      this.searchList = [];
      this.searchDropdownVisible = false;
      this.cancelPendingSync();
      this.emitChangeIfCommitted();
      this.restoreBrowseStateIfUncommitted();
    },
    handleSearchInput() {
      const keyword = (this.searchKeyword || "").trim();
      clearTimeout(this.searchTimer);
      this.searchDropdownVisible = !!keyword;
      if (!keyword) {
        this.handleSearchClear();
        return;
      }
      this.searchTimer = setTimeout(() => {
        this.doSearch(keyword);
      }, SEARCH_DEBOUNCE);
    },
    handleSearchClear() {
      this.searchList = [];
      this.searchDropdownVisible = false;
    },
    doSearch(keyword) {
      this.searchLoading = true;
      this.requestAreaList({ fullName: keyword, size: 20 }).then((rows) => {
        this.searchList = rows;
        this.searchLoading = false;
      });
    },
    getParentId() {
      if (this.activeLevel <= 1) {
        return 0;
      }
      const parent = this.selectedPath[this.activeLevel - 2];
      return parent ? this.getAreaId(parent) : 0;
    },
    loadLevelItems(parentId) {
      this.browseLoading = true;
      this.fetchChildren(parentId).then((rows) => {
        this.levelItems = rows;
        this.browseLoading = false;
      });
    },
    switchLevel(level) {
      if (level > this.browseLevelCount || !this.isTabEnabled(level)) {
        return;
      }
      this.cancelPendingSync();
      this.activeLevel = level;
      this.loadLevelItems(this.getParentId());
    },
    prevLevel() {
      if (!this.canPrevLevel) {
        return;
      }
      this.switchLevel(this.activeLevel - 1);
    },
    nextLevel() {
      if (!this.canNextLevel) {
        return;
      }
      this.switchLevel(this.activeLevel + 1);
    },
    selectSearchItem(item) {
      this.cancelPendingSync();
      this.syncBrowseStateFromArea(item).then(() => {
        this.searchKeyword = "";
        this.searchList = [];
        this.searchDropdownVisible = false;
        this.applySelection(item).then(() => {
          this.popoverVisible = false;
        });
      });
    },
    selectItem(item) {
      if (!this.isTabEnabled(this.activeLevel)) {
        return;
      }
      this.cancelPendingSync();
      const level = this.activeLevel;
      this.selectedPath = this.selectedPath.slice(0, level - 1);
      this.selectedPath.push(item);
      this.applySelection(item).then(() => {
        if (level >= this.maxLevel) {
          this.popoverVisible = false;
          return;
        }
        this.fetchChildren(this.getAreaId(item)).then((children) => {
          if (!children.length) {
            this.popoverVisible = false;
            return;
          }
          this.activeLevel = level + 1;
          this.levelItems = children;
        });
      });
    },
    async clampPathToMaxLevel(path) {
      if (!path || !path.length || path.length <= this.maxLevel) {
        return path || [];
      }
      const trimmed = path.slice(0, this.maxLevel);
      const last = trimmed[trimmed.length - 1];
      const id = this.getAreaId(last);
      if (id != null && (!last.name || !last.fullName)) {
        const detail = await this.fetchAreaById(id);
        if (detail) {
          trimmed[trimmed.length - 1] = detail;
        }
      }
      return trimmed;
    },
    async applySelection(item) {
      let path = this.selectedPath.length
        ? this.selectedPath.map((node) => ({ ...node }))
        : [item];
      path = await this.clampPathToMaxLevel(path);
      this.selectedPath = path.map((node) => ({ ...node }));
      this.activeLevel = path.length || 1;
      let last = path[path.length - 1] || item;
      const leafId = this.getAreaId(last);
      if (leafId != null && !last.fullName) {
        const detail = await this.fetchAreaById(leafId);
        if (detail) {
          last = detail;
          if (path.length) {
            path = path.slice();
            path[path.length - 1] = detail;
            this.$set(this.selectedPath, this.selectedPath.length - 1, detail);
          }
        }
      }
      const label = last.fullName || this.buildPathLabel(path);
      const formattedValue = this.formatBoundValue(path, last);
      this.lastSelectionPayload = {
        value: formattedValue,
        row: last,
        label,
        path: path.map((node) => ({ ...node })),
      };
      this.skipValueSync = true;
      this.$emit("input", formattedValue);
      this.$emit("update:labelValue", label);
      this.$nextTick(() => {
        this.skipValueSync = false;
      });
    },
    handleClear() {
      const oldVal = this.value;
      const wasOpen = this.popoverVisible;
      this.lastSelectionPayload = { value: null, row: null, label: null };
      this.skipValueSync = true;
      this.$emit("input", null);
      this.$emit("update:labelValue", null);
      this.resetBrowseState(false);
      this.popoverVisible = false;
      this.$nextTick(() => {
        this.skipValueSync = false;
        if (!wasOpen && !this.isSameBoundValue(oldVal, null)) {
          this.$emit("change", this.lastSelectionPayload);
          this.committedBoundValue = null;
          this.lastSelectionPayload = null;
        }
      });
    },
    resetBrowseState(emitEvent) {
      this.searchKeyword = "";
      this.searchList = [];
      this.selectedPath = [];
      this.activeLevel = 1;
      this.levelItems = [];
      this.committedBrowseSnapshot = null;
      if (emitEvent) {
        this.$emit("input", null);
        this.$emit("update:labelValue", null);
      }
    },
  },
};
</script>

<style lang="scss" scoped>
@import "~@/styles/variables.scss";

.base-area {
  width: 100%;
  display: block;
  box-sizing: border-box;
}

.base-area-trigger {
  display: block;
  width: 100%;
  box-sizing: border-box;
}

.base-area-input {
  width: 100%;
  cursor: pointer;

  .base-area-clear {
    cursor: pointer;
    display: none;
  }

  &:hover .base-area-clear,
  &.is-focus .base-area-clear {
    display: inline-block;
  }
}

.base-area-panel {
  padding: 4px 0 0;
  overflow: visible;
}

.base-area-search {
  position: relative;
  margin-bottom: 8px;
  z-index: 2;
}

.base-area-search-dropdown {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  max-height: 200px;
  overflow-y: auto;
  background: #fff;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  z-index: 20;
}

.base-area-dropdown-empty {
  padding: 12px 10px;
  text-align: center;
  color: #909399;
  font-size: 12px;
}

.base-area-fade-enter-active,
.base-area-fade-leave-active {
  transition: opacity 0.15s ease;
}

.base-area-fade-enter,
.base-area-fade-leave-to {
  opacity: 0;
}

.base-area-result-item {
  padding: 8px 10px;
  font-size: 12px;
  line-height: 1.4;
  color: #606266;
  cursor: pointer;
  border-radius: 4px;

  &:hover {
    background: rgba($baseColor, 0.08);
    color: $baseColor;
  }

  &.on {
    background: rgba($baseColor, 0.12);
    color: $baseColor;
  }
}

.base-area-path {
  padding: 6px 10px;
  margin-bottom: 10px;
  background: #f5f7fa;
  border: 1px solid #ebeef5;
  border-radius: 4px;
}

.base-area-tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  width: 100%;
  gap: 0;
}

.base-area-path-sep {
  flex-shrink: 0;
  padding: 0 2px;
  font-size: 12px;
  color: #c0c4cc;
  line-height: 24px;
  user-select: none;
}

.base-area-tab {
  flex-shrink: 1;
  min-width: 0;
  max-width: 100%;
  padding: 0 4px;
  height: 24px;
  line-height: 24px;
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: transparent;
  font-size: 12px;
  color: #909399;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: color 0.15s, border-color 0.15s;

  &.active {
    color: $baseColor;
    border-bottom-color: $baseColor;
    font-weight: 500;
    background: transparent;
  }

  &.selected:not(.active) {
    color: #303133;
  }

  &.disabled {
    color: #c0c4cc;
    cursor: not-allowed;
    background: transparent;
    pointer-events: none;
  }

  &:hover:not(.disabled):not(.active) {
    color: $baseColor;
  }
}

.base-area-grid-wrap {
  padding: 8px;
  background: #fff;
  border: 1px solid #eee;
  border-radius: 8px;
}

.base-area-grid {
  display: flex;
  flex-wrap: wrap;
  min-height: 120px;
  max-height: 294px;
  overflow-y: auto;
  margin: 0 -4px;
}

.base-area-item {
  width: calc(25% - 8px);
  margin: 0 4px 8px;
  padding: 0 10px;
  height: 34px;
  line-height: 34px;
  border: 1px solid #eee;
  border-radius: 8px;
  font-size: 12px;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;

  &:hover {
    color: $baseColor;
    border-color: $baseColor;
  }

  &.on {
    background: $baseColor;
    border-color: $baseColor;
    color: #fff;
  }
}

.base-area-empty {
  width: 100%;
  padding: 24px 0;
  text-align: center;
  color: #909399;
  font-size: 12px;
}

.base-area-level-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 8px;

  .base-area-nav-btn {
    min-width: 32px;
    padding: 7px 10px;
    border-radius: 8px;
    color: #fff;
    border-color: $baseColor;
    background-color: $baseColor;

    i {
      font-weight: bold;
    }

    &.el-button:focus,
    &.el-button:active {
      color: #fff;
      border-color: $baseColor;
      background-color: $baseColor;
    }

    &.el-button:hover:not(.is-disabled) {
      color: #fff;
      border-color: $baseColor;
      background-color: $baseColor;
      opacity: 0.92;
    }

    &.is-disabled,
    &.el-button.is-disabled {
      color: #c0c4cc;
      border-color: #eee;
      background-color: #f5f7fa;
      opacity: 1;
    }
  }

  .base-area-level-label {
    flex: 1;
    text-align: center;
    font-size: 12px;
    color: #606266;
    line-height: 28px;
    user-select: none;
  }
}
</style>

<style lang="scss">
@import "~@/styles/variables.scss";

/* el-popover 根节点为 inline span，需强制块级铺满 */
.base-area {
  width: 100% !important;
  display: block !important;
  box-sizing: border-box;

  > span {
    display: block !important;
    width: 100% !important;
    box-sizing: border-box;
  }

  .el-popover__reference-wrapper,
  .el-popover__reference,
  .base-area-trigger {
    display: block !important;
    width: 100% !important;
    box-sizing: border-box;
  }

  .base-area-input.el-input {
    width: 100% !important;
    display: block !important;
  }

  .base-area-input .base-area-clear {
    cursor: pointer;
    display: none;
  }

  .base-area-input:hover .base-area-clear,
  .base-area-input.is-focus .base-area-clear {
    display: inline-block;
  }
}

.base-area-popover {
  padding: 10px 12px;
  overflow: visible;
  box-sizing: border-box;

  .base-area-panel {
    width: 100%;
    box-sizing: border-box;
  }

  .base-area-search {
    width: 100%;
    box-sizing: border-box;
  }

  .base-area-search-input,
  .base-area-search .el-input {
    width: 100% !important;
    display: block;
    box-sizing: border-box;
  }

  .base-area-nav-btn.el-button {
    border-radius: 8px;
    color: #fff;
    border-color: $baseColor;
    background-color: $baseColor;
  }

  .base-area-nav-btn.el-button:focus,
  .base-area-nav-btn.el-button:active {
    color: #fff;
    border-color: $baseColor;
    background-color: $baseColor;
  }

  .base-area-nav-btn.el-button.is-disabled {
    color: #c0c4cc;
    border-color: #eee;
    background-color: #f5f7fa;
  }

  .base-area-tab.disabled {
    color: #c0c4cc;
    cursor: not-allowed;
    background: transparent;
    pointer-events: none;
  }
}
</style>
