/**
 * ImportTab Component
 * Handles import functionality for the selected node
 */
Vue.component('import-tab', {
    props: {
        selectedNode: {
            type: Object,
            default: null
        },
        connection: {
            type: Object,
            default: null
        },
        platformType: {
            type: String,
            default: null
        }
    },
    data() {
        return {
            contentTypes: [],
            inSetup: false,
            targetServers: [],
            targetServer: null,
            targetSite: null,
            targetModules: null,
            targetModule: null,
            targetSource: null,
            targetFields: null,
            AllModules: null,
            targetTypes: null,
            targetType: null,
            moduleDetails: null,
            currentStep: 0,
            mappings: null,
            MappingName: "",
            MappingDescription: "",
            existingMappings: null,
            /** "Immediate" | "Recursive" — stored on mapping and used by import runner */
            childrenSelection: "Immediate",
            /** "Normal" | "Explicit" — per import plan item selection mode */
            itemSelectionMode: "Normal",
            /** Sitefinity sf_culture (e.g. en, de). Empty = default language. */
            importCulture: "",
            /** Same field list as Node Details (GetNodeFields); includes regular + system fields */
            nodeDetailFields: [],
            loadingNodeFields: false,
            /** When set, SaveMapping updates this row (same start node). */
            editingMappingId: null,
            /** Populated by GetTypeDetails — API JSON shape varies (camelCase/PascalCase/nesting). */
            targetFieldsCache: [],
            /** True after a full content-type field list was loaded (edit / GetTypeDetails). */
            targetFieldsLoadComplete: false,
            /** When true, targetModule/targetType watchers skip resets and duplicate fetches during EditMapping restore. */
            restoringImportWizard: false,
            /** Edit only: true while GetImportMappingForEdit runs — hides wizard steps until mappings are ready. */
            editImportLoading: false,
            /** Sitefinity: full list from GetAllContentTypes (same as target “Templates with Fields”). */
            sitefinityAllContentTypes: [],
            sitefinityContentTypesLoadedForId: null,
            loadingSitefinityContentTypes: false,
            /** While GetSiteModules is in flight after a site is chosen. */
            loadingSiteModules: false,
            /** Blog posts: parent blog Id (OData <c>blogs</c>) — saved on mapping as ParentId. */
            blogParentId: null,
            blogParentOptions: [],
            loadingBlogParents: false,
            /** After Next → Mappings: until Sitefinity types/fields are ready. */
            loadingMappingsStep: false,
            /** Import run history dialog (per mapping). */
            importLogVisible: false,
            importLogMapping: null,
            importLogRows: [],
            importLogLoading: false,
            /** 'runs' | 'items' | 'steps' — drill-down from log list → items → step detail */
            importLogView: "runs",
            importLogSelectedRun: null,
            importLogRunDetail: null,
            importLogRunItems: [],
            importLogRunItemsLoading: false,
            importLogSelectedItem: null,
            /** Unified ops polling (see long-running-operations.js) */
            importMappingStatusById: {},
            globalRunningImports: [],
            _longRunningUnsub: null,
            editImportProgressMessage: "",
            loadingMappingsProgressMessage: "",
            _editWatchUnsub: null,
            _initWatchUnsub: null,
            /** Sitefinity library providers + default album (per mapping row) for image / related-image fields */
            librarySourcesForSite: [],
            librarySourcesLoading: false,
            libraryFolderDialogOpen: false,
            /** "fieldRow" | "htmlInline" — folder picker for related-image row vs mapping-wide rich-text images */
            libraryFolderDialogMode: "fieldRow",
            libraryFolderDialogRow: null,
            libraryFolderTempProviderKey: "",
            libraryFolderTempAlbumId: "",
            libraryFolderAlbums: [],
            libraryFolderAlbumsLoading: false,
            /** Saved on ChameleonMapping: Sitefinity folder for &lt;img&gt; inside long text / HTML fields */
            htmlInlineImagesLibraryProviderName: "",
            htmlInlineImagesLibraryDataSourceName: "",
            htmlInlineImagesParentAlbumId: "",
            htmlInlineImagesParentAlbumTitle: "",
            htmlInlineImagesParentEntitySet: "albums",
            /** Multi-type plan: assign template per source content type under node */
            useTemplatePlanWizard: true,
            useClassicSingleTypeWizard: false,
            sourceTypesForPlan: [],
            templatePlanRows: [],
            importTemplatesList: [],
            loadingSourceTypesPlan: false,
            templateAssociationsByType: {},
            templateCreateMode: false,
            /** When editing a reusable template from the plan wizard (Save template updates this id). */
            editingTemplateId: 0,
            templateCreateReturnTypeKey: "",
            /** WordPress post_type when creating/editing a per-type template mapping. */
            templateCreateSourceTemplateName: "",
            templatePlanDraftBeforeCreate: null,
            importErrorModalVisible: false,
            importErrorModalTitle: "Cannot save import plan",
            importErrorModalMessage: "",
            importErrorModalDetails: "",
            scriptEditors: {},
            defaultCustomFieldScript:
                "// Globals: Item, SitefinityResult, Target, Source, SourceValue, PlatformType, Logger, Sitefinity\n" +
                "// Docs: docs/import-script-relate-existing-image.md\n" +
                "// Read a source field from Item (BaseModel) by name:\n" +
                "var title = Item.GetValue(\"Title\");\n" +
                "Logger.Info(\"Mapping \" + Item.Name + \", Title=\" + (title ?? \"\"));\n" +
                "if (!string.IsNullOrEmpty(Target))\n" +
                "    SitefinityResult[Target] = string.IsNullOrEmpty(SourceValue) ? title : SourceValue;\n" +
                "// OData lookup: Sitefinity.QueryEntities(entitySet, \"contains(Title,'x')\", provider, culture);\n" +
                "// Pages: map Title + UrlName as field rows; add widgets in a Custom script row:\n" +
                "// Net Core: QueueContentBlockWidget(SourceValue ?? \"\");\n" +
                "// Classic MVC: QueueClassicMvcContentBlockWidget(SourceValue ?? \"\", placeholderId: \"Contentplaceholder1\");\n" +
                "// Sitefinity.FindPage(urlName: \"existing-page\"); Sitefinity.UpsertPage(new SitefinityPageUpsertRequest { ... });\n" +
                "// Link field: SitefinityResult[Target] = CreateLink(href: \"https://…\", text: \"Label\"); or SetLink(href: \"…\");\n" +
                "// Relate image: RelateExistingImage(imageId, \"kaustlibraries\"); see docs/import-script-*.md\n"
        };
    },
    computed: {
        mappingTableColspan() {
            return this.targetSite ? 5 : 4;
        },
        /** Mirrors OverviewTab / Node Details: all fields from API, not only tree node cache */
        sourceFields() {
            if (this.nodeDetailFields && this.nodeDetailFields.length > 0) return this.nodeDetailFields;
            if (this.selectedNode && this.selectedNode.fields && this.selectedNode.fields.length > 0) return this.selectedNode.fields;
            return [];
        },
        targetFieldsList() {
            if (this.targetFieldsCache && this.targetFieldsCache.length) {
                return this.targetFieldsCache;
            }
            const md = this.moduleDetails;
            if (!md) return [];
            return this.extractTargetFieldsFromModuleDetailsResponse(md);
        },
        /** Libraries Image/Document/Video: Sitefinity exposes UrlName + Title */
        isLibraryMediaTarget() {
            if (!this.targetType) return false;
            const itemName = (this.targetType.contentTypeItemName || this.targetType.name || "").toLowerCase();
            const full = (this.targetType.contentTypeName || "").toLowerCase();
            if (itemName.includes("image") || full.includes("libraries.model.image")) return true;
            if (itemName.includes("document") || full.includes("libraries.model.document")) return true;
            if (itemName.includes("video") || full.includes("libraries.model.video")) return true;
            return false;
        },
        /** Sitefinity pages OData target (<code>pages</code> entity set). */
        isPageTarget() {
            const seg = this.pickODataSegmentForSave();
            if (seg === "pages") return true;
            if (!this.targetType) return false;
            const itemName = (this.targetType.contentTypeItemName || this.targetType.name || "").toLowerCase();
            const full = (this.targetType.contentTypeName || this.targetType.fullTypeName || "").toLowerCase();
            const es = (this.targetType.entitySet || this.targetType.EntitySet || "").toLowerCase();
            if (es === "pages") return true;
            if (itemName === "page" || full.indexOf("pages.model.page") >= 0) return true;
            const mod = this.targetModule && (this.targetModule.Title || this.targetModule.title || "");
            if (String(mod).toLowerCase() === "pages") return true;
            return false;
        },
        targetFieldsForMapping() {
            const base = this.targetFieldsList || [];
            if (this.isPageTarget) {
                const seen = new Set();
                for (let i = 0; i < base.length; i++) {
                    const b = base[i];
                    const n = (b.name || b.Name || b.title || b.Title || "").toString().toLowerCase();
                    if (n) seen.add(n);
                }
                const head = [];
                const ensure = (title, name) => {
                    const key = name.toLowerCase();
                    if (!seen.has(key)) {
                        head.push({ title: title, name: name, typeName: "ShortText" });
                        seen.add(key);
                    }
                };
                ensure("Title", "Title");
                ensure("UrlName", "UrlName");
                ensure("ParentId", "ParentId");
                ensure("TemplateName", "TemplateName");
                ensure("ShowInNavigation", "ShowInNavigation");
                return head.length ? head.concat(base) : base;
            }
            if (!this.isLibraryMediaTarget) return base;
            const seen = new Set();
            for (let i = 0; i < base.length; i++) {
                const b = base[i];
                const n = (b.name || b.Name || b.title || b.Title || "").toString().toLowerCase();
                if (n) seen.add(n);
            }
            const head = [];
            if (!seen.has("urlname")) {
                head.push({ title: "UrlName", name: "UrlName", typeName: "ShortText" });
                seen.add("urlname");
            }
            if (!seen.has("title")) {
                head.push({ title: "Title", name: "Title", typeName: "ShortText" });
                seen.add("title");
            }
            return head.length ? head.concat(base) : base;
        },
        /** Alphabetical labels for source field dropdown (mapping step). */
        sourceFieldsSorted() {
            const arr = (this.sourceFields || []).slice();
            const label = (f) => {
                const n = this.sourceFieldLabel(f);
                const ct = (f.contentType || f.ContentType || "").toString();
                const lang = (f.language || f.Language || "").toString();
                return `${n} ${ct} ${lang}`.trim().toLowerCase();
            };
            arr.sort((a, b) => label(a).localeCompare(label(b), undefined, { sensitivity: "base" }));
            return arr;
        },
        /** Alphabetical labels for target field dropdown (mapping step). */
        targetFieldsForMappingSorted() {
            const arr = (this.targetFieldsForMapping || []).slice();
            arr.sort((a, b) =>
                this.targetFieldMappingDropdownLabel(a).localeCompare(
                    this.targetFieldMappingDropdownLabel(b),
                    undefined,
                    { sensitivity: "base" }
                )
            );
            return arr;
        },
        /** Content types for the selected module (from assessment); pool fallback if <code>targetTypes</code> is empty. */
        moduleContentTypesForDropdown() {
            const raw = this.targetTypes && this.targetTypes.items ? this.targetTypes.items : [];
            if (raw.length) return raw;
            return this._moduleContentTypePoolFallback();
        },
        isUmbracoPlatform() {
            const p = (this.platformType || (this.connection && this.connection.platformType) || "").toString().toLowerCase();
            return p === "umbraco";
        },
        isCustomPlatform() {
            const p = (this.platformType || (this.connection && this.connection.platformType) || "").toString().toLowerCase();
            return p === "custom";
        },
        isWordPressPlatform() {
            const p = (this.platformType || (this.connection && this.connection.platformType) || "").toString().toLowerCase();
            return p === "wordpress";
        },
        /** Umbraco, Custom, and WordPress load source fields for the full import scope (not only the selected tree node). */
        usesScopeAggregateFields() {
            return this.isUmbracoPlatform || this.isCustomPlatform || this.isWordPressPlatform;
        },
        /** Multisite module list (handles PascalCase / camelCase). */
        targetModulesDataSources() {
            const tm = this.targetModules;
            if (!tm) return [];
            const ds = tm.DataSources != null ? tm.DataSources : tm.dataSources;
            return Array.isArray(ds) ? ds : [];
        },
        /** Alphabetical module titles for the Import wizard. */
        targetModulesSortedDataSources() {
            const arr = this.targetModulesDataSources.slice();
            const label = (m) => (m.Title || m.title || m.Name || m.name || "").toString();
            arr.sort((a, b) => label(a).localeCompare(label(b), undefined, { sensitivity: "base" }));
            return arr;
        },
        /** Sorted content-type options for the selected module. */
        moduleContentTypesSorted() {
            const raw = this.moduleContentTypesForDropdown || [];
            return raw.slice().sort((a, b) => {
                const x = (a.contentTypeItemName || a.name || a.Name || "").toString();
                const y = (b.contentTypeItemName || b.name || b.Name || "").toString();
                return x.localeCompare(y, undefined, { sensitivity: "base" });
            });
        },
        /** Module dropdown stays disabled until modules + GetAllContentTypes (init) have finished. */
        sitefinityModuleSelectDisabled() {
            if (!this.targetSite) return true;
            if (this.loadingSiteModules || this.loadingSitefinityContentTypes) return true;
            if (!this.targetModules || !this.targetModulesDataSources.length) return true;
            return false;
        },
        /** True when sfmeta indicates a parent item must be chosen (BlogPost→Blog, Image→Album, hierarchical dynamic types, …). */
        needsParentSelection() {
            const tt = this.targetType;
            if (!tt) return false;
            const req = tt.requiresParentSelection != null ? tt.requiresParentSelection : tt.RequiresParentSelection;
            if (req === true) return true;
            if (req === false) return false;
            return !!this.parentEntitySetFromType(tt);
        },
        /** HTML <option> values are strings — bind connection id, not the server object (avoids <code>[object Object]</code>). */
        targetServerConnectionId: {
            get() {
                if (!this.targetServer) return "";
                const id = this.targetServer.id != null ? this.targetServer.id : this.targetServer.Id;
                return id != null ? String(id) : "";
            },
            set(val) {
                if (val === null || val === undefined || val === "" || !this.targetServers || !this.targetServers.length) {
                    this.targetServer = null;
                    return;
                }
                const id = String(val);
                const found = this.targetServers.find((s) => String(s.id != null ? s.id : s.Id) === id);
                this.targetServer = found || null;
            }
        },
        /** Bind module by stable key (DataSourceName / moduleId / title), not list index — avoids 0/1/2 confusion and reorder bugs. */
        targetModuleSelectKey: {
            get() {
                if (!this.targetModule) return "";
                return this.moduleSelectKey(this.targetModule);
            },
            set(val) {
                if (val === null || val === undefined || val === "") {
                    this.targetModule = null;
                    return;
                }
                const k = String(val);
                const ds = this.targetModulesSortedDataSources;
                const found = ds.find((m) => this.moduleSelectKey(m) === k);
                this.targetModule = found || null;
            }
        },
        /** Bind content type by fullTypeName or id — stable across filtering. */
        targetTypeSelectKey: {
            get() {
                if (!this.targetType) return "";
                return this.contentTypeSelectValue(this.targetType);
            },
            set(val) {
                if (val === null || val === undefined || val === "") {
                    this.targetType = null;
                    return;
                }
                const k = String(val);
                const list = this.moduleContentTypesSorted;
                const found = list.find((t) => this.contentTypeSelectValue(t) === k);
                this.targetType = found || null;
            }
        },
        wizardSteps() {
            if (this.templateCreateMode) {
                return ["Server Info", "Mappings"];
            }
            return this.useTemplatePlanWizard ? ["Plan & scope", "Assign templates"] : ["Server Info", "Mappings"];
        },
        /** Step 1 field-mapping grid (classic import or edit/create reusable template). */
        showFieldMappingsStep() {
            return this.currentStep === 1 && (this.templateCreateMode || !this.useTemplatePlanWizard);
        },
        saveButtonLabel() {
            if (this.templateCreateMode) {
                return this.editingTemplateId ? "Update template" : "Save template";
            }
            return this.useTemplatePlanWizard ? "Save import plan" : "Save Mappings";
        }
    },
    watch: {
        selectedNode: {
            immediate: true,
            handler(newVal) {
                this.LoadMappings();
                this.loadNodeFieldsForImport();
            }
        },
        connection: {
            immediate: true,
            handler() {
                this.loadNodeFieldsForImport();
                this.LoadMappings();
            }
        },
        existingMappings: {
            handler(val) {
                if (val && val.length) this.startLongRunningPolling();
                else this.stopLongRunningPolling();
            }
        },
        targetServer: {
            handler(newVal, oldVal) {
                const nid = newVal && (newVal.id != null ? newVal.id : newVal.Id);
                const oid = oldVal && (oldVal.id != null ? oldVal.id : oldVal.Id);
                if (nid !== oid) {
                    this.sitefinityAllContentTypes = [];
                    this.sitefinityContentTypesLoadedForId = null;
                    if (!newVal) {
                        this.targetSite = null;
                    } else if (!this.restoringImportWizard && this.serverSites(newVal).length) {
                        const first = this.serverSites(newVal)[0];
                        this.targetSite = first.Id != null ? first.Id : first.id;
                    } else if (!this.restoringImportWizard && !this.serverSites(newVal).length) {
                        this.targetSite = null;
                    }
                }
            }
        },
        targetSite: {
            immediate: true,
            handler(newVal) {
                if (!newVal) {
                    this.targetModules = null;
                    this.targetModule = null;
                    this.blogParentId = null;
                    this.blogParentOptions = [];
                    return;
                }
                if (this.restoringImportWizard) return;
                this.targetModule = null;
                this.targetModules = null;
                this.targetType = null;
                this.targetSource = null;
                this.moduleDetails = null;
                this.targetFieldsCache = [];
                this.blogParentId = null;
                this.blogParentOptions = [];
                this.librarySourcesForSite = [];
                this.GetSiteModules(newVal);
            }
        },
        targetSource: {
            handler() {
                if (this.restoringImportWizard) return;
                if (this.needsParentSelection) this.$nextTick(() => this.loadParentOptions());
            }
        },
        targetModule: {
            immediate: true,
            handler(newVal) {
                if (newVal) {
                    if (this.restoringImportWizard) return;
                    this.targetType = null;
                    this.targetSource = null;
                    this.moduleDetails = null;
                    this.targetFieldsCache = [];
                    this.GetModuleDetails();
                }
            }
        },
        targetType: {
            immediate: true,
            handler(newVal) {
                if (!this.restoringImportWizard) {
                    this.targetFieldsLoadComplete = false;
                }
                if (newVal) {
                    if (!this.restoringImportWizard) {
                        this.loadTargetFieldsForCurrentType(null);
                    }
                }
                if (!this.needsParentSelection) {
                    if (!this.restoringImportWizard) {
                        this.blogParentId = null;
                        this.blogParentOptions = [];
                    }
                } else if (!this.restoringImportWizard) {
                    this.$nextTick(() => this.loadParentOptions());
                }
            }
        },
        currentStep(val, oldVal) {
            if (val === 0 && oldVal === 1 && this.inSetup) {
                var self = this;
                this.$nextTick(function () { self.rebindWizardTargetSelections(); });
            }
        },
        childrenSelection: {
            handler() {
                if (this.usesScopeAggregateFields) this.loadNodeFieldsForImport();
            }
        },
        templateCreateReturnTypeKey() {
            if (this.isWordPressPlatform && this.templateCreateMode) {
                this.loadNodeFieldsForImport();
            }
        },
        useClassicSingleTypeWizard(val) {
            this.useTemplatePlanWizard = !val;
            // Only reset step when switching *into* classic mode. Turning classic off (e.g. after
            // "Create new mapping" save) must not jump back to step 0 — that hid the Assign templates screen.
            // Skip reset when opening template edit (we land on Mappings after load).
            if (this.inSetup && val && !(this.templateCreateMode && this.editingTemplateId)) {
                this.currentStep = 0;
            }
        },
        /** Monaco hosts exist only on the Mappings step — remount when that step becomes visible. */
        showFieldMappingsStep(val) {
            if (val) {
                if (this.isWordPressPlatform) {
                    this.loadNodeFieldsForImport();
                }
                var self = this;
                this.$nextTick(function () {
                    self.$nextTick(function () { self.remountAllScriptEditors(); });
                });
            } else {
                this.disposeAllScriptEditors();
            }
        }
    },
    destroyed() {
        this.stopLongRunningPolling();
        this.unwatchEditMappingProgress();
        this.unwatchSitefinityInitProgress();
        this.disposeAllScriptEditors();
    },
    methods: {
        /** Grep server console or browser devtools: CHAM-SF-SCHEMA */
        _sfSchemaLog(event, payload) {
            const msg = "[CHAM-SF-SCHEMA] " + event;
            if (payload !== undefined) console.log(msg, payload);
            else console.log(msg);
        },
        _sfSchemaSummarizeModule(m) {
            if (!m) return {};
            const links = m.Links || m.links;
            const firstLink = Array.isArray(links) && links.length ? links[0] : null;
            let firstDs = "";
            if (firstLink) {
                const ds = firstLink.DataSourceName != null ? firstLink.DataSourceName : firstLink.dataSourceName;
                if (ds != null && String(ds).trim() !== "") firstDs = String(ds).trim();
            }
            if (!firstDs) {
                const t = m.Title != null ? m.Title : m.title;
                if (t != null && String(t).trim() !== "") firstDs = String(t).trim();
            }
            return {
                title: m.Title || m.title || m.Name || m.name || "",
                name: m.Name || m.name || "",
                firstLinkDataSourceName: firstDs,
                moduleId: m.moduleId != null ? m.moduleId : m.ModuleId,
                linkCount: Array.isArray(links) ? links.length : 0
            };
        },
        showImportErrorModal(message, title, details) {
            this.importErrorModalTitle = title || "Cannot save import plan";
            this.importErrorModalMessage = message || "";
            this.importErrorModalDetails = details || "";
            this.importErrorModalVisible = true;
        },
        closeImportErrorModal() {
            this.importErrorModalVisible = false;
        },
        collectUnassignedTemplatePlanTypeLabels() {
            const rows = this.templatePlanRows || [];
            const missing = [];
            for (let i = 0; i < rows.length; i++) {
                const row = rows[i];
                const a = row.assignment;
                if (a === "" || a == null) {
                    const label = (row.sourceTemplateName || row.sourceTemplateId || "").trim();
                    missing.push(label || "Unknown type");
                }
            }
            return missing;
        },
        formatTemplatePlanMissingAssignmentsMessage(missing) {
            if (!missing.length) return "";
            if (missing.length === 1) {
                return "Choose an import action (reusable template or skip) for this source content type:\n\n• " + missing[0];
            }
            return "Choose an import action (reusable template or skip) for every source content type listed below:\n\n• " + missing.join("\n• ");
        },
        _sfSchemaSummarizeContentType(ct) {
            if (!ct) return {};
            const fields = ct.fields != null ? ct.fields : ct.Fields;
            return {
                name: ct.name != null ? ct.name : ct.Name,
                fullTypeName: ct.fullTypeName != null ? ct.fullTypeName : ct.FullTypeName,
                moduleName: ct.moduleName != null ? ct.moduleName : ct.ModuleName,
                moduleId: ct.moduleId != null ? ct.moduleId : ct.ModuleId,
                entitySet: ct.entitySet != null ? ct.entitySet : ct.EntitySet,
                contentTypeId: ct.contentTypeId != null ? ct.contentTypeId : ct.ContentTypeId,
                contentTypeItemName: ct.contentTypeItemName != null ? ct.contentTypeItemName : ct.ContentTypeItemName,
                fieldCount: Array.isArray(fields) ? fields.length : 0
            };
        },
        _sfSchemaLogContentTypeList(event, list) {
            const arr = Array.isArray(list) ? list : [];
            this._sfSchemaLog(event, { count: arr.length, types: arr.map((t) => this._sfSchemaSummarizeContentType(t)) });
        },
        /** Stable <option> value for a multisite module row (aligns with pickDataSourceNameForModule / filter). */
        moduleSelectKey(m) {
            if (!m) return "";
            const links = m.Links || m.links;
            if (Array.isArray(links) && links.length > 0) {
                const l0 = links[0];
                const ds = l0 && (l0.DataSourceName != null ? l0.DataSourceName : l0.dataSourceName);
                if (ds != null && String(ds).trim() !== "") return "ds:" + String(ds).trim();
            }
            const mid = m.moduleId != null ? m.moduleId : m.ModuleId;
            if (mid != null && String(mid).trim() !== "") return "mid:" + String(mid).trim();
            const title = (m.Title || m.Name || m.title || m.name || "").toString().trim();
            if (title) return "title:" + title.toLowerCase();
            return "";
        },
        contentTypeSelectValue(t) {
            if (!t) return "";
            const fn = t.fullTypeName != null ? t.fullTypeName : t.FullTypeName;
            if (fn != null && String(fn).trim() !== "") return String(fn).trim();
            const id = t.contentTypeId != null ? t.contentTypeId : t.ContentTypeId;
            if (id != null && String(id).trim() !== "") return "id:" + String(id).trim();
            const cn = t.contentTypeName != null ? t.contentTypeName : t.ContentTypeName;
            if (cn != null && String(cn).trim() !== "") return String(cn).trim();
            const item = t.contentTypeItemName != null ? t.contentTypeItemName : t.ContentTypeItemName;
            if (item != null && String(item).trim() !== "") return String(item).trim();
            const nm = t.name != null ? t.name : t.Name;
            if (nm != null && String(nm).trim() !== "") return String(nm).trim();
            return "";
        },
        moduleLabelFromModule(m) {
            if (!m) return "";
            const s = (m.Title || m.title || m.Name || m.name || "").toString().trim();
            return s || "Module";
        },
        sourceConnectionId() {
            if (!this.connection) return null;
            return this.connection.id != null ? this.connection.id : this.connection.Id;
        },
        sourceNodeId() {
            if (!this.selectedNode) return "";
            return this.selectedNode.id != null ? this.selectedNode.id : this.selectedNode.Id;
        },
        /** WordPress post_type filter for import wizard source fields (per-type template mapping). */
        wordPressImportSourcePostType() {
            const explicit = (this.templateCreateSourceTemplateName || "").toString().trim();
            if (explicit) return explicit;
            const key = (this.templateCreateReturnTypeKey || "").toString().trim().toLowerCase();
            if (key.startsWith("name:")) return key.substring(5).trim();
            return "";
        },
        sourceFieldLabel(f) {
            if (!f) return "";
            return (f.name || f.Name || f.title || f.Title || "").toString();
        },
        normalizeSourceFieldRows(fields) {
            const rows = [];
            const seen = new Set();
            for (let i = 0; i < (fields || []).length; i++) {
                const f = fields[i] || {};
                const name = (f.name || f.Name || f.title || f.Title || "").toString().trim();
                if (!name) continue;
                const key = name.toLowerCase();
                if (seen.has(key)) continue;
                seen.add(key);
                rows.push({
                    name: name,
                    title: (f.title || f.Title || name).toString(),
                    contentType: (f.contentType || f.ContentType || "").toString(),
                    language: (f.language || f.Language || "").toString(),
                    value: f.value != null ? f.value : f.Value
                });
            }
            return rows;
        },
        mergeSourceFieldRows(primary, extra) {
            const merged = (primary || []).slice();
            const seen = new Set();
            for (let i = 0; i < merged.length; i++) {
                const n = (merged[i].name || "").toLowerCase();
                if (n) seen.add(n);
            }
            for (let j = 0; j < (extra || []).length; j++) {
                const f = extra[j];
                const n = (f.name || "").toLowerCase();
                if (!n || seen.has(n)) continue;
                seen.add(n);
                merged.push(f);
            }
            return merged;
        },
        wordPressHasCustomSourceFields(fields) {
            for (let i = 0; i < (fields || []).length; i++) {
                const ct = (fields[i].contentType || "").toString().toLowerCase();
                if (ct && ct !== "system") return true;
            }
            return false;
        },
        isWordPressPostNodeId(nodeId) {
            return /^\d+$/.test(String(nodeId || "").trim());
        },
        serverSites(server) {
            if (!server) return [];
            const parsed = server.parsedResponse || server.ParsedResponse || null;
            if (!parsed) return [];
            const items = parsed.Items != null ? parsed.Items : parsed.items;
            return Array.isArray(items) ? items : [];
        },
        looksLikeSitefinityFieldObject(o) {
            if (!o || typeof o !== "object") return false;
            return o.name != null || o.Name != null || o.title != null || o.Title != null;
        },
        extractTargetFieldsFromModuleDetailsResponse(md) {
            if (!md || typeof md !== "object") return [];
            var r = md.result != null ? md.result : md.Result;
            if (r && typeof r === "object") {
                var ti = r.typeInfo != null ? r.typeInfo : r.TypeInfo;
                if (ti && typeof ti === "object") {
                    var fields = ti.fields != null ? ti.fields : ti.Fields;
                    if (Array.isArray(fields) && fields.length) return fields;
                    var inner = ti.typeInfo != null ? ti.typeInfo : ti.TypeInfo;
                    if (inner && typeof inner === "object") {
                        fields = inner.fields != null ? inner.fields : inner.Fields;
                        if (Array.isArray(fields) && fields.length) return fields;
                    }
                }
                var rf = r.fields != null ? r.fields : r.Fields;
                if (Array.isArray(rf) && rf.length) return rf;
            }
            var topF = md.fields != null ? md.fields : md.Fields;
            if (Array.isArray(topF) && topF.length) return topF;
            var stack = [md];
            var seen = [];
            while (stack.length) {
                var cur = stack.pop();
                if (!cur || typeof cur !== "object") continue;
                if (seen.indexOf(cur) !== -1) continue;
                seen.push(cur);
                if (Array.isArray(cur)) {
                    if (cur.length && this.looksLikeSitefinityFieldObject(cur[0])) return cur;
                    for (var ai = 0; ai < cur.length; ai++) stack.push(cur[ai]);
                    continue;
                }
                var keys = Object.keys(cur);
                for (var ki = 0; ki < keys.length; ki++) {
                    var v = cur[keys[ki]];
                    if (Array.isArray(v) && v.length && this.looksLikeSitefinityFieldObject(v[0])) return v;
                    if (v && typeof v === "object") stack.push(v);
                }
            }
            return [];
        },
        SetupImport() {
            this.editingMappingId = null;
            this.templateCreateMode = false;
            this.editingTemplateId = 0;
            this.templateCreateReturnTypeKey = "";
            this.templateCreateSourceTemplateName = "";
            this.templatePlanDraftBeforeCreate = null;
            this.childrenSelection = "Immediate";
            this.itemSelectionMode = "Normal";
            this.importCulture = "";
            this.targetServer = null;
            this.targetSite = null;
            this.targetModules = null;
            this.targetModule = null;
            this.targetSource = null;
            this.targetFields = null;
            this.AllModules = null;
            this.targetTypes = null;
            this.targetType = null;
            this.moduleDetails = null;
            this.targetFieldsCache = [];
            this.targetFieldsLoadComplete = false;
            this.sitefinityAllContentTypes = [];
            this.sitefinityContentTypesLoadedForId = null;
            this.blogParentId = null;
            this.blogParentOptions = [];
            this.mappings = null;
            this.MappingName = "";
            this.MappingDescription = "";
            this.currentStep = 0;
            this.editImportLoading = false;
            this.inSetup = true;
            this.htmlInlineImagesLibraryProviderName = "";
            this.htmlInlineImagesLibraryDataSourceName = "";
            this.htmlInlineImagesParentAlbumId = "";
            this.htmlInlineImagesParentAlbumTitle = "";
            this.htmlInlineImagesParentEntitySet = "albums";
            this.useTemplatePlanWizard = true;
            this.useClassicSingleTypeWizard = false;
            this.templatePlanRows = [];
            this.sourceTypesForPlan = [];
            this.importTemplatesList = [];
            this.templateAssociationsByType = {};
            this.loadNodeFieldsForImport();
            this.GetSitefinityConnections();
        },
        async loadNodeFieldsForImport() {
            if (!this.selectedNode || !this.connection) {
                this.nodeDetailFields = [];
                return;
            }
            const nodeId = this.selectedNode.id || this.selectedNode.Id;
            const connId = this.connection.id || this.connection.Id;
            if (!nodeId || connId == null) {
                this.nodeDetailFields = [];
                return;
            }
            this.loadingNodeFields = true;
            try {
                if (this.isUmbracoPlatform) {
                    const scope = this.childrenSelection === "Recursive" ? "Recursive" : "Immediate";
                    const res = await fetch(
                        `/api/ContentTree/GetUmbracoImportAggregateFields?nodeId=${encodeURIComponent(nodeId)}&childrenScope=${encodeURIComponent(scope)}&connectionId=${encodeURIComponent(connId)}`
                    );
                    const data = await res.json();
                    if (data.success === false) {
                        this.nodeDetailFields = [];
                    } else {
                        this.nodeDetailFields = data.fields || [];
                    }
                    return;
                }
                if (this.isCustomPlatform) {
                    const scope = this.childrenSelection === "Recursive" ? "Recursive" : "Immediate";
                    const res = await fetch(
                        `/api/ContentTree/GetCustomImportAggregateFields?nodeId=${encodeURIComponent(nodeId)}&childrenScope=${encodeURIComponent(scope)}&connectionId=${encodeURIComponent(connId)}`
                    );
                    const data = await res.json();
                    if (data.success === false || data.Success === false) {
                        this.nodeDetailFields = [];
                    } else {
                        this.nodeDetailFields = data.fields || data.Fields || [];
                    }
                    return;
                }
                if (this.isWordPressPlatform) {
                    const scope = this.childrenSelection === "Recursive" ? "Recursive" : "Immediate";
                    const postType = this.wordPressImportSourcePostType();
                    let aggregateNodeId = nodeId;
                    if (postType) {
                        aggregateNodeId = "type:" + postType;
                    }
                    let wpUrl =
                        `/api/ContentTree/GetWordPressImportAggregateFields?nodeId=${encodeURIComponent(aggregateNodeId)}&childrenScope=${encodeURIComponent(scope)}&connectionId=${encodeURIComponent(connId)}`;
                    if (postType) {
                        wpUrl += `&sourcePostType=${encodeURIComponent(postType)}`;
                    }
                    const res = await fetch(wpUrl);
                    const data = await res.json();
                    if (data.success === false || data.Success === false) {
                        this.nodeDetailFields = [];
                        const msg = data.message || data.Message;
                        if (msg && typeof Helpers !== "undefined") {
                            Helpers.showNotification("Could not load WordPress source fields: " + msg, "error");
                        }
                    } else {
                        let fields = this.normalizeSourceFieldRows(data.fields || data.Fields || []);
                        const needsFallback =
                            !fields.length ||
                            (!this.wordPressHasCustomSourceFields(fields) &&
                                (this.isWordPressPostNodeId(nodeId) || !!postType));
                        if (needsFallback && this.isWordPressPostNodeId(nodeId)) {
                            const platformToUse = this.platformType || this.connection.platformType || "WordPress";
                            const fieldsUrl =
                                `/api/ContentTree/GetNodeFields?id=${encodeURIComponent(nodeId)}&platformType=${encodeURIComponent(platformToUse)}&connectionId=${encodeURIComponent(connId)}`;
                            const resNode = await fetch(fieldsUrl);
                            const dataNode = await resNode.json();
                            if (dataNode.success !== false && dataNode.Success !== false) {
                                const nodeFields = this.normalizeSourceFieldRows(dataNode.fields || dataNode.Fields || []);
                                fields = this.mergeSourceFieldRows(fields, nodeFields);
                            }
                        }
                        this.nodeDetailFields = fields;
                    }
                    return;
                }
                const platformToUse = this.platformType || this.connection.platformType || "SharePoint";
                const fieldsUrl = typeof Helpers !== "undefined" && Helpers.buildGetNodeFieldsUrl
                    ? Helpers.buildGetNodeFieldsUrl(nodeId, platformToUse, connId, this.selectedNode)
                    : `/api/ContentTree/GetNodeFields?id=${encodeURIComponent(nodeId)}&platformType=${encodeURIComponent(platformToUse)}&connectionId=${encodeURIComponent(connId)}`;
                const res = await fetch(fieldsUrl);
                const data = await res.json();
                if (data.success === false || data.Success === false) {
                    this.nodeDetailFields = (this.selectedNode.fields && this.selectedNode.fields.slice()) || [];
                } else {
                    this.nodeDetailFields = this.normalizeSourceFieldRows(data.fields || data.Fields || []);
                }
            } catch (e) {
                console.error("loadNodeFieldsForImport:", e);
                this.nodeDetailFields = (this.selectedNode.fields && this.selectedNode.fields.slice()) || [];
            } finally {
                this.loadingNodeFields = false;
            }
        },
        /** When <code>targetTypes</code> is empty, rebuild from merged assessment list + module filter. */
        _moduleContentTypePoolFallback() {
            if (!this.sitefinityAllContentTypes || !this.sitefinityAllContentTypes.length || !this.targetModule) return [];
            const filtered = this.filterContentTypesForModule(this.sitefinityAllContentTypes, this.targetModule);
            return filtered.map((t) => this.mapCtToDropdownRow(t));
        },
        /** If the module exposes exactly one content type, select it (convenience). */
        ensureSingleContentTypeSelected() {
            if (this.restoringImportWizard) return;
            const list = this.moduleContentTypesSorted;
            if (!list || list.length !== 1) return;
            if (!this.targetType) this.targetType = list[0];
        },
        async goNextStep() {
            if (this.useTemplatePlanWizard && this.currentStep === 0) {
                const connId = this.sourceConnectionId();
                const nodeId = this.sourceNodeId();
                if (connId == null || nodeId === "") {
                    Helpers.showNotification("Connect and select a tree node first.", "error");
                    return;
                }
                if (!this.MappingName || !this.MappingName.trim()) {
                    Helpers.showNotification("Enter a mapping name.", "error");
                    return;
                }
                await this.loadTemplatePlanStepData();
                this.currentStep++;
                if (this.usesScopeAggregateFields) {
                    this.$nextTick(() => this.loadNodeFieldsForImport());
                }
                return;
            }
            if (this.currentStep === 0 && this.needsParentSelection) {
                if (!this.targetSource) {
                    Helpers.showNotification("Select a provider for the target content type.", "error");
                    return;
                }
                if (!this.blogParentId) {
                    Helpers.showNotification("Select a parent item for the target content type.", "error");
                    return;
                }
            }
            if (this.currentStep === 0 && this.targetServer && this.targetSite) {
                if (this.loadingSiteModules || this.loadingSitefinityContentTypes) {
                    Helpers.showNotification("Wait for modules and Sitefinity content types to finish loading.", "warning");
                    return;
                }
                if (!this.targetModule) {
                    Helpers.showNotification("Select a Sitefinity module.", "error");
                    return;
                }
                if (this.targetModule && this.moduleContentTypesSorted.length && !this.targetType) {
                    Helpers.showNotification("Select a content type before continuing to mappings.", "error");
                    return;
                }
                this.loadingMappingsStep = true;
                const tsId = this.targetServerId();
                this.watchSitefinityInitProgress(tsId);
                try {
                    await this.prepareSitefinityBeforeMappings();
                } finally {
                    this.loadingMappingsStep = false;
                    this.unwatchSitefinityInitProgress();
                }
            }
            this.currentStep++;
            if (this.usesScopeAggregateFields) {
                this.$nextTick(() => this.loadNodeFieldsForImport());
            }
        },
        /** Ensures assessment cache + module/type field lists before showing the Mappings step. */
        async prepareSitefinityBeforeMappings() {
            this.targetFieldsLoadComplete = false;
            await this.ensureSitefinityAllContentTypes();
            if (this.targetModule) {
                await this.GetModuleDetails();
            }
            this.rebindWizardTargetSelections();
            if (this.targetType) {
                await this.loadTargetFieldsForCurrentType(null);
            }
            if (this.targetSite && this.targetServerId() != null) {
                await this.loadLibrarySourcesForSite();
            }
        },
        resolveTargetServerByConnectionId(connectionId) {
            if (connectionId == null || connectionId === "") return null;
            const id = String(connectionId);
            const list = this.targetServers || [];
            if (!list.length) return null;
            return list.find((s) => String(s.id != null ? s.id : s.Id) === id) || null;
        },
        async GetSitefinityConnections() {
            try {
                const connections = await fetch("/api/SitefinityConnectivity/GetAvailableConnectionStrings");
                if (!connections.ok) {
                    console.warn("GetSitefinityConnections: HTTP", connections.status);
                    return;
                }
                const json = await connections.json();
                const list = Array.isArray(json) ? json : (json.result || json.Result || []);
                for (let i = 0; i < list.length; i++) {
                    const serverToProcess = list[i];
                    try {
                        const raw = serverToProcess.rawSiteResponse != null
                            ? serverToProcess.rawSiteResponse
                            : serverToProcess.RawSiteResponse;
                        if (!raw || typeof raw !== "string") continue;
                        const trimmed = raw.trim();
                        if (!trimmed || trimmed.charAt(0) === "<") {
                            serverToProcess.parsedResponse = null;
                            continue;
                        }
                        const jsonResult = JSON.parse(trimmed);
                        if (jsonResult) {
                            serverToProcess.parsedResponse = jsonResult;
                        }
                    } catch (err) {
                        console.warn("Could not parse site response for server", serverToProcess.name || serverToProcess.Name, err);
                        serverToProcess.parsedResponse = null;
                    }
                }

                this.targetServers = list;
            } catch (error) {
                console.error('Error fetching connections:', error);
                Helpers.showNotification('Error loading connections', 'error');
            }
        },
        async GetSiteModules(siteId) {
            this.loadingSiteModules = true;
            try {
                const connId = this.targetServer && (this.targetServer.id != null ? this.targetServer.id : this.targetServer.Id);
                const modulesJson = await fetch(`/api/SitefinityConnectivity/GetSiteModules?siteId=${siteId}&connectionId=${connId}`).then((r) => r.json());
                this.targetModules = JSON.parse(modulesJson.result);
                const ds = this.targetModules && (this.targetModules.DataSources || this.targetModules.dataSources);
                const moduleArr = Array.isArray(ds) ? ds : [];
                this._sfSchemaLog("import-site-modules-loaded", {
                    connectionId: connId,
                    siteId: siteId,
                    moduleCount: moduleArr.length,
                    modules: moduleArr.map((m) => this._sfSchemaSummarizeModule(m))
                });
                await this.fetchSitefinityAllContentTypes();
            } catch (error) {
                console.error('Error fetching site modules:', error);
                Helpers.showNotification('Error loading site modules', 'error');
                this.targetModules = null;
            } finally {
                this.loadingSiteModules = false;
            }
        },
        /** Same shape as GetSiteModuleDetails API response (wrapper with result). */
        applySiteModuleDetailsFromFetchJson(json) {
            const r = json && json.result ? json.result : {};
            const ct = r.contentTypes || r.ContentTypes;
            const md = r.moduleDetails || r.ModuleDetails;
            if (ct) {
                const arr = ct.items || ct.Items;
                const list = Array.isArray(arr) ? arr : [];
                this.targetTypes = { items: list };
            } else {
                this.targetTypes = { items: [] };
            }
            if (md) {
                const modItems = md.items || md.Items;
                this.AllModules = modItems != null ? Object.assign({}, md, { items: modItems }) : md;
            }
        },
        targetServerId() {
            if (!this.targetServer) return null;
            return this.targetServer.id != null ? this.targetServer.id : this.targetServer.Id;
        },
        async fetchSitefinityAllContentTypes() {
            const tsId = this.targetServerId();
            if (tsId == null) return;
            this.loadingSitefinityContentTypes = true;
            try {
                const res = await fetch(`/api/SitefinityConnectivity/GetAllContentTypes?connectionId=${encodeURIComponent(tsId)}`);
                const json = await res.json();
                const raw = json.contentTypes != null ? json.contentTypes : json.ContentTypes;
                this.sitefinityAllContentTypes = Array.isArray(raw) ? raw : [];
                this.sitefinityContentTypesLoadedForId = tsId;
                this._sfSchemaLog("import-all-content-types-loaded", {
                    connectionId: tsId,
                    targetSite: this.targetSite,
                    count: this.sitefinityAllContentTypes.length
                });
                this._sfSchemaLogContentTypeList("import-all-content-types", this.sitefinityAllContentTypes);
            } catch (e) {
                console.error("fetchSitefinityAllContentTypes", e);
                this.sitefinityAllContentTypes = [];
                Helpers.showNotification("Could not load Sitefinity content types.", "error");
            } finally {
                this.loadingSitefinityContentTypes = false;
            }
        },
        async ensureSitefinityAllContentTypes() {
            const tsId = this.targetServerId();
            if (tsId == null) return;
            if (this.sitefinityContentTypesLoadedForId === tsId && this.sitefinityAllContentTypes.length > 0) return;
            await this.fetchSitefinityAllContentTypes();
        },
        /** OData segment for the parent collection (from metadata <c>Parent</c> binding target). */
        parentEntitySetFromType(tt) {
            if (!tt) return "";
            const pe = tt.parentEntitySet != null ? tt.parentEntitySet : tt.ParentEntitySet;
            return pe != null ? String(pe).trim() : "";
        },
        async loadParentOptions() {
            const pe = this.parentEntitySetFromType(this.targetType);
            if (!pe || !this.targetSite || !this.targetSource) {
                this.blogParentOptions = [];
                return;
            }
            const cid = this.targetServerId();
            if (cid == null) return;
            this.loadingBlogParents = true;
            try {
                const u = "/api/SitefinityConnectivity/GetParentList?connectionId=" + encodeURIComponent(cid)
                    + "&siteId=" + encodeURIComponent(this.targetSite)
                    + "&providerName=" + encodeURIComponent(this.targetSource)
                    + "&entitySet=" + encodeURIComponent(pe);
                const res = await fetch(u);
                if (!res.ok) {
                    this.blogParentOptions = [];
                    return;
                }
                const json = await res.json();
                const raw = json.blogs != null ? json.blogs : json.Blogs;
                const arr = Array.isArray(raw) ? raw : [];
                this.blogParentOptions = arr.map((b) => ({
                    id: b.id != null ? b.id : b.Id,
                    title: (b.title != null ? b.title : b.Title) || ""
                }));
            } catch (e) {
                console.error("loadParentOptions", e);
                this.blogParentOptions = [];
            } finally {
                this.loadingBlogParents = false;
            }
        },
        /** Map assessment row to import dropdown shape (contentTypeId / contentTypeItemName / contentTypeName). */
        mapCtToDropdownRow(ct) {
            const full = (ct.fullTypeName != null ? ct.fullTypeName : ct.FullTypeName) || "";
            const name = (ct.name != null ? ct.name : ct.Name) || "";
            const ctid = ct.contentTypeId != null ? ct.contentTypeId : ct.ContentTypeId;
            const ctin = ct.contentTypeItemName != null ? ct.contentTypeItemName : ct.ContentTypeItemName;
            const label = (ctin && String(ctin).trim() !== "") ? ctin : (name || full);
            const peRaw = ct.parentEntitySet != null ? ct.parentEntitySet : ct.ParentEntitySet;
            const peNorm = peRaw != null && String(peRaw).trim() !== "" ? String(peRaw).trim() : null;
            const pft = ct.parentFullTypeName != null ? ct.parentFullTypeName : ct.ParentFullTypeName;
            const pftNorm = pft != null && String(pft).trim() !== "" ? String(pft).trim() : null;
            const rps = ct.requiresParentSelection != null ? ct.requiresParentSelection : ct.RequiresParentSelection;
            const o = Object.assign({}, ct, {
                contentTypeId: ctid,
                ContentTypeId: ctid,
                contentTypeItemName: label,
                ContentTypeItemName: label,
                contentTypeName: full,
                ContentTypeName: full,
                name: name || full,
                Name: name || full,
                parentEntitySet: peNorm,
                ParentEntitySet: peNorm,
                parentFullTypeName: pftNorm,
                ParentFullTypeName: pftNorm
            });
            if (rps === true || rps === false) {
                o.requiresParentSelection = rps;
                o.RequiresParentSelection = rps;
            }
            return o;
        },
        /**
         * Restrict GetAllContentTypes list to the selected module (Blogs, News, Libraries, …).
         * Prefer same moduleId after matching by OData entity set or title/fullTypeName.
         */
        filterContentTypesForModule(all, targetModule) {
            if (!all || !all.length || !targetModule) {
                this._sfSchemaLog("import-filter-skipped", {
                    reason: !all || !all.length ? "no-assessment-types" : "no-target-module",
                    allCount: all ? all.length : 0
                });
                return [];
            }
            const title = (targetModule.Title || targetModule.Name || "").trim();
            const titleLower = title.toLowerCase();
            const pick = this.pickDataSourceNameForModule();
            const pickNorm = pick.toLowerCase().replace(/\s+/g, "");

            const byModuleName = all.filter((t) => {
                const mn = String((t.moduleName != null ? t.moduleName : t.ModuleName) ?? "").trim().toLowerCase();
                if (!mn) return false;
                if (pickNorm && mn.replace(/\s+/g, "") === pickNorm) return true;
                if (titleLower && mn === titleLower) return true;
                return false;
            });
            let bucket = byModuleName;
            let stage = "moduleName";
            if (!bucket.length) {
                bucket = all.filter((t) => {
                    const es = String((t.entitySet != null ? t.entitySet : t.EntitySet) ?? "").toLowerCase().replace(/\s+/g, "");
                    if (!pickNorm || !es) return false;
                    if (es === pickNorm) return true;
                    if (es + "s" === pickNorm || pickNorm + "s" === es) return true;
                    return es === pickNorm.replace(/s$/, "") || pickNorm === es.replace(/s$/, "");
                });
                stage = "entitySet";
            }
            if (!bucket.length && title) {
                const tl = title.toLowerCase();
                bucket = all.filter((t) => {
                    const fn = String((t.fullTypeName != null ? t.fullTypeName : t.FullTypeName) ?? "").toLowerCase();
                    const nm = String((t.name != null ? t.name : t.Name) ?? "").toLowerCase();
                    return fn.includes(tl) || nm.includes(tl);
                });
                stage = "titleSubstring";
            }
            if (!bucket.length && title) {
                const tl = title.toLowerCase();
                bucket = all.filter((t) => {
                    const mt = String((t.moduleTitle != null ? t.moduleTitle : t.ModuleTitle) ?? "").trim().toLowerCase();
                    return mt && mt === tl;
                });
                stage = "moduleTitle";
            }
            if (!bucket.length) {
                this._sfSchemaLog("import-filter-empty", {
                    targetModule: this._sfSchemaSummarizeModule(targetModule),
                    pickDataSourceName: pick,
                    pickNorm: pickNorm,
                    title: title,
                    allCount: all.length,
                    stageTried: stage
                });
                return [];
            }

            const mid0 = bucket[0].moduleId != null ? bucket[0].moduleId : bucket[0].ModuleId;
            let result = bucket;
            let expandedByModuleId = false;
            if (mid0 != null && mid0 !== "") {
                const mid = String(mid0);
                const same = all.filter((t) => String(t.moduleId != null ? t.moduleId : t.ModuleId || "") === mid);
                if (same.length) {
                    result = same;
                    expandedByModuleId = true;
                    stage = "moduleIdExpand";
                }
            }
            this._sfSchemaLog("import-filter-result", {
                targetModule: this._sfSchemaSummarizeModule(targetModule),
                pickDataSourceName: pick,
                pickNorm: pickNorm,
                title: title,
                stage: stage,
                expandedByModuleId: expandedByModuleId,
                moduleIdUsed: mid0 != null ? String(mid0) : "",
                bucketCount: bucket.length,
                resultCount: result.length
            });
            this._sfSchemaLogContentTypeList("import-filter-matched", result);
            return result;
        },
        buildSyntheticAllModulesFromModule() {
            const modName = (this.targetModule && (this.targetModule.Name || this.targetModule.Title) || "").trim();
            const list = this.targetTypes && this.targetTypes.items ? this.targetTypes.items : [];
            let mid = list.length > 0 ? (list[0].moduleId != null ? list[0].moduleId : list[0].ModuleId) : null;
            if (mid == null || mid === "") {
                mid = this.targetModule && (this.targetModule.moduleId != null ? this.targetModule.moduleId : this.targetModule.ModuleId);
            }
            this.AllModules = {
                items: [{
                    contentTypeName: modName,
                    ContentTypeName: modName,
                    moduleId: mid,
                    ModuleId: mid
                }]
            };
        },
        applyTargetTypesFromAllContentTypes() {
            const filtered = this.filterContentTypesForModule(this.sitefinityAllContentTypes, this.targetModule);
            const mapped = filtered.map((t) => this.mapCtToDropdownRow(t));
            this.targetTypes = { items: mapped };
            this.buildSyntheticAllModulesFromModule();
            this._sfSchemaLog("import-dropdown-types-applied", {
                targetModule: this._sfSchemaSummarizeModule(this.targetModule),
                dropdownCount: mapped.length
            });
            this._sfSchemaLogContentTypeList("import-dropdown-types", mapped);
        },
        async fetchModuleContentTypesOnDemand() {
            const connId = this.targetServerId();
            if (connId == null || !this.targetModule) return false;
            const dsn = this.pickDataSourceNameForModule();
            const title = this.moduleTitleForSave();
            if (!dsn && !title) return false;
            try {
                let url = "/api/SitefinityConnectivity/GetModuleContentTypes?connectionId=" + encodeURIComponent(connId);
                if (dsn) url += "&dataSourceName=" + encodeURIComponent(dsn);
                if (title) url += "&moduleTitle=" + encodeURIComponent(title);
                const res = await fetch(url);
                const json = await res.json();
                if (!res.ok || json.success === false) {
                    this._sfSchemaLog("import-module-content-types-on-demand-failed", json);
                    return false;
                }
                const types = json.contentTypes || json.ContentTypes || [];
                this._sfSchemaLog("import-module-content-types-on-demand", {
                    connectionId: connId,
                    dataSourceName: dsn,
                    moduleTitle: title,
                    count: types.length,
                    cacheKey: json.cacheKey || json.CacheKey
                });
                await this.fetchSitefinityAllContentTypes();
                return types.length > 0;
            } catch (e) {
                console.error("fetchModuleContentTypesOnDemand:", e);
                return false;
            }
        },
        async GetModuleDetails() {
            try {
                await this.ensureSitefinityAllContentTypes();
                if (!this.sitefinityAllContentTypes.length) {
                    const loaded = await this.fetchModuleContentTypesOnDemand();
                    if (!loaded) {
                        this.targetTypes = { items: [] };
                        this.AllModules = null;
                        Helpers.showNotification("No Sitefinity content types loaded. Connect to the target and wait for assessment, then try again.", "warning");
                        return;
                    }
                }
                this.applyTargetTypesFromAllContentTypes();
                if (this.targetModule && (!this.moduleContentTypesSorted || !this.moduleContentTypesSorted.length)) {
                    const loaded = await this.fetchModuleContentTypesOnDemand();
                    if (loaded) this.applyTargetTypesFromAllContentTypes();
                }
                this.$nextTick(() => this.ensureSingleContentTypeSelected());
            } catch (error) {
                console.error("GetModuleDetails", error);
                Helpers.showNotification("Error loading module content types", "error");
            }
        },
        /** Same shape as GetTypeDetails API response (full JSON body). */
        applyTypeDetailsFromFetchJson(moduleDetailsJson) {
            this.moduleDetails = moduleDetailsJson;
            let fields = this.extractTargetFieldsFromModuleDetailsResponse(moduleDetailsJson);
            fields = this.normalizeFieldsFromAssessment(fields);
            fields = this.appendUrlNameFieldIfMissing(fields);
            this.targetFieldsCache = fields;
        },
        collectTypeIdHints(mappingRecord) {
            const hints = [];
            const push = (v) => {
                if (v == null) return;
                const s = String(v).trim();
                if (s) hints.push(s);
            };
            if (mappingRecord) {
                push(mappingRecord.typeId);
                push(mappingRecord.TypeId);
            }
            if (this.targetType) {
                const t = this.targetType;
                push(t.contentTypeId);
                push(t.ContentTypeId);
                push(t.fullTypeName);
                push(t.FullTypeName);
                push(t.contentTypeItemName);
                push(t.ContentTypeItemName);
                push(t.contentTypeName);
                push(t.ContentTypeName);
                push(t.name);
                push(t.Name);
                push(t.entitySet);
                push(t.EntitySet);
            }
            push(this.pickTypeIdForSave());
            const seen = new Set();
            const out = [];
            for (let i = 0; i < hints.length; i++) {
                const key = hints[i].toLowerCase();
                if (seen.has(key)) continue;
                seen.add(key);
                out.push(hints[i]);
            }
            return out;
        },
        findContentTypeInAssessmentList(allTypes, typeHint) {
            if (!Array.isArray(allTypes) || !allTypes.length || typeHint == null) return null;
            const tid = String(typeHint).trim().toLowerCase();
            if (!tid) return null;
            const matchVal = (v) => v != null && String(v).trim().toLowerCase() === tid;
            for (let i = 0; i < allTypes.length; i++) {
                const ct = allTypes[i];
                if (!ct) continue;
                if (matchVal(ct.fullTypeName) || matchVal(ct.FullTypeName)
                    || matchVal(ct.contentTypeId) || matchVal(ct.ContentTypeId)
                    || matchVal(ct.contentTypeItemName) || matchVal(ct.ContentTypeItemName)
                    || matchVal(ct.name) || matchVal(ct.Name)
                    || matchVal(ct.contentTypeName) || matchVal(ct.ContentTypeName)
                    || matchVal(ct.entitySet) || matchVal(ct.EntitySet)) {
                    return ct;
                }
            }
            return null;
        },
        /** Built-in Sitefinity types (News, Blog post, …): custom fields come from ModuleEditor, not always from assessment sfmeta cache. */
        targetTypeNeedsModuleEditorFieldRefresh(ct) {
            const t = ct || this.targetType;
            if (!t) return false;
            const full = (t.fullTypeName != null ? t.fullTypeName : t.FullTypeName) || "";
            if (full.indexOf("Telerik.Sitefinity.DynamicTypes.Model.") >= 0) return false;
            const tid = (t.contentTypeId != null ? t.contentTypeId : t.ContentTypeId) || this.pickTypeIdForSave() || "";
            const candidate = String(full || tid).trim();
            if (!candidate) return false;
            if (candidate.indexOf("Telerik.Sitefinity.") >= 0 && candidate.indexOf(".Model.") >= 0) return true;
            const mod = this.targetModule && (this.targetModule.Title || this.targetModule.Name || "");
            if (mod && /^news$/i.test(String(mod).trim())) return true;
            return false;
        },
        applyTargetFieldsFromAssessmentEntry(ct, sourceLabel) {
            if (!ct) return false;
            const raw = ct.fields != null ? ct.fields : ct.Fields;
            if (!Array.isArray(raw) || !raw.length) return false;
            let fields = this.normalizeFieldsFromAssessment(raw);
            fields = this.appendUrlNameFieldIfMissing(fields);
            this.targetFieldsCache = fields;
            this.moduleDetails = null;
            this._sfSchemaLog("import-target-fields-from-all-content-types", {
                source: sourceLabel,
                targetType: this._sfSchemaSummarizeContentType(ct),
                fieldCount: fields.length,
                fieldNames: fields.map((f) => this.fieldPickName(f))
            });
            return true;
        },
        /** Same field list as GET /api/SitefinityConnectivity/GetAllContentTypes (assessment / init cache). */
        applyTargetFieldsFromAllContentTypes(typeHints) {
            const all = this.sitefinityAllContentTypes;
            if (!Array.isArray(all) || !all.length) return false;
            const hints = Array.isArray(typeHints) ? typeHints : (typeHints ? [typeHints] : []);
            for (let hi = 0; hi < hints.length; hi++) {
                const ct = this.findContentTypeInAssessmentList(all, hints[hi]);
                if (this.applyTargetFieldsFromAssessmentEntry(ct, "hint:" + hints[hi])) return true;
            }
            if (this.targetModule && this.targetType) {
                const filtered = this.filterContentTypesForModule(all, this.targetModule);
                const wantKey = this.contentTypeSelectValue(this.targetType);
                if (wantKey) {
                    for (let fi = 0; fi < filtered.length; fi++) {
                        if (this.contentTypeSelectValue(filtered[fi]) === wantKey) {
                            if (this.applyTargetFieldsFromAssessmentEntry(filtered[fi], "module-filter-key")) return true;
                        }
                    }
                }
            }
            return false;
        },
        async loadTargetFieldsForCurrentType(mappingRecord) {
            await this.ensureSitefinityAllContentTypes();
            const inEdit = !!(this.templateCreateMode && this.editingTemplateId) || !!this.editingMappingId;
            if (inEdit && this.targetFieldsLoadComplete && (this.targetFieldsCache || []).length > 0) {
                return;
            }
            const hints = this.collectTypeIdHints(mappingRecord);
            const usedCache = this.applyTargetFieldsFromAllContentTypes(hints);
            const refreshFromModuleEditor = this.targetTypeNeedsModuleEditorFieldRefresh();
            if (!this.targetType) return;
            if (usedCache && !refreshFromModuleEditor && !inEdit) return;
            const forceApi =
                refreshFromModuleEditor ||
                inEdit ||
                !!(this.templateCreateMode || this.editingMappingId || this.editingTemplateId);
            await this.GetTypeDetails(forceApi);
        },
        /** Load full Sitefinity field list when opening edit (not only fields already mapped). */
        async applyTargetFieldsForEdit(payload) {
            const sm = (payload && payload.mapping) ? payload.mapping : {};
            this.targetFieldsLoadComplete = false;
            if (payload && Array.isArray(payload.allContentTypes) && payload.allContentTypes.length) {
                this.sitefinityAllContentTypes = payload.allContentTypes;
                this.sitefinityContentTypesLoadedForId = this.targetServerId();
            }
            await this.ensureSitefinityAllContentTypes();
            if (payload && payload.typeDetails) {
                this.applyTypeDetailsFromFetchJson(payload.typeDetails);
            }
            const hints = this.collectTypeIdHints(sm);
            this.applyTargetFieldsFromAllContentTypes(hints);
            if (this.targetType) {
                await this.GetTypeDetails(true);
            }
            this.targetFieldsLoadComplete = (this.targetFieldsCache || []).length > 0;
            if (this.targetFieldsLoadComplete) {
                this._sfSchemaLog("import-edit-fields-loaded", {
                    fieldCount: (this.targetFieldsCache || []).length,
                    fieldNames: (this.targetFieldsCache || []).map((f) => this.fieldPickName(f))
                });
            }
        },
        findTargetTypeByHints(typeId, list) {
            if (typeId == null || String(typeId).trim() === "") return null;
            const tid = String(typeId).trim();
            const matchArr = (arr) => {
                if (!arr || !arr.length) return null;
                return arr.find((t) => String(t.contentTypeId || t.ContentTypeId || "") === tid)
                    || arr.find((t) => String(t.contentTypeItemName || t.ContentTypeItemName || "") === tid)
                    || arr.find((t) => String(t.fullTypeName || t.FullTypeName || "") === tid)
                    || arr.find((t) => String(t.contentTypeName || t.ContentTypeName || "") === tid)
                    || arr.find((t) => String(t.contentTypeName || t.ContentTypeName || "").toLowerCase() === tid.toLowerCase())
                    || arr.find((t) => String(t.name || t.Name || "") === tid)
                    || arr.find((t) => String(t.name || t.Name || "").toLowerCase() === tid.toLowerCase());
            };
            const fromList = matchArr(list);
            if (fromList) return fromList;
            const ct = this.findContentTypeInAssessmentList(this.sitefinityAllContentTypes, tid);
            if (!ct) return null;
            const mapped = this.mapCtToDropdownRow(ct);
            const key = this.contentTypeSelectValue(mapped);
            if (key && list && list.length) {
                const bound = list.find((t) => this.contentTypeSelectValue(t) === key);
                if (bound) return bound;
            }
            return mapped;
        },
        rebindWizardTargetSelections(mappingRecord) {
            const rec = mappingRecord || {};
            const ds = this.targetModules && (this.targetModules.DataSources || this.targetModules.dataSources || []);
            if (ds && ds.length) {
                const mod = this.findTargetModuleFromDataSources(ds, rec) || this.targetModule;
                if (mod) {
                    const mk = this.moduleSelectKey(mod);
                    const found = (this.targetModulesSortedDataSources || []).find((m) => this.moduleSelectKey(m) === mk);
                    this.targetModule = found || mod;
                }
            }
            if (this.sitefinityAllContentTypes && this.sitefinityAllContentTypes.length && this.targetModule) {
                this.applyTargetTypesFromAllContentTypes();
            }
            const typeId = this.pickRecordString(rec, "typeId", "TypeId") || this.pickTypeIdForSave();
            const list = this.moduleContentTypesSorted;
            const tt = this.findTargetTypeByHints(typeId, list);
            if (tt) this.targetType = tt;
            const prov = this.pickRecordString(rec, "providerName", "ProviderName");
            if (prov) this.targetSource = prov;
            const parentId = this.pickRecordString(rec, "parentId", "ParentId");
            if (parentId) this.blogParentId = parentId;
        },
        /** Fields from GetAllContentTypes / assessment cache (Title, Name, TypeName, …). */
        normalizeFieldsFromAssessment(fields) {
            if (!Array.isArray(fields) || !fields.length) return [];
            return fields.map((f) => {
                const name = f.name != null ? f.name : f.Name;
                const title = f.title != null ? f.title : f.Title;
                const typeName = f.typeName != null ? f.typeName : f.TypeName;
                const typeUIName = f.typeUIName != null ? f.typeUIName : f.TypeUIName;
                const displayType = this.resolveRelatedMediaDisplayLabel(f);
                return {
                    name: name,
                    Name: name,
                    title: title || name,
                    Title: title || name,
                    typeName: displayType || typeName,
                    TypeName: displayType || typeName,
                    typeUIName: typeUIName,
                    TypeUIName: typeUIName,
                    allowMultipleImages: f.allowMultipleImages != null ? f.allowMultipleImages : f.AllowMultipleImages,
                    AllowMultipleImages: f.allowMultipleImages != null ? f.allowMultipleImages : f.AllowMultipleImages,
                    allowMultipleVideos: f.allowMultipleVideos != null ? f.allowMultipleVideos : f.AllowMultipleVideos,
                    AllowMultipleVideos: f.allowMultipleVideos != null ? f.allowMultipleVideos : f.AllowMultipleVideos,
                    allowMultipleFiles: f.allowMultipleFiles != null ? f.allowMultipleFiles : f.AllowMultipleFiles,
                    AllowMultipleFiles: f.allowMultipleFiles != null ? f.allowMultipleFiles : f.AllowMultipleFiles,
                    allowImageLibrary: f.allowImageLibrary != null ? f.allowImageLibrary : f.AllowImageLibrary,
                    AllowImageLibrary: f.allowImageLibrary != null ? f.allowImageLibrary : f.AllowImageLibrary,
                    relatedDataType: f.relatedDataType != null ? f.relatedDataType : f.RelatedDataType,
                    RelatedDataType: f.relatedDataType != null ? f.relatedDataType : f.RelatedDataType,
                    dBType: f.dBType != null ? f.dBType : f.DBType,
                    DBType: f.dBType != null ? f.dBType : f.DBType
                };
            });
        },
        stripRelatedMediaCardinalitySuffix(raw) {
            return String(raw || "").replace(/\s*\(multiple\s+(images?|videos?|files?)\)\s*$/i, "").trim();
        },
        isGenericRelatedMediaLabel(label) {
            const compact = String(label || "").replace(/\s+/g, "");
            const lower = String(label || "").trim().toLowerCase();
            return compact.toLowerCase() === "relatedmedia"
                || compact.toLowerCase() === "relateddata"
                || lower === "related data"
                || lower === "related media"
                || lower === "related image"
                || lower === "single image";
        },
        classifyRelatedMediaKind(f) {
            if (!f || typeof f !== "object") return "";
            const allowImageLibrary = f.allowImageLibrary === true || f.AllowImageLibrary === true;
            if (allowImageLibrary) return "image";
            const tokens = [
                f.relatedDataType || f.RelatedDataType,
                f.typeUIName || f.TypeUIName,
                f.typeName || f.TypeName,
                f.dBType || f.DBType
            ];
            for (let i = 0; i < tokens.length; i++) {
                const t = this.stripRelatedMediaCardinalitySuffix(tokens[i]).replace(/\s+/g, "");
                if (!t) continue;
                if (/libraries\.model\.image/i.test(t) || /^image$/i.test(t) || /^relatedimage$/i.test(t)) return "image";
                if (/libraries\.model\.video/i.test(t) || /^video$/i.test(t) || /^relatedvideo$/i.test(t)) return "video";
                if (/libraries\.model\.document/i.test(t) || /^document$/i.test(t) || /^relateddocument$/i.test(t)) return "document";
                if (this.isGenericRelatedMediaLabel(this.stripRelatedMediaCardinalitySuffix(tokens[i]))) return "image";
            }
            return "";
        },
        relatedMediaCanonicalType(kind) {
            if (kind === "image") return "RelatedImage";
            if (kind === "document") return "RelatedDocument";
            if (kind === "video") return "RelatedVideo";
            return "";
        },
        resolveRelatedMediaDisplayLabel(f) {
            const kind = this.classifyRelatedMediaKind(f);
            if (!kind) return "";
            const multImg = f.allowMultipleImages === true || f.AllowMultipleImages === true;
            const multVid = f.allowMultipleVideos === true || f.AllowMultipleVideos === true;
            const multFile = f.allowMultipleFiles === true || f.AllowMultipleFiles === true;
            const base = kind === "image" ? "Related Image" : (kind === "document" ? "Related Document" : "Related Video");
            const multiple = kind === "image" ? multImg : (kind === "document" ? multFile : multVid);
            return multiple ? base + " (multiple)" : base;
        },
        appendUrlNameFieldIfMissing(fields) {
            const arr = Array.isArray(fields) ? fields.slice() : [];
            const seen = new Set();
            const deduped = [];
            for (let i = 0; i < arr.length; i++) {
                const n = (arr[i].name || arr[i].Name || "").toString().toLowerCase();
                if (n && seen.has(n)) continue;
                if (n) seen.add(n);
                deduped.push(arr[i]);
            }
            if (!seen.has("urlname")) {
                deduped.push({ title: "UrlName", name: "UrlName", Title: "UrlName", Name: "UrlName", typeName: "ShortText", TypeName: "ShortText" });
            }
            return deduped;
        },
        async GetTypeDetails(forceApiFetch) {
            try {
                const t = this.targetType;
                if (!t) {
                    this.targetFieldsCache = [];
                    return;
                }
                const forceApi = forceApiFetch === true;
                if (!forceApi) {
                    const embedded = t.fields != null ? t.fields : t.Fields;
                    if (Array.isArray(embedded) && embedded.length > 0) {
                        let norm = this.normalizeFieldsFromAssessment(embedded);
                        norm = this.appendUrlNameFieldIfMissing(norm);
                        this.targetFieldsCache = norm;
                        this.moduleDetails = null;
                        this.targetFieldsLoadComplete = norm.length > 0;
                        this._sfSchemaLog("import-type-fields-from-cache", {
                            targetType: this._sfSchemaSummarizeContentType(t),
                            fieldCount: norm.length,
                            fieldNames: norm.map((f) => this.fieldPickName(f))
                        });
                        return;
                    }
                }

                const tsId = this.targetServerId();
                const modName = this.targetModule && (this.targetModule.Name || this.targetModule.Title);
                const items = this.AllModules ? (this.AllModules.items || this.AllModules.Items || []) : [];
                const moduleRows = modName
                    ? items.filter((i) => {
                        const cn = i.contentTypeName || i.ContentTypeName || "";
                        return cn === modName || (cn && modName && String(cn).toLowerCase() === String(modName).toLowerCase());
                    })
                    : [];
                let mid = null;
                if (moduleRows && moduleRows.length > 0) {
                    mid = moduleRows[0].moduleId != null ? moduleRows[0].moduleId : moduleRows[0].ModuleId;
                }
                if (mid == null || mid === "") {
                    mid = this.pickModuleIdForSave();
                }

                let tid = this.pickTypeIdForSave();

                if (!tid || tid === "") {
                    console.warn("GetTypeDetails: missing typeId", { mid, tid });
                    this.targetFieldsCache = [];
                    return;
                }

                const dsn = this.pickDataSourceNameForModule();
                const dsnQ = dsn ? `&dataSourceName=${encodeURIComponent(dsn)}` : "";
                const forceQ = forceApi ? "&forceApi=true" : "";
                const midStr = mid != null && mid !== "" ? String(mid) : "";

                this.targetFieldsCache = [];
                const url = `/api/SitefinityConnectivity/GetTypeDetails?connectionId=${tsId}&moduleId=${encodeURIComponent(midStr)}&typeId=${encodeURIComponent(String(tid))}${dsnQ}${forceQ}`;
                const moduleInfo = await fetch(url);
                if (!moduleInfo.ok) {
                    console.error("GetTypeDetails failed", moduleInfo.status);
                    this.targetFieldsCache = [];
                    Helpers.showNotification("Could not load target content type fields from Sitefinity.", "error");
                    return;
                }
                const moduleDetails = await moduleInfo.json();
                this.applyTypeDetailsFromFetchJson(moduleDetails);
                const fields = this.targetFieldsCache || [];
                this.targetFieldsLoadComplete = fields.length > 0;
                this._sfSchemaLog("import-type-fields-from-api", {
                    targetType: this._sfSchemaSummarizeContentType(t),
                    moduleId: midStr,
                    typeId: String(tid),
                    dataSourceName: dsn,
                    fieldCount: fields.length,
                    fieldNames: fields.map((f) => this.fieldPickName(f))
                });
            } catch (error) {
                this.targetFieldsCache = [];
                console.error("Error fetching type details:", error);
                Helpers.showNotification("Error loading type details", "error");
            }
        },
        async AddMapping() {
            if (this.mappings == null) {
                this.mappings = [];
            }
            this.mappings.push({
                Id: new Date().getTime(),
                mappingKind: "Field",
                Source: "",
                Target: "",
                script: "",
                scriptUniqueId: "",
                libraryProviderName: "",
                libraryDataSourceName: "",
                libraryParentAlbumId: "",
                libraryParentEntitySet: "albums",
                libraryParentAlbumTitle: ""
            });
        },
        fieldPickName(obj) {
            if (obj == null) return "";
            if (typeof obj === "string") return obj;
            if (obj.name != null && obj.name !== "") return obj.name;
            if (obj.Name != null && obj.Name !== "") return obj.Name;
            if (obj.title != null && obj.title !== "") return obj.title;
            if (obj.Title != null && obj.Title !== "") return obj.Title;
            return "";
        },
        /** Sitefinity Module Builder label → OData importer canonical type (ShortText, LongText, …). */
        canonicalTargetFieldType(raw) {
            const s = this.stripRelatedMediaCardinalitySuffix(String(raw || "").trim());
            if (!s) return "";
            const key = s.toLowerCase().replace(/\s+/g, " ");
            const aliases = {
                "short text": "ShortText",
                "long text": "LongText",
                "rich text": "LongText",
                "rich content": "LongText",
                textarea: "LongText",
                "textarea (max)": "LongText",
                html: "LongText",
                number: "Number",
                numeric: "Number",
                integer: "Number",
                decimal: "Number",
                datetime: "DateTime",
                date: "DateTime",
                "date and time": "DateTime",
                "date picker": "DateTime",
                "date picker with time": "DateTime",
                "yes / no": "YesNo",
                "yes/no": "YesNo",
                "true / false": "YesNo",
                toggle: "YesNo",
                checkbox: "YesNo",
                email: "ShortText",
                url: "ShortText",
                label: "ShortText",
                "related data": "RelatedImage",
                "related media (images, videos, files)": "RelatedImage",
                "related media": "RelatedImage",
                relatedmedia: "RelatedImage",
                relateddata: "RelatedImage",
                "related image": "RelatedImage",
                image: "RelatedImage",
                "single image": "RelatedImage",
                "related document": "RelatedDocument",
                relateddocument: "RelatedDocument",
                document: "RelatedDocument",
                "related video": "RelatedVideo",
                relatedvideo: "RelatedVideo",
                video: "RelatedVideo"
            };
            if (aliases[key]) return aliases[key];
            const compact = s.replace(/\s+/g, "");
            if (/^(shorttext|longtext|datetime|yesno|relatedimage|relateddocument|relatedvideo|number)$/i.test(compact)) return compact;
            if (this.isGenericRelatedMediaLabel(s)) return "RelatedImage";
            return s;
        },
        /** Sitefinity type from typeDetails / OData (e.g. ShortText, RelatedImage). */
        pickTargetTypeName(f) {
            if (!f || typeof f !== "object") return "";
            const fromKind = this.relatedMediaCanonicalType(this.classifyRelatedMediaKind(f));
            if (fromKind) return fromKind;
            const raw = (f.typeUIName != null ? f.typeUIName : f.TypeUIName)
                || (f.typeName != null ? f.typeName : f.TypeName)
                || "";
            return this.canonicalTargetFieldType(raw);
        },
        formatSitefinityFieldTypeForDisplay(f) {
            const relatedLabel = this.resolveRelatedMediaDisplayLabel(f);
            if (relatedLabel) return relatedLabel;
            const raw = this.pickTargetTypeName(f);
            if (!raw) return "";
            if (raw.indexOf(" ") >= 0) return raw;
            return raw.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ");
        },
        targetFieldMappingDropdownLabel(f) {
            if (f == null) return "";
            if (typeof f === "string") return f;
            const name = this.fieldPickName(f);
            const ft = this.formatSitefinityFieldTypeForDisplay(f);
            return ft ? name + " (" + ft + ")" : name;
        },
        isScriptMappingRow(row) {
            return row && String(row.mappingKind || "").toLowerCase() === "script";
        },
        flushScriptFromEditor(row) {
            if (!row || !this.scriptEditors[row.Id]) return;
            if (typeof ChameleonImportScriptEditor !== "undefined") {
                row.script = ChameleonImportScriptEditor.getValue(this.scriptEditors[row.Id]);
            }
        },
        buildMappingRowForSave(m) {
            if (this.isScriptMappingRow(m)) this.flushScriptFromEditor(m);
            const isScript = this.isScriptMappingRow(m);
            const targetName = m.Target ? this.fieldPickName(m.Target) : "";
            return {
                Source: isScript ? (m.Source ? this.fieldPickName(m.Source) : "") : this.fieldPickName(m.Source),
                Target: targetName,
                MappingKind: isScript ? "Script" : "Field",
                // Keep the selected target field type even for Script mappings so edit dialog changes persist.
                TargetTypeName: this.pickTargetTypeName(m.Target) || (isScript ? "Script" : ""),
                Script: isScript ? (m.script || "") : null,
                ScriptUniqueId: isScript ? (m.scriptUniqueId || "s-" + m.Id) : null,
                TargetFullDetails: m.Target,
                SourceFullDetails: m.Source,
                libraryProviderName: m.libraryProviderName || null,
                libraryDataSourceName: m.libraryDataSourceName || null,
                libraryParentAlbumId: m.libraryParentAlbumId || null,
                libraryParentEntitySet: m.libraryParentEntitySet || null,
                libraryParentAlbumTitle: m.libraryParentAlbumTitle || null
            };
        },
        onMappingKindChange(row) {
            if (!row) return;
            if (this.isScriptMappingRow(row)) {
                if (!row.scriptUniqueId) this.$set(row, "scriptUniqueId", "s-" + row.Id);
                if (row.script == null || String(row.script).trim() === "")
                    this.$set(row, "script", this.defaultCustomFieldScript);
                this.$set(row, "TargetTypeName", this.pickTargetTypeName(row.Target) || "Script");
                var self = this;
                this.$nextTick(function () { self.mountScriptEditor(row); });
            } else {
                this.destroyScriptEditor(row);
                if (row.Target) this.$set(row, "TargetTypeName", this.pickTargetTypeName(row.Target));
            }
        },
        remountAllScriptEditors() {
            if (!this.showFieldMappingsStep || typeof ChameleonImportScriptEditor === "undefined") return;
            var self = this;
            (this.mappings || []).forEach(function (row) {
                if (self.isScriptMappingRow(row)) self.mountScriptEditor(row, 0);
            });
        },
        mountScriptEditor(row, attempt) {
            if (!row || !this.isScriptMappingRow(row)) return;
            if (typeof ChameleonImportScriptEditor === "undefined") return;
            var retry = typeof attempt === "number" ? attempt : 0;
            var self = this;
            ChameleonImportScriptEditor.ensureMonaco()
                .then(function () {
                    self.$nextTick(function () {
                        var el = document.getElementById("import-script-ed-" + row.Id);
                        if (!el) {
                            if (retry < 12) {
                                setTimeout(function () { self.mountScriptEditor(row, retry + 1); }, 80);
                            }
                            return;
                        }
                        if (self.scriptEditors[row.Id]) {
                            ChameleonImportScriptEditor.dispose(self.scriptEditors[row.Id]);
                        }
                        var text = row.script != null && String(row.script).length > 0
                            ? row.script
                            : self.defaultCustomFieldScript;
                        self.scriptEditors[row.Id] = ChameleonImportScriptEditor.create(
                            el,
                            text,
                            function (v) { row.script = v; }
                        );
                    });
                })
                .catch(function (e) {
                    console.error("Monaco load failed", e);
                    if (typeof Helpers !== "undefined") {
                        Helpers.showNotification("Could not load script editor (Monaco).", "error");
                    }
                });
        },
        destroyScriptEditor(row) {
            if (!row || !this.scriptEditors[row.Id]) return;
            ChameleonImportScriptEditor.dispose(this.scriptEditors[row.Id]);
            delete this.scriptEditors[row.Id];
        },
        disposeAllScriptEditors() {
            var self = this;
            if (typeof ChameleonImportScriptEditor === "undefined") return;
            Object.keys(this.scriptEditors || {}).forEach(function (id) {
                ChameleonImportScriptEditor.dispose(self.scriptEditors[id]);
            });
            this.scriptEditors = {};
        },
        validateCustomScript(row) {
            if (!row || !this.isScriptMappingRow(row)) return Promise.resolve(true);
            this.flushScriptFromEditor(row);
            var body = {
                script: row.script || "",
                scriptUniqueId: row.scriptUniqueId || "s-" + row.Id
            };
            return fetch("/api/Import/ValidateCustomFieldScript", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(body)
            })
                .then(function (r) { return r.json(); })
                .then(function (data) {
                    if (data && data.success) {
                        if (typeof Helpers !== "undefined") Helpers.showNotification("Script compiled successfully.", "success");
                        return true;
                    }
                    var errs = (data && data.errors) || ["Compilation failed."];
                    if (typeof Helpers !== "undefined") {
                        Helpers.showNotification(errs.join("\n"), "error");
                    }
                    return false;
                })
                .catch(function (e) {
                    console.error(e);
                    if (typeof Helpers !== "undefined") Helpers.showNotification("Script validation request failed.", "error");
                    return false;
                });
        },
        /** Sitefinity JSON may use camelCase or PascalCase; module row lives on AllModules.items. */
        pickModuleRowFromAllModules() {
            if (!this.targetModule || !this.AllModules) return null;
            const tmName = (this.targetModule.Name || this.targetModule.Title || "").trim();
            const items = this.AllModules.items || this.AllModules.Items || [];
            for (let i = 0; i < items.length; i++) {
                const row = items[i];
                const cn = (row.contentTypeName || row.ContentTypeName || "").trim();
                if (cn && tmName && cn === tmName) return row;
            }
            const lower = tmName.toLowerCase();
            for (let i = 0; i < items.length; i++) {
                const row = items[i];
                const cn = (row.contentTypeName || row.ContentTypeName || "").trim().toLowerCase();
                if (cn && lower && cn === lower) return row;
            }
            return null;
        },
        pickModuleIdForSave() {
            if (this.targetType) {
                const tid = this.targetType.moduleId != null ? this.targetType.moduleId : this.targetType.ModuleId;
                if (tid != null && tid !== "") return String(tid);
            }
            const row = this.pickModuleRowFromAllModules();
            if (row) {
                const id = row.moduleId != null ? row.moduleId : row.ModuleId;
                if (id != null && id !== "") return String(id);
            }
            const items = this.targetTypes && this.targetTypes.items ? this.targetTypes.items : [];
            if (items.length > 0) {
                const id0 = items[0].moduleId != null ? items[0].moduleId : items[0].ModuleId;
                if (id0 != null && id0 !== "") return String(id0);
            }
            if (this.targetModule) {
                const id = this.targetModule.moduleId != null ? this.targetModule.moduleId : this.targetModule.ModuleId;
                if (id != null && id !== "") return String(id);
            }
            const dsn = this.pickDataSourceNameForModule();
            if (dsn) return dsn;
            const tm = this.targetModule && (this.targetModule.Name || this.targetModule.Title);
            return tm ? String(tm).trim() : "";
        },
        /** Same key as SitefinityInitState / Excel: first link DataSourceName, else module Title (cache is per data source). */
        pickDataSourceNameForModule() {
            if (!this.targetModule) return "";
            const links = this.targetModule.Links || this.targetModule.links;
            if (Array.isArray(links) && links.length > 0) {
                const l0 = links[0];
                const ds = l0 && (l0.DataSourceName != null ? l0.DataSourceName : l0.dataSourceName);
                if (ds != null && String(ds).trim() !== "") return String(ds).trim();
            }
            const t = this.targetModule.Title != null ? this.targetModule.Title : this.targetModule.title;
            if (t != null && String(t).trim() !== "") return String(t).trim();
            const n = this.targetModule.Name != null ? this.targetModule.Name : this.targetModule.name;
            return n != null && String(n).trim() !== "" ? String(n).trim() : "";
        },
        pickTypeIdForSave() {
            if (!this.targetType) return "";
            const t = this.targetType;
            const full = t.fullTypeName != null ? t.fullTypeName : t.FullTypeName;
            if (full != null && String(full).trim() !== "") {
                const fullStr = String(full).trim();
                if (fullStr.indexOf("Telerik.Sitefinity.") >= 0 && fullStr.indexOf(".Model.") >= 0) {
                    return fullStr;
                }
            }
            const id =
                t.contentTypeId != null ? t.contentTypeId
                : t.ContentTypeId != null ? t.ContentTypeId
                : t.id != null ? t.id
                : t.Id;
            if (id != null && id !== "") return String(id);
            if (full != null && String(full).trim() !== "") return String(full).trim();
            return "";
        },
        /** Vue 2 <select> compares option values by reference; rebind saved JSON to actual option objects. */
        fieldKeyLoose(f) {
            if (!f) return "";
            const n = f.name != null ? f.name : f.Name;
            const t = f.title != null ? f.title : f.Title;
            return String(n || t || "").trim().toLowerCase();
        },
        matchFieldInOptions(options, saved) {
            if (!saved || !options || !options.length) return null;
            const candidates = new Set();
            if (typeof saved === "string") {
                const s = saved.trim().toLowerCase();
                if (s) candidates.add(s);
            } else {
                [saved.name, saved.Name, saved.title, saved.Title, saved.typeName, saved.TypeName].forEach((x) => {
                    if (x != null && x !== "") candidates.add(String(x).trim().toLowerCase());
                });
            }
            for (let i = 0; i < options.length; i++) {
                const o = options[i];
                const k = this.fieldKeyLoose(o);
                if (k && candidates.has(k)) return o;
            }
            return null;
        },
        pickRecordString(rec, camel, pascal) {
            if (!rec) return "";
            const v = rec[camel] != null ? rec[camel] : rec[pascal];
            return v != null && String(v).trim() !== "" ? String(v).trim() : "";
        },
        applyCultureFromRecord(rec) {
            if (!rec) return;
            this.importCulture = this.pickRecordString(rec, "culture", "Culture") || "";
        },
        applyHtmlInlineImagesFromRecord(rec) {
            if (!rec) return;
            this.htmlInlineImagesLibraryProviderName = this.pickRecordString(
                rec, "htmlInlineImagesLibraryProviderName", "HtmlInlineImagesLibraryProviderName"
            );
            this.htmlInlineImagesLibraryDataSourceName = this.pickRecordString(
                rec, "htmlInlineImagesLibraryDataSourceName", "HtmlInlineImagesLibraryDataSourceName"
            );
            this.htmlInlineImagesParentAlbumId = this.pickRecordString(
                rec, "htmlInlineImagesParentAlbumId", "HtmlInlineImagesParentAlbumId"
            );
            this.htmlInlineImagesParentAlbumTitle = this.pickRecordString(
                rec, "htmlInlineImagesParentAlbumTitle", "HtmlInlineImagesParentAlbumTitle"
            );
            const entitySet = this.pickRecordString(
                rec, "htmlInlineImagesParentEntitySet", "HtmlInlineImagesParentEntitySet"
            );
            this.htmlInlineImagesParentEntitySet = entitySet || "albums";
        },
        ensureTargetFieldsIncludeSavedMappings() {
            let cache = (this.targetFieldsCache && this.targetFieldsCache.length)
                ? this.targetFieldsCache.slice()
                : (this.targetFieldsList || []).slice();
            const seen = new Set();
            for (let i = 0; i < cache.length; i++) {
                const k = this.fieldKeyLoose(cache[i]);
                if (k) seen.add(k);
            }
            const rows = this.mappings || [];
            for (let i = 0; i < rows.length; i++) {
                const row = rows[i];
                let t = row.Target;
                if (!t) continue;
                if (typeof t === "string") {
                    const nm = t.trim();
                    if (!nm) continue;
                    const key = nm.toLowerCase();
                    if (seen.has(key)) continue;
                    cache.push({
                        name: nm,
                        title: nm,
                        typeName: row.targetTypeName || row.TargetTypeName || "ShortText"
                    });
                    seen.add(key);
                    continue;
                }
                const key = this.fieldKeyLoose(t);
                if (key && !seen.has(key)) {
                    cache.push(Object.assign({}, t));
                    seen.add(key);
                }
            }
            this.targetFieldsCache = cache;
            this.moduleDetails = null;
        },
        async finishMappingRowsRebind() {
            await this.loadNodeFieldsForImport();
            if (this.targetType && !this.targetFieldsLoadComplete) {
                await this.loadTargetFieldsForCurrentType(null);
            }
            await this.$nextTick();
            this.ensureTargetFieldsIncludeSavedMappings();
            this.rebindMappingRowSelectionsToDropdowns();
            await this.$nextTick();
            if (this.showFieldMappingsStep) this.remountAllScriptEditors();
        },
        async hydrateTemplateTargetFromRecord(template) {
            if (!template || !this.targetServer) return;
            const siteId = this.pickRecordString(template, "siteId", "SiteId");
            if (siteId) this.targetSite = siteId;
            this.restoringImportWizard = true;
            try {
                if (this.targetSite) {
                    await this.GetSiteModules(this.targetSite);
                    await this.$nextTick();
                }
                const ds = this.targetModules && (this.targetModules.DataSources || this.targetModules.dataSources || []);
                if (ds && ds.length) {
                    const mod = this.findTargetModuleFromDataSources(ds, template);
                    if (mod) this.targetModule = mod;
                    await this.$nextTick();
                }
                const typeId = this.pickRecordString(template, "typeId", "TypeId");
                const list = this.moduleContentTypesSorted;
                this.targetType = this.findTargetTypeByHints(typeId, list);
                this.rebindWizardTargetSelections(template);
                const prov = this.pickRecordString(template, "providerName", "ProviderName");
                if (prov) this.targetSource = prov;
                const parentId = this.pickRecordString(template, "parentId", "ParentId");
                if (parentId) this.blogParentId = parentId;
                if (this.targetModule && (!this.targetFieldsCache || !this.targetFieldsCache.length)) {
                    await this.GetModuleDetails();
                }
                if (this.targetType) {
                    await this.loadTargetFieldsForCurrentType(template);
                }
                if (this.targetSite) {
                    await this.loadLibrarySourcesForSite();
                }
            } finally {
                this.restoringImportWizard = false;
            }
        },
        rebindMappingRowSelectionsToDropdowns() {
            const targets = this.targetFieldsForMapping || [];
            const sources = this.sourceFields || [];
            if (!this.mappings || !this.mappings.length) return;
            for (let i = 0; i < this.mappings.length; i++) {
                const row = this.mappings[i];
                if (row.Target) {
                    const m = this.matchFieldInOptions(targets, row.Target);
                    if (m) this.$set(this.mappings[i], "Target", m);
                }
                if (row.Source) {
                    const m = this.matchFieldInOptions(sources, row.Source);
                    if (m) this.$set(this.mappings[i], "Source", m);
                }
            }
        },
        /** OData segment from assessment (<code>entitySet</code>); normalizes e.g. <code>blogpost</code> → <code>blogposts</code>, <code>news</code> → <code>newsitems</code>. */
        normalizeODataEntitySegmentValue(raw) {
            if (raw == null || String(raw).trim() === "") return "";
            let s = String(raw).trim().toLowerCase().replace(/\s+/g, "");
            if (s === "blogpost") s = "blogposts";
            if (s === "news" || s === "newsitem" || s === "newsmanager" || s.indexOf("newsmanager") >= 0) s = "newsitems";
            if (s.indexOf("telerik.sitefinity") >= 0 || s.indexOf(".model.") >= 0 || s.indexOf(".modules.") >= 0) return "";
            return s;
        },
        normalizeODataSegmentForSave(t) {
            if (!t) return "";
            const es = t.entitySet != null ? t.entitySet : t.EntitySet;
            if (es != null && String(es).trim() !== "") {
                const n = this.normalizeODataEntitySegmentValue(es);
                if (n) return n;
            }
            const full = (t.fullTypeName != null ? t.fullTypeName : t.FullTypeName) || "";
            if (full.toLowerCase().indexOf("newsitem") >= 0 || full.toLowerCase().indexOf("telerik.sitefinity.news") >= 0) {
                return "newsitems";
            }
            if (full.toLowerCase().indexOf("blogpost") >= 0) return "blogposts";
            if (full.toLowerCase().indexOf("pages.model.page") >= 0 || full.toLowerCase().indexOf("telerik.sitefinity.pages") >= 0) {
                return "pages";
            }
            return "";
        },
        resolveEntitySetFromAssessmentCache(typeHint) {
            const ct = this.findContentTypeInAssessmentList(this.sitefinityAllContentTypes, typeHint);
            if (!ct) return "";
            return this.normalizeODataSegmentForSave(ct);
        },
        /** Sitefinity OData entity set for POST/PATCH — from content type <code>entitySet</code>, never module CLR names. */
        pickODataSegmentForSave() {
            const tt = this.targetType;
            if (tt) {
                const n = this.normalizeODataSegmentForSave(tt);
                if (n) return n;
            }
            const tid = this.pickTypeIdForSave();
            if (tid) {
                const fromCache = this.resolveEntitySetFromAssessmentCache(tid);
                if (fromCache) return fromCache;
                const tl = String(tid).toLowerCase();
                if (tl.indexOf("newsitem") >= 0 || tl.indexOf("telerik.sitefinity.news") >= 0) return "newsitems";
                if (tl.indexOf("blogpost") >= 0) return "blogposts";
                if (tl.indexOf("pages.model.page") >= 0 || tl.indexOf("telerik.sitefinity.pages") >= 0 || tl === "page") return "pages";
            }
            const dsn = this.pickDataSourceNameForModule();
            if (dsn) {
                const n = this.normalizeODataEntitySegmentValue(dsn);
                if (n) return n;
            }
            return "";
        },
        sourceTypeKey(templateId, templateName) {
            const id = (templateId || "").toString().trim().toLowerCase();
            if (id) return "id:" + id;
            const nm = (templateName || "").toString().trim().toLowerCase();
            if (nm) return "name:" + nm;
            return "";
        },
        sourceTypeKeyForRow(row) {
            return this.sourceTypeKey(row && row.sourceTemplateId, row && row.sourceTemplateName);
        },
        async loadTemplateAssociationsForConnection() {
            const connId = this.sourceConnectionId();
            this.templateAssociationsByType = {};
            if (connId == null) return;
            try {
                const u = `/api/SitefinityConnectivity/GetMappings?connectionId=${encodeURIComponent(connId)}`;
                const r = await fetch(u);
                const j = await r.json();
                const all = j.result || [];
                const dict = {};
                for (let i = 0; i < all.length; i++) {
                    const m = all[i] || {};
                    const binds = m.typeBindings || m.TypeBindings || [];
                    for (let bi = 0; bi < binds.length; bi++) {
                        const b = binds[bi] || {};
                        const skipMode = (b.skipMode != null ? b.skipMode : b.SkipMode || "Import").toString();
                        const tid = b.importMappingTemplateId != null ? b.importMappingTemplateId : b.ImportMappingTemplateId;
                        if (skipMode !== "Import" || tid == null) continue;
                        const key = this.sourceTypeKey(
                            b.sourceTemplateId != null ? b.sourceTemplateId : b.SourceTemplateId,
                            b.sourceTemplateName != null ? b.sourceTemplateName : b.SourceTemplateName
                        );
                        if (!key) continue;
                        if (!dict[key]) dict[key] = {};
                        dict[key][String(tid)] = true;
                    }
                }
                this.templateAssociationsByType = dict;
            } catch (e) {
                console.error("loadTemplateAssociationsForConnection", e);
            }
        },
        templateOptionsForRow(row) {
            const all = this.importTemplatesList || [];
            const key = this.sourceTypeKeyForRow(row);
            const idsMap = (key && this.templateAssociationsByType && this.templateAssociationsByType[key]) || null;
            const sel = row && row.assignment != null ? String(row.assignment).trim() : "";
            const skipVals = new Set(["", "skipsubtree", "skipitemchildrenonly"]);
            const selIsTemplateId = sel && !skipVals.has(sel.toLowerCase()) && /^\d+$/.test(sel);
            // List every reusable template for this connection so brand-new templates (not yet on any plan)
            // appear. Optionally bubble templates already used for this source type to the top.
            if (!idsMap || !Object.keys(idsMap).length) return all;
            const pickId = (t) => String(t.id != null ? t.id : t.Id);
            const usedHere = (t) => idsMap[pickId(t)] === true;
            const associated = all.filter(usedHere);
            const rest = all.filter((t) => !usedHere(t));
            const ordered = associated.concat(rest);
            if (selIsTemplateId && !ordered.some((t) => pickId(t) === sel)) {
                const missing = all.find((t) => pickId(t) === sel);
                if (missing) ordered.unshift(missing);
            }
            return ordered;
        },
        selectedTemplateIdForRow(row) {
            const sel = row && row.assignment != null ? String(row.assignment).trim() : "";
            const skipVals = new Set(["", "skipsubtree", "skipitemchildrenonly"]);
            if (!sel || skipVals.has(sel.toLowerCase())) return null;
            if (!/^\d+$/.test(sel)) return null;
            const n = Number(sel);
            return Number.isFinite(n) && n > 0 ? n : null;
        },
        rowHasSelectedTemplate(row) {
            return this.selectedTemplateIdForRow(row) != null;
        },
        findImportTemplateById(templateId) {
            if (templateId == null) return null;
            const id = String(templateId);
            const list = this.importTemplatesList || [];
            return list.find((t) => String(t.id != null ? t.id : t.Id) === id) || null;
        },
        enterTemplateFieldMappingEditor() {
            this.templateCreateMode = true;
            this.useClassicSingleTypeWizard = true;
            this.useTemplatePlanWizard = false;
            this.inSetup = true;
            this.currentStep = 1;
        },
        async applyImportTemplateRecordForEdit(template, templateId) {
            if (!template) {
                throw new Error("Template not found in the templates list. Refresh the wizard and try again.");
            }
            const connId = this.sourceConnectionId();
            const tid = templateId != null ? templateId : (template.id != null ? template.id : template.Id);
            let hydrated = false;
            if (connId != null && tid != null) {
                try {
                    const editUrl = "/api/SitefinityConnectivity/GetImportMappingTemplateForEdit?templateId="
                        + encodeURIComponent(tid)
                        + "&sourceConnectionId=" + encodeURIComponent(connId);
                    const res = await fetch(editUrl);
                    const ct = (res.headers.get("content-type") || "").toLowerCase();
                    if (ct.indexOf("json") >= 0) {
                        const payload = await res.json();
                        const apiOk = res.ok && payload && payload.success !== false;
                        if (apiOk) {
                            try {
                                await this.applyClassicMappingEditFromPayload(payload);
                                hydrated = true;
                            } catch (applyErr) {
                                console.warn("applyImportTemplateRecordForEdit: full hydrate failed", applyErr);
                            }
                        } else {
                            const msg = (payload && (payload.message || payload.Message)) || res.statusText;
                            console.warn("applyImportTemplateRecordForEdit: edit API failed", msg);
                            Helpers.showNotification(
                                msg || "Could not load full Sitefinity data for this template; using saved template fields only.",
                                "warning"
                            );
                        }
                    }
                } catch (e) {
                    console.warn("applyImportTemplateRecordForEdit: Sitefinity hydrate failed", e);
                }
            }
            const serverId = template.targetServerConnectionId != null
                ? template.targetServerConnectionId
                : template.TargetServerConnectionId;
            const serverIdStr = serverId != null && Number(serverId) > 0 ? String(serverId) : "";
            if (!hydrated) {
                this.targetServerConnectionId = serverIdStr;
                this.targetServer = this.resolveTargetServerByConnectionId(serverIdStr);
                this.MappingName = template.name || template.Name || "";
                this.MappingDescription = template.description || template.Description || "";
            }
            this.applyHtmlInlineImagesFromRecord(template);
            this.applyCultureFromRecord(template);
            if (!hydrated) {
                let rows = [];
                const mj = template.mappingsJSON != null ? template.mappingsJSON : template.MappingsJSON;
                if (mj) {
                    try { rows = JSON.parse(mj); } catch (e) { rows = []; }
                }
                this.mappings = this.mapRowsFromSaved(rows);
                await this.hydrateTemplateTargetFromRecord(template);
            }
            await this.finishMappingRowsRebind();
        },
        cancelTemplateWizardAndRestorePlan() {
            const draft = this.templatePlanDraftBeforeCreate || null;
            this.templateCreateMode = false;
            this.editingTemplateId = 0;
            this.useClassicSingleTypeWizard = false;
            this.useTemplatePlanWizard = true;
            this.templatePlanDraftBeforeCreate = null;
            this.templateCreateReturnTypeKey = "";
            this.templateCreateSourceTemplateName = "";
            this.currentStep = 1;
            if (draft) {
                this.editingMappingId = draft.editingMappingId || 0;
                this.MappingName = draft.MappingName || "";
                this.MappingDescription = draft.MappingDescription || "";
                this.childrenSelection = draft.childrenSelection || "Immediate";
                this.importCulture = draft.importCulture || "";
                if (draft.templatePlanRows) {
                    this.templatePlanRows = JSON.parse(JSON.stringify(draft.templatePlanRows));
                }
            }
        },
        beginCreateTemplateForRow(row) {
            const retKey = this.sourceTypeKeyForRow(row);
            if (!retKey) {
                Helpers.showNotification("Cannot determine source type for this row.", "error");
                return;
            }
            this.templatePlanDraftBeforeCreate = {
                editingMappingId: this.editingMappingId || 0,
                MappingName: this.MappingName || "",
                MappingDescription: this.MappingDescription || "",
                childrenSelection: this.childrenSelection || "Immediate",
                importCulture: this.importCulture || "",
                templatePlanRows: JSON.parse(JSON.stringify(this.templatePlanRows || []))
            };
            this.templateCreateReturnTypeKey = retKey;
            this.templateCreateSourceTemplateName = (row && row.sourceTemplateName) || "";
            this.templateCreateMode = true;
            this.editingTemplateId = 0;
            this.inSetup = true;
            this.useClassicSingleTypeWizard = true;
            this.useTemplatePlanWizard = false;
            this.currentStep = 0;
            this.mappings = [];
            this.editingMappingId = 0;
            this.targetServer = null;
            this.targetSite = null;
            this.targetModules = null;
            this.targetModule = null;
            this.targetSource = null;
            this.targetTypes = null;
            this.targetType = null;
            this.moduleDetails = null;
            this.targetFieldsCache = [];
            this.blogParentId = null;
            this.blogParentOptions = [];
            this.htmlInlineImagesLibraryProviderName = "";
            this.htmlInlineImagesLibraryDataSourceName = "";
            this.htmlInlineImagesParentAlbumId = "";
            this.htmlInlineImagesParentAlbumTitle = "";
            this.htmlInlineImagesParentEntitySet = "albums";
            this.MappingName = "";
            this.MappingDescription = "";
            this.$nextTick(() => this.loadNodeFieldsForImport());
        },
        async beginEditTemplateForRow(row) {
            const templateId = this.selectedTemplateIdForRow(row);
            if (templateId == null) {
                Helpers.showNotification("Select a mapping to edit.", "error");
                return;
            }
            const retKey = this.sourceTypeKeyForRow(row);
            if (!retKey) {
                Helpers.showNotification("Cannot determine source type for this row.", "error");
                return;
            }
            this.templatePlanDraftBeforeCreate = {
                editingMappingId: this.editingMappingId || 0,
                MappingName: this.MappingName || "",
                MappingDescription: this.MappingDescription || "",
                childrenSelection: this.childrenSelection || "Immediate",
                importCulture: this.importCulture || "",
                templatePlanRows: JSON.parse(JSON.stringify(this.templatePlanRows || []))
            };
            this.templateCreateReturnTypeKey = retKey;
            this.templateCreateSourceTemplateName = (row && row.sourceTemplateName) || "";
            this.templateCreateMode = true;
            this.editingTemplateId = templateId;
            this.inSetup = true;
            this.editImportLoading = true;
            let template = null;
            try {
                this.restoringImportWizard = true;
                await this.GetSitefinityConnections();
                const connId = this.sourceConnectionId();
                if (connId == null) {
                    Helpers.showNotification("Connect a source before editing a template.", "error");
                    this.cancelTemplateWizardAndRestorePlan();
                    return;
                }
                template = this.findImportTemplateById(templateId);
                if (!template) {
                    try {
                        const uT = "/api/SitefinityConnectivity/GetImportMappingTemplates?connectionId="
                            + encodeURIComponent(connId);
                        const rT = await fetch(uT);
                        const jT = await rT.json();
                        this.importTemplatesList = jT.result || jT.Result || [];
                        template = this.findImportTemplateById(templateId);
                    } catch (e) { /* ignore */ }
                }
                const tConn = template && (template.targetServerConnectionId != null ? template.targetServerConnectionId : template.TargetServerConnectionId);
                if (tConn != null) this.watchEditMappingProgress(-tConn);
                await this.applyImportTemplateRecordForEdit(template, templateId);
                this.enterTemplateFieldMappingEditor();
                if (!this.mappings || !this.mappings.length) {
                    Helpers.showNotification(
                        "Template opened with no field mappings yet. Add mappings below, then click Update template.",
                        "warning"
                    );
                }
            } catch (e) {
                console.error("beginEditTemplateForRow", e);
                Helpers.showNotification(e.message || "Could not open mapping for editing.", "error");
                this.cancelTemplateWizardAndRestorePlan();
            } finally {
                this.restoringImportWizard = false;
                this.editImportLoading = false;
                this.unwatchEditMappingProgress();
                this.rebindWizardTargetSelections(template);
                if (this.needsParentSelection) {
                    this.$nextTick(() => this.loadParentOptions());
                }
            }
        },
        async saveTemplateFromClassicWizard() {
            if (!this.mappings || !this.mappings.length) {
                Helpers.showNotification("Add at least one field mapping.", "error");
                return;
            }
            const connId = this.sourceConnectionId();
            if (connId == null) {
                Helpers.showNotification("Connect a source before saving template.", "error");
                return;
            }
            const mappingsToSave = this.mappings.map((m) => this.buildMappingRowForSave(m));
            let moduleIdStr = this.pickModuleIdForSave();
            let typeIdStr = this.pickTypeIdForSave();
            if (!moduleIdStr || !typeIdStr) {
                Helpers.showNotification(
                    "Could not resolve Sitefinity module or content type id. Re-select target server/site/module/type/provider.",
                    "error"
                );
                return;
            }
            const payload = {
                id: this.editingTemplateId || 0,
                connectionId: connId,
                name: this.MappingName || "New template",
                description: this.MappingDescription || "",
                platformType: this.platformType || "",
                targetServerConnectionId: this.targetServer && (this.targetServer.id != null ? this.targetServer.id : this.targetServer.Id),
                siteId: this.targetSite || "",
                moduleId: moduleIdStr,
                oData: this.pickODataSegmentForSave(),
                typeId: typeIdStr,
                providerName: this.targetSource || "",
                parentId: this.needsParentSelection ? (this.blogParentId || null) : null,
                libraryMediaKind: null,
                htmlInlineImagesLibraryProviderName: this.htmlInlineImagesLibraryProviderName || null,
                htmlInlineImagesLibraryDataSourceName: this.htmlInlineImagesLibraryDataSourceName || null,
                htmlInlineImagesParentAlbumId: this.htmlInlineImagesParentAlbumId || null,
                htmlInlineImagesParentAlbumTitle: this.htmlInlineImagesParentAlbumTitle || null,
                htmlInlineImagesParentEntitySet: this.htmlInlineImagesParentEntitySet || null,
                culture: (this.importCulture || "").trim() || null,
                mappingsJSON: JSON.stringify(mappingsToSave)
            };
            let newTemplateId = null;
            try {
                const res = await fetch("/api/SitefinityConnectivity/SaveImportMappingTemplate", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify(payload)
                });
                const json = await res.json();
                if (!res.ok || !json || json.success === false) {
                    Helpers.showNotification((json && (json.message || json.Message)) || "Error saving template.", "error");
                    return;
                }
                newTemplateId = json.id != null ? json.id : json.Id;
            } catch (e) {
                console.error("saveTemplateFromClassicWizard", e);
                Helpers.showNotification("Error saving template.", "error");
                return;
            }
            const draft = this.templatePlanDraftBeforeCreate || null;
            const draftRowsSnapshot = draft ? JSON.parse(JSON.stringify(draft.templatePlanRows || [])) : [];
            const retKey = this.templateCreateReturnTypeKey || "";
            this.templateCreateMode = false;
            this.templatePlanDraftBeforeCreate = null;
            this.useClassicSingleTypeWizard = false;
            this.useTemplatePlanWizard = true;
            this.currentStep = 1;
            if (draft) {
                this.editingMappingId = draft.editingMappingId || 0;
                this.MappingName = draft.MappingName || "";
                this.MappingDescription = draft.MappingDescription || "";
                this.childrenSelection = draft.childrenSelection || "Immediate";
                this.importCulture = draft.importCulture || "";
            }
            await this.loadTemplatePlanStepData();
            const prevByKey = {};
            for (let i = 0; i < draftRowsSnapshot.length; i++) {
                const r = draftRowsSnapshot[i];
                const k = this.sourceTypeKey(r.sourceTemplateId, r.sourceTemplateName);
                const a = r.assignment;
                if (k && a != null && String(a).trim() !== "") prevByKey[k] = String(a);
            }
            for (let i = 0; i < this.templatePlanRows.length; i++) {
                const row = this.templatePlanRows[i];
                const k = this.sourceTypeKeyForRow(row);
                if (k && prevByKey[k] != null) row.assignment = prevByKey[k];
            }
            if (retKey && newTemplateId != null) {
                for (let i = 0; i < this.templatePlanRows.length; i++) {
                    const row = this.templatePlanRows[i];
                    if (this.sourceTypeKeyForRow(row) === retKey) {
                        row.assignment = String(newTemplateId);
                        break;
                    }
                }
            }
            this.templateCreateReturnTypeKey = "";
            this.templateCreateSourceTemplateName = "";
            this.editingTemplateId = 0;
            Helpers.showNotification("Template saved and selected.", "success");
        },
        async loadTemplatePlanStepData() {
            const connId = this.sourceConnectionId();
            const nodeId = this.sourceNodeId();
            const cs = this.childrenSelection || "Immediate";
            this.loadingSourceTypesPlan = true;
            try {
                const uT = `/api/SitefinityConnectivity/GetImportMappingTemplates?connectionId=${encodeURIComponent(connId)}`;
                const uS = `/api/SitefinityConnectivity/GetDistinctImportSourceTypes?connectionId=${encodeURIComponent(connId)}&nodeId=${encodeURIComponent(nodeId)}&childrenSelection=${encodeURIComponent(cs)}`;
                const [rT, rS] = await Promise.all([fetch(uT), fetch(uS)]);
                const jT = await rT.json();
                const jS = await rS.json();
                this.importTemplatesList = jT.result || jT.Result || [];
                const types = jS.result || jS.Result || [];
                this.sourceTypesForPlan = types;
                this.templatePlanRows = types.map((st) => ({
                    sourceTemplateId: st.sourceTemplateId != null ? st.sourceTemplateId : st.SourceTemplateId,
                    sourceTemplateName: st.sourceTemplateName != null ? st.sourceTemplateName : st.SourceTemplateName,
                    itemCount: st.itemCount != null ? st.itemCount : st.ItemCount,
                    assignment: ""
                }));
                await this.loadTemplateAssociationsForConnection();
            } catch (e) {
                console.error("loadTemplatePlanStepData", e);
                Helpers.showNotification("Could not load source types or templates.", "error");
                this.templatePlanRows = [];
            } finally {
                this.loadingSourceTypesPlan = false;
            }
        },
        async saveTemplatePlanMappings() {
            const connId = this.sourceConnectionId();
            const nodeId = this.sourceNodeId();
            if (connId == null || nodeId === "") {
                this.showImportErrorModal(
                    "Connect to a source and select a tree node as the import start node before saving this plan."
                );
                return;
            }
            const connNum = Number(connId);
            if (!Number.isFinite(connNum) || connNum <= 0) {
                this.showImportErrorModal("The source connection is invalid. Reconnect and try again.");
                return;
            }
            if (!this.MappingName || !String(this.MappingName).trim()) {
                this.showImportErrorModal("Enter a mapping name before saving.");
                return;
            }
            const rows = this.templatePlanRows || [];
            if (!rows.length) {
                this.showImportErrorModal(
                    "No source content types were found for this scope. Adjust the tree node or import scope, then try again."
                );
                return;
            }
            const unassigned = this.collectUnassignedTemplatePlanTypeLabels();
            if (unassigned.length) {
                this.showImportErrorModal(
                    this.formatTemplatePlanMissingAssignmentsMessage(unassigned),
                    "Missing import actions"
                );
                return;
            }
            const typeBindings = [];
            for (let i = 0; i < rows.length; i++) {
                const row = rows[i];
                const a = row.assignment;
                if (a === "SkipSubtree") {
                    typeBindings.push({
                        sourceTemplateId: row.sourceTemplateId || null,
                        sourceTemplateName: row.sourceTemplateName || null,
                        importMappingTemplateId: null,
                        skipMode: "SkipSubtree"
                    });
                    continue;
                }
                if (a === "SkipItemChildrenOnly") {
                    typeBindings.push({
                        sourceTemplateId: row.sourceTemplateId || null,
                        sourceTemplateName: row.sourceTemplateName || null,
                        importMappingTemplateId: null,
                        skipMode: "SkipItemChildrenOnly"
                    });
                    continue;
                }
                const tid = Number(a);
                if (!Number.isFinite(tid) || tid <= 0) {
                    const typeLabel = (row.sourceTemplateName || row.sourceTemplateId || "").trim() || "Unknown type";
                    this.showImportErrorModal(
                        "The selected reusable template is invalid for source type:\n\n• " + typeLabel,
                        "Invalid template selection"
                    );
                    return;
                }
                typeBindings.push({
                    sourceTemplateId: row.sourceTemplateId || null,
                    sourceTemplateName: row.sourceTemplateName || null,
                    importMappingTemplateId: tid,
                    skipMode: "Import"
                });
            }

            const mappingJSON = {
                id: this.editingMappingId || 0,
                name: String(this.MappingName).trim(),
                description: this.MappingDescription || "",
                connectionId: connNum,
                nodeId: String(nodeId),
                platformType: this.platformType,
                childrenSelection: this.childrenSelection,
                itemSelectionMode: this.itemSelectionMode || "Normal",
                culture: (this.importCulture || "").trim() || null,
                importMode: "TemplatePlan",
                typeBindings: typeBindings,
                mappingsJSON: "[]",
                targetServerConnectionId: 0,
                siteId: "",
                moduleId: "",
                oData: "",
                typeId: "",
                providerName: "",
                parentId: null,
                htmlInlineImagesLibraryProviderName: null,
                htmlInlineImagesLibraryDataSourceName: null,
                htmlInlineImagesParentAlbumId: null,
                htmlInlineImagesParentAlbumTitle: null,
                htmlInlineImagesParentEntitySet: null
            };

            try {
                const res = await fetch("/api/SitefinityConnectivity/SaveMapping", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify(mappingJSON)
                });
                let json = null;
                try {
                    json = await res.json();
                } catch (parseErr) {
                    /* ignore */
                }
                if (!res.ok || !json || json.result === false || json.success === false) {
                    const msg = (json && (json.message || json.Message))
                        || "Could not save import plan. Check assignments and try again.";
                    this.showImportErrorModal(msg);
                    return;
                }
                this.editingMappingId = null;
                await this.LoadMappings();
                this.editImportLoading = false;
                this.inSetup = false;
                Helpers.showNotification("Import plan saved.", "success");
                this.$emit("imports-updated");
            } catch (error) {
                console.error("saveTemplatePlanMappings", error);
                this.showImportErrorModal(
                    "An unexpected error occurred while saving the import plan. See the browser console for details.",
                    "Error saving import plan",
                    error && (error.message || String(error))
                );
            }
        },
        async SaveMappings() {
            const connId = this.sourceConnectionId();
            const nodeId = this.sourceNodeId();
            if (connId == null || nodeId === "") {
                if (this.useTemplatePlanWizard) {
                    this.showImportErrorModal(
                        "Connect to a source and select a tree node as the import start node before saving this plan."
                    );
                } else {
                    Helpers.showNotification("Connect and select a tree node (import start node) before saving.", "error");
                }
                return;
            }
            if (this.useTemplatePlanWizard) {
                await this.saveTemplatePlanMappings();
                return;
            }
            if (this.templateCreateMode) {
                await this.saveTemplateFromClassicWizard();
                return;
            }
            if (!this.mappings || !this.mappings.length) {
                Helpers.showNotification("Add at least one field mapping.", "error");
                return;
            }
            const mappingsToSave = this.mappings.map((m) => this.buildMappingRowForSave(m));

            let moduleIdStr = this.pickModuleIdForSave();
            let typeIdStr = this.pickTypeIdForSave();
            if (!moduleIdStr || !typeIdStr) {
                Helpers.showNotification(
                    "Could not resolve Sitefinity module or content type id. Re-select Target Server, Site, module, content type, and provider, then save again.",
                    "error"
                );
                return;
            }

            const mappingJSON = {
                id: this.editingMappingId || 0,
                Name: this.MappingName,
                Description: this.MappingDescription,
                ConnectionId: connId,
                SiteId: this.targetSite,
                MappingsJSON: JSON.stringify(mappingsToSave),
                ModuleId: moduleIdStr,
                OData: this.pickODataSegmentForSave(),
                TypeId: typeIdStr,
                NodeId: String(nodeId),
                TargetServerConnectionId: this.targetServer && (this.targetServer.id != null ? this.targetServer.id : this.targetServer.Id),
                ProviderName: this.targetSource,
                PlatformType: this.platformType,
                ChildrenSelection: this.childrenSelection,
                ItemSelectionMode: this.itemSelectionMode || "Normal",
                Culture: (this.importCulture || "").trim() || null,
                LibraryMediaKind: null,
                ParentId: this.needsParentSelection ? (this.blogParentId || null) : null,
                HtmlInlineImagesLibraryProviderName: this.htmlInlineImagesLibraryProviderName || null,
                HtmlInlineImagesLibraryDataSourceName: this.htmlInlineImagesLibraryDataSourceName || null,
                HtmlInlineImagesParentAlbumId: this.htmlInlineImagesParentAlbumId || null,
                HtmlInlineImagesParentAlbumTitle: this.htmlInlineImagesParentAlbumTitle || null,
                HtmlInlineImagesParentEntitySet: this.htmlInlineImagesParentEntitySet || null
            };

            try {
                await fetch("/api/SitefinityConnectivity/SaveMapping", {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(mappingJSON)
                });
                this.editingMappingId = null;
                await this.LoadMappings();
                this.editImportLoading = false;
                this.inSetup = false;
                Helpers.showNotification('Mappings saved successfully', 'success');
                this.$emit('imports-updated');
            } catch (error) {
                console.error('Error saving mappings:', error);
                Helpers.showNotification('Error saving mappings', 'error');
            }
        },
        async LoadMappings() {
            const connId = this.sourceConnectionId();
            const nodeId = this.sourceNodeId();
            this.existingMappings = [];
            if (connId == null || nodeId === "") {
                return;
            }
            try {
                const url = '/api/SitefinityConnectivity/GetMappings?connectionId=' + encodeURIComponent(connId) + '&nodeId=' + encodeURIComponent(String(nodeId));
                const mappings = await fetch(url);
                if (!mappings.ok) {
                    const errText = await mappings.text();
                    throw new Error((errText || mappings.statusText || 'Request failed').slice(0, 300));
                }
                const json = await mappings.json();
                this.existingMappings = json.result || [];
            } catch (error) {
                console.error('Error loading mappings:', error);
                const msg = (error && error.message) ? error.message : String(error);
                Helpers.showNotification('Error loading mappings: ' + msg, 'error');
            }
        },
        isValidSiteModulesDataSources(ds) {
            if (!Array.isArray(ds) || !ds.length) return false;
            const first = ds[0];
            return first && typeof first === "object" && !Array.isArray(first);
        },
        normalizeTargetModulesPayload(raw) {
            if (!raw || typeof raw !== "object") return null;
            const ds = raw.DataSources || raw.dataSources;
            if (this.isValidSiteModulesDataSources(ds)) return raw;
            return null;
        },
        normalizeImportEditModuleToken(raw) {
            if (raw == null) return "";
            let s = String(raw).trim().toLowerCase();
            if (!s) return "";
            if (s.endsWith("[]")) s = s.substring(0, s.length - 2);
            if (s.indexOf(".") >= 0) s = s.replace(/telerik\.sitefinity\./gi, "");
            if (s.endsWith("items")) s = s.substring(0, s.length - "items".length);
            s = s.replace(/[\s\-_]+/g, "");
            return s.trim();
        },
        matchDataSourceByToken(ds, token) {
            const want = this.normalizeImportEditModuleToken(token);
            if (!want || !ds || !ds.length) return null;
            for (let i = 0; i < ds.length; i++) {
                const m = ds[i];
                const name = this.normalizeImportEditModuleToken(m.Name != null ? m.Name : m.name);
                const title = this.normalizeImportEditModuleToken(m.Title != null ? m.Title : m.title);
                if (name === want || title === want) return m;
                const links = m.Links || m.links;
                if (!Array.isArray(links)) continue;
                for (let li = 0; li < links.length; li++) {
                    const lo = links[li];
                    const dsn = this.normalizeImportEditModuleToken(
                        lo && (lo.DataSourceName != null ? lo.DataSourceName : lo.dataSourceName)
                    );
                    if (dsn === want) return m;
                }
            }
            return null;
        },
        /** Match saved ModuleId / OData to a module from GetSiteModules (no indexOf('') — that matches every row). */
        findTargetModuleFromDataSources(ds, mapping) {
            if (!mapping || !ds || !ds.length) return null;
            const midRaw = mapping.moduleId != null ? mapping.moduleId : mapping.ModuleId;
            const mid = midRaw != null && String(midRaw).trim() !== "" ? String(midRaw).trim() : "";
            const odataRaw = mapping.odata != null ? mapping.odata : mapping.OData;
            const odata = odataRaw != null && String(odataRaw).trim() !== "" ? String(odataRaw).trim() : "";
            const typeId = mapping.typeId != null ? mapping.typeId : mapping.TypeId;

            if (mid) {
                const byMid = ds.find((m) => {
                    const id = m.moduleId != null ? m.moduleId : m.ModuleId;
                    return id != null && String(id).trim().toLowerCase() === mid.toLowerCase();
                });
                if (byMid) return byMid;
                const byMidToken = this.matchDataSourceByToken(ds, mid);
                if (byMidToken) return byMidToken;
            }
            if (odata) {
                const byOdata = this.matchDataSourceByToken(ds, odata);
                if (byOdata) return byOdata;
            }
            if (typeId) {
                const parts = String(typeId).split(".");
                const skip = { telerik: 1, sitefinity: 1, dynamictypes: 1, model: 1, libraries: 1, content: 1, items: 1 };
                for (let pi = 0; pi < parts.length; pi++) {
                    const p = parts[pi].trim();
                    if (p.length < 3 || skip[p.toLowerCase()]) continue;
                    const byType = this.matchDataSourceByToken(ds, p);
                    if (byType) return byType;
                }
            }
            return null;
        },
        mapRowsFromSaved(rows) {
            if (!Array.isArray(rows)) return [];
            return rows.map((row, i) => {
                let src = row.SourceFullDetails || row.sourceFullDetails;
                let tgt = row.TargetFullDetails || row.targetFullDetails;
                if (!src && row.Source != null) {
                    src = typeof row.Source === 'string' ? { name: row.Source, title: row.Source } : row.Source;
                }
                if (!tgt && row.Target != null) {
                    tgt = typeof row.Target === 'string' ? { name: row.Target, title: row.Target } : row.Target;
                }
                const targetTypeName = row.TargetTypeName != null ? row.TargetTypeName : row.targetTypeName;
                const rawKind = row.MappingKind != null ? row.MappingKind
                    : (row.mappingKind != null ? row.mappingKind
                        : (String(targetTypeName || "").toLowerCase() === "script" ? "Script" : "Field"));
                const mappingKind = String(rawKind || "Field").toLowerCase() === "script" ? "Script" : "Field";
                const rowId = Date.now() + i;
                var scriptBody = row.Script != null ? row.Script : (row.script != null ? row.script : "");
                if ((!scriptBody || String(scriptBody).trim() === "") && mappingKind === "Script") {
                    var legacy = row.scriptBody != null ? row.scriptBody : row.ScriptBody;
                    if (legacy != null) scriptBody = legacy;
                }
                return {
                    Id: rowId,
                    mappingKind: mappingKind,
                    Source: src || null,
                    Target: tgt || null,
                    targetTypeName: targetTypeName,
                    TargetTypeName: targetTypeName,
                    script: scriptBody,
                    scriptUniqueId: row.ScriptUniqueId != null ? row.ScriptUniqueId
                        : (row.scriptUniqueId != null ? row.scriptUniqueId : ("s-" + rowId)),
                    libraryProviderName: row.libraryProviderName != null ? row.libraryProviderName : row.LibraryProviderName,
                    libraryDataSourceName: row.libraryDataSourceName != null ? row.libraryDataSourceName : row.LibraryDataSourceName,
                    libraryParentAlbumId: row.libraryParentAlbumId != null ? row.libraryParentAlbumId : row.LibraryParentAlbumId,
                    libraryParentEntitySet: row.libraryParentEntitySet != null ? row.libraryParentEntitySet : row.LibraryParentEntitySet,
                    libraryParentAlbumTitle: row.libraryParentAlbumTitle != null ? row.libraryParentAlbumTitle : row.LibraryParentAlbumTitle
                };
            });
        },
        mappingMatchesSelectedNode(mapping) {
            if (!mapping || !this.selectedNode) return false;
            const startNodeId = String(mapping.nodeId != null ? mapping.nodeId : mapping.NodeId || "");
            const currentId = String(this.sourceNodeId() || "");
            if (startNodeId !== currentId) {
                Helpers.showNotification(
                    "This import is bound to another tree node. Select that node in the tree, then open Imports again.",
                    "warning"
                );
                return false;
            }
            return true;
        },
        duplicateMappingName(baseName) {
            const base = String(baseName || "Import").trim() || "Import";
            const exists = (name) => {
                return (this.existingMappings || []).some(function (m) {
                    return String(m.name != null ? m.name : m.Name || "").trim().toLowerCase() === name.toLowerCase();
                });
            };
            let candidate = base + " (copy)";
            let n = 2;
            while (exists(candidate)) {
                candidate = base + " (copy " + n + ")";
                n++;
            }
            return candidate;
        },
        duplicateTemplateName(baseName) {
            const base = String(baseName || "Template").trim() || "Template";
            const exists = (name) => {
                return (this.importTemplatesList || []).some(function (t) {
                    return String(t.name != null ? t.name : t.Name || "").trim().toLowerCase() === name.toLowerCase();
                });
            };
            let candidate = base + " (copy)";
            let n = 2;
            while (exists(candidate)) {
                candidate = base + " (copy " + n + ")";
                n++;
            }
            return candidate;
        },
        isTemplatePlanMapping(mapping) {
            if (!mapping) return false;
            const mode = String(mapping.importMode != null ? mapping.importMode : mapping.ImportMode || "").trim();
            if (mode.toLowerCase() === "templateplan") return true;
            const binds = mapping.typeBindings || mapping.TypeBindings;
            return Array.isArray(binds) && binds.length > 0;
        },
        askDuplicateTemplatesForPlan() {
            return window.confirm(
                "Also duplicate the reusable field-mapping templates?\n\n" +
                "OK — create a copy of each template and assign the copies to the new plan.\n" +
                "Cancel — keep the same template references as the original plan."
            );
        },
        buildImportMappingTemplateSavePayload(rec, overrides) {
            const r = rec || {};
            const o = overrides || {};
            const pick = (camel, pascal) => {
                const v = r[camel] != null ? r[camel] : r[pascal];
                return v != null && v !== "" ? v : null;
            };
            const cultureRaw = o.culture != null ? o.culture : pick("culture", "Culture");
            return {
                id: o.id != null ? o.id : 0,
                connectionId: Number(o.connectionId != null ? o.connectionId : pick("connectionId", "ConnectionId") || this.sourceConnectionId()),
                name: o.name != null ? o.name : (pick("name", "Name") || "Template"),
                description: pick("description", "Description") || "",
                platformType: pick("platformType", "PlatformType") || this.platformType || "",
                targetServerConnectionId: pick("targetServerConnectionId", "TargetServerConnectionId"),
                siteId: pick("siteId", "SiteId") || "",
                moduleId: pick("moduleId", "ModuleId") || "",
                oData: pick("oData", "OData") || "",
                typeId: pick("typeId", "TypeId") || "",
                providerName: pick("providerName", "ProviderName") || "",
                parentId: pick("parentId", "ParentId"),
                libraryMediaKind: pick("libraryMediaKind", "LibraryMediaKind"),
                culture: cultureRaw ? String(cultureRaw).trim() : null,
                htmlInlineImagesLibraryProviderName: pick("htmlInlineImagesLibraryProviderName", "HtmlInlineImagesLibraryProviderName"),
                htmlInlineImagesLibraryDataSourceName: pick("htmlInlineImagesLibraryDataSourceName", "HtmlInlineImagesLibraryDataSourceName"),
                htmlInlineImagesParentAlbumId: pick("htmlInlineImagesParentAlbumId", "HtmlInlineImagesParentAlbumId"),
                htmlInlineImagesParentAlbumTitle: pick("htmlInlineImagesParentAlbumTitle", "HtmlInlineImagesParentAlbumTitle"),
                htmlInlineImagesParentEntitySet: pick("htmlInlineImagesParentEntitySet", "HtmlInlineImagesParentEntitySet"),
                mappingsJSON: pick("mappingsJSON", "MappingsJSON") || "[]"
            };
        },
        async saveImportMappingTemplatePayload(payload) {
            const res = await fetch("/api/SitefinityConnectivity/SaveImportMappingTemplate", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(payload)
            });
            const json = await res.json();
            if (!res.ok || !json || json.success === false) {
                throw new Error((json && (json.message || json.Message)) || "Error saving template.");
            }
            const newId = json.id != null ? json.id : json.Id;
            if (newId == null) throw new Error("Template saved but no id was returned.");
            return newId;
        },
        async fetchImportMappingTemplateRecord(templateId, connId) {
            const editUrl = "/api/SitefinityConnectivity/GetImportMappingTemplateForEdit?templateId="
                + encodeURIComponent(templateId)
                + "&sourceConnectionId=" + encodeURIComponent(connId);
            const res = await fetch(editUrl);
            if (!res.ok) return null;
            const payload = await res.json();
            return (payload && payload.mapping) ? payload.mapping : null;
        },
        async refreshImportTemplatesList(connId) {
            const cid = connId != null ? connId : this.sourceConnectionId();
            if (cid == null) return;
            const r = await fetch("/api/SitefinityConnectivity/GetImportMappingTemplates?connectionId=" + encodeURIComponent(cid));
            const j = await r.json();
            this.importTemplatesList = j.result || j.Result || [];
        },
        collectTemplateIdsFromPlanRows() {
            const ids = new Set();
            const rows = this.templatePlanRows || [];
            for (let i = 0; i < rows.length; i++) {
                const a = String(rows[i].assignment || "").trim();
                if (!a || a === "SkipSubtree" || a === "SkipItemChildrenOnly") continue;
                const tid = Number(a);
                if (Number.isFinite(tid) && tid > 0) ids.add(tid);
            }
            return ids;
        },
        async duplicateTemplatesForImportPlan() {
            const connId = this.sourceConnectionId();
            if (connId == null) throw new Error("Connect a source before duplicating templates.");
            const templateIds = this.collectTemplateIdsFromPlanRows();
            if (!templateIds.size) return;
            const idMap = {};
            for (const oldId of templateIds) {
                let src = this.findImportTemplateById(oldId);
                const mj = src && (src.mappingsJSON != null ? src.mappingsJSON : src.MappingsJSON);
                if (!src || !mj) {
                    src = await this.fetchImportMappingTemplateRecord(oldId, connId);
                }
                if (!src) {
                    throw new Error("Could not load template #" + oldId + " for duplication.");
                }
                const baseName = src.name != null ? src.name : src.Name || "Template";
                const payload = this.buildImportMappingTemplateSavePayload(src, {
                    id: 0,
                    connectionId: Number(connId),
                    name: this.duplicateTemplateName(baseName)
                });
                idMap[oldId] = await this.saveImportMappingTemplatePayload(payload);
            }
            const rows = this.templatePlanRows || [];
            for (let i = 0; i < rows.length; i++) {
                const a = String(rows[i].assignment || "").trim();
                const tid = Number(a);
                if (idMap[tid]) rows[i].assignment = String(idMap[tid]);
            }
            await this.refreshImportTemplatesList(connId);
            await this.loadTemplateAssociationsForConnection();
        },
        async fetchImportMappingEditPayload(mapping) {
            if (!mapping) return null;
            const mappingId = mapping.id != null ? mapping.id : mapping.Id;
            const connId = this.sourceConnectionId();
            if (connId == null) {
                Helpers.showNotification("Connect a source before editing a mapping.", "error");
                return null;
            }
            const editUrl = "/api/SitefinityConnectivity/GetImportMappingForEdit?mappingId=" + encodeURIComponent(mappingId)
                + "&sourceConnectionId=" + encodeURIComponent(connId);
            const res = await fetch(editUrl);
            if (!res.ok) {
                let msg = "Could not load mapping.";
                try {
                    const errBody = await res.json();
                    msg = errBody.message || errBody.Message || msg;
                } catch (e2) { /* ignore */ }
                Helpers.showNotification(msg, "error");
                return null;
            }
            return res.json();
        },
        async applyTemplatePlanEditFromPayload(payload, duplicate) {
            const smPlan = payload.mapping || {};
            this.useClassicSingleTypeWizard = false;
            this.useTemplatePlanWizard = true;
            this.editingMappingId = duplicate ? null : (smPlan.id != null ? smPlan.id : smPlan.Id);
            this.MappingName = duplicate
                ? this.duplicateMappingName(smPlan.name || smPlan.Name || "Import plan")
                : (smPlan.name || smPlan.Name || "");
            this.MappingDescription = smPlan.description || smPlan.Description || "";
            this.childrenSelection = smPlan.childrenSelection || smPlan.ChildrenSelection || "Immediate";
            this.itemSelectionMode = smPlan.itemSelectionMode || smPlan.ItemSelectionMode || "Normal";
            this.importCulture = this.pickRecordString(smPlan, "culture", "Culture") || "";
            this.importTemplatesList = payload.importTemplates || [];
            await this.loadTemplatePlanStepData();
            const binds = smPlan.typeBindings || smPlan.TypeBindings || [];
            for (let ri = 0; ri < this.templatePlanRows.length; ri++) {
                const row = this.templatePlanRows[ri];
                const match = binds.find((b) => {
                    const bid = b.sourceTemplateId != null ? b.sourceTemplateId : b.SourceTemplateId;
                    const sid = row.sourceTemplateId;
                    if (bid && sid && String(bid).trim().toLowerCase() === String(sid).trim().toLowerCase()) return true;
                    const bn = (b.sourceTemplateName || b.SourceTemplateName || "").trim().toLowerCase();
                    const rn = (row.sourceTemplateName || "").trim().toLowerCase();
                    return bn && rn && bn === rn;
                });
                if (!match) continue;
                const smode = (match.skipMode || match.SkipMode || "Import").toString();
                if (smode === "SkipSubtree") row.assignment = "SkipSubtree";
                else if (smode === "SkipItemChildrenOnly") row.assignment = "SkipItemChildrenOnly";
                else {
                    const tid = match.importMappingTemplateId != null ? match.importMappingTemplateId : match.ImportMappingTemplateId;
                    row.assignment = tid != null ? String(tid) : "";
                }
            }
            this.currentStep = 1;
        },
        async openImportMappingForEdit(mapping, duplicate, duplicateTemplatesAlso) {
            if (!mapping || !this.mappingMatchesSelectedNode(mapping)) return;
            duplicate = !!duplicate;
            this.inSetup = true;
            this.currentStep = 0;
            this.disposeAllScriptEditors();
            this.mappings = null;
            this.restoringImportWizard = true;
            this.editImportLoading = true;
            const editMid = mapping.id != null ? mapping.id : mapping.Id;
            this.watchEditMappingProgress(editMid);
            try {
                await this.GetSitefinityConnections();
                const payload = await this.fetchImportMappingEditPayload(mapping);
                if (!payload) return;
                if (payload.templatePlanEdit) {
                    await this.applyTemplatePlanEditFromPayload(payload, duplicate);
                    if (duplicate && duplicateTemplatesAlso) {
                        await this.duplicateTemplatesForImportPlan();
                        Helpers.showNotification(
                            "Templates duplicated and linked to the new plan. Review culture and fields, then save.",
                            "success"
                        );
                    }
                    return;
                }
                await this.applyClassicMappingEditFromPayload(payload, duplicate);
                this.currentStep = 1;
                await this.$nextTick();
                this.remountAllScriptEditors();
            } catch (e) {
                console.error(duplicate ? "DuplicateMapping" : "EditMapping", e);
                Helpers.showNotification(
                    e.message || (duplicate ? "Could not duplicate mapping." : "Could not open mapping for editing."),
                    "error"
                );
            } finally {
                this.restoringImportWizard = false;
                this.editImportLoading = false;
                this.unwatchEditMappingProgress();
                if (this.needsParentSelection) {
                    this.$nextTick(() => this.loadParentOptions());
                }
            }
        },
        async DuplicateMapping(mapping) {
            if (!mapping || !this.mappingMatchesSelectedNode(mapping)) return;
            let duplicateTemplatesAlso = false;
            if (this.isTemplatePlanMapping(mapping)) {
                duplicateTemplatesAlso = this.askDuplicateTemplatesForPlan();
            }
            await this.openImportMappingForEdit(mapping, true, duplicateTemplatesAlso);
        },
        async applyClassicMappingEditFromPayload(payload, duplicate) {
            duplicate = !!duplicate;
            this.useClassicSingleTypeWizard = true;
            this.useTemplatePlanWizard = false;
            const sm = payload.mapping || {};
            const wantServerId = Number(
                sm.targetServerConnectionId != null ? sm.targetServerConnectionId : sm.TargetServerConnectionId
            );
            const serverIdStr = Number.isFinite(wantServerId) && wantServerId > 0 ? String(wantServerId) : "";
            this.targetServerConnectionId = serverIdStr;
            this.targetServer = this.resolveTargetServerByConnectionId(serverIdStr);
            await this.$nextTick();
            if (!this.targetServer) {
                const name = sm.name || sm.Name || "template";
                throw new Error(
                    "Target Sitefinity server (id " + serverIdStr + ") for \"" + name + "\" was not found in Target Servers. "
                    + "The connection may have been removed, or servers are still loading."
                );
            }
            this.targetSite = sm.siteId != null ? sm.siteId : sm.SiteId;
            this.targetModules = this.normalizeTargetModulesPayload(payload.targetModules);
            const idx = payload.resolvedModuleIndex;
            let ds = this.targetModules && (this.targetModules.DataSources || this.targetModules.dataSources || []);
            if (!this.isValidSiteModulesDataSources(ds) && this.targetSite) {
                await this.GetSiteModules(this.targetSite);
                ds = this.targetModules && (this.targetModules.DataSources || this.targetModules.dataSources || []);
            }
            if (!this.isValidSiteModulesDataSources(ds) || idx == null || idx < 0 || idx >= ds.length) {
                throw new Error("Could not resolve module selection from Sitefinity.");
            }
            this.targetModule = ds[idx];
            const payloadAll = Array.isArray(payload.allContentTypes) ? payload.allContentTypes : [];
            this.sitefinityAllContentTypes = payloadAll;
            this.sitefinityContentTypesLoadedForId = payloadAll.length ? this.targetServerId() : null;
            this.librarySourcesForSite = Array.isArray(payload.librarySources) ? payload.librarySources : [];
            if (payload.siteModuleDetails) {
                this.applySiteModuleDetailsFromFetchJson(payload.siteModuleDetails);
            }
            if (this.sitefinityAllContentTypes && this.sitefinityAllContentTypes.length && this.targetModule) {
                this.applyTargetTypesFromAllContentTypes();
            } else if (!payload.siteModuleDetails) {
                this.applyTargetTypesFromAllContentTypes();
            }

            if (!this.templateCreateMode && !duplicate) {
                this.editingMappingId = sm.id != null ? sm.id : sm.Id;
            } else if (duplicate) {
                this.editingMappingId = null;
            }
            this.MappingName = duplicate
                ? this.duplicateMappingName(sm.name || sm.Name || "Import")
                : (sm.name || sm.Name || "");
            this.MappingDescription = sm.description || sm.Description || "";
            if (!this.templateCreateMode) {
                this.childrenSelection = sm.childrenSelection || sm.ChildrenSelection || "Immediate";
                this.itemSelectionMode = sm.itemSelectionMode || sm.ItemSelectionMode || "Normal";
            }

            await this.$nextTick();
            const typeId = String(sm.typeId != null ? sm.typeId : sm.TypeId || "");
            const list = this.moduleContentTypesSorted;
            this.targetType = this.findTargetTypeByHints(typeId, list);
            this.rebindWizardTargetSelections(sm);
            this.targetSource = sm.providerName != null && sm.providerName !== ""
                ? sm.providerName
                : (sm.ProviderName != null && sm.ProviderName !== "" ? sm.ProviderName : null);

            this.blogParentId = sm.parentId != null && sm.parentId !== ""
                ? sm.parentId
                : (sm.ParentId != null && sm.ParentId !== "" ? sm.ParentId : null);

            this.applyHtmlInlineImagesFromRecord(sm);
            this.applyCultureFromRecord(sm);

            await this.applyTargetFieldsForEdit(payload);

            let rows = [];
            const mj = sm.mappingsJSON != null ? sm.mappingsJSON : sm.MappingsJSON;
            if (mj) {
                try { rows = JSON.parse(mj); } catch (e) { rows = []; }
            }
            this.mappings = this.mapRowsFromSaved(rows);
            if (this.targetSite && this.targetServer && (!this.librarySourcesForSite || !this.librarySourcesForSite.length)) {
                try {
                    await this.loadLibrarySourcesForSite();
                } catch (e) { /* optional */ }
            }
            await this.finishMappingRowsRebind();
        },
        async EditMapping(mapping) {
            await this.openImportMappingForEdit(mapping, false);
        },
        mappingIdsForOpsPoll() {
            if (!this.existingMappings || !this.existingMappings.length) return [];
            return this.existingMappings
                .map((m) => (m.id != null ? m.id : m.Id))
                .filter((id) => id != null);
        },
        applyLongRunningSnapshot(snap) {
            if (!snap) return;
            this.globalRunningImports = snap.runningImports || [];
            const statuses = snap.mappingStatuses || {};
            Object.keys(statuses).forEach((k) => {
                this.$set(this.importMappingStatusById, parseInt(k, 10), statuses[k]);
            });
        },
        startLongRunningPolling() {
            if (typeof LongRunningOps === "undefined") return;
            this.stopLongRunningPolling();
            const self = this;
            this._longRunningUnsub = LongRunningOps.subscribe(
                function (snap) { self.applyLongRunningSnapshot(snap); },
                function () { return self.mappingIdsForOpsPoll(); }
            );
        },
        stopLongRunningPolling() {
            if (this._longRunningUnsub) {
                this._longRunningUnsub();
                this._longRunningUnsub = null;
            }
        },
        async refreshLongRunningSnapshot() {
            if (typeof LongRunningOps === "undefined") return;
            await LongRunningOps.forceRefresh(() => this.mappingIdsForOpsPoll());
            this.applyLongRunningSnapshot(LongRunningOps.getSnapshot());
        },
        watchEditMappingProgress(mappingId) {
            this.unwatchEditMappingProgress();
            if (typeof LongRunningOps === "undefined" || mappingId == null) return;
            const self = this;
            const opId = LongRunningOps.ids.mappingEdit(mappingId);
            this._editWatchUnsub = LongRunningOps.watch(opId, function (op) {
                self.editImportProgressMessage = LongRunningOps.operationMessage(op);
            }, function () { return self.mappingIdsForOpsPoll(); });
        },
        unwatchEditMappingProgress() {
            if (this._editWatchUnsub) {
                this._editWatchUnsub();
                this._editWatchUnsub = null;
            }
            this.editImportProgressMessage = "";
        },
        watchSitefinityInitProgress(connectionId) {
            this.unwatchSitefinityInitProgress();
            if (typeof LongRunningOps === "undefined" || connectionId == null) return;
            const self = this;
            const opId = LongRunningOps.ids.sitefinityInit(connectionId);
            this._initWatchUnsub = LongRunningOps.watch(opId, function (op) {
                self.loadingMappingsProgressMessage = LongRunningOps.operationMessage(op);
            });
        },
        unwatchSitefinityInitProgress() {
            if (this._initWatchUnsub) {
                this._initWatchUnsub();
                this._initWatchUnsub = null;
            }
            this.loadingMappingsProgressMessage = "";
        },
        /** True if this mapping has the active import (global list or per-mapping status). */
        isMappingImportRunning(mapping) {
            if (!mapping) return false;
            const mid = mapping.id != null ? mapping.id : mapping.Id;
            const g = (this.globalRunningImports || []).find(r => (r.mappingId != null ? r.mappingId : r.MappingId) === mid);
            if (g) return true;
            const s = this.importMappingStatusById[mid];
            if (!s) return false;
            const run = s.isRunning !== undefined ? s.isRunning : s.IsRunning;
            return run === true;
        },
        /** Disable Run when any import is running anywhere (single global import lock). */
        isRunImportDisabled(mapping) {
            return (this.globalRunningImports && this.globalRunningImports.length > 0)
                || this.isMappingImportRunning(mapping);
        },
        itemSelectionModeLabel(mode) {
            const m = String(mode || "Normal").trim().toLowerCase();
            return m === "explicit" ? "Explicit" : "Normal";
        },
        runImportDisabledTitle(mapping) {
            if (!this.globalRunningImports || !this.globalRunningImports.length) {
                return this.isMappingImportRunning(mapping) ? "An import is already running" : "";
            }
            const mid = mapping && (mapping.id != null ? mapping.id : mapping.Id);
            const gr0 = this.globalRunningImports[0];
            const gMid = gr0 && (gr0.mappingId != null ? gr0.mappingId : gr0.MappingId);
            if (gMid === mid) return "An import is already running for this mapping";
            return "Another import is in progress — use Force stop above or on that mapping.";
        },
        mappingRunningRunId(mapping) {
            if (!mapping) return null;
            const mid = mapping.id != null ? mapping.id : mapping.Id;
            const g = (this.globalRunningImports || []).find(r => (r.mappingId != null ? r.mappingId : r.MappingId) === mid);
            if (g) {
                const id = g.id != null ? g.id : g.Id;
                return id != null ? id : null;
            }
            const s = this.importMappingStatusById[mid];
            if (!s) return null;
            return s.runningRunId != null ? s.runningRunId : s.RunningRunId;
        },
        globalRunningRunIdForBanner() {
            const gr0 = this.globalRunningImports && this.globalRunningImports[0];
            if (!gr0) return null;
            const id = gr0.id != null ? gr0.id : gr0.Id;
            return id != null ? id : null;
        },
        mappingImportStatusLine(mapping) {
            if (!mapping) return "";
            const mid = mapping.id != null ? mapping.id : mapping.Id;
            const s = this.importMappingStatusById[mid];
            if (!s) return "";
            const lr = s.lastRun != null ? s.lastRun : s.LastRun;
            if (!lr) return "";
            const st = lr.status != null ? lr.status : lr.Status;
            const proc = lr.processedItems != null ? lr.processedItems : lr.ProcessedItems;
            const tot = lr.totalItems != null ? lr.totalItems : lr.TotalItems;
            if (this.isMappingImportRunning(mapping)) {
                return "Import running…" + (proc != null && tot != null ? " " + proc + " / " + tot : "");
            }
            if (st) return "Last run: " + st + (proc != null && tot != null ? " (" + proc + "/" + tot + ")" : "");
            return "";
        },
        async forceStopImport(mapping) {
            if (!mapping) return;
            const rid = this.mappingRunningRunId(mapping);
            if (rid == null) {
                Helpers.showNotification("No active run id for this mapping.", "warning");
                return;
            }
            await this.forceStopImportByRunId(rid);
        },
        async forceStopImportByRunId(runId) {
            if (runId == null) return;
            if (!confirm("Force stop this import?")) return;
            try {
                const r = await fetch("/api/Import/ForceStopImport?runId=" + encodeURIComponent(runId), { method: "POST" });
                if (!r.ok) {
                    const t = await r.text();
                    throw new Error(t || r.statusText);
                }
                Helpers.showNotification("Stop requested.", "success");
                await this.refreshLongRunningSnapshot();
            } catch (e) {
                console.error("ForceStopImport", e);
                Helpers.showNotification("Could not stop import.", "error");
            }
        },
        isExplicitItemSelectionMode(mapping) {
            const mode = mapping && (mapping.itemSelectionMode != null ? mapping.itemSelectionMode : mapping.ItemSelectionMode);
            return String(mode || "Normal").trim().toLowerCase() === "explicit";
        },
        getImportMarksForRun() {
            const connId = this.sourceConnectionId();
            if (connId == null || typeof ImportMarksStore === "undefined") {
                return { excludedIds: [], includedIds: [] };
            }
            const plat = this.platformType || (this.connection && this.connection.platformType) || "Sitecore";
            const excluded = ImportMarksStore.getExcludedIds(connId, plat);
            const included = ImportMarksStore.getIncludedIds(connId, plat);
            return {
                excludedIds: excluded instanceof Set ? [...excluded] : (Array.isArray(excluded) ? excluded : []),
                includedIds: included instanceof Set ? [...included] : (Array.isArray(included) ? included : [])
            };
        },
        async RunImport(mapping) {
            if (this.isRunImportDisabled(mapping)) {
                Helpers.showNotification(this.runImportDisabledTitle(mapping) || "There is already a running import.", "warning");
                return;
            }
            if (this.isExplicitItemSelectionMode(mapping)) {
                const marks = this.getImportMarksForRun();
                if (!marks.includedIds || marks.includedIds.length === 0) {
                    this.showImportErrorModal(
                        "This plan uses Explicit item selection. Mark at least one item for import in Node Details (or mark a folder to include its subtree), then run again.",
                        "No items selected"
                    );
                    return;
                }
            }
            const marks = this.getImportMarksForRun();
            const importPayload = {
                connectionId: this.sourceConnectionId(),
                sourceItemId: this.sourceNodeId(),
                mappingId: mapping.id,
                name: `${mapping.name}`,
                excludedIds: marks.excludedIds,
                includedIds: marks.includedIds
            };

            try {
                const running = await fetch("/api/Import/StartImport", {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(importPayload)
                });

                if (running.status == 400) {
                    const errMsg = await running.text();
                    Helpers.showNotification(`Error starting import: ${errMsg}`, 'error');
                } else {
                    Helpers.showNotification('Import started successfully', 'success');
                    await this.refreshLongRunningSnapshot();
                    if (this.importLogVisible && this.importLogMapping && this.importLogMapping.id === mapping.id) {
                        this.loadImportLog(mapping);
                    }
                }
            } catch (error) {
                console.error('Error running import:', error);
                Helpers.showNotification('Error starting import', 'error');
            }
        },
        openImportLog(mapping) {
            this.importLogMapping = mapping;
            this.importLogVisible = true;
            this.importLogView = "runs";
            this.importLogSelectedRun = null;
            this.importLogRunDetail = null;
            this.importLogRunItems = [];
            this.importLogSelectedItem = null;
            this.loadImportLog(mapping);
        },
        closeImportLog() {
            this.importLogVisible = false;
            this.importLogMapping = null;
            this.importLogRows = [];
            this.importLogView = "runs";
            this.importLogSelectedRun = null;
            this.importLogRunDetail = null;
            this.importLogRunItems = [];
            this.importLogSelectedItem = null;
        },
        async loadImportLog(mapping) {
            if (!mapping || !mapping.id) return;
            this.importLogLoading = true;
            try {
                const res = await fetch(`/api/Import/GetImportHistory?mappingId=${encodeURIComponent(mapping.id)}&count=20`);
                if (!res.ok) {
                    const t = await res.text();
                    throw new Error(t || res.statusText);
                }
                this.importLogRows = await res.json();
            } catch (e) {
                console.error('GetImportHistory', e);
                Helpers.showNotification('Could not load import history.', 'error');
                this.importLogRows = [];
            } finally {
                this.importLogLoading = false;
            }
        },
        formatImportLogDate(val) {
            if (val == null || val === '') return '—';
            const d = new Date(val);
            if (isNaN(d.getTime())) return String(val);
            return d.toLocaleString();
        },
        importLogProgress(row) {
            const p = row.processedItems != null ? row.processedItems : row.ProcessedItems;
            const t = row.totalItems != null ? row.totalItems : row.TotalItems;
            if (p == null && t == null) return '—';
            const ps = p != null ? p : '—';
            const ts = t != null ? t : '—';
            return `${ps} / ${ts}`;
        },
        /** User-facing outcome: Success, Failed, With errors, or raw status (e.g. Running). */
        importLogOutcome(row) {
            const st = (row.status != null ? row.status : row.Status || '').toString();
            const err = (row.errorCount != null ? row.errorCount : row.ErrorCount);
            const errNum = err != null ? Number(err) : 0;
            if (st === 'Failed') return 'Failed';
            if (st === 'CompletedWithErrors' || errNum > 0) return 'With errors';
            if (st === 'Running') return 'Running';
            if (st === 'Completed' || st === 'Finished') return 'Success';
            return st !== '' ? st : '—';
        },
        importLogErrCount(row) {
            const n = row.errorCount != null ? row.errorCount : row.ErrorCount;
            if (n === null || n === undefined) return '—';
            return n;
        },
        importLogWarnCount(row) {
            const n = row.warningCount != null ? row.warningCount : row.WarningCount;
            if (n === null || n === undefined) return '—';
            return n;
        },
        importLogMessage(row) {
            const m = row.finishReason != null ? row.finishReason : row.FinishReason;
            return m != null && String(m).trim() !== '' ? String(m).trim() : '—';
        },
        openImportRunDetail(row) {
            if (!row) return;
            const rid = row.id != null ? row.id : row.Id;
            if (rid == null) return;
            this.importLogSelectedRun = row;
            this.importLogView = "items";
            this.importLogSelectedItem = null;
            this.importLogRunDetail = null;
            this.importLogRunItems = [];
            this.loadImportRunDetail(rid);
        },
        async loadImportRunDetail(runId) {
            this.importLogRunItemsLoading = true;
            try {
                const res = await fetch(`/api/Import/GetImportRunDetail?runId=${encodeURIComponent(runId)}&maxItems=3000`);
                if (!res.ok) {
                    const t = await res.text();
                    throw new Error(t || res.statusText);
                }
                const json = await res.json();
                this.importLogRunDetail = json;
                const items = json.items != null ? json.items : json.Items;
                this.importLogRunItems = Array.isArray(items) ? items : [];
            } catch (e) {
                console.error("GetImportRunDetail", e);
                Helpers.showNotification("Could not load import run detail.", "error");
                this.importLogRunItems = [];
            } finally {
                this.importLogRunItemsLoading = false;
            }
        },
        openImportItemDetail(item) {
            this.importLogSelectedItem = item;
            this.importLogView = "steps";
        },
        importLogBackToRuns() {
            this.importLogView = "runs";
            this.importLogSelectedRun = null;
            this.importLogRunDetail = null;
            this.importLogRunItems = [];
            this.importLogSelectedItem = null;
        },
        importLogBackToItems() {
            this.importLogView = "items";
            this.importLogSelectedItem = null;
        },
        importLogItemSteps(item) {
            if (!item) return [];
            const s = item.steps != null ? item.steps : item.Steps;
            return Array.isArray(s) ? s : [];
        },
        importLogItemOutcome(item) {
            if (!item) return "—";
            const ok = item.success != null ? item.success : item.Success;
            const sec = item.hasSecondaryErrors != null ? item.hasSecondaryErrors : item.HasSecondaryErrors;
            if (ok === true) return "OK";
            if (sec === true) return "Succeeded with errors";
            if (ok === false) return "Failed";
            return "—";
        },
        importLogItemOutcomeStyle(item) {
            if (!item) return {};
            const ok = item.success != null ? item.success : item.Success;
            const sec = item.hasSecondaryErrors != null ? item.hasSecondaryErrors : item.HasSecondaryErrors;
            if (sec === true && ok === false) return { color: "#e8a04a" };
            if (ok === false) return { color: "#f88" };
            if (ok === true) return { color: "var(--accent-bright)" };
            return {};
        },
        importLogStepIsError(st) {
            return st && (st.isError === true || st.IsError === true);
        },
        importLogStepCategoryStyle(st) {
            const base = { fontSize: "0.72rem", textTransform: "uppercase", letterSpacing: "0.04em" };
            return this.importLogStepIsError(st) ? { ...base, color: "#f88" } : { ...base, color: "var(--accent-bright)" };
        },
        importLogStepSummaryStyle(st) {
            return this.importLogStepIsError(st) ? { color: "#f88", fontWeight: "500" } : { color: "var(--text-primary)" };
        },
        importLogStepDetailStyle(st) {
            return this.importLogStepIsError(st) ? { color: "#f99" } : { color: "var(--text-secondary)" };
        },
        async DeleteMapping(mapping) {
            if (confirm("Are you sure you want to delete this mapping?")) {
                try {
                    await fetch(`/api/SitefinityConnectivity/DeleteMapping?mappingId=${mapping.id}`);
                    this.LoadMappings();
                    Helpers.showNotification('Mapping deleted successfully', 'success');
                    this.$emit('imports-updated');
                } catch (error) {
                    console.error('Error deleting mapping:', error);
                    Helpers.showNotification('Error deleting mapping', 'error');
                }
            }
        },
        async ClearMemory(mapping) {
            if (confirm("This operation is intended if you have imported items and then deleted them.\nAre you sure you want to proceed?")) {
                try {
                    const res = await fetch(`/api/Import/ClearMemoryMappings?mappingId=${mapping.id}`);
                    const jsonRes = await res.json();
                    if (jsonRes && jsonRes.NumberOfItemsRemoved && jsonRes.Succeess) {
                        Helpers.showNotification(`Successfully removed ${jsonRes.NumberOfItemsRemoved} items from memory`, 'success');
                        this.LoadMappings();
                    }
                } catch (error) {
                    console.error('Error clearing memory:', error);
                    Helpers.showNotification('Error clearing memory', 'error');
                }
            }
        },
        PopulateItemFieldMapping(mapping) {
            if (mapping && mapping.Target) {
                const tn = this.pickTargetTypeName(mapping.Target);
                if (tn) this.$set(mapping, "TargetTypeName", tn);
            }
            if (!mapping || !mapping.Target || !this.targetRowIsImageField(mapping)) {
                if (mapping) {
                    this.$delete(mapping, "libraryProviderName");
                    this.$delete(mapping, "libraryDataSourceName");
                    this.$delete(mapping, "libraryParentAlbumId");
                    this.$delete(mapping, "libraryParentEntitySet");
                    this.$delete(mapping, "libraryParentAlbumTitle");
                }
            } else if (mapping && !mapping.libraryParentEntitySet) {
                this.$set(mapping, "libraryParentEntitySet", "albums");
            }
        },
        isTargetFieldImageLike(f) {
            if (!f) return false;
            if (this.classifyRelatedMediaKind(f) === "image") return true;
            const canon = this.pickTargetTypeName(f);
            if (String(canon).toLowerCase() === "relatedimage") return true;
            const t = ((f.typeName != null ? f.typeName : f.TypeName) || "") + " " + ((f.typeUIName != null ? f.typeUIName : f.TypeUIName) || "");
            const lower = t.toLowerCase();
            if (lower.includes("image") && (lower.includes("library") || lower.includes("media") || lower.includes("related") || lower.includes("single"))) return true;
            if (lower.includes("related image") || lower.includes("single-image") || lower.includes("singleimage")) return true;
            if (f.allowMultipleImages === true || f.AllowMultipleImages === true) return true;
            if (f.allowImageLibrary === true || f.AllowImageLibrary === true) return true;
            return false;
        },
        targetRowIsImageField(row) {
            return !!(row && row.Target && this.isTargetFieldImageLike(row.Target));
        },
        libraryProviderKey(s) {
            if (!s) return "";
            const pn = s.providerName != null ? s.providerName : s.ProviderName;
            const ds = s.dataSourceName != null ? s.dataSourceName : s.DataSourceName;
            return String(pn || "") + "::" + String(ds || "");
        },
        libraryProviderLabel(s) {
            if (!s) return "";
            const pt = s.providerTitle != null ? s.providerTitle : s.ProviderTitle;
            const ds = s.dataSourceName != null ? s.dataSourceName : s.DataSourceName;
            if (pt != null && String(pt).trim() !== "") return String(pt).trim();
            if (ds != null && String(ds).trim() !== "") return String(ds).trim();
            return this.libraryProviderKey(s);
        },
        async loadLibrarySourcesForSite() {
            const cid = this.targetServerId();
            if (cid == null || !this.targetSite) return;
            this.librarySourcesLoading = true;
            try {
                const res = await fetch(
                    "/api/SitefinityConnectivity/GetLibraryProviderSources?connectionId=" +
                        encodeURIComponent(cid) +
                        "&siteId=" +
                        encodeURIComponent(this.targetSite)
                );
                const json = await res.json();
                const raw = json.sources != null ? json.sources : json.Sources;
                this.librarySourcesForSite = Array.isArray(raw) ? raw : [];
            } catch (e) {
                console.error("loadLibrarySourcesForSite", e);
                this.librarySourcesForSite = [];
                Helpers.showNotification("Could not load library providers for this site.", "error");
            } finally {
                this.librarySourcesLoading = false;
            }
        },
        async loadLibraryAlbumsForDialog(providerName, entitySet) {
            const cid = this.targetServerId();
            if (cid == null || !this.targetSite || !providerName) {
                this.libraryFolderAlbums = [];
                return;
            }
            const seg = entitySet && String(entitySet).trim() !== "" ? String(entitySet).trim() : "albums";
            this.libraryFolderAlbumsLoading = true;
            try {
                const u =
                    "/api/SitefinityConnectivity/GetParentList?connectionId=" +
                    encodeURIComponent(cid) +
                    "&siteId=" +
                    encodeURIComponent(this.targetSite) +
                    "&providerName=" +
                    encodeURIComponent(providerName) +
                    "&entitySet=" +
                    encodeURIComponent(seg);
                const res = await fetch(u);
                if (!res.ok) {
                    this.libraryFolderAlbums = [];
                    return;
                }
                const json = await res.json();
                const raw = json.blogs != null ? json.blogs : json.Blogs;
                const arr = Array.isArray(raw) ? raw : [];
                this.libraryFolderAlbums = arr.map((b) => ({
                    id: b.id != null ? b.id : b.Id,
                    title: (b.title != null ? b.title : b.Title) || ""
                }));
            } catch (e) {
                console.error("loadLibraryAlbumsForDialog", e);
                this.libraryFolderAlbums = [];
            } finally {
                this.libraryFolderAlbumsLoading = false;
            }
        },
        async openLibraryFolderDialog(row) {
            this.libraryFolderDialogMode = "fieldRow";
            this.libraryFolderDialogRow = row;
            this.libraryFolderTempAlbumId = row.libraryParentAlbumId || "";
            const key =
                row.libraryProviderName && row.libraryDataSourceName
                    ? row.libraryProviderName + "::" + row.libraryDataSourceName
                    : "";
            this.libraryFolderTempProviderKey = key;
            this.libraryFolderDialogOpen = true;
            if (!this.librarySourcesForSite.length) await this.loadLibrarySourcesForSite();
            const src = this.librarySourcesForSite.find((s) => this.libraryProviderKey(s) === this.libraryFolderTempProviderKey);
            const pn = src ? (src.providerName != null ? src.providerName : src.ProviderName) : "";
            const pe = src
                ? src.suggestedParentEntitySet != null
                    ? src.suggestedParentEntitySet
                    : src.SuggestedParentEntitySet
                : "albums";
            if (pn) await this.loadLibraryAlbumsForDialog(pn, pe || "albums");
            else this.libraryFolderAlbums = [];
        },
        async openHtmlInlineImagesFolderDialog() {
            this.libraryFolderDialogMode = "htmlInline";
            this.libraryFolderDialogRow = null;
            this.libraryFolderTempAlbumId = this.htmlInlineImagesParentAlbumId || "";
            const key =
                this.htmlInlineImagesLibraryProviderName && this.htmlInlineImagesLibraryDataSourceName
                    ? this.htmlInlineImagesLibraryProviderName + "::" + this.htmlInlineImagesLibraryDataSourceName
                    : "";
            this.libraryFolderTempProviderKey = key;
            this.libraryFolderDialogOpen = true;
            if (!this.librarySourcesForSite.length) await this.loadLibrarySourcesForSite();
            const src = this.librarySourcesForSite.find((s) => this.libraryProviderKey(s) === this.libraryFolderTempProviderKey);
            const pn = src ? (src.providerName != null ? src.providerName : src.ProviderName) : "";
            const pe = src
                ? src.suggestedParentEntitySet != null
                    ? src.suggestedParentEntitySet
                    : src.SuggestedParentEntitySet
                : this.htmlInlineImagesParentEntitySet || "albums";
            if (pn) await this.loadLibraryAlbumsForDialog(pn, pe || "albums");
            else this.libraryFolderAlbums = [];
        },
        closeLibraryFolderDialog() {
            this.libraryFolderDialogOpen = false;
            this.libraryFolderDialogRow = null;
            this.libraryFolderDialogMode = "fieldRow";
            this.libraryFolderTempProviderKey = "";
            this.libraryFolderTempAlbumId = "";
            this.libraryFolderAlbums = [];
        },
        async onLibraryFolderProviderKeyChange() {
            const src = this.librarySourcesForSite.find((s) => this.libraryProviderKey(s) === this.libraryFolderTempProviderKey);
            const pn = src ? (src.providerName != null ? src.providerName : src.ProviderName) : "";
            const pe = src
                ? src.suggestedParentEntitySet != null
                    ? src.suggestedParentEntitySet
                    : src.SuggestedParentEntitySet
                : "albums";
            this.libraryFolderTempAlbumId = "";
            if (pn) await this.loadLibraryAlbumsForDialog(pn, pe || "albums");
            else this.libraryFolderAlbums = [];
        },
        applyLibraryFolderDialog() {
            if (this.libraryFolderDialogMode === "htmlInline") {
                const src = this.librarySourcesForSite.find((s) => this.libraryProviderKey(s) === this.libraryFolderTempProviderKey);
                if (src) {
                    this.htmlInlineImagesLibraryProviderName = src.providerName != null ? src.providerName : src.ProviderName;
                    this.htmlInlineImagesLibraryDataSourceName = src.dataSourceName != null ? src.dataSourceName : src.DataSourceName;
                    const pe = src.suggestedParentEntitySet != null ? src.suggestedParentEntitySet : src.SuggestedParentEntitySet;
                    this.htmlInlineImagesParentEntitySet = pe || "albums";
                } else {
                    this.htmlInlineImagesLibraryProviderName = "";
                    this.htmlInlineImagesLibraryDataSourceName = "";
                    this.htmlInlineImagesParentEntitySet = "albums";
                }
                const albumTitle = (this.libraryFolderAlbums.find((x) => x.id === (this.libraryFolderTempAlbumId || "")) || {}).title;
                this.htmlInlineImagesParentAlbumId = this.libraryFolderTempAlbumId || "";
                this.htmlInlineImagesParentAlbumTitle = albumTitle || "";
                this.closeLibraryFolderDialog();
                return;
            }
            const row = this.libraryFolderDialogRow;
            if (!row) {
                this.closeLibraryFolderDialog();
                return;
            }
            const src = this.librarySourcesForSite.find((s) => this.libraryProviderKey(s) === this.libraryFolderTempProviderKey);
            if (src) {
                this.$set(row, "libraryProviderName", src.providerName != null ? src.providerName : src.ProviderName);
                this.$set(row, "libraryDataSourceName", src.dataSourceName != null ? src.dataSourceName : src.DataSourceName);
                const pe = src.suggestedParentEntitySet != null ? src.suggestedParentEntitySet : src.SuggestedParentEntitySet;
                this.$set(row, "libraryParentEntitySet", pe || "albums");
            } else {
                this.$set(row, "libraryProviderName", "");
                this.$set(row, "libraryDataSourceName", "");
                this.$set(row, "libraryParentEntitySet", "albums");
            }
            const albumTitle = (this.libraryFolderAlbums.find((x) => x.id === (this.libraryFolderTempAlbumId || "")) || {}).title;
            this.$set(row, "libraryParentAlbumId", this.libraryFolderTempAlbumId || "");
            this.$set(row, "libraryParentAlbumTitle", albumTitle || "");
            this.closeLibraryFolderDialog();
        },
        libraryFolderSummary(row) {
            if (!row || !row.libraryParentAlbumId) return "—";
            const t = row.libraryParentAlbumTitle || row.LibraryParentAlbumTitle;
            if (t) return t;
            return row.libraryParentAlbumId;
        },
        htmlInlineImagesFolderSummary() {
            if (!this.htmlInlineImagesParentAlbumId) return "—";
            if (this.htmlInlineImagesParentAlbumTitle) return this.htmlInlineImagesParentAlbumTitle;
            return this.htmlInlineImagesParentAlbumId;
        }
    },
    template: `
        <div class="import-tab">
            <error-message-modal
                :visible="importErrorModalVisible"
                :title="importErrorModalTitle"
                :message="importErrorModalMessage"
                :details="importErrorModalDetails"
                @close="closeImportErrorModal">
            </error-message-modal>

            <div v-show="inSetup" class="connection-overlay import-wizard-overlay">
                <div class="connect-panel connection-dialog-panel import-wizard-panel" style="position:relative;">
                    <div class="connection-dialog-header">
                        <h2>{{ editingMappingId ? 'Edit import' : 'Import Wizard' }}</h2>
                        <button type="button" class="connection-dialog-close" @click="inSetup = false; editImportLoading = false" aria-label="Close">
                            <i class="bi bi-x-lg"></i>
                        </button>
                    </div>
                    <template v-if="editImportLoading">
                        <div class="dashboard-loading-overlay import-edit-loading">
                            <div class="import-edit-loading-icon" aria-hidden="true">
                                <i class="bi bi-hourglass-split"></i>
                            </div>
                            <div class="dashboard-loading-text">This could take a while…</div>
                            <p v-if="editImportProgressMessage" class="dashboard-loading-sub import-edit-loading-sub" style="color:var(--accent-bright); font-weight:500;">
                                {{ editImportProgressMessage }}
                            </p>
                            <p class="dashboard-loading-sub import-edit-loading-sub">
                                Loading mapping and preparing Sitefinity target data (modules, content types, fields, and libraries). If this target Sitefinity has not been initialized yet, the first load is slower; after that, opening Edit is usually much faster.
                            </p>
                        </div>
                    </template>
                    <template v-else>
                    <div v-if="loadingMappingsStep" class="dashboard-loading-overlay import-wizard-preparing-overlay" style="position:absolute; inset:0; z-index:5; border-radius:inherit; min-height:200px; display:flex; flex-direction:column; align-items:center; justify-content:center; background:rgba(12,14,18,0.72); backdrop-filter:blur(4px);">
                        <div class="spinner-ring"></div>
                        <div class="dashboard-loading-text" style="margin-top:1rem;">Loading target fields…</div>
                        <p v-if="loadingMappingsProgressMessage" class="dashboard-loading-sub" style="max-width:22rem; text-align:center; color:var(--accent-bright); font-weight:500;">{{ loadingMappingsProgressMessage }}</p>
                        <div class="dashboard-loading-sub" style="max-width:22rem; text-align:center;">Preparing Sitefinity content types and field definitions for mappings.</div>
                    </div>
                    <p style="color:var(--text-secondary); font-size:0.88rem; margin:0 0 1.25rem 0;">
                        Map source content to a Sitefinity target. Current platform: <strong style="color:var(--accent-bright);">{{ platformType }}</strong>
                    </p>

                    <div class="connection-dialog-section" style="margin-top:0; padding-top:0; border-top:none;">
                        <div class="detail-field-label">Mapping Name</div>
                        <input class="form-control-dark" v-model="MappingName" type="text" placeholder="e.g. News to Blog"
                            style="width:100%; margin-bottom:1rem; font-family:'IBM Plex Sans',sans-serif;" />
                        <div class="detail-field-label">Description</div>
                        <textarea class="form-control-dark" v-model="MappingDescription" placeholder="Optional description"
                            style="width:100%; min-height:60px; margin-bottom:1rem; font-family:'IBM Plex Sans',sans-serif; resize:vertical;"></textarea>
                        <p v-if="templateCreateMode" style="color:var(--accent-bright); font-size:0.84rem; margin:0 0 1rem 0;">
                            <template v-if="editingTemplateId">Editing reusable mapping template — map source fields to Sitefinity target fields below.</template>
                            <template v-else>Creating reusable mapping template — pick Sitefinity target on step 1, then map fields.</template>
                        </p>
                        <label v-if="!templateCreateMode" style="display:flex; align-items:center; gap:0.5rem; margin-bottom:1rem; cursor:pointer; font-size:0.84rem; color:var(--text-secondary);">
                            <input type="checkbox" v-model="useClassicSingleTypeWizard" />
                            <span>Use classic single-type mapping wizard (pick Sitefinity target and map fields on this screen)</span>
                        </label>
                    </div>

                    <!-- Step 0 (template plan): scope only -->
                    <div v-if="currentStep === 0 && useTemplatePlanWizard && !templateCreateMode" class="connection-dialog-section">
                        <table class="table-dark-custom import-wizard-table">
                            <tbody>
                                <tr>
                                    <td style="width:180px; color:var(--text-muted); font-weight:500;">Import scope</td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="childrenSelection" style="width:100%;">
                                            <option value="Immediate">Only immediate children of the selected tree node</option>
                                            <option value="Recursive">In depth — all descendants under the selected node (recursive)</option>
                                        </select>
                                    </td>
                                </tr>
                                <tr>
                                    <td style="color:var(--text-muted); font-weight:500;">Item selection</td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="itemSelectionMode" style="width:100%;">
                                            <option value="Normal">Normal — import all in scope except items marked Not needed</option>
                                            <option value="Explicit">Explicit — import only items marked for import</option>
                                        </select>
                                        <p style="color:var(--text-muted); font-size:0.78rem; margin:0.5rem 0 0 0; line-height:1.45;">Mark items in <strong>Node Details</strong>. Marking a folder includes its subtree. Skip and import marks cannot apply to the same item.</p>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                        <p style="color:var(--text-muted); font-size:0.82rem; margin:0.75rem 0 0 0; line-height:1.45;">
                            Next: assign a <strong>reusable template</strong> or skip behavior to each source content type under this node.
                            Templates belong to this source connection; create them via <code style="font-size:0.78rem;">POST /api/SitefinityConnectivity/SaveImportMappingTemplate</code> or tooling, then refresh this wizard.
                        </p>
                    </div>

                    <!-- Step 0: Server Info (classic) -->
                    <div v-if="currentStep === 0 && !useTemplatePlanWizard" class="connection-dialog-section">
                        <table class="table-dark-custom import-wizard-table">
                            <tbody>
                                <tr>
                                    <td style="width:180px; color:var(--text-muted); font-weight:500;">Import scope</td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="childrenSelection" style="width:100%;">
                                            <option value="Immediate">Only immediate children of the selected tree node</option>
                                            <option value="Recursive">In depth — all descendants under the selected node (recursive)</option>
                                        </select>
                                    </td>
                                </tr>
                                <tr>
                                    <td style="color:var(--text-muted); font-weight:500;">Item selection</td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="itemSelectionMode" style="width:100%;">
                                            <option value="Normal">Normal — import all in scope except items marked Not needed</option>
                                            <option value="Explicit">Explicit — import only items marked for import</option>
                                        </select>
                                        <p style="color:var(--text-muted); font-size:0.78rem; margin:0.5rem 0 0 0; line-height:1.45;">Mark items in <strong>Node Details</strong>. Marking a folder includes its subtree. Skip and import marks cannot apply to the same item.</p>
                                    </td>
                                </tr>
                                <tr>
                                    <td style="width:140px; color:var(--text-muted); font-weight:500;">Target Server</td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="targetServerConnectionId" style="width:100%;">
                                            <option value="">Select target server...</option>
                                            <option v-for="server in targetServers" :key="server.id || server.Id" :value="String(server.id != null ? server.id : server.Id)">{{ server.name || server.Name }}</option>
                                        </select>
                                    </td>
                                </tr>
                                <tr v-if="targetServer && serverSites(targetServer).length">
                                    <td style="color:var(--text-muted); font-weight:500;">Site</td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="targetSite" style="width:100%;">
                                            <option value="">Select site...</option>
                                            <option v-for="site in serverSites(targetServer)" :key="site.Id || site.id" :value="site.Id != null ? site.Id : site.id">{{ site.Name || site.name }}</option>
                                        </select>
                                    </td>
                                </tr>
                                <tr v-if="targetSite">
                                    <td style="color:var(--text-muted); font-weight:500;">Module</td>
                                    <td>
                                        <div v-if="loadingSiteModules" style="display:flex; align-items:center; gap:0.5rem;">
                                            <div class="spinner-ring" style="width:18px;height:18px;"></div>
                                            <span style="font-size:0.82rem; color:var(--text-muted);">Loading modules…</span>
                                        </div>
                                        <template v-else>
                                            <select class="form-control-dark select-dark" v-model="targetModuleSelectKey" style="width:100%;" :disabled="sitefinityModuleSelectDisabled">
                                                <option value="">{{ loadingSitefinityContentTypes ? 'Preparing content types…' : 'Select module…' }}</option>
                                                <option v-for="(module, idx) in targetModulesSortedDataSources" :key="moduleSelectKey(module) + '_' + idx" :value="moduleSelectKey(module)">{{ moduleLabelFromModule(module) }}</option>
                                            </select>
                                            <p v-if="!loadingSitefinityContentTypes && targetModules && !targetModulesDataSources.length" style="font-size:0.82rem; color:var(--text-muted); margin:0.35rem 0 0 0;">No modules returned for this site.</p>
                                            <p v-if="loadingSitefinityContentTypes" style="font-size:0.82rem; color:var(--text-muted); margin:0.35rem 0 0 0;">Preparing Sitefinity content types (assessment)…</p>
                                        </template>
                                    </td>
                                </tr>
                                <tr v-if="targetModule && targetTypes && moduleContentTypesSorted.length">
                                    <td style="color:var(--text-muted); font-weight:500;">Content Type</td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="targetTypeSelectKey" style="width:100%;">
                                            <option value="">Select content type...</option>
                                            <option v-for="(t, ctIdx) in moduleContentTypesSorted" :key="contentTypeSelectValue(t) + '_' + ctIdx" :value="contentTypeSelectValue(t)">{{ t.contentTypeItemName || t.name }}</option>
                                        </select>
                                    </td>
                                </tr>
                                <tr v-if="targetModule && targetTypes && !moduleContentTypesSorted.length">
                                    <td colspan="2" style="padding-top:0.5rem;">
                                        <p style="color:var(--text-muted); font-size:0.82rem; margin:0;">No content types matched this module. Run Sitefinity assessment (content types init) on the target connection, or check multisite module / DataSourceName vs assessment <code>moduleName</code>.</p>
                                    </td>
                                </tr>
                                <tr v-if="targetModule && targetModule.Links && targetType">
                                    <td style="color:var(--text-muted); font-weight:500;">Provider</td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="targetSource" style="width:100%;">
                                            <option value="">Select provider...</option>
                                            <option v-for="link in targetModule.Links" :key="link.ProviderName" :value="link.ProviderName">{{ link.ProviderTitle || link.ProviderName }}</option>
                                        </select>
                                    </td>
                                </tr>
                                <tr v-if="needsParentSelection && targetSite && targetSource">
                                    <td style="color:var(--text-muted); font-weight:500;">Parent</td>
                                    <td>
                                        <div v-if="loadingBlogParents" style="display:flex; align-items:center; gap:0.5rem;">
                                            <div class="spinner-ring" style="width:18px;height:18px;"></div>
                                            <span style="font-size:0.82rem; color:var(--text-muted);">Loading parents…</span>
                                        </div>
                                        <select v-else class="form-control-dark select-dark" v-model="blogParentId" style="width:100%;">
                                            <option value="">Select parent…</option>
                                            <option v-for="b in blogParentOptions" :key="b.id" :value="b.id">{{ b.title || b.id }}</option>
                                        </select>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </div>

                    <!-- Step 1: Template assignments -->
                    <div v-if="useTemplatePlanWizard && currentStep === 1 && !templateCreateMode" class="connection-dialog-section">
                        <div class="detail-field-label">Import scope (children)</div>
                        <select class="form-control-dark select-dark" v-model="childrenSelection" style="width:100%; max-width:480px; margin-bottom:1.25rem;">
                            <option value="Immediate">Only immediate children of the selected tree node</option>
                            <option value="Recursive">In depth — all descendants under the selected node (recursive)</option>
                        </select>
                        <div class="detail-field-label">Item selection</div>
                        <select class="form-control-dark select-dark" v-model="itemSelectionMode" style="width:100%; max-width:480px; margin-bottom:1.25rem;">
                            <option value="Normal">Normal — import all in scope except items marked Not needed</option>
                            <option value="Explicit">Explicit — import only items marked for import</option>
                        </select>
                        <div class="detail-field-label">Assign template per source content type</div>
                        <p v-if="loadingSourceTypesPlan" style="color:var(--text-muted); font-size:0.82rem;">Loading types under this node…</p>
                        <table v-else class="table-dark-custom import-wizard-table">
                            <thead>
                                <tr>
                                    <th>Source type</th>
                                    <th style="width:6rem;">Items</th>
                                    <th style="min-width:14rem;">Import using</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr v-for="(row, idx) in templatePlanRows" :key="'tpl-' + idx">
                                    <td>
                                        <strong style="color:var(--text-primary);">{{ row.sourceTemplateName || '—' }}</strong>
                                        <div v-if="row.sourceTemplateId" style="font-size:0.76rem; color:var(--text-muted); margin-top:0.15rem;">{{ row.sourceTemplateId }}</div>
                                    </td>
                                    <td>{{ row.itemCount }}</td>
                                    <td>
                                        <div style="display:flex; align-items:center; gap:0.5rem;">
                                        <select class="form-control-dark select-dark" v-model="row.assignment" style="width:100%;">
                                            <option value="">— Choose —</option>
                                            <option v-for="t in templateOptionsForRow(row)" :key="'opt-' + (t.id != null ? t.id : t.Id)" :value="String(t.id != null ? t.id : t.Id)">{{ t.name || t.Name }}</option>
                                            <option value="SkipSubtree">Skip item and subtree</option>
                                            <option value="SkipItemChildrenOnly">Skip item, process children</option>
                                        </select>
                                        <span style="display:flex; flex-direction:column; gap:0.2rem; flex-shrink:0;">
                                            <a v-if="rowHasSelectedTemplate(row)" href="javascript:void(0);" class="import-action-link" style="white-space:nowrap;" @click.prevent="beginEditTemplateForRow(row)">Edit mapping</a>
                                            <a href="javascript:void(0);" class="import-action-link" style="white-space:nowrap;" @click.prevent="beginCreateTemplateForRow(row)">Create new mapping</a>
                                        </span>
                                        </div>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                        <p v-if="!loadingSourceTypesPlan && templatePlanRows.length === 0" style="color:var(--text-muted); font-size:0.82rem;">No content types found for this scope. Adjust the tree node or import scope.</p>
                    </div>

                    <!-- Step 1: Field Mappings (classic or template edit) -->
                    <div v-if="showFieldMappingsStep" class="connection-dialog-section" style="position:relative;">
                        <div class="detail-field-label">Import scope (children)</div>
                        <select class="form-control-dark select-dark" v-model="childrenSelection" style="width:100%; max-width:480px; margin-bottom:1.25rem;">
                            <option value="Immediate">Only immediate children of the selected tree node</option>
                            <option value="Recursive">In depth — all descendants under the selected node (recursive)</option>
                        </select>
                        <div class="detail-field-label">Item selection</div>
                        <select class="form-control-dark select-dark" v-model="itemSelectionMode" style="width:100%; max-width:480px; margin-bottom:1.25rem;">
                            <option value="Normal">Normal — import all in scope except items marked Not needed</option>
                            <option value="Explicit">Explicit — import only items marked for import</option>
                        </select>
                        <div class="detail-field-label">Sitefinity language (culture)</div>
                        <input type="text" class="form-control-dark" v-model="importCulture" placeholder="e.g. en, de, fr (empty = default)"
                            style="max-width:480px; margin-bottom:0.35rem;" />
                        <p style="color:var(--text-muted); font-size:0.82rem; margin:0 0 1.25rem 0; line-height:1.45; max-width:42rem;">
                            When set, imports use OData <code style="font-size:0.78rem;">sf_culture</code> for POST/PATCH and related steps.
                            Item memory is shared by source item id across all import mappings on this connection — use a second mapping with a different culture to add or update a translation (PATCH), not a duplicate item.
                        </p>
                        <div class="detail-field-label" style="margin-bottom:0.75rem;">Map source fields to target fields</div>
                        <p v-if="loadingNodeFields" style="color:var(--text-muted); font-size:0.82rem; margin:0 0 0.75rem 0;">
                            <template v-if="usesScopeAggregateFields">Loading fields for all items in the import scope…</template>
                            <template v-else>Loading fields from Node Details…</template>
                        </p>
                        <template v-else>
                            <p v-if="isUmbracoPlatform" style="color:var(--text-muted); font-size:0.82rem; margin:0 0 0.75rem 0; line-height:1.45;">
                                Source lists every cms property alias for each content type that appears under this node
                                (<strong>{{ childrenSelection === 'Recursive' ? 'recursive: all descendants' : 'immediate children only' }}</strong>).
                                Adjust <strong>Import scope</strong> on this step or step 1 to refresh.
                            </p>
                            <p v-if="isCustomPlatform" style="color:var(--text-muted); font-size:0.82rem; margin:0 0 0.75rem 0; line-height:1.45;">
                                Source lists JSON field names from every item under this node in the same scope
                                (<strong>{{ childrenSelection === 'Recursive' ? 'recursive: all descendants' : 'immediate children only' }}</strong>),
                                plus <strong>Item Name</strong>, <strong>Item Id</strong> (manifest/business id), and <strong>Item Path</strong> (internal JSON tree node id when an id field is configured). Adjust <strong>Import scope</strong> above to refresh.
                            </p>
                            <p v-if="isWordPressPlatform" style="color:var(--text-muted); font-size:0.82rem; margin:0 0 0.75rem 0; line-height:1.45;">
                                Source lists <strong>wp_posts</strong> columns and <strong>postmeta</strong> keys
                                <template v-if="wordPressImportSourcePostType()"> for content type <strong>{{ wordPressImportSourcePostType() }}</strong></template>
                                <template v-else> from every post under this node (<strong>{{ childrenSelection === 'Recursive' ? 'recursive: all descendants' : 'immediate children only' }}</strong>)</template>,
                                plus <strong>Item Name</strong> and <strong>Item Id</strong>. Field values are loaded per item during import. Adjust <strong>Import scope</strong> above to refresh.
                            </p>
                            <p v-if="usesScopeAggregateFields && sourceFields.length === 0" style="color:var(--text-muted); font-size:0.82rem; margin:0 0 0.75rem 0;">No fields found for this scope. Try another tree node or a different import scope.</p>
                            <p v-if="!usesScopeAggregateFields && sourceFields.length === 0" style="color:var(--text-muted); font-size:0.82rem; margin:0 0 0.75rem 0;">No source fields loaded. Select a node with fields, or open Node Details once.</p>
                            <p v-if="!usesScopeAggregateFields && sourceFields.length > 0" style="color:var(--text-muted); font-size:0.82rem; margin:0 0 0.75rem 0;">Fields match the Node Details tab for the selected item.</p>
                        </template>
                        <table class="table-dark-custom import-wizard-table">
                            <thead>
                                <tr>
                                    <th style="width:7.5rem;">Type</th>
                                    <th>Source Field</th>
                                    <th>Target Field</th>
                                    <th v-if="targetSite" style="min-width:11rem;">Media library</th>
                                    <th style="width:80px;"></th>
                                </tr>
                            </thead>
                            <tbody>
                                <template v-for="row in mappings">
                                <tr :key="row.Id">
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="row.mappingKind" @change="onMappingKindChange(row)" style="width:100%;">
                                            <option value="Field">Field</option>
                                            <option value="Script">Custom script</option>
                                        </select>
                                    </td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="row.Source" style="width:100%;">
                                            <option :value="null">{{ isScriptMappingRow(row) ? '— optional —' : '— Select —' }}</option>
                                            <option v-for="f in sourceFieldsSorted" :key="(f.name || f.Name || '') + '_' + (f.language || f.Language || '') + '_' + (f.contentType || f.ContentType || '')" :value="f">{{ sourceFieldLabel(f) }}<template v-if="f.contentType || f.ContentType"> ({{ f.contentType || f.ContentType }})</template><template v-if="f.language || f.Language"> ({{ f.language || f.Language }})</template></option>
                                        </select>
                                    </td>
                                    <td>
                                        <select class="form-control-dark select-dark" v-model="row.Target" @change="PopulateItemFieldMapping(row)" style="width:100%;">
                                            <option :value="null">— Select —</option>
                                            <option v-for="f in targetFieldsForMappingSorted" :key="fieldPickName(f) + '_' + pickTargetTypeName(f)" :value="f">{{ targetFieldMappingDropdownLabel(f) }}</option>
                                        </select>
                                    </td>
                                    <td v-if="targetSite">
                                        <template v-if="!isScriptMappingRow(row) && targetRowIsImageField(row)">
                                            <button type="button" class="btn-outline-subtle" style="padding:0.25rem 0.5rem; font-size:0.78rem; white-space:normal; text-align:left; max-width:100%;" :title="'Default Sitefinity library folder for images imported into this field'"
                                                @click="openLibraryFolderDialog(row)">
                                                <i class="bi bi-folder-symlink"></i> {{ libraryFolderSummary(row) }}
                                            </button>
                                        </template>
                                        <span v-else style="color:var(--text-muted); font-size:0.78rem;">—</span>
                                    </td>
                                    <td>
                                        <a href="javascript:void(0);" class="import-action-link text-danger" @click.prevent="destroyScriptEditor(row); mappings = mappings.filter(function(m) { return m !== row; })">Remove</a>
                                    </td>
                                </tr>
                                <tr v-if="isScriptMappingRow(row)" :key="row.Id + '-script'">
                                    <td :colspan="mappingTableColspan" style="padding-top:0;">
                                        <div class="detail-field-label" style="margin-bottom:0.35rem;">C# script (Roslyn) — globals: <code style="font-size:0.78rem;">Item</code>, <code style="font-size:0.78rem;">SitefinityResult</code>, <code style="font-size:0.78rem;">Target</code>, <code style="font-size:0.78rem;">Sitefinity.QueryEntities</code>, <code style="font-size:0.78rem;">Sitefinity.FindPage</code>, <code style="font-size:0.78rem;">QueueContentBlockWidget</code>, <code style="font-size:0.78rem;">CreateLink</code>, <code style="font-size:0.78rem;">SetLink</code>, <code style="font-size:0.78rem;">RelateExistingImage</code>, <code style="font-size:0.78rem;">Logger</code> (see <code style="font-size:0.76rem;">docs/import-script-*.md</code>)</div>
                                        <div :id="'import-script-ed-' + row.Id" class="import-script-editor-host"></div>
                                        <div style="margin-top:0.5rem; display:flex; gap:0.5rem;">
                                            <button type="button" class="btn-outline-subtle" style="font-size:0.78rem;" @click="validateCustomScript(row)"><i class="bi bi-check2-circle"></i> Validate compile</button>
                                        </div>
                                    </td>
                                </tr>
                                </template>
                            </tbody>
                        </table>
                        <button class="btn-outline-subtle" style="margin-top:0.75rem;" @click="AddMapping"><i class="bi bi-plus"></i> Add mapping</button>

                        <div v-if="targetSite" style="margin-top:1.25rem; padding-top:1rem; border-top:1px solid var(--border-subtle);">
                            <div class="detail-field-label">Rich text / HTML inline images</div>
                            <p style="font-size:0.82rem; color:var(--text-muted); margin:0 0 0.65rem 0; line-height:1.45;">
                                Sitefinity library folder for images referenced inside long text fields (<code style="font-size:0.78rem;">&lt;img src&gt;</code>). Applies to this import mapping. If unset, <code style="font-size:0.78rem;">Chameleon:ImportHtmlImages</code> defaults are used.
                            </p>
                            <button type="button" class="btn-outline-subtle" style="padding:0.25rem 0.5rem; font-size:0.78rem; white-space:normal; text-align:left; max-width:100%;" title="Library folder for inline images in rich text"
                                @click="openHtmlInlineImagesFolderDialog">
                                <i class="bi bi-folder-symlink"></i> {{ htmlInlineImagesFolderSummary() }}
                            </button>
                        </div>

                        <div v-if="libraryFolderDialogOpen" class="connection-overlay" style="position:absolute; inset:0; z-index:20; border-radius:inherit; align-items:center; justify-content:center; display:flex; background:rgba(0,0,0,0.5);">
                            <div class="connect-panel connection-dialog-panel" style="max-width:24rem; width:min(96vw, 24rem); margin:1rem;" @click.stop>
                                <div class="connection-dialog-header">
                                    <h4 style="margin:0;">{{ libraryFolderDialogMode === 'htmlInline' ? 'Rich text inline images folder' : 'Media library folder' }}</h4>
                                    <button type="button" class="connection-dialog-close" @click="closeLibraryFolderDialog" aria-label="Close"><i class="bi bi-x-lg"></i></button>
                                </div>
                                <p v-if="libraryFolderDialogMode === 'htmlInline'" style="font-size:0.82rem; color:var(--text-muted); margin:0 0 0.75rem 0; line-height:1.45;">Pick the Sitefinity <strong>library source</strong> and <strong>album</strong> where images from mapped long text / HTML fields will be uploaded during import.</p>
                                <p v-else style="font-size:0.82rem; color:var(--text-muted); margin:0 0 0.75rem 0; line-height:1.45;">Pick the Sitefinity <strong>library source</strong> (provider) and <strong>default parent album</strong> for inline / related images for this field. Import wiring uses these values later.</p>
                                <div v-if="librarySourcesLoading" style="font-size:0.82rem; color:var(--text-muted); margin-bottom:0.75rem;">Loading library sources…</div>
                                <select v-else class="form-control-dark select-dark" v-model="libraryFolderTempProviderKey" @change="onLibraryFolderProviderKeyChange" style="width:100%; margin-bottom:0.75rem;">
                                    <option value="">— Select library source —</option>
                                    <option v-for="s in librarySourcesForSite" :key="libraryProviderKey(s)" :value="libraryProviderKey(s)">{{ libraryProviderLabel(s) }}</option>
                                </select>
                                <div v-if="libraryFolderAlbumsLoading" style="font-size:0.82rem; color:var(--text-muted); margin-bottom:0.75rem;">Loading albums…</div>
                                <select v-else class="form-control-dark select-dark" v-model="libraryFolderTempAlbumId" style="width:100%; margin-bottom:1rem;">
                                    <option value="">— Default album (folder) —</option>
                                    <option v-for="a in libraryFolderAlbums" :key="a.id" :value="a.id">{{ a.title || a.id }}</option>
                                </select>
                                <div style="display:flex; justify-content:flex-end; gap:0.5rem;">
                                    <button type="button" class="btn-outline-subtle" @click="closeLibraryFolderDialog">Cancel</button>
                                    <button type="button" class="btn-accent" @click="applyLibraryFolderDialog">Apply</button>
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="connection-dialog-section" style="display:flex; justify-content:space-between; align-items:center; margin-top:1.5rem; padding-top:1rem; border-top:1px solid var(--border-subtle);">
                        <button class="btn-outline-subtle" @click="currentStep = currentStep - 1" v-if="currentStep > 0">
                            <i class="bi bi-chevron-left"></i> {{ wizardSteps[currentStep - 1] }}
                        </button>
                        <div v-else></div>
                        <div>
                            <button class="btn-outline-subtle" @click="goNextStep" v-if="currentStep < wizardSteps.length - 1" style="margin-right:0.5rem;"
                                :disabled="loadingMappingsStep || loadingSourceTypesPlan || (currentStep === 0 && !useTemplatePlanWizard && targetSite && (loadingSiteModules || loadingSitefinityContentTypes))">
                                {{ wizardSteps[currentStep + 1] }} <i class="bi bi-chevron-right"></i>
                            </button>
                            <button type="button" class="btn-accent" v-if="currentStep === 1" @click="SaveMappings">
                                <i class="bi bi-check-lg"></i> {{ saveButtonLabel }}
                            </button>
                        </div>
                    </div>
                    </template>
                </div>
            </div>

            <div v-show="importLogVisible" class="connection-overlay import-wizard-overlay">
                <div class="connect-panel connection-dialog-panel import-wizard-panel" style="position:relative; max-width:72rem; width:min(98vw, 72rem);">
                    <div class="connection-dialog-header">
                        <h2>Import log</h2>
                        <button type="button" class="connection-dialog-close" @click="closeImportLog" aria-label="Close">
                            <i class="bi bi-x-lg"></i>
                        </button>
                    </div>
                    <p v-if="importLogMapping" style="color:var(--text-secondary); font-size:0.88rem; margin:0 0 1rem 0;">
                        <strong style="color:var(--text-primary);">{{ importLogMapping.name }}</strong>
                        <span style="color:var(--text-muted);"> — {{ importLogView === 'runs' ? 'last 20 runs (click a row for items)' : '' }}{{ importLogView === 'items' ? 'items in this run' : '' }}{{ importLogView === 'steps' ? 'import steps' : '' }}</span>
                    </p>
                    <div v-if="importLogLoading" class="dashboard-loading-overlay import-edit-loading" style="min-height:160px; padding:2rem 1rem; position:relative;">
                        <div class="spinner-ring"></div>
                        <div class="dashboard-loading-text">Loading history…</div>
                    </div>
                    <template v-else-if="importLogView === 'runs'">
                        <div style="overflow-x:auto;">
                            <table class="table-dark-custom" style="margin:0;">
                                <thead>
                                    <tr>
                                        <th>Started</th>
                                        <th>Finished</th>
                                        <th>Outcome</th>
                                        <th style="text-align:right;">Err</th>
                                        <th style="text-align:right;">Warn</th>
                                        <th>Progress</th>
                                        <th>Summary</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr v-for="row in importLogRows" :key="row.id || row.Id" @click="openImportRunDetail(row)" title="View items in this run" style="cursor:pointer;" :style="{ background: 'transparent' }" @mouseenter="$event.currentTarget.style.background='rgba(255,255,255,0.04)'" @mouseleave="$event.currentTarget.style.background='transparent'">
                                        <td style="white-space:nowrap; font-size:0.82rem;">{{ formatImportLogDate(row.startedOn != null ? row.startedOn : row.StartedOn) }}</td>
                                        <td style="white-space:nowrap; font-size:0.82rem;">{{ formatImportLogDate(row.finisedOn != null ? row.finisedOn : row.FinisedOn) }}</td>
                                        <td style="font-size:0.82rem;">{{ importLogOutcome(row) }}</td>
                                        <td style="font-size:0.82rem; text-align:right;">{{ importLogErrCount(row) }}</td>
                                        <td style="font-size:0.82rem; text-align:right;">{{ importLogWarnCount(row) }}</td>
                                        <td style="font-size:0.82rem; white-space:nowrap;">{{ importLogProgress(row) }}</td>
                                        <td style="font-size:0.78rem; color:var(--text-secondary); max-width:18rem; overflow:hidden; text-overflow:ellipsis;" :title="importLogMessage(row)">{{ importLogMessage(row) }}</td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                        <p v-if="!importLogRows || importLogRows.length === 0" style="color:var(--text-muted); font-size:0.85rem; margin:0.75rem 0 0 0;">
                            No import runs recorded for this mapping yet.
                        </p>
                    </template>
                    <template v-else-if="importLogView === 'items'">
                        <div style="margin-bottom:0.75rem;">
                            <button type="button" class="btn-outline-subtle" style="padding:0.35rem 0.65rem; font-size:0.82rem;" @click="importLogBackToRuns">
                                <i class="bi bi-arrow-left"></i> Back to runs
                            </button>
                        </div>
                        <p v-if="importLogSelectedRun" style="font-size:0.82rem; color:var(--text-muted); margin:0 0 0.75rem 0;">
                            Run #{{ importLogSelectedRun.id != null ? importLogSelectedRun.id : importLogSelectedRun.Id }}
                            <span v-if="importLogRunDetail && importLogRunDetail.run"> — {{ formatImportLogDate(importLogRunDetail.run.startedOn != null ? importLogRunDetail.run.startedOn : importLogRunDetail.run.StartedOn) }}</span>
                        </p>
                        <div v-if="importLogRunItemsLoading" class="dashboard-loading-overlay import-edit-loading" style="min-height:120px; padding:1.5rem 1rem; position:relative;">
                            <div class="spinner-ring"></div>
                            <div class="dashboard-loading-text">Loading items…</div>
                        </div>
                        <div v-else style="overflow-x:auto; max-height:min(60vh, 28rem); overflow-y:auto;">
                            <table class="table-dark-custom" style="margin:0;">
                                <thead>
                                    <tr>
                                        <th>#</th>
                                        <th>Source item</th>
                                        <th>Name</th>
                                        <th>Outcome</th>
                                        <th>Sitefinity Id</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr v-for="it in importLogRunItems" :key="it.id || it.Id" @click="openImportItemDetail(it)" title="View steps" style="cursor:pointer;" @mouseenter="$event.currentTarget.style.background='rgba(255,255,255,0.04)'" @mouseleave="$event.currentTarget.style.background='transparent'">
                                        <td style="font-size:0.82rem;">{{ it.ordinal != null ? it.ordinal : it.Ordinal }}</td>
                                        <td style="font-size:0.78rem; word-break:break-all; max-width:10rem;">{{ it.sourceItemId != null ? it.sourceItemId : it.SourceItemId }}</td>
                                        <td style="font-size:0.82rem;">{{ (it.itemName != null ? it.itemName : it.ItemName) || '—' }}</td>
                                        <td :style="{ fontSize: '0.82rem', ...importLogItemOutcomeStyle(it) }">{{ importLogItemOutcome(it) }}</td>
                                        <td style="font-size:0.78rem; word-break:break-all; max-width:12rem;">{{ (it.targetSitefinityId != null ? it.targetSitefinityId : it.TargetSitefinityId) || '—' }}</td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                        <p v-if="!importLogRunItemsLoading && (!importLogRunItems || importLogRunItems.length === 0)" style="color:var(--text-muted); font-size:0.85rem; margin:0.75rem 0 0 0;">
                            No per-item log for this run (imports started before this feature have no detail).
                        </p>
                    </template>
                    <template v-else-if="importLogView === 'steps' && importLogSelectedItem">
                        <div style="margin-bottom:0.75rem;">
                            <button type="button" class="btn-outline-subtle" style="padding:0.35rem 0.65rem; font-size:0.82rem;" @click="importLogBackToItems">
                                <i class="bi bi-arrow-left"></i> Back to items
                            </button>
                        </div>
                        <p style="font-size:0.9rem; color:var(--text-primary); font-weight:500; margin:0 0 0.25rem 0;">
                            {{ (importLogSelectedItem.itemName != null ? importLogSelectedItem.itemName : importLogSelectedItem.ItemName) || 'Item' }}
                        </p>
                        <p style="font-size:0.78rem; color:var(--text-muted); margin:0 0 1rem 0; word-break:break-all;">
                            Source: {{ importLogSelectedItem.sourceItemId != null ? importLogSelectedItem.sourceItemId : importLogSelectedItem.SourceItemId }}
                            <span v-if="(importLogSelectedItem.targetSitefinityId != null ? importLogSelectedItem.targetSitefinityId : importLogSelectedItem.TargetSitefinityId)"> → Sitefinity: {{ importLogSelectedItem.targetSitefinityId != null ? importLogSelectedItem.targetSitefinityId : importLogSelectedItem.TargetSitefinityId }}</span>
                        </p>
                        <p v-if="(importLogSelectedItem.errorMessage != null ? importLogSelectedItem.errorMessage : importLogSelectedItem.ErrorMessage)" style="font-size:0.82rem; margin:0 0 0.75rem 0;" :style="{ color: (importLogSelectedItem.hasSecondaryErrors != null ? importLogSelectedItem.hasSecondaryErrors : importLogSelectedItem.HasSecondaryErrors) ? '#e8a04a' : '#f88' }">
                            {{ importLogSelectedItem.errorMessage != null ? importLogSelectedItem.errorMessage : importLogSelectedItem.ErrorMessage }}
                        </p>
                        <div style="max-height:min(55vh, 26rem); overflow-y:auto; border:1px solid var(--border-subtle); border-radius:6px; padding:0.5rem 0.75rem;">
                            <div v-for="st in importLogItemSteps(importLogSelectedItem)" :key="(st.stepOrder != null ? st.stepOrder : st.StepOrder) + '-' + (st.category || st.Category)" style="padding:0.5rem 0; border-bottom:1px solid var(--border-subtle); font-size:0.82rem;">
                                <div style="display:flex; align-items:baseline; gap:0.5rem; flex-wrap:wrap;">
                                    <span v-if="(st.timeLabel != null ? st.timeLabel : st.TimeLabel)" style="font-family:ui-monospace,monospace; font-size:0.72rem; color:var(--text-muted); min-width:6.5rem;">{{ st.timeLabel != null ? st.timeLabel : st.TimeLabel }}</span>
                                    <span :style="importLogStepCategoryStyle(st)">{{ st.category != null ? st.category : st.Category }}</span>
                                    <span :style="importLogStepSummaryStyle(st)">{{ st.summary != null ? st.summary : st.Summary }}</span>
                                </div>
                                <pre v-if="(st.detail != null ? st.detail : st.Detail)" style="margin:0.35rem 0 0 0; font-size:0.76rem; white-space:pre-wrap; word-break:break-word; font-family:ui-monospace,monospace;" :style="importLogStepDetailStyle(st)">{{ st.detail != null ? st.detail : st.Detail }}</pre>
                            </div>
                        </div>
                        <p v-if="importLogItemSteps(importLogSelectedItem).length === 0" style="color:var(--text-muted); font-size:0.85rem;">No steps recorded.</p>
                    </template>
                </div>
            </div>

            <!-- Existing Mappings Table -->
            <div v-if="existingMappings && existingMappings.length > 0" class="section-card" style="margin-top:1.5rem;">
                <div class="section-card-header" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:0.5rem;">
                    <h6 style="margin:0;"><i class="bi bi-link-45deg" style="color:var(--accent-bright);"></i> Saved Mappings</h6>
                    <a href="javascript:void(0);" class="import-action-link" @click.prevent="SetupImport" title="Add another import setup on this node (e.g. another language)">
                        <i class="bi bi-plus-circle"></i> Create new
                    </a>
                </div>
                <div v-if="globalRunningImports && globalRunningImports.length" class="section-card-body" style="padding:0.75rem 1rem; border-bottom:1px solid var(--border-subtle); background:rgba(255,180,80,0.06);">
                    <div style="display:flex; align-items:center; flex-wrap:wrap; gap:0.75rem; font-size:0.85rem; color:var(--text-secondary);">
                        <span><i class="bi bi-hourglass-split" style="color:var(--accent-bright);"></i> An import is in progress (run #{{ globalRunningRunIdForBanner() }}<span v-if="globalRunningImports[0] && (globalRunningImports[0].name || globalRunningImports[0].Name)"> — {{ globalRunningImports[0].name || globalRunningImports[0].Name }}</span>).</span>
                        <button type="button" class="btn-outline-subtle" style="padding:0.25rem 0.55rem; font-size:0.78rem; color:#f88; border-color:rgba(255,136,136,0.45);" @click="forceStopImportByRunId(globalRunningRunIdForBanner())" title="Cancel the running import">
                            <i class="bi bi-stop-circle"></i> Force stop
                        </button>
                    </div>
                </div>
                <div class="section-card-body" style="padding:0;">
                    <table class="table-dark-custom">
                        <thead>
                            <tr>
                                <th>Name</th>
                                <th>Description</th>
                                <th>Actions</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr v-for="mapping in existingMappings" :key="mapping.id">
                                <td style="color:var(--text-primary); font-weight:500;">
                                    {{ mapping.name }}
                                    <div v-if="mappingImportStatusLine(mapping)" style="font-size:0.75rem; color:var(--accent-bright); font-weight:400; margin-top:0.25rem;">{{ mappingImportStatusLine(mapping) }}</div>
                                    <div style="font-size:0.72rem; color:var(--text-muted); font-weight:400; margin-top:0.2rem;">Item selection: {{ itemSelectionModeLabel(mapping.itemSelectionMode || mapping.ItemSelectionMode) }}</div>
                                </td>
                                <td style="color:var(--text-secondary); font-size:0.85rem;">{{ mapping.description || '—' }}</td>
                                <td>
                                    <button class="btn-outline-subtle" style="padding:0.3rem 0.6rem; font-size:0.78rem; margin-right:0.4rem;" @click="EditMapping(mapping)">
                                        <i class="bi bi-pencil"></i> Edit
                                    </button>
                                    <a href="javascript:void(0);" class="import-action-link" style="margin-right:0.5rem;" @click.prevent="DuplicateMapping(mapping)" title="Copy this setup as a new mapping; template plans can optionally copy reusable templates too">Duplicate</a>
                                    <button class="btn-outline-subtle" style="padding:0.3rem 0.6rem; font-size:0.78rem; margin-right:0.4rem;" @click="RunImport(mapping)" :disabled="isRunImportDisabled(mapping)" :title="runImportDisabledTitle(mapping)">
                                        <i class="bi bi-play-fill"></i> Run
                                    </button>
                                    <button v-if="isMappingImportRunning(mapping)" type="button" class="btn-outline-subtle" style="padding:0.3rem 0.6rem; font-size:0.78rem; margin-right:0.4rem; color:#f88; border-color:rgba(255,136,136,0.4);" @click="forceStopImport(mapping)" title="Request cancellation">
                                        <i class="bi bi-stop-circle"></i> Force stop
                                    </button>
                                    <button class="btn-outline-subtle" style="padding:0.3rem 0.6rem; font-size:0.78rem; margin-right:0.4rem;" @click="openImportLog(mapping)" title="View recent import runs">
                                        <i class="bi bi-journal-text"></i> Log
                                    </button>
                                    <a href="javascript:void(0);" class="import-action-link" style="margin-right:0.5rem;" @click.prevent="DeleteMapping(mapping)">Delete</a>
                                    <a href="javascript:void(0);" class="import-action-link text-danger" @click.prevent="ClearMemory(mapping)">Clear memory</a>
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
            <div v-else-if="connection && !selectedNode" class="empty-tab-placeholder" style="padding:2rem;">
                <i class="bi bi-diagram-3" style="font-size:2rem; color:var(--text-muted);"></i>
                <p style="color:var(--text-secondary); margin-top:1rem;">Select a node in the content tree to view or create imports for that <strong>start node</strong>.</p>
            </div>
            <div v-else-if="connection && selectedNode" class="empty-tab-placeholder" style="padding:2rem; min-height:60vh;">
                <i class="bi bi-arrow-left-right" style="font-size:2rem; color:var(--text-muted);"></i>
                <p style="color:var(--text-secondary); margin-top:1rem; margin-bottom:0.35rem;">
                    Selected node: <strong style="color:var(--text-primary);">{{ selectedNode.name || selectedNode.Name }}</strong>
                    <span style="color:var(--text-muted);"> ({{ sourceNodeId() }})</span>
                </p>
                <p style="color:var(--text-secondary); margin-top:0; margin-bottom:1rem;">No mappings yet for this node.</p>
                <button type="button" class="btn-accent" @click="SetupImport">
                    <i class="bi bi-plus-circle"></i> Setup Import
                </button>
            </div>
        </div>
    `
}); 
