import { ValidationError } from '@shade/core';
import type { TransferInput } from '../types.js';

export interface NormalizedInput {
  /** Total plaintext bytes when known. `undefined` for indeterminate streams. */
  size: number | undefined;
  name?: string;
  contentType?: string;
  /** Read bytes [start, end). End is exclusive. Must be supported when `size` is known. */
  read(start: number, end: number): Promise<Uint8Array>;
  /** Sequential reader for unknown-size inputs. Returns `null` at end-of-stream. */
  readNext(maxBytes: number): Promise<Uint8Array | null>;
  /** Whether the source supports random `read(start, end)` access. */
  readonly randomAccess: boolean;
  /** Free any underlying resources (no-op when nothing to release). */
  close(): Promise<void>;
}

/**
 * Normalize any of the supported `TransferInput` shapes into a uniform
 * reader. Determines size eagerly when possible.
 */
export async function normalizeInput(input: TransferInput): Promise<NormalizedInput> {
  if (input instanceof Uint8Array) return fromUint8Array(input);
  if (typeof Blob !== 'undefined' && input instanceof Blob) return fromBlob(input);
  if (isReadableStream(input)) return fromReadableStream(input);
  throw new ValidationError('Unsupported TransferInput', 'input');
}

function fromUint8Array(buf: Uint8Array): NormalizedInput {
  return {
    size: buf.length,
    randomAccess: true,
    async read(start, end) {
      return buf.slice(start, end);
    },
    async readNext() {
      throw new Error('readNext is not supported on random-access inputs');
    },
    async close() {
      /* nothing to release */
    },
  };
}

function fromBlob(blob: Blob): NormalizedInput {
  // Blob.slice / Blob.arrayBuffer are not in `lib: ["ES2022"]`. We cast
  // through `unknown` to avoid pulling in the full DOM lib.
  const blobLike = blob as unknown as {
    size: number;
    slice(start: number, end: number): { arrayBuffer(): Promise<ArrayBuffer> };
    arrayBuffer(): Promise<ArrayBuffer>;
  };
  const out: NormalizedInput = {
    size: blobLike.size,
    randomAccess: true,
    async read(start, end) {
      const slice = blobLike.slice(start, end);
      const ab = await slice.arrayBuffer();
      return new Uint8Array(ab);
    },
    async readNext() {
      throw new Error('readNext is not supported on random-access inputs');
    },
    async close() {
      /* nothing to release */
    },
  };
  // File extends Blob with name/type
  const f = blob as Blob & { name?: string; type?: string };
  if (typeof f.name === 'string') out.name = f.name;
  if (typeof f.type === 'string' && f.type.length > 0) out.contentType = f.type;
  return out;
}

function fromReadableStream(stream: ReadableStream<Uint8Array>): NormalizedInput {
  const reader = stream.getReader();
  let pending: Uint8Array = new Uint8Array(0);
  let done = false;

  return {
    size: undefined,
    randomAccess: false,
    async read() {
      throw new Error('Random access not supported for ReadableStream input');
    },
    async readNext(maxBytes) {
      while (pending.length < maxBytes && !done) {
        const r = await reader.read();
        if (r.done) {
          done = true;
          break;
        }
        pending = concat(pending, r.value as Uint8Array);
      }
      if (pending.length === 0 && done) return null;
      const out = pending.subarray(0, Math.min(maxBytes, pending.length));
      pending = pending.subarray(out.length);
      return out;
    },
    async close() {
      try {
        await reader.cancel();
      } catch {
        /* already closed */
      }
    },
  };
}

function isReadableStream(x: unknown): x is ReadableStream<Uint8Array> {
  return (
    typeof x === 'object' &&
    x !== null &&
    typeof (x as { getReader?: unknown }).getReader === 'function'
  );
}

function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
  const out = new Uint8Array(a.length + b.length);
  out.set(a, 0);
  out.set(b, a.length);
  return out as Uint8Array;
}
