/**
 * Group-by dimensions + time-to-first-tool latency for the traces insight
 * engine (the console's Issue bar).
 *
 * Pure functions, no I/O. Classifiers are DATA-DRIVEN TABLES — a new task type
 * or timing bucket is a row, not a new if/else-by-name arm. The integrator
 * (`insights.ts` / `sync.ts`) tags each session; this file does not write the
 * shard.
 *
 * Input is the redacted `SessionDetail` from `agents traces sync` plus the
 * SyncRow fields the detail strips (prompt, gitBranch, files). Structural —
 * `SessionDetail` is a valid `SegmentSession`.
 */
// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------
export const TASK_TYPES = ['bugfix', 'feature', 'refactor', 'test', 'chore', 'other'];
export const FAILURE_TIMINGS = ['early', 'mid', 'late'];
// ---------------------------------------------------------------------------
// Tables — the classifiers. Add a row; do not add an if/else-by-name arm.
// ---------------------------------------------------------------------------
/**
 * Tool name → file action. Lowercased. Write-family is a new file; Edit-family
 * is a patch. Anything else is ignored for diff-shape.
 */
export const FILE_ACTION_TOOLS = [
    { action: 'new', tools: ['write', 'write_file', 'create_file', 'create'] },
    { action: 'edit', tools: ['edit', 'strreplace', 'apply_patch', 'applypatch', 'notebookedit'] },
];
/** Path heuristics for a test-only diff. */
const TEST_PATH_PATTERN = /(?:^|\/)(?:__tests__|testdata|test)\/|\.(?:test|spec)\.[cm]?[jt]sx?$|_test\.(?:go|py|rs)$/i;
/** Path heuristics for a chore-only diff (lockfiles, CI, docs). */
const CHORE_PATH_PATTERN = /(?:^|\/)(?:package(?:-lock)?\.json|bun\.lockb?|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|go\.sum|Dockerfile|Makefile|\.github\/|CHANGELOG[^/]*|README[^/]*|\.gitignore)$/i;
const DIFF_SHAPE_MATCHERS = [
    {
        key: 'testOnly',
        match: (files) => files.length > 0 && files.every((f) => TEST_PATH_PATTERN.test(f.path)),
    },
    {
        key: 'newOnly',
        match: (files) => files.length > 0 && files.every((f) => f.action === 'new'),
    },
    {
        key: 'choreOnly',
        match: (files) => files.length > 0 && files.every((f) => CHORE_PATH_PATTERN.test(f.path)),
    },
];
/**
 * Task-type rules, first match wins. Prompt/branch keywords outrank diff shape
 * except where `shape` is set (test-only files, chore-only files, all-new files).
 *
 * Order is the product priority: test-only work is not a "feature"; a `fix/`
 * branch is a bugfix even if files are new; chore lockfile edits are not features.
 */
export const TASK_TYPE_RULES = [
    {
        type: 'test',
        patterns: [
            /\bunit tests?\b/,
            /\bintegration tests?\b/,
            /\btestdata\b/,
            /\bcoverage\b/,
            /\b(?:add|write|update|fix)\s+tests?\b/,
            /\btest[- ]only\b/,
            /(?:^|\/)tests?\//,
            /^test\//,
            /\.(?:test|spec)\./,
        ],
        shape: 'testOnly',
    },
    {
        type: 'bugfix',
        patterns: [
            /\bbugfix(?:es)?\b/,
            /\bhot[- ]?fix(?:es)?\b/,
            /\bfixes\b/,
            /\bfixing\b/,
            /\bfix\b/,
            /\bbugs?\b/,
            /\bregression\b/,
            /\bcrash(?:es|ed|ing)?\b/,
            /\bbroken\b/,
            /(?:^|\/)fix(?:es)?\//,
            /^bugfix\//,
        ],
    },
    {
        type: 'refactor',
        patterns: [
            /\brefactor(?:s|ing|ed)?\b/,
            /\bclean[- ]?ups?\b/,
            /\bextract(?:s|ing|ed)?\b/,
            /\brename(?:s|d|ing)?\b/,
            /(?:^|\/)refactor\//,
        ],
    },
    {
        type: 'chore',
        patterns: [
            /\bchore\b/,
            /\bdependenc(?:y|ies)\b/,
            /\bbump\b/,
            /\blint(?:ing|er)?\b/,
            /\bformat(?:ting)?\b/,
            /(?:^|\/)chore\//,
            /\b(?:ci|cd)[-_/]/,
        ],
        shape: 'choreOnly',
    },
    {
        type: 'feature',
        patterns: [
            /\bfeat(?:ure)?s?\b/,
            /\bimplement(?:s|ing|ed)?\b/,
            /\bnew feature\b/,
            /(?:^|\/)feat(?:ure)?\//,
        ],
        shape: 'newOnly',
    },
];
/**
 * Normalized position of the first failing step inside the session span.
 * Early failures cascade — highest-signal bucket. Exclusive upper bound.
 */
export const FAILURE_TIMING_BUCKETS = [
    { timing: 'early', maxExclusive: 1 / 3 },
    { timing: 'mid', maxExclusive: 2 / 3 },
    { timing: 'late', maxExclusive: Number.POSITIVE_INFINITY },
];
const AGENT_FIELDS = {
    model: [
        (s) => s.meta?.model,
        (s) => s.model,
    ],
    harness: [
        (s) => s.meta?.agent,
        (s) => s.harness,
        (s) => s.agent,
    ],
};
// ---------------------------------------------------------------------------
// Public functions
// ---------------------------------------------------------------------------
/** Model × harness unit — the #1 group-by axis. */
export function deriveAgent(session) {
    return {
        model: firstPresent(AGENT_FIELDS.model.map((read) => read(session))),
        harness: firstPresent(AGENT_FIELDS.harness.map((read) => read(session))),
    };
}
/**
 * Classify the session's work from the opening prompt + diff shape (files
 * touched, new vs edit, test-only). First matching table row wins.
 */
export function classifyTaskType(session) {
    const haystack = taskHaystack(session);
    const shape = inferDiffShape(session);
    for (const rule of TASK_TYPE_RULES) {
        if (rule.patterns.some((pattern) => pattern.test(haystack)))
            return rule.type;
        if (rule.shape && shape[rule.shape])
            return rule.type;
    }
    return 'other';
}
/**
 * Bucket the FIRST failing step by its normalized position in the session
 * span (`firstFailure.startMs / spanMs`). Sessions with no failing step
 * return null — they do not belong on the failure-timing axis.
 */
export function failureTiming(session) {
    const firstFailure = (session.steps ?? []).find((step) => step.outcome === 'error');
    if (!firstFailure)
        return null;
    const span = sessionSpanMs(session);
    const position = span > 0 ? firstFailure.startMs / span : 0;
    return FAILURE_TIMING_BUCKETS.find((bucket) => position < bucket.maxExclusive)?.timing ?? null;
}
/**
 * Time-to-first-tool latency from `steps[0].startMs` across sessions that
 * recorded at least one step. Nearest-rank percentiles (same formula as the
 * traces index stats) so p99 on `/tmp/traces-real` lands at ~128s.
 */
export function computeLatency(sessions) {
    const samples = [];
    for (const session of sessions) {
        const steps = session.steps ?? [];
        if (steps.length === 0)
            continue;
        const startMs = steps[0]?.startMs;
        if (typeof startMs !== 'number' || !Number.isFinite(startMs))
            continue;
        samples.push(startMs);
    }
    return {
        firstToolMs: {
            p50: percentileNearest(samples, 0.5),
            p90: percentileNearest(samples, 0.9),
            p99: percentileNearest(samples, 0.99),
            max: samples.length === 0 ? 0 : Math.max(...samples),
        },
    };
}
/** All three group-by dimensions for one session. */
export function deriveDimensions(session) {
    return {
        agent: deriveAgent(session),
        taskType: classifyTaskType(session),
        failureTiming: failureTiming(session),
    };
}
// ---------------------------------------------------------------------------
// Internals
// ---------------------------------------------------------------------------
function firstPresent(values) {
    for (const value of values) {
        if (typeof value === 'string' && value.length > 0)
            return value;
    }
    return 'unknown';
}
function taskHaystack(session) {
    const filePaths = inferFiles(session).map((file) => file.path);
    const writeLabels = (session.steps ?? [])
        .filter((step) => fileActionForTool(step.tool) !== undefined)
        .map((step) => step.label ?? '');
    return [
        session.prompt,
        session.gitBranch,
        session.topic,
        session.label,
        session.meta?.repo,
        ...filePaths,
        ...writeLabels,
    ]
        .filter((value) => typeof value === 'string' && value.length > 0)
        .join('\n')
        .toLowerCase();
}
function inferFiles(session) {
    if (session.files && session.files.length > 0)
        return session.files;
    const files = [];
    for (const step of session.steps ?? []) {
        const action = fileActionForTool(step.tool);
        if (!action)
            continue;
        const path = filePathFromLabel(step.label);
        if (!path)
            continue;
        files.push({ path, action });
    }
    return files;
}
function fileActionForTool(tool) {
    if (!tool)
        return undefined;
    const name = tool.toLowerCase();
    for (const row of FILE_ACTION_TOOLS) {
        if (row.tools.includes(name))
            return row.action;
    }
    return undefined;
}
/** Best-effort path from a redacted Write/Edit label (`path/to/file.ts`). */
function filePathFromLabel(label) {
    if (!label)
        return undefined;
    const match = label.match(/(?:^|[\s`'"])(\/?[\w.@-]+(?:\/[\w.@-]+)+\.[A-Za-z][\w.]*)/);
    return match?.[1];
}
function inferDiffShape(session) {
    const files = inferFiles(session);
    const shape = { files, testOnly: false, newOnly: false, choreOnly: false };
    for (const matcher of DIFF_SHAPE_MATCHERS) {
        shape[matcher.key] = matcher.match(files);
    }
    return shape;
}
function sessionSpanMs(session) {
    const declared = session.meta?.spanMs ?? session.spanMs ?? 0;
    if (declared > 0)
        return declared;
    let max = 0;
    for (const step of session.steps ?? []) {
        max = Math.max(max, step.startMs + (step.durationMs ?? 0));
    }
    return max;
}
/**
 * Nearest-rank percentile, ratio in (0, 1]. Same index rule as
 * `sync.ts`'s private `percentile` — linear interpolation (`lib/percentile.ts`)
 * would report ~71s for p99 on the 738-session corpus; the published figure is
 * 128s / 2m8s.
 */
function percentileNearest(values, ratio) {
    if (values.length === 0)
        return 0;
    const sorted = [...values].sort((a, b) => a - b);
    return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)];
}
