import { Buffer } from 'node:buffer';
import { mkdir } from 'node:fs/promises';
import { basename, extname, join, resolve } from 'node:path';
import { randomUUID } from 'node:crypto';
import { RuntimeWorkspaceService } from '../../../core/runtime/workspaces/index.js';
import { controlPlaneChatSessionsController } from '../../../server/controllers/trpc/control-plane/chat-sessions-controller.js';
import { getWorkspaceOperationLogger } from '../../../server/logging/workspace-operation-logger.js';
export const CHAT_SESSION_IMAGE_UPLOAD_LIMITS = {
    maxFiles: 10,
    maxFileSizeBytes: 10 * 1024 * 1024,
};
const SUPPORTED_IMAGE_TYPES = {
    '.gif': 'image/gif',
    '.jpeg': 'image/jpeg',
    '.jpg': 'image/jpeg',
    '.png': 'image/png',
    '.webp': 'image/webp',
};
/**
 * Owns browser-uploaded session image storage for the local control plane.
 *
 * Images are persisted under the selected workspace state root so the runtime can
 * later inspect them through the existing filesystem-backed view_image tool.
 */
export class ChatSessionImageUploadService {
    options;
    constructor(options) {
        this.options = options;
    }
    resolveWorkspace(request) {
        const workspaceContext = RuntimeWorkspaceService.resolveContext({
            workspaceRoot: this.options.workspaceRoot,
            stateRoot: this.options.stateRoot,
        });
        const workspaceId = this.readWorkspaceId(request);
        if (!workspaceId) {
            return workspaceContext.activeWorkspace;
        }
        const workspace = workspaceContext.workspaces.find((candidate) => candidate.id === workspaceId);
        if (!workspace) {
            throw new ChatSessionUploadError(404, `Workspace not found: ${workspaceId}`);
        }
        return workspace;
    }
    async resolveUploadDirectory(request) {
        const sessionId = this.readSessionId(request);
        const workspace = this.resolveWorkspace(request);
        this.requireSession(workspace, sessionId);
        const uploadDirectory = join(workspace.stateRoot, 'uploads', 'sessions', sessionId);
        await mkdir(uploadDirectory, { recursive: true });
        return uploadDirectory;
    }
    createStoredFilename(file) {
        return `${randomUUID()}${this.resolveSupportedExtension(file)}`;
    }
    acceptsImageFile(file) {
        return Boolean(this.resolveSupportedExtension(file));
    }
    completeUploads(request) {
        const sessionId = this.readSessionId(request);
        const workspace = this.resolveWorkspace(request);
        this.requireSession(workspace, sessionId);
        const files = this.readUploadedFiles(request.files);
        if (!files.length) {
            throw new ChatSessionUploadError(400, 'Upload at least one image file.');
        }
        if (files.length > CHAT_SESSION_IMAGE_UPLOAD_LIMITS.maxFiles) {
            throw new ChatSessionUploadError(400, `Upload at most ${CHAT_SESSION_IMAGE_UPLOAD_LIMITS.maxFiles} images at a time.`);
        }
        const uploadRoot = resolve(workspace.stateRoot, 'uploads', 'sessions', sessionId);
        const uploads = files.map((file) => this.projectUploadedFile(uploadRoot, file));
        getWorkspaceOperationLogger(workspace.stateRoot).info({
            sessionId,
            uploadCount: uploads.length,
            workspaceId: workspace.id,
            workspaceRoot: workspace.workspaceRoot,
            stateRoot: workspace.stateRoot,
        }, 'Control-plane session images uploaded');
        return {
            uploads,
        };
    }
    requireSession(workspace, sessionId) {
        const session = controlPlaneChatSessionsController.readDetail({
            workspaceRoot: workspace.workspaceRoot,
            stateRoot: workspace.stateRoot,
            sessionStoragePath: resolve(workspace.stateRoot, 'chat-sessions.catalog.json'),
            workspaceId: workspace.id,
        }, sessionId);
        if (!session) {
            throw new ChatSessionUploadError(404, `Chat session not found: ${sessionId}`);
        }
    }
    projectUploadedFile(uploadRoot, file) {
        const resolvedPath = resolve(file.path);
        if (!resolvedPath.startsWith(`${uploadRoot}/`)) {
            throw new ChatSessionUploadError(400, 'Uploaded file was stored outside the session upload directory.');
        }
        return {
            id: basename(file.filename, extname(file.filename)),
            path: resolvedPath,
            originalName: this.decodeOriginalFilename(file.originalname),
            mediaType: file.mimetype,
            sizeBytes: file.size,
        };
    }
    readUploadedFiles(files) {
        if (Array.isArray(files)) {
            return files;
        }
        if (!files || typeof files !== 'object') {
            return [];
        }
        return Object.values(files).flat();
    }
    resolveSupportedExtension(file) {
        const extension = extname(file.originalname).toLowerCase();
        const expectedMediaType = SUPPORTED_IMAGE_TYPES[extension];
        if (!expectedMediaType) {
            return undefined;
        }
        return !file.mimetype || file.mimetype === expectedMediaType ? extension : undefined;
    }
    readSessionId(request) {
        const sessionId = typeof request.params.sessionId === 'string' ? request.params.sessionId.trim() : '';
        if (!sessionId) {
            throw new ChatSessionUploadError(400, 'Missing sessionId.');
        }
        return sessionId;
    }
    readWorkspaceId(request) {
        const rawWorkspaceId = request.query.workspaceId;
        const workspaceId = typeof rawWorkspaceId === 'string' ? rawWorkspaceId.trim() : '';
        return workspaceId || undefined;
    }
    decodeOriginalFilename(originalName) {
        if (!looksLikeMojibake(originalName)) {
            return originalName;
        }
        const decoded = Buffer.from(originalName, 'latin1').toString('utf8');
        return decoded.includes('\uFFFD') ? originalName : decoded;
    }
}
function looksLikeMojibake(value) {
    return /[ÃÂâÆæÅå]/.test(value);
}
export class ChatSessionUploadError extends Error {
    statusCode;
    constructor(statusCode, message) {
        super(message);
        this.statusCode = statusCode;
        this.name = 'ChatSessionUploadError';
    }
}
export function isChatSessionUploadError(error) {
    return error instanceof ChatSessionUploadError;
}
//# sourceMappingURL=chat-session-image-uploads.js.map