import {
  MAX_BOOK_BYTES,
  validateBookFile,
  type BookFileErrorCode,
} from "../domain/file-validation.ts";
import type { BookFormat } from "../domain/types.ts";

export const MULTIPART_PART_BYTES = 8 * 1024 * 1024;
export const UPLOAD_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;

export type UploadStatus =
  | "created"
  | "uploading"
  | "completing"
  | "complete"
  | "aborted";

export interface UploadedPart {
  partNumber: number;
  etag: string;
}

export interface UploadSession {
  id: string;
  r2UploadId: string;
  objectKey: string;
  fileName: string;
  mimeType: string;
  format: BookFormat;
  totalBytes: number;
  partSize: number;
  partCount: number;
  uploadedParts: UploadedPart[];
  status: UploadStatus;
  expiresAt: string;
  createdAt: string;
  updatedAt: string;
}

export interface UploadSessionStore {
  create(session: UploadSession): Promise<void>;
  get(id: string): Promise<UploadSession | null>;
  save(session: UploadSession): Promise<void>;
  findExpired(expiresBefore: string, limit?: number): Promise<UploadSession[]>;
}

interface MultipartUploadHandle {
  uploadPart(partNumber: number, value: ArrayBuffer | ArrayBufferView | Blob | ReadableStream): Promise<UploadedPart>;
  complete(parts: UploadedPart[]): Promise<{ key: string; size?: number }>;
  abort(): Promise<void>;
}

export interface MultipartBucket {
  createMultipartUpload(key: string, options?: unknown): Promise<{
    key: string;
    uploadId: string;
  }>;
  resumeMultipartUpload(key: string, uploadId: string): MultipartUploadHandle;
}

export type UploadErrorCode =
  | BookFileErrorCode
  | "upload_not_found"
  | "upload_expired"
  | "upload_closed"
  | "invalid_metadata"
  | "invalid_part_number"
  | "wrong_part_size"
  | "missing_parts";

export class UploadError extends Error {
  readonly code: UploadErrorCode;

  constructor(code: UploadErrorCode, message: string) {
    super(message);
    this.name = "UploadError";
    this.code = code;
  }
}

function formatFromName(fileName: string): BookFormat | null {
  const extension = fileName.toLowerCase().split(".").at(-1);
  return extension === "pdf" || extension === "epub" ? extension : null;
}

function safeFileName(fileName: string): string {
  const normalized = fileName.normalize("NFKC").trim();
  const extension = normalized.toLowerCase().endsWith(".epub") ? ".epub" : ".pdf";
  const stem = normalized.slice(0, -extension.length)
    .replace(/[^\p{Letter}\p{Number}._-]+/gu, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 80);
  return `${stem || "book"}${extension}`;
}

function assertMetadata(input: {
  fileName: string;
  mimeType: string;
  totalBytes: number;
}): BookFormat {
  const format = formatFromName(input.fileName);
  if (!format) {
    throw new UploadError("invalid_metadata", "第一版只支持 EPUB 和 PDF。");
  }
  if (!Number.isInteger(input.totalBytes) || input.totalBytes <= 0) {
    throw new UploadError("invalid_metadata", "文件大小必须是正整数。");
  }
  if (input.totalBytes > MAX_BOOK_BYTES) {
    throw new UploadError("file_too_large", "文件超过 200 MB。");
  }
  const allowedMimes = format === "pdf"
    ? ["application/pdf", "application/octet-stream", ""]
    : ["application/epub+zip", "application/zip", "application/x-zip-compressed", "application/octet-stream", ""];
  if (!allowedMimes.includes(input.mimeType.toLowerCase())) {
    throw new UploadError("invalid_metadata", "文件 MIME 类型与扩展名不一致。");
  }
  return format;
}

function expectedPartBytes(session: UploadSession, partNumber: number): number {
  if (partNumber < 1 || partNumber > session.partCount) {
    throw new UploadError("invalid_part_number", "分片序号超出范围。");
  }
  if (partNumber < session.partCount) return session.partSize;
  return session.totalBytes - session.partSize * (session.partCount - 1);
}

function byteLength(value: ArrayBuffer | ArrayBufferView | Blob): number {
  if (value instanceof ArrayBuffer) return value.byteLength;
  if (ArrayBuffer.isView(value)) return value.byteLength;
  return value.size;
}

async function headerBytes(value: ArrayBuffer | ArrayBufferView | Blob): Promise<Uint8Array> {
  if (value instanceof Blob) {
    return new Uint8Array(await value.slice(0, 16).arrayBuffer());
  }
  if (value instanceof ArrayBuffer) return new Uint8Array(value.slice(0, 16));
  return new Uint8Array(value.buffer, value.byteOffset, Math.min(16, value.byteLength));
}

function mapValidationError(error: unknown): never {
  if (
    error instanceof Error &&
    "code" in error &&
    typeof error.code === "string"
  ) {
    throw new UploadError(error.code as BookFileErrorCode, error.message);
  }
  throw error;
}

export function createUploadCoordinator(dependencies: {
  store: UploadSessionStore;
  bucket: MultipartBucket;
  assertStorageAdmission?(bytes: number): Promise<void>;
  now?: () => Date;
  randomId?: () => string;
}) {
  const now = dependencies.now ?? (() => new Date());
  const randomId = dependencies.randomId ?? (() => crypto.randomUUID());

  async function cleanupExpiredUploads(expiresBefore: Date): Promise<void> {
    const expired = await dependencies.store.findExpired(
      expiresBefore.toISOString(),
      25,
    );
    for (const session of expired) {
      try {
        await dependencies.bucket
          .resumeMultipartUpload(session.objectKey, session.r2UploadId)
          .abort();
        await dependencies.store.save({
          ...session,
          status: "aborted",
          updatedAt: expiresBefore.toISOString(),
        });
      } catch {
        // Leave the reservation active when R2 could not confirm the abort.
      }
    }
  }

  async function load(id: string): Promise<UploadSession> {
    const session = await dependencies.store.get(id);
    if (!session) {
      throw new UploadError("upload_not_found", "找不到该上传任务。");
    }
    if (Date.parse(session.expiresAt) <= now().getTime() && session.status !== "complete") {
      throw new UploadError("upload_expired", "上传任务已过期，请重新开始。");
    }
    return session;
  }

  return {
    async create(input: {
      fileName: string;
      mimeType: string;
      totalBytes: number;
    }): Promise<UploadSession> {
      const format = assertMetadata(input);
      const timestamp = now();
      await cleanupExpiredUploads(timestamp);
      await dependencies.assertStorageAdmission?.(input.totalBytes);
      const id = randomId();
      const objectKey = `sources/${id}/${safeFileName(input.fileName)}`;
      const r2Upload = await dependencies.bucket.createMultipartUpload(objectKey, {
        httpMetadata: { contentType: input.mimeType },
        customMetadata: { originalName: input.fileName, format },
      });
      const session: UploadSession = {
        id,
        r2UploadId: r2Upload.uploadId,
        objectKey,
        fileName: input.fileName,
        mimeType: input.mimeType,
        format,
        totalBytes: input.totalBytes,
        partSize: MULTIPART_PART_BYTES,
        partCount: Math.ceil(input.totalBytes / MULTIPART_PART_BYTES),
        uploadedParts: [],
        status: "created",
        expiresAt: new Date(timestamp.getTime() + UPLOAD_LIFETIME_MS).toISOString(),
        createdAt: timestamp.toISOString(),
        updatedAt: timestamp.toISOString(),
      };
      await dependencies.store.create(session);
      return session;
    },

    async status(id: string): Promise<UploadSession> {
      return load(id);
    },

    async uploadPart(
      id: string,
      partNumber: number,
      value: ArrayBuffer | ArrayBufferView | Blob,
    ): Promise<UploadedPart> {
      const session = await load(id);
      if (session.status === "complete" || session.status === "aborted") {
        throw new UploadError("upload_closed", "该上传任务已经结束。");
      }
      const expectedBytes = expectedPartBytes(session, partNumber);
      if (byteLength(value) !== expectedBytes) {
        throw new UploadError(
          "wrong_part_size",
          `第 ${partNumber} 片应为 ${expectedBytes} 字节。`,
        );
      }
      if (partNumber === 1) {
        try {
          validateBookFile({
            name: session.fileName,
            size: session.totalBytes,
            mimeType: session.mimeType,
            header: await headerBytes(value),
          });
        } catch (error) {
          mapValidationError(error);
        }
      }

      const multipart = dependencies.bucket.resumeMultipartUpload(
        session.objectKey,
        session.r2UploadId,
      );
      const part = await multipart.uploadPart(partNumber, value);
      const uploadedParts = session.uploadedParts.filter(
        (entry) => entry.partNumber !== partNumber,
      );
      uploadedParts.push({ partNumber: part.partNumber, etag: part.etag });
      uploadedParts.sort((left, right) => left.partNumber - right.partNumber);
      await dependencies.store.save({
        ...session,
        status: "uploading",
        uploadedParts,
        updatedAt: now().toISOString(),
      });
      return part;
    },

    async complete(id: string): Promise<UploadSession> {
      const session = await load(id);
      if (session.status === "complete") return session;
      if (session.status === "aborted") {
        throw new UploadError("upload_closed", "该上传任务已取消。");
      }
      const expectedParts = Array.from(
        { length: session.partCount },
        (_, index) => index + 1,
      );
      if (
        session.uploadedParts.length !== session.partCount ||
        session.uploadedParts.some(
          (part, index) => part.partNumber !== expectedParts[index],
        )
      ) {
        throw new UploadError("missing_parts", "仍有分片没有上传完成。");
      }

      const completing = {
        ...session,
        status: "completing" as const,
        updatedAt: now().toISOString(),
      };
      await dependencies.store.save(completing);
      const multipart = dependencies.bucket.resumeMultipartUpload(
        session.objectKey,
        session.r2UploadId,
      );
      await multipart.complete(session.uploadedParts);
      const complete = {
        ...completing,
        status: "complete" as const,
        updatedAt: now().toISOString(),
      };
      await dependencies.store.save(complete);
      return complete;
    },

    async abort(id: string): Promise<UploadSession> {
      const session = await load(id);
      if (session.status === "complete") {
        throw new UploadError("upload_closed", "已完成的上传不能取消。");
      }
      if (session.status === "aborted") return session;
      const multipart = dependencies.bucket.resumeMultipartUpload(
        session.objectKey,
        session.r2UploadId,
      );
      await multipart.abort();
      const aborted = {
        ...session,
        status: "aborted" as const,
        updatedAt: now().toISOString(),
      };
      await dependencies.store.save(aborted);
      return aborted;
    },
  };
}
