/**
 * StorageService — the seam between the app and local disk.
 *
 * Callers deal only in object keys and public URLs; they never touch
 * the filesystem or path logic directly. POST /uploads is the single
 * media-upload entry point; ticket inventory then relocates keys
 * between `unused/` and `used/` via {@link move}.
 *
 * Key ↔ URL contract:
 *   - a `key` is a relative POSIX path of the form
 *     `{subdomain}/{filename}`, `{subdomain}/{used|unused}/{filename}`,
 *     `{subdomain}/{mediaFolder}/{filename}` or
 *     `deleted/{subdomain}/{mediaFolder}/{filename}`
 *     (never absolute, never containing `..`);
 *   - `url(key)` maps it to `/api/uploads/{key}` (persisted in the DB);
 *   - `keyFromUrl(url)` is the inverse (null for URLs this backend
 *     did not issue, including traversal / foreign hosts).
 */
import type { MediaFolder } from '../media/media.constants';

export interface StoredObject {
  key: string;
  url: string;
}

export abstract class StorageService {
  /**
   * Persist `body` under a freshly-generated key inside `prefix`
   * (the tenant subdomain), optionally under a media `folder`.
   * `extension` is the sniffed extension (e.g. `.pdf`) — never the
   * client filename.
   */
  abstract save(
    prefix: string,
    extension: string,
    body: Buffer,
    folder?: MediaFolder,
  ): Promise<StoredObject>;

  /** Move/rename an object (e.g. `…/unused/x` → `…/used/x`). No-op if absent. */
  abstract move(fromKey: string, toKey: string): Promise<void>;

  /** Delete an object. No-op if it doesn't exist. */
  abstract delete(key: string): Promise<void>;

  /** Whether an object currently exists. */
  abstract exists(key: string): Promise<boolean>;

  /** Public/servable URL for a key (also what we persist in the DB). */
  abstract url(key: string): string;

  /** Inverse of {@link url}. Null when `url` wasn't issued by this backend. */
  abstract keyFromUrl(url: string): string | null;
}
