/**
 * Plaintext schemas for stream control messages.
 *
 * Stream-init / -finish / -abort / -resume ride the existing 0x02 ratchet
 * envelope — Double Ratchet AEAD already authenticates them and rejects
 * replays. Each carries a JSON plaintext with a `kind` discriminator.
 * Stream-chunk uses dedicated wire type 0x11 (see `@shade/proto/wire.ts`).
 */
import { ValidationError } from '@shade/core';
import { validateFileMetadata } from './file-metadata.js';
import type { LaneInitSpec, StreamMetadata } from './types.js';

export type StreamControlKind =
  | 'shade.stream-init/v1'
  | 'shade.stream-finish/v1'
  | 'shade.stream-abort/v1'
  | 'shade.stream-resume-request/v1'
  | 'shade.stream-resume-state/v1';

export interface StreamInitMessage {
  kind: 'shade.stream-init/v1';
  /** base64url-encoded 16-byte streamId. */
  streamId: string;
  /** base64url-encoded 32-byte streamSecret. */
  streamSecret: string;
  metadata: StreamMetadata;
  lanes: LaneInitSpec[];
}

export interface StreamFinishMessage {
  kind: 'shade.stream-finish/v1';
  streamId: string;
  /** Per-lane integrity fingerprints, base64-encoded sha256 of plaintext bytes the lane carried. */
  laneSha256: Array<{
    laneId: number;
    sha256: string;
    chunkCount: number;
    byteCount: number;
  }>;
  /** base64-encoded sha256 over the original plaintext file in original byte order. */
  overallSha256: string;
  finishedAt: number;
}

export type StreamAbortReason = 'sender-cancel' | 'receiver-cancel' | 'fatal-error';

export interface StreamAbortMessage {
  kind: 'shade.stream-abort/v1';
  streamId: string;
  reason: StreamAbortReason;
  message?: string;
  abortedAt: number;
}

export interface StreamResumeRequestMessage {
  kind: 'shade.stream-resume-request/v1';
  streamId: string;
  requestedAt: number;
}

export interface StreamResumeStateMessage {
  kind: 'shade.stream-resume-state/v1';
  streamId: string;
  /** lastSeqAcked = -1 means no chunk for this lane has been received yet. */
  lanes: Array<{ laneId: number; lastSeqAcked: number }>;
  resumedAt: number;
}

export type StreamControlMessage =
  | StreamInitMessage
  | StreamFinishMessage
  | StreamAbortMessage
  | StreamResumeRequestMessage
  | StreamResumeStateMessage;

/** Type guard: does the value look like a stream control message? */
export function isStreamControlMessage(value: unknown): value is StreamControlMessage {
  if (typeof value !== 'object' || value === null) return false;
  const kind = (value as { kind?: unknown }).kind;
  if (typeof kind !== 'string') return false;
  return kind.startsWith('shade.stream-');
}

/** Parse JSON plaintext; throw ValidationError on shape mismatch. */
export function parseStreamControl(plaintext: string): StreamControlMessage {
  let parsed: unknown;
  try {
    parsed = JSON.parse(plaintext);
  } catch (err) {
    throw new ValidationError(
      `stream control plaintext is not valid JSON: ${(err as Error).message}`,
      'plaintext',
    );
  }
  if (!isStreamControlMessage(parsed)) {
    throw new ValidationError('plaintext is not a stream control message', 'plaintext');
  }
  if (parsed.kind === 'shade.stream-init/v1' && parsed.metadata?.fileMetadata !== undefined) {
    validateFileMetadata(parsed.metadata.fileMetadata);
  }
  return parsed;
}

/** Encode a stream control message as JSON; throws on circular refs. */
export function encodeStreamControl(msg: StreamControlMessage): string {
  if (msg.kind === 'shade.stream-init/v1' && msg.metadata?.fileMetadata !== undefined) {
    validateFileMetadata(msg.metadata.fileMetadata);
  }
  return JSON.stringify(msg);
}
