/**
 * Files Write Operations
 *
 * HTTP endpoints for modifying files and directories:
 * - Write file
 * - Create file
 * - Create directory
 * - Rename file/directory
 * - Duplicate file/directory
 * - Upload file
 * - Delete file/directory
 */

import { t } from 'elysia';
import { createRouter } from '$shared/utils/ws-server';
import { debug } from '$shared/utils/logger';
import {
	writeFileOperation,
	createFileOperation,
	createDirectoryOperation,
	renameOperation,
	duplicateOperation,
	deleteOperation
} from '../../files/file-operations';
import { createZipOperation, extractZipOperation } from '../../files/file-archive';
import { stat as fsStat, readdir as fsReaddir, rename as fsRename } from 'node:fs/promises';
import { requireFilePathAccess, requireSharedFilePathAccess } from './path-access';

export const writeHandler = createRouter()
	// Write file operation
	.http('files:write-file', {
		data: t.Object({
			filePath: t.String(),
			content: t.String(),
			// Optimistic concurrency token: the disk mtime the client's buffer is
			// based on. When present and it no longer matches disk, the write is
			// rejected instead of overwriting newer content. Omit to force-write.
			baseModified: t.Optional(t.String())
		}),
		response: t.Object({
			message: t.String(),
			size: t.Number(),
			modified: t.String()
		})
	}, async ({ data, conn }) => {
		const filePath = await requireFilePathAccess(conn, data.filePath);
		debug.log('file', 'Write file operation:', {
			filePath,
			contentLength: data.content.length
		});
		return await writeFileOperation(filePath, data.content, data.baseModified);
	})

	// Create file operation
	.http('files:create-file', {
		data: t.Object({
			filePath: t.String(),
			content: t.Optional(t.String())
		}),
		response: t.Object({
			message: t.String(),
			path: t.String(),
			size: t.Number(),
			modified: t.String()
		})
	}, async ({ data, conn }) => {
		const filePath = await requireFilePathAccess(conn, data.filePath);
		return await createFileOperation(filePath, data.content);
	})

	// Create directory operation
	.http('files:create-directory', {
		data: t.Object({
			dirPath: t.String()
		}),
		response: t.Object({
			message: t.String(),
			path: t.String(),
			modified: t.String()
		})
	}, async ({ data, conn }) => {
		// Uses the shared guard so FolderBrowser can create candidate project
		// folders outside any existing project, while still preventing writes
		// inside another user's project.
		const dirPath = await requireSharedFilePathAccess(conn, data.dirPath);
		return await createDirectoryOperation(dirPath);
	})

	// Rename operation
	.http('files:rename', {
		data: t.Object({
			oldPath: t.String(),
			newPath: t.String()
		}),
		response: t.Object({
			message: t.String(),
			oldPath: t.String(),
			newPath: t.String(),
			modified: t.String()
		})
	}, async ({ data, conn }) => {
		const oldPath = await requireFilePathAccess(conn, data.oldPath);
		const newPath = await requireFilePathAccess(conn, data.newPath);
		return await renameOperation(oldPath, newPath);
	})

	// Duplicate operation
	.http('files:duplicate', {
		data: t.Object({
			sourcePath: t.String(),
			targetPath: t.String()
		}),
		response: t.Object({
			message: t.String(),
			sourcePath: t.String(),
			targetPath: t.String(),
			size: t.Number(),
			modified: t.String()
		})
	}, async ({ data, conn }) => {
		const sourcePath = await requireFilePathAccess(conn, data.sourcePath);
		const targetPath = await requireFilePathAccess(conn, data.targetPath);
		return await duplicateOperation(sourcePath, targetPath);
	})

	// Uploads moved to the HTTP route POST /api/files/upload — the WS path
	// kept getting dropped by the Vite dev proxy with `write EPIPE` on
	// sustained binary transfers (any chunk size). See backend/http/files-upload.ts.

	// Zip operation — compress a set of files/directories into a single archive
	.http('files:zip', {
		data: t.Object({
			sourcePaths: t.Array(t.String()),
			targetPath: t.String()
		}),
		response: t.Object({
			message: t.String(),
			path: t.String(),
			size: t.Number(),
			modified: t.String()
		})
	}, async ({ data, conn }) => {
		if (data.sourcePaths.length === 0) {
			throw new Error('At least one source path is required');
		}
		const resolvedSources: string[] = [];
		for (const p of data.sourcePaths) {
			resolvedSources.push(await requireFilePathAccess(conn, p));
		}
		const resolvedTarget = await requireFilePathAccess(conn, data.targetPath);
		return await createZipOperation(resolvedSources, resolvedTarget);
	})

	// Extract operation — extract a zip archive into a target directory
	.http('files:extract', {
		data: t.Object({
			archivePath: t.String(),
			targetDir: t.String()
		}),
		response: t.Object({
			message: t.String(),
			path: t.String(),
			entries: t.Number(),
			modified: t.String()
		})
	}, async ({ data, conn }) => {
		const archivePath = await requireFilePathAccess(conn, data.archivePath);
		const targetDir = await requireFilePathAccess(conn, data.targetDir);
		return await extractZipOperation(archivePath, targetDir);
	})

	// Delete operation
	.http('files:delete', {
		data: t.Object({
			filePath: t.String(),
			force: t.Optional(t.Boolean())
		}),
		response: t.Object({
			message: t.String(),
			path: t.String()
		})
	}, async ({ data, conn }) => {
		const filePath = await requireFilePathAccess(conn, data.filePath);
		return await deleteOperation(filePath, data.force);
	})

	// Delete an empty directory from FolderBrowser. Restricted to:
	// - real directory (not a file)
	// - empty contents (no force, no recursive)
	// - shared guard (outside all projects, or inside the user's own project)
	.http('files:delete-directory', {
		data: t.Object({
			dirPath: t.String()
		}),
		response: t.Object({
			message: t.String(),
			path: t.String()
		})
	}, async ({ data, conn }) => {
		const dirPath = await requireSharedFilePathAccess(conn, data.dirPath);

		const stats = await fsStat(dirPath);
		if (!stats.isDirectory()) {
			throw new Error('Path is not a directory');
		}
		const entries = await fsReaddir(dirPath);
		if (entries.length > 0) {
			throw new Error('Directory is not empty');
		}

		return await deleteOperation(dirPath, false);
	})

	// Rename a directory from FolderBrowser (e.g. fix typo on a candidate
	// project folder). Restricted to real directories on both sides and
	// guarded by the shared path access policy. Uses fs.rename directly
	// because Bun.file(...).exists() returns false for directories, which
	// makes the generic renameOperation unusable here.
	.http('files:rename-directory', {
		data: t.Object({
			oldPath: t.String(),
			newPath: t.String()
		}),
		response: t.Object({
			message: t.String(),
			oldPath: t.String(),
			newPath: t.String(),
			modified: t.String()
		})
	}, async ({ data, conn }) => {
		const oldPath = await requireSharedFilePathAccess(conn, data.oldPath);
		const newPath = await requireSharedFilePathAccess(conn, data.newPath);

		let oldStats;
		try {
			oldStats = await fsStat(oldPath);
		} catch {
			throw new Error('Source path does not exist');
		}
		if (!oldStats.isDirectory()) {
			throw new Error('Source path is not a directory');
		}

		// Reject only when the destination is a genuinely different entry. On
		// case-insensitive filesystems (default on macOS and Windows) a
		// case-only rename like "Foo" -> "foo" makes the destination resolve to
		// the same inode as the source, so allow that case through.
		try {
			const newStats = await fsStat(newPath);
			const sameEntry = newStats.ino === oldStats.ino && newStats.dev === oldStats.dev;
			if (!sameEntry) {
				throw new Error('Destination path already exists');
			}
		} catch (err) {
			if (err instanceof Error && err.message === 'Destination path already exists') {
				throw err;
			}
			// fsStat threw because newPath does not exist — that is what we want.
		}

		await fsRename(oldPath, newPath);
		const newStats = await fsStat(newPath);

		return {
			message: 'Directory renamed successfully',
			oldPath,
			newPath,
			modified: newStats.mtime.toISOString()
		};
	});
