import { ulid } from './ulid';
import type {
  CrossRealmEnvelopeLegacy,
  ReplyEnvelopeLegacy,
  TaggedEnvelope,
  MxEnvelopeV1,
  MxMsgType,
  MxAddress,
} from './envelope';
import { errorMessage, isRecord, safeJsonParse, type JsonObject } from './util/guards';

export function nowIso(): string {
  return new Date().toISOString();
}

export function generateCorrelationId(): string {
  return ulid();
}

const MX_MSG_TYPES: readonly MxMsgType[] = ['Cmd', 'Evt', 'Result', 'Sub', 'Unsub', 'Nack'] as const;

function isMxMsgType(v: unknown): v is MxMsgType {
  return typeof v === 'string' && (MX_MSG_TYPES as readonly string[]).includes(v);
}

function isMxAddress(v: unknown): v is MxAddress {
  if (!isRecord(v)) return false;
  // Accept both 'root' and legacy 'realm' field
  const rootVal = v.root ?? v.realm;
  return typeof rootVal === 'string' && (rootVal as string).length > 0 && typeof v.path === 'string' && v.path.length > 0;
}

/** Normalize an MxAddress from wire format (may have 'realm' instead of 'root') */
function normalizeMxAddress(v: Record<string, unknown>): MxAddress {
  const root = (v.root ?? v.realm) as string;
  return { root, path: v.path as string };
}

function isMxEnvelopeV1(v: unknown): v is MxEnvelopeV1 {
  if (!isRecord(v)) return false;
  if (v.ver !== 1) return false;
  if (!isMxMsgType(v.msgType)) return false;
  if (typeof v.msgId !== 'string' || v.msgId.length === 0) return false;
  if (typeof v.timestamp !== 'string' || v.timestamp.length === 0) return false;
  if (!isMxAddress(v.to)) return false;
  if (!isMxAddress(v.from)) return false;
  if (v.replyTo !== undefined && !isMxAddress(v.replyTo)) return false;

  if (v.correlationId !== undefined && typeof v.correlationId !== 'string') return false;
  if (v.ttl !== undefined && typeof v.ttl !== 'number') return false;

  // Tracing fields (optional strings)
  if (v.traceId !== undefined && typeof v.traceId !== 'string') return false;
  if (v.spanId !== undefined && typeof v.spanId !== 'string') return false;
  if (v.parentSpanId !== undefined && typeof v.parentSpanId !== 'string') return false;

  // Result fields
  if (v.ok !== undefined && typeof v.ok !== 'boolean') return false;
  if (v.error !== undefined) {
    if (!isRecord(v.error)) return false;
    if (typeof v.error.code !== 'string' || typeof v.error.message !== 'string') return false;
  }
  // body may be any JSON value
  return true;
}

/** Normalize a parsed MxEnvelopeV1 so addresses use 'root' consistently */
function normalizeMxEnvelopeV1(v: Record<string, unknown>): MxEnvelopeV1 {
  const env = { ...v } as Record<string, unknown>;
  env.to = normalizeMxAddress(v.to as Record<string, unknown>);
  env.from = normalizeMxAddress(v.from as Record<string, unknown>);
  if (v.replyTo) {
    env.replyTo = normalizeMxAddress(v.replyTo as Record<string, unknown>);
  }
  return env as unknown as MxEnvelopeV1;
}

function isLegacyRequest(v: unknown): v is CrossRealmEnvelopeLegacy {
  if (!isRecord(v)) return false;
  // Accept both toRoot/fromRoot and legacy toRealm/fromRealm
  const toRoot = v.toRoot ?? v.toRealm;
  const fromRoot = v.fromRoot ?? v.fromRealm;
  return (
    typeof toRoot === 'string' &&
    typeof v.toPath === 'string' &&
    typeof fromRoot === 'string' &&
    typeof v.replyTo === 'string' &&
    typeof v.correlationId === 'string'
  );
}

/** Normalize legacy request to use toRoot/fromRoot */
function normalizeLegacyRequest(v: Record<string, unknown>): CrossRealmEnvelopeLegacy {
  const toRoot = (v.toRoot ?? v.toRealm) as string;
  const fromRoot = (v.fromRoot ?? v.fromRealm) as string;
  return {
    ...v,
    toRoot,
    fromRoot,
    toRealm: toRoot,
    fromRealm: fromRoot,
  } as unknown as CrossRealmEnvelopeLegacy;
}

function isLegacyReply(v: unknown): v is ReplyEnvelopeLegacy {
  if (!isRecord(v)) return false;
  if (typeof v.ok !== 'boolean') return false;
  const toRoot = v.toRoot ?? v.toRealm;
  const fromRoot = v.fromRoot ?? v.fromRealm;
  return (
    typeof toRoot === 'string' &&
    typeof v.toPath === 'string' &&
    typeof fromRoot === 'string' &&
    typeof v.correlationId === 'string'
  );
}

/** Normalize legacy reply to use toRoot/fromRoot */
function normalizeLegacyReply(v: Record<string, unknown>): ReplyEnvelopeLegacy {
  const toRoot = (v.toRoot ?? v.toRealm) as string;
  const fromRoot = (v.fromRoot ?? v.fromRealm) as string;
  return {
    ...v,
    toRoot,
    fromRoot,
    toRealm: toRoot,
    fromRealm: fromRoot,
  } as unknown as ReplyEnvelopeLegacy;
}

export function deserializeTaggedEnvelope(json: string): TaggedEnvelope | null {
  const parsed = safeJsonParse(json);
  if (!isRecord(parsed)) return null;

  // MXENV/1 detection
  if (isMxEnvelopeV1(parsed)) {
    return { profile: 'mxenv1', envelope: normalizeMxEnvelopeV1(parsed) };
  }

  // Legacy Matrix-3
  if (isLegacyReply(parsed)) {
    return { profile: 'matrix3', kind: 'reply', envelope: normalizeLegacyReply(parsed) };
  }
  if (isLegacyRequest(parsed)) {
    return { profile: 'matrix3', kind: 'request', envelope: normalizeLegacyRequest(parsed) };
  }

  return null;
}

export interface LegacyEnvelopeValidationResult {
  ok: boolean;
  error?: string;
}

export function validateLegacyRequestEnvelope(e: CrossRealmEnvelopeLegacy): LegacyEnvelopeValidationResult {
  if (!e || typeof e !== 'object') return { ok: false, error: 'not an object' };

  const reqFields: readonly string[] = ['toRoot', 'toPath', 'fromRoot', 'replyTo', 'correlationId'];
  for (const f of reqFields) {
    const v = (e as unknown as JsonObject)[f];
    if (typeof v !== 'string' || v.length === 0) {
      return { ok: false, error: `missing/invalid field ${f}` };
    }
  }
  // toPath must contain a plane token
  if (!e.toPath.includes('.accepts.') && !e.toPath.includes('.emits.')) {
    return { ok: false, error: 'toPath must contain .accepts. or .emits.' };
  }
  return { ok: true };
}

// ===== Encoding helpers for Matrix-3 legacy =====

export function createLegacyRequest(params: {
  toRoot: string;
  toPath: string;
  fromRoot: string;
  replyTo: string;
  correlationId?: string;
  body: unknown;
  timestamp?: string;
  ttlMs?: number;
  traceId?: string;
  type?: 'request' | 'event';
  /** @deprecated Use toRoot */
  toRealm?: string;
  /** @deprecated Use fromRoot */
  fromRealm?: string;
}): CrossRealmEnvelopeLegacy {
  const toRoot = params.toRoot ?? params.toRealm!;
  const fromRoot = params.fromRoot ?? params.fromRealm!;
  return {
    toRoot,
    toPath: params.toPath,
    fromRoot,
    replyTo: params.replyTo,
    correlationId: params.correlationId ?? generateCorrelationId(),
    body: params.body,
    timestamp: params.timestamp ?? nowIso(),
    ttlMs: params.ttlMs,
    traceId: params.traceId,
    type: params.type,
    // Wire compat aliases
    toRealm: toRoot,
    fromRealm: fromRoot,
  };
}

export function createLegacyReply(params: {
  request: CrossRealmEnvelopeLegacy;
  responderRoot: string;
  body: unknown;
  ok: boolean;
  error?: ReplyEnvelopeLegacy['error'];
  timestamp?: string;
  traceId?: string;
  type?: 'reply';
  /** @deprecated Use responderRoot */
  responderRealm?: string;
}): ReplyEnvelopeLegacy {
  const responderRoot = params.responderRoot ?? params.responderRealm!;
  const toRoot = params.request.fromRoot ?? params.request.fromRealm!;
  return {
    toRoot,
    toPath: params.request.replyTo,
    fromRoot: responderRoot,
    correlationId: params.request.correlationId,
    body: params.body,
    ok: params.ok,
    error: params.error,
    timestamp: params.timestamp ?? nowIso(),
    traceId: params.traceId,
    type: params.type,
    // Wire compat aliases
    toRealm: toRoot,
    fromRealm: responderRoot,
  };
}

export function serializeLegacyEnvelope(e: CrossRealmEnvelopeLegacy | ReplyEnvelopeLegacy): string {
  return JSON.stringify(e);
}

// ===== MXENV/1 encoding =====

function addr(root: string, path: string): MxAddress {
  return { root, path, realm: root };
}

export function createMxEnvV1(params: {
  msgType: MxMsgType;
  toRoot: string;
  toPath: string;
  fromRoot: string;
  fromPath?: string;
  replyToRoot?: string;
  replyToPath?: string;
  correlationId?: string;
  msgId?: string;
  ttl?: number;
  // Tracing (synced with matrix-transport)
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  body: unknown;
  ok?: boolean;
  error?: MxEnvelopeV1['error'];
  /** @deprecated Use toRoot */
  toRealm?: string;
  /** @deprecated Use fromRoot */
  fromRealm?: string;
  /** @deprecated Use replyToRoot */
  replyToRealm?: string;
}): MxEnvelopeV1 {
  const msgId = params.msgId ?? ulid();
  const toRoot = params.toRoot ?? params.toRealm!;
  const fromRoot = params.fromRoot ?? params.fromRealm!;
  const replyToRoot = params.replyToRoot ?? params.replyToRealm;

  // Build in canonical field order (synced with matrix-transport MxEnvelopeCodec)
  const base: MxEnvelopeV1 = {
    ver: 1,
    msgType: params.msgType,
    msgId,
    timestamp: nowIso(),
    correlationId: params.correlationId,
    to: addr(toRoot, params.toPath),
    from: addr(fromRoot, params.fromPath ?? '_client'),
    ttl: params.ttl,
    traceId: params.traceId,
    spanId: params.spanId ?? msgId,
    parentSpanId: params.parentSpanId,
    body: params.body,
    ok: params.ok,
    error: params.error,
  };

  if (replyToRoot && params.replyToPath) {
    base.replyTo = addr(replyToRoot, params.replyToPath);
  }

  return base;
}

export function serializeMxEnvV1(e: MxEnvelopeV1): string {
  return JSON.stringify(e);
}

// Small helper used by callers to normalize errors into replies.
export function toErrorMessage(e: unknown): string {
  return errorMessage(e);
}
