// src/transport/NatsTransport.ts

import type {
  ITransportAdapter,
  ITransportSubscribeOptions,
  IWireTopicInfo,
  TransportHandler,
} from '../engine/remoting/ITransportAdapter';
import type { ITransportMessageMeta } from '../engine/remoting/ITransportMessageMeta';

export interface INatsLikeMsg {
  readonly subject: string;
  readonly data: Uint8Array;
  readonly reply?: string;
}

export interface INatsLikeSubscription {
  unsubscribe(): void;
}

export interface INatsLikeConnection {
  publish(subject: string, data?: Uint8Array): void;
  subscribe(
    subject: string,
    options: {
      queue?: string;
      callback: (err: Error | null, msg: INatsLikeMsg) => void;
    },
  ): INatsLikeSubscription;
  isClosed(): boolean;
  flush?(): Promise<void>;
  drain(): Promise<void>;
  close(): Promise<void>;
}

export interface INatsTransportOptions {
  /**
   * DNS root for this transport (e.g., 'com.nimbletec.richard.santomauro').
   * Replaces the old single-token realm. All wire subjects are prefixed with this root.
   */
  root?: string;

  /**
   * @deprecated Use `root` instead. Kept as alias for backward compatibility.
   */
  realm?: string;

  /**
   * Optional prefix prepended to all wire topics.
   * Primarily for transitional compatibility with prefix-based routing.
   */
  topicPrefix?: string;

  /**
   * If true (default), payloads are JSON encoded/decoded.
   */
  jsonMode?: boolean;

  /**
   * Optional error callback.
   */
  onError?: (err: Error) => void;

  /**
   * Enable debug logging.
   */
  debug?: boolean;

  /**
   * Optional hook for observing messages with no local handlers.
   */
  onUnhandledMessage?: (topic: string, payload: unknown) => void;

  /**
   * Optional credentials associated with this transport's NATS connection.
   * The connection itself must already be established with these credentials.
   */
  credentials?: {
    readonly user: string;
    readonly pass: string;
  };

  /**
   * Convenience aliases for credentials.
   */
  user?: string;
  pass?: string;
}

interface ISubscriptionEntry {
  readonly handlers: Set<TransportHandler>;
  readonly subscription: INatsLikeSubscription;
}

export class NatsTransport implements ITransportAdapter {
  private readonly _connection: INatsLikeConnection;
  private readonly _encoder = new TextEncoder();
  private readonly _decoder = new TextDecoder();
  private readonly _root: string;
  private readonly _rootTokens: readonly string[];
  private readonly _rootPrefix: string; // "root." for fast startsWith checks
  private readonly _topicPrefix: string;
  private readonly _jsonMode: boolean;
  private readonly _debug: boolean;
  private readonly _onError?: (err: Error) => void;
  private readonly _onUnhandledMessage?: (topic: string, payload: unknown) => void;
  private readonly _credentials?: { readonly user: string; readonly pass: string };
  private readonly _subscriptions = new Map<string, ISubscriptionEntry>();

  /** DNS root validation: alphanumeric (upper+lower), hyphens, dots, underscores (for hierarchy). */
  static readonly ROOT_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/;

  constructor(connection: INatsLikeConnection, root: string);
  constructor(connection: INatsLikeConnection, options: INatsTransportOptions);
  constructor(connection: INatsLikeConnection, rootOrOptions: string | INatsTransportOptions) {
    const options: INatsTransportOptions = typeof rootOrOptions === 'string'
      ? { root: rootOrOptions }
      : rootOrOptions;

    const root = options.root ?? options.realm;
    if (!root || root.trim().length === 0) {
      throw new Error('NatsTransport: root (or realm) is required');
    }
    // Validate root format: alphanumeric with hyphens, dots, underscores (DNS hierarchy, upper or lower).
    if (!NatsTransport.ROOT_PATTERN.test(root)) {
      throw new Error(
        `NatsTransport: invalid root "${root}". Must be alphanumeric with hyphens, dots, and underscores only.`
      );
    }

    this._connection = connection;
    this._root = root;
    this._rootTokens = root.split('.');
    this._rootPrefix = root + '.';
    this._topicPrefix = options.topicPrefix ?? '';
    this._jsonMode = options.jsonMode ?? true;
    this._debug = options.debug ?? false;
    this._onError = options.onError;
    this._onUnhandledMessage = options.onUnhandledMessage;

    const explicitCredentials = options.credentials;
    if (explicitCredentials) {
      this._credentials = {
        user: explicitCredentials.user,
        pass: explicitCredentials.pass,
      };
    } else if (typeof options.user === 'string' || typeof options.pass === 'string') {
      if (!options.user || !options.pass) {
        throw new Error('NatsTransport: both user and pass are required when specifying credentials');
      }
      this._credentials = {
        user: options.user,
        pass: options.pass,
      };
    }
  }

  /** DNS root for this transport (e.g., 'com.nimbletec.richard.santomauro'). */
  get root(): string {
    return this._root;
  }

  /** @deprecated Alias for `root`. Use `root` instead. */
  get realm(): string {
    return this._root;
  }

  get topicPrefix(): string | undefined {
    return this._topicPrefix.length > 0 ? this._topicPrefix : undefined;
  }

  get credentials(): { readonly user: string; readonly pass: string } | undefined {
    return this._credentials;
  }

  getUnderlyingClient(): INatsLikeConnection {
    return this._connection;
  }

  getWireTopicInfo(topic: string): IWireTopicInfo {
    return {
      appTopic: topic,
      wireTopic: this.semanticToNats(topic),
      prefix: this._topicPrefix,
      root: this._root,
      realm: this._root, // backward compat
    };
  }

  publish(topic: string, payload?: unknown): void {
    const wireTopic = this.semanticToNats(topic);
    const data = this.encodePayload(payload);
    this._connection.publish(wireTopic, data);

    if (this._debug) {
      // eslint-disable-next-line no-console -- kernel NATS transport debug diagnostics are opt-in and have no actor context
      console.log(`[NATS-OUT] wireTopic=${wireTopic}`);
    }
  }

  subscribe(topic: string, handler: TransportHandler, options?: ITransportSubscribeOptions): () => void {
    const wireTopic = this.semanticToNats(topic);
    const queue = options?.queue?.trim();
    const subscriptionKey = queue ? `${wireTopic}\0${queue}` : wireTopic;
    let entry = this._subscriptions.get(subscriptionKey);

    if (!entry) {
      const handlers = new Set<TransportHandler>();
      const subscription = this._connection.subscribe(wireTopic, {
        ...(queue ? { queue } : {}),
        callback: (err: Error | null, msg: INatsLikeMsg) => {
          if (err) {
            this.handleError(err);
            return;
          }

          this.handleMessage(subscriptionKey, msg);
        },
      });

      entry = { handlers, subscription };
      this._subscriptions.set(subscriptionKey, entry);
    }

    entry.handlers.add(handler);

    return () => {
      const current = this._subscriptions.get(subscriptionKey);
      if (!current) {
        return;
      }

      current.handlers.delete(handler);
      if (current.handlers.size === 0) {
        current.subscription.unsubscribe();
        this._subscriptions.delete(subscriptionKey);
      }
    };
  }

  async disconnect(): Promise<void> {
    // Clear JWT refresh timer if set by createBrowserNatsTransport.
    // It is a holder ({ id }) so we cancel whatever timer is CURRENTLY pending,
    // even after the self-rescheduling refresh loop has reassigned it.
    const timerHolder = (this as unknown as { _jwtRefreshTimer?: { id?: ReturnType<typeof setTimeout> } })._jwtRefreshTimer;
    if (timerHolder?.id !== undefined) {
      clearTimeout(timerHolder.id);
      timerHolder.id = undefined;
    }

    for (const entry of this._subscriptions.values()) {
      entry.subscription.unsubscribe();
    }
    this._subscriptions.clear();

    if (!this._connection.isClosed()) {
      await this._connection.drain();
    }

    if (!this._connection.isClosed()) {
      await this._connection.close();
    }
  }

  async flush(): Promise<void> {
    await this._connection.flush?.();
  }

  /**
   * Convert a semantic topic to a DNS-hierarchical NATS subject.
   *
   * Format: {root}.{mount}.{$facet}
   *   system.llm/$inbox  →  com.example.system.llm.$inbox
   *   $replies.abc-123   →  com.example.$reply.abc-123
   *   system.llm/#       →  com.example.system.llm.>
   *   #                  →  com.example.>
   *   generic.topic      →  com.example.generic.topic
   */
  semanticToNats(topic: string): string {
    const replyPrefix = '$replies.';
    let subject: string;

    // Rooted reply subjects are already wire-ready.
    // Example: com.example.$reply.corr-123
    if (topic.startsWith(this._rootPrefix) && topic.includes('.$reply.')) {
      return this._withPrefix(topic);
    }

    // Cross-root: targetRoot/localTopic
    // Local mounts use '.' separators, never '/'. Local facets use '/$facet'.
    // Local wildcards use '/#'. If the first '/' is followed by neither '$' nor '#',
    // this is a cross-root topic: targetRoot/mount[/$facet].
    const firstSlash = topic.indexOf('/');
    if (firstSlash > 0) {
      const afterSlash = topic.slice(firstSlash + 1);
      if (!afterSlash.startsWith('$') && !afterSlash.startsWith('#')) {
        const targetRoot = topic.slice(0, firstSlash);
        const localTopic = afterSlash;
        return this._buildSubjectForRoot(targetRoot, localTopic);
      }
    }

    // Wildcard all: {root}.>
    if (topic === '#') {
      subject = this._toSubject(null, ['>']);
      return this._withPrefix(subject);
    }

    // Reply: {root}.$reply.{cid}
    if (topic.startsWith(replyPrefix)) {
      const correlationId = topic.slice(replyPrefix.length);
      if (correlationId.length === 0) {
        throw new Error('NatsTransport: $replies topic must include correlation id');
      }
      subject = this._toSubject(null, ['$reply', ...this._splitPathTokens(correlationId)]);
      return this._withPrefix(subject);
    }

    // Root-level facets (no mount): {root}.$inbox, {root}.$events, {root}.$exit
    if (topic === '$inbox') {
      subject = this._toSubject('$inbox', []);
      return this._withPrefix(subject);
    }
    if (topic === '$events') {
      subject = this._toSubject('$events', []);
      return this._withPrefix(subject);
    }
    if (topic === '$exit') {
      subject = this._toSubject('$exit', []);
      return this._withPrefix(subject);
    }

    // Mount + facet: {root}.{mount}.$inbox, etc.
    if (topic.endsWith('/$inbox')) {
      const mount = topic.slice(0, -'/$inbox'.length);
      subject = this._toSubject('$inbox', this._splitPathTokens(mount));
      return this._withPrefix(subject);
    }
    if (topic.endsWith('/$events')) {
      const mount = topic.slice(0, -'/$events'.length);
      subject = this._toSubject('$events', this._splitPathTokens(mount));
      return this._withPrefix(subject);
    }
    if (topic.endsWith('/$exit')) {
      const mount = topic.slice(0, -'/$exit'.length);
      subject = this._toSubject('$exit', this._splitPathTokens(mount));
      return this._withPrefix(subject);
    }

    // Mount + wildcard: {root}.{mount}.>
    if (topic.endsWith('/#')) {
      const mount = topic.slice(0, -'/#'.length);
      const tokens = this._splitPathTokens(mount);
      tokens.push('>');
      subject = this._toSubject(null, tokens);
      return this._withPrefix(subject);
    }

    // Generic topic (no facet): {root}.{topic}
    subject = this._toSubject(null, this._splitPathTokens(topic));
    return this._withPrefix(subject);
  }

  /**
   * Convert a DNS-hierarchical NATS subject back to a semantic topic.
   *
   * Format: {root}.{mount}.{$facet} → mount/$facet
   *   com.example.system.llm.$inbox  →  system.llm/$inbox
   *   com.example.$reply.abc-123     →  $replies.abc-123
   *   com.example.system.llm.>       →  system.llm/#
   *   com.example.>                  →  #
   *   com.example.generic.topic      →  generic.topic
   */
  natsToSemantic(wireTopic: string): string {
    const topic = this._stripPrefix(wireTopic);

    // Must start with our root prefix
    if (!topic.startsWith(this._rootPrefix)) {
      // Check exact root match (root-level subjects like {root}.$inbox)
      if (topic === this._root) {
        return topic; // bare root — shouldn't happen, return as-is
      }
      return topic; // not our root — return unchanged
    }

    // Strip root prefix to get remaining tokens
    const remainder = topic.slice(this._rootPrefix.length);
    if (remainder.length === 0) {
      return topic; // bare root — return as-is
    }

    const tokens = remainder.split('.');

    // Wildcard all: {root}.> → #
    if (tokens.length === 1 && tokens[0] === '>') {
      return '#';
    }

    // Reply: {root}.$reply.{cid} → $replies.{cid}
    if (tokens[0] === '$reply') {
      if (tokens.length < 2) {
        return topic; // malformed reply — no CID
      }
      return `$replies.${tokens.slice(1).join('.')}`;
    }

    // Check last token for facet ($inbox, $events, $exit)
    const lastToken = tokens[tokens.length - 1];

    if (lastToken === '$inbox' || lastToken === '$events' || lastToken === '$exit') {
      const mountTokens = tokens.slice(0, -1);
      const mount = this._joinPathTokens(mountTokens);
      return this._appendSemanticSuffix(mount, lastToken);
    }

    // Mount + wildcard: {root}.{mount}.> → mount/#
    if (lastToken === '>') {
      const mountTokens = tokens.slice(0, -1);
      const mount = this._joinPathTokens(mountTokens);
      return mount.length > 0 ? `${mount}/#` : '#';
    }

    // Generic topic (no facet): {root}.{topic} → topic
    return this._joinPathTokens(tokens);
  }

  private encodePayload(payload: unknown): Uint8Array {
    if (this._jsonMode) {
      try {
        return this._encoder.encode(JSON.stringify(payload ?? null));
      } catch (err) {
        this.handleError(err);
        return this._encoder.encode('null');
      }
    }

    if (payload == null) {
      return new Uint8Array(0);
    }

    if (typeof payload === 'string') {
      return this._encoder.encode(payload);
    }

    if (payload instanceof Uint8Array) {
      return payload;
    }

    if (payload instanceof ArrayBuffer) {
      return new Uint8Array(payload);
    }

    if (ArrayBuffer.isView(payload)) {
      return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
    }

    this.handleError(new Error(`NatsTransport: Unsupported payload type for publish: ${typeof payload}`));
    return new Uint8Array(0);
  }

  private decodePayload(data: Uint8Array): unknown {
    if (!this._jsonMode) {
      return data;
    }

    if (data.length === 0) {
      return null;
    }

    const raw = this._decoder.decode(data);
    try {
      return JSON.parse(raw);
    } catch {
      return raw;
    }
  }

  private handleMessage(subscriptionTopic: string, msg: INatsLikeMsg): void {
    const entry = this._subscriptions.get(subscriptionTopic);
    const payload = this._withProtocolReply(this.decodePayload(msg.data), msg.reply);

    if (!entry || entry.handlers.size === 0) {
      this._onUnhandledMessage?.(msg.subject, payload);
      return;
    }

    if (this._debug) {
      // eslint-disable-next-line no-console -- kernel NATS transport debug diagnostics are opt-in and have no actor context
      console.log(`[NATS-IN] wireTopic=${msg.subject} handlers=${entry.handlers.size}`);
    }

    const meta: ITransportMessageMeta = {
      topic: this.natsToSemantic(msg.subject),
      ts: Date.now(),
    };

    for (const handler of entry.handlers) {
      Promise.resolve(handler(payload, meta)).catch((err: unknown) => {
        this.handleError(err);
      });
    }
  }

  private handleError(err: unknown): void {
    const error = err instanceof Error ? err : new Error(String(err));
    if (this._onError) {
      this._onError(error);
      return;
    }

    // eslint-disable-next-line no-console -- kernel NATS transport has no actor context for transport-level error fallback
    console.error('[NatsTransport] Error:', error);
  }

  private _withProtocolReply(payload: unknown, reply: string | undefined): unknown {
    const trimmedReply = reply?.trim();
    if (!trimmedReply || !payload || typeof payload !== 'object' || Array.isArray(payload)) {
      return payload;
    }

    return {
      ...(payload as Record<string, unknown>),
      replyTo: trimmedReply,
    };
  }

  private _semanticTokenToNats(token: string): string {
    if (token === '#') {
      return '>';
    }
    if (token === '+') {
      return '*';
    }
    return token;
  }

  private _natsTokenToSemantic(token: string): string {
    if (token === '>') {
      return '#';
    }
    if (token === '*') {
      return '+';
    }
    return token;
  }

  private _splitPathTokens(path: string): string[] {
    if (path.length === 0) {
      return [];
    }
    return path.split('.').map((token) => this._semanticTokenToNats(token));
  }

  private _joinPathTokens(tokens: readonly string[]): string {
    return tokens
      .map((token) => this._natsTokenToSemantic(token))
      .join('.');
  }

  /**
   * Build a DNS-hierarchical NATS subject: {root}.{mountTokens}.{facet}
   * @param facet - Terminal facet like '$inbox', '$events', '$exit', or null for no facet
   * @param mountTokens - Mount path tokens (already converted from semantic to NATS wildcards)
   */
  private _toSubject(facet: string | null, mountTokens: readonly string[]): string {
    const parts: string[] = [...this._rootTokens, ...mountTokens];
    if (facet) parts.push(facet);
    return parts.join('.');
  }

  /**
   * Build a NATS subject for a cross-root topic using the target root instead of the local root.
   * localTopic follows the same format as local semantic topics (e.g., 'daemon/$inbox').
   */
  private _buildSubjectForRoot(targetRoot: string, localTopic: string): string {
    const targetTokens = targetRoot.split('.');

    // localTopic: 'mount/$facet', '$facet', 'mount/#', or 'generic.topic'
    if (localTopic.endsWith('/$inbox')) {
      const mount = localTopic.slice(0, -'/$inbox'.length);
      return this._withPrefix([...targetTokens, ...this._splitPathTokens(mount), '$inbox'].join('.'));
    }
    if (localTopic.endsWith('/$events')) {
      const mount = localTopic.slice(0, -'/$events'.length);
      return this._withPrefix([...targetTokens, ...this._splitPathTokens(mount), '$events'].join('.'));
    }
    if (localTopic.endsWith('/$exit')) {
      const mount = localTopic.slice(0, -'/$exit'.length);
      return this._withPrefix([...targetTokens, ...this._splitPathTokens(mount), '$exit'].join('.'));
    }
    if (localTopic === '$inbox') {
      return this._withPrefix([...targetTokens, '$inbox'].join('.'));
    }
    if (localTopic === '$events') {
      return this._withPrefix([...targetTokens, '$events'].join('.'));
    }
    if (localTopic.endsWith('/#')) {
      const mount = localTopic.slice(0, -'/#'.length);
      return this._withPrefix([...targetTokens, ...this._splitPathTokens(mount), '>'].join('.'));
    }
    if (localTopic === '#') {
      return this._withPrefix([...targetTokens, '>'].join('.'));
    }
    // Generic topic
    return this._withPrefix([...targetTokens, ...this._splitPathTokens(localTopic)].join('.'));
  }

  private _appendSemanticSuffix(mount: string, suffix: string): string {
    if (mount.length === 0) {
      return suffix;
    }
    return `${mount}/${suffix}`;
  }

  private _withPrefix(subject: string): string {
    if (this._topicPrefix.length > 0) {
      return `${this._topicPrefix}${subject}`;
    }
    return subject;
  }

  private _stripPrefix(subject: string): string {
    if (this._topicPrefix.length > 0 && subject.startsWith(this._topicPrefix)) {
      return subject.slice(this._topicPrefix.length);
    }
    return subject;
  }
}
