import { UmbSubmittableWorkspaceContextBase } from '../submittable/index.js';
import { UmbEntityWorkspaceDataManager } from '../entity/entity-workspace-data-manager.js';
import { UMB_ACTION_EVENT_CONTEXT } from '../../action/index.js';
import { UmbEntityContext } from '../../entity/index.js';
import { UMB_DISCARD_CHANGES_MODAL, umbOpenModal } from '../../modal/index.js';
import { UmbObjectState } from '../../../../libs/observable-api/index.js';
import { UmbEntityUpdatedEvent, UmbRequestReloadChildrenOfEntityEvent, UmbRequestReloadStructureForEntityEvent, } from '../../entity-action/index.js';
import { UmbExtensionApiInitializer } from '../../../../libs/extension-api/index.js';
import { umbExtensionsRegistry } from '../../extension-registry/index.js';
import { UmbStateManager } from '../../utils/index.js';
import { UmbValidationContext } from '../../validation/index.js';
import { UmbId } from '../../id/index.js';
import { UmbApiError } from '../../resources/index.js';
const LOADING_STATE_UNIQUE = 'umbLoadingEntityDetail';
const FORBIDDEN_STATE_UNIQUE = 'umbForbiddenEntityDetail';
export class UmbEntityDetailWorkspaceContextBase extends UmbSubmittableWorkspaceContextBase {
    #entityContext;
    #eventContext;
    #createUnderParent;
    #initResolver;
    #initialized;
    #init;
    constructor(host, args) {
        super(host, args.workspaceAlias);
        // Just for context token safety:
        this.IS_ENTITY_DETAIL_WORKSPACE_CONTEXT = true;
        /**
         * @description Data manager for the workspace.
         * @protected
         * @memberof UmbEntityWorkspaceContextBase
         */
        this._data = new UmbEntityWorkspaceDataManager(this);
        this.#entityContext = new UmbEntityContext(this);
        this.entityType = this.#entityContext.entityType;
        this.unique = this.#entityContext.unique;
        this.data = this._data.current;
        this.persistedData = this._data.persisted;
        this.loading = new UmbStateManager(this);
        this.forbidden = new UmbStateManager(this);
        this.#createUnderParent = new UmbObjectState(undefined);
        // eslint-disable-next-line @typescript-eslint/naming-convention
        this._internal_createUnderParent = this.#createUnderParent.asObservable();
        // eslint-disable-next-line @typescript-eslint/naming-convention
        this._internal_createUnderParentEntityUnique = this.#createUnderParent.asObservablePart((parent) => parent ? parent.unique : undefined);
        // eslint-disable-next-line @typescript-eslint/naming-convention
        this._internal_createUnderParentEntityType = this.#createUnderParent.asObservablePart((parent) => parent ? parent.entityType : undefined);
        /**
         * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead to get the parent both when creating and editing.
         */
        this.parentUnique = this.#createUnderParent.asObservablePart((parent) => parent ? parent.unique : undefined);
        /**
         * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead to get the parent both when creating and editing.
         */
        this.parentEntityType = this.#createUnderParent.asObservablePart((parent) => parent ? parent.entityType : undefined);
        /**
         * The base validation context for the workspace. This ensures that at least one validation context is always present.
         * @example You can manually validate all properties on the context:
         * ```ts
         * try {
         *   await this.validationContext.validate();
         * } catch (error) {
         *   console.error(error);
         * }
         * ```
         * @example You can set the data path on the context to map to a specific part of the server validation:
         * ```ts
         * this.validationContext.setDataPath('path.to.data');
         * ```
         */
        this.validationContext = new UmbValidationContext(this);
        this.#initialized = false;
        this.#init = new Promise((resolve) => {
            if (this.#initialized) {
                resolve();
            }
            else {
                this.#initResolver = resolve;
            }
        });
        this.#allowNavigateAway = false;
        this.#onWillNavigate = async (e) => {
            const newUrl = e.detail.url;
            if (this.#allowNavigateAway) {
                return true;
            }
            if (this._checkWillNavigateAway(newUrl) && this.getHasUnpersistedChanges()) {
                /* Since ours modals are async while events are synchronous, we need to prevent the default behavior of the event, even if the modal hasn’t been resolved yet.
                Once the modal is resolved (the user accepted to discard the changes and navigate away from the route), we will push a new history state.
                This push will make the "willchangestate" event happen again and due to this somewhat "backward" behavior,
                we set an "allowNavigateAway"-flag to prevent the "discard-changes" functionality from running in a loop.*/
                e.preventDefault();
                try {
                    // navigate to the new url when discarding changes
                    await umbOpenModal(this, UMB_DISCARD_CHANGES_MODAL);
                    this.#allowNavigateAway = true;
                    history.pushState({}, '', e.detail.url);
                    return true;
                }
                catch {
                    return false;
                }
            }
            return true;
        };
        // Discriminator to identify events from this workspace context
        this._workspaceEventUnique = UmbId.new();
        this.#onEntityUpdatedEvent = (event) => {
            const eventEntityUnique = event.getUnique();
            const eventEntityType = event.getEntityType();
            const eventDiscriminator = event.getEventUnique();
            // Ignore events for other entities
            if (eventEntityType !== this.getEntityType())
                return;
            if (eventEntityUnique !== this.getUnique())
                return;
            // Ignore events from this workspace so we don't reload the data twice. Ex saving this workspace
            if (eventDiscriminator === this._workspaceEventUnique)
                return;
            this.reload();
        };
        this.addValidationContext(this.validationContext);
        this.#entityContext.setEntityType(args.entityType);
        window.addEventListener('willchangestate', this.#onWillNavigate);
        this.#observeRepository(args.detailRepositoryAlias);
        this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => {
            this.#eventContext = context;
            this.#eventContext?.removeEventListener(UmbEntityUpdatedEvent.TYPE, this.#onEntityUpdatedEvent);
            this.#eventContext?.addEventListener(UmbEntityUpdatedEvent.TYPE, this.#onEntityUpdatedEvent);
        });
    }
    /**
     * Get the entity type
     * @returns { string } The entity type
     */
    getEntityType() {
        const entityType = this.#entityContext.getEntityType();
        if (!entityType)
            throw new Error('Entity type is not set');
        return entityType;
    }
    /**
     * Get the current data
     * @returns { DetailModelType | undefined } The entity context
     */
    getData() {
        return this._data.getCurrent();
    }
    /**
     * Get the current data
     * @param {DetailModelType | undefined} data - New data of this workspace.
     */
    setData(data) {
        this._data.setCurrent(data);
    }
    /**
     * Get the persisted data
     * @returns { DetailModelType | undefined } The persisted data
     */
    getPersistedData() {
        return this._data.getPersisted();
    }
    /**
     * Get the unique
     * @returns { string | undefined } The unique identifier
     */
    getUnique() {
        return this.#entityContext.getUnique();
    }
    setUnique(unique) {
        this.#entityContext.setUnique(unique);
    }
    /**
     * Gets the parent that a new entity will be created under.
     * @returns { UmbEntityModel | undefined } The parent entity
     */
    // eslint-disable-next-line @typescript-eslint/naming-convention
    _internal_getCreateUnderParent() {
        return this.#createUnderParent.getValue();
    }
    /**
     * Sets the parent that a new entity will be created under.
     * @param {UmbEntityModel} parent The parent entity
     */
    // eslint-disable-next-line @typescript-eslint/naming-convention
    _internal_setCreateUnderParent(parent) {
        this.#createUnderParent.setValue(parent);
    }
    /**
     * Get the parent
     * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead to get the parent both when creating and editing.
     * @returns { UmbEntityModel | undefined } The parent entity
     */
    getParent() {
        return this.#createUnderParent.getValue();
    }
    /**
     * Set the parent
     * @deprecated Will be removed in v.18.
     * @param { UmbEntityModel } parent The parent entity
     */
    setParent(parent) {
        this.#createUnderParent.setValue(parent);
    }
    /**
     * Get the parent unique
     * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead to get the parent both when creating and editing.
     * @returns { string | undefined } The parent unique identifier
     */
    getParentUnique() {
        return this.#createUnderParent.getValue()?.unique;
    }
    /**
     * Get the parent entity type
     * @deprecated Will be removed in v.18
     * @returns { string | undefined } The parent entity type
     */
    getParentEntityType() {
        return this.#createUnderParent.getValue()?.entityType;
    }
    async load(unique) {
        if (unique === this.getUnique() && this._getDataPromise) {
            return await this._getDataPromise;
        }
        this.resetState();
        this.setIsNew(false);
        this.#entityContext.setUnique(unique);
        this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Loading ${this.getEntityType()} Details` });
        await this.#init;
        this._getDataPromise = this._detailRepository.requestByUnique(unique);
        const response = (await this._getDataPromise);
        const { data, error, asObservable } = response;
        if (error) {
            this.removeUmbControllerByAlias('umbEntityDetailTypeStoreObserver');
            if (UmbApiError.isUmbApiError(error)) {
                if (error.status === 401 || error.status === 403) {
                    this.forbidden.addState({ unique: FORBIDDEN_STATE_UNIQUE, message: error.message });
                }
            }
        }
        else if (data) {
            const processedData = await this._processIncomingData(data);
            this._data.setPersisted(processedData);
            this._data.setCurrent(processedData);
            this.observe(asObservable?.(), (entity) => this.#onDetailStoreChange(entity), 'umbEntityDetailTypeStoreObserver');
        }
        this.loading.removeState(LOADING_STATE_UNIQUE);
        return response;
    }
    /**
     * Reload the workspace data
     * @returns { Promise<void> } The promise of the reload
     */
    async reload() {
        const unique = this.getUnique();
        if (!unique)
            throw new Error('Unique is not set');
        const { data } = await this._detailRepository.requestByUnique(unique);
        if (data) {
            const processedData = await this._processIncomingData(data);
            this._data.setPersisted(processedData);
            this._data.setCurrent(processedData);
        }
    }
    /**
     * Method to check if the workspace data is loaded.
     * @returns { Promise<any> | undefined } true if the workspace data is loaded.
     * @memberof UmbEntityWorkspaceContextBase
     */
    isLoaded() {
        return this._getDataPromise;
    }
    /**
     * Create a data scaffold
     * @param {CreateArgsType} args The arguments to create the scaffold.
     * @param {UmbEntityModel} args.parent The parent entity.
     * @param {UmbEntityUnique} args.parent.unique The unique identifier of the parent entity.
     * @param {string} args.parent.entityType The entity type of the parent entity.
     * @param {Partial<DetailModelType>} args.preset The preset data.
     * @returns { Promise<any> | undefined } The data of the scaffold.
     */
    async createScaffold(args) {
        this.resetState();
        this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Creating ${this.getEntityType()} scaffold` });
        await this.#init;
        // keeping setParent for backwards compatibility. Remove in v18.
        this.setParent(args.parent);
        this._internal_setCreateUnderParent(args.parent);
        const request = this._detailRepository.createScaffold(args.preset);
        this._getDataPromise = request;
        let { data } = await request;
        if (data) {
            data = await this._scaffoldProcessData(data);
            if (this.modalContext) {
                // Notice if the preset comes with values, they will overwrite the scaffolded values... [NL]
                data = { ...data, ...this.modalContext.data.preset };
            }
            this.setIsNew(true);
            this.#entityContext.setUnique(data.unique);
            this._data.setPersisted(data);
            this._data.setCurrent(data);
        }
        this.loading.removeState(LOADING_STATE_UNIQUE);
        return data;
    }
    /**
     * @deprecated Override `_processIncomingData` instead. `_scaffoldProcessData` will be removed in v.18.
     * @param {DetailModelType} data - The data to process.
     * @returns {Promise<DetailModelType>} The processed data.
     */
    async _scaffoldProcessData(data) {
        return await this._processIncomingData(data);
    }
    async _processIncomingData(data) {
        return data;
    }
    async submit() {
        await this.#init;
        const currentData = this.getData();
        if (!currentData) {
            throw new Error('Data is not set');
        }
        if (currentData.unique === undefined) {
            throw new Error('Unique is not set');
        }
        if (this.getIsNew()) {
            const parent = this.#createUnderParent.getValue();
            if (parent?.unique === undefined)
                throw new Error('Parent unique is missing');
            if (!parent.entityType)
                throw new Error('Parent entity type is missing');
            await this._create(currentData, parent);
        }
        else {
            await this._update(currentData);
        }
    }
    /**
     * Deletes the entity.
     * @param unique The unique identifier of the entity to delete.
     */
    async delete(unique) {
        await this.#init;
        await this._detailRepository.delete(unique);
    }
    /**
     * Check if the workspace is about to navigate away.
     * @protected
     * @param {string | URL} newUrl The new url that the workspace is navigating to.
     * @returns {boolean} true if the workspace is navigating away.
     * @memberof UmbEntityWorkspaceContextBase
     */
    _checkWillNavigateAway(newUrl) {
        if (newUrl instanceof URL) {
            newUrl = newUrl.href;
        }
        return !newUrl.includes(this.routes.getActiveLocalPath());
    }
    async _create(currentData, parent) {
        if (!this._detailRepository)
            throw new Error('Detail repository is not set');
        const { error, data } = await this._detailRepository.create(currentData, parent.unique);
        if (error || !data) {
            throw error?.message ?? 'Repository did not return data after create.';
        }
        this.#entityContext.setUnique(data.unique);
        this._data.setPersisted(data);
        this._data.setCurrent(data);
        this.setIsNew(false);
        const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
        if (!eventContext)
            throw new Error('Event context not found.');
        const reloadStructureEvent = new UmbRequestReloadStructureForEntityEvent({
            entityType: parent.entityType,
            unique: parent.unique,
        });
        eventContext.dispatchEvent(reloadStructureEvent);
        const reloadChildren = new UmbRequestReloadChildrenOfEntityEvent({
            entityType: parent.entityType,
            unique: parent.unique,
        });
        eventContext.dispatchEvent(reloadChildren);
    }
    async _update(currentData) {
        const { error, data } = await this._detailRepository.save(currentData);
        if (error || !data) {
            throw error?.message ?? 'Repository did not return data after create.';
        }
        this._data.setPersisted(data);
        this._data.setCurrent(data);
        const unique = this.getUnique();
        const entityType = this.getEntityType();
        const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
        if (!eventContext)
            throw new Error('Event context not found.');
        const event = new UmbRequestReloadStructureForEntityEvent({ unique, entityType });
        eventContext.dispatchEvent(event);
        const updatedEvent = new UmbEntityUpdatedEvent({
            unique,
            entityType,
            eventUnique: this._workspaceEventUnique,
        });
        eventContext.dispatchEvent(updatedEvent);
    }
    #allowNavigateAway;
    #onWillNavigate;
    /**
     * Check if there are unpersisted changes.
     * @returns { boolean } true if there are unpersisted changes.
     */
    getHasUnpersistedChanges() {
        return this._data.getHasUnpersistedChanges();
    }
    resetState() {
        super.resetState();
        this.loading.clear();
        this.forbidden.clear();
        this._data.clear();
        this.#allowNavigateAway = false;
        this._getDataPromise = undefined;
    }
    #checkIfInitialized() {
        if (this._detailRepository) {
            this.#initialized = true;
            this.#initResolver?.();
        }
    }
    #observeRepository(repositoryAlias) {
        if (!repositoryAlias)
            throw new Error('Entity Workspace must have a repository alias.');
        new UmbExtensionApiInitializer(this, umbExtensionsRegistry, repositoryAlias, [], (permitted, ctrl) => {
            this._detailRepository = permitted ? ctrl.api : undefined;
            this.#checkIfInitialized();
        });
    }
    #onDetailStoreChange(entity) {
        if (!entity) {
            this._data.clear();
        }
    }
    #onEntityUpdatedEvent;
    destroy() {
        window.removeEventListener('willchangestate', this.#onWillNavigate);
        this.#eventContext?.removeEventListener(UmbEntityUpdatedEvent.TYPE, this.#onEntityUpdatedEvent);
        this._detailRepository?.destroy();
        this.#entityContext.destroy();
        this._getDataPromise = undefined;
        super.destroy();
    }
}
