/**
 * Photo blob storage abstraction.
 *
 * Two backends, selected by config.STORAGE_BACKEND:
 *   - local : ./data/blobs/<key>          (dev default)
 *   - minio : S3 PUT/GET against MINIO_*  (prod)
 *
 * Used by both the upload path (originals) and the thumbnail worker
 * (thumbnails, previews). Keep this file the only place that knows about
 * the filesystem / MinIO split.
 */

import { mkdirSync } from 'fs';
import { readFile } from 'fs/promises';
import { join } from 'path';

import { loadConfig, type Config } from '@/app/config';

export type StorageBackend = 'local' | 'minio';

/**
 * Per-call override for talking to a remote S3 endpoint (e.g. imajin's MinIO
 * during the ETL). When provided, completely supersedes the default config.
 */
export interface MinioOverride {
  readonly endpoint: string;
  readonly accessKey: string;
  readonly secretKey: string;
  readonly bucket: string;
}

export class StorageError extends Error {
  readonly key: string;
  constructor(message: string, key: string, cause?: unknown) {
    super(message);
    this.name = 'StorageError';
    this.key = key;
    if (cause !== undefined) this.cause = cause;
  }
}

export function getStorageBucket(): string {
  const config = loadConfig();
  return config.STORAGE_BACKEND === 'minio' ? config.MINIO_BUCKET : 'local';
}

export async function writeBlob(
  key: string,
  contentType: string,
  body: ReadableStream | Buffer | Uint8Array,
  override?: MinioOverride,
): Promise<void> {
  try {
    if (override) {
      await uploadToMinioOverride(override, key, contentType, body);
      return;
    }
    const config = loadConfig();
    if (config.STORAGE_BACKEND === 'minio') {
      await uploadToMinio(config, key, contentType, body);
      return;
    }
    await writeToLocal(config.STORAGE_LOCAL_PATH, key, body);
  } catch (err) {
    throw new StorageError(`writeBlob failed for key ${key}`, key, err);
  }
}

export async function readBlob(key: string, override?: MinioOverride): Promise<Buffer> {
  try {
    if (override) {
      return await downloadFromMinioOverride(override, key);
    }
    const config = loadConfig();
    if (config.STORAGE_BACKEND === 'minio') {
      return await downloadFromMinio(config, key);
    }
    return await readFile(join(config.STORAGE_LOCAL_PATH, key));
  } catch (err) {
    throw new StorageError(`readBlob failed for key ${key}`, key, err);
  }
}

/**
 * HEAD probe for resumable ETL — returns true if the object already exists.
 * Override-only (we only use this against the target bucket during transfer).
 */
export async function headBlob(key: string, override?: MinioOverride): Promise<boolean> {
  const ovrOrConfig = override ?? defaultMinioOverride();
  if (!ovrOrConfig) return false;
  const url = `${ovrOrConfig.endpoint}/${ovrOrConfig.bucket}/${key}`;
  try {
    const res = await fetch(url, { method: 'HEAD' });
    return res.ok;
  } catch {
    return false;
  }
}

function defaultMinioOverride(): MinioOverride | null {
  const config = loadConfig();
  if (config.STORAGE_BACKEND !== 'minio') return null;
  if (!config.MINIO_ENDPOINT || !config.MINIO_ACCESS_KEY || !config.MINIO_SECRET_KEY) return null;
  return {
    endpoint: config.MINIO_ENDPOINT,
    accessKey: config.MINIO_ACCESS_KEY,
    secretKey: config.MINIO_SECRET_KEY,
    bucket: config.MINIO_BUCKET,
  };
}

async function writeToLocal(
  basePath: string,
  key: string,
  body: ReadableStream | Buffer | Uint8Array,
): Promise<void> {
  const fullPath = join(basePath, key);
  const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
  mkdirSync(dir, { recursive: true });

  if (body instanceof Buffer || body instanceof Uint8Array) {
    await Bun.write(fullPath, body);
    return;
  }

  const arr = new Uint8Array(await new Response(body).arrayBuffer());
  await Bun.write(fullPath, arr);
}

async function uploadToMinio(
  config: Config,
  key: string,
  contentType: string,
  body: ReadableStream | Buffer | Uint8Array,
): Promise<void> {
  const { MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_BUCKET } = config;
  if (!MINIO_ENDPOINT || !MINIO_ACCESS_KEY || !MINIO_SECRET_KEY) {
    throw new Error('MinIO not configured: set MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY');
  }

  let buf: Buffer;
  if (body instanceof Buffer) {
    buf = body;
  } else if (body instanceof Uint8Array) {
    buf = Buffer.from(body);
  } else {
    try {
      const arr = new Uint8Array(await new Response(body).arrayBuffer());
      buf = Buffer.from(arr);
    } catch (err) {
      throw new Error(`failed to read stream for MinIO upload (${key}): ${err instanceof Error ? err.message : String(err)}`);
    }
  }

  const url = `${MINIO_ENDPOINT}/${MINIO_BUCKET}/${key}`;
  const res = await fetch(url, {
    method: 'PUT',
    headers: { 'Content-Type': contentType, 'Content-Length': String(buf.byteLength) },
    body: new Uint8Array(buf),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(`MinIO PUT ${key} failed: ${res.status} ${text}`);
  }
}

async function downloadFromMinio(config: Config, key: string): Promise<Buffer> {
  const { MINIO_ENDPOINT, MINIO_BUCKET } = config;
  if (!MINIO_ENDPOINT) throw new Error('MinIO not configured: MINIO_ENDPOINT missing');
  const url = `${MINIO_ENDPOINT}/${MINIO_BUCKET}/${key}`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`MinIO GET ${key} failed: ${res.status}`);
  const arr = new Uint8Array(await res.arrayBuffer());
  return Buffer.from(arr);
}

async function uploadToMinioOverride(
  override: MinioOverride,
  key: string,
  contentType: string,
  body: ReadableStream | Buffer | Uint8Array,
): Promise<void> {
  let buf: Buffer;
  if (body instanceof Buffer) {
    buf = body;
  } else if (body instanceof Uint8Array) {
    buf = Buffer.from(body);
  } else {
    const arr = new Uint8Array(await new Response(body).arrayBuffer());
    buf = Buffer.from(arr);
  }
  const url = `${override.endpoint}/${override.bucket}/${key}`;
  const res = await fetch(url, {
    method: 'PUT',
    headers: { 'Content-Type': contentType, 'Content-Length': String(buf.byteLength) },
    body: new Uint8Array(buf),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(`MinIO PUT ${key} → ${override.endpoint}/${override.bucket} failed: ${res.status} ${text}`);
  }
}

async function downloadFromMinioOverride(override: MinioOverride, key: string): Promise<Buffer> {
  const url = `${override.endpoint}/${override.bucket}/${key}`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`MinIO GET ${key} ← ${override.endpoint}/${override.bucket} failed: ${res.status}`);
  const arr = new Uint8Array(await res.arrayBuffer());
  return Buffer.from(arr);
}
