/**
 * photo-storage.ts
 *
 * 현장사진 증빙 로컬 저장소.
 * 기본 위치는 ~/.agent-safety-oss/photos 이며 SAFETY_LOCAL_DIR 로 루트 변경 가능.
 */
import { readFile, readdir } from "node:fs/promises";
import { createHash } from "node:crypto";
import { homedir } from "node:os";
import { basename, extname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { ensureSecureDir, writeSecureJson } from "./secure-fs.js";
const STORAGE_ENV_NAME = "SAFETY_LOCAL_DIR";
const DEFAULT_STORAGE_ROOT = resolve(homedir(), ".agent-safety-oss");
const PHOTO_ID_PREFIX = "photo:";
const PHOTO_IRI_PREFIX = "safety:photo_evidence/";
const PHOTO_CONTEXT = {
    safety: "https://safety.ratelworks.org/ontology/v1/",
    cc: "https://construction.ratelworks.org/ontology/v1/",
    prov: "http://www.w3.org/ns/prov#",
    foaf: "http://xmlns.com/foaf/0.1/",
    xsd: "http://www.w3.org/2001/XMLSchema#",
    rdfs: "http://www.w3.org/2000/01/rdf-schema#",
};
export function getSafetyStorageRoot() {
    return resolve(process.env[STORAGE_ENV_NAME] || DEFAULT_STORAGE_ROOT);
}
export function getPhotoStorageDir() {
    return join(getSafetyStorageRoot(), "photos");
}
export function toPhotoIri(photoId) {
    const normalized = normalizePhotoId(photoId);
    return `${PHOTO_IRI_PREFIX}${localPhotoSlug(normalized)}`;
}
export function normalizePhotoId(photoIdOrIri) {
    const trimmed = photoIdOrIri.trim();
    if (trimmed.startsWith(PHOTO_IRI_PREFIX)) {
        return `${PHOTO_ID_PREFIX}${trimmed.slice(PHOTO_IRI_PREFIX.length)}`;
    }
    if (trimmed.startsWith(PHOTO_ID_PREFIX))
        return trimmed;
    return `${PHOTO_ID_PREFIX}${trimmed}`;
}
export async function savePhotoEvidence(input) {
    await ensurePhotoDir();
    const prepared = await preparePhotoFile(input);
    const photoId = makePhotoId(input.capturedAt, prepared.hashSeed);
    const now = new Date().toISOString();
    const record = {
        "@context": PHOTO_CONTEXT,
        "@id": toPhotoIri(photoId),
        "@type": ["safety:PhotoEvidence", "prov:Entity", "foaf:Image"],
        photoId,
        label: makePhotoLabel(input.evidenceType, input.relatedTask, input.capturedAt),
        fileUri: prepared.fileUri,
        originalFilePath: prepared.originalFilePath,
        storagePath: prepared.storagePath,
        byteSize: prepared.byteSize,
        sha256: prepared.sha256,
        relatedTask: input.relatedTask,
        relatedSite: input.relatedSite,
        relatedRisk: input.relatedRisk,
        evidenceType: input.evidenceType,
        capturedAt: input.capturedAt,
        createdAt: now,
        evidences: [],
    };
    await writePhotoRecord(record);
    return record;
}
export async function loadPhotoEvidence(photoIdOrIri) {
    try {
        const text = await readFile(photoRecordPath(photoIdOrIri), "utf8");
        return JSON.parse(text);
    }
    catch {
        return null;
    }
}
export async function listPhotoEvidence(filter = {}) {
    await ensurePhotoDir();
    const files = await readdir(getPhotoStorageDir());
    const records = [];
    for (const filename of files) {
        if (!filename.endsWith(".jsonld"))
            continue;
        try {
            const text = await readFile(join(getPhotoStorageDir(), filename), "utf8");
            const record = JSON.parse(text);
            if (!matchesPhotoFilter(record, filter))
                continue;
            records.push(record);
        }
        catch {
            // 손상된 사진 메타는 목록에서 제외한다.
        }
    }
    return records.sort((a, b) => b.capturedAt.localeCompare(a.capturedAt));
}
export async function appendPhotoEvidenceIssue(photoIdOrIri, issueId) {
    const record = await loadPhotoEvidence(photoIdOrIri);
    if (!record)
        return null;
    if (!record.evidences.includes(issueId)) {
        record.evidences.push(issueId);
        await writePhotoRecord(record);
    }
    return record;
}
async function preparePhotoFile(input) {
    if (input.filePath) {
        const sourcePath = resolve(input.filePath);
        // stat + readFile 분리 TOCTOU race 회피 — open(O_RDONLY) 후 fstat 으로 동일 inode 검증.
        const { open } = await import("node:fs/promises");
        const fh = await open(sourcePath, "r");
        let buffer;
        let sourceStat;
        try {
            sourceStat = await fh.stat();
            if (!sourceStat.isFile())
                throw new Error(`사진 파일이 아닙니다: ${sourcePath}`);
            buffer = await fh.readFile();
        }
        finally {
            await fh.close();
        }
        const sha256 = createHash("sha256").update(buffer).digest("hex");
        const tempPhotoId = makePhotoId(input.capturedAt, sha256);
        const extension = extname(sourcePath) || ".bin";
        const storagePath = join(getPhotoStorageDir(), `${safeFilename(tempPhotoId)}${extension}`);
        // 이미 메모리에 읽은 buffer 를 write — copyFile 대신 writeFile (재경합 회피).
        const { writeFile } = await import("node:fs/promises");
        await writeFile(storagePath, buffer, { mode: 0o600 });
        return {
            fileUri: pathToFileURL(storagePath).href,
            originalFilePath: sourcePath,
            storagePath,
            byteSize: sourceStat.size,
            sha256,
            hashSeed: sha256,
        };
    }
    if (!input.fileUri)
        throw new Error("filePath 또는 fileUri 중 하나는 필요합니다.");
    return {
        fileUri: input.fileUri,
        hashSeed: createHash("sha256").update(input.fileUri).digest("hex"),
    };
}
async function writePhotoRecord(record) {
    await ensurePhotoDir();
    await writeSecureJson(photoRecordPath(record.photoId), record);
}
async function ensurePhotoDir() {
    await ensureSecureDir(getPhotoStorageDir());
}
function photoRecordPath(photoIdOrIri) {
    return join(getPhotoStorageDir(), `${safeFilename(photoIdOrIri)}.jsonld`);
}
function matchesPhotoFilter(record, filter) {
    if (filter.siteId && record.relatedSite !== filter.siteId)
        return false;
    if (filter.activityId && record.relatedTask !== filter.activityId)
        return false;
    if (filter.since && record.capturedAt < filter.since && record.createdAt < filter.since)
        return false;
    return true;
}
function makePhotoId(capturedAt, hashSeed) {
    const stamp = capturedAt
        .replace(/[^0-9]/g, "")
        .slice(0, 14)
        .padEnd(14, "0");
    return `${PHOTO_ID_PREFIX}${stamp}-${hashSeed.slice(0, 10)}`;
}
function makePhotoLabel(evidenceType, relatedTask, capturedAt) {
    const typeLabel = {
        before_action: "조치 전",
        after_action: "조치 후",
        inspection: "점검",
    };
    return `${typeLabel[evidenceType]} 사진 증빙 - ${relatedTask} - ${capturedAt}`;
}
function safeFilename(photoIdOrIri) {
    return basename(localPhotoSlug(normalizePhotoId(photoIdOrIri))).replace(/[^a-zA-Z0-9_.-]/g, "_");
}
function localPhotoSlug(photoId) {
    return photoId.startsWith(PHOTO_ID_PREFIX) ? photoId.slice(PHOTO_ID_PREFIX.length) : photoId;
}
//# sourceMappingURL=photo-storage.js.map