import { ReasonCode } from '../types/index.js';
import { createNimiError } from './errors.js';
import { toIsoFromTimestamp } from './helpers.js';
import { AppInstallJobPhase as ProtoAppInstallJobPhase, AppInstallJobState as ProtoAppInstallJobState, AppInstallSourceKind as ProtoAppInstallSourceKind, AppLifecycleJobKind as ProtoAppLifecycleJobKind, AppOpenFlowStep as ProtoAppOpenFlowStep, AppOpenState as ProtoAppOpenState, } from './generated/runtime/v1/app.js';
import { ReasonCode as RuntimeReasonCode } from './generated/runtime/v1/common.js';
// ── Proto → typed decode (fail-closed) ─────────────────────────────────
function decodeError(message, reasonCode) {
    throw createNimiError({
        message,
        reasonCode,
        actionHint: 'check_runtime_app_lifecycle_projection',
        source: 'runtime',
    });
}
function optionalString(value) {
    return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}
function decodePhase(value) {
    switch (value) {
        case ProtoAppInstallJobPhase.QUEUED:
            return 'queued';
        case ProtoAppInstallJobPhase.RESOLVE_DESCRIPTOR:
            return 'resolve_descriptor';
        case ProtoAppInstallJobPhase.DOWNLOAD:
            return 'download';
        case ProtoAppInstallJobPhase.VERIFY:
            return 'verify';
        case ProtoAppInstallJobPhase.MATERIALIZE:
            return 'materialize';
        case ProtoAppInstallJobPhase.UNPACK:
            return 'unpack';
        case ProtoAppInstallJobPhase.EVIDENCE:
            return 'evidence';
        case ProtoAppInstallJobPhase.INSTALLED:
            return 'installed';
        case ProtoAppInstallJobPhase.SWAP:
            return 'swap';
        case ProtoAppInstallJobPhase.FAILED:
            return 'failed';
        case ProtoAppInstallJobPhase.CANCELLED:
            return 'cancelled';
        case ProtoAppInstallJobPhase.UNINSTALLED:
            return 'uninstalled';
        default:
            return decodeError(`runtime app install job has unspecified phase: ${String(value)}`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
}
function decodeState(value) {
    switch (value) {
        case ProtoAppInstallJobState.QUEUED:
            return 'queued';
        case ProtoAppInstallJobState.IN_PROGRESS:
            return 'in_progress';
        case ProtoAppInstallJobState.INSTALLED:
            return 'installed';
        case ProtoAppInstallJobState.FAILED:
            return 'failed';
        case ProtoAppInstallJobState.CANCELLED:
            return 'cancelled';
        case ProtoAppInstallJobState.UNINSTALLED:
            return 'uninstalled';
        default:
            return decodeError(`runtime app install job has unspecified state: ${String(value)}`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
}
function decodeKind(value) {
    switch (value) {
        case ProtoAppLifecycleJobKind.INSTALL:
            return 'install';
        case ProtoAppLifecycleJobKind.UPDATE:
            return 'update';
        case ProtoAppLifecycleJobKind.REPAIR:
            return 'repair';
        case ProtoAppLifecycleJobKind.UNINSTALL:
            return 'uninstall';
        default:
            return decodeError(`runtime app install job has unspecified kind: ${String(value)}`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
}
function decodeSourceKind(value) {
    switch (value) {
        case ProtoAppInstallSourceKind.BUNDLED:
            return 'bundled';
        case ProtoAppInstallSourceKind.EXTERNAL_ARTIFACT:
            return 'external_artifact';
        default:
            return decodeError(`runtime app install job has unspecified source kind: ${String(value)}`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
}
/**
 * Map a runtime-owned numeric `ReasonCode` to its stable string name. An
 * unknown / unspecified code fail-closes — it is never collapsed silently.
 */
function decodeReasonCode(value) {
    if (!Number.isFinite(value) || value === RuntimeReasonCode.REASON_CODE_UNSPECIFIED) {
        return undefined;
    }
    const name = RuntimeReasonCode[value];
    if (!name) {
        return decodeError(`runtime app lifecycle job carries an unknown reason code: ${String(value)}`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    return String(name).trim();
}
function decodeStorage(storage) {
    if (!storage) {
        return decodeError('runtime app lifecycle projection is missing the storage roots', ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    return {
        appRoot: storage.appRoot,
        releaseRoot: storage.releaseRoot,
        durableDataRoot: storage.durableDataRoot,
        cacheRoot: storage.cacheRoot,
        tempRoot: storage.tempRoot,
    };
}
function toBytes(value) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : 0;
}
function toSequence(value) {
    const parsed = Number(value);
    if (!Number.isFinite(parsed) || parsed < 0) {
        return decodeError(`runtime app install job event has an invalid sequence: ${String(value)}`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    return parsed;
}
/**
 * Decode a proto `AppInstallJob` into the typed projection. A missing job
 * fail-closes — the consumer never receives a synthesized placeholder.
 */
export function decodeAppInstallJob(job) {
    if (!job) {
        return decodeError('runtime app lifecycle response is missing the job projection', ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    const jobId = optionalString(job.jobId);
    if (!jobId) {
        return decodeError('runtime app install job is missing a job id', ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    const appId = optionalString(job.appId);
    if (!appId) {
        return decodeError('runtime app install job is missing an app id', ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    const state = decodeState(job.state);
    const reasonCode = decodeReasonCode(job.reasonCode);
    // A failed / cancelled job must always carry a typed reason code so the
    // consumer never has to invent one — fail-close if the runtime omits it.
    if ((state === 'failed' || state === 'cancelled') && !reasonCode) {
        return decodeError(`runtime app install job ${jobId} is ${state} without a typed reason code`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    return {
        jobId,
        appId,
        kind: decodeKind(job.kind),
        releaseDescriptorRef: job.releaseDescriptorRef,
        installedVersion: job.installedVersion,
        ...(optionalString(job.previousVersion)
            ? { previousVersion: optionalString(job.previousVersion) }
            : {}),
        state,
        phase: decodePhase(job.phase),
        sourceKind: decodeSourceKind(job.sourceKind),
        ...(optionalString(job.sha256) ? { sha256: optionalString(job.sha256) } : {}),
        artifactBytes: toBytes(job.artifactBytes),
        storage: decodeStorage(job.storage),
        ...(reasonCode ? { reasonCode } : {}),
        ...(optionalString(job.failureDetail)
            ? { failureDetail: optionalString(job.failureDetail) }
            : {}),
        retryable: job.retryable,
        ...(optionalString(job.createdAt) ? { createdAt: optionalString(job.createdAt) } : {}),
        ...(optionalString(job.updatedAt) ? { updatedAt: optionalString(job.updatedAt) } : {}),
    };
}
export function decodeUninstallResult(result, jobProjection) {
    if (!result) {
        return decodeError('runtime uninstall response is missing the result projection', ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    const appId = optionalString(result.appId);
    if (!appId) {
        return decodeError('runtime uninstall result is missing an app id', ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    const reasonCode = decodeReasonCode(result.reasonCode);
    // The uninstall response carries the watchable uninstall lifecycle job
    // (K-APP-017) — the single live-job truth source for the `uninstalling`
    // card state. A missing job fail-closes; it is never synthesized.
    const job = decodeAppInstallJob(jobProjection);
    return {
        appId,
        releaseRemoved: result.releaseRemoved,
        durableDataRemoved: result.durableDataRemoved,
        storage: decodeStorage(result.storage),
        ...(reasonCode ? { reasonCode } : {}),
        job,
    };
}
function decodeOpenFlowStep(value) {
    switch (value) {
        case ProtoAppOpenFlowStep.RESOLVE_REGISTRY:
            return 'resolve_registry';
        case ProtoAppOpenFlowStep.VERIFY_PACKAGE:
            return 'verify_package';
        case ProtoAppOpenFlowStep.VERIFY_LIBRARY:
            return 'verify_library';
        case ProtoAppOpenFlowStep.VERIFY_APP_DATA:
            return 'verify_app_data';
        case ProtoAppOpenFlowStep.VERIFY_PERMISSIONS:
            return 'verify_permissions';
        case ProtoAppOpenFlowStep.ENSURE_AICONFIG:
            return 'ensure_aiconfig';
        case ProtoAppOpenFlowStep.VALIDATE_MANIFEST:
            return 'validate_manifest';
        case ProtoAppOpenFlowStep.LAUNCH:
            return 'launch';
        default:
            return decodeError(`runtime app open projection has an unspecified flow step: ${String(value)}`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
}
function decodeOpenState(value) {
    switch (value) {
        case ProtoAppOpenState.LAUNCHED:
            return 'launched';
        case ProtoAppOpenState.BLOCKED:
            return 'blocked';
        default:
            return decodeError(`runtime app open projection has an unspecified state: ${String(value)}`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
}
function decodeOpenScope(scope) {
    if (!scope)
        return undefined;
    const ownerId = optionalString(scope.ownerId);
    if (scope.kind !== 'app' || !ownerId) {
        return decodeError('runtime app open projection carries a non-canonical app-launch scope', ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    const surfaceId = optionalString(scope.surfaceId);
    return {
        kind: 'app',
        ownerId,
        ...(surfaceId ? { surfaceId } : {}),
    };
}
/**
 * Decode a proto `AppOpenProjection` into the typed projection. A `blocked`
 * open must always carry a typed reason code so the consumer never has to
 * invent one — fail-close if the runtime omits it.
 */
export function decodeOpenProjection(projection) {
    if (!projection) {
        return decodeError('runtime open response is missing the open projection', ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    const appId = optionalString(projection.appId);
    if (!appId) {
        return decodeError('runtime app open projection is missing an app id', ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    const state = decodeOpenState(projection.state);
    const reasonCode = decodeReasonCode(projection.reasonCode);
    if (state === 'blocked' && !reasonCode) {
        return decodeError(`runtime app open projection for ${appId} is blocked without a typed reason code`, ReasonCode.SDK_RUNTIME_RESPONSE_DECODE_FAILED);
    }
    const activeVersion = optionalString(projection.activeVersion);
    const detail = optionalString(projection.detail);
    const scope = decodeOpenScope(projection.scope);
    return {
        appId,
        state,
        reachedStep: decodeOpenFlowStep(projection.reachedStep),
        launched: projection.launched,
        ...(activeVersion ? { activeVersion } : {}),
        ...(scope ? { scope } : {}),
        ...(reasonCode ? { reasonCode } : {}),
        ...(detail ? { detail } : {}),
    };
}
export function decodeJobEvent(event) {
    const timestamp = toIsoFromTimestamp(event.timestamp);
    return {
        sequence: toSequence(event.sequence),
        job: decodeAppInstallJob(event.job),
        ...(timestamp ? { timestamp } : {}),
    };
}
//# sourceMappingURL=runtime-app-lifecycle-decode.js.map