import { __esDecorate, __runInitializers } from "tslib";
import { nanoid } from '../nanoid.js';
import { ApiModelKind, DataDomainKind, ExposedEntityKind } from '../models/kinds.js';
import { Thing } from '../models/Thing.js';
import { DataDomain } from './DataDomain.js';
import { DependentModel } from './DependentModel.js';
import { observed, toRaw } from '../decorators/observed.js';
import pluralize from '@jarrodek/pluralize';
import { createDomainKey } from './helpers/keying.js';
import { ExposedEntity } from './ExposedEntity.js';
import { RateLimitingConfiguration } from './rules/RateLimitingConfiguration.js';
import { restoreAccessRule } from './rules/index.js';
let ApiModel = (() => {
    let _classSuper = DependentModel;
    let _exposes_decorators;
    let _exposes_initializers = [];
    let _exposes_extraInitializers = [];
    let _accessRule_decorators;
    let _accessRule_initializers = [];
    let _accessRule_extraInitializers = [];
    let _rateLimiting_decorators;
    let _rateLimiting_initializers = [];
    let _rateLimiting_extraInitializers = [];
    let _termsOfService_decorators;
    let _termsOfService_initializers = [];
    let _termsOfService_extraInitializers = [];
    let _contact_decorators;
    let _contact_initializers = [];
    let _contact_extraInitializers = [];
    let _license_decorators;
    let _license_initializers = [];
    let _license_extraInitializers = [];
    return class ApiModel extends _classSuper {
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
            _exposes_decorators = [observed({ deep: true })];
            _accessRule_decorators = [observed({ deep: true })];
            _rateLimiting_decorators = [observed()];
            _termsOfService_decorators = [observed()];
            _contact_decorators = [observed({ deep: true })];
            _license_decorators = [observed({ deep: true })];
            __esDecorate(this, null, _exposes_decorators, { kind: "accessor", name: "exposes", static: false, private: false, access: { has: obj => "exposes" in obj, get: obj => obj.exposes, set: (obj, value) => { obj.exposes = value; } }, metadata: _metadata }, _exposes_initializers, _exposes_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);
            __esDecorate(this, null, _termsOfService_decorators, { kind: "accessor", name: "termsOfService", static: false, private: false, access: { has: obj => "termsOfService" in obj, get: obj => obj.termsOfService, set: (obj, value) => { obj.termsOfService = value; } }, metadata: _metadata }, _termsOfService_initializers, _termsOfService_extraInitializers);
            __esDecorate(this, null, _contact_decorators, { kind: "accessor", name: "contact", static: false, private: false, access: { has: obj => "contact" in obj, get: obj => obj.contact, set: (obj, value) => { obj.contact = value; } }, metadata: _metadata }, _contact_initializers, _contact_extraInitializers);
            __esDecorate(this, null, _license_decorators, { kind: "accessor", name: "license", static: false, private: false, access: { has: obj => "license" in obj, get: obj => obj.license, set: (obj, value) => { obj.license = value; } }, metadata: _metadata }, _license_initializers, _license_extraInitializers);
            if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        /**
         * The data domain kind recognizable by the ecosystem.
         */
        kind;
        /**
         * The unique key of the data domain schema.
         * This is a stable identifier that does not change across versions.
         */
        key;
        /**
         * The description of the domain property.
         */
        info;
        /**
         * The designated Data Entity that represents a "User".
         * This entity must be marked with the "User" semantic in the Data Modeler.
         *
         * This property is required to publish the API.
         */
        user;
        /**
         * Configuration for how users prove their identity.
         * The API model is invalid if this is not set.
         */
        authentication;
        /**
         * Configuration for what authenticated users are allowed to do.
         * The API model is invalid if this is not set.
         */
        authorization;
        /**
         * Configuration for the transport and payload of the user session.
         * The API model is invalid if this is not set.
         */
        session;
        #exposes_accessor_storage = __runInitializers(this, _exposes_initializers, void 0);
        /**
         * The specific subset of Data Entities to be exposed by this API.
         * These are the entities that are included in the data domain schema.
         */
        get exposes() { return this.#exposes_accessor_storage; }
        set exposes(value) { this.#exposes_accessor_storage = value; }
        #accessRule_accessor_storage = (__runInitializers(this, _exposes_extraInitializers), __runInitializers(this, _accessRule_initializers, void 0));
        /**
         * Optional array of access rules that define the access control policies
         * for the API. These rules are used to enforce security and permissions
         * on the exposed entities.
         *
         * These rules apply to all exposed entities and actions. An API action
         * can declare its own access rules, which will override these.
         */
        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 API-wide rate limiting and throttling.
         * Defines rules to protect the API from overuse.
         */
        get rateLimiting() { return this.#rateLimiting_accessor_storage; }
        set rateLimiting(value) { this.#rateLimiting_accessor_storage = value; }
        #termsOfService_accessor_storage = (__runInitializers(this, _rateLimiting_extraInitializers), __runInitializers(this, _termsOfService_initializers, void 0));
        /**
         * A URL to the Terms of Service for the API.
         */
        get termsOfService() { return this.#termsOfService_accessor_storage; }
        set termsOfService(value) { this.#termsOfService_accessor_storage = value; }
        #contact_accessor_storage = (__runInitializers(this, _termsOfService_extraInitializers), __runInitializers(this, _contact_initializers, void 0));
        /**
         * The contact information for the exposed API.
         */
        get contact() { return this.#contact_accessor_storage; }
        set contact(value) { this.#contact_accessor_storage = value; }
        #license_accessor_storage = (__runInitializers(this, _contact_extraInitializers), __runInitializers(this, _license_initializers, void 0));
        /**
         * The license information for the API.
         */
        get license() { return this.#license_accessor_storage; }
        set license(value) { this.#license_accessor_storage = value; }
        /**
         * When the initializing flag is set to true,
         * the domain is not notified of changes.
         */
        #initializing = (__runInitializers(this, _license_extraInitializers), true);
        /**
         * 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;
        /**
         * A convenience getter that returns the DataDomain associated with this API model.
         * Since the API model can have only one DataDomain,
         * this getter returns the first dependency in the list.
         *
         * The parent interface `DependentModel` allows for multiple dependencies,
         * to unify the dependency management across different models.
         */
        get domain() {
            if (this.dependencyList.length === 0) {
                return undefined;
            }
            const domain = this.dependencyList[0];
            return this.dependencies.get(domain.key);
        }
        static createSchema(input = {}) {
            const { key = nanoid(), exposes = [] } = input;
            const info = Thing.fromJSON(input.info, { name: 'Unnamed API' }).toJSON();
            const result = {
                kind: ApiModelKind,
                key,
                info,
                exposes,
            };
            if (input.user) {
                result.user = structuredClone(input.user);
            }
            if (input.dependencyList) {
                result.dependencyList = structuredClone(input.dependencyList);
            }
            if (input.authentication) {
                result.authentication = structuredClone(input.authentication);
            }
            if (input.authorization) {
                result.authorization = structuredClone(input.authorization);
            }
            if (input.session) {
                result.session = structuredClone(input.session);
            }
            if (Array.isArray(input.accessRule)) {
                result.accessRule = structuredClone(input.accessRule);
            }
            if (input.rateLimiting) {
                result.rateLimiting = structuredClone(input.rateLimiting);
            }
            if (input.termsOfService) {
                result.termsOfService = input.termsOfService;
            }
            if (input.contact) {
                result.contact = structuredClone(input.contact);
            }
            if (input.license) {
                result.license = structuredClone(input.license);
            }
            return result;
        }
        constructor(state, domain) {
            const init = ApiModel.createSchema(state);
            const instances = [];
            if (domain instanceof DataDomain) {
                instances.push(domain);
            }
            else if (typeof domain === 'object' && domain.kind === DataDomainKind) {
                instances.push(new DataDomain(domain));
            }
            else if (domain) {
                throw new Error(`Invalid domain provided. Expected a DataDomain instance or schema.`);
            }
            // Note that since we're using the `DependentModel` class, but the API Model can have only one dependency,
            // we keep the reference to the data domain under the `dependencyList` property.
            // It is all handled by the parent class `DependentModel`. This way we simplify the dependency management (loading)
            // process when the API model is loaded from the API.
            if (domain) {
                if (!domain.info.version) {
                    throw new Error(`Domain ${domain.key} must have a version.`);
                }
                init.dependencyList = [{ key: domain.key, version: domain.info.version }];
            }
            super(init.dependencyList, instances);
            this.kind = init.kind;
            this.key = init.key;
            this.info = new Thing(init.info);
            this.user = init.user;
            if (init.authentication) {
                this.authentication = structuredClone(init.authentication);
            }
            if (init.authorization) {
                this.authorization = structuredClone(init.authorization);
            }
            if (init.session) {
                this.session = structuredClone(init.session);
            }
            if (Array.isArray(init.exposes)) {
                this.exposes = init.exposes.map((e) => new ExposedEntity(this, e));
            }
            else {
                this.exposes = [];
            }
            if (Array.isArray(init.accessRule)) {
                this.accessRule = init.accessRule.map((rule) => restoreAccessRule(rule));
            }
            else {
                this.accessRule = [];
            }
            if (init.rateLimiting) {
                this.rateLimiting = new RateLimitingConfiguration(init.rateLimiting);
            }
            if (init.termsOfService) {
                this.termsOfService = init.termsOfService;
            }
            if (init.contact) {
                this.contact = init.contact;
            }
            if (init.license) {
                this.license = init.license;
            }
            this.#initializing = false;
            this.info.addEventListener('change', () => {
                this.notifyChange();
            });
        }
        toJSON() {
            const result = {
                kind: this.kind,
                key: this.key,
                info: this.info.toJSON(),
                exposes: this.exposes.map((e) => e.toJSON()),
            };
            if (this.user) {
                result.user = { ...this.user };
            }
            if (this.dependencyList.length > 0) {
                result.dependencyList = structuredClone(this.dependencyList);
            }
            if (this.authentication) {
                result.authentication = structuredClone(this.authentication);
            }
            if (this.authorization) {
                result.authorization = structuredClone(this.authorization);
            }
            if (this.session) {
                result.session = structuredClone(this.session);
            }
            if (Array.isArray(this.accessRule) && this.accessRule.length > 0) {
                result.accessRule = this.accessRule.map((rule) => rule.toJSON());
            }
            if (this.rateLimiting) {
                result.rateLimiting = this.rateLimiting.toJSON();
            }
            if (this.termsOfService) {
                result.termsOfService = this.termsOfService;
            }
            if (this.contact) {
                result.contact = structuredClone(toRaw(this, this.contact));
            }
            if (this.license) {
                result.license = structuredClone(toRaw(this, this.license));
            }
            return result;
        }
        /**
         * This function is used internally by all domain elements to notify that something has changed.
         * Since we want to notify listeners after the operation commits, we use microtask
         * to ensure that the event is dispatched after the current operation.
         */
        notifyChange() {
            if (this.#notifying || this.#initializing) {
                return;
            }
            this.#notifying = true;
            queueMicrotask(() => {
                this.#notifying = false;
                const event = new Event('change');
                this.dispatchEvent(event);
            });
        }
        /**
         * Exposes a new entity in the API model.
         * If the entity already exists, it returns the existing one.
         *
         * The logic regarding exposing an entity:
         * - If the entity is already exposed as a root entity, it returns the existing one.
         * - If the entity has an association, we expose that entity as a nested entity (by setting the parent property).
         * - If the associated entity is already exposed as a root entity, we do not follow the association.
         *
         * @param entity The entity key and domain to expose.
         * @returns The exposed entity.
         */
        exposeEntity(entity, options) {
            const domain = this.domain;
            if (!domain) {
                throw new Error(`No domain attached to API model`);
            }
            // checks whether the entity is already exposed as a root exposure.
            const existing = this.exposes.find((e) => e.isRoot && e.entity.key === entity.key && e.entity.domain === entity.domain);
            if (existing) {
                // quietly return the existing exposure
                // TBD: should we throw an error here?
                return existing;
            }
            const domainEntity = domain.findEntity(entity.key, entity.domain);
            if (!domainEntity) {
                throw new Error(`Entity not found in domain: ${entity.key}`);
            }
            const name = domainEntity.info.name || '';
            const segment = pluralize(name.toLocaleLowerCase());
            let relativeCollectionPath = `/${segment}`;
            let relativeResourcePath = `/${segment}/{id}`;
            // Check for root path collision and resolve by appending a number
            let counter = 1;
            const originalCollectionPath = relativeCollectionPath;
            while (this.exposes.some((e) => e.isRoot && e.collectionPath === relativeCollectionPath)) {
                relativeCollectionPath = `${originalCollectionPath}-${counter}`;
                relativeResourcePath = `${relativeCollectionPath}/{id}`;
                counter++;
            }
            const newEntity = {
                kind: ExposedEntityKind,
                key: nanoid(),
                entity: { ...entity },
                actions: [],
                isRoot: true,
                collectionPath: relativeCollectionPath,
                resourcePath: relativeResourcePath,
                hasCollection: true,
            };
            if (options) {
                newEntity.exposeOptions = { ...options };
            }
            const created = new ExposedEntity(this, newEntity);
            this.exposes.push(created);
            // Follow associations if requested
            if (options?.followAssociations) {
                if (options?.maxDepth === undefined || options.maxDepth > 0) {
                    this.followEntityAssociations(newEntity, options);
                }
            }
            this.notifyChange();
            return created;
        }
        /**
         * Follows associations for a newly exposed entity if configured to do so.
         * This creates nested exposures based on the entity's associations.
         *
         * @param parentExposure The root exposure to follow associations from
         * @param options The expose options containing follow configuration
         */
        followEntityAssociations(parentExposure, options) {
            const domain = this.domain;
            if (!domain) {
                return;
            }
            const maxDepth = options.maxDepth ?? 6;
            const follow = (currentEntity, parentKey, depth, currentPath) => {
                // Find the domain entity
                const domainEntity = domain.findEntity(currentEntity.key, currentEntity.domain);
                if (!domainEntity)
                    return;
                // Iterate through associations
                for (const association of domainEntity.listAssociations()) {
                    for (const target of association.targets) {
                        // Skip self-referencing associations
                        if (target.key === currentEntity.key && target.domain === currentEntity.domain) {
                            continue;
                        }
                        const targetKeys = createDomainKey(target);
                        // Circular dependency check: if this entity is already in our *current* traversal path
                        if (currentPath.includes(targetKeys)) {
                            continue;
                        }
                        // Check if this entity is ALREADY exposed anywhere in the model
                        const existingExposure = this.getExposedEntity(target);
                        if (existingExposure) {
                            // If it's already exposed and NOT root (i.e., it's currently a nested child of someone else),
                            // promote it to ROOT to avoid duplicating it or deeply nesting it in multiple places.
                            if (!existingExposure.isRoot) {
                                // 1. Calculate new root paths (handling collisions)
                                const targetDomEntity = domain.findEntity(target.key, target.domain);
                                if (targetDomEntity) {
                                    const name = targetDomEntity.info.name || '';
                                    const segment = pluralize(name.toLocaleLowerCase());
                                    let relativeCollectionPath = `/${segment}`;
                                    let relativeResourcePath = `/${segment}/{id}`;
                                    let counter = 1;
                                    const originalCollectionPath = relativeCollectionPath;
                                    while (this.exposes.some((e) => e.isRoot && e.collectionPath === relativeCollectionPath)) {
                                        relativeCollectionPath = `${originalCollectionPath}-${counter}`;
                                        relativeResourcePath = `${relativeCollectionPath}/{id}`;
                                        counter++;
                                    }
                                    // 2. Update properties to make it root
                                    existingExposure.isRoot = true;
                                    existingExposure.parent = undefined;
                                    existingExposure.collectionPath = relativeCollectionPath;
                                    existingExposure.resourcePath = relativeResourcePath;
                                    existingExposure.hasCollection = true;
                                }
                            }
                            // Whether it was already root or we just promoted it, we stop following here.
                            continue;
                        }
                        // Find the target domain entity for path generation
                        const targetDomainEntity = domain.findEntity(target.key, target.domain);
                        if (!targetDomainEntity)
                            continue;
                        const name = association.info.name || '';
                        const segment = pluralize(name.toLocaleLowerCase());
                        const isCollection = association.multiple !== false;
                        const relativeCollectionPath = isCollection ? `/${segment}` : undefined;
                        const relativeResourcePath = isCollection ? `/${segment}/{id}` : `/${segment}`;
                        // Create nested exposure
                        const nestedExposure = {
                            kind: ExposedEntityKind,
                            key: nanoid(),
                            entity: { ...target },
                            actions: [],
                            isRoot: false,
                            collectionPath: relativeCollectionPath,
                            resourcePath: relativeResourcePath,
                            hasCollection: isCollection,
                            parent: {
                                key: parentKey,
                                association: {
                                    key: association.key,
                                    domain: currentEntity.domain,
                                },
                                depth: depth + 1,
                            },
                        };
                        this.exposes.push(new ExposedEntity(this, nestedExposure));
                        if (depth + 1 >= maxDepth) {
                            nestedExposure.truncated = true;
                        }
                        else {
                            // Recursively follow associations
                            follow(target, nestedExposure.key, depth + 1, [...currentPath, targetKeys]);
                        }
                    }
                }
            };
            // Start following from the root exposure
            // Initial path contains the root entity itself
            const rootKey = createDomainKey(parentExposure.entity);
            follow(parentExposure.entity, parentExposure.key, 0, [rootKey]);
        }
        /**
         * Removes an exposed entity from the API model.
         * This also removes any nested exposed entities that are children of the removed entity.
         *
         * @param key The key of the exposed entity to remove.
         */
        removeExposedEntity(key) {
            const index = this.exposes.findIndex((e) => e.key === key);
            if (index < 0) {
                throw new Error(`Exposed entity with key "${key}" not found.`);
            }
            this.removeWithChildren(key);
        }
        removeWithChildren(key) {
            const index = this.exposes.findIndex((e) => e.key === key);
            if (index < 0) {
                return;
            }
            // Remove the parent itself
            this.exposes.splice(index, 1);
            // Remove all children recursively
            const removeChildren = (parentKey) => {
                // Find all exposures whose parent.key matches parentKey
                const children = this.exposes.filter((e) => e.parent?.key === parentKey);
                for (const child of children) {
                    removeChildren(child.key);
                    const childIndex = this.exposes.findIndex((e) => e.key === child.key);
                    if (childIndex >= 0) {
                        this.exposes.splice(childIndex, 1);
                    }
                }
            };
            // Then also remove children
            removeChildren(key);
            this.notifyChange();
        }
        /**
         * Returns the exposed entity by its key.
         * @param entityKey The key of the entity to find.
         * @returns The exposed entity or undefined if not found.
         */
        getExposedEntity(entity) {
            return this.exposes.find((e) => e.entity.key === entity.key && e.entity.domain === entity.domain);
        }
        /**
         * Clears the API model for a new data domain change.
         * This method resets the dependencies, exposes, user,
         * authentication, authorization, and session properties.
         */
        cleanForDomainChange() {
            this.dependencies.clear();
            this.dependencyList = [];
            this.exposes = [];
            this.user = undefined;
            if (this.session) {
                this.session.properties = [];
            }
            if (this.authentication && this.authentication.strategy === 'UsernamePassword') {
                const typed = this.authentication;
                typed.passwordKey = undefined;
            }
            if (this.authorization && this.authorization.strategy == 'RBAC') {
                const typed = this.authorization;
                typed.roleKey = '';
            }
        }
        /**
         * Attaches a DataDomain to this API model.
         * This method clears any existing dependencies and sets the new domain.
         *
         * @param domain The DataDomain to attach to this API model.
         * @throws Error if the domain does not have a version set in its info.
         */
        attachDataDomain(domain) {
            if (!domain.info.version) {
                throw new Error(`Cannot attach DataDomain without a version. Please set the version in the domain info.`);
            }
            this.cleanForDomainChange();
            this.dependencies.set(domain.key, domain);
            this.dependencyList = [{ key: domain.key, version: domain.info.version }];
            this.notifyChange();
        }
    };
})();
export { ApiModel };
//# sourceMappingURL=ApiModel.js.map