/**
 * SubscriptionForwarder - Lease-based event egress for federation
 *
 * Solves the "unsolicited event" problem: local emits have no destination.
 * Unlike Cmd/Result (which use correlationId for routing), Evt messages
 * need explicit subscription leases to know where to forward.
 *
 * MXSUB Protocol:
 * 1. Client sends Cmd to _federation.accepts.subscribe with sourceFilter
 * 2. Forwarder creates lease, subscribes to local bus, returns subId
 * 3. Matching local events are wrapped as Evt and published to client's mailbox
 * 4. Client can refresh lease or unsubscribe when done
 *
 * Security invariants:
 * - Anti-relay: events only forwarded to requesting root (no third-party relay)
 * - Lease expiry: subscriptions automatically cleaned up
 * - Rate limiting: max subscriptions per root
 *
 * @module packages/federation/src/runtime/subscription_forwarder
 */

import type { FlushableTransport, Transport } from '../transport/base';
import type { WireProfile } from '../wire_profile';
import { getInternalTopic, getMxIngressTopic } from '../topics';
import { createMxEnvV1, serializeMxEnvV1 } from '../envelope_codec';

/**
 * Subscription request from remote client.
 */
export interface SubscribeRequest {
  /** Local topic filter to watch (e.g., "daemon.*.emits.scanComplete") */
  sourceFilter: string;
  /** Client's sink path where events will be delivered */
  sinkPath: string;
  /** Lease duration in seconds */
  leaseSeconds: number;
}

/**
 * Subscription response.
 */
export interface SubscribeResponse {
  /** Subscription ID for refresh/unsubscribe */
  subId: string;
  /** Expiration timestamp (Unix ms) */
  expiresAt: number;
}

/**
 * Refresh request.
 */
export interface RefreshRequest {
  /** Subscription ID to refresh */
  subId: string;
  /** New lease duration in seconds */
  leaseSeconds: number;
}

/**
 * Unsubscribe request.
 */
export interface UnsubscribeRequest {
  /** Subscription ID to cancel */
  subId: string;
}

/**
 * Internal representation of an active subscription lease.
 */
interface Lease {
  subId: string;
  sourceFilter: string;
  sinkRoot: string;
  sinkPath: string;
  expiresAt: number;
  createdAt: number;
}

/**
 * Metrics for subscription forwarder.
 */
export interface SubscriptionMetrics {
  activeSubscriptions: number;
  forwardedEvents: number;
  expiredLeases: number;
  rejectedRequests: number;
  subscribeRequests: number;
  refreshRequests: number;
  unsubscribeRequests: number;
}

/**
 * Options for SubscriptionForwarder.
 */
export interface SubscriptionForwarderOptions {
  /** Local root identifier */
  localRoot: string;
  /** @deprecated Use localRoot */
  localRealm?: string;

  /** Backbone transport for publishing events to remote mailboxes */
  backbone: Transport;

  /** Local bus transport for subscribing to local events */
  localBus: Transport;

  /** Wire profile for envelope formatting */
  profile: WireProfile;

  /** Maximum lease duration in seconds (default: 3600 = 1 hour) */
  maxLeaseSeconds?: number;

  /** Maximum subscriptions per root (default: 100) */
  maxSubscriptionsPerRoot?: number;
  /** @deprecated Use maxSubscriptionsPerRoot */
  maxSubscriptionsPerRealm?: number;

  /** Shard identifier for backbone mailbox (default: '00') */
  shard?: string;

  /** Lease expiry check interval in ms (default: 1000) */
  expiryCheckIntervalMs?: number;
}

/**
 * SubscriptionForwarder implements lease-based event egress.
 *
 * Clients request event forwarding via Cmd/Result to _federation.accepts.*
 * endpoints. Matching local events are wrapped as Evt and published to
 * the client's backbone mailbox.
 */
export class SubscriptionForwarder {
  private readonly localRoot: string;
  private readonly backbone: Transport;
  private readonly localBus: Transport;
  private readonly profile: WireProfile;
  private readonly shard: string;
  private readonly maxLeaseSeconds: number;
  private readonly maxSubscriptionsPerRoot: number;
  private readonly expiryCheckIntervalMs: number;

  private readonly leases = new Map<string, Lease>();
  private readonly unsubscribers = new Map<string, () => void>();
  private readonly rootSubscriptionCounts = new Map<string, number>();

  private expiryTimer: ReturnType<typeof setInterval> | null = null;
  private localUnsubscribers: (() => void)[] = [];
  private started = false;

  private metrics: SubscriptionMetrics = {
    activeSubscriptions: 0,
    forwardedEvents: 0,
    expiredLeases: 0,
    rejectedRequests: 0,
    subscribeRequests: 0,
    refreshRequests: 0,
    unsubscribeRequests: 0,
  };

  constructor(options: SubscriptionForwarderOptions) {
    this.localRoot = options.localRoot ?? options.localRealm!;
    this.backbone = options.backbone;
    this.localBus = options.localBus;
    this.profile = options.profile;
    this.shard = options.shard ?? '00';
    this.maxLeaseSeconds = options.maxLeaseSeconds ?? 3600;
    this.maxSubscriptionsPerRoot = options.maxSubscriptionsPerRoot ?? options.maxSubscriptionsPerRealm ?? 100;
    this.expiryCheckIntervalMs = options.expiryCheckIntervalMs ?? 1000;
  }

  /**
   * Start the forwarder.
   * Subscribes to _federation.accepts.* on local bus for subscription management.
   */
  start(): void {
    if (this.started) return;
    this.started = true;

    // Subscribe to federation management endpoints on local bus
    // FederationPeer routes to internal topics: <toPath> (semantic, no wire prefix)
    const subscribeTopic = getInternalTopic(this.localRoot, '_federation.accepts.subscribe');
    const refreshTopic = getInternalTopic(this.localRoot, '_federation.accepts.refresh');
    const unsubscribeTopic = getInternalTopic(this.localRoot, '_federation.accepts.unsubscribe');

    this.localUnsubscribers.push(
      this.localBus.subscribe(subscribeTopic, (topic, payload) => {
        this.handleSubscribe(topic, payload);
      }),
    );

    this.localUnsubscribers.push(
      this.localBus.subscribe(refreshTopic, (topic, payload) => {
        this.handleRefresh(topic, payload);
      }),
    );

    this.localUnsubscribers.push(
      this.localBus.subscribe(unsubscribeTopic, (topic, payload) => {
        this.handleUnsubscribe(topic, payload);
      }),
    );

    // Start expiry timer
    this.expiryTimer = setInterval(() => this.cleanupExpiredLeases(), this.expiryCheckIntervalMs);
  }

  /**
   * Stop the forwarder.
   * Cleans up all leases and subscriptions.
   */
  stop(): void {
    if (!this.started) return;
    this.started = false;

    // Stop expiry timer
    if (this.expiryTimer) {
      clearInterval(this.expiryTimer);
      this.expiryTimer = null;
    }

    // Unsubscribe from local bus management endpoints
    for (const unsub of this.localUnsubscribers.splice(0)) {
      unsub();
    }

    // Clean up all active leases
    for (const unsub of this.unsubscribers.values()) {
      unsub();
    }
    this.unsubscribers.clear();
    this.leases.clear();
    this.rootSubscriptionCounts.clear();

    this.metrics.activeSubscriptions = 0;
  }

  /**
   * Get current metrics.
   */
  getMetrics(): SubscriptionMetrics {
    return { ...this.metrics };
  }

  /**
   * Reset metrics (for testing).
   */
  resetMetrics(): void {
    this.metrics = {
      activeSubscriptions: this.leases.size,
      forwardedEvents: 0,
      expiredLeases: 0,
      rejectedRequests: 0,
      subscribeRequests: 0,
      refreshRequests: 0,
      unsubscribeRequests: 0,
    };
  }

  // ---------------------------------------------------------------------------
  // Handler implementations
  // ---------------------------------------------------------------------------

  private handleSubscribe(_topic: string, payload: string): void {
    this.metrics.subscribeRequests++;

    let raw: Record<string, unknown>;
    try {
      raw = JSON.parse(payload);
    } catch {
      return; // Invalid JSON, ignore
    }

    // Handle both direct format and InternalRequestWire format (FederationPeer wraps body in params)
    const params = (raw.params ?? raw) as SubscribeRequest;
    const { sourceFilter, sinkPath, leaseSeconds } = params;
    const replyTo = raw.replyTo as string | undefined;
    const correlationId = raw.correlationId as string | undefined;
    const _mx = raw._mx as { fromRoot?: string; fromRealm?: string } | undefined;
    const fromRoot = _mx?.fromRoot ?? _mx?.fromRealm;

    // Validate required fields
    if (!sourceFilter || !sinkPath || leaseSeconds === undefined || !replyTo || !correlationId) {
      this.sendError(replyTo, correlationId, 'MX.SUB.INVALID', 'Missing required fields');
      this.metrics.rejectedRequests++;
      return;
    }

    // ARCHITECTURAL FIX (Issue #7 from critique):
    // Validate leaseSeconds is a finite positive number.
    if (typeof leaseSeconds !== 'number' || !Number.isFinite(leaseSeconds) || leaseSeconds <= 0) {
      this.sendError(replyTo, correlationId, 'MX.SUB.INVALID_LEASE', 'leaseSeconds must be a positive finite number');
      this.metrics.rejectedRequests++;
      return;
    }

    // Anti-relay: fromRoot must be present
    if (!fromRoot) {
      this.sendError(replyTo, correlationId, 'MX.AUTH.MISSING_ROOT', 'Missing fromRoot in _mx');
      this.metrics.rejectedRequests++;
      return;
    }

    // Validate lease duration (cap at maxLeaseSeconds)
    const effectiveLease = Math.min(leaseSeconds, this.maxLeaseSeconds);

    // Check rate limit per root
    const currentCount = this.rootSubscriptionCounts.get(fromRoot) ?? 0;
    if (currentCount >= this.maxSubscriptionsPerRoot) {
      this.sendError(replyTo, correlationId, 'MX.SUB.LIMIT_EXCEEDED', `Maximum subscriptions (${this.maxSubscriptionsPerRoot}) per root exceeded`);
      this.metrics.rejectedRequests++;
      return;
    }

    // Create lease
    const subId = this.generateSubId();
    const now = Date.now();
    const expiresAt = now + effectiveLease * 1000;

    const lease: Lease = {
      subId,
      sourceFilter,
      sinkRoot: fromRoot,
      sinkPath,
      expiresAt,
      createdAt: now,
    };

    this.leases.set(subId, lease);
    this.rootSubscriptionCounts.set(fromRoot, currentCount + 1);
    this.metrics.activeSubscriptions = this.leases.size;

    // Subscribe to local bus for matching events
    const unsub = this.localBus.subscribe(sourceFilter, (evtTopic, evtPayload) => {
      this.forwardEvent(lease, evtTopic, evtPayload);
    });
    this.unsubscribers.set(subId, unsub);

    // Send success response
    const response: SubscribeResponse = { subId, expiresAt };
    this.sendReply(replyTo, correlationId, response);
  }

  private handleRefresh(_topic: string, payload: string): void {
    this.metrics.refreshRequests++;

    let raw: Record<string, unknown>;
    try {
      raw = JSON.parse(payload);
    } catch {
      return;
    }

    // Handle both direct format and InternalRequestWire format
    const params = (raw.params ?? raw) as RefreshRequest;
    const { subId, leaseSeconds } = params;
    const replyTo = raw.replyTo as string | undefined;
    const correlationId = raw.correlationId as string | undefined;
    const _mx = raw._mx as { fromRoot?: string; fromRealm?: string } | undefined;
    const fromRoot = _mx?.fromRoot ?? _mx?.fromRealm;

    if (!subId || leaseSeconds === undefined || !replyTo || !correlationId) {
      this.sendError(replyTo, correlationId, 'MX.SUB.INVALID', 'Missing required fields');
      this.metrics.rejectedRequests++;
      return;
    }

    // ARCHITECTURAL FIX (Issue #7 from critique):
    // Validate leaseSeconds is a finite positive number.
    if (typeof leaseSeconds !== 'number' || !Number.isFinite(leaseSeconds) || leaseSeconds <= 0) {
      this.sendError(replyTo, correlationId, 'MX.SUB.INVALID_LEASE', 'leaseSeconds must be a positive finite number');
      this.metrics.rejectedRequests++;
      return;
    }

    const lease = this.leases.get(subId);
    if (!lease) {
      this.sendError(replyTo, correlationId, 'MX.SUB.NOT_FOUND', 'Subscription not found');
      this.metrics.rejectedRequests++;
      return;
    }

    // Security: only the original root can refresh
    if (fromRoot && lease.sinkRoot !== fromRoot) {
      this.sendError(replyTo, correlationId, 'MX.AUTH.FORBIDDEN', 'Cannot refresh subscription owned by another root');
      this.metrics.rejectedRequests++;
      return;
    }

    // Update expiry
    const effectiveLease = Math.min(leaseSeconds, this.maxLeaseSeconds);
    lease.expiresAt = Date.now() + effectiveLease * 1000;

    const response: SubscribeResponse = { subId, expiresAt: lease.expiresAt };
    this.sendReply(replyTo, correlationId, response);
  }

  private handleUnsubscribe(_topic: string, payload: string): void {
    this.metrics.unsubscribeRequests++;

    let raw: Record<string, unknown>;
    try {
      raw = JSON.parse(payload);
    } catch {
      return;
    }

    // Handle both direct format and InternalRequestWire format
    const params = (raw.params ?? raw) as UnsubscribeRequest;
    const { subId } = params;
    const replyTo = raw.replyTo as string | undefined;
    const correlationId = raw.correlationId as string | undefined;
    const _mx = raw._mx as { fromRoot?: string; fromRealm?: string } | undefined;
    const fromRoot = _mx?.fromRoot ?? _mx?.fromRealm;

    if (!subId || !replyTo || !correlationId) {
      this.sendError(replyTo, correlationId, 'MX.SUB.INVALID', 'Missing required fields');
      this.metrics.rejectedRequests++;
      return;
    }

    const lease = this.leases.get(subId);
    if (!lease) {
      // Idempotent: unsubscribing non-existent subscription is OK
      this.sendReply(replyTo, correlationId, { ok: true });
      return;
    }

    // Security: only the original root can unsubscribe
    if (fromRoot && lease.sinkRoot !== fromRoot) {
      this.sendError(replyTo, correlationId, 'MX.AUTH.FORBIDDEN', 'Cannot unsubscribe subscription owned by another root');
      this.metrics.rejectedRequests++;
      return;
    }

    this.removeLease(subId);
    this.sendReply(replyTo, correlationId, { ok: true });
  }

  // ---------------------------------------------------------------------------
  // Event forwarding
  // ---------------------------------------------------------------------------

  private forwardEvent(lease: Lease, sourceTopic: string, payload: string): void {
    // Check if lease is still valid
    if (Date.now() > lease.expiresAt) {
      return;
    }

    let body: unknown;
    try {
      const parsed = JSON.parse(payload);
      body = parsed.body ?? parsed;
    } catch {
      body = payload;
    }

    // Build Evt envelope
    const evt = createMxEnvV1({
      msgType: 'Evt',
      toRoot: lease.sinkRoot,
      toPath: lease.sinkPath,
      fromRoot: this.localRoot,
      fromPath: sourceTopic,
      body,
    });

    // Publish to subscriber's mailbox on backbone
    const mailbox = getMxIngressTopic(lease.sinkRoot, this.shard);
    this.backbone.publish(mailbox, serializeMxEnvV1(evt));
    this.metrics.forwardedEvents++;
  }

  // ---------------------------------------------------------------------------
  // Lease management
  // ---------------------------------------------------------------------------

  private cleanupExpiredLeases(): void {
    const now = Date.now();
    const expired: string[] = [];

    for (const [subId, lease] of this.leases) {
      if (now > lease.expiresAt) {
        expired.push(subId);
      }
    }

    for (const subId of expired) {
      this.removeLease(subId);
      this.metrics.expiredLeases++;
    }
  }

  private removeLease(subId: string): void {
    const lease = this.leases.get(subId);
    if (!lease) return;

    // Unsubscribe from local bus
    const unsub = this.unsubscribers.get(subId);
    if (unsub) {
      unsub();
      this.unsubscribers.delete(subId);
    }

    // Decrement root count
    const count = this.rootSubscriptionCounts.get(lease.sinkRoot) ?? 0;
    if (count <= 1) {
      this.rootSubscriptionCounts.delete(lease.sinkRoot);
    } else {
      this.rootSubscriptionCounts.set(lease.sinkRoot, count - 1);
    }

    this.leases.delete(subId);
    this.metrics.activeSubscriptions = this.leases.size;
  }

  // ---------------------------------------------------------------------------
  // Helpers
  // ---------------------------------------------------------------------------

  private generateSubId(): string {
    // ARCHITECTURAL FIX (Issue #13 from critique):
    // Use crypto.randomUUID() for security-sensitive handles (not guessable).
    if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
      return `sub-${crypto.randomUUID()}`;
    }

    // Fallback: crypto.getRandomValues if available (CSPRNG)
    if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
      const bytes = new Uint8Array(16);
      crypto.getRandomValues(bytes);
      const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
      return `sub-${hex}`;
    }

    // Last resort: Math.random (not recommended for production)
    console.warn('[SubscriptionForwarder] Using Math.random for subId - consider polyfilling crypto');
    return `sub-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
  }

  private sendReply(replyTo: string | undefined, correlationId: string | undefined, body: unknown): void {
    if (!replyTo || !correlationId) return;

    const reply = {
      correlationId,
      ok: true,
      body,
    };
    this.localBus.publish(replyTo, JSON.stringify(reply));
  }

  private sendError(
    replyTo: string | undefined,
    correlationId: string | undefined,
    code: string,
    message: string,
  ): void {
    if (!replyTo || !correlationId) return;

    const reply = {
      correlationId,
      ok: false,
      error: { code, message },
    };
    this.localBus.publish(replyTo, JSON.stringify(reply));
  }
}
