/**
 * File-per-message body storage backend.
 *
 * Disk layout: {basePath}/{accountId}/<xx>/<uuid>.eml
 * Filename is an opaque UUID. Two-char prefix dir for filesystem fan-out.
 *
 * CRITICAL: the on-disk filename carries NO semantic meaning — not folder
 * id, not UID, not Message-ID. A new UUID is minted on every `putMessage`.
 * Moves, UID renumbers, UIDVALIDITY bumps cannot shadow a body because no
 * filename is ever reused. DB's `body_path` column is the sole authority
 * on where a given message's body lives.
 */

import * as fs from "node:fs";
import * as path from "node:path";
import { randomUUID } from "node:crypto";
import type { MessageStore } from "@bobfrankston/mailx-types";

export class FileMessageStore implements MessageStore {
    constructor(private basePath: string) {
        fs.mkdirSync(basePath, { recursive: true });
    }

    /** Fresh opaque path per call. No inputs from the caller affect the name. */
    private newMessagePath(accountId: string): string {
        const uuid = randomUUID().replace(/-/g, "");
        const prefix = uuid.slice(0, 2);
        return path.join(this.basePath, accountId, prefix, `${uuid}.eml`);
    }

    /** Verify a given path resolves inside this store's basePath — refuses
     *  any value that doesn't (cheap directory-traversal guard). */
    private inStore(fullPath: string): boolean {
        if (!fullPath) return false;
        const rel = path.relative(path.resolve(this.basePath), path.resolve(fullPath));
        return !rel.startsWith("..") && !path.isAbsolute(rel);
    }

    /** Write a new body. Always a fresh UUID path. Caller MUST persist the
     *  returned path in the DB (`body_path`) and use it for all reads. The
     *  (folderId, uid) args are kept for interface compatibility; they do
     *  NOT affect the filename. */
    async putMessage(accountId: string, _folderId: number, _uid: number, raw: Buffer): Promise<string> {
        const filePath = this.newMessagePath(accountId);
        fs.mkdirSync(path.dirname(filePath), { recursive: true });
        fs.writeFileSync(filePath, raw);
        return filePath;
    }

    /** Read by absolute path (DB `body_path`). The primary read API. */
    async readByPath(fullPath: string): Promise<Buffer> {
        if (!this.inStore(fullPath)) throw new Error(`refusing to read outside store: ${fullPath}`);
        return fs.readFileSync(fullPath);
    }

    async hasByPath(fullPath: string): Promise<boolean> {
        if (!this.inStore(fullPath)) return false;
        return fs.existsSync(fullPath);
    }

    async unlinkByPath(fullPath: string): Promise<void> {
        if (!this.inStore(fullPath)) return;
        if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
    }

    // MessageStore interface compatibility (unused once all callers migrate to
    // path-based reads). These used to compose {folderId}/{uid}.eml and they
    // would resurrect the comingling bug if restored. Kept as throwing stubs
    // so any accidental caller surfaces loudly rather than silently misbehave.
    getMessagePath(_accountId: string, _folderId: number, _uid: number): string {
        throw new Error("FileMessageStore.getMessagePath is retired — read body_path from DB");
    }
    async getMessage(_accountId: string, _folderId: number, _uid: number): Promise<Buffer> {
        throw new Error("FileMessageStore.getMessage(folder,uid) is retired — use readByPath(body_path)");
    }
    async hasMessage(_accountId: string, _folderId: number, _uid: number): Promise<boolean> {
        throw new Error("FileMessageStore.hasMessage(folder,uid) is retired — use hasByPath(body_path)");
    }
    async deleteMessage(_accountId: string, _folderId: number, _uid: number): Promise<void> {
        throw new Error("FileMessageStore.deleteMessage(folder,uid) is retired — use unlinkByPath(body_path)");
    }
}
