import { resolveTokenFromStorage } from '../../session';
import type { PersonalDocumentSlot } from './personal-document.types';

const BASE_API_URL =
  (import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
  'http://localhost:3000/api';

/** What the server says when it refuses a file — see ProfileDocumentsService. */
export interface PersonalDocumentError {
  message?: string;
  [detail: string]: unknown;
}

export type PersonalDocumentUploadResult =
  | { ok: true; slot: PersonalDocumentSlot }
  | { ok: false; error: PersonalDocumentError };

/**
 * Uploads one file and reports how far it has got.
 *
 * XHR rather than `fetch`, and therefore outside RTK Query: `fetch` has no
 * upload progress event, so a request body of any size is a spinner with
 * nothing behind it. That is tolerable for a 5 MB scan and not for the video a
 * slot can now be opened to, where the difference between "uploading" and
 * "uploading, 12%" is the difference between waiting and reloading the page.
 *
 * The caller refetches the vault afterwards; nothing here touches the cache.
 */
function upload(
  path: string,
  method: 'POST' | 'PUT',
  body: FormData,
  onProgress?: (percent: number) => void,
): Promise<PersonalDocumentUploadResult> {
  return new Promise((resolve) => {
    const request = new XMLHttpRequest();
    request.open(method, `${BASE_API_URL}${path}`);

    const token = resolveTokenFromStorage();
    if (token) request.setRequestHeader('Authorization', `Bearer ${token}`);

    request.upload.onprogress = (event) => {
      // Not every browser knows the total for a streamed body; without it a
      // percentage would be invented, so the caller keeps its spinner.
      if (!event.lengthComputable || !onProgress) return;
      onProgress(Math.round((event.loaded / event.total) * 100));
    };

    request.onload = () => {
      const parsed = parseBody(request.responseText);
      if (request.status >= 200 && request.status < 300) {
        resolve({ ok: true, slot: parsed as PersonalDocumentSlot });
        return;
      }
      resolve({ ok: false, error: toError(parsed, request.status) });
    };

    // A dropped connection and a cancelled request both land here; neither
    // carries a server message, so the caller falls back to its own wording.
    request.onerror = () => resolve({ ok: false, error: {} });
    request.onabort = () => resolve({ ok: false, error: {} });

    request.send(body);
  });
}

function parseBody(text: string): unknown {
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

/**
 * Nest wraps a thrown `BadRequestException({ message, ... })` as
 * `{ message: { message, ... } }`, and a plain string message as
 * `{ message: "slot_full" }`. Both are flattened to the object the UI
 * translates by its `message` key.
 */
function toError(parsed: unknown, status: number): PersonalDocumentError {
  const message = (parsed as { message?: unknown } | null)?.message;
  if (typeof message === 'object' && message !== null) {
    return message as PersonalDocumentError;
  }
  if (typeof message === 'string') return { message };
  return { message: `http_${status}` };
}

/** Adds one file to a personal document slot. */
export function uploadPersonalDocumentFile(params: {
  documentKey: string;
  file: File;
  onProgress?: (percent: number) => void;
}): Promise<PersonalDocumentUploadResult> {
  const body = new FormData();
  body.append('documentKey', params.documentKey);
  body.append('file', params.file, params.file.name);
  return upload('/profiles/me/documents', 'POST', body, params.onProgress);
}

/** Swaps one file for another in the same slot. */
export function replacePersonalDocumentFile(params: {
  fileId: string;
  file: File;
  onProgress?: (percent: number) => void;
}): Promise<PersonalDocumentUploadResult> {
  const body = new FormData();
  body.append('file', params.file, params.file.name);
  return upload(
    `/profiles/me/documents/files/${params.fileId}`,
    'PUT',
    body,
    params.onProgress,
  );
}
