import { BaseEngine, Metadata, } from '@memberjunction/core';
import { UUIDsEqual } from '@memberjunction/global';
/**
 * Name of the seeded `MJ: Record Process Categories` row that marks a Record
 * Process as a Feature Pipeline. Authored in
 * `metadata/record-process-categories/.record-process-categories.json` and pushed
 * with `mj sync`. Mirrors `FEATURE_PIPELINE_CATEGORY_NAME` in the server-side
 * `@memberjunction/predictive-studio` engine — kept in sync by value (the two
 * packages don't share a dependency, so the constant is intentionally duplicated
 * rather than re-exported across the package boundary).
 */
export const FEATURE_PIPELINE_CATEGORY_NAME = 'Feature Pipeline';
/**
 * **FeaturePipelineEngine** (Knowledge Hub, Angular) — the discovery + monitoring
 * cache for Feature Pipelines (Predictive Studio SP6).
 *
 * A Feature Pipeline has no dedicated entity: it IS an `MJ: Record Processes` row
 * categorized into the seeded {@link FEATURE_PIPELINE_CATEGORY_NAME} category. This
 * engine caches those rows (plus the recent `MJ: Process Runs` headers) so the KH
 * UI can list "what feature pipelines exist, what entity each writes to, and when
 * each last ran" — reactively, via the canonical MJ BaseEngine pattern (`Config()`
 * declaring `BaseEnginePropertyConfig[]`, lazy-loaded, `ObserveProperty`
 * reactivity for free).
 *
 * The freshness/projection logic is a pure static function ({@link BuildSummaries})
 * so it can be exercised without a provider, and it intentionally mirrors the
 * server-side `@memberjunction/predictive-studio` engine of the same name (the two
 * don't share a dependency).
 */
export class FeaturePipelineEngine extends BaseEngine {
    /** Singleton accessor for the default provider — never `new` this directly. */
    static get Instance() {
        return super.getInstance();
    }
    _RecordProcesses = [];
    _ProcessRuns = [];
    /**
     * Lazy-load the Record Process + recent Process Run caches. Safe to call from
     * every entry point — a no-op when already loaded (unless `forceRefresh`).
     * Process Runs are ordered newest-first so the most recent run per pipeline is
     * the first match.
     */
    async Config(forceRefresh, contextUser, provider) {
        const c = [
            { Type: 'entity', EntityName: 'MJ: Record Processes', PropertyName: '_RecordProcesses', OrderBy: 'Name' },
            { Type: 'entity', EntityName: 'MJ: Process Runs', PropertyName: '_ProcessRuns', OrderBy: '__mj_CreatedAt DESC' },
        ];
        await super.Load(c, provider ?? Metadata.Provider, forceRefresh, contextUser);
    }
    // ---- Cached arrays (sync getters; observe via the streams for reactive UI) ----
    /** Every cached Record Process categorized as a Feature Pipeline. */
    get Pipelines() {
        return (this._RecordProcesses ?? []).filter((rp) => FeaturePipelineEngine.isFeaturePipeline(rp));
    }
    /** All cached Process Run headers (newest first; all entities — callers filter). */
    get Runs() {
        return this._ProcessRuns ?? [];
    }
    /** Reactive stream of the raw Record Processes — re-emits on any save/delete/remote-invalidate. */
    get Pipelines$() {
        return this.ObserveProperty('_RecordProcesses');
    }
    /** Reactive stream of the Process Run headers. */
    get Runs$() {
        return this.ObserveProperty('_ProcessRuns');
    }
    // ---- Projection ----
    /** Build the UI summaries for every feature pipeline from the loaded caches. */
    GetSummaries() {
        return FeaturePipelineEngine.BuildSummaries(this.Pipelines, this.Runs);
    }
    /**
     * Pure, side-effect-free summary builder — used by {@link GetSummaries} and by
     * unit tests (which pass in-memory arrays, no provider).
     */
    static BuildSummaries(pipelines, runs) {
        return pipelines.map((p) => FeaturePipelineEngine.buildSummary(p, runs));
    }
    // ---- Internal helpers ----
    /** Whether a Record Process belongs to the seeded Feature Pipeline category. */
    static isFeaturePipeline(rp) {
        return (rp.Category ?? '').trim().toLowerCase() === FEATURE_PIPELINE_CATEGORY_NAME.toLowerCase();
    }
    /** Project one pipeline + the run set into a {@link FeaturePipelineSummary}. */
    static buildSummary(p, runs) {
        const latest = FeaturePipelineEngine.newestRun(runs, p.ID);
        return {
            ID: p.ID,
            Name: p.Name,
            Description: p.Description ?? null,
            Status: p.Status,
            WorkType: p.WorkType,
            TargetEntityID: p.EntityID,
            TargetEntity: p.Entity,
            OutputAttribute: FeaturePipelineEngine.deriveOutputAttribute(p.OutputMapping),
            OnDemandEnabled: p.OnDemandEnabled,
            ScheduleEnabled: p.ScheduleEnabled,
            LastRunAt: latest?.StartTime ?? latest?.EndTime ?? null,
            LastRunStatus: FeaturePipelineEngine.mapRunStatus(latest),
            LastRunProcessRunID: latest?.ID ?? null,
            LastRunProcessed: latest?.ProcessedItems ?? null,
            LastRunSuccess: latest?.SuccessCount ?? null,
            LastRunErrors: latest?.ErrorCount ?? null,
        };
    }
    /** Newest run for a pipeline id, scanning by StartTime/EndTime/createdAt regardless of input order. */
    static newestRun(runs, recordProcessID) {
        let best;
        let bestTime = -Infinity;
        for (const r of runs) {
            if (r.RecordProcessID == null || !UUIDsEqual(r.RecordProcessID, recordProcessID))
                continue;
            const candidate = r.StartTime ?? r.EndTime ?? r.__mj_CreatedAt;
            const t = candidate ? new Date(candidate).getTime() : -Infinity;
            if (t > bestTime) {
                bestTime = t;
                best = r;
            }
        }
        return best;
    }
    /** Normalize a Process Run status into the small union the UI renders. `undefined` → `'Never'`. */
    static mapRunStatus(run) {
        if (!run)
            return 'Never';
        switch (run.Status) {
            case 'Completed':
                return 'Completed';
            case 'Failed':
                return 'Failed';
            case 'Cancelled':
                return 'Cancelled';
            case 'Running':
            case 'Pending':
            case 'Paused':
                return 'Running';
            default:
                return 'Never';
        }
    }
    /**
     * Best-effort attribute the pipeline writes, parsed from the Record Process
     * `OutputMapping` JSON. Probes the common shapes (`field` / `targetField` /
     * `attribute`, or a `fields` array / object) and returns null otherwise. Never
     * throws on malformed JSON.
     */
    static deriveOutputAttribute(outputMapping) {
        if (!outputMapping)
            return null;
        let parsed;
        try {
            parsed = JSON.parse(outputMapping);
        }
        catch {
            return null;
        }
        if (parsed == null || typeof parsed !== 'object')
            return null;
        const obj = parsed;
        const single = obj['field'] ?? obj['targetField'] ?? obj['attribute'];
        if (typeof single === 'string' && single.trim())
            return single.trim();
        const names = FeaturePipelineEngine.fieldNamesFrom(obj['fields']);
        return names.length > 0 ? names.join(', ') : null;
    }
    /** Collect field names from either an object map or an array of names. */
    static fieldNamesFrom(fields) {
        if (Array.isArray(fields)) {
            return fields.filter((f) => typeof f === 'string' && f.trim().length > 0);
        }
        if (fields != null && typeof fields === 'object') {
            return Object.keys(fields);
        }
        return [];
    }
}
//# sourceMappingURL=feature-pipeline.engine.js.map