import type { TransferOutput } from '../types.js';

/**
 * Generic chunk sink. The orchestrator writes plaintext chunks IN ORIGINAL
 * BYTE ORDER, then calls `finalize()`. `toBytes()` is only valid for
 * `'buffer'` sinks.
 */
export interface OutputSink {
  write(chunk: Uint8Array): Promise<void>;
  finalize(): Promise<void>;
  abort(reason?: string): Promise<void>;
  toBytes(): Uint8Array | null;
}

/**
 * Adapter from a `TransferOutput` (consumer-supplied) to an `OutputSink`.
 *
 * The `'file'` sink is Bun-specific (uses `Bun.file(path).writer()`) — for
 * Node use a `'pipe'` sink with an `fs.createWriteStream`.
 */
export async function resolveOutputSink(out: TransferOutput): Promise<OutputSink> {
  switch (out.kind) {
    case 'pipe':
      return createPipeSink(out.pipeTo);
    case 'callback':
      return createCallbackSink(out.onChunk);
    case 'buffer':
      return createBufferSink();
    case 'file':
      return createBunFileSink(out.path);
    case 'fileHandle':
      return createFileHandleSink(out.handle);
  }
}

function createPipeSink(stream: WritableStream<Uint8Array>): OutputSink {
  const writer = stream.getWriter();
  return {
    async write(chunk) {
      await writer.write(chunk);
    },
    async finalize() {
      try {
        await writer.close();
      } finally {
        writer.releaseLock?.();
      }
    },
    async abort(reason) {
      try {
        await writer.abort(reason);
      } catch {
        /* swallow */
      } finally {
        writer.releaseLock?.();
      }
    },
    toBytes() {
      return null;
    },
  };
}

function createCallbackSink(
  onChunk: (chunk: Uint8Array) => void | Promise<void>,
): OutputSink {
  return {
    async write(chunk) {
      await onChunk(chunk);
    },
    async finalize() {
      /* no-op */
    },
    async abort() {
      /* no-op */
    },
    toBytes() {
      return null;
    },
  };
}

function createBufferSink(): OutputSink {
  const parts: Uint8Array[] = [];
  let totalLen = 0;
  return {
    async write(chunk) {
      parts.push(chunk);
      totalLen += chunk.length;
    },
    async finalize() {
      /* no-op */
    },
    async abort() {
      parts.length = 0;
      totalLen = 0;
    },
    toBytes() {
      const out = new Uint8Array(totalLen);
      let off = 0;
      for (const p of parts) {
        out.set(p, off);
        off += p.length;
      }
      return out;
    },
  };
}

async function createBunFileSink(path: string): Promise<OutputSink> {
  // Lazy import so non-Bun runtimes don't fail at import time.
  const bunGlobal = (globalThis as unknown as { Bun?: { file: (p: string) => { writer: () => unknown } } })
    .Bun;
  if (bunGlobal === undefined) {
    throw new Error("'file' output sink requires Bun runtime");
  }
  const writer = bunGlobal.file(path).writer() as {
    write: (chunk: Uint8Array) => number | Promise<number>;
    end: () => Promise<number> | number;
    flush: () => Promise<number> | number;
  };
  return {
    async write(chunk) {
      await Promise.resolve(writer.write(chunk));
    },
    async finalize() {
      await Promise.resolve(writer.flush());
      await Promise.resolve(writer.end());
    },
    async abort() {
      try {
        await Promise.resolve(writer.end());
      } catch {
        /* swallow */
      }
    },
    toBytes() {
      return null;
    },
  };
}

async function createFileHandleSink(handle: {
  createWritable(): Promise<unknown>;
}): Promise<OutputSink> {
  const writable = (await handle.createWritable()) as {
    write: (chunk: Uint8Array) => Promise<void>;
    close: () => Promise<void>;
    abort?: (reason?: unknown) => Promise<void>;
  };
  return {
    async write(chunk) {
      await writable.write(chunk);
    },
    async finalize() {
      await writable.close();
    },
    async abort(reason) {
      if (writable.abort) await writable.abort(reason);
      else await writable.close();
    },
    toBytes() {
      return null;
    },
  };
}
