/**
 * Media Service v2 hook.
 *
 * Base URL: `<API_BASE>/media/v2`
 *
 * Endpoint prefixes (auto-selected based on context):
 * - Personal: `/files/`
 * - Instructor: `/instructor/files/`
 * - Enterprise: `/enterprise/files/`
 * - Instructor (enterprise context): `/instructor/{enterprise_id}/files/`
 *
 * Basic usage (context auto-detected from cookies):
 * const media = useMediaServiceV2();
 *
 * Create + upload (single PUT):
 * const { data } = await media.createFiles({
 *   tag: "lesson",
 *   caption: "Lesson 1",
 *   duration: "00:01:30",
 *   lecture_id: "<lecture_id>",
 *   files: [{ name: file.name, mimetype: file.type, size: file.size, upload_type: "put" }],
 * });
 * const upload = data.results[0];
 * await media.uploadToPresignedUrl({
 *   url: upload.upload_url,
 *   file,
 *   headers: upload.required_headers,
 * });
 * await media.confirmUpload(upload.file_id, { upload_type: "put" });
 *
 * Multipart upload:
 * const upload = data.results[0];
 * const parts = await media.uploadMultipartParts({
 *   file,
 *   uploadUrls: upload.upload_urls,
 *   partSize: 5 * 1024 * 1024,
 *   headers: upload.required_headers,
 * });
 * await media.confirmUpload(upload.file_id, {
 *   upload_type: "multipart",
 *   upload_id: upload.upload_id,
 *   parts,
 * });
 *
 * Refresh presigned URLs:
 * await media.presignUploadUrls(upload.file_id, {
 *   upload_type: "multipart",
 *   upload_id: upload.upload_id,
 *   part_numbers: [1, 2, 3],
 * });
 *
 * Playback + download:
 * const streamUrl = media.buildStreamUrl(fileId);
 * const segmentUrl = media.buildStreamPathUrl(fileId, "segment.ts");
 * const downloadUrl = media.buildDownloadUrl(fileId);
 *
 * Override account context (per hook or per call):
 * useMediaServiceV2({ accountType: "instructor", enterpriseAccountId: "<id>" });
 * await media.listFiles({ context: { accountType: "enterprise" } });
 *
 * Context detection uses cookies: defaultAccountType, defaultAccountID,
 * affiliateAccount. Instructor variants like "instructor-affiliate" normalize
 * to "instructor".
 */
import { useCallback, useMemo } from "react";

// ============================================================================
// TYPE DEFINITIONS (JSDoc)
// ============================================================================

/**
 * Account type for context resolution.
 * @typedef {"personal" | "instructor" | "enterprise"} AccountType
 */

/**
 * Upload type for file uploads.
 * @typedef {"put" | "multipart"} UploadType
 */

/**
 * Context for API calls.
 * @typedef {Object} MediaServiceContext
 * @property {AccountType} [accountType] - The account type (personal/instructor/enterprise)
 * @property {string} [accountId] - The account ID for _account query param
 * @property {string} [enterpriseAccountId] - Enterprise ID for instructor affiliate context
 * @property {string} [affiliateAccountId] - Alias for enterpriseAccountId
 */

/**
 * Hook initialization options.
 * @typedef {Object} MediaServiceOptions
 * @property {AccountType} [accountType] - Override default account type
 * @property {string} [accountId] - Override default account ID
 * @property {string} [enterpriseAccountId] - Enterprise ID for instructor affiliate context
 * @property {string} [affiliateAccountId] - Alias for enterpriseAccountId
 */

// ============================================================================
// REQUEST TYPES
// ============================================================================

/**
 * File metadata for upload request.
 * @typedef {Object} FileUploadRequest
 * @property {string} name - Original filename (e.g., "intro.mp4")
 * @property {string} mimetype - MIME type (e.g., "video/mp4")
 * @property {number} size - File size in bytes
 * @property {UploadType} upload_type - Upload method ("put" for single, "multipart" for chunked)
 */

/**
 * Request payload for creating file records.
 * @typedef {Object} CreateFilesPayload
 * @property {string} [tag] - File tag/category (e.g., "lesson", "profile")
 * @property {string} [caption] - File caption/description
 * @property {string} [duration] - Duration in "HH:MM:SS" format (for audio/video)
 * @property {string} [lecture_id] - Associated lecture ID
 * @property {FileUploadRequest[]} files - Array of file metadata
 */

/**
 * Request payload for refreshing presigned upload URLs.
 * @typedef {Object} PresignUploadUrlsPayload
 * @property {UploadType} upload_type - Upload method
 * @property {string} [upload_id] - Multipart upload ID (required for multipart)
 * @property {number[]} [part_numbers] - Part numbers to refresh (for multipart)
 */

/**
 * Multipart upload part for confirmation.
 * @typedef {Object} UploadPart
 * @property {string} ETag - ETag returned from S3 after uploading the part
 * @property {number} PartNumber - Part number (1-indexed)
 */

/**
 * Request payload for confirming upload completion.
 * @typedef {Object} ConfirmUploadPayload
 * @property {UploadType} upload_type - Upload method used
 * @property {string} [upload_id] - Multipart upload ID (required for multipart)
 * @property {UploadPart[]} [parts] - Array of uploaded parts (required for multipart)
 */

// ============================================================================
// RESPONSE TYPES
// ============================================================================

/**
 * Multipart upload URL entry.
 * @typedef {Object} MultipartUploadUrl
 * @property {number} part_number - Part number (1-indexed)
 * @property {string} url - Presigned URL for this part
 */

/**
 * Upload result from create files or presign refresh.
 * @typedef {Object} UploadResult
 * @property {string} file_id - Unique file identifier
 * @property {string} object_key - S3/storage object key path
 * @property {UploadType} upload_type - Upload method
 * @property {string | null} upload_url - Presigned PUT URL (for single upload)
 * @property {MultipartUploadUrl[] | null} upload_urls - Presigned URLs per part (for multipart)
 * @property {string | null} upload_id - Multipart upload ID (for multipart)
 * @property {Object.<string, string>} required_headers - Headers to include in upload request
 * @property {string} expires_at - ISO 8601 timestamp when presigned URLs expire (default 15 min)
 * @property {string} upload_complete_url - URL to call for confirming upload
 */

/**
 * Response from POST /files/ (create files).
 * @typedef {Object} CreateFilesResponse
 * @property {UploadResult[]} results - Array of upload results
 */

/**
 * Response from POST /files/{id}/presign_upload_urls/.
 * Same structure as UploadResult.
 * @typedef {UploadResult} PresignUploadUrlsResponse
 */

/**
 * Response from POST /files/{id}/confirm_upload/.
 * @typedef {Object} ConfirmUploadResponse
 * @property {boolean} uploaded - Whether upload was confirmed successfully
 */

/**
 * File record from list/retrieve operations.
 * @typedef {Object} MediaFile
 * @property {string} id - Unique file identifier
 * @property {string} name - Original filename
 * @property {string} mimetype - MIME type
 * @property {number} size - File size in bytes
 * @property {string} [tag] - File tag/category
 * @property {string} [caption] - File caption
 * @property {string} [duration] - Duration in "HH:MM:SS" format
 * @property {string} [lecture_id] - Associated lecture ID
 * @property {string} [url] - Stream URL
 * @property {string} [download_url] - Download URL
 * @property {string} [thumbnail_url] - Thumbnail URL (for images/videos)
 * @property {string} [processing_status] - Processing status (pending/processing/complete/failed)
 * @property {string} created_at - ISO 8601 creation timestamp
 * @property {string} updated_at - ISO 8601 last update timestamp
 */

/**
 * Paginated list response.
 * @typedef {Object} ListFilesResponse
 * @property {number} count - Total number of files
 * @property {string | null} next - URL for next page
 * @property {string | null} previous - URL for previous page
 * @property {MediaFile[]} results - Array of file records
 */

/**
 * Response from GET /files/{id}/stream/.
 * Returns 302 redirect to:
 * - Presigned original file URL (if processing not complete)
 * - HLS index.m3u8 (if HLS is ready)
 * @typedef {Object} StreamResponse
 * @property {string} url - Redirect URL
 */

/**
 * Response from GET /files/{id}/download/.
 * Returns 302 redirect to presigned URL for original upload.
 * @typedef {Object} DownloadResponse
 * @property {string} url - Presigned download URL
 */

/**
 * Result from uploading to presigned URL.
 * @typedef {Object} PresignedUploadResult
 * @property {Response} response - Fetch response object
 * @property {string | null} etag - ETag header from response (needed for multipart confirm)
 */

/**
 * Options for uploading to presigned URL.
 * @typedef {Object} UploadToPresignedUrlOptions
 * @property {string} url - Presigned URL
 * @property {File | Blob} file - File or blob to upload
 * @property {Object.<string, string>} [headers] - Headers from required_headers
 * @property {"PUT" | "POST"} [method] - HTTP method (default: PUT)
 */

/**
 * Options for multipart upload.
 * @typedef {Object} MultipartUploadOptions
 * @property {File} file - File to upload
 * @property {MultipartUploadUrl[]} uploadUrls - Presigned URLs from server
 * @property {number} [partSize] - Part size in bytes (default: 5MB, must match server)
 * @property {Object.<string, string>} [headers] - Headers from required_headers
 * @property {Array<{partNumber: number, url: string, body: Blob}>} [parts] - Pre-built parts (optional)
 */

// ============================================================================
// IMPLEMENTATION
// ============================================================================

import axios from "../utils/axiosConfig";
import useCookiePolling from "./utils/cookiePolling";

/** Default part size for multipart uploads (5MB) */
const DEFAULT_PART_SIZE = 5 * 1024 * 1024;

/**
 * Normalizes account type string to standard values.
 * @param {string} accountType - Raw account type
 * @returns {AccountType | ""} Normalized account type
 */
const normalizeAccountType = (accountType) => {
  if (!accountType) return "";
  const normalized = String(accountType).toLowerCase();
  if (normalized.startsWith("instructor")) return "instructor";
  if (normalized === "enterprise") return "enterprise";
  if (normalized === "personal" || normalized === "student") return "personal";
  return normalized;
};

/**
 * Builds the API prefix based on account type and enterprise context.
 * @param {AccountType} accountType - Account type
 * @param {string} [enterpriseAccountId] - Enterprise ID for instructor affiliate
 * @returns {string} API prefix path
 */
const buildPrefix = (accountType, enterpriseAccountId) => {
  const normalized = normalizeAccountType(accountType);
  if (normalized === "instructor") {
    return enterpriseAccountId
      ? `/instructor/${enterpriseAccountId}/files/`
      : "/instructor/files/";
  }
  if (normalized === "enterprise") {
    return "/enterprise/files/";
  }
  return "/files/";
};

/**
 * Sanitizes stream path by removing leading slash.
 * @param {string} [path] - Path to sanitize
 * @returns {string} Sanitized path
 */
const sanitizeStreamPath = (path = "") =>
  path.startsWith("/") ? path.slice(1) : path;

/**
 * Adds _account query param to axios config.
 * @param {Object} [config] - Axios config
 * @param {string} [accountId] - Account ID
 * @returns {Object} Config with _account param
 */
const withAccountParam = (config = {}, accountId) => {
  if (!accountId) return config;
  return {
    ...config,
    params: {
      _account: accountId,
      ...(config.params || {}),
    },
  };
};

/**
 * Uploads a file or blob to a presigned S3/Spaces URL.
 *
 * IMPORTANT: For S3-compatible storage (Digital Ocean Spaces, AWS S3, etc.):
 * Do NOT add headers that aren't provided by the server.
 * The presigned URL contains the signature in query params, and adding
 * unexpected headers (like Content-Type) can break the signature or
 * trigger CORS preflight requests that the bucket may not allow.
 *
 * @param {UploadToPresignedUrlOptions} options - Upload options
 * @returns {Promise<PresignedUploadResult>} Upload result with response and etag
 * @throws {Error} If url or file is missing, or upload fails
 */
const uploadToPresignedUrl = async ({
  url,
  file,
  headers = {},
  method = "PUT",
}) => {
  if (!url || !file) {
    throw new Error("uploadToPresignedUrl requires url and file.");
  }
  
  // Only use headers explicitly provided - don't add Content-Type
  // as it may conflict with presigned URL signature or trigger CORS preflight
  const response = await fetch(url, {
    method,
    body: file,
    headers,
  });

  if (!response.ok) {
    throw new Error(`Upload failed with status ${response.status}.`);
  }

  const etag = response.headers.get("etag") || response.headers.get("ETag");
  return { response, etag };
};

/**
 * Builds multipart upload parts from file and upload URLs.
 * @param {Object} options - Build options
 * @param {File} options.file - File to split
 * @param {MultipartUploadUrl[]} options.uploadUrls - Presigned URLs from server
 * @param {number} options.partSize - Part size in bytes
 * @returns {Array<{partNumber: number, url: string, body: Blob}>} Parts ready for upload
 * @throws {Error} If partSize is missing or doesn't match file size
 */
const buildMultipartParts = ({ file, uploadUrls, partSize }) => {
  if (!file || !Array.isArray(uploadUrls)) return [];
  if (!partSize) {
    throw new Error("partSize is required to build multipart parts.");
  }

  return uploadUrls.map((part, index) => {
    const partNumber = part.part_number ?? index + 1;
    const start = (partNumber - 1) * partSize;
    if (start >= file.size) {
      throw new Error("partSize does not match file size for uploadUrls.");
    }
    const end = Math.min(start + partSize, file.size);
    return {
      partNumber,
      url: part.url,
      body: file.slice(start, end),
    };
  });
};

/**
 * Uploads all parts of a multipart upload sequentially.
 * @param {MultipartUploadOptions} options - Upload options
 * @returns {Promise<UploadPart[]>} Array of uploaded parts with ETags for confirmation
 * @throws {Error} If ETag is not returned (CORS must expose ETag header)
 */
const uploadMultipartParts = async ({
  file,
  uploadUrls,
  partSize = DEFAULT_PART_SIZE,
  headers = {},
  parts,
}) => {
  const resolvedParts =
    parts || buildMultipartParts({ file, uploadUrls, partSize });

  const uploadedParts = [];
  for (const part of resolvedParts) {
    const { etag } = await uploadToPresignedUrl({
      url: part.url,
      file: part.body,
      headers,
      method: "PUT",
    });
    
    // Include part even if ETag is null - let backend handle validation
    // Some storage providers don't expose ETag in CORS headers
    if (!etag) {
      console.warn(
        `Part ${part.partNumber}: ETag header not exposed. ` +
        "CORS should include 'ETag' in Access-Control-Expose-Headers."
      );
    }
    
    uploadedParts.push({
      ETag: etag,
      PartNumber: part.partNumber,
    });
  }

  return uploadedParts;
};

/**
 * Media Service v2 React hook.
 *
 * Provides methods for file upload, management, and playback via the Media Service v2 API.
 *
 * @param {MediaServiceOptions} [options] - Hook initialization options
 * @returns {Object} Media service methods and utilities
 */
const useMediaServiceV2 = (options = {}) => {
  const cookieAccountType = useCookiePolling("defaultAccountType");
  const cookieAccountId = useCookiePolling("defaultAccountID");
  const cookieAffiliateAccountId = useCookiePolling("affiliateAccount");

  /** @type {MediaServiceContext} */
  const baseContext = useMemo(
    () => ({
      accountType: normalizeAccountType(
        options.accountType || cookieAccountType
      ),
      accountId: options.accountId || cookieAccountId,
      enterpriseAccountId:
        options.enterpriseAccountId ||
        options.affiliateAccountId ||
        cookieAffiliateAccountId,
    }),
    [
      options.accountId,
      options.accountType,
      options.enterpriseAccountId,
      options.affiliateAccountId,
      cookieAccountType,
      cookieAccountId,
      cookieAffiliateAccountId,
    ]
  );

  /**
   * Resolves context by merging override with base context.
   * @param {MediaServiceContext} [override] - Context override
   * @returns {MediaServiceContext} Resolved context
   */
  const resolveContext = useCallback(
    (override = {}) => ({
      accountType: normalizeAccountType(
        override.accountType || baseContext.accountType
      ),
      accountId: override.accountId || baseContext.accountId,
      enterpriseAccountId:
        override.enterpriseAccountId ||
        override.affiliateAccountId ||
        baseContext.enterpriseAccountId,
    }),
    [baseContext]
  );

  /**
   * Gets the base API path for the given context.
   * @param {MediaServiceContext} [override] - Context override
   * @returns {string} Base API path (e.g., "/media/v2/files/")
   */
  const getBasePath = useCallback(
    (override) => {
      const ctx = resolveContext(override);
      return `/media/v2${buildPrefix(
        ctx.accountType,
        ctx.enterpriseAccountId
      )}`;
    },
    [resolveContext]
  );

  /**
   * Builds URL for a specific file.
   * @param {string} id - File ID
   * @param {MediaServiceContext} [override] - Context override
   * @returns {string} File URL
   */
  const buildFileUrl = useCallback(
    (id, override) => `${getBasePath(override)}${id}/`,
    [getBasePath]
  );

  /**
   * Builds stream URL for playback.
   * Returns 302 to presigned original URL or HLS index.m3u8.
   * @param {string} id - File ID
   * @param {MediaServiceContext} [override] - Context override
   * @returns {string} Stream URL
   */
  const buildStreamUrl = useCallback(
    (id, override) => `${getBasePath(override)}${id}/stream/`,
    [getBasePath]
  );

  /**
   * Builds URL for HLS segment or nested playlist.
   * @param {string} id - File ID
   * @param {string} path - Segment or playlist path
   * @param {MediaServiceContext} [override] - Context override
   * @returns {string} Stream path URL
   */
  const buildStreamPathUrl = useCallback(
    (id, path, override) =>
      `${getBasePath(override)}${id}/stream/${sanitizeStreamPath(path)}`,
    [getBasePath]
  );

  /**
   * Builds download URL for original file.
   * Returns 302 to presigned download URL.
   * @param {string} id - File ID
   * @param {MediaServiceContext} [override] - Context override
   * @returns {string} Download URL
   */
  const buildDownloadUrl = useCallback(
    (id, override) => `${getBasePath(override)}${id}/download/`,
    [getBasePath]
  );

  /**
   * Creates file record(s) and gets presigned upload URL(s).
   * POST /files/
   * @param {CreateFilesPayload} payload - File creation payload
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: CreateFilesResponse}>} Axios response with upload results
   */
  const createFiles = useCallback(
    async (payload, { context, config } = {}) => {
      const ctx = resolveContext(context);
      const url = getBasePath(ctx);
      return axios.post(url, payload, withAccountParam(config, ctx.accountId));
    },
    [getBasePath, resolveContext]
  );

  /**
   * Lists files with optional filtering.
   * GET /files/
   * @param {Object} [options] - Request options
   * @param {Object} [options.params] - Query parameters
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: ListFilesResponse}>} Axios response with paginated files
   */
  const listFiles = useCallback(
    async ({ params, context, config } = {}) => {
      const ctx = resolveContext(context);
      const url = getBasePath(ctx);
      const mergedConfig = {
        ...(config || {}),
        params: {
          ...(params || {}),
          ...(config?.params || {}),
        },
      };
      return axios.get(url, withAccountParam(mergedConfig, ctx.accountId));
    },
    [getBasePath, resolveContext]
  );

  /**
   * Retrieves a single file record.
   * GET /files/{id}/
   * @param {string} id - File ID
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: MediaFile}>} Axios response with file record
   */
  const getFile = useCallback(
    async (id, { context, config } = {}) => {
      const ctx = resolveContext(context);
      return axios.get(
        buildFileUrl(id, ctx),
        withAccountParam(config, ctx.accountId)
      );
    },
    [buildFileUrl, resolveContext]
  );

  /**
   * Updates a file record.
   * PATCH /files/{id}/
   * @param {string} id - File ID
   * @param {Partial<MediaFile>} data - Fields to update
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: MediaFile}>} Axios response with updated file
   */
  const updateFile = useCallback(
    async (id, data, { context, config } = {}) => {
      const ctx = resolveContext(context);
      return axios.patch(
        buildFileUrl(id, ctx),
        data,
        withAccountParam(config, ctx.accountId)
      );
    },
    [buildFileUrl, resolveContext]
  );

  /**
   * Deletes a file record.
   * DELETE /files/{id}/
   * @param {string} id - File ID
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<void>} Axios response
   */
  const deleteFile = useCallback(
    async (id, { context, config } = {}) => {
      const ctx = resolveContext(context);
      return axios.delete(
        buildFileUrl(id, ctx),
        withAccountParam(config, ctx.accountId)
      );
    },
    [buildFileUrl, resolveContext]
  );

  /**
   * Refreshes presigned upload URLs for an existing file.
   * Use when URLs are near expiry (check expires_at) or after 403 from S3.
   * POST /files/{id}/presign_upload_urls/
   * @param {string} id - File ID
   * @param {PresignUploadUrlsPayload} data - Refresh payload
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: PresignUploadUrlsResponse}>} Axios response with fresh URLs
   */
  const presignUploadUrls = useCallback(
    async (id, data, { context, config } = {}) => {
      const ctx = resolveContext(context);
      const url = `${buildFileUrl(id, ctx)}presign_upload_urls/`;
      return axios.post(url, data, withAccountParam(config, ctx.accountId));
    },
    [buildFileUrl, resolveContext]
  );

  /**
   * Confirms upload completion (triggers server-side processing).
   * POST /files/{id}/confirm_upload/
   * @param {string} id - File ID
   * @param {ConfirmUploadPayload} data - Confirmation payload
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: ConfirmUploadResponse}>} Axios response with confirmation
   */
  const confirmUpload = useCallback(
    async (id, data, { context, config } = {}) => {
      const ctx = resolveContext(context);
      const url = `${buildFileUrl(id, ctx)}confirm_upload/`;
      return axios.post(url, data, withAccountParam(config, ctx.accountId));
    },
    [buildFileUrl, resolveContext]
  );

  /**
   * Requests stream URL for playback.
   * GET /files/{id}/stream/
   * Returns 302 to presigned original URL (if processing incomplete) or HLS index.m3u8.
   * @param {string} id - File ID
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: StreamResponse}>} Axios response (follows redirect)
   */
  const requestStream = useCallback(
    async (id, { context, config } = {}) => {
      const ctx = resolveContext(context);
      return axios.get(
        buildStreamUrl(id, ctx),
        withAccountParam(config, ctx.accountId)
      );
    },
    [buildStreamUrl, resolveContext]
  );

  /**
   * Requests download URL for original file.
   * GET /files/{id}/download/
   * Returns 302 to presigned download URL.
   * @param {string} id - File ID
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: DownloadResponse}>} Axios response (follows redirect)
   */
  const requestDownload = useCallback(
    async (id, { context, config } = {}) => {
      const ctx = resolveContext(context);
      return axios.get(
        buildDownloadUrl(id, ctx),
        withAccountParam(config, ctx.accountId)
      );
    },
    [buildDownloadUrl, resolveContext]
  );

  /**
   * Requests HLS segment or nested playlist.
   * GET /files/{id}/stream/{path}
   * Returns 302 to presigned segment URL or rewritten manifest.
   * @param {string} id - File ID
   * @param {string} path - Segment or playlist path
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<Response>} Axios response
   */
  const requestStreamPath = useCallback(
    async (id, path, { context, config } = {}) => {
      const ctx = resolveContext(context);
      return axios.get(
        buildStreamPathUrl(id, path, ctx),
        withAccountParam(config, ctx.accountId)
      );
    },
    [buildStreamPathUrl, resolveContext]
  );

  /**
   * Resolves the presigned URL from the stream endpoint by following the redirect.
   * Uses a tiny ranged GET to avoid downloading large media, with a GET fallback.
   * When preferHls is true, probes the HLS manifest first.
   *
   * For images: Always returns the presigned URL from the redirect.
   * For video/audio: Returns presigned original URL (if not processed) or HLS manifest URL.
   *
   * @param {string} id - File ID
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {boolean} [options.preferHls] - If true, try HLS manifest first
   * @returns {Promise<string|null>} Resolved presigned URL
   */
  const resolvePresignedUrl = useCallback(
    async (id, { context, preferHls } = {}) => {
      if (!id) return null;

      const ctx = resolveContext(context);
      const streamPath = buildStreamUrl(id, ctx);
      const baseConfig = {
        ...withAccountParam({}, ctx.accountId),
        maxRedirects: 5,
      };
      let lastError = null;

      const extractResolvedUrl = (response, error) => {
        return (
          response?.request?.responseURL ||
          error?.request?.responseURL ||
          response?.headers?.location ||
          error?.response?.headers?.location ||
          null
        );
      };

      if (preferHls) {
        const indexPath = buildStreamPathUrl(id, "index.m3u8", ctx);
        try {
          await axios.get(indexPath, baseConfig);
          const baseURL = axios.defaults.baseURL || "";
          return `${baseURL}${indexPath}`;
        } catch (err) {
          lastError = err;
        }
      }

      try {
        const rangeResponse = await axios.get(streamPath, {
          ...baseConfig,
          headers: {
            ...(baseConfig.headers || {}),
            Range: "bytes=0-0",
          },
        });

        const rangeUrl = extractResolvedUrl(rangeResponse);
        console.log(rangeUrl, 'this is stream range response');
        if (rangeUrl) return rangeUrl;
      } catch (err) {
        lastError = err;
        const rangeUrl = extractResolvedUrl(null, err);
        
        if (rangeUrl) return rangeUrl;
      }

      try {
        const response = await axios.get(streamPath, baseConfig);
        const finalUrl = extractResolvedUrl(response);
        if (finalUrl) return finalUrl;
      } catch (err) {
        lastError = err;
        const finalUrl = extractResolvedUrl(null, err);
        if (finalUrl) return finalUrl;
      }

      if (lastError) {
        console.error("Failed to resolve presigned URL:", lastError);
      }
      return null;
    },
    [buildStreamUrl, resolveContext]
  );

  /**
   * Resolves the presigned download URL by following the redirect.
   * Uses a tiny ranged GET to avoid downloading large files, with a GET fallback.
   * 
   * @param {string} id - File ID
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @returns {Promise<string|null>} Resolved presigned download URL
   */
  const resolveDownloadUrl = useCallback(
    async (id, { context } = {}) => {
      if (!id) return null;

      const ctx = resolveContext(context);
      const downloadPath = buildDownloadUrl(id, ctx);
      const baseConfig = {
        ...withAccountParam({}, ctx.accountId),
        maxRedirects: 5,
      };
      let lastError = null;

      const extractResolvedUrl = (response, error) => {
        return (
          response?.request?.responseURL ||
          error?.request?.responseURL ||
          response?.headers?.location ||
          error?.response?.headers?.location ||
          null
        );
      };

      try {
        const rangeResponse = await axios.get(downloadPath, {
          ...baseConfig,
          headers: {
            ...(baseConfig.headers || {}),
            Range: "bytes=0-0",
          },
        });

        const rangeUrl = extractResolvedUrl(rangeResponse);
        if (rangeUrl) return rangeUrl;
      } catch (err) {
        lastError = err;
        const rangeUrl = extractResolvedUrl(null, err);
        
        if (rangeUrl) return rangeUrl;
      }

      try {
        const response = await axios.get(downloadPath, baseConfig);
        const finalUrl = extractResolvedUrl(response);
        if (finalUrl) return finalUrl;
      } catch (err) {
        lastError = err;
        const finalUrl = extractResolvedUrl(null, err);
        if (finalUrl) return finalUrl;
      }

      if (lastError) {
        console.error("Failed to resolve download URL:", lastError);
      }
      return null;
    },
    [buildDownloadUrl, resolveContext]
  );

  /**
   * Builds absolute stream URL for direct browser access (img src, etc.)
   * Note: For axios requests, don't use this - axios interceptor adds _account.
   * @param {string} id - File ID
   * @param {MediaServiceContext} [override] - Context override
   * @returns {string} Absolute stream URL
   */
  const buildStreamUrlWithAccount = useCallback(
    (id, override) => {
      const ctx = resolveContext(override);
      const streamPath = buildStreamUrl(id, ctx);
      const baseURL = axios.defaults.baseURL || "";
      return `${baseURL}${streamPath}`;
    },
    [buildStreamUrl, resolveContext]
  );

  /**
   * Builds absolute download URL for direct browser access.
   * Note: For axios requests, don't use this - axios interceptor adds _account.
   * @param {string} id - File ID
   * @param {MediaServiceContext} [override] - Context override
   * @returns {string} Absolute download URL
   */
  const buildDownloadUrlWithAccount = useCallback(
    (id, override) => {
      const ctx = resolveContext(override);
      const downloadPath = buildDownloadUrl(id, ctx);
      const baseURL = axios.defaults.baseURL || "";
      return `${baseURL}${downloadPath}`;
    },
    [buildDownloadUrl, resolveContext]
  );

  // ============================================================================
  // V2 BULK DOWNLOAD ENDPOINTS
  // ============================================================================

  /**
   * Downloads all deletable files (files that exceed storage limit).
   * GET /files/download-all-deletable-files/
   * 
   * Uses deletable selection (storage limit) and filters to uploaded/converted files.
   * Returns presigned download URLs that are short-lived - download immediately.
   * 
   * @param {Object} [options] - Request options
   * @param {string} [options.category] - Filter by category: "video", "audio", "image", "others"
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: {results: Array<{id: string, url: string}>}}>} Axios response with download URLs
   * @throws {Error} 404 if any media record can't be presigned
   */
  const downloadAllDeletableFiles = useCallback(
    async ({ category, context, config } = {}) => {
      const ctx = resolveContext(context);
      const url = `${getBasePath(ctx)}download-all-deletable-files/`;
      const mergedConfig = {
        ...(config || {}),
        params: {
          ...(category ? { category } : {}),
          ...(config?.params || {}),
        },
      };
      return axios.get(url, withAccountParam(mergedConfig, ctx.accountId));
    },
    [getBasePath, resolveContext]
  );

  /**
   * Downloads specific files by IDs (bulk download).
   * POST /files/download-files/
   * 
   * Only returns files accessible to the requesting account.
   * Filters out pending uploads (not uploaded + not converted + not legacy).
   * Returns presigned download URLs that are short-lived - download immediately.
   * Updates last_used for returned files.
   * 
   * @param {string[]} ids - Array of file IDs to download
   * @param {Object} [options] - Request options
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{data: {results: Array<{id: string, url: string}>}}>} Axios response with download URLs
   * @throws {Error} 400 if ids is missing or empty
   * @throws {Error} 404 if any media record can't be presigned
   */
  const downloadFiles = useCallback(
    async (ids, { context, config } = {}) => {
      if (!ids || !Array.isArray(ids) || ids.length === 0) {
        throw new Error("downloadFiles requires a non-empty array of file IDs.");
      }
      const ctx = resolveContext(context);
      const url = `${getBasePath(ctx)}download-files/`;
      return axios.post(url, { ids }, withAccountParam(config, ctx.accountId));
    },
    [getBasePath, resolveContext]
  );

  /**
   * Helper function to trigger browser download from presigned URL.
   * Creates a temporary anchor element to initiate download.
   * 
   * @param {string} url - Presigned download URL
   * @param {string} [filename] - Optional filename for the download
   */
  const triggerBrowserDownload = useCallback((url, filename) => {
    const link = document.createElement("a");
    link.href = url;
    if (filename) {
      link.download = filename;
    }
    link.target = "_blank";
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  }, []);

  /**
   * Downloads multiple files using the V2 bulk download endpoint.
   * Fetches presigned URLs and triggers browser downloads for each file.
   * 
   * @param {string[]} ids - Array of file IDs to download
   * @param {Object} [options] - Request options
   * @param {Object.<string, string>} [options.filenames] - Map of file ID to filename
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{success: string[], failed: string[]}>} Result with successful and failed downloads
   */
  const bulkDownloadFiles = useCallback(
    async (ids, { filenames = {}, context, config } = {}) => {
      const result = { success: [], failed: [] };
      
      try {
        const response = await downloadFiles(ids, { context, config });
        const results = response?.data?.results || [];
        
        for (const item of results) {
          try {
            triggerBrowserDownload(item.url, filenames[item.id]);
            result.success.push(item.id);
          } catch (err) {
            console.error(`Failed to download file ${item.id}:`, err);
            result.failed.push(item.id);
          }
        }
        
        // Mark any IDs not in results as failed
        const returnedIds = new Set(results.map(r => r.id));
        for (const id of ids) {
          if (!returnedIds.has(id) && !result.failed.includes(id)) {
            result.failed.push(id);
          }
        }
      } catch (err) {
        console.error("Bulk download failed:", err);
        result.failed = ids;
      }
      
      return result;
    },
    [downloadFiles, triggerBrowserDownload]
  );

  /**
   * Downloads all deletable files (recommended for deletion due to storage limits).
   * Fetches presigned URLs and triggers browser downloads for each file.
   * 
   * @param {Object} [options] - Request options
   * @param {string} [options.category] - Filter by category: "video", "audio", "image", "others"
   * @param {Object.<string, string>} [options.filenames] - Map of file ID to filename
   * @param {MediaServiceContext} [options.context] - Context override
   * @param {Object} [options.config] - Axios config
   * @returns {Promise<{success: string[], failed: string[]}>} Result with successful and failed downloads
   */
  const bulkDownloadDeletableFiles = useCallback(
    async ({ category, filenames = {}, context, config } = {}) => {
      const result = { success: [], failed: [] };
      
      try {
        const response = await downloadAllDeletableFiles({ category, context, config });
        const results = response?.data?.results || [];
        
        for (const item of results) {
          try {
            triggerBrowserDownload(item.url, filenames[item.id]);
            result.success.push(item.id);
          } catch (err) {
            console.error(`Failed to download file ${item.id}:`, err);
            result.failed.push(item.id);
          }
        }
      } catch (err) {
        console.error("Bulk download deletable files failed:", err);
      }
      
      return result;
    },
    [downloadAllDeletableFiles, triggerBrowserDownload]
  );

  return {
    // Context
    context: baseContext,
    
    // URL builders
    getBasePath,
    buildFileUrl,
    buildStreamUrl,
    buildStreamPathUrl,
    buildDownloadUrl,
    buildStreamUrlWithAccount,
    buildDownloadUrlWithAccount,
    
    // CRUD operations
    createFiles,
    listFiles,
    getFile,
    updateFile,
    deleteFile,
    
    // Upload operations
    presignUploadUrls,
    confirmUpload,
    uploadToPresignedUrl,
    uploadMultipartParts,
    
    // Playback/download
    requestStream,
    requestStreamPath,
    requestDownload,
    resolvePresignedUrl,
    resolveDownloadUrl,
    
    // V2 Bulk download operations
    downloadAllDeletableFiles,
    downloadFiles,
    bulkDownloadFiles,
    bulkDownloadDeletableFiles,
    triggerBrowserDownload,
  };
};

export default useMediaServiceV2;
