/**
 * FederationPeer - Unified cross-root bridge
 *
 * Replaces both EdgeGateway (daemon) and CrossRealmRouter (browser) with a
 * single shared implementation. Configuration-only differences, identical code.
 *
 * PRINCIPLE: "Symmetric peer-to-peer - should work no matter how many brokers
 * we put in between."
 *
 * Features from EdgeGateway:
 * - Security: policy, rate limiter, replay cache, anti-relay
 * - Legacy Matrix3 format support (optional)
 * - Metrics tracking
 * - includeInternalMeta/includeInboundEnvelope options
 *
 * Features from CrossRealmRouter:
 * - invoke() RPC with local/cross-root routing
 * - Tab isolation via InstanceResolver
 * - Group channel subscription (broadcast discovery)
 * - Lazy backbone initialization
 * - PATH_NOT_FOUND check (optional)
 */

import type { FlushableTransport, Transport } from './transport/base';
import { getInternalTopic, getLegacyIngressTopic, getMxIngressTopic } from './topics';
import {
  createLegacyReply,
  createMxEnvV1,
  deserializeTaggedEnvelope,
  generateCorrelationId,
  serializeLegacyEnvelope,
  serializeMxEnvV1,
} from './envelope_codec';
import type {
  CrossRealmEnvelopeLegacy,
  MxEnvelopeV1,
  ReplyEnvelopeLegacy,
  TaggedEnvelope,
} from './envelope';
import type { WireProfile } from './wire_profile';
import { WIRE_PROFILES } from './wire_profile';
import type { Clock } from './runtime/clock';
import { SystemClock } from './runtime/clock';
import type { Policy } from './security/policy';
import { AllowAllPolicy } from './security/policy';
import type { TokenBucketRateLimiter } from './security/ratelimit';
import type { ReplayCache } from './security/replay';
import { derivePrincipal } from './security/principal';
import type { InternalMeta, InternalRequest } from './runtime/internal_messages';
import { normalizeInternalReply } from './runtime/internal_messages';
import { DEFAULT_SHARD } from './runtime/shard_allocator';
import { parseRootIdentity, isRootInstance } from './root_identity';
import { CrossRealmClient } from './cross_realm_client';

/**
 * @deprecated TS-8 DORMANT: ingress/shard envelope path is dormant until Phase 2
 * cross-account auth metadata is finalized.
 */
export type DormantMxEnvV1 = MxEnvelopeV1;
/** Reason for security block. */
export type SecurityBlockReason = 'anti_relay' | 'replay' | 'rate_limit' | 'policy';

/**
 * Target shard for all outbound messages.
 * Single-shard model: everyone uses DEFAULT_SHARD ("00").
 */
const TARGET_SHARD = DEFAULT_SHARD;

/**
 * Parse an accepts-format path into mount + op.
 *
 * FP-20b: Actors use $inbox with op-based dispatch. Old paths use .accepts.OP format.
 * This helper converts old-format paths for routing to $inbox.
 *
 * 'daemon.accepts.$introspect'      -> { mount: 'daemon', op: '$introspect' }
 * 'projects.my-app.accepts.list'    -> { mount: 'projects.my-app', op: 'list' }
 * 'daemon/$inbox'                   -> { mount: 'daemon', op: null }
 * 'daemon'                          -> { mount: 'daemon', op: null }
 */
function parseAcceptsPath(path: string): { mount: string; op: string | null } {
  // Internal federation management endpoints are topic-addressed directly.
  // Keep "_federation.accepts.*" as a raw internal topic so SubscriptionForwarder
  // receives the request on _federation.accepts.* (semantic topic).
  if (path.startsWith('_federation.accepts.')) {
    return { mount: path, op: null };
  }

  const idx = path.lastIndexOf('.accepts.');
  if (idx >= 0) {
    return {
      mount: path.slice(0, idx),
      op: path.slice(idx + '.accepts.'.length),
    };
  }
  return { mount: path, op: null };
}

// ===================================================================
// Instance Resolver - Abstraction for tab/shard isolation
// ===================================================================

/**
 * Determines if a target root is "local" and rewrites topics as needed.
 * Different implementations for daemon (exact match) and browser (tab isolation).
 */
export interface InstanceResolver {
  /** Base routing root for a tab or shard-local resolver. */
  readonly routingBaseRoot?: string;

  /** Check if target root routes locally */
  isLocal(targetRoot: string): boolean;

  /** Rewrite topic for local routing (e.g., add tab suffix) */
  rewriteForLocal(topic: string): string;
}

/**
 * Simple exact-match resolver for daemon.
 * Only the exact local root is considered local.
 */
export class ExactInstanceResolver implements InstanceResolver {
  readonly routingBaseRoot: string;

  constructor(private readonly localRoot: string) {
    this.routingBaseRoot = localRoot;
  }

  isLocal(targetRoot: string): boolean {
    return targetRoot === this.localRoot;
  }

  rewriteForLocal(topic: string): string {
    return topic; // No rewriting needed for daemon
  }
}

/**
 * Tab-aware resolver for browser.
 * Handles: flowpad -> flowpad.tab-abc, flowpad.tab-abc -> flowpad.tab-abc
 */
export class TabInstanceResolver implements InstanceResolver {
  private readonly localRoot: string;
  private readonly baseRoot: string;
  readonly routingBaseRoot: string;

  constructor(localRoot: string) {
    this.localRoot = localRoot;
    this.baseRoot = localRoot.replace(/\.tab-[a-z0-9]+$/, '');
    this.routingBaseRoot = this.baseRoot;
  }

  isLocal(targetRoot: string): boolean {
    // Exact match
    if (targetRoot === this.localRoot) return true;

    // Base root match (flowpad -> flowpad.tab-abc)
    if (targetRoot === this.baseRoot) return true;

    // Different tab = not local
    if (targetRoot.startsWith(this.baseRoot + '.tab-')) {
      return targetRoot === this.localRoot;
    }

    return false;
  }

  rewriteForLocal(topic: string): string {
    // With DNS hierarchical addressing, topics are semantic (no mx/1/ prefix).
    // Just handle root-based rewriting for tab isolation.
    return this.rewriteMatrixUri(topic);
  }

  /**
   * Rewrite URI for tab isolation.
   *
   * Examples (localRoot = 'flowpad.tab-abc', baseRoot = 'flowpad'):
   *   flowpad.db.accepts.Query -> flowpad.tab-abc.db.accepts.Query
   *   flowpad.tab-abc.db.accepts.Query -> flowpad.tab-abc.db.accepts.Query (no change)
   *   flowpad/db.accepts.Query -> flowpad.tab-abc.db.accepts.Query
   *   other.db.accepts.Query -> other.db.accepts.Query (no change)
   */
  private rewriteMatrixUri(topic: string): string {
    const prefix = '';
    const body = topic.slice(prefix.length);

    // Already has full localRoot (with tab suffix) - no rewrite needed
    if (body.startsWith(this.localRoot + '.') || body.startsWith(this.localRoot + '/')) {
      // Convert slash format to dot format if needed
      if (body.startsWith(this.localRoot + '/')) {
        const rest = body.slice(this.localRoot.length + 1);
        return prefix + this.localRoot + '.' + rest;
      }
      return topic;
    }

    // Check if topic starts with base root
    if (body.startsWith(this.baseRoot + '.')) {
      const rest = body.slice(this.baseRoot.length); // Keep the dot
      return prefix + this.localRoot + rest;
    }
    if (body.startsWith(this.baseRoot + '/')) {
      const rest = body.slice(this.baseRoot.length + 1);
      return prefix + this.localRoot + '.' + rest;
    }

    return topic;
  }
}

// ===================================================================
// Types
// ===================================================================

export interface FederationPeerMetrics {
  inboundRequests: number;
  inboundReplies: number;
  outboundReplies: number;
  timeouts: number;
  securityBlocks: { antiRelay: number; replay: number; rateLimit: number; policy: number };
  pendingRequests: number;
}

export interface FederationPeerOptions {
  // Identity
  localRoot: string;
  /** @deprecated Use localRoot */
  localRealm?: string;
  shard?: string;

  // Transports (backbone optional for lazy init)
  localBus: Transport;
  backbone?: Transport;

  // Behavior
  profile?: WireProfile;
  internalTimeoutMs?: number;

  /** Subscribe to group channel for broadcast discovery (Phase 1B) */
  subscribeToGroupChannel?: boolean;

  /** Check if local handler exists before routing (PATH_NOT_FOUND) */
  checkLocalSubscriber?: boolean;

  /** Instance resolver for tab/shard isolation */
  instanceResolver?: InstanceResolver;

  // Internal request options
  /** Include _mx meta in internal requests (default: true) */
  includeInternalMeta?: boolean;

  /** Include inbound envelope in internal request payload */
  includeInboundEnvelope?: boolean;

  // Security (optional - both daemon and browser can use)
  securityPolicy?: Policy;
  rateLimiter?: TokenBucketRateLimiter;
  replayCache?: ReplayCache;
  enforceAntiRelay?: boolean;

  // Clocks / Observability
  clock?: Clock;

  onInboundRequest?: (envelope: TaggedEnvelope, internalTopic: string) => void;
  onInboundReply?: (envelope: TaggedEnvelope) => void;
  onOutboundReply?: (replyEnvelope: ReplyEnvelopeLegacy | MxEnvelopeV1, ingressTopic: string) => void;
  onTimeout?: (correlationId: string, toPath: string) => void;
  onDrop?: (reason: string, topic: string, rawPayload: string) => void;
  onSecurityBlock?: (reason: SecurityBlockReason, envelope: TaggedEnvelope) => void;

  /** Enable verbose logging. Defaults to false. */
  logging?: boolean;
}

interface SubscriberAwareTransport extends Transport {
  hasSubscriber(topic: string): boolean;
}

function isSubscriberAwareTransport(transport: Transport): transport is SubscriberAwareTransport {
  const candidate: Partial<SubscriberAwareTransport> = transport;
  return typeof candidate.hasSubscriber === 'function';
}

// ===================================================================
// FederationPeer - Unified Implementation
// ===================================================================

/**
 * FederationPeer is the unified bridge between:
 *   - the backbone transport (NATS leaf-node, cloud, etc.)
 *   - the local root bus (in-memory or NATS)
 *
 * Same class for daemon and browser. Configuration-only differences.
 */
export class FederationPeer {
  readonly localRoot: string;
  /** @deprecated Use localRoot */
  get localRealm(): string { return this.localRoot; }
  private readonly localBus: Transport;
  private backbone: Transport | null;

  private readonly profile: WireProfile;
  private readonly shard: string;
  private readonly internalTimeoutMs: number;
  private readonly subscribeToGroupChannel: boolean;
  private readonly checkLocalSubscriber: boolean;
  private readonly instanceResolver: InstanceResolver;

  private readonly includeInternalMeta: boolean;
  private readonly includeInboundEnvelope: boolean;

  private readonly policy: Policy;
  private readonly rateLimiter?: TokenBucketRateLimiter;
  private readonly replayCache?: ReplayCache;
  private readonly enforceAntiRelay: boolean;
  private readonly clock: Clock;

  private readonly onInboundRequest?: FederationPeerOptions['onInboundRequest'];
  private readonly onInboundReply?: FederationPeerOptions['onInboundReply'];
  private readonly onOutboundReply?: FederationPeerOptions['onOutboundReply'];
  private readonly onTimeout?: FederationPeerOptions['onTimeout'];
  private readonly onDrop?: FederationPeerOptions['onDrop'];
  private readonly onSecurityBlock?: FederationPeerOptions['onSecurityBlock'];
  private readonly _verbose: boolean;

  private started = false;
  private unsubBackbone: (() => void)[] = [];
  private crossRealmClient: CrossRealmClient | null = null;

  private metrics: FederationPeerMetrics = {
    inboundRequests: 0,
    inboundReplies: 0,
    outboundReplies: 0,
    timeouts: 0,
    securityBlocks: { antiRelay: 0, replay: 0, rateLimit: 0, policy: 0 },
    pendingRequests: 0,
  };

  // Construction

  static validateOptions(opts: FederationPeerOptions): { valid: boolean; errors: string[] } {
    const errors: string[] = [];
    const localRoot = opts.localRoot ?? opts.localRealm;
    if (!localRoot || localRoot.trim().length === 0) errors.push('localRoot is required');
    if (!opts.localBus) errors.push('localBus is required');
    if (opts.shard && opts.shard.trim().length === 0) errors.push('shard must be non-empty if provided');
    if (opts.internalTimeoutMs != null && (!Number.isFinite(opts.internalTimeoutMs) || opts.internalTimeoutMs < 0)) {
      errors.push('internalTimeoutMs must be a non-negative finite number (0 = no timeout)');
    }
    return { valid: errors.length === 0, errors };
  }

  constructor(opts: FederationPeerOptions) {
    this.localRoot = opts.localRoot ?? opts.localRealm!;
    this.localBus = opts.localBus;
    this.backbone = opts.backbone ?? null;

    this.profile = opts.profile ?? WIRE_PROFILES.mxenv1;
    this.shard = opts.shard ?? DEFAULT_SHARD;
    this.internalTimeoutMs = opts.internalTimeoutMs ?? 30_000;
    this.subscribeToGroupChannel = opts.subscribeToGroupChannel ?? false;
    this.checkLocalSubscriber = opts.checkLocalSubscriber ?? false;
    this.instanceResolver = opts.instanceResolver ?? new ExactInstanceResolver(this.localRoot);

    this.includeInternalMeta = opts.includeInternalMeta ?? true;
    this.includeInboundEnvelope = opts.includeInboundEnvelope ?? false;

    this.policy = opts.securityPolicy ?? new AllowAllPolicy();
    this.rateLimiter = opts.rateLimiter;
    this.replayCache = opts.replayCache;
    this.enforceAntiRelay = opts.enforceAntiRelay ?? true;
    this.clock = opts.clock ?? new SystemClock();

    this.onInboundRequest = opts.onInboundRequest;
    this.onInboundReply = opts.onInboundReply;
    this.onOutboundReply = opts.onOutboundReply;
    this.onTimeout = opts.onTimeout;
    this.onDrop = opts.onDrop;
    this.onSecurityBlock = opts.onSecurityBlock;
    this._verbose = opts.logging === true;

    // Validate eagerly
    const v = FederationPeer.validateOptions(opts);
    if (!v.valid) {
      throw new Error(`Invalid FederationPeerOptions: ${v.errors.join('; ')}`);
    }
  }

  // Lifecycle

  /**
   * Start the FederationPeer.
   * Subscribes to backbone ingress topics and optionally waits for SUBACK.
   */
  async start(): Promise<void> {
    if (this.started) return;
    this.started = true;

    if (!this.backbone) {
      if (this._verbose) console.log(`[FederationPeer] Started in local-only mode (no backbone): ${this.localRoot}`);
      return;
    }

    if (this._verbose) console.log(`[FederationPeer] Starting for localRoot: ${this.localRoot}, shard: ${this.shard}, profile: ${this.profile.name}`);

    // Subscribe to legacy ingress if profile requires
    if (this.profile.subscribeLegacyIngress) {
      const t = getLegacyIngressTopic(this.localRoot);
      if (this._verbose) console.log(`[FederationPeer] SUBSCRIBING to LEGACY ingress: ${t}`);
      this.unsubBackbone.push(this.backbone.subscribe(t, (topic, payload) => this.onBackbone(topic, payload)));
    }

    // Subscribe to instance channel (unicast)
    if (this.profile.subscribeMxIngress) {
      const t = getMxIngressTopic(this.localRoot, this.shard);
      if (this._verbose) console.log(`[FederationPeer] SUBSCRIBING to MxEnvV1 ingress: ${t}`);
      this.unsubBackbone.push(this.backbone.subscribe(t, (topic, payload) => this.onBackbone(topic, payload)));
    }

    // Subscribe to group channel (broadcast) if enabled and this is an instance root
    if (this.subscribeToGroupChannel) {
      const rootIdentity = parseRootIdentity(this.localRoot);
      if (isRootInstance(rootIdentity)) {
        const groupTopic = getMxIngressTopic(rootIdentity.group.value, this.shard);
        if (this._verbose) console.log(`[FederationPeer] SUBSCRIBING to GROUP ingress: ${groupTopic}`);
        this.unsubBackbone.push(this.backbone.subscribe(groupTopic, (topic, payload) => this.onBackbone(topic, payload)));
      }
    }

    // Wait for SUBACK if transport supports it
    if (this.isFlushable(this.backbone)) {
      await this.backbone.flushSubscriptions();
    }
  }

  /**
   * Stop the FederationPeer.
   */
  stop(): void {
    for (const u of this.unsubBackbone.splice(0)) u();
    this.started = false;

    if (this.crossRealmClient) {
      this.crossRealmClient.stop();
      this.crossRealmClient = null;
    }
  }

  /**
   * Set the backbone transport (lazy initialization).
   * Restarts ingress subscriptions if already started.
   */
  setBackbone(transport: Transport): void {
    this.backbone = transport;
    if (this._verbose) console.log(`[FederationPeer] Backbone transport set for root: ${this.localRoot}`);

    // If already started, restart subscriptions with new backbone
    if (this.started) {
      for (const u of this.unsubBackbone.splice(0)) u();
      this.started = false;
      this.start();
    }
  }

  /**
   * Check if backbone is connected.
   */
  get hasBackbone(): boolean {
    return this.backbone !== null;
  }

  /**
   * Get the backbone transport (for SubscriptionForwarder coordination).
   */
  getBackbone(): Transport | null {
    return this.backbone;
  }

  /**
   * Get the local bus transport (for SubscriptionForwarder coordination).
   */
  getLocalBus(): Transport {
    return this.localBus;
  }

  private isFlushable(transport: Transport): transport is FlushableTransport {
    return typeof (transport as FlushableTransport).flushSubscriptions === 'function';
  }

  // Metrics

  getMetrics(): FederationPeerMetrics {
    return {
      inboundRequests: this.metrics.inboundRequests,
      inboundReplies: this.metrics.inboundReplies,
      outboundReplies: this.metrics.outboundReplies,
      timeouts: this.metrics.timeouts,
      securityBlocks: { ...this.metrics.securityBlocks },
      pendingRequests: this.metrics.pendingRequests,
    };
  }

  resetMetrics(): void {
    this.metrics = {
      inboundRequests: 0,
      inboundReplies: 0,
      outboundReplies: 0,
      timeouts: 0,
      securityBlocks: { antiRelay: 0, replay: 0, rateLimit: 0, policy: 0 },
      pendingRequests: 0,
    };
  }

  // Outbound helpers

  sendMx(env: MxEnvelopeV1): void {
    if (!this.backbone) {
      if (this._verbose) console.warn('[FederationPeer] Cannot sendMx: no backbone transport');
      return;
    }
    this.backbone.publish(getMxIngressTopic(env.to.root, TARGET_SHARD), serializeMxEnvV1(env));
  }

  sendLegacy(env: CrossRealmEnvelopeLegacy | ReplyEnvelopeLegacy): void {
    // DORMANT (TS-8): retained for Phase 2 cross-account auth metadata compatibility.
    if (!this.backbone) {
      if (this._verbose) console.warn('[FederationPeer] Cannot sendLegacy: no backbone transport');
      return;
    }
    this.backbone.publish(getLegacyIngressTopic(env.toRoot), serializeLegacyEnvelope(env));
  }

  // Inbound from backbone

  private onBackbone(topic: string, raw: string): void {
    if (this._verbose) console.log(`[FederationPeer] onBackbone received on topic: ${topic}`);

    const tagged = deserializeTaggedEnvelope(raw);
    if (!tagged) {
      if (this._verbose) console.log(`[FederationPeer] Failed to deserialize envelope`);
      this.onDrop?.('decode_failed', topic, raw);
      return;
    }

    if (tagged.profile === 'matrix3') {
      this.handleLegacy(topic, raw, tagged);
      return;
    }

    this.handleMx(topic, raw, tagged.envelope as MxEnvelopeV1);
  }

  // Legacy (Matrix3) handling

  private handleLegacy(_topic: string, _raw: string, tagged: TaggedEnvelope): void {
    if (tagged.profile !== 'matrix3') return;

    if (tagged.kind === 'reply') {
      this.metrics.inboundReplies += 1;
      this.onInboundReply?.(tagged);
      return;
    }

    const req = tagged.envelope as CrossRealmEnvelopeLegacy;
    if (req.toRoot !== this.localRoot) return;

    const msgType = req.type === 'event' ? 'Evt' : 'Cmd';
    const principalKey = `legacy:${req.fromRoot}`;

    // Policy check
    const pol = this.policy.authorize({
      principal: { principalKey, kind: 'anon' },
      fromRoot: req.fromRoot,
      toRoot: req.toRoot,
      toPath: req.toPath,
      msgType,
    });
    if (pol.ok === false) {
      this.metrics.securityBlocks.policy += 1;
      this.onSecurityBlock?.('policy', tagged);
      const rep: ReplyEnvelopeLegacy = {
        toRoot: req.fromRoot,
        toPath: req.replyTo,
        fromRoot: this.localRoot,
        correlationId: req.correlationId,
        body: null,
        ok: false,
        error: { code: pol.code, message: pol.message, details: pol.details },
        timestamp: this.clock.nowIso(),
        type: 'reply',
        // Wire compat
        toRealm: req.fromRoot,
        fromRealm: this.localRoot,
      };
      this.backbone!.publish(getLegacyIngressTopic(req.fromRoot), serializeLegacyEnvelope(rep));
      this.metrics.outboundReplies += 1;
      this.onOutboundReply?.(rep, getLegacyIngressTopic(req.fromRoot));
      return;
    }

    // Rate limit check
    if (this.rateLimiter) {
      const d = this.rateLimiter.allow(req.fromRoot);
      if (!d.ok) {
        this.metrics.securityBlocks.rateLimit += 1;
        this.onSecurityBlock?.('rate_limit', tagged);
        const rep: ReplyEnvelopeLegacy = {
          toRoot: req.fromRoot,
          toPath: req.replyTo,
          fromRoot: this.localRoot,
          correlationId: req.correlationId,
          body: null,
          ok: false,
          error: { code: 'MXSEC.RATELIMIT', message: 'rate limited', details: { retryAfterMs: d.retryAfterMs } },
          timestamp: this.clock.nowIso(),
          type: 'reply',
          toRealm: req.fromRoot,
          fromRealm: this.localRoot,
        };
        this.backbone!.publish(getLegacyIngressTopic(req.fromRoot), serializeLegacyEnvelope(rep));
        this.metrics.outboundReplies += 1;
        this.onOutboundReply?.(rep, getLegacyIngressTopic(req.fromRoot));
        return;
      }
    }

    if (msgType === 'Evt') {
      this.deliverInternalEvent('matrix3', req.fromRoot, req.toPath, req.body, { principalKey });
      return;
    }

    this.handleLegacyCmd(req, tagged);
  }

  private handleLegacyCmd(req: CrossRealmEnvelopeLegacy, tagged: TaggedEnvelope): void {
    this.metrics.inboundRequests += 1;

    const syntheticMsgId = generateCorrelationId();
    const corr = req.correlationId || generateCorrelationId();

    // FP-21-3: Parse accepts path and route to $inbox
    const parsed = parseAcceptsPath(req.toPath);
    // When op is in body (e.g. { op: '$introspect' }) rather than path (.accepts.$introspect),
    // still route to $inbox. This handles URI-style invocations like invoke('ROOT/mount', { op }).
    const bodyOp = parsed.op == null && req.body && typeof req.body === 'object' && 'op' in (req.body as Record<string, unknown>)
      ? (req.body as Record<string, unknown>).op as string
      : null;
    const effectiveOp = parsed.op ?? bodyOp;
    const reqTopic = effectiveOp != null
      ? getInternalTopic(this.localRoot, (parsed.mount ? `${parsed.mount}/$inbox` : '$inbox'))
      : getInternalTopic(this.localRoot, req.toPath);
    const replyPath = `_peer.replies.emits.${syntheticMsgId}`;
    const replyTopic = getInternalTopic(this.localRoot, replyPath);

    // Rewrite both request and reply topics for local bus format
    const rewrittenReqTopic = this.instanceResolver.rewriteForLocal(reqTopic);
    const rewrittenReplyTopic = this.instanceResolver.rewriteForLocal(replyTopic);

    this.onInboundRequest?.(tagged, rewrittenReqTopic);

    const requestCtx: CrossRealmEnvelopeLegacy = {
      toRoot: this.localRoot,
      toPath: req.toPath,
      fromRoot: req.fromRoot,
      replyTo: req.replyTo,
      correlationId: corr,
      body: req.body,
      toRealm: this.localRoot,
      fromRealm: req.fromRoot,
    };

    let finished = false;
    let timer: ReturnType<typeof setTimeout> | undefined;

    this.metrics.pendingRequests += 1;

    const unsub = this.localBus.subscribe(rewrittenReplyTopic, (_t, payload) => {
      if (finished) return;
      finished = true;
      this.metrics.pendingRequests = Math.max(0, this.metrics.pendingRequests - 1);
      if (timer) clearTimeout(timer);

      try {
        const rep = normalizeInternalReply(payload, corr);
        const ok = rep.ok;
        const body = rep.body;
        const err = rep.error;

        const reply = createLegacyReply({
          request: requestCtx,
          responderRoot: this.localRoot,
          body,
          ok,
          error: ok ? undefined : err ?? { code: 'MX.HANDLER.ERROR', message: 'handler error' },
          type: 'reply',
        });

        const ingressTopic = getLegacyIngressTopic(req.fromRoot);
        this.backbone!.publish(ingressTopic, serializeLegacyEnvelope(reply));
        this.metrics.outboundReplies += 1;
        this.onOutboundReply?.(reply, ingressTopic);
      } finally {
        unsub();
      }
    });

    if (this.internalTimeoutMs > 0) {
      timer = setTimeout(() => {
        if (finished) return;
        finished = true;
        this.metrics.pendingRequests = Math.max(0, this.metrics.pendingRequests - 1);
        unsub();

        this.metrics.timeouts += 1;
        this.onTimeout?.(corr, req.toPath);

        const reply = createLegacyReply({
          request: requestCtx,
          responderRoot: this.localRoot,
          body: null,
          ok: false,
          error: { code: 'MX.PEER.TIMEOUT', message: 'internal handler timeout', details: { toPath: req.toPath } },
          type: 'reply',
        });

        const ingressTopic = getLegacyIngressTopic(req.fromRoot);
        this.backbone!.publish(ingressTopic, serializeLegacyEnvelope(reply));
        this.metrics.outboundReplies += 1;
        this.onOutboundReply?.(reply, ingressTopic);
      }, this.internalTimeoutMs);
    }

    const meta: InternalMeta = { fromRoot: req.fromRoot, fromRealm: req.fromRoot, incomingProfile: 'matrix3', traceId: req.traceId };

    const wirePayload: Record<string, unknown> = effectiveOp != null
      ? { op: effectiveOp, correlationId: corr, replyTo: rewrittenReplyTopic, payload: bodyOp != null ? (req.body as Record<string, unknown>).payload : req.body }
      : { correlationId: corr, replyTo: rewrittenReplyTopic, params: req.body };
    if (this.includeInternalMeta) wirePayload._mx = meta;
    if (this.includeInboundEnvelope) wirePayload.envelope = tagged;

    this.localBus.publish(rewrittenReqTopic, JSON.stringify(wirePayload));
  }

  // MxEnvV1 handling

  private handleMx(topic: string, raw: string, env: MxEnvelopeV1): void {
    // Check if message is for us (exact match or via group channel)
    if (env.to.root !== this.localRoot) {
      // Might be a group channel message - check if we're an instance of the target group
      const rootIdentity = parseRootIdentity(this.localRoot);
      if (!isRootInstance(rootIdentity) || rootIdentity.group.value !== env.to.root) {
        return;
      }
      // It's a group message and we're an instance - proceed
    }

    const tagged: TaggedEnvelope = { profile: 'mxenv1', envelope: env };

    // Replay check
    if (this.replayCache && env.msgId) {
      if (this.replayCache.has(env.msgId)) {
        this.metrics.securityBlocks.replay += 1;
        this.onSecurityBlock?.('replay', tagged);
        this.onDrop?.('replay', topic, raw);
        return;
      }
      this.replayCache.add(env.msgId);
    }

    const principal = derivePrincipal(env);

    // Anti-relay: replyTo.root must equal from.root
    if (this.enforceAntiRelay && env.replyTo) {
      if (env.replyTo.root !== env.from.root) {
        this.metrics.securityBlocks.antiRelay += 1;
        this.onSecurityBlock?.('anti_relay', tagged);
        this.onDrop?.('anti_relay', topic, raw);
        return;
      }
    }

    // Rate limit
    if (this.rateLimiter) {
      const d = this.rateLimiter.allow(env.from.root);
      if (!d.ok) {
        this.metrics.securityBlocks.rateLimit += 1;
        this.onSecurityBlock?.('rate_limit', tagged);
        this.sendNack(env, { code: 'MXSEC.RATELIMIT', message: 'rate limited', details: { retryAfterMs: d.retryAfterMs } });
        return;
      }
    }

    // Policy
    const pol = this.policy.authorize({
      principal,
      fromRoot: env.from.root,
      toRoot: env.to.root,
      toPath: env.to.path,
      msgType: env.msgType,
    });
    if (pol.ok === false) {
      this.metrics.securityBlocks.policy += 1;
      this.onSecurityBlock?.('policy', tagged);
      this.sendNack(env, { code: pol.code, message: pol.message, details: pol.details });
      return;
    }

    // Handle replies
    if (env.msgType === 'Result' || env.msgType === 'Nack') {
      this.handleMxReply(env, tagged);
      return;
    }

    // Handle events
    if (env.msgType === 'Evt') {
      this.deliverInternalEvent('mxenv1', env.from.root, env.to.path, env.body, {
        principalKey: principal.principalKey,
        msgId: env.msgId,
      });
      return;
    }

    // Handle commands
    if (env.msgType !== 'Cmd' && env.msgType !== 'Sub' && env.msgType !== 'Unsub') {
      return;
    }

    if (!env.replyTo || !env.correlationId) {
      return; // fire-and-forget not supported
    }

    this.handleMxCmd(env, principal.principalKey, tagged);
  }

  private handleMxReply(env: MxEnvelopeV1, tagged: TaggedEnvelope): void {
    this.metrics.inboundReplies += 1;
    this.onInboundReply?.(tagged);

    // Publish to local bus so CrossRealmClient can receive it.
    // Rewrite for instance resolver (semantic topic for browser).
    const localTopic = getInternalTopic(this.localRoot, env.to.path);
    const rewrittenTopic = this.instanceResolver.rewriteForLocal(localTopic);
    this.localBus.publish(rewrittenTopic, JSON.stringify({
      correlationId: env.correlationId,
      ok: env.ok,
      result: env.body,
      error: env.error,
    }));
  }

  private handleMxCmd(env: MxEnvelopeV1, principalKey: string, tagged: TaggedEnvelope): void {
    this.metrics.inboundRequests += 1;

    const corr = env.correlationId!;

    // FP-21-3: For Cmd, parse accepts path and route to $inbox with MxEnvelope format.
    // Sub/Unsub stay on old per-op topics for subscription_forwarder compatibility.
    const parsed = env.msgType === 'Cmd' ? parseAcceptsPath(env.to.path) : null;
    // When op is in body rather than path, still route to $inbox (URI-style invocations).
    const mxBodyOp = parsed != null && parsed.op == null && env.body && typeof env.body === 'object' && 'op' in (env.body as Record<string, unknown>)
      ? (env.body as Record<string, unknown>).op as string
      : null;
    const mxEffectiveOp = parsed?.op ?? mxBodyOp;
    const reqTopic = mxEffectiveOp != null
      ? getInternalTopic(this.localRoot, (parsed!.mount ? `${parsed!.mount}/$inbox` : '$inbox'))
      : getInternalTopic(this.localRoot, env.to.path);
    const replyPath = `_peer.replies.emits.${env.msgId}`;
    const replyTopic = getInternalTopic(this.localRoot, replyPath);

    // Rewrite both request and reply topics for local bus format.
    // For TabInstanceResolver: returns semantic path for local bus
    // For ExactInstanceResolver: returns unchanged
    const rewrittenReqTopic = this.instanceResolver.rewriteForLocal(reqTopic);
    const rewrittenReplyTopic = this.instanceResolver.rewriteForLocal(replyTopic);

    // PATH_NOT_FOUND check
    if (this.checkLocalSubscriber) {
      if (!this.hasLocalSubscriber(rewrittenReqTopic)) {
        if (this._verbose) console.log(`[FederationPeer] MX.PATH.NOT_FOUND: no handler for ${env.to.path}`);
        this.sendNack(env, { code: 'MX.PATH.NOT_FOUND', message: `No handler for path: ${env.to.path}` });
        return;
      }
    }

    this.onInboundRequest?.(tagged, rewrittenReqTopic);

    let finished = false;
    let timer: ReturnType<typeof setTimeout> | undefined;

    this.metrics.pendingRequests += 1;

    const unsub = this.localBus.subscribe(rewrittenReplyTopic, (_t, payload) => {
      if (finished) return;
      finished = true;
      this.metrics.pendingRequests = Math.max(0, this.metrics.pendingRequests - 1);
      if (timer) clearTimeout(timer);

      try {
        const rep = normalizeInternalReply(payload, corr);
        const ok = rep.ok;
        const body = rep.body;
        const err = rep.error;

        const res = createMxEnvV1({
          msgType: ok ? 'Result' : 'Nack',
          toRoot: env.replyTo!.root,
          toPath: env.replyTo!.path,
          fromRoot: this.localRoot,
          fromPath: env.to.path,
          correlationId: corr,
          body,
          ok,
          error: ok ? undefined : err ?? { code: 'MX.HANDLER.ERROR', message: 'handler error' },
          ttl: env.ttl != null ? Math.max(0, env.ttl - 1) : undefined,
        });

        const ingressTopic = getMxIngressTopic(env.replyTo!.root, TARGET_SHARD);
        this.backbone!.publish(ingressTopic, serializeMxEnvV1(res));
        this.metrics.outboundReplies += 1;
        this.onOutboundReply?.(res, ingressTopic);
      } finally {
        unsub();
      }
    });

    if (this.internalTimeoutMs > 0) {
      timer = setTimeout(() => {
        if (finished) return;
        finished = true;
        this.metrics.pendingRequests = Math.max(0, this.metrics.pendingRequests - 1);
        unsub();

        this.metrics.timeouts += 1;
        this.onTimeout?.(corr, env.to.path);

        const nack = createMxEnvV1({
          msgType: 'Nack',
          toRoot: env.replyTo!.root,
          toPath: env.replyTo!.path,
          fromRoot: this.localRoot,
          fromPath: env.to.path,
          correlationId: corr,
          body: null,
          ok: false,
          error: { code: 'MX.PEER.TIMEOUT', message: 'internal handler timeout', details: { toPath: env.to.path } },
          ttl: env.ttl != null ? Math.max(0, env.ttl - 1) : undefined,
        });

        const ingressTopic = getMxIngressTopic(env.replyTo!.root, TARGET_SHARD);
        this.backbone!.publish(ingressTopic, serializeMxEnvV1(nack));
        this.metrics.outboundReplies += 1;
        this.onOutboundReply?.(nack, ingressTopic);
      }, this.internalTimeoutMs);
    }

    const meta: InternalMeta = {
      fromRoot: env.from.root,
      fromRealm: env.from.root,
      incomingProfile: 'mxenv1',
      msgId: env.msgId,
      principalKey,
    };

    const wirePayload: Record<string, unknown> = mxEffectiveOp != null
      ? { op: mxEffectiveOp, correlationId: corr, replyTo: rewrittenReplyTopic, payload: mxBodyOp != null ? (env.body as Record<string, unknown>).payload : env.body }
      : { correlationId: corr, replyTo: rewrittenReplyTopic, params: env.body };
    if (this.includeInternalMeta) wirePayload._mx = meta;
    if (this.includeInboundEnvelope) wirePayload.envelope = tagged;

    this.localBus.publish(rewrittenReqTopic, JSON.stringify(wirePayload));
  }

  private hasLocalSubscriber(topic: string): boolean {
    if (isSubscriberAwareTransport(this.localBus)) {
      return this.localBus.hasSubscriber(topic);
    }
    throw new Error('MX.PATH.SUBSCRIBER_CHECK_UNSUPPORTED: local transport does not implement hasSubscriber');
  }

  private deliverInternalEvent(
    incomingProfile: 'matrix3' | 'mxenv1',
    fromRoot: string,
    toPath: string,
    body: unknown,
    extra: { principalKey?: string; msgId?: string },
  ): void {
    const internalTopic = getInternalTopic(this.localRoot, toPath);
    const rewrittenTopic = this.instanceResolver.rewriteForLocal(internalTopic);

    const meta: InternalMeta = {
      fromRoot,
      fromRealm: fromRoot,
      incomingProfile,
      msgId: extra.msgId,
      principalKey: extra.principalKey,
    };

    const msg: Record<string, unknown> = { body };
    if (this.includeInternalMeta) msg._mx = meta;
    this.localBus.publish(rewrittenTopic, JSON.stringify(msg));
  }

  private sendNack(request: MxEnvelopeV1, error: { code: string; message: string; details?: unknown }): void {
    if (!request.replyTo || !request.correlationId || !this.backbone) return;

    const nack = createMxEnvV1({
      msgType: 'Nack',
      toRoot: request.replyTo.root,
      toPath: request.replyTo.path,
      fromRoot: this.localRoot,
      fromPath: request.to.path,
      correlationId: request.correlationId,
      body: null,
      ok: false,
      error,
      ttl: request.ttl != null ? Math.max(0, request.ttl - 1) : undefined,
    });

    const ingressTopic = getMxIngressTopic(request.replyTo.root, TARGET_SHARD);
    this.backbone.publish(ingressTopic, serializeMxEnvV1(nack));
    this.metrics.outboundReplies += 1;
    this.onOutboundReply?.(nack, ingressTopic);
  }

  // invoke() RPC - Local and Cross-root

  /**
   * Invoke RPC on target root with request/reply semantics.
   *
   * Accepts two calling conventions:
   *   invoke(targetRoot, toPath, body, timeoutMs)  -- explicit root + path
   *   invoke(uri, body, timeoutMs)                 -- root/path URI
   *
   * - Local root: use local request/reply via correlation ID
   * - Foreign root: delegate to CrossRealmClient (mailbox pattern)
   */
  async invoke<T = unknown>(
    targetRootOrUri: string,
    toPathOrBody: unknown,
    bodyOrTimeout?: unknown,
    timeoutMs?: number,
  ): Promise<T> {
    let targetRoot: string;
    let toPath: string;
    let body: unknown;
    let timeout: number;

    if (typeof toPathOrBody !== 'string') {
      // URI calling convention: invoke(uri, body, timeoutMs)
      const parsed = this.parseInvokeUri(targetRootOrUri);
      targetRoot = parsed.root;
      toPath = parsed.path;
      body = toPathOrBody;
      timeout = (typeof bodyOrTimeout === 'number' ? bodyOrTimeout : 15000);
    } else {
      // Explicit calling convention: invoke(root, toPath, body, timeoutMs)
      targetRoot = targetRootOrUri;
      toPath = toPathOrBody as string;
      body = bodyOrTimeout;
      timeout = timeoutMs ?? 15000;
    }

    if (this.instanceResolver.isLocal(targetRoot)) {
      return this.invokeLocal<T>(toPath, body, timeout);
    }

    // Cross-root - use CrossRealmClient
    const client = this.getOrCreateCrossRealmClient();
    return client.invoke<T>(targetRoot, toPath, body, timeout);
  }

  /**
   * Parse a Matrix URI for invoke: root/path or root.path
   */
  private parseInvokeUri(uri: string): { root: string; path: string } {
    const body = uri;

    // Try to match known local root first
    const localRoot = this.localRoot;
    const baseRoot = this.instanceResolver.routingBaseRoot ?? localRoot;

    // Check tab root: flowpad.tab-abc/component.accepts.method
    if (body.startsWith(localRoot + '/')) {
      return { root: localRoot, path: body.slice(localRoot.length + 1) };
    }
    if (body.startsWith(localRoot + '.')) {
      // Dot format: flowpad.tab-abc.component.accepts.method
      // Split: root = flowpad.tab-abc, path = component.accepts.method
      return { root: localRoot, path: body.slice(localRoot.length + 1) };
    }

    // Check base root: flowpad/component.accepts.method
    if (baseRoot !== localRoot) {
      if (body.startsWith(baseRoot + '/')) {
        return { root: baseRoot, path: body.slice(baseRoot.length + 1) };
      }
      if (body.startsWith(baseRoot + '.')) {
        return { root: baseRoot, path: body.slice(baseRoot.length + 1) };
      }
    }

    // Generic: first slash or first accepts/emits boundary
    const slashIdx = body.indexOf('/');
    if (slashIdx > 0) {
      return { root: body.slice(0, slashIdx), path: body.slice(slashIdx + 1) };
    }

    // All-dot format: find root boundary
    // Try: root is everything before first known path component
    // Heuristic: look for .accepts. or .emits. to find boundary
    const acceptsIdx = body.indexOf('.accepts.');
    const emitsIdx = body.indexOf('.emits.');
    const planeIdx = acceptsIdx >= 0 && emitsIdx >= 0
      ? Math.min(acceptsIdx, emitsIdx)
      : acceptsIdx >= 0 ? acceptsIdx : emitsIdx;

    if (planeIdx > 0) {
      // Walk back from plane to find root boundary
      const lastDotBefore = body.lastIndexOf('.', planeIdx - 1);
      if (lastDotBefore > 0) {
        // Heuristic: if this looks like CAPS-ROOT.PATH, split at first dot
        const firstDot = body.indexOf('.');
        return { root: body.slice(0, firstDot), path: body.slice(firstDot + 1) };
      }
    }

    // Default parse: first dot is root boundary
    const dotIdx = body.indexOf('.');
    if (dotIdx > 0) {
      return { root: body.slice(0, dotIdx), path: body.slice(dotIdx + 1) };
    }

    return { root: body, path: '' };
  }

  private async invokeLocal<T>(
    toPath: string,
    body: unknown,
    timeoutMs: number
  ): Promise<T> {
    // FP-21-3: Parse accepts path and route to $inbox
    const parsed = parseAcceptsPath(toPath);
    const inboxPath = parsed.op != null
      ? (parsed.mount ? `${parsed.mount}/$inbox` : '$inbox')
      : toPath;
    const topic = getInternalTopic(this.localRoot, inboxPath);
    const rewrittenTopic = this.instanceResolver.rewriteForLocal(topic);
    const correlationId = generateCorrelationId();
    const replyPath = `_peer.rpc.emits.${correlationId}`;
    const replyTopic = getInternalTopic(this.localRoot, replyPath);
    const rewrittenReplyTopic = this.instanceResolver.rewriteForLocal(replyTopic);

    if (this._verbose) console.log(`[FederationPeer] Local RPC: ${rewrittenTopic}`);

    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        unsub();
        reject(new Error(`Local RPC timeout: ${toPath}`));
      }, timeoutMs);

      const unsub = this.localBus.subscribe(rewrittenReplyTopic, (_topic, payload) => {
        clearTimeout(timer);
        unsub();
        try {
          const reply = JSON.parse(payload);
          if (reply.ok === false) {
            reject(new Error(reply.error?.message ?? 'Local RPC error'));
          } else {
            resolve((reply.result ?? reply.body ?? reply) as T);
          }
        } catch {
          resolve(payload as unknown as T);
        }
      });

      const request = parsed.op != null
        ? { op: parsed.op, correlationId, replyTo: rewrittenReplyTopic, payload: body }
        : { correlationId, replyTo: rewrittenReplyTopic, params: body };
      this.localBus.publish(rewrittenTopic, JSON.stringify(request));
    });
  }

  private getOrCreateCrossRealmClient(): CrossRealmClient {
    if (!this.crossRealmClient) {
      if (!this.backbone) {
        throw new Error('[FederationPeer] No backbone transport configured for cross-root RPC');
      }
      this.crossRealmClient = new CrossRealmClient({
        localRoot: this.localRoot,
        transport: this.backbone,
        profile: this.profile,
      });
      this.crossRealmClient.start();
      if (this._verbose) console.log(`[FederationPeer] Created CrossRealmClient for root: ${this.localRoot}`);
    }
    return this.crossRealmClient;
  }

  // Browser integration: route() and rewriteLocalTopic()

  /**
   * Rewrite topic for tab isolation.
   * Delegates to the instance resolver.
   * PUBLIC API: Single source of truth for topic rewriting.
   */
  rewriteLocalTopic(topic: string): string {
    return this.instanceResolver.rewriteForLocal(topic);
  }

  /**
   * Convert a local bus topic back to canonical URI format.
   *
   * Local bus format:  FLOWPAD-ROOT.tab-abc.db.accepts.Query  (dots)
   * Canonical URI:     FLOWPAD-ROOT.tab-abc/db.accepts.Query  (slash after root)
   *
   * This is the inverse of rewriteForLocal() for topics.
   * Use this for display, clipboard copy, and mx-call targeting.
   */
  toCanonicalUri(localBusTopic: string): string {
    const prefix = '';
    if (!localBusTopic.startsWith(prefix)) return localBusTopic;

    const body = localBusTopic.slice(prefix.length);

    // Check if it starts with our local root (with tab suffix)
    if (body.startsWith(this.localRoot + '.')) {
      const path = body.slice(this.localRoot.length + 1);
      return path ? `${prefix}${this.localRoot}/${path}` : `${prefix}${this.localRoot}`;
    }

    // Check base root
    const baseRoot = this.localRoot.replace(/\.tab-[a-z0-9]+$/, '');
    if (baseRoot !== this.localRoot && body.startsWith(baseRoot + '.')) {
      const path = body.slice(baseRoot.length + 1);
      return path ? `${prefix}${this.localRoot}/${path}` : `${prefix}${this.localRoot}`;
    }

    // If it's just the root with no path
    if (body === this.localRoot) {
      return localBusTopic;
    }

    return localBusTopic;
  }

  /**
   * Route a message to the appropriate destination.
   *
   * For local root: rewrites topic and publishes to local bus.
   * For cross-root: wraps in envelope and publishes to backbone ingress.
   *
   * Works with semantic topic format (DNS hierarchical addressing).
   */
  async route(topic: string, payload: unknown): Promise<void> {
    const payloadStr = typeof payload === 'string' ? payload : JSON.stringify(payload);

    // Extract root from topic to determine if local
    const targetRoot = this.extractRoot(topic);
    if (!targetRoot || this.instanceResolver.isLocal(targetRoot)) {
      // Local routing: rewrite for tab isolation and publish to local bus
      const rewrittenTopic = this.instanceResolver.rewriteForLocal(topic);
      this.localBus.publish(rewrittenTopic, payloadStr);
      return;
    }

    // Cross-root: publish to backbone
    if (!this.backbone) {
      if (this._verbose) console.warn(`[FederationPeer] Cannot route cross-root to ${targetRoot}: no backbone`);
      return;
    }

    const ingressTopic = getMxIngressTopic(targetRoot, TARGET_SHARD);
    const envelope = {
      sourceRoot: this.localRoot,
      targetPath: topic,
      payload,
    };
    this.backbone.publish(ingressTopic, JSON.stringify(envelope));
  }

  /**
   * Extract root from a topic string.
   * With DNS addressing, topics are semantic -- match known local root prefix.
   */
  private extractRoot(topic: string): string | null {
    // Match known local root prefix
    if (topic.startsWith(this.localRoot + '.') || topic.startsWith(this.localRoot + '/')) {
      return this.localRoot;
    }
    // Try base root from instance resolver
    const baseRoot = this.instanceResolver.routingBaseRoot;
    if (baseRoot) {
      if (topic.startsWith(baseRoot + '.') || topic.startsWith(baseRoot + '/')) {
        return baseRoot;
      }
    }
    // Default: first dot-segment or slash-segment
    const dotIdx = topic.indexOf('.');
    const slashIdx = topic.indexOf('/');
    if (dotIdx < 0 && slashIdx < 0) return topic;
    const sepIdx = dotIdx >= 0 && slashIdx >= 0 ? Math.min(dotIdx, slashIdx) : dotIdx >= 0 ? dotIdx : slashIdx;
    return topic.slice(0, sepIdx) || null;
  }
}
