import { Response } from 'express';
import { StorageService } from '@/services/storage/storage.service.js';
import { parseXml, toXml } from '../xml.js';
import { sendS3Error } from '../errors.js';
import { S3GatewayRequest, getS3Bucket } from '../request.js';

interface ParsedDelete {
  Delete?: {
    Object?: { Key?: string } | Array<{ Key?: string }>;
    Quiet?: string | boolean;
  };
}

function isNotFound(err: unknown): boolean {
  const e = err as { name?: string; Code?: string; $metadata?: { httpStatusCode?: number } };
  return (
    e?.name === 'NoSuchKey' ||
    e?.Code === 'NoSuchKey' ||
    e?.name === 'NotFound' ||
    e?.$metadata?.httpStatusCode === 404
  );
}

// AWS caps DeleteObjects at 1000 keys per request. 1 MiB of request XML is
// plenty of headroom for that while preventing a client from streaming an
// unbounded body into memory before we can reject it.
const MAX_DELETE_BODY_BYTES = 1024 * 1024;

export async function handle(req: S3GatewayRequest, res: Response): Promise<void> {
  const chunks: Buffer[] = [];
  let received = 0;
  for await (const c of req) {
    const b = c as Buffer;
    received += b.length;
    if (received > MAX_DELETE_BODY_BYTES) {
      sendS3Error(
        res,
        'EntityTooLarge',
        `Delete request body exceeds ${MAX_DELETE_BODY_BYTES} bytes`,
        {
          resource: req.path,
          requestId: req.s3Auth.requestId,
        }
      );
      req.unpipe?.();
      req.destroy?.();
      return;
    }
    chunks.push(b);
  }
  const body = Buffer.concat(chunks);

  let parsed: ParsedDelete;
  try {
    parsed = (await parseXml(body)) as ParsedDelete;
  } catch {
    sendS3Error(res, 'MalformedXML', 'Request body is not valid XML', {
      resource: req.path,
      requestId: req.s3Auth.requestId,
    });
    return;
  }

  const delBlock = parsed?.Delete ?? {};
  const quiet = delBlock.Quiet === true || delBlock.Quiet === 'true';
  let items: Array<{ Key?: string }> = [];
  if (Array.isArray(delBlock.Object)) {
    items = delBlock.Object;
  } else if (delBlock.Object) {
    items = [delBlock.Object];
  }
  const keys = items.map((i) => i.Key).filter((k): k is string => !!k);

  const bucket = getS3Bucket(req);
  const svc = StorageService.getInstance();

  const deleted: Array<{ Key: string }> = [];
  const errors: Array<{ Key: string; Code: string; Message: string }> = [];

  // Delete each object individually and track outcomes per key. Only count
  // actual deletions (and 404s, per S3 idempotency) as success; provider
  // failures become <Error> entries and their DB rows are NOT removed.
  await Promise.all(
    keys.map(async (k) => {
      try {
        await svc.getProvider().deleteObject(bucket, k);
        deleted.push({ Key: k });
      } catch (err) {
        if (isNotFound(err)) {
          deleted.push({ Key: k });
        } else {
          errors.push({
            Key: k,
            Code: 'InternalError',
            Message: err instanceof Error ? err.message : 'Delete failed',
          });
        }
      }
    })
  );

  // Only delete the DB rows whose provider-side deletes succeeded.
  const successKeys = deleted.map((d) => d.Key);
  await svc.deleteObjectRowsBatch(bucket, successKeys);

  const xml = toXml({
    DeleteResult: {
      $: { xmlns: 'http://s3.amazonaws.com/doc/2006-03-01/' },
      ...(quiet ? {} : { Deleted: deleted }),
      ...(errors.length ? { Error: errors } : {}),
    },
  });
  res.status(200).type('application/xml').send(xml);
}
