/**
 * S3-compatible backup transport — pushes the bundle to any S3 API endpoint
 * (AWS S3, Backblaze B2, Wasabi, Cloudflare R2, MinIO). Reuses the `minio`
 * client the storage backend already depends on, so no new SDK.
 */

import path from 'node:path';

import { Client as MinioClient } from 'minio';

import {
  BACKUP_NAME_PREFIX,
  sortBundlesNewestFirst,
  type BackupTransport,
  type S3DestConfig,
} from './types';

/** Split a configured endpoint (host or URL) into minio's endPoint/port/useSSL. */
export function parseS3Endpoint(
  endpoint: string,
  cfg: { useSSL?: boolean; port?: number },
): { endPoint: string; port?: number; useSSL: boolean } {
  let host = endpoint.trim();
  let useSSL = cfg.useSSL ?? true;
  let port = cfg.port;
  const m = /^(https?):\/\/([^/:]+)(?::(\d+))?/i.exec(host);
  if (m) {
    useSSL = m[1]!.toLowerCase() === 'https';
    host = m[2]!;
    if (m[3]) port = Number(m[3]);
  } else {
    host = host.replace(/\/.*$/, '');
  }
  return { endPoint: host, ...(port ? { port } : {}), useSSL };
}

export class S3Transport implements BackupTransport {
  private readonly prefix: string;

  constructor(private readonly cfg: S3DestConfig) {
    // Normalise prefix to "" or "dir/".
    const p = (cfg.prefix ?? '').replace(/^\/+|\/+$/g, '');
    this.prefix = p ? `${p}/` : '';
  }

  private client(): MinioClient {
    const { endPoint, port, useSSL } = parseS3Endpoint(this.cfg.endpoint, {
      useSSL: this.cfg.useSSL,
      port: this.cfg.port,
    });
    return new MinioClient({
      endPoint,
      ...(port ? { port } : {}),
      useSSL,
      accessKey: this.cfg.accessKey,
      secretKey: this.cfg.secretKey,
      ...(this.cfg.region ? { region: this.cfg.region } : {}),
    });
  }

  async test(): Promise<void> {
    const exists = await this.client().bucketExists(this.cfg.bucket);
    if (!exists) throw new Error(`Bucket not found or not accessible: ${this.cfg.bucket}`);
  }

  async push(localPath: string, remoteName: string): Promise<{ remoteRef: string; bytes: number }> {
    const key = `${this.prefix}${remoteName}`;
    await this.client().fPutObject(this.cfg.bucket, key, localPath, {
      'Content-Type': 'application/x-tar',
    });
    const { stat } = await import('node:fs/promises');
    const s = await stat(localPath);
    return { remoteRef: `s3://${this.cfg.bucket}/${key}`, bytes: s.size };
  }

  async prune(retentionCount: number | null): Promise<{ deleted: number }> {
    if (retentionCount === null || retentionCount < 0) return { deleted: 0 };
    const client = this.client();
    const names = await this.listBundleKeys(client);
    const sorted = sortBundlesNewestFirst(names.map((k) => path.posix.basename(k)));
    const keepBasenames = new Set(sorted.slice(0, retentionCount));
    const toDelete = names.filter(
      (k) =>
        path.posix.basename(k).startsWith(BACKUP_NAME_PREFIX) &&
        !keepBasenames.has(path.posix.basename(k)),
    );
    for (const key of toDelete) await client.removeObject(this.cfg.bucket, key);
    return { deleted: toDelete.length };
  }

  private listBundleKeys(client: MinioClient): Promise<string[]> {
    return new Promise((resolve, reject) => {
      const keys: string[] = [];
      const stream = client.listObjectsV2(this.cfg.bucket, this.prefix, true);
      stream.on('data', (obj) => {
        if (obj.name && path.posix.basename(obj.name).startsWith(BACKUP_NAME_PREFIX)) {
          keys.push(obj.name);
        }
      });
      stream.on('error', reject);
      stream.on('end', () => resolve(keys));
    });
  }
}
