import type { CryptoProvider, ShadeEnvelope } from '@shade/core';
import { ValidationError } from '@shade/core';
import {
  encodeStreamControl,
  parseStreamControl,
  type StreamControlMessage,
} from '@shade/streams';
import {
  type IControlChannel,
  type TransferAuthenticator,
} from '@shade/transfer';
import type { Shade } from './shade.js';

/**
 * Adapter contract for shipping ratchet envelopes between two Shade
 * instances. The SDK provides an HTTP implementation that POSTs to the
 * peer's `transferRoute()` control endpoint; tests use a memory pair.
 */
export interface ControlEnvelopeTransport {
  send(peerAddress: string, envelope: ShadeEnvelope): Promise<void>;
}

/**
 * `IControlChannel` implementation that rides on top of an existing Shade
 * instance's `send`/`receive` API. Each control message is JSON-encoded
 * plaintext and shipped through the Double Ratchet — the same path as
 * regular chat messages.
 *
 * Receiver-side: the SDK installs a `Shade.onMessage` handler at
 * construction. Plaintext is parsed for the `shade.stream-` `kind` prefix;
 * matches are dispatched to subscribed handlers, non-matches are passed
 * through to the consumer's regular onMessage handlers via `passthrough`.
 */
export class ShadeControlChannel implements IControlChannel {
  private readonly handlers = new Set<
    (from: string, message: StreamControlMessage) => void | Promise<void>
  >();
  private readonly unsubscribeShadeMessages: () => void;

  constructor(
    private readonly shade: Shade,
    private readonly envelopeTransport: ControlEnvelopeTransport,
    private readonly passthrough?: (from: string, plaintext: string) => void,
  ) {
    this.unsubscribeShadeMessages = shade.onMessage(async (from, plaintext) => {
      if (!plaintext.includes('shade.stream-')) {
        this.passthrough?.(from, plaintext);
        return;
      }
      let msg: StreamControlMessage;
      try {
        msg = parseStreamControl(plaintext);
      } catch {
        // Not a stream control message; treat as ordinary plaintext.
        this.passthrough?.(from, plaintext);
        return;
      }
      // Await each handler. The engine relies on this for sequencing:
      // sender's `controlChannel.send` only resolves once the receiver has
      // fully processed `stream-init`, so chunks never race ahead.
      for (const handler of [...this.handlers]) {
        try {
          await handler(from, msg);
        } catch (err) {
          console.error('[ShadeControlChannel] handler error:', err);
        }
      }
    });
  }

  async send(peerAddress: string, message: StreamControlMessage): Promise<void> {
    const envelope = await this.shade.send(peerAddress, encodeStreamControl(message));
    await this.envelopeTransport.send(peerAddress, envelope);
  }

  /**
   * Receive-side: hand a freshly-decrypted envelope from the peer to this
   * channel. The plaintext is fed through the same `onMessage` pipeline.
   * Used by `Shade.acceptTransferEnvelope` for control-plane messages.
   */
  async acceptEnvelope(from: string, envelope: ShadeEnvelope): Promise<void> {
    await this.shade.receive(from, envelope);
  }

  onMessage(
    handler: (from: string, message: StreamControlMessage) => void | Promise<void>,
  ): () => void {
    this.handlers.add(handler);
    return () => this.handlers.delete(handler);
  }

  destroy(): void {
    this.unsubscribeShadeMessages();
    this.handlers.clear();
  }
}

/**
 * Ed25519-signing authenticator for `ShadeTransferHttpTransport`.
 *
 * Header format on `/chunk`:
 *   `X-Shade-Sender-Address: <address>`
 *   `X-Shade-Signed-At: <unix-ms>`
 *   `X-Shade-Signature: <base64-of-sig-over: address|signedAt|streamId|laneId|seq|sha256(body)>`
 *
 * The receiver-side `ShadeTransferRouteAuthenticator` (below) verifies this
 * using the sender's public identity key fetched from the prekey server.
 */
export class ShadeTransferAuthenticator implements TransferAuthenticator {
  constructor(
    private readonly crypto: CryptoProvider,
    private readonly senderAddress: string,
    private readonly signingPrivateKey: Uint8Array,
  ) {
    if (signingPrivateKey.length !== 32) {
      throw new ValidationError('signingPrivateKey must be 32 bytes', 'signingPrivateKey');
    }
  }

  async signChunk(args: {
    streamId: string;
    laneId: number;
    seq: bigint;
    bodyHash: Uint8Array;
  }): Promise<Record<string, string>> {
    const signedAt = Date.now();
    const message = canonicalChunkBytes({
      address: this.senderAddress,
      signedAt,
      streamId: args.streamId,
      laneId: args.laneId,
      seq: args.seq,
      bodyHash: args.bodyHash,
    });
    const sig = await this.crypto.sign(this.signingPrivateKey, message);
    return {
      'X-Shade-Sender-Address': this.senderAddress,
      'X-Shade-Signed-At': String(signedAt),
      'X-Shade-Signature': bytesToBase64(sig),
    };
  }

  async signControl(args: {
    streamId: string;
    method: string;
    path: string;
  }): Promise<Record<string, string>> {
    const signedAt = Date.now();
    const message = canonicalControlBytes({
      address: this.senderAddress,
      signedAt,
      streamId: args.streamId,
      method: args.method,
      path: args.path,
    });
    const sig = await this.crypto.sign(this.signingPrivateKey, message);
    return {
      'X-Shade-Sender-Address': this.senderAddress,
      'X-Shade-Signed-At': String(signedAt),
      'X-Shade-Signature': bytesToBase64(sig),
    };
  }
}

/** Helper: canonical bytes for chunk signature. */
export function canonicalChunkBytes(args: {
  address: string;
  signedAt: number;
  streamId: string;
  laneId: number;
  seq: bigint;
  bodyHash: Uint8Array;
}): Uint8Array {
  const enc = new TextEncoder();
  const fields = [
    `chunk\0`,
    `addr=${args.address}\0`,
    `at=${args.signedAt}\0`,
    `sid=${args.streamId}\0`,
    `lane=${args.laneId}\0`,
    `seq=${args.seq.toString()}\0`,
    `bodyHash=${bytesToHex(args.bodyHash)}\0`,
  ];
  return enc.encode(fields.join(''));
}

/** Helper: canonical bytes for control signature. */
export function canonicalControlBytes(args: {
  address: string;
  signedAt: number;
  streamId: string;
  method: string;
  path: string;
}): Uint8Array {
  const enc = new TextEncoder();
  const fields = [
    `control\0`,
    `addr=${args.address}\0`,
    `at=${args.signedAt}\0`,
    `sid=${args.streamId}\0`,
    `method=${args.method}\0`,
    `path=${args.path}\0`,
  ];
  return enc.encode(fields.join(''));
}

function bytesToBase64(b: Uint8Array): string {
  let bin = '';
  for (let i = 0; i < b.length; i++) bin += String.fromCharCode(b[i]!);
  return btoa(bin);
}

function bytesToHex(b: Uint8Array): string {
  return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
}
