import { TransferAbortError, TransferTransportError } from '../errors.js';
import { withRetry, type RetryPolicy } from '../retry.js';
import type {
  ChunkAck,
  ChunkSendOptions,
  ITransferTransport,
  TransferResumeState,
} from './transport.js';

/**
 * Resolves the base URL for a given peer address.
 *
 * In M-Stream-5 the SDK wires this to the prekey-server's directory or to a
 * peer-supplied `transfer.baseUrl` field on the prekey bundle. For
 * standalone use, the caller can pass a static map.
 */
export type PeerBaseUrlResolver = (peerAddress: string) => Promise<string>;

/**
 * Authentication hook. Adds outgoing request headers and produces a
 * "request signature" that the receiver verifies. The default is a no-op
 * authenticator suitable for trusted/local networks; for production use the
 * SDK provides an Ed25519-signing implementation in M-Stream-5.
 */
export interface TransferAuthenticator {
  signChunk(args: {
    streamId: string;
    laneId: number;
    seq: bigint;
    bodyHash: Uint8Array;
  }): Promise<Record<string, string>>;
  signControl(args: { streamId: string; method: string; path: string }): Promise<
    Record<string, string>
  >;
}

export const NoopAuthenticator: TransferAuthenticator = {
  async signChunk() {
    return {};
  },
  async signControl() {
    return {};
  },
};

export interface ShadeTransferHttpTransportOptions {
  /** Resolves the peer's HTTP base URL (e.g., `https://server.example.com/shade`). */
  resolveBaseUrl: PeerBaseUrlResolver;
  /** Optional authenticator (defaults to no-op). */
  authenticator?: TransferAuthenticator;
  /** Override `fetch` (e.g., for tests). Defaults to `globalThis.fetch`. */
  fetch?: typeof fetch;
  /** Retry policy for chunk POSTs. */
  retryPolicy?: RetryPolicy;
}

/**
 * HTTP-based chunk transport. POSTs each `0x11` envelope to
 * `<base>/v1/transfer/:streamId/chunk` and parses the JSON ACK response.
 *
 * Mounting on the receiver side: `createTransferRoutes(engine, options)`.
 */
export class ShadeTransferHttpTransport implements ITransferTransport {
  private readonly fetchFn: typeof fetch;
  private readonly auth: TransferAuthenticator;

  constructor(private readonly options: ShadeTransferHttpTransportOptions) {
    this.fetchFn = options.fetch ?? globalThis.fetch;
    this.auth = options.authenticator ?? NoopAuthenticator;
  }

  async probe(peerAddress: string): Promise<void> {
    const base = await this.options.resolveBaseUrl(peerAddress);
    const url = `${stripTrailingSlash(base)}/v1/transfer/health`;
    let res: Response;
    try {
      res = await this.fetchFn(url, { method: 'GET' });
    } catch (err) {
      throw new TransferTransportError(`probe failed: ${(err as Error).message}`);
    }
    if (!res.ok) {
      throw new TransferTransportError(`probe failed: HTTP ${res.status}`, res.status);
    }
  }

  async sendChunk(
    peerAddress: string,
    streamId: string,
    laneId: number,
    seq: number | bigint,
    bytes: Uint8Array,
    options?: ChunkSendOptions,
  ): Promise<ChunkAck> {
    const base = await this.options.resolveBaseUrl(peerAddress);
    const url = `${stripTrailingSlash(base)}/v1/transfer/${encodeURIComponent(streamId)}/chunk`;
    const seqBig = typeof seq === 'bigint' ? seq : BigInt(seq);
    const bodyHash = await sha256(bytes);
    const headers: Record<string, string> = {
      'Content-Type': 'application/octet-stream',
      'X-Shade-Lane-Id': String(laneId),
      'X-Shade-Seq': seqBig.toString(),
    };
    Object.assign(
      headers,
      await this.auth.signChunk({ streamId, laneId, seq: seqBig, bodyHash }),
    );

    const policy = this.options.retryPolicy;
    return withRetry(
      async () => {
        let res: Response;
        try {
          res = await this.fetchFn(url, {
            method: 'POST',
            headers,
            // BodyInit isn't in lib:["ES2022"]; cast through unknown.
            body: bytes as unknown as never,
            ...(options?.signal !== undefined ? { signal: options.signal } : {}),
          });
        } catch (err) {
          if ((err as { name?: string }).name === 'AbortError') {
            throw new TransferAbortError('signal aborted');
          }
          throw new TransferTransportError(`sendChunk failed: ${(err as Error).message}`);
        }
        if (!res.ok) {
          const text = await res.text().catch(() => '');
          throw new TransferTransportError(
            `sendChunk failed: HTTP ${res.status} ${text}`,
            res.status,
          );
        }
        const body = (await res.json()) as { lastSeq: number; bytesReceived?: number };
        return {
          lastSeq: body.lastSeq,
          ...(body.bytesReceived !== undefined ? { bytesReceived: body.bytesReceived } : {}),
        };
      },
      {
        ...(policy !== undefined ? { policy } : {}),
        ...(options?.signal !== undefined ? { signal: options.signal } : {}),
      },
    );
  }

  async fetchResumeState(
    peerAddress: string,
    streamId: string,
  ): Promise<TransferResumeState | null> {
    const base = await this.options.resolveBaseUrl(peerAddress);
    const url = `${stripTrailingSlash(base)}/v1/transfer/${encodeURIComponent(streamId)}/state`;
    const headers = await this.auth.signControl({ streamId, method: 'GET', path: url });
    const res = await this.fetchFn(url, { method: 'GET', headers });
    if (res.status === 404) return null;
    if (!res.ok) {
      throw new TransferTransportError(`fetchResumeState failed: HTTP ${res.status}`, res.status);
    }
    return (await res.json()) as TransferResumeState;
  }
}

function stripTrailingSlash(s: string): string {
  return s.endsWith('/') ? s.slice(0, -1) : s;
}

async function sha256(data: Uint8Array): Promise<Uint8Array> {
  const buf = await globalThis.crypto.subtle.digest('SHA-256', data as unknown as ArrayBuffer);
  return new Uint8Array(buf);
}
