/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Code, LiteSubscriptionAction } from '../../proto/apache/rocketmq/v2/definition_pb';
import {
  NotifyUnsubscribeLiteCommand,
  SyncLiteSubscriptionRequest,
  SyncLiteSubscriptionResponse,
} from '../../proto/apache/rocketmq/v2/service_pb';
import { ClientException } from '../exception';
import { Resource } from '../route';
import { LitePushConsumerImpl } from './LitePushConsumerImpl';
import { OffsetOption } from './OffsetOption';

const SYNC_LITE_SUBSCRIPTION_INTERVAL = 30000; // 30 seconds

/**
 * Manages lite topic subscriptions for LitePushConsumer.
 *
 * <p>LiteSubscriptionManager handles:
 * - Maintaining the set of subscribed lite topics
 * - Periodic synchronization of lite subscriptions with server
 * - Quota management and verification
 * - Handling unsubscribe commands from server</p>
 */
export class LiteSubscriptionManager {
  private readonly consumerImpl: LitePushConsumerImpl;
  private readonly bindTopic: Resource;
  private readonly group: Resource;
  private readonly liteTopicSet = new Set<string>();
  private liteSubscriptionQuota: number;
  private maxLiteTopicSize: number;
  private syncTimer?: NodeJS.Timeout;

  constructor(
    consumerImpl: LitePushConsumerImpl,
    bindTopic: Resource,
    group: Resource,
  ) {
    this.consumerImpl = consumerImpl;
    this.bindTopic = bindTopic;
    this.group = group;
    this.liteSubscriptionQuota = 100; // Default quota
    this.maxLiteTopicSize = 64; // Default max length
  }

  /**
   * Start periodic sync of lite subscriptions.
   */
  public startUp() {
    // syncAll after startup for subscribeLite("*")
    this.syncAllLiteSubscription();
    this.syncTimer = setInterval(
      () => this.syncAllLiteSubscription(),
      SYNC_LITE_SUBSCRIPTION_INTERVAL,
    );
  }

  /**
   * Stop the sync timer.
   */
  public shutdown() {
    // Clear sync timer
    if (this.syncTimer) {
      clearInterval(this.syncTimer);
      this.syncTimer = undefined;
    }
    // Clear lite topic set to free memory
    this.liteTopicSet.clear();
  }

  /**
   * Get the bind topic name.
   */
  public getBindTopicName(): string {
    return this.bindTopic.getName();
  }

  /**
   * Get the consumer group name.
   */
  public getConsumerGroupName(): string {
    return this.group.getName();
  }

  /**
   * Get a copy of the lite topic set.
   */
  public getLiteTopicSet(): Set<string> {
    return new Set(this.liteTopicSet);
  }

  /**
   * Sync settings from server.
   */
  public sync(settings: any) {
    if (!settings?.hasSubscription?.()) {
      return;
    }

    const subscription = settings.getSubscription();
    if (subscription.hasLiteSubscriptionQuota?.()) {
      this.liteSubscriptionQuota = subscription.getLiteSubscriptionQuota();
    }
    if (subscription.hasMaxLiteTopicSize?.()) {
      this.maxLiteTopicSize = subscription.getMaxLiteTopicSize();
    }
  }

  /**
   * Subscribe to a lite topic.
   */
  public async subscribeLite(
    liteTopic: string,
    offsetOption?: OffsetOption | null,
  ): Promise<void> {
    // Check if consumer is running
    if (!this.consumerImpl.isRunning()) {
      throw new ClientException(500, 'Consumer is not running');
    }

    // Already subscribed
    if (this.liteTopicSet.has(liteTopic)) {
      return;
    }

    // Validate lite topic
    this.validateLiteTopic(liteTopic);

    // Check quota
    this.checkLiteSubscriptionQuota(1);

    try {
      await this.syncLiteSubscription(
        LiteSubscriptionAction.PARTIAL_ADD,
        [ liteTopic ],
        offsetOption ?? null,
      );

      this.liteTopicSet.add(liteTopic);
      this.consumerImpl.getLogger().info(
        'SubscribeLite %s, topic=%s, group=%s, clientId=%s',
        liteTopic,
        this.getBindTopicName(),
        this.getConsumerGroupName(),
        this.consumerImpl.clientId,
      );
    } catch (error) {
      this.consumerImpl.getLogger().error(
        'Failed to subscribeLite %s, error=%s',
        liteTopic,
        error,
      );
      throw error;
    }
  }

  /**
   * Unsubscribe from a lite topic.
   */
  public async unsubscribeLite(liteTopic: string): Promise<void> {
    // Check if consumer is running
    if (!this.consumerImpl.isRunning()) {
      throw new ClientException(500, 'Consumer is not running');
    }

    // Not subscribed
    if (!this.liteTopicSet.has(liteTopic)) {
      return;
    }

    try {
      await this.syncLiteSubscription(
        LiteSubscriptionAction.PARTIAL_REMOVE,
        [ liteTopic ],
        null,
      );

      this.liteTopicSet.delete(liteTopic);
      this.consumerImpl.getLogger().info(
        'UnsubscribeLite %s, topic=%s, group=%s, clientId=%s',
        liteTopic,
        this.getBindTopicName(),
        this.getConsumerGroupName(),
        this.consumerImpl.clientId,
      );
    } catch (error) {
      this.consumerImpl.getLogger().error(
        'Failed to unsubscribeLite %s, error=%s',
        liteTopic,
        error,
      );
      throw error;
    }
  }

  /**
   * Periodic sync of all lite subscriptions.
   */
  private async syncAllLiteSubscription() {
    try {
      this.checkLiteSubscriptionQuota(0);
      await this.syncLiteSubscription(
        LiteSubscriptionAction.COMPLETE_ADD,
        Array.from(this.liteTopicSet),
        null,
      );
    } catch (error) {
      this.consumerImpl.getLogger().error(
        'Schedule syncAllLiteSubscription error, clientId=%s, error=%s',
        this.consumerImpl.clientId,
        error,
      );
    }
  }

  /**
   * Sync lite subscription with server.
   */
  private async syncLiteSubscription(
    action: LiteSubscriptionAction,
    liteTopics: string[],
    offsetOption: OffsetOption | null,
  ): Promise<void> {
    const logger = this.consumerImpl.getLogger();
    if (logger.debug) {
      logger.debug(
        'SyncLiteSubscription: action=%s, liteTopics=[%s], offsetOption=%s',
        LiteSubscriptionAction[action],
        liteTopics.join(', '),
        offsetOption?.toString() ?? 'null',
      );
    }

    // Build the request
    const request = new SyncLiteSubscriptionRequest()
      .setAction(action)
      .setTopic(this.bindTopic.toProtobuf())
      .setGroup(this.group.toProtobuf())
      .setLiteTopicSetList(liteTopics);

    // Set offset option if provided
    if (offsetOption) {
      request.setOffsetOption(offsetOption.toProtobuf());
    }

    try {
      // Call RPC client using public methods from LitePushConsumerImpl
      const response: SyncLiteSubscriptionResponse = await this.consumerImpl.getRpcClientManager().syncLiteSubscription(
        this.consumerImpl.getEndpoints(),
        request,
        this.consumerImpl.getRequestTimeout(),
      );

      // Handle response status
      const status = response.getStatus();
      if (status && status.getCode() !== Code.OK) {
        throw new ClientException(
          status.getCode(),
          `Failed to sync lite subscription: ${status.getMessage()}`,
        );
      }

      if (logger.info) {
        logger.info(
          'SyncLiteSubscription success: action=%s, liteTopics=[%s], clientId=%s',
          LiteSubscriptionAction[action],
          liteTopics.join(', '),
          this.consumerImpl.clientId,
        );
      }
    } catch (error) {
      if (logger.error) {
        logger.error(
          'SyncLiteSubscription failed: action=%s, liteTopics=[%s], clientId=%s, error=%s',
          LiteSubscriptionAction[action],
          liteTopics.join(', '),
          this.consumerImpl.clientId,
          error,
        );
      }
      throw error;
    }
  }

  /**
   * Handle notify unsubscribe lite command from server.
   */
  public onNotifyUnsubscribeLiteCommand(command: NotifyUnsubscribeLiteCommand) {
    const liteTopic = command.getLiteTopic();
    this.consumerImpl.getLogger().info(
      'Notify unsubscribe lite: liteTopic=%s, group=%s, bindTopic=%s',
      liteTopic,
      this.getConsumerGroupName(),
      this.getBindTopicName(),
    );

    if (liteTopic && liteTopic.trim().length > 0) {
      this.liteTopicSet.delete(liteTopic);
    }
  }

  /**
   * Validate lite topic name.
   */
  private validateLiteTopic(liteTopic: string, maxLength?: number) {
    const maxLen = maxLength ?? this.maxLiteTopicSize;

    if (!liteTopic || liteTopic.trim().length === 0) {
      throw new Error('liteTopic is blank');
    }

    if (liteTopic.length > maxLen) {
      throw new Error(
        `liteTopic length exceeded max length ${maxLen}, liteTopic: ${liteTopic}`,
      );
    }
  }

  /**
   * Check lite subscription quota.
   */
  private checkLiteSubscriptionQuota(delta: number) {
    if (this.liteTopicSet.size + delta > this.liteSubscriptionQuota) {
      throw new ClientException(
        429,
        `Lite subscription quota exceeded ${this.liteSubscriptionQuota}`,
      );
    }
  }

  /**
   * Returns a string representation of this LiteSubscriptionManager.
   *
   * @return String representation
   */
  toString(): string {
    return `LiteSubscriptionManager{bindTopic=${this.bindTopic.getName()}, group=${this.group.getName()}, liteTopicSet=[${Array.from(this.liteTopicSet).join(', ')}], liteSubscriptionQuota=${this.liteSubscriptionQuota}, maxLiteTopicSize=${this.maxLiteTopicSize}}`;
  }
}
