'use node';

/**
 * Convex storage provider for the integration sandbox.
 *
 * Implements StorageProvider using ActionCtx.storage:
 * - download: fetch URL → blob → ctx.storage.store(blob) — no base64 conversion
 * - store: base64/utf-8 string → Blob → ctx.storage.store(blob)
 */

import { internal } from '../../../_generated/api';
import type { ActionCtx } from '../../../_generated/server';
import { base64ToBytes } from '../../../lib/crypto/base64_to_bytes';
import { toPublicUrl } from '../../../lib/helpers/public_storage_url';
import type { StorageProvider } from '../types';
import { resolveAndValidateUrl } from './url_rewrite';

export function createConvexStorageProvider(
  ctx: ActionCtx,
  organizationId: string,
): StorageProvider {
  return {
    async download({ url, headers, fileName, allowedHosts }) {
      const resolvedUrl = resolveAndValidateUrl(url, allowedHosts);

      const response = await globalThis.fetch(resolvedUrl, {
        headers,
        redirect: 'manual',
      });
      if (response.status >= 300 && response.status < 400) {
        const location = response.headers.get('location') ?? 'unknown';
        throw new Error(
          `File download redirected to "${location}" for "${url}". Add the redirect host to allowedHosts.`,
        );
      }
      if (!response.ok) {
        throw new Error(
          `File download failed: ${response.status} ${response.statusText} for "${url}"`,
        );
      }

      const blob = await response.blob();
      const storageId = await ctx.storage.store(blob);
      const storageUrl = (await ctx.storage.getUrl(storageId)) ?? '';
      const contentType =
        blob.type ||
        response.headers.get('content-type') ||
        'application/octet-stream';

      await ctx.runMutation(
        internal.file_metadata.internal_mutations.saveFileMetadata,
        {
          organizationId,
          storageId,
          fileName,
          contentType,
          size: blob.size,
          // Write-time default for raw integration blobs. When the workflow
          // promotes one to a document, linkDocumentToFile rewrites this to the
          // connector's provenance (e.g. 'confluence') from the document's
          // sourceProvider; transient blobs that never become a document stay
          // 'agent' and remain eligible for the agent-temp retention sweep.
          source: 'agent',
        },
      );

      return {
        fileId: String(storageId),
        url: toPublicUrl(storageUrl),
        fileName,
        contentType,
        size: blob.size,
      };
    },

    async store({ data, encoding, contentType, fileName }) {
      let blob: Blob;
      if (encoding === 'base64') {
        const bytes = base64ToBytes(data);
        // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- SharedArrayBuffer TS compat
        blob = new Blob([bytes as unknown as ArrayBuffer], {
          type: contentType,
        });
      } else {
        blob = new Blob([data], { type: contentType });
      }

      const storageId = await ctx.storage.store(blob);
      const storageUrl = (await ctx.storage.getUrl(storageId)) ?? '';

      await ctx.runMutation(
        internal.file_metadata.internal_mutations.saveFileMetadata,
        {
          organizationId,
          storageId,
          fileName,
          contentType,
          size: blob.size,
          // Write-time default for raw integration blobs. When the workflow
          // promotes one to a document, linkDocumentToFile rewrites this to the
          // connector's provenance (e.g. 'confluence') from the document's
          // sourceProvider; transient blobs that never become a document stay
          // 'agent' and remain eligible for the agent-temp retention sweep.
          source: 'agent',
        },
      );

      return {
        fileId: String(storageId),
        url: toPublicUrl(storageUrl),
        fileName,
        contentType,
        size: blob.size,
      };
    },
  };
}
