import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { bobbitStateDir } from "../bobbit-dir.js";
import { storageKeyForChangesetId } from "./walkthrough-store.js";
export const PR_WALKTHROUGH_AGENT_STORE_SCHEMA_VERSION = 1;
const STORE_DIR = "pr-walkthrough-agents";
export function createSubmissionProof() {
    return randomBytes(32).toString("base64url");
}
export function hashSubmissionProof(jobId, sessionId, proof) {
    return createHash("sha256").update(`${jobId}\0${sessionId}\0${proof}`).digest("hex");
}
export function verifySubmissionProof(proof, job) {
    if (!proof || !job.submissionProofHash)
        return false;
    const expected = Buffer.from(job.submissionProofHash, "hex");
    const actual = Buffer.from(hashSubmissionProof(job.jobId, job.childSessionId, proof), "hex");
    return expected.length === actual.length && timingSafeEqual(expected, actual);
}
export function rotateSubmissionProofForRestoredJob(stateDir, sessionId, jobId) {
    const store = new WalkthroughAgentStore(stateDir);
    const job = store.get(jobId);
    if (!job || job.childSessionId !== sessionId || job.status === "ready" || job.status === "error")
        return undefined;
    const proof = createSubmissionProof();
    const updated = store.update(jobId, { submissionProofHash: hashSubmissionProof(job.jobId, job.childSessionId, proof) });
    if (!updated)
        return undefined;
    return {
        BOBBIT_SESSION_ID: sessionId,
        BOBBIT_WALKTHROUGH_JOB_ID: jobId,
        BOBBIT_WALKTHROUGH_SUBMIT_PROOF: proof,
        ...walkthroughTargetEnvForJob(updated),
    };
}
export function walkthroughTargetEnvForJob(job) {
    const target = job.target;
    const number = typeof target.number === "number" ? target.number : Number(target.number);
    if (target.provider !== "github" || !target.owner || !target.repo || !Number.isInteger(number))
        return {};
    return {
        BOBBIT_WALKTHROUGH_TARGET_PROVIDER: "github",
        BOBBIT_WALKTHROUGH_TARGET_OWNER: target.owner,
        BOBBIT_WALKTHROUGH_TARGET_REPO: target.repo,
        BOBBIT_WALKTHROUGH_TARGET_NUMBER: String(number),
    };
}
export class WalkthroughAgentStore {
    rootDir;
    constructor(stateDir = bobbitStateDir()) {
        this.rootDir = path.join(stateDir, STORE_DIR, `v${PR_WALKTHROUGH_AGENT_STORE_SCHEMA_VERSION}`);
    }
    save(record) {
        const now = new Date().toISOString();
        const stored = sanitizeJob({ ...record, updatedAt: record.updatedAt || now });
        this.ensureDir();
        fs.writeFileSync(this.filePath(stored.jobId), `${JSON.stringify(stored, null, 2)}\n`, "utf-8");
        return stored;
    }
    create(input) {
        const now = new Date().toISOString();
        return this.save({
            ...input,
            schemaVersion: PR_WALKTHROUGH_AGENT_STORE_SCHEMA_VERSION,
            createdAt: input.createdAt ?? now,
            updatedAt: input.updatedAt ?? now,
        });
    }
    update(jobId, updates) {
        const existing = this.get(jobId);
        if (!existing)
            return null;
        return this.save({
            ...existing,
            ...updates,
            updatedAt: new Date().toISOString(),
        });
    }
    get(jobId) {
        try {
            const raw = JSON.parse(fs.readFileSync(this.filePath(jobId), "utf-8"));
            return parseStoredJob(raw, jobId);
        }
        catch (error) {
            if (isNodeError(error) && error.code === "ENOENT")
                return null;
            return null;
        }
    }
    getByChildSession(childSessionId) {
        return this.list().find(job => job.childSessionId === childSessionId) ?? null;
    }
    findByParentAndTarget(parentSessionId, canonicalTargetKey) {
        return this.list().find(job => job.parentSessionId === parentSessionId && job.target.canonicalKey === canonicalTargetKey) ?? null;
    }
    list() {
        try {
            return fs.readdirSync(this.rootDir, { withFileTypes: true })
                .filter(entry => entry.isFile() && entry.name.endsWith(".json"))
                .map(entry => this.readFile(path.join(this.rootDir, entry.name)))
                .filter((job) => job !== null)
                .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
        }
        catch (error) {
            if (isNodeError(error) && error.code === "ENOENT")
                return [];
            return [];
        }
    }
    readFile(filePath) {
        try {
            return parseStoredJob(JSON.parse(fs.readFileSync(filePath, "utf-8")));
        }
        catch {
            return null;
        }
    }
    ensureDir() {
        fs.mkdirSync(this.rootDir, { recursive: true });
    }
    filePath(jobId) {
        return path.join(this.rootDir, `${storageKeyForChangesetId(jobId)}.json`);
    }
}
export function sanitizeJob(record) {
    return sanitizeValue(record);
}
function parseStoredJob(raw, expectedJobId) {
    if (raw.schemaVersion !== PR_WALKTHROUGH_AGENT_STORE_SCHEMA_VERSION)
        return null;
    if (typeof raw.jobId !== "string" || (expectedJobId && raw.jobId !== expectedJobId))
        return null;
    if (typeof raw.parentSessionId !== "string" || typeof raw.childSessionId !== "string")
        return null;
    if (typeof raw.cwd !== "string" || typeof raw.changesetId !== "string" || typeof raw.tabId !== "string")
        return null;
    if (typeof raw.title !== "string" || typeof raw.createdAt !== "string" || typeof raw.updatedAt !== "string")
        return null;
    if (!isStatus(raw.status) || !isRecord(raw.target) || typeof raw.target.canonicalKey !== "string")
        return null;
    return sanitizeJob({
        schemaVersion: PR_WALKTHROUGH_AGENT_STORE_SCHEMA_VERSION,
        jobId: raw.jobId,
        parentSessionId: raw.parentSessionId,
        childSessionId: raw.childSessionId,
        projectId: typeof raw.projectId === "string" ? raw.projectId : undefined,
        cwd: raw.cwd,
        target: raw.target,
        changesetId: raw.changesetId,
        tabId: raw.tabId,
        status: raw.status,
        title: raw.title,
        createdAt: raw.createdAt,
        updatedAt: raw.updatedAt,
        lastValidationError: isRecord(raw.lastValidationError) ? raw.lastValidationError : undefined,
        submittedAt: typeof raw.submittedAt === "string" ? raw.submittedAt : undefined,
        payloadUpdatedAt: typeof raw.payloadUpdatedAt === "string" ? raw.payloadUpdatedAt : undefined,
        warnings: Array.isArray(raw.warnings) ? raw.warnings : undefined,
        error: isRecord(raw.error) ? raw.error : undefined,
        reminderCount: typeof raw.reminderCount === "number" ? raw.reminderCount : undefined,
        submissionProofHash: typeof raw.submissionProofHash === "string" ? raw.submissionProofHash : undefined,
        analysisBundle: isRecord(raw.analysisBundle) ? raw.analysisBundle : undefined,
    });
}
function isStatus(value) {
    return value === "starting" || value === "waiting_for_yaml" || value === "validation_failed" || value === "ready" || value === "error";
}
function sanitizeValue(value) {
    if (Array.isArray(value))
        return value.map(item => sanitizeValue(item));
    if (!isRecord(value))
        return value;
    const out = {};
    for (const [key, entry] of Object.entries(value)) {
        if (isSensitiveKey(key))
            continue;
        out[key] = sanitizeValue(entry);
    }
    return out;
}
function isSensitiveKey(key) {
    if (/proof/i.test(key) && !/hash$/i.test(key))
        return true;
    return /(^|[-_])(token|secret|authorization|auth[-_]?header|auth[-_]?headers|raw[-_]?headers|headers?)($|[-_])/i.test(key)
        || /^(token|secret|authorization|auth|headers)$/i.test(key);
}
function isRecord(value) {
    return typeof value === "object" && value !== null;
}
function isNodeError(error) {
    return error instanceof Error && "code" in error;
}
//# sourceMappingURL=walkthrough-agent-store.js.map