import { compareCodeUnits, formatBytes } from '../../utils.js';
import { renderEntityContextText } from './context.js';
import { renderInlineSourceBlock } from '../source-inline.js';
const NON_AUTHORITATIVE_ENTITY_COSMETIC = 'n/a (entity-record compatibility field; not whole-diff proof)';
/**
 * Compact text renderings.
 *
 * These are the fallback representation for clients that consume text rather
 * than structured content, so they stay terse and stable: one entity per line,
 * facts only, no colour and no box drawing.
 */
export function formatEntityRef(ref) {
    const location = `${ref.relativePath}:${ref.startLine}`;
    // The qualified name is printed as-is rather than rebuilt from container and
    // name: a JSON pointer is `/scripts/dev`, and joining with a dot would render
    // it `/scripts.dev`, which is not a path into anything.
    const marks = [ref.isExported ? 'exported' : '', ref.isTest ? 'test' : '', ref.provisional ? 'uncommitted' : '']
        .filter(Boolean)
        .join(', ');
    return `${ref.kind} ${ref.qualifiedName ?? ref.name}  ${location}${marks ? `  (${marks})` : ''}`;
}
export function formatEntityList(refs) {
    if (refs.length === 0)
        return 'No matching entities.';
    return refs.map(formatEntityRef).join('\n');
}
function formatDiagnostics(diagnostics) {
    if (diagnostics.length === 0)
        return '';
    return `\n\nDiagnostics:\n${diagnostics.map(diagnostic => `  [${diagnostic.code}] ${diagnostic.message}`).join('\n')}`;
}
function formatEvidenceGaps(summary) {
    if (summary.count === 0)
        return '';
    const lines = ['', `Evidence gaps (${summary.count}):`];
    for (const reason of summary.reasons)
        lines.push(`  [${reason.code}] ${reason.message}`);
    if (summary.count > summary.reasons.length) {
        lines.push(`  ... and ${summary.count - summary.reasons.length} more`);
    }
    return `\n${lines.join('\n')}`;
}
function diagnosticIdentity(diagnostic) {
    return JSON.stringify([
        diagnostic.code,
        diagnostic.message,
        diagnostic.commit ?? null,
        diagnostic.relativePath ?? null,
    ]);
}
function formatMiss(result) {
    if (result.status === 'error')
        return `Error (${result.code}): ${result.message}`;
    const listing = result.candidates.length > 0 ? `\n\nCandidates:\n${formatEntityList(result.candidates)}` : '';
    return `${result.message}${listing}`;
}
function formatWholeDiffEvidence(result) {
    const lines = [];
    const scope = result.gitScope;
    lines.push(scope
        ? `Git scope: comparison=${scope.comparison} layers=${scope.includedLayers.join(',') || '(none)'} untracked=${scope.untracked}`
        : 'Git scope: unavailable');
    const gaps = result.evidenceGaps.unclassifiedFiles;
    const basisCounts = new Map();
    for (const gap of gaps)
        basisCounts.set(gap.basis, (basisCounts.get(gap.basis) ?? 0) + 1);
    const bases = [...basisCounts.entries()].sort((a, b) => b[1] - a[1] || compareCodeUnits(a[0], b[0]));
    const shownBases = bases.slice(0, 5).map(([basis, count]) => `${basis}=${count}`);
    const omittedBases = Math.max(0, bases.length - shownBases.length);
    lines.push(`Unknown bases: ${shownBases.length > 0 ? shownBases.join(', ') : 'none'}${omittedBases > 0 ? `, and ${omittedBases} more` : ''}`);
    if (gaps.length > 0) {
        lines.push(`Unclassified files (${gaps.length}):`);
        for (const gap of gaps.slice(0, 5))
            lines.push(`  ${gap.relativePath} [${gap.basis}]`);
        if (gaps.length > 5)
            lines.push(`  ... and ${gaps.length - 5} more`);
    }
    return lines.join('\n');
}
export function formatEntityDiff(result) {
    const entityRecords = `Entity records: verdict=${result.semantic.verdict} coverage=${result.semantic.coverage} ` +
        `logic=${result.semantic.counts.logic} cosmetic=${NON_AUTHORITATIVE_ENTITY_COSMETIC} ` +
        `unknown=${result.semantic.counts.unknown}`;
    const wholeDiff = result.wholeDiffSemantic
        ? `Whole diff: proof=${result.wholeDiffSemantic.proof} verdict=${result.wholeDiffSemantic.verdict} ` +
            `coverage=${result.wholeDiffSemantic.coverage} logic=${result.wholeDiffSemantic.counts.logic} ` +
            `cosmetic=${result.wholeDiffSemantic.counts.cosmetic ?? 'n/a (legacy receipt)'} ` +
            `whitespace-only=${result.wholeDiffSemantic.counts.whitespaceOnly ?? 'n/a'} ` +
            `comment-only=${result.wholeDiffSemantic.counts.commentOnly ?? 'n/a'} ` +
            `no-op=${result.wholeDiffSemantic.counts.noOp} unknown=${result.wholeDiffSemantic.counts.unknown} ` +
            `manifest=${result.wholeDiffSemantic.grammarManifestDigest ?? '-'} ` +
            `trivia-policy=${result.wholeDiffSemantic.triviaPolicyDigest ?? '-'}`
        : 'Whole diff: unavailable (authoritative whole-diff proof was not emitted)';
    const summaries = `${entityRecords}\n${wholeDiff}`;
    const evidence = result.wholeDiffSemantic ? `\n${formatWholeDiffEvidence(result.wholeDiffSemantic)}` : '';
    if (result.status === 'error') {
        return `Error (${result.code ?? 'unknown'}): ${result.message ?? ''}\n${summaries}${evidence}`;
    }
    const header = result.range
        ? `${result.range.kind}: ${result.range.base || '(none)'} -> ${result.range.head || '(worktree)'}`
        : 'working tree';
    const obligations = result.obligations
        ? [
            '',
            `Obligations: coverage=${result.obligations.coverage ?? 'unreported'}`,
            `  logic=${result.obligations.split.logic}`,
            `  skipped changes: ${result.obligations.evidenceGaps?.count ?? 'unreported'}`,
            `  untested logic gaps: ${result.obligations.gaps.length}`,
            ...result.obligations.gaps.map(gap => `    ${gap.qualifiedName} (${gap.entityId}) nearest-tested-ancestor=${gap.nearestTestedAncestor ?? '-'}`),
            `  seam crossings: ${result.obligations.seamCrossings.length}`,
            ...result.obligations.seamCrossings.map(seamKey => `    ${seamKey}`),
        ].join('\n')
        : '';
    if (result.changes.length === 0) {
        return `${header}\n${summaries}${evidence}\n${result.changedFiles.length} files changed, no entity changes.${obligations}${formatDiagnostics(result.diagnostics)}`;
    }
    const lines = result.changes.map(change => {
        const ref = change.current ?? change.previous;
        const where = ref ? `${ref.relativePath}:${ref.startLine}` : '';
        const name = change.current?.qualifiedName ?? change.previous?.qualifiedName ?? '?';
        const moved = change.previous && change.current && change.previous.relativePath !== change.current.relativePath
            ? `  (from ${change.previous.relativePath})`
            : '';
        const renamed = change.previous && change.current && change.previous.qualifiedName !== change.current.qualifiedName
            ? `  (was ${change.previous.qualifiedName})`
            : '';
        const structural = change.structuralChange === null ? 'unknown' : String(change.structuralChange);
        return `${change.changeType.padEnd(18)} ${name}  ${where}  structural=${structural}${moved}${renamed}`;
    });
    return `${header}\n${summaries}${evidence}\n${result.changedFiles.length} files, ${result.changes.length} entity changes\n\n${lines.join('\n')}${obligations}${formatDiagnostics(result.diagnostics)}`;
}
export function formatEntityImpact(result) {
    if (result.status !== 'resolved')
        return formatMiss(result);
    const shellArg = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
    const section = (title, entries) => entries.length === 0
        ? ''
        : `\n${title} (${entries.length}):\n${entries
            .map(entry => {
            const line = `  d${entry.distance} ${formatEntityRef(entry.entity)}  via ${entry.via.join('+')}`;
            const command = entry.selector?.command;
            const commandText = command
                ? `${line}\n    cwd: ${shellArg(command.cwd)}\n    run: ${[command.executable, ...command.args]
                    .map(shellArg)
                    .join(' ')}`
                : line;
            return entry.inlineContext
                ? `${commandText}\n${renderInlineSourceBlock(entry.inlineContext, '    ')}`
                : commandText;
        })
            .join('\n')}`;
    const head = `${formatEntityRef(result.entity)}\nscope=${result.scope} coverage=${result.coverage} ` +
        `truncated=${result.truncated} unreachableEdges=${result.unreachableEdges}`;
    const rootInline = result.inlineContext ? `\n${renderInlineSourceBlock(result.inlineContext)}` : '';
    return `${head}${rootInline}${section('Dependencies', result.dependencies)}${section('Dependents', result.dependents)}${section('Transitive', result.transitive)}${section('Affected tests', result.tests)}${formatDiagnostics(result.diagnostics)}`;
}
export function formatEntityBlame(result) {
    if (result.status === 'error')
        return `Error (${result.code}): ${result.message}`;
    if (result.status === 'not_found')
        return result.message;
    if (result.entries.length === 0)
        return `No entities in ${result.relativePath}.`;
    const lines = result.entries.map(entry => {
        const who = entry.author ? `${entry.author}` : '-';
        const when = entry.date ? entry.date.slice(0, 10) : '-';
        const sha = entry.commit ? entry.commit.slice(0, 8) : '-'.repeat(8);
        const what = entry.changeType ?? '-';
        return `${sha} ${when} ${who.padEnd(18)} ${entry.state.padEnd(22)} ${what.padEnd(18)} ${entry.entity.qualifiedName ?? entry.entity.name}`;
    });
    return `${result.relativePath}\n\n${lines.join('\n')}${formatDiagnostics(result.diagnostics)}`;
}
export function formatEntityHistory(result) {
    if (result.status !== 'resolved')
        return formatMiss(result);
    if (result.entries.length === 0) {
        return `${formatEntityRef(result.entity)}\n\nNo recorded history.${formatDiagnostics(result.diagnostics)}`;
    }
    const lines = result.entries.map(entry => {
        const structural = entry.structuralChange === null ? 'unknown' : String(entry.structuralChange);
        const renamed = entry.previousName && entry.previousName !== entry.currentName ? `  (was ${entry.previousName})` : '';
        const moved = entry.previousPath && entry.previousPath !== entry.currentPath ? `  (from ${entry.previousPath})` : '';
        return `${entry.commit.slice(0, 8)} ${entry.date.slice(0, 10)} ${entry.changeType.padEnd(18)} structural=${structural.padEnd(7)} ${entry.subject}${renamed}${moved}`;
    });
    return `${formatEntityRef(result.entity)}\n\n${lines.join('\n')}${formatDiagnostics(result.diagnostics)}`;
}
export function formatEntityHotspots(result) {
    if (result.status === 'error')
        return `Error (${result.code}): ${result.message}`;
    const coverage = result.honesty.coverage;
    const header = `${result.rows.length} hotspot(s); ${coverage.commitsConsidered}/${coverage.commitsIndexed} indexed commits considered; ` +
        `truncated=${result.honesty.truncation.truncated} omitted=${result.honesty.truncation.omitted}; ` +
        `ambiguities=${result.honesty.ambiguityCount} evidence-gaps=${result.honesty.evidenceGaps.count}`;
    const evidence = formatEvidenceGaps(result.honesty.evidenceGaps);
    if (result.rows.length === 0) {
        return `${header}\nNo logic-changing entity events in the selected window.${evidence}`;
    }
    const lines = result.rows.map(row => {
        const graph = row.pageRank === undefined ? '' : ` pagerank=${row.pageRank.toFixed(2)}`;
        const boundary = row.birthAtWindowBoundary ? ' boundary-birth' : '';
        return (`${row.score.toFixed(2).padStart(10)}  logic=${String(row.logicChangeCount).padStart(3)} ` +
            `cosmetic=${NON_AUTHORITATIVE_ENTITY_COSMETIC} ` +
            `unknown=${String(row.unknownChangeCount).padStart(3)} age=${String(row.ageDays).padStart(4)}d ` +
            `${row.entity.qualifiedName ?? row.entity.name}  ${row.entity.relativePath}:${row.entity.startLine}` +
            `${graph}${boundary}`);
    });
    return `${header}\n\n${lines.join('\n')}${evidence}`;
}
export function formatEntityCochange(result) {
    if (result.status !== 'resolved')
        return formatMiss(result);
    const target = result.entity ? ` for ${result.entity.qualifiedName ?? result.entity.name}` : '';
    const header = `${result.pairs.length} co-change pair(s)${target}; ${result.honesty.coverage.commitsConsidered}/` +
        `${result.windowCommits} indexed commits contributed; skipped=${result.honesty.truncation.commitsSkipped} ` +
        `omitted=${result.honesty.truncation.omitted}; ambiguities=${result.honesty.ambiguityCount} ` +
        `evidence-gaps=${result.honesty.evidenceGaps.count}`;
    const evidence = formatEvidenceGaps(result.honesty.evidenceGaps);
    if (result.pairs.length === 0) {
        return `${header}\nNo pairs met min-count=${result.minCount} and min-confidence=${result.minConfidence}.${evidence}`;
    }
    const lines = result.pairs.map(pair => `${pair.confidence.toFixed(3)}  ${String(pair.cochangeCount).padStart(3)}/${String(pair.aChangeCount).padEnd(3)} ` +
        `${pair.a.qualifiedName ?? pair.a.name}  ↔  ${pair.b.qualifiedName ?? pair.b.name}  ` +
        `${pair.a.relativePath}:${pair.a.startLine}  ${pair.b.relativePath}:${pair.b.startLine}  ${pair.lastCoChange}`);
    return `${header}\n\n${lines.join('\n')}${evidence}`;
}
export function formatEntityDrift(result) {
    if (result.status === 'error')
        return `Error (${result.code}): ${result.message}`;
    const coverage = result.honesty.coverage;
    const header = `${result.rows.length} drift row(s); links resolved=${coverage.linksResolved} ` +
        `refused=${coverage.linksRefused} (ambiguous=${coverage.linksAmbiguous}, not-found=${coverage.linksNotFound}); ` +
        `docs=${coverage.documentsConsidered} all-links-refused=${coverage.documentsAllLinksRefused}; ` +
        `history-unavailable=${coverage.linksWithoutDocHistory} truncated=${result.honesty.truncation.truncated} ` +
        `evidence-gaps=${result.honesty.evidenceGaps.count}`;
    const refusedDocs = coverage.allRefusedDocuments.length === 0
        ? ''
        : `\nDocuments whose every candidate was refused (${coverage.documentsAllLinksRefused}):\n${coverage.allRefusedDocuments
            .map(docPath => `  ${docPath}`)
            .join('\n')}${coverage.allRefusedDocumentsOmitted > 0 ? `\n  ... and ${coverage.allRefusedDocumentsOmitted} more` : ''}`;
    const evidence = formatEvidenceGaps(result.honesty.evidenceGaps);
    const diagnosticText = result.diagnostics.length === 0
        ? ''
        : `\n\nLink diagnostics (${result.diagnostics.length + coverage.diagnosticsOmitted}):\n${result.diagnostics
            .map(diagnostic => `  [${diagnostic.code}] ${diagnostic.docPath}:${diagnostic.line} ${diagnostic.message}`)
            .join('\n')}${coverage.diagnosticsOmitted > 0 ? `\n  ... and ${coverage.diagnosticsOmitted} more` : ''}`;
    if (result.rows.length === 0) {
        const reason = coverage.linksExtracted === 0
            ? 'No reference candidates were found in the selected Markdown prose.'
            : coverage.linksResolved === 0
                ? 'No references resolved uniquely; no absence claim is made.'
                : 'References resolved, but no document section had comparable committed history.';
        return `${header}\n${reason}${refusedDocs}${diagnosticText}${evidence}`;
    }
    const rows = result.rows.map(row => `${String(row.changesSince.count).padStart(3)} ${row.changesSince.strongestEvidence.padEnd(11)} ` +
        `${row.docPath}:${row.docSpan.startLine}-${row.docSpan.endLine} -> ` +
        `${row.entityRef.relativePath}:${row.entityRef.startLine}#${row.entityRef.qualifiedName ?? row.entityRef.name} ` +
        `(doc ${row.docLastTouch.commit.slice(0, 8)})`);
    return `${header}\n\n${rows.join('\n')}${refusedDocs}${diagnosticText}${evidence}`;
}
/**
 * What the index covers, and whether it still describes the repository.
 *
 * Leads with truncation and staleness because those are the two facts that
 * change how every other answer should be read: an empty result from a starved
 * or stale index is not a fact about the repository.
 */
export function formatEntityStatus(status, freshness, bounds, cache) {
    const head = status.head ? status.head.slice(0, 12) : '(none)';
    const lines = [
        `index      ${status.mode}, head ${head}`,
        `contracts  code-index ${status.codeIndexSchemaVersion === undefined ? '(legacy)' : `v${status.codeIndexSchemaVersion}`}, ` +
            `parser ${status.parserContract ? `${status.parserContract.parserName}@${status.parserContract.parserVersion}` : '(unknown)'}`,
        `covers     ${status.commitsIndexed} commit(s), ${status.lineageCount} lineage(s), ${status.eventsRecorded} event(s)`,
        `truncated  ${status.truncated}`,
        `evidence   ${status.evidenceComplete ? 'complete' : 'incomplete'}`,
        `fresh      ${freshness.fresh}${freshness.reasons.length > 0 ? ` (${freshness.reasons.join(', ')})` : ''}`,
    ];
    if (status.parserEvidence) {
        const evidence = status.parserEvidence;
        lines.push(`parsers    ${evidence.totalFiles} file(s), ${evidence.checkerFiles} checker, ${evidence.syntaxFiles} syntax, ` +
            `${evidence.recoveredFiles} recovered, ${evidence.fallbackFiles} fallback, ${evidence.degradedFiles} degraded`);
    }
    if (!freshness.fresh && freshness.head && freshness.head !== freshness.indexedHead) {
        lines.push(`repo head  ${freshness.head.slice(0, 12)}`);
    }
    if (freshness.workingTree) {
        const drift = freshness.workingTree;
        lines.push(`worktree   +${drift.added} ~${drift.modified} -${drift.deleted}`);
    }
    else {
        lines.push('worktree   unmeasured');
    }
    lines.push(`bounds     ${bounds.maxCommits} commits, ${bounds.maxEntitiesTotal} entities, ${bounds.maxTotalBlobBytes} blob bytes`);
    if (cache) {
        lines.push(`cache      ${cache.entryCount} entries, ${formatBytes(cache.bytes)} at ${cache.root}`);
        for (const root of cache.supersededRoots) {
            lines.push(`superseded ${formatBytes(root.bytes)} at ${root.path}`);
        }
    }
    // Reasons in full — there are a handful and each one is actionable. The rest
    // are summarized by code, because a real repository produces thousands of
    // per-file skips and a text rendering that prints them all is not a rendering.
    // The complete array stays in the structured payload either way.
    if (status.reasons.length > 0) {
        lines.push('', 'Truncated because:');
        for (const reason of status.reasons)
            lines.push(`  [${reason.code}] ${reason.message}`);
    }
    const reasonIdentities = new Set(status.reasons.map(diagnosticIdentity));
    const rest = status.diagnostics.filter(diagnostic => !reasonIdentities.has(diagnosticIdentity(diagnostic)));
    if (rest.length > 0) {
        const counts = new Map();
        for (const diagnostic of rest)
            counts.set(diagnostic.code, (counts.get(diagnostic.code) ?? 0) + 1);
        const summary = [...counts.entries()]
            .sort((a, b) => b[1] - a[1] || compareCodeUnits(a[0], b[0]))
            .map(([code, count]) => `${code} ×${count}`)
            .join(', ');
        lines.push('', `Other diagnostics (${rest.length}): ${summary}`);
    }
    return lines.join('\n');
}
export function createEntityCacheStatus(meta, freshness, cacheDir, maxCommits, cache) {
    return {
        mode: 'metadata',
        cachePresent: meta !== null,
        cacheDir: cacheDir ?? null,
        fresh: freshness.fresh,
        ...(cache ? { cache } : {}),
        ...(freshness.head ? { head: freshness.head } : {}),
        ...(freshness.indexedHead || meta?.indexedHead ? { indexedHead: freshness.indexedHead ?? meta?.indexedHead } : {}),
        ...(meta
            ? {
                commitCount: meta.commitCount,
                lineageCount: meta.lineageCount,
                eventCount: meta.eventCount,
                truncated: meta.truncated,
                builtAt: meta.builtAt,
            }
            : { coldBuildMaxCommits: maxCommits }),
    };
}
export function formatEntityCacheStatus(status) {
    const lines = [
        `cache      ${status.cachePresent ? 'present' : 'missing'} at ${status.cacheDir ?? '(disabled)'}`,
        `head       ${status.head?.slice(0, 12) ?? '(none)'}`,
        `indexed    ${status.indexedHead?.slice(0, 12) ?? '(none)'}`,
        `fresh      ${status.fresh}`,
        `covers     ${status.commitCount ?? 0} commit(s), ${status.lineageCount ?? 0} lineage(s), ${status.eventCount ?? 0} event(s)`,
        `truncated  ${status.truncated ?? 'unknown'}`,
        `built      ${status.builtAt ?? '(never)'}`,
    ];
    if (status.cache) {
        lines.push(`size       ${status.cache.entryCount} entries, ${formatBytes(status.cache.bytes)}`);
        for (const root of status.cache.supersededRoots) {
            lines.push(`superseded ${formatBytes(root.bytes)} at ${root.path}`);
        }
    }
    if (!status.cachePresent) {
        lines.push(`No cache: first entity command pays a cold build of up to ~${status.coldBuildMaxCommits ?? '?'} commits.`);
    }
    return lines.join('\n');
}
export function formatEntityContext(result) {
    return renderEntityContextText(result);
}
export function formatAnchorCapture(result) {
    if (result.status === 'error')
        return `Error (${result.code}): ${result.message}`;
    if (result.status !== 'resolved')
        return result.message;
    const anchor = result.anchor;
    const symbol = anchor.symbolKey.containerName
        ? `${anchor.symbolKey.containerName}.${anchor.symbolKey.name}`
        : anchor.symbolKey.name;
    const tiers = anchor.expected.normalizedHash !== undefined && anchor.expected.structuralHash !== undefined
        ? 'body+normalized+structural'
        : 'body only';
    const lines = [
        `lineage   ${anchor.lineageId}`,
        `symbol    ${symbol} (${anchor.symbolKey.relativePath})`,
        `base      ${anchor.baseCommit ? anchor.baseCommit.slice(0, 12) : '(none — expected-only anchor)'}`,
        `bodyHash  ${anchor.expected.bodyHash}`,
        `tiers     ${tiers} (v${anchor.expected.hashVersion}, ${anchor.expected.algo})`,
    ];
    if (anchor.provisional)
        lines.push('provisional: uncommitted entity, not history');
    if (anchor.indeterminate)
        lines.push('indeterminate: bound-exhausted index, no proved lineage');
    if (anchor.uncommittedContent)
        lines.push('uncommitted-content: working tree differs from the committed head');
    for (const warning of result.warnings)
        lines.push(`warning   ${warning}`);
    return lines.join('\n');
}
//# sourceMappingURL=output.js.map