/**
 * Browser/React Native-safe blob storage client.
 *
 * Wraps the platform-service blob endpoints to provide:
 * - SAS URL generation for direct upload/download
 * - Direct blob upload via SAS URL (PUT with raw body)
 * - Direct blob download via SAS URL
 * - Blob listing and metadata
 *
 * Requires a fetch-compatible environment (browser, React Native, Node 18+).
 *
 * @example
 * ```ts
 * import { createBlobClient } from '@bytelyst/blob-client';
 *
 * const blob = createBlobClient({
 *   baseUrl: 'http://localhost:4003/api',
 *   productId: 'nomgap',
 *   getAccessToken: () => authClient.getAccessToken(),
 * });
 *
 * // Upload a file
 * const { url } = await blob.upload('attachments', file, {
 *   contentType: 'image/jpeg',
 *   blobName: 'nomgap/user123/photos/meal.jpg',
 * });
 *
 * // Download a file
 * const data = await blob.download('attachments', 'nomgap/user123/photos/meal.jpg');
 *
 * // List blobs
 * const { blobs } = await blob.list('attachments', { prefix: 'nomgap/user123/' });
 * ```
 */

// ── Types ────────────────────────────────────────────────────

export interface BlobClientConfig {
  /** Platform-service base URL (e.g. "http://localhost:4003/api"). */
  baseUrl: string;

  /** Product identifier sent as x-product-id header. */
  productId: string;

  /** Function that returns the current access token, or null. */
  getAccessToken: () => string | null;

  /** Request timeout in milliseconds for API calls. Default: 15000. */
  timeoutMs?: number;

  /** Upload timeout in milliseconds for direct blob uploads. Default: 120000. */
  uploadTimeoutMs?: number;
}

export interface SasUrlResponse {
  sasUrl: string;
  container: string;
  blobName: string;
  permissions: string;
  expiresInMinutes: number;
  expiresAt: string;
}

export interface BlobInfo {
  name: string;
  container: string;
  contentType?: string;
  size: number;
  lastModified?: string;
  url: string;
  metadata: Record<string, string>;
}

export interface ListBlobsResponse {
  blobs: BlobInfo[];
  count: number;
  container: string;
  prefix: string | null;
}

export interface UploadOptions {
  /** Content-Type of the blob (e.g. "image/jpeg", "application/pdf"). */
  contentType: string;

  /** Full blob path. If omitted, auto-generated as `<productId>/<userId>/<timestamp>-<random>`. */
  blobName?: string;

  /** SAS token expiry in minutes. Default: 30. */
  expiresInMinutes?: number;
}

export interface UploadResult {
  sasUrl: string;
  container: string;
  blobName: string;
}

export interface BlobClient {
  /** Get a SAS URL for direct upload or download. */
  getSasUrl(
    container: string,
    blobName: string,
    permissions?: 'r' | 'w' | 'rw' | 'rwc',
    expiresInMinutes?: number
  ): Promise<SasUrlResponse>;

  /** Upload a blob via SAS URL (requests SAS, then PUTs directly to Azure). */
  upload(
    container: string,
    data: Blob | ArrayBuffer | Uint8Array | string,
    options: UploadOptions
  ): Promise<UploadResult>;

  /** Download a blob via SAS URL. Returns the Response for streaming. */
  download(container: string, blobName: string): Promise<Response>;

  /** List blobs in a container. */
  list(
    container: string,
    options?: { prefix?: string; limit?: number }
  ): Promise<ListBlobsResponse>;

  /** Get blob metadata/info. */
  info(container: string, blobName: string): Promise<BlobInfo>;
}

// ── Errors ───────────────────────────────────────────────────

export class BlobApiError extends Error {
  constructor(
    public readonly status: number,
    public readonly body: unknown,
    message?: string
  ) {
    super(message ?? `Blob API error ${status}`);
    this.name = 'BlobApiError';
  }
}

export class BlobUploadError extends Error {
  constructor(
    public readonly status: number,
    message?: string
  ) {
    super(message ?? `Blob upload failed with status ${status}`);
    this.name = 'BlobUploadError';
  }
}

// ── UUID helper ──────────────────────────────────────────────

function uuid(): string {
  if (typeof globalThis.crypto?.randomUUID === 'function') {
    return globalThis.crypto.randomUUID();
  }
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    const r = (Math.random() * 16) | 0;
    return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
  });
}

// ── Factory ──────────────────────────────────────────────────

export function createBlobClient(config: BlobClientConfig): BlobClient {
  const {
    baseUrl,
    productId,
    getAccessToken,
    timeoutMs = 15_000,
    uploadTimeoutMs = 120_000,
  } = config;

  function authHeaders(): Record<string, string> {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'x-product-id': productId,
      'x-request-id': uuid(),
    };
    const token = getAccessToken();
    if (token) headers['Authorization'] = `Bearer ${token}`;
    return headers;
  }

  async function apiRequest<T>(method: string, path: string, body?: unknown): Promise<T> {
    const url = `${baseUrl}${path}`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);

    try {
      const res = await globalThis.fetch(url, {
        method,
        headers: authHeaders(),
        body: body != null ? JSON.stringify(body) : undefined,
        signal: controller.signal,
      });

      const json = await res.json().catch(() => ({}));

      if (!res.ok) {
        throw new BlobApiError(
          res.status,
          json,
          (json as Record<string, string>).message ?? `HTTP ${res.status}`
        );
      }

      return json as T;
    } finally {
      clearTimeout(timer);
    }
  }

  async function getSasUrl(
    container: string,
    blobName: string,
    permissions: 'r' | 'w' | 'rw' | 'rwc' = 'r',
    expiresInMinutes = 60
  ): Promise<SasUrlResponse> {
    return apiRequest<SasUrlResponse>('POST', '/blob/sas', {
      container,
      blobName,
      permissions,
      expiresInMinutes,
    });
  }

  async function upload(
    container: string,
    data: Blob | ArrayBuffer | Uint8Array | string,
    options: UploadOptions
  ): Promise<UploadResult> {
    const blobName = options.blobName ?? `${productId}/${Date.now()}-${uuid().slice(0, 8)}`;

    const sas = await getSasUrl(container, blobName, 'w', options.expiresInMinutes ?? 30);

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), uploadTimeoutMs);

    try {
      const res = await globalThis.fetch(sas.sasUrl, {
        method: 'PUT',
        headers: {
          'x-ms-blob-type': 'BlockBlob',
          'Content-Type': options.contentType,
        },
        body: data as BodyInit,
        signal: controller.signal,
      });

      if (!res.ok) {
        throw new BlobUploadError(res.status, `Upload to Azure failed: HTTP ${res.status}`);
      }

      return { sasUrl: sas.sasUrl.split('?')[0], container, blobName };
    } finally {
      clearTimeout(timer);
    }
  }

  async function download(container: string, blobName: string): Promise<Response> {
    const sas = await getSasUrl(container, blobName, 'r', 15);

    const res = await globalThis.fetch(sas.sasUrl);
    if (!res.ok) {
      throw new BlobApiError(res.status, null, `Download failed: HTTP ${res.status}`);
    }
    return res;
  }

  async function list(
    container: string,
    options?: { prefix?: string; limit?: number }
  ): Promise<ListBlobsResponse> {
    const params = new URLSearchParams({ container });
    if (options?.prefix) params.set('prefix', options.prefix);
    if (options?.limit) params.set('limit', String(options.limit));

    return apiRequest<ListBlobsResponse>('GET', `/blob/list?${params.toString()}`);
  }

  async function info(container: string, blobName: string): Promise<BlobInfo> {
    return apiRequest<BlobInfo>(
      'GET',
      `/blob/info/${encodeURIComponent(container)}/${encodeURIComponent(blobName)}`
    );
  }

  return { getSasUrl, upload, download, list, info };
}
