import { createHash, randomUUID } from "crypto";
import { join } from "path";
import { ensureSecureDir, appendSecureLog } from "./secure-fs.js";
import { getTracesDir } from "../config/paths.js";
// Issue #5 lateral (2026-05-22) — 활동 로그 파일명 KST 기준
import { kstToday } from "./datetime-kst.js";
const TRACE_DISABLE_ENV = "SAFETY_TRACE_DISABLE";
// SAFETY_TRACE_DIR 가 우선이지만 미설정 시 paths.ts 의 getTracesDir() 가 SAFETY_LOCAL_DIR
// 도 함께 반영해 ~/.agent-safety-oss/traces 또는 SAFETY_LOCAL_DIR/traces 로 라우팅한다.
const TRACE_DIR_ENV = "SAFETY_TRACE_DIR";
const TRACE_AGENT_IRI = "safety:agent/llm-client";
const TRACE_ID_PREFIX = "safety:trace";
const HASH_ID_PREFIX = "safety:hash/sha256";
const CIRCULAR_REFERENCE_MARKER = "[Circular]";
// MCP Tool 호출을 PROV-O Activity JSONL 로 append-only 기록한다.
export class TraceRecorder {
    traceDir;
    writeQueue = Promise.resolve();
    constructor(traceDir) {
        // SAFETY_TRACE_DIR 명시 > paths.ts 의 getTracesDir() (SAFETY_LOCAL_DIR 우선, fallback ~/.agent-safety-oss/traces)
        this.traceDir = traceDir ?? process.env[TRACE_DIR_ENV] ?? getTracesDir();
    }
    async recordToolCall(toolName, input, output, startedAt, endedAt) {
        if (isTraceDisabled()) {
            return;
        }
        try {
            const inputHash = sha256(input);
            const outputHash = sha256(output);
            const activity = {
                "@id": `${TRACE_ID_PREFIX}/${randomUUID()}`,
                "@type": ["prov:Activity", "owl:NamedIndividual"],
                "prov:startedAtTime": startedAt,
                "prov:endedAtTime": endedAt,
                "prov:wasAssociatedWith": TRACE_AGENT_IRI,
                "prov:used": `${HASH_ID_PREFIX}/${inputHash}`,
                "prov:generated": `${HASH_ID_PREFIX}/${outputHash}`,
                "safety:toolName": toolName,
                "safety:inputHash": inputHash,
                "safety:outputHash": outputHash,
            };
            const traceFilePath = join(this.traceDir, `${getTraceDate(startedAt)}.jsonl`);
            const traceLine = `${JSON.stringify(activity)}\n`;
            const writeOperation = async () => {
                await ensureSecureDir(this.traceDir);
                await appendSecureLog(traceFilePath, traceLine);
            };
            // 같은 프로세스 안에서는 순서 보장, 프로세스 간에는 O_APPEND appendFile 에 위임한다.
            const queuedWrite = this.writeQueue.then(writeOperation, writeOperation);
            this.writeQueue = queuedWrite;
            await queuedWrite;
        }
        catch (err) {
            console.error(`[TraceRecorder] trace append failed: ${getErrorMessage(err)}`);
        }
    }
}
function isTraceDisabled() {
    return process.env[TRACE_DISABLE_ENV]?.trim().toLowerCase() === "true";
}
function getTraceDate(startedAt) {
    // Issue #5 lateral — KST 기준 파일명 (UTC slice 회귀 해소)
    return /^\d{4}-\d{2}-\d{2}/.test(startedAt)
        ? startedAt.slice(0, 10)
        : kstToday();
}
function sha256(value) {
    return createHash("sha256").update(stableStringify(value)).digest("hex");
}
function stableStringify(value) {
    return JSON.stringify(normalizeForJson(value, new WeakSet()));
}
function normalizeForJson(value, seen) {
    if (value === undefined) {
        return "[undefined]";
    }
    if (value === null ||
        typeof value === "string" ||
        typeof value === "number" ||
        typeof value === "boolean") {
        return value;
    }
    if (typeof value === "bigint") {
        return value.toString();
    }
    if (typeof value === "symbol") {
        return value.toString();
    }
    if (typeof value === "function") {
        return `[function:${value.name || "anonymous"}]`;
    }
    if (value instanceof Date) {
        return value.toISOString();
    }
    if (value instanceof Error) {
        return {
            message: value.message,
            name: value.name,
        };
    }
    if (Array.isArray(value)) {
        if (seen.has(value)) {
            return CIRCULAR_REFERENCE_MARKER;
        }
        seen.add(value);
        const normalized = value.map((item) => normalizeForJson(item, seen));
        seen.delete(value);
        return normalized;
    }
    if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
        return {
            data: value.toString("base64"),
            type: "Buffer",
        };
    }
    if (typeof value === "object") {
        if (seen.has(value)) {
            return CIRCULAR_REFERENCE_MARKER;
        }
        seen.add(value);
        const normalized = {};
        for (const key of Object.keys(value).sort()) {
            normalized[key] = normalizeForJson(value[key], seen);
        }
        seen.delete(value);
        return normalized;
    }
    return String(value);
}
function getErrorMessage(err) {
    return err instanceof Error ? err.message : String(err);
}
//# sourceMappingURL=trace-recorder.js.map