import { createHash } from "crypto";
import { mkdir, readFile, writeFile } from "fs/promises";
import { basename, extname, join, relative } from "path";
import { Blob } from "buffer";
export const DEFAULT_OPENAI_IMAGE_MODEL = "gpt-image-2";
export const DEFAULT_OPENAI_IMAGES_BASE_URL = "https://api.openai.com/v1";
export const DEFAULT_OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
export const OPENAI_IMAGE_API_KEY_ACTION = "Use an OpenAI Platform project API key: create it through Platform OAuth, keep any encrypted copy outside the repo, decrypt only locally, run one command with OPENAI_API_KEY=<ephemeral>, then unset the env/temp key. Codex/ChatGPT OAuth tokens are only for login and cannot call the Images API.";
export class OpenAiImageError extends Error {
    kind;
    status;
    action;
    constructor(message, options) {
        super(message);
        this.name = "OpenAiImageError";
        this.kind = options.kind;
        this.status = options.status;
        this.action = options.action;
    }
}
export class OpenAiImageClient {
    apiKey;
    fetchImpl;
    baseUrl;
    timeoutMs;
    constructor(options) {
        this.apiKey = options.apiKey;
        this.fetchImpl = options.fetch ?? fetch;
        this.baseUrl = (options.baseUrl ?? DEFAULT_OPENAI_IMAGES_BASE_URL).replace(/\/$/, "");
        this.timeoutMs = options.timeoutMs ?? 120_000;
    }
    async generate(options) {
        const model = options.model ?? DEFAULT_OPENAI_IMAGE_MODEL;
        const body = compactRecord({
            model,
            prompt: options.prompt,
            n: options.n ?? 1,
            size: options.size,
            quality: options.quality,
            background: options.background,
            output_format: options.outputFormat ?? "png",
            user: options.user,
        });
        const response = await this.requestJson("/images/generations", body);
        return this.toImageResult("generate", response, {
            model,
            prompt: options.prompt,
            outputFormat: options.outputFormat ?? "png",
        });
    }
    async edit(options) {
        const model = options.model ?? DEFAULT_OPENAI_IMAGE_MODEL;
        if (options.imagePaths.length === 0) {
            throw new OpenAiImageError("At least one source image is required for image edits.", {
                kind: "api",
                action: "Pass one or more image paths to `omk image edit`.",
            });
        }
        const form = new FormData();
        form.set("model", model);
        form.set("prompt", options.prompt);
        form.set("n", String(options.n ?? 1));
        form.set("output_format", options.outputFormat ?? "png");
        appendOptionalFormValue(form, "size", options.size);
        appendOptionalFormValue(form, "quality", options.quality);
        appendOptionalFormValue(form, "background", options.background);
        appendOptionalFormValue(form, "user", options.user);
        for (const imagePath of options.imagePaths) {
            const blob = await readImageBlob(imagePath);
            form.append("image", blob, basename(imagePath));
        }
        if (options.maskPath) {
            const mask = await readImageBlob(options.maskPath);
            form.set("mask", mask, basename(options.maskPath));
        }
        const response = await this.requestForm("/images/edits", form);
        return this.toImageResult("edit", response, {
            model,
            prompt: options.prompt,
            outputFormat: options.outputFormat ?? "png",
            imageCount: options.imagePaths.length,
            maskProvided: Boolean(options.maskPath),
        });
    }
    async requestJson(path, body) {
        return this.request(path, {
            method: "POST",
            headers: {
                authorization: `Bearer ${this.apiKey}`,
                "content-type": "application/json",
            },
            body: JSON.stringify(body),
        });
    }
    async requestForm(path, body) {
        return this.request(path, {
            method: "POST",
            headers: {
                authorization: `Bearer ${this.apiKey}`,
            },
            body,
        });
    }
    async request(path, init) {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
        try {
            const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
                ...init,
                signal: controller.signal,
            });
            const body = await parseResponseBody(response);
            if (!response.ok) {
                throw mapOpenAiError(response.status, body);
            }
            if (!isRecord(body)) {
                throw new OpenAiImageError("OpenAI Images API returned a non-object response.", {
                    kind: "invalid_response",
                    action: "Retry the request; if it repeats, capture `--json` output and report the response shape without secrets.",
                });
            }
            return body;
        }
        catch (error) {
            if (error instanceof OpenAiImageError)
                throw error;
            if (error instanceof Error && error.name === "AbortError") {
                throw new OpenAiImageError("OpenAI Images API request timed out.", {
                    kind: "network",
                    action: "Retry with a smaller image or increase the timeout option when wiring the CLI.",
                });
            }
            const message = error instanceof Error ? error.message : String(error);
            throw new OpenAiImageError(`OpenAI Images API request failed: ${redactSensitiveText(message)}`, {
                kind: "network",
                action: "Check network connectivity and the OpenAI API status page, then retry.",
            });
        }
        finally {
            clearTimeout(timeout);
        }
    }
    async toImageResult(operation, response, context) {
        const first = response.data?.[0];
        if (!first) {
            throw new OpenAiImageError("OpenAI Images API returned no image data.", {
                kind: "invalid_response",
                action: "Retry the request; if it repeats, verify the selected model supports the Images API.",
            });
        }
        const image = first.b64_json
            ? Buffer.from(first.b64_json, "base64")
            : first.url
                ? await this.downloadImage(first.url)
                : undefined;
        if (!image || image.length === 0) {
            throw new OpenAiImageError("OpenAI Images API returned an empty image payload.", {
                kind: "invalid_response",
                action: "Retry the request; if it repeats, inspect the model and output format options.",
            });
        }
        const outputFormat = response.output_format ?? context.outputFormat;
        return {
            operation,
            model: context.model,
            image,
            mimeType: mimeTypeForOutputFormat(outputFormat),
            outputFormat,
            created: response.created,
            usage: response.usage,
            promptHash: sha256(context.prompt),
            promptLength: context.prompt.length,
            imageCount: context.imageCount,
            maskProvided: context.maskProvided,
        };
    }
    async downloadImage(url) {
        const response = await this.fetchImpl(url, { method: "GET", signal: AbortSignal.timeout(this.timeoutMs) });
        if (!response.ok) {
            throw new OpenAiImageError(`OpenAI image download failed (${response.status}).`, {
                kind: "api",
                status: response.status,
                action: "Retry the request; generated image URLs can expire quickly.",
            });
        }
        return Buffer.from(await response.arrayBuffer());
    }
}
export function resolveOpenAiApiKey(env = process.env, envName = DEFAULT_OPENAI_API_KEY_ENV) {
    const credential = env[envName]?.trim();
    if (!credential) {
        throw new OpenAiImageError(`OpenAI API key is missing from ${envName}.`, {
            kind: "auth",
            action: `${OPENAI_IMAGE_API_KEY_ACTION} Set ${envName} only in the runtime environment for this command; OMK does not store it.`,
        });
    }
    if (!isOpenAiPlatformApiKey(credential)) {
        const issue = isLikelyOpenAiOAuthCredential(credential)
            ? "the value looks like a Codex/ChatGPT OAuth or session token"
            : "the value does not look like an OpenAI Platform API key";
        throw new OpenAiImageError(`OpenAI Images API requires an OpenAI Platform project API key in ${envName}; ${issue}.`, {
            kind: "auth",
            action: OPENAI_IMAGE_API_KEY_ACTION,
        });
    }
    return credential;
}
export function isOpenAiPlatformApiKey(credential) {
    const value = credential.trim();
    return /^sk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{8,}$/.test(value);
}
export function isLikelyOpenAiOAuthCredential(credential) {
    const value = credential.trim();
    return /^Bearer\s+/i.test(value)
        || /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/.test(value)
        || /^(?:oauth|acct|codex|chatgpt):/i.test(value)
        || /^(?:sess|oai)-[A-Za-z0-9_-]+/i.test(value)
        || /^(?:access|refresh)[_-]?token[:=]/i.test(value);
}
export async function saveOpenAiImageResult(root, result, options = {}, now = new Date()) {
    const dir = join(root, ".omk", "images");
    await mkdir(dir, { recursive: true });
    const stem = safeTimestamp(now);
    const imagePath = join(dir, `${stem}.${result.outputFormat}`);
    const metadataPath = join(dir, `${stem}.json`);
    const relativeImagePath = relative(root, imagePath).replace(/\\/g, "/");
    const relativeMetadataPath = relative(root, metadataPath).replace(/\\/g, "/");
    await writeFile(imagePath, result.image);
    const metadata = {
        createdAt: now.toISOString(),
        operation: result.operation,
        provider: "openai",
        model: result.model,
        output: {
            path: relativeImagePath,
            mimeType: result.mimeType,
            bytes: result.image.length,
        },
        request: {
            promptSha256: result.promptHash,
            promptLength: result.promptLength,
            size: options.size,
            quality: options.quality,
            background: options.background,
            outputFormat: result.outputFormat,
            n: options.n ?? 1,
            imageCount: result.imageCount,
            maskProvided: result.maskProvided,
        },
        response: {
            created: result.created,
            usage: result.usage,
        },
    };
    await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf-8");
    return {
        ...result,
        imagePath,
        metadataPath,
        relativeImagePath,
        relativeMetadataPath,
    };
}
function appendOptionalFormValue(form, name, value) {
    if (value !== undefined && value !== "")
        form.set(name, value);
}
async function readImageBlob(filePath) {
    const bytes = await readFile(filePath);
    return new Blob([bytes], { type: mimeTypeForPath(filePath) });
}
function mimeTypeForPath(filePath) {
    const ext = extname(filePath).toLowerCase();
    if (ext === ".jpg" || ext === ".jpeg")
        return "image/jpeg";
    if (ext === ".webp")
        return "image/webp";
    return "image/png";
}
function mimeTypeForOutputFormat(format) {
    if (format === "jpeg")
        return "image/jpeg";
    if (format === "webp")
        return "image/webp";
    return "image/png";
}
async function parseResponseBody(response) {
    const text = await response.text();
    if (!text)
        return {};
    try {
        return JSON.parse(text);
    }
    catch {
        return text;
    }
}
function mapOpenAiError(status, body) {
    const message = summarizeOpenAiError(body) || `OpenAI Images API request failed (${status}).`;
    const lower = message.toLowerCase();
    if (lower.includes("content_policy") || lower.includes("moderation") || lower.includes("safety")) {
        return new OpenAiImageError(redactSensitiveText(message), {
            kind: "moderation",
            status,
            action: "Revise the prompt or input image to satisfy OpenAI image safety policy, then retry.",
        });
    }
    if (status === 401) {
        return new OpenAiImageError(redactSensitiveText(message), {
            kind: "auth",
            status,
            action: `${OPENAI_IMAGE_API_KEY_ACTION} Check the configured API-key environment variable; OMK reads it only from runtime env and never stores it.`,
        });
    }
    if (status === 403) {
        return new OpenAiImageError(redactSensitiveText(message), {
            kind: "permission",
            status,
            action: "Check project permissions, API tier/model access, organization policy, and whether Images API access is enabled for this key.",
        });
    }
    if (status === 429) {
        return new OpenAiImageError(redactSensitiveText(message), {
            kind: lower.includes("quota") || lower.includes("billing") ? "billing" : "rate_limit",
            status,
            action: lower.includes("quota") || lower.includes("billing")
                ? "Check OpenAI billing, project quota, and usage limits before retrying."
                : "Wait, then retry with lower concurrency or smaller images.",
        });
    }
    if (status === 402 || lower.includes("billing") || lower.includes("insufficient_quota") || lower.includes("quota")) {
        return new OpenAiImageError(redactSensitiveText(message), {
            kind: "billing",
            status,
            action: "Check OpenAI billing status, project quota, and monthly usage limits, then retry.",
        });
    }
    return new OpenAiImageError(redactSensitiveText(message), {
        kind: "api",
        status,
        action: "Verify prompt, input image constraints, selected model, and output options, then retry.",
    });
}
function summarizeOpenAiError(body) {
    if (typeof body === "string")
        return body.slice(0, 800);
    if (!isRecord(body))
        return "";
    const error = body.error;
    if (isRecord(error)) {
        const message = typeof error.message === "string" ? error.message : "";
        const code = typeof error.code === "string" ? error.code : "";
        const type = typeof error.type === "string" ? error.type : "";
        return [message, code, type].filter(Boolean).join(" ").slice(0, 800);
    }
    const message = typeof body.message === "string" ? body.message : "";
    return message.slice(0, 800);
}
function compactRecord(values) {
    const entries = Object.entries(values).filter(([, value]) => value !== undefined && value !== "");
    return Object.fromEntries(entries);
}
function safeTimestamp(date) {
    return date.toISOString().replace(/[:.]/g, "-");
}
function sha256(value) {
    return createHash("sha256").update(value).digest("hex");
}
function redactSensitiveText(value) {
    return value
        .replace(/sk-[A-Za-z0-9_-]+/g, "[redacted-openai-key]")
        .replace(/\bBearer\s+[^\s"',;]+/gi, "Bearer [redacted-oauth-token]")
        .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?\b/g, "[redacted-oauth-token]")
        .replace(/\b(?:oauth|acct|codex|chatgpt):[^\s"',;]+/gi, "[redacted-oauth-token]")
        .replace(/\b(?:sess|oai)-[^\s"',;]+/gi, "[redacted-oauth-token]")
        .replace(/\b((?:access|refresh)[_-]?token[:=])[^\s"',;]+/gi, "$1[redacted-oauth-token]");
}
function isRecord(value) {
    return typeof value === "object" && value !== null;
}
