import { __esDecorate, __runInitializers } from "tslib";
import { observed } from '../decorators/observed.js';
import { ExposedEntityKind } from '../models/kinds.js';
import { nanoid } from '../nanoid.js';
import { ensureLeadingSlash, joinPaths } from './helpers/endpointHelpers.js';
/**
 * A class that specializes in representing an exposed Data Entity within an API Model.
 *
 * @fires change - Emitted when the exposed entity has changed.
 */
let ExposedEntity = (() => {
    let _classSuper = EventTarget;
    let _entity_decorators;
    let _entity_initializers = [];
    let _entity_extraInitializers = [];
    let _collectionPath_decorators;
    let _collectionPath_initializers = [];
    let _collectionPath_extraInitializers = [];
    let _resourcePath_decorators;
    let _resourcePath_initializers = [];
    let _resourcePath_extraInitializers = [];
    let _actions_decorators;
    let _actions_initializers = [];
    let _actions_extraInitializers = [];
    let _accessRule_decorators;
    let _accessRule_initializers = [];
    let _accessRule_extraInitializers = [];
    let _rateLimiting_decorators;
    let _rateLimiting_initializers = [];
    let _rateLimiting_extraInitializers = [];
    return class ExposedEntity extends _classSuper {
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
            _entity_decorators = [observed()];
            _collectionPath_decorators = [observed()];
            _resourcePath_decorators = [observed()];
            _actions_decorators = [observed({ deep: true })];
            _accessRule_decorators = [observed({ deep: true })];
            _rateLimiting_decorators = [observed({ deep: true })];
            __esDecorate(this, null, _entity_decorators, { kind: "accessor", name: "entity", static: false, private: false, access: { has: obj => "entity" in obj, get: obj => obj.entity, set: (obj, value) => { obj.entity = value; } }, metadata: _metadata }, _entity_initializers, _entity_extraInitializers);
            __esDecorate(this, null, _collectionPath_decorators, { kind: "accessor", name: "collectionPath", static: false, private: false, access: { has: obj => "collectionPath" in obj, get: obj => obj.collectionPath, set: (obj, value) => { obj.collectionPath = value; } }, metadata: _metadata }, _collectionPath_initializers, _collectionPath_extraInitializers);
            __esDecorate(this, null, _resourcePath_decorators, { kind: "accessor", name: "resourcePath", static: false, private: false, access: { has: obj => "resourcePath" in obj, get: obj => obj.resourcePath, set: (obj, value) => { obj.resourcePath = value; } }, metadata: _metadata }, _resourcePath_initializers, _resourcePath_extraInitializers);
            __esDecorate(this, null, _actions_decorators, { kind: "accessor", name: "actions", static: false, private: false, access: { has: obj => "actions" in obj, get: obj => obj.actions, set: (obj, value) => { obj.actions = value; } }, metadata: _metadata }, _actions_initializers, _actions_extraInitializers);
            __esDecorate(this, null, _accessRule_decorators, { kind: "accessor", name: "accessRule", static: false, private: false, access: { has: obj => "accessRule" in obj, get: obj => obj.accessRule, set: (obj, value) => { obj.accessRule = value; } }, metadata: _metadata }, _accessRule_initializers, _accessRule_extraInitializers);
            __esDecorate(this, null, _rateLimiting_decorators, { kind: "accessor", name: "rateLimiting", static: false, private: false, access: { has: obj => "rateLimiting" in obj, get: obj => obj.rateLimiting, set: (obj, value) => { obj.rateLimiting = value; } }, metadata: _metadata }, _rateLimiting_initializers, _rateLimiting_extraInitializers);
            if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        /**
         * The exposed entity kind recognizable by the ecosystem.
         */
        kind;
        /**
         * The unique key of the exposed entity.
         * This is a stable identifier that does not change across versions.
         */
        key;
        #entity_accessor_storage = __runInitializers(this, _entity_initializers, void 0);
        /**
         * A pointer to a Data Entity from the Data Domain.
         */
        get entity() { return this.#entity_accessor_storage; }
        set entity(value) { this.#entity_accessor_storage = value; }
        /**
         * Indicates whether this exposure has a collection endpoint.
         * A collection endpoint is optional for nested exposures where the association is 1:1
         * and the schema is embedded directly under the parent resource.
         *
         * Note that this property is not observed for changes as it is immutable after creation.
         */
        hasCollection = __runInitializers(this, _entity_extraInitializers);
        #collectionPath_accessor_storage = __runInitializers(this, _collectionPath_initializers, void 0);
        /**
         * Path to the collection endpoint for this exposure.
         * Starts with '/'. Not set for 1:1 nested exposures where collection does not exist.
         */
        get collectionPath() { return this.#collectionPath_accessor_storage; }
        set collectionPath(value) { this.#collectionPath_accessor_storage = value; }
        #resourcePath_accessor_storage = (__runInitializers(this, _collectionPath_extraInitializers), __runInitializers(this, _resourcePath_initializers, void 0));
        /**
         * Path to the resource endpoint for this exposure.
         * Starts with '/'. For 1:1 nested exposures the resource path typically does not include an id segment.
         */
        get resourcePath() { return this.#resourcePath_accessor_storage; }
        set resourcePath(value) { this.#resourcePath_accessor_storage = value; }
        /**
         * Whether this exposure is a root exposure (top-level collection).
         * If this is set then the `parent` reference must be populated.
         *
         * Note that this property is not observed for changes as it is immutable after creation.
         */
        isRoot = __runInitializers(this, _resourcePath_extraInitializers);
        /**
         * Parent reference when this exposure was created via following an association.
         *
         * Note that this property is not observed for changes as it is immutable after creation.
         */
        parent;
        /**
         * Expose-time config used to create this exposure (persisted for auditing/UI).
         * This is only populated for the root exposure. All children exposures inherit this config.
         *
         * Note that this property is not observed for changes as it is immutable after creation.
         */
        exposeOptions;
        #actions_accessor_storage = __runInitializers(this, _actions_initializers, void 0);
        /**
         * The list of enabled API actions for this exposure (List/Read/Create/etc.)
         */
        get actions() { return this.#actions_accessor_storage; }
        set actions(value) { this.#actions_accessor_storage = value; }
        #accessRule_accessor_storage = (__runInitializers(this, _actions_extraInitializers), __runInitializers(this, _accessRule_initializers, void 0));
        /**
         * Optional array of access rules that define the access control policies for this exposure.
         */
        get accessRule() { return this.#accessRule_accessor_storage; }
        set accessRule(value) { this.#accessRule_accessor_storage = value; }
        #rateLimiting_accessor_storage = (__runInitializers(this, _accessRule_extraInitializers), __runInitializers(this, _rateLimiting_initializers, void 0));
        /**
         * Optional configuration for rate limiting for this exposure.
         */
        get rateLimiting() { return this.#rateLimiting_accessor_storage; }
        set rateLimiting(value) { this.#rateLimiting_accessor_storage = value; }
        /**
         * When true, generation for this exposure hit configured limits
         *
         * Note that this property is not observed for changes as it is immutable after creation.
         */
        truncated = __runInitializers(this, _rateLimiting_extraInitializers);
        /**
         * When the notifying flag is set to true,
         * the domain is pending a notification.
         * No other notifications will be sent until
         * the current notification is sent.
         */
        #notifying = false;
        /**
         * When the initializing flag is set to true,
         * the domain is not notified of changes.
         */
        #initializing = true;
        /**
         * A reference to the parent API Model instance.
         */
        api;
        static createSchema(input = {}) {
            const { key = nanoid(), entity = { key: '' }, collectionPath, resourcePath = '/', hasCollection = true, isRoot, parent, exposeOptions, actions = [], accessRule, rateLimiting, truncated, } = input;
            const result = {
                kind: ExposedEntityKind,
                key,
                entity: { ...entity },
                hasCollection,
                resourcePath,
                actions: actions.map((a) => ({ ...a })),
            };
            if (collectionPath !== undefined) {
                result.collectionPath = collectionPath;
            }
            if (isRoot !== undefined) {
                result.isRoot = isRoot;
            }
            if (parent !== undefined) {
                result.parent = { ...parent };
            }
            if (exposeOptions !== undefined) {
                result.exposeOptions = { ...exposeOptions };
            }
            if (accessRule !== undefined) {
                result.accessRule = accessRule.map((ar) => ({ ...ar }));
            }
            if (rateLimiting !== undefined) {
                result.rateLimiting = { ...rateLimiting };
            }
            if (truncated !== undefined) {
                result.truncated = truncated;
            }
            return result;
        }
        constructor(model, state) {
            super();
            this.api = model;
            const init = ExposedEntity.createSchema(state);
            this.kind = init.kind;
            this.key = init.key;
            this.entity = init.entity;
            this.hasCollection = init.hasCollection;
            this.collectionPath = init.collectionPath;
            this.resourcePath = init.resourcePath;
            this.isRoot = init.isRoot;
            this.parent = init.parent;
            this.exposeOptions = init.exposeOptions;
            this.actions = init.actions;
            this.accessRule = init.accessRule;
            this.rateLimiting = init.rateLimiting;
            this.truncated = init.truncated;
            this.#initializing = false;
        }
        notifyChange() {
            if (this.#notifying || this.#initializing) {
                return;
            }
            this.#notifying = true;
            queueMicrotask(() => {
                this.#notifying = false;
                const event = new Event('change');
                this.dispatchEvent(event);
            });
        }
        toJSON() {
            const result = {
                kind: this.kind,
                key: this.key,
                entity: { ...this.entity },
                resourcePath: this.resourcePath,
                actions: this.actions.map((a) => ({ ...a })),
                hasCollection: this.hasCollection,
            };
            if (this.collectionPath !== undefined) {
                result.collectionPath = this.collectionPath;
            }
            if (this.isRoot !== undefined) {
                result.isRoot = this.isRoot;
            }
            if (this.parent !== undefined) {
                result.parent = { ...this.parent };
            }
            if (this.exposeOptions !== undefined) {
                result.exposeOptions = { ...this.exposeOptions };
            }
            if (this.accessRule !== undefined) {
                result.accessRule = this.accessRule.map((ar) => ({ ...ar }));
            }
            if (this.rateLimiting !== undefined) {
                result.rateLimiting = { ...this.rateLimiting };
            }
            if (this.truncated !== undefined) {
                result.truncated = this.truncated;
            }
            return result;
        }
        /**
         * Sets a new collection path for this exposed entity.
         *
         * It:
         * - updates the collectionPath property
         * - updates the absoluteCollectionPath property accordingly
         * - updates the resourcePath accordingly.
         * @param path The new path to set.
         */
        setCollectionPath(path) {
            if (!this.hasCollection) {
                throw new Error(`Cannot set collection path on an exposure that does not have a collection`);
            }
            const cleaned = ensureLeadingSlash(path);
            // Ensure exactly one non-empty segment
            const segments = cleaned.split('/').filter(Boolean);
            if (segments.length !== 1) {
                throw new Error(`Collection path must contain exactly one segment. Received: "${path}"`);
            }
            const normalizedCollection = `/${segments[0]}`;
            // Preserve current parameter name if present, otherwise default to {id}
            let param = '{id}';
            if (this.resourcePath) {
                const curSegments = this.resourcePath.split('/').filter(Boolean);
                const maybeParam = curSegments[1];
                if (maybeParam && /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(maybeParam)) {
                    param = maybeParam;
                }
            }
            const nextResource = `${normalizedCollection}/${param}`;
            this.collectionPath = normalizedCollection;
            this.resourcePath = nextResource;
            // rely on ApiModel.exposes deep observation to notify on property sets
        }
        /**
         * Sets a new resource path for this exposed entity.
         *
         * Rules:
         * - Must start with '/'.
         * - If this exposure has a collection, the path must be exactly the collection path plus a single
         *   parameter segment (e.g. `/products/{productId}`) and only the parameter name may vary.
         * - If this exposure does NOT have a collection, the path can be any two segments (e.g. `/profile/{id}` or `/a/b`).
         */
        setResourcePath(path) {
            const cleaned = ensureLeadingSlash(path);
            const segments = cleaned.split('/').filter(Boolean);
            if (this.hasCollection) {
                if (!this.collectionPath) {
                    throw new Error('Cannot set resource path: missing collection path for this exposure');
                }
                const colSegments = this.collectionPath.split('/').filter(Boolean);
                if (colSegments.length !== 1) {
                    throw new Error(`Invalid stored collection path "${this.collectionPath}"`);
                }
                if (segments.length !== 2) {
                    throw new Error(`Resource path must be exactly two segments (collection + parameter). Received: "${cleaned}"`);
                }
                const [s1, s2] = segments;
                if (s1 !== colSegments[0]) {
                    throw new Error(`Resource path must start with the collection segment "${colSegments[0]}". Received: "${s1}"`);
                }
                // s2 must be a parameter segment {name}
                if (!/^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(s2)) {
                    throw new Error(`The second segment must be a parameter in braces, e.g. {id}. Received: "${s2}"`);
                }
                if (this.resourcePath !== cleaned) {
                    this.resourcePath = `/${s1}/${s2}`;
                }
                return;
            }
            // No collection: allow any two segments
            if (segments.length !== 2) {
                throw new Error(`Resource path must contain exactly two segments when no collection is present. Received: "${cleaned}"`);
            }
            if (this.resourcePath !== cleaned) {
                this.resourcePath = `/${segments[0]}/${segments[1]}`;
            }
        }
        /**
         * Computes the absolute path for this exposure's resource endpoint by
         * walking up the exposure tree using `parent.key` until reaching a root exposure.
         * The absolute path is composed by concatenating each ancestor's resource path
         * with this exposure's resource path.
         */
        getAbsoluteResourcePath() {
            let absolute = ensureLeadingSlash(this.resourcePath);
            // Traverse parents, always joining with the parent's resource path
            let parentKey = this.parent?.key;
            while (parentKey) {
                const parent = this.api.exposes.find((e) => e.key === parentKey);
                if (!parent)
                    break;
                const parentResource = ensureLeadingSlash(parent.resourcePath);
                absolute = joinPaths(parentResource, absolute);
                parentKey = parent.parent?.key;
            }
            return absolute;
        }
        /**
         * Computes the absolute path for this exposure's collection endpoint (if any)
         * by walking up the exposure tree using `parent.key` until reaching a root exposure.
         * The absolute path is composed by concatenating each ancestor's resource path
         * with this exposure's collection path.
         * Returns undefined if this exposure has no collection.
         */
        getAbsoluteCollectionPath() {
            if (!this.hasCollection || !this.collectionPath)
                return undefined;
            let absolute = ensureLeadingSlash(this.collectionPath);
            // Traverse parents, always joining with the parent's resource path
            let parentKey = this.parent?.key;
            while (parentKey) {
                const parent = this.api.exposes.find((e) => e.key === parentKey);
                if (!parent)
                    break;
                const parentResource = ensureLeadingSlash(parent.resourcePath);
                absolute = joinPaths(parentResource, absolute);
                parentKey = parent.parent?.key;
            }
            return absolute;
        }
    };
})();
export { ExposedEntity };
//# sourceMappingURL=ExposedEntity.js.map