/**
 * Auto-resolves flat key-value runtime inputs into the full YAML structure
 * that the Harness pipeline execute API expects.
 *
 * Flow:
 * 1. Fetch the runtime input template for the pipeline (POST /pipeline/api/inputSets/template)
 * 2. Parse the template YAML to find `<+input>` placeholders
 * 3. Match user-provided flat key-value pairs to the placeholders by field name
 * 4. Return the filled YAML string ready for the execute API
 */
import YAML from "yaml";
import { createLogger } from "./logger.js";
import { isRecord, asRecord, asString } from "./type-guards.js";
const log = createLogger("runtime-inputs");
const INPUT_PLACEHOLDER = /^<\+input>\.?/;
const HAS_DEFAULT = /^<\+input>\.default\(/;
const TEMPLATE_CACHE_TTL_MS = 5 * 60_000; // 5 minutes
const templateCache = new Map();
function templateCacheKey(opts) {
    return `${opts.pipelineId}|${opts.orgId ?? ""}|${opts.projectId ?? ""}|${opts.branch ?? ""}`;
}
/** Evict expired entries. Called on cache writes to prevent unbounded growth. */
function evictExpired() {
    const now = Date.now();
    for (const [key, entry] of templateCache) {
        if (now >= entry.expiresAt)
            templateCache.delete(key);
    }
}
/** Clear the template cache (useful for testing). */
export function clearTemplateCache() {
    templateCache.clear();
}
/**
 * Fetch the runtime input template for a pipeline.
 * Returns the raw template YAML string with `<+input>` placeholders, or null if no inputs needed.
 */
export async function fetchRuntimeInputTemplate(client, options) {
    const cacheKey = templateCacheKey(options);
    const cached = templateCache.get(cacheKey);
    if (cached && Date.now() < cached.expiresAt) {
        log.debug("Runtime input template cache hit", { pipelineId: options.pipelineId });
        return cached.yaml;
    }
    const params = {
        pipelineIdentifier: options.pipelineId,
    };
    if (options.orgId)
        params.orgIdentifier = options.orgId;
    if (options.projectId)
        params.projectIdentifier = options.projectId;
    if (options.branch)
        params.branch = options.branch;
    const raw = await client.request({
        method: "POST",
        path: "/pipeline/api/inputSets/template",
        params,
        body: {},
    });
    const data = asRecord(asRecord(raw)?.data);
    const templateYaml = asString(data?.inputSetTemplateYaml);
    const result = (templateYaml && templateYaml.trim() !== "") ? templateYaml : null;
    if (!result) {
        log.debug("Pipeline has no runtime inputs");
    }
    evictExpired();
    templateCache.set(cacheKey, { yaml: result, expiresAt: Date.now() + TEMPLATE_CACHE_TTL_MS });
    return result;
}
/**
 * Check if a value looks like a flat key-value map of runtime inputs
 * (as opposed to already being a full pipeline YAML structure or string).
 */
export function isFlatKeyValueInputs(inputs) {
    if (!isRecord(inputs))
        return false;
    // If it has a "pipeline" key, it's already a full structure
    if ("pipeline" in inputs)
        return false;
    // If all values are primitives (string, number, boolean), it's flat key-value
    return Object.values(inputs).every((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean");
}
/**
 * Check if inputs can be resolved through the template system.
 * Returns true for any object that isn't already a full pipeline YAML structure.
 * Broader than isFlatKeyValueInputs — also accepts structural/nested inputs
 * (e.g. codebase build objects with nested type/spec).
 */
export function isResolvableInputs(inputs) {
    if (!isRecord(inputs))
        return false;
    if ("pipeline" in inputs)
        return false;
    return true;
}
/**
 * Flatten nested object inputs into dot-separated keys for template matching.
 * Preserves intermediate object values at each level for whole-subtree matching.
 *
 * Example: { build: { type: "branch", spec: { branch: "main" } } }
 * → { "build": { type: "branch", ... }, "build.type": "branch", "build.spec.branch": "main" }
 */
export function flattenInputs(inputs) {
    const result = {};
    function recurse(obj, prefix) {
        for (const [key, value] of Object.entries(obj)) {
            const fullKey = prefix ? `${prefix}.${key}` : key;
            result[fullKey] = value;
            if (value !== null && typeof value === "object" && !Array.isArray(value)) {
                recurse(value, fullKey);
            }
        }
    }
    recurse(inputs, "");
    return result;
}
/**
 * Given a template YAML with `<+input>` placeholders and a flat key-value map,
 * substitute matching fields and return the filled YAML string.
 *
 * Matching strategy:
 * - Walk the YAML tree depth-first
 * - For any value that is `<+input>` (or starts with `<+input>.`), look for a user-provided
 *   value by the leaf field name (e.g. "branch", "env", "tag")
 * - Also try matching by the full dotted path from the YAML root (e.g. "stages.deploy.spec.branch")
 */
export function substituteInputs(templateYaml, userInputs) {
    const doc = YAML.parseDocument(templateYaml);
    const matched = [];
    const unmatchedRequired = [];
    const unmatchedOptional = [];
    const expectedKeys = [];
    const normalizedInputs = new Map();
    for (const [key, value] of Object.entries(userInputs)) {
        normalizedInputs.set(key.toLowerCase(), value);
    }
    /**
     * For Harness variable-style entries like:
     *   - name: "branch"
     *     value: "<+input>"
     * When the leaf key is "value" or "default", look at the sibling "name" field
     * to get the actual variable name for matching.
     */
    function getSiblingName(parentMap) {
        for (const pair of parentMap.items) {
            const key = YAML.isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
            if (key === "name" && YAML.isScalar(pair.value)) {
                return String(pair.value.value);
            }
        }
        return undefined;
    }
    function walk(node, path, parentMap, parentPair) {
        if (YAML.isMap(node)) {
            for (const pair of node.items) {
                const key = YAML.isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
                walk(pair.value, [...path, key], node, pair);
            }
        }
        else if (YAML.isSeq(node)) {
            for (let i = 0; i < node.items.length; i++) {
                walk(node.items[i], [...path, String(i)], undefined, undefined);
            }
        }
        else if (YAML.isScalar(node)) {
            const val = String(node.value);
            if (INPUT_PLACEHOLDER.test(val)) {
                const rawLeafKey = path[path.length - 1] ?? "";
                const leafKey = rawLeafKey.toLowerCase();
                const fullPath = path.join(".").toLowerCase();
                const isOptional = HAS_DEFAULT.test(val);
                let variableNameRaw;
                let variableNameLower;
                if ((leafKey === "value" || leafKey === "default") && parentMap) {
                    variableNameRaw = getSiblingName(parentMap);
                    variableNameLower = variableNameRaw?.toLowerCase();
                }
                const bestKey = variableNameRaw ?? rawLeafKey ?? path.join(".");
                expectedKeys.push(bestKey);
                let replacement = undefined;
                let matchedAs;
                if (variableNameLower && normalizedInputs.has(variableNameLower)) {
                    replacement = normalizedInputs.get(variableNameLower);
                    matchedAs = variableNameLower;
                }
                else if (normalizedInputs.has(leafKey)) {
                    replacement = normalizedInputs.get(leafKey);
                    matchedAs = leafKey;
                }
                else if (normalizedInputs.has(fullPath)) {
                    replacement = normalizedInputs.get(fullPath);
                    matchedAs = fullPath;
                }
                // Suffix matching: try progressively shorter path suffixes for dot-separated
                // flattened keys (e.g. "build.type" matches "...codebase.build.type")
                if (replacement === undefined) {
                    for (let i = 1; i < path.length - 1; i++) {
                        const suffix = path.slice(i).join(".").toLowerCase();
                        if (normalizedInputs.has(suffix)) {
                            replacement = normalizedInputs.get(suffix);
                            matchedAs = suffix;
                            break;
                        }
                    }
                }
                const displayName = variableNameRaw ?? rawLeafKey ?? path.join(".");
                if (replacement !== undefined) {
                    if (typeof replacement === "object" && replacement !== null && parentPair) {
                        // Replace scalar <+input> placeholder with a structured YAML node
                        parentPair.value = doc.createNode(replacement);
                    }
                    else {
                        node.value = replacement;
                    }
                    matched.push(matchedAs ?? displayName);
                }
                else if (isOptional) {
                    unmatchedOptional.push(displayName);
                }
                else {
                    unmatchedRequired.push(displayName);
                }
            }
        }
    }
    walk(doc.contents, [], undefined, undefined);
    return {
        yaml: doc.toString(),
        matched,
        unmatchedRequired,
        unmatchedOptional,
        expectedKeys,
    };
}
/**
 * High-level resolver: fetches the template and substitutes user inputs.
 * Returns the resolved YAML string ready for the pipeline execute API.
 *
 * If the pipeline has no runtime inputs, returns an empty string.
 * unmatchedRequired: placeholders that MUST be provided (will cause API 400).
 * unmatchedOptional: placeholders with .default() — the API fills them in.
 */
export async function resolveRuntimeInputs(client, flatInputs, options) {
    log.info(`Resolving runtime inputs for pipeline ${options.pipelineId}`);
    const templateYaml = await fetchRuntimeInputTemplate(client, options);
    if (!templateYaml) {
        log.info("Pipeline has no runtime inputs, ignoring user-provided inputs");
        return { yaml: "", matched: [], unmatchedRequired: [], unmatchedOptional: [], expectedKeys: [] };
    }
    log.debug("Template YAML fetched", { templateLength: templateYaml.length });
    const result = substituteInputs(templateYaml, flatInputs);
    if (result.matched.length > 0) {
        log.info(`Resolved ${result.matched.length} runtime inputs: ${result.matched.join(", ")}`);
    }
    if (result.unmatchedRequired.length > 0) {
        log.warn(`${result.unmatchedRequired.length} required placeholders unresolved: ${result.unmatchedRequired.join(", ")}`);
    }
    if (result.unmatchedOptional.length > 0) {
        log.debug(`${result.unmatchedOptional.length} optional placeholders (have defaults): ${result.unmatchedOptional.join(", ")}`);
    }
    return result;
}
//# sourceMappingURL=runtime-input-resolver.js.map