"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.OxyServicesAssetsMixin = OxyServicesAssetsMixin;
var _expoFileSystem = require("expo-file-system");
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } /**
 * Asset & File Methods Mixin
 */
function OxyServicesAssetsMixin(Base) {
  return class extends Base {
    constructor(...args) {
      super(...args);
    }
    // ============================================================================
    // FILE METHODS (Convenience wrappers using Asset Service)
    // ============================================================================

    /**
     * Delete file
     */
    async deleteFile(fileId) {
      try {
        // Central Asset Service delete with force=true behavior controlled by caller via assetDelete
        return await this.makeRequest('DELETE', `/api/assets/${encodeURIComponent(fileId)}`, undefined, {
          cache: false
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Get file download URL (synchronous - uses stream endpoint for images to avoid ORB blocking)
     * The stream endpoint serves images directly with proper CORS headers, avoiding browser ORB blocking
     * For better performance with signed URLs, use getFileDownloadUrlAsync when possible
     */
    getFileDownloadUrl(fileId, variant, expiresIn) {
      const base = this.getBaseURL();
      const params = new URLSearchParams();
      if (variant) params.set('variant', variant);
      if (expiresIn) params.set('expiresIn', String(expiresIn));
      params.set('fallback', 'placeholderVisible');
      const token = this.getClient().getAccessToken();
      if (token) params.set('token', token);

      // Use stream endpoint which serves images directly with proper CORS headers
      // This avoids ERR_BLOCKED_BY_ORB errors that occur with redirect-based endpoints
      const qs = params.toString();
      return `${base}/api/assets/${encodeURIComponent(fileId)}/stream${qs ? `?${qs}` : ''}`;
    }

    /**
     * Get file download URL asynchronously (returns signed URL directly from CDN)
     * This is more efficient than the synchronous version as it avoids redirects
     * Use this when you can handle async operations (e.g., in useEffect, useMemo with async)
     */
    async getFileDownloadUrlAsync(fileId, variant, expiresIn) {
      try {
        const url = await this.fetchAssetDownloadUrl(fileId, variant, this.getAssetUrlCacheTTL(expiresIn), expiresIn);
        return url || this.getFileDownloadUrl(fileId, variant, expiresIn);
      } catch (error) {
        // Fallback to synchronous method on error
        return this.getFileDownloadUrl(fileId, variant, expiresIn);
      }
    }

    /**
     * List user files
     */
    async listUserFiles(limit, offset) {
      try {
        const paramsObj = {};
        if (limit) paramsObj.limit = limit;
        if (offset) paramsObj.offset = offset;
        return await this.makeRequest('GET', '/api/assets', paramsObj, {
          cache: false // Don't cache file lists - always get fresh data
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Get account storage usage (server-side usage aggregated from assets)
     *
     * NOTE: This is NOT the same as `getStorage()` from the language mixin (which returns local storage).
     */
    async getAccountStorageUsage() {
      try {
        return await this.makeRequest('GET', '/api/storage/usage', undefined, {
          cache: false
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Get file content as text
     */
    async getFileContentAsText(fileId, variant) {
      try {
        const downloadUrl = await this.fetchAssetDownloadUrl(fileId, variant, this.getAssetUrlCacheTTL());
        if (!downloadUrl) {
          throw new Error('No download URL returned for asset');
        }
        return await this.fetchAssetContent(downloadUrl, 'text');
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Get file content as blob
     */
    async getFileContentAsBlob(fileId, variant) {
      try {
        const downloadUrl = await this.fetchAssetDownloadUrl(fileId, variant, this.getAssetUrlCacheTTL());
        if (!downloadUrl) {
          throw new Error('No download URL returned for asset');
        }
        return await this.fetchAssetContent(downloadUrl, 'blob');
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Get batch access to multiple files
     * Returns URLs and access status for each file
     */
    async getBatchFileAccess(fileIds, context) {
      try {
        return await this.makeRequest('POST', '/api/assets/batch-access', {
          fileIds,
          context
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Get download URLs for multiple files efficiently
     */
    async getFileDownloadUrls(fileIds, context) {
      const response = await this.getBatchFileAccess(fileIds, context);
      const urls = {};
      // response.results is the map
      const results = response.results || {};
      for (const [id, result] of Object.entries(results)) {
        if (result.allowed && result.url) {
          urls[id] = result.url;
        }
      }
      return urls;
    }

    /**
     * Upload raw file data
     */
    async uploadRawFile(file, visibility, metadata) {
      // Switch to Central Asset Service upload flow
      return this.assetUpload(file, visibility, metadata);
    }

    // ============================================================================
    // CENTRAL ASSET SERVICE METHODS
    // ============================================================================

    /**
     * Get base64 string from file - uses expo-file-system when URI is available
     * Returns base64 string directly for use with expo-crypto
     */
    async getFileBase64(file) {
      // Check for URI from DocumentPicker (Expo 54)
      const uri = file.uri;
      if (uri && typeof uri === 'string') {
        // Use Expo 54 FileSystem API
        try {
          const fileInstance = new _expoFileSystem.File(uri);
          return await fileInstance.base64();
        } catch (error) {
          throw new Error(`Failed to read file from URI: ${error.message || 'Unknown error'}`);
        }
      }

      // For files without URI (web Blobs), convert to base64 using fetch
      const blobUrl = URL.createObjectURL(file);
      try {
        const response = await fetch(blobUrl);
        if (!response.ok) {
          throw new Error(`Failed to fetch file: ${response.statusText || 'HTTP ' + response.status}`);
        }
        const buffer = await response.arrayBuffer();
        // Convert ArrayBuffer to base64 using chunked approach to avoid stack overflow
        return this.arrayBufferToBase64Safe(buffer);
      } finally {
        URL.revokeObjectURL(blobUrl);
      }
    }

    /**
     * Convert File or Blob to ArrayBuffer - uses expo-file-system when URI available
     */
    async fileToArrayBuffer(file) {
      // Check for URI from DocumentPicker (Expo 54)
      const uri = file.uri;
      if (uri && typeof uri === 'string') {
        // Use Expo 54 FileSystem API
        try {
          const fileInstance = new _expoFileSystem.File(uri);
          const bytes = await fileInstance.bytes();
          return bytes.buffer;
        } catch (error) {
          throw new Error(`Failed to read file from URI: ${error.message || 'Unknown error'}`);
        }
      }

      // For files without URI, use native arrayBuffer if available, else fetch
      if (typeof file.arrayBuffer === 'function') {
        return await file.arrayBuffer();
      }

      // Fallback: fetch via blob URL
      const blobUrl = URL.createObjectURL(file);
      try {
        const response = await fetch(blobUrl);
        if (!response.ok) {
          throw new Error(`Failed to fetch file: ${response.statusText || 'HTTP ' + response.status}`);
        }
        return await response.arrayBuffer();
      } finally {
        URL.revokeObjectURL(blobUrl);
      }
    }

    /**
     * Convert ArrayBuffer to hex string
     */
    arrayBufferToHex(buffer) {
      const bytes = new Uint8Array(buffer);
      return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
    }

    /**
     * Convert base64 string to ArrayBuffer (safe for large files)
     */
    base64ToArrayBuffer(base64) {
      const binaryString = atob(base64);
      const bytes = new Uint8Array(binaryString.length);
      for (let i = 0; i < binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
      }
      return bytes.buffer;
    }

    /**
     * Convert binary string to base64 (manual implementation for Node.js when btoa is not available)
     */
    binaryToBase64(binary) {
      const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
      let result = '';
      let i = 0;
      while (i < binary.length) {
        const a = binary.charCodeAt(i++);
        const b = i < binary.length ? binary.charCodeAt(i++) : 0;
        const c = i < binary.length ? binary.charCodeAt(i++) : 0;
        const bitmap = a << 16 | b << 8 | c;
        result += base64Chars.charAt(bitmap >> 18 & 63);
        result += base64Chars.charAt(bitmap >> 12 & 63);
        result += i - 2 < binary.length ? base64Chars.charAt(bitmap >> 6 & 63) : '=';
        result += i - 1 < binary.length ? base64Chars.charAt(bitmap & 63) : '=';
      }
      return result;
    }

    /**
     * Convert ArrayBuffer to base64 string (safe chunked approach to avoid stack overflow)
     */
    arrayBufferToBase64Safe(buffer) {
      const bytes = new Uint8Array(buffer);
      const chunkSize = 8192; // Process in chunks to avoid stack overflow

      // Use chunked approach for large buffers
      if (bytes.length > chunkSize) {
        let binary = '';
        for (let i = 0; i < bytes.length; i += chunkSize) {
          const chunk = bytes.slice(i, i + chunkSize);
          binary += String.fromCharCode.apply(null, Array.from(chunk));
        }
        // Use btoa if available (browser/React Native), otherwise use manual encoding
        return typeof btoa !== 'undefined' ? btoa(binary) : this.binaryToBase64(binary);
      }

      // Small buffers can use direct conversion
      const binary = String.fromCharCode.apply(null, Array.from(bytes));
      return typeof btoa !== 'undefined' ? btoa(binary) : this.binaryToBase64(binary);
    }

    /**
     * Calculate SHA256 hash of file content - uses expo-crypto (Expo 54 unified API)
     */
    async calculateSHA256(file) {
      // Use expo-crypto (works on all platforms with Expo 54)
      const CryptoModule = await (specifier => new Promise(r => r(`${specifier}`)).then(s => _interopRequireWildcard(require(s))))('expo-crypto').catch(() => null);
      if (!CryptoModule) {
        throw new Error('expo-crypto is not available. Install it with: npx expo install expo-crypto');
      }
      const Crypto = CryptoModule.default || CryptoModule;
      if (!Crypto?.digestStringAsync) {
        throw new Error('expo-crypto.digestStringAsync is not available');
      }
      try {
        // Read file as base64 (uses expo-file-system if URI available, else converts)
        const base64 = await this.getFileBase64(file);
        const algorithm = Crypto.CryptoDigestAlgorithm?.SHA256 || 'SHA256';
        const encoding = Crypto.CryptoEncoding?.BASE64 || 'base64';
        return await Crypto.digestStringAsync(algorithm, base64, {
          encoding
        });
      } catch (error) {
        const errorMessage = error?.message || String(error) || 'Unknown error';
        throw new Error(`Failed to calculate SHA256 hash: ${errorMessage}`);
      }
    }

    /**
     * Initialize asset upload - returns pre-signed URL and file ID
     */
    async assetInit(sha256, size, mime) {
      try {
        return await this.makeRequest('POST', '/api/assets/init', {
          sha256,
          size,
          mime
        }, {
          cache: false
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Complete asset upload - commit metadata and trigger variant generation
     */
    async assetComplete(fileId, originalName, size, mime, visibility, metadata) {
      try {
        return await this.makeRequest('POST', '/api/assets/complete', {
          fileId,
          originalName,
          size,
          mime,
          visibility,
          metadata
        }, {
          cache: false
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Upload file using Central Asset Service
     */
    async assetUpload(file, visibility, metadata, onProgress) {
      const fileName = file.name || 'unknown';
      const fileSize = file.size;
      const fileType = file.type || 'application/octet-stream';
      try {
        // Calculate SHA256
        let sha256;
        try {
          sha256 = await this.calculateSHA256(file);
        } catch (error) {
          throw new Error(`Failed to calculate file hash for "${fileName}": ${error.message || 'Unknown error'}`);
        }

        // Initialize upload
        let initResponse;
        try {
          initResponse = await this.assetInit(sha256, fileSize, fileType);
        } catch (error) {
          throw new Error(`Failed to initialize upload for "${fileName}": ${error.message || 'Unknown error'}`);
        }

        // Try presigned URL first, fallback to direct upload
        try {
          await this.uploadToPresignedUrl(initResponse.uploadUrl, file, onProgress);
        } catch (uploadError) {
          // Fallback: direct upload via API to avoid CORS issues
          try {
            const fd = new FormData();
            fd.append('file', file);
            await this.getClient().request({
              method: 'POST',
              url: `/api/assets/${encodeURIComponent(initResponse.fileId)}/upload-direct`,
              data: fd,
              cache: false
            });
          } catch (directUploadError) {
            throw new Error(`Failed to upload file "${fileName}" (${(fileSize / 1024 / 1024).toFixed(2)}MB): ` + `Presigned URL failed: ${uploadError.message || 'Unknown error'}. ` + `Direct upload failed: ${directUploadError.message || 'Unknown error'}`);
          }
        }

        // Complete upload
        try {
          return await this.assetComplete(initResponse.fileId, fileName, fileSize, fileType, visibility, metadata);
        } catch (error) {
          throw new Error(`Failed to complete upload for "${fileName}": ${error.message || 'Unknown error'}`);
        }
      } catch (error) {
        // Log the original error for debugging
        console.error('File upload error:', error);

        // Preserve original error message before passing to handleError
        // This ensures we don't lose the error details
        let errorMessage = 'File upload failed';
        if (error instanceof Error) {
          errorMessage = error.message || errorMessage;
        } else if (error && typeof error === 'object') {
          // Try to extract message from various error formats
          if ('message' in error) {
            errorMessage = String(error.message) || errorMessage;
          } else if ('error' in error && typeof error.error === 'string') {
            errorMessage = error.error;
          } else if ('data' in error && error.data?.message) {
            errorMessage = String(error.data.message);
          }
        } else if (error) {
          errorMessage = String(error) || errorMessage;
        }

        // Add file context to error for better debugging
        const contextError = error;
        if (!contextError.fileContext) {
          contextError.fileContext = {
            fileName,
            fileSize,
            fileType
          };
        }

        // If the error already has a message, preserve it
        if (error instanceof Error && error.message) {
          // Pass through handleError but ensure message is preserved
          const handledError = this.handleError(contextError);
          // If handleError stripped the message, restore it
          if (!handledError.message || handledError.message.trim() === 'An unexpected error occurred') {
            handledError.message = errorMessage;
          }
          throw handledError;
        }

        // For non-Error objects, create a new Error with the message
        const newError = new Error(errorMessage);
        newError.fileContext = contextError.fileContext;
        throw this.handleError(newError);
      }
    }

    /**
     * Upload file to pre-signed URL
     */
    async uploadToPresignedUrl(url, file, onProgress) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.upload.addEventListener('progress', event => {
          if (event.lengthComputable && onProgress) {
            const progress = event.loaded / event.total * 100;
            onProgress(progress);
          }
        });
        xhr.addEventListener('load', () => {
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve();
          } else {
            reject(new Error(`Upload failed with status ${xhr.status}`));
          }
        });
        xhr.addEventListener('error', () => {
          reject(new Error('Upload failed'));
        });
        xhr.open('PUT', url);
        xhr.setRequestHeader('Content-Type', file.type);
        xhr.send(file);
      });
    }

    /**
     * Link asset to an entity
     */
    async assetLink(fileId, app, entityType, entityId, visibility, webhookUrl) {
      try {
        const body = {
          app,
          entityType,
          entityId
        };
        if (visibility) body.visibility = visibility;
        if (webhookUrl) body.webhookUrl = webhookUrl;
        return await this.makeRequest('POST', `/api/assets/${fileId}/links`, body, {
          cache: false
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Unlink asset from an entity
     */
    async assetUnlink(fileId, app, entityType, entityId) {
      try {
        return await this.makeRequest('DELETE', `/api/assets/${fileId}/links`, {
          app,
          entityType,
          entityId
        }, {
          cache: false
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Get asset metadata
     */
    async assetGet(fileId) {
      try {
        return await this.makeRequest('GET', `/api/assets/${fileId}`, undefined, {
          cache: true,
          cacheTTL: 5 * 60 * 1000 // 5 minutes cache
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Get asset URL (CDN or signed URL)
     */
    async assetGetUrl(fileId, variant, expiresIn) {
      try {
        const params = {};
        if (variant) params.variant = variant;
        if (expiresIn) params.expiresIn = expiresIn;
        return await this.makeRequest('GET', `/api/assets/${fileId}/url`, params, {
          cache: true,
          cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Restore asset from trash
     */
    async assetRestore(fileId) {
      try {
        return await this.makeRequest('POST', `/api/assets/${fileId}/restore`, undefined, {
          cache: false
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Delete asset with optional force
     */
    async assetDelete(fileId, force = false) {
      try {
        const params = force ? {
          force: 'true'
        } : undefined;
        return await this.makeRequest('DELETE', `/api/assets/${fileId}`, params, {
          cache: false
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Get list of available variants for an asset
     */
    async assetGetVariants(fileId) {
      try {
        const assetData = await this.assetGet(fileId);
        return assetData.file?.variants || [];
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Update asset visibility
     * @param fileId - The file ID
     * @param visibility - New visibility level ('private', 'public', or 'unlisted')
     * @returns Updated asset information
     */
    async assetUpdateVisibility(fileId, visibility) {
      try {
        return await this.makeRequest('PATCH', `/api/assets/${fileId}/visibility`, {
          visibility
        }, {
          cache: false
        });
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Helper: Upload and link avatar with automatic public visibility
     * @param file - The avatar file
     * @param userId - User ID to link to
     * @param app - App name (defaults to 'profiles')
     * @returns The uploaded and linked asset
     */
    async uploadAvatar(file, userId, app = 'profiles') {
      try {
        // Upload as public
        const asset = await this.assetUpload(file, 'public');

        // Link to user profile as avatar
        await this.assetLink(asset.file.id, app, 'avatar', userId, 'public');
        return asset;
      } catch (error) {
        throw this.handleError(error);
      }
    }

    /**
     * Helper: Upload and link profile banner with automatic public visibility
     * @param file - The banner file
     * @param userId - User ID to link to
     * @param app - App name (defaults to 'profiles')
     * @returns The uploaded and linked asset
     */
    async uploadProfileBanner(file, userId, app = 'profiles') {
      try {
        // Upload as public
        const asset = await this.assetUpload(file, 'public');

        // Link to user profile as banner
        await this.assetLink(asset.file.id, app, 'profile-banner', userId, 'public');
        return asset;
      } catch (error) {
        throw this.handleError(error);
      }
    }
    getAssetUrlCacheTTL(expiresIn) {
      const desiredTtlMs = (expiresIn ?? 3600) * 1000;
      return Math.min(desiredTtlMs, 10 * 60 * 1000);
    }
    async fetchAssetDownloadUrl(fileId, variant, cacheTTL, expiresIn) {
      const params = {};
      if (variant) params.variant = variant;
      if (expiresIn) params.expiresIn = expiresIn;
      const urlRes = await this.makeRequest('GET', `/api/assets/${encodeURIComponent(fileId)}/url`, Object.keys(params).length ? params : undefined, {
        cache: true,
        cacheTTL: cacheTTL ?? 10 * 60 * 1000 // default 10 minutes cache for URLs
      });
      return urlRes?.url || null;
    }
    async fetchAssetContent(url, type) {
      const response = await fetch(url);
      if (!response?.ok) {
        throw new Error(`Failed to fetch asset content (status ${response?.status})`);
      }
      return type === 'text' ? response.text() : response.blob();
    }
  };
}
//# sourceMappingURL=OxyServices.assets.js.map