/**
 * Branded ID types — nominal typing for entity identifiers.
 *
 * All entity IDs use branded types to prevent accidental misuse.
 * A `TaskId` cannot be passed where a `RunId` is expected, even
 * though both are strings at runtime.
 *
 * Constructor functions validate format and return `Result<T, E>` —
 * parse-don't-validate at the boundary, then pass branded values
 * through the pipeline.
 *
 * The `Brand` utility and `Result` type are defined here as the
 * foundation. Existing branded types in the codebase (`ReportId`,
 * `ISOTimestamp`) use inline branding — those will be migrated to
 * use this utility in Phase 7.
 *
 * @see docs/design-docs/architecture-overhaul/domain-model.md (canonical)
 * @see docs/design-docs/parse-dont-validate.md (design principle)
 */
/** Construct a success result */
export function ok(value) {
    return { ok: true, value };
}
/** Construct a failure result */
export function err(error) {
    return { ok: false, error };
}
// ---------------------------------------------------------------------------
// Constructor functions — parse-don't-validate
// ---------------------------------------------------------------------------
/**
 * Parse a raw string into a `TaskId`.
 *
 * Valid format: alphanumeric + hyphens, 1–128 characters.
 * Examples: `"groq-projection-basics"`, `"mcp-server-tools-list"`
 */
export function taskId(raw) {
    if (!raw.match(/^[a-z0-9][a-z0-9-]{0,127}$/)) {
        return err({
            code: "INVALID_TASK_ID",
            raw,
            message: `Invalid TaskId "${raw}": must be 1–128 lowercase alphanumeric characters or hyphens, starting with a letter or digit`,
        });
    }
    return ok(raw);
}
/**
 * Canonical shape emitted by `generateRunId` (D0035).
 * `run_YYYYMMDDTHHMMSSZ_<8 lowercase base36>` — sortable + readable.
 */
const RUN_ID_RE = /^run_\d{8}T\d{6}Z_[0-9a-z]{8}$/;
/**
 * Legacy UUIDv7-hex shape. Accepted so existing runs in GCS and the
 * Content Lake remain valid; never emitted by `generateRunId`.
 */
const LEGACY_RUN_ID_RE = /^run_[0-9a-f]{32}$/;
/**
 * Parse a raw string into a `RunId`.
 *
 * Accepts the canonical `run_YYYYMMDDTHHMMSSZ_<8 base36>` shape (D0035)
 * and the legacy `run_<32 hex>` UUIDv7 shape for back-compat with runs
 * generated before the scheme change.
 */
export function runId(raw) {
    if (RUN_ID_RE.test(raw) || LEGACY_RUN_ID_RE.test(raw)) {
        return ok(raw);
    }
    return err({
        code: "INVALID_RUN_ID",
        raw,
        message: `Invalid RunId "${raw}": must match run_YYYYMMDDTHHMMSSZ_<8 base36> or legacy run_<32 hex>`,
    });
}
/**
 * Generate a new `RunId` with a human-readable UTC timestamp plus a
 * random suffix (D0035). Shape: `run_YYYYMMDDTHHMMSSZ_<8 base36>`.
 *
 * Lexicographic ordering matches creation time. One generator call per
 * pipeline invocation; every step reads the resulting id from
 * `AppContext.runId`.
 */
export function generateRunId() {
    const ts = new Date()
        .toISOString()
        .replace(/[-:]/g, "")
        .replace(/\.\d{3}Z$/, "Z");
    // Rejection-sample bytes against the largest multiple of 36 ≤ 256
    // (252) before applying `% 36`. Naive `b % 36` over [0, 255] biases
    // digits 0..3 (probability 8/256) over 4..35 (probability 7/256) by
    // ~14% per character. Drawing fresh bytes whenever the buffer runs
    // dry keeps the loop terminating with overwhelming probability
    // (each byte is kept with probability 252/256 ≈ 98.4%).
    const suffixChars = [];
    while (suffixChars.length < 8) {
        const buf = crypto.getRandomValues(new Uint8Array(8));
        for (const b of buf) {
            if (b >= 252)
                continue; // reject biased range
            suffixChars.push((b % 36).toString(36));
            if (suffixChars.length === 8)
                break;
        }
    }
    const suffix = suffixChars.join("");
    return `run_${ts}_${suffix}`;
}
/**
 * Parse a raw string into a `SuiteId`.
 *
 * Valid format: `suite_` prefix followed by alphanumeric characters.
 */
export function suiteId(raw) {
    if (!raw.match(/^suite_[a-zA-Z0-9]{4,}$/)) {
        return err({
            code: "INVALID_SUITE_ID",
            raw,
            message: `Invalid SuiteId "${raw}": must match suite_[a-zA-Z0-9]{4,}`,
        });
    }
    return ok(raw);
}
/**
 * Parse a raw string into a `ResultId`.
 *
 * Valid format: `res_` prefix followed by alphanumeric characters.
 */
export function resultId(raw) {
    if (!raw.match(/^res_[a-zA-Z0-9]{8,}$/)) {
        return err({
            code: "INVALID_RESULT_ID",
            raw,
            message: `Invalid ResultId "${raw}": must match res_[a-zA-Z0-9]{8,}`,
        });
    }
    return ok(raw);
}
/**
 * Parse a raw string into a `TraceId`.
 *
 * Valid format: `trace_` prefix followed by alphanumeric characters.
 */
export function traceId(raw) {
    if (!raw.match(/^trace_[a-zA-Z0-9]{8,}$/)) {
        return err({
            code: "INVALID_TRACE_ID",
            raw,
            message: `Invalid TraceId "${raw}": must match trace_[a-zA-Z0-9]{8,}`,
        });
    }
    return ok(raw);
}
/**
 * Parse a raw string into a `ProviderId`.
 *
 * Valid format: colon-separated segments (e.g., `"openai:chat:gpt-4o"`).
 */
export function providerId(raw) {
    if (!raw.match(/^[a-zA-Z0-9][a-zA-Z0-9:._-]{0,127}$/)) {
        return err({
            code: "INVALID_PROVIDER_ID",
            raw,
            message: `Invalid ProviderId "${raw}": must be 1–128 alphanumeric characters, colons, dots, underscores, or hyphens`,
        });
    }
    return ok(raw);
}
/**
 * Parse a raw string into a `FixtureId`.
 *
 * Valid format: alphanumeric + hyphens, 1–128 characters.
 */
export function fixtureId(raw) {
    if (!raw.match(/^[a-z0-9][a-z0-9-]{0,127}$/)) {
        return err({
            code: "INVALID_FIXTURE_ID",
            raw,
            message: `Invalid FixtureId "${raw}": must be 1–128 lowercase alphanumeric characters or hyphens`,
        });
    }
    return ok(raw);
}
/**
 * Canonical shape for a `JudgmentId`.
 *
 * Two accepted forms:
 *  - `judgment_<runId-suffix>_<sanitized-task>__<sanitized-model>__<dimension>`
 *    — minted by `generateJudgmentId` for synthesized fall-back judgments
 *    so dedup is per-run (a re-run of the same task produces a distinct id).
 *  - `j_<alphanumeric>` — short form used by test fixtures and any caller
 *    that wants a stable, opaque id without the structured composite.
 *
 * Inner segments may carry alphanumerics, hyphens, dots, and colons (the
 * provider id surface is colon-separated). The full string is bounded to
 * 256 characters to keep the id index-friendly downstream.
 */
const JUDGMENT_ID_RE = /^(?:judgment_[0-9a-z]{1,16}_[a-z0-9][a-z0-9.:_-]*__[a-z0-9][a-z0-9.:_-]*__[a-z0-9][a-z0-9-]*|j_[A-Za-z0-9_-]{4,})$/;
/**
 * Parse a raw string into a `JudgmentId`.
 *
 * See `JUDGMENT_ID_RE` for the accepted formats.
 */
export function judgmentId(raw) {
    if (raw.length === 0 || raw.length > 256 || !JUDGMENT_ID_RE.test(raw)) {
        return err({
            code: "INVALID_JUDGMENT_ID",
            raw,
            message: `Invalid JudgmentId "${raw}": must match judgment_<runSalt>_<task>__<model>__<dimension> or j_<alnum>`,
        });
    }
    return ok(raw);
}
/** Strip a value down to the alphanumeric+hyphen alphabet the id format allows. */
function sanitizeJudgmentSegment(value) {
    // Lowercase + replace runs of non-alphanumerics with a single hyphen,
    // trim leading/trailing hyphens. Keeps dots and colons in `modelId`-like
    // values (`openai:gpt-5.2`) since the regex permits them.
    return value
        .toLowerCase()
        .replace(/[^a-z0-9.:_-]+/g, "-")
        .replace(/^-+|-+$/g, "");
}
/**
 * Generate a deterministic `JudgmentId` for a synthesized fall-back
 * judgment. Salting with `runId` (when supplied) makes the id unique
 * per-run so consumers' `(taskId, modelId, dimension)` dedup key
 * doesn't collide across re-runs of the same task — every run writes
 * fresh ids that still encode the natural composite key.
 *
 * When `runId` is absent the salt collapses to `nosalt`, preserving the
 * legacy "deterministic across runs" shape for callers that explicitly
 * want it (e.g. unit tests that assert the exact id string).
 */
export function generateJudgmentId(input) {
    // Take the trailing 8 chars of the runId (the random base36 suffix on
    // the canonical shape) so the salt stays compact while still cycling
    // every run. Falls back to a constant marker when runId isn't passed.
    const runSalt = input.runId
        ? sanitizeJudgmentSegment(String(input.runId).slice(-8)) || "nosalt"
        : "nosalt";
    const task = sanitizeJudgmentSegment(input.taskId);
    const model = sanitizeJudgmentSegment(input.modelId);
    const dimension = sanitizeJudgmentSegment(input.dimension);
    return `judgment_${runSalt}_${task}__${model}__${dimension}`;
}
