/**
 * S3-Compatible Storage Implementation
 *
 * Uses the AWS SDK v3 for S3 operations.
 * Works with AWS S3, Cloudflare R2, Minio, and other S3-compatible services.
 */

import {
	S3Client,
	PutObjectCommand,
	GetObjectCommand,
	DeleteObjectCommand,
	HeadObjectCommand,
	ListObjectsV2Command,
	type ListObjectsV2Response,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

import type {
	Storage,
	S3StorageConfig,
	UploadResult,
	DownloadResult,
	ListResult,
	ListOptions,
	SignedUploadUrl,
	SignedUploadOptions,
} from "./types.js";
import { EmDashStorageError } from "./types.js";

const TRAILING_SLASH_PATTERN = /\/$/;

/** Type guard for AWS SDK errors (have a `name` property) */
function hasErrorName(error: unknown): error is Error & { name: string } {
	return error instanceof Error && typeof error.name === "string";
}

/**
 * S3-compatible storage implementation
 */
export class S3Storage implements Storage {
	private client: S3Client;
	private bucket: string;
	private publicUrl?: string;
	private endpoint: string;

	constructor(config: S3StorageConfig) {
		this.bucket = config.bucket;
		this.publicUrl = config.publicUrl;
		this.endpoint = config.endpoint;

		this.client = new S3Client({
			endpoint: config.endpoint,
			region: config.region || "auto",
			credentials: {
				accessKeyId: config.accessKeyId,
				secretAccessKey: config.secretAccessKey,
			},
			// Required for R2 and some S3-compatible services
			forcePathStyle: true,
		});
	}

	async upload(options: {
		key: string;
		body: Buffer | Uint8Array | ReadableStream<Uint8Array>;
		contentType: string;
	}): Promise<UploadResult> {
		try {
			// Convert ReadableStream to Buffer if needed
			let body: Buffer | Uint8Array;
			if (options.body instanceof ReadableStream) {
				const chunks: Uint8Array[] = [];
				const reader = options.body.getReader();
				while (true) {
					const { done, value } = await reader.read();
					if (done) break;
					chunks.push(value);
				}
				body = Buffer.concat(chunks);
			} else {
				body = options.body;
			}

			await this.client.send(
				new PutObjectCommand({
					Bucket: this.bucket,
					Key: options.key,
					Body: body,
					ContentType: options.contentType,
				}),
			);

			return {
				key: options.key,
				url: this.getPublicUrl(options.key),
				size: body.length,
			};
		} catch (error) {
			throw new EmDashStorageError(
				`Failed to upload file: ${options.key}`,
				"UPLOAD_FAILED",
				error,
			);
		}
	}

	async download(key: string): Promise<DownloadResult> {
		try {
			const response = await this.client.send(
				new GetObjectCommand({
					Bucket: this.bucket,
					Key: key,
				}),
			);

			if (!response.Body) {
				throw new EmDashStorageError(`File not found: ${key}`, "NOT_FOUND");
			}

			// Convert SDK stream to web ReadableStream
			const body = response.Body.transformToWebStream();

			return {
				body,
				contentType: response.ContentType || "application/octet-stream",
				size: response.ContentLength || 0,
			};
		} catch (error) {
			if (
				error instanceof EmDashStorageError ||
				(hasErrorName(error) && error.name === "NoSuchKey")
			) {
				throw new EmDashStorageError(`File not found: ${key}`, "NOT_FOUND", error);
			}
			throw new EmDashStorageError(`Failed to download file: ${key}`, "DOWNLOAD_FAILED", error);
		}
	}

	async delete(key: string): Promise<void> {
		try {
			await this.client.send(
				new DeleteObjectCommand({
					Bucket: this.bucket,
					Key: key,
				}),
			);
		} catch (error) {
			// S3 delete is idempotent, so we ignore "not found" errors
			if (!hasErrorName(error) || error.name !== "NoSuchKey") {
				throw new EmDashStorageError(`Failed to delete file: ${key}`, "DELETE_FAILED", error);
			}
		}
	}

	async exists(key: string): Promise<boolean> {
		try {
			await this.client.send(
				new HeadObjectCommand({
					Bucket: this.bucket,
					Key: key,
				}),
			);
			return true;
		} catch (error) {
			if (hasErrorName(error) && error.name === "NotFound") {
				return false;
			}
			throw new EmDashStorageError(
				`Failed to check file existence: ${key}`,
				"HEAD_FAILED",
				error,
			);
		}
	}

	async list(options: ListOptions = {}): Promise<ListResult> {
		try {
			// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- S3 client.send returns generic output; narrowing to ListObjectsV2Response
			const response = (await this.client.send(
				new ListObjectsV2Command({
					Bucket: this.bucket,
					Prefix: options.prefix,
					MaxKeys: options.limit,
					ContinuationToken: options.cursor,
				}),
			)) as ListObjectsV2Response;

			return {
				files: (response.Contents || []).map(
					(item: { Key?: string; Size?: number; LastModified?: Date; ETag?: string }) => ({
						key: item.Key!,
						size: item.Size || 0,
						lastModified: item.LastModified || new Date(),
						etag: item.ETag,
					}),
				),
				nextCursor: response.NextContinuationToken,
			};
		} catch (error) {
			throw new EmDashStorageError("Failed to list files", "LIST_FAILED", error);
		}
	}

	async getSignedUploadUrl(options: SignedUploadOptions): Promise<SignedUploadUrl> {
		try {
			const expiresIn = options.expiresIn || 3600; // 1 hour default

			const command = new PutObjectCommand({
				Bucket: this.bucket,
				Key: options.key,
				ContentType: options.contentType,
				ContentLength: options.size,
			});

			const url = await getSignedUrl(this.client, command, { expiresIn });

			const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();

			return {
				url,
				method: "PUT",
				headers: {
					"Content-Type": options.contentType,
					...(options.size ? { "Content-Length": String(options.size) } : {}),
				},
				expiresAt,
			};
		} catch (error) {
			throw new EmDashStorageError(
				`Failed to generate signed URL for: ${options.key}`,
				"SIGNED_URL_FAILED",
				error,
			);
		}
	}

	getPublicUrl(key: string): string {
		if (this.publicUrl) {
			return `${this.publicUrl.replace(TRAILING_SLASH_PATTERN, "")}/${key}`;
		}
		// Default to endpoint + bucket + key
		return `${this.endpoint.replace(TRAILING_SLASH_PATTERN, "")}/${this.bucket}/${key}`;
	}
}

/**
 * Create S3 storage adapter
 * This is the factory function called at runtime
 */
export function createStorage(config: Record<string, unknown>): Storage {
	const { endpoint, bucket, accessKeyId, secretAccessKey, region, publicUrl } = config;
	if (
		typeof endpoint !== "string" ||
		typeof bucket !== "string" ||
		typeof accessKeyId !== "string" ||
		typeof secretAccessKey !== "string"
	) {
		throw new Error(
			"S3Storage requires 'endpoint', 'bucket', 'accessKeyId', and 'secretAccessKey' string config values",
		);
	}
	return new S3Storage({
		endpoint,
		bucket,
		accessKeyId,
		secretAccessKey,
		region: typeof region === "string" ? region : undefined,
		publicUrl: typeof publicUrl === "string" ? publicUrl : undefined,
	});
}
