import { $inject, Alepha } from "alepha";
import { DateTimeProvider, type Timeout } from "alepha/datetime";
import { $logger } from "alepha/logger";
import { TemplatedPathParser } from "alepha/router";
import { TopicTimeoutError } from "../errors/TopicTimeoutError.ts";
import { $subscriber } from "../primitives/$subscriber.ts";
import {
  $topic,
  type TopicHandler,
  type TopicMessage,
  type TopicMessageSchema,
  type TopicWaitOptions,
} from "../primitives/$topic.ts";

/**
 * Base class for topic providers.
 */
export abstract class TopicProvider {
  protected readonly alepha = $inject(Alepha);
  protected readonly log = $logger();
  protected readonly dateTimeProvider = $inject(DateTimeProvider);

  /**
   * Publish a raw message to a topic.
   *
   * @param topic - The topic to publish to.
   * @param message - The message to publish.
   */
  public abstract publish(
    topic: string,
    message: string,
    options?: TopicPublishOptions,
  ): Promise<void>;

  /**
   * Subscribe to a topic with a raw callback.
   *
   * @param topic - The topic to subscribe to.
   * @param callback - The callback to call when a message is received.
   */
  public abstract subscribe(
    topic: string,
    callback: SubscribeCallback,
    options?: TopicSubscribeOptions,
  ): Promise<UnSubscribeFn>;

  /**
   * Unsubscribe from a topic.
   *
   * @param topic - The topic to unsubscribe from.
   */
  public abstract unsubscribe(topic: string): Promise<void>;

  /**
   * Encode and publish a typed message to a topic.
   */
  public async publishMessage<T extends TopicMessageSchema>(
    name: string,
    schema: T["payload"],
    payload: TopicMessage<T>["payload"],
    options?: TopicPublishOptions,
  ): Promise<void> {
    let topicName = name;
    if (options?.params) {
      const parser = new TemplatedPathParser(name);
      topicName = parser.interpolate(options.params);
    }

    await this.publish(
      topicName,
      JSON.stringify({
        payload: this.alepha.codec.encode(schema, payload),
      }),
      options,
    );
  }

  /**
   * Parse a raw message string into a typed topic message.
   */
  public parseMessage<T extends TopicMessageSchema>(
    schema: T["payload"],
    message: string,
  ): TopicMessage<T> {
    const { payload } = JSON.parse(message);
    return {
      payload: this.alepha.codec.decode(
        schema,
        payload,
      ) as TopicMessage<T>["payload"],
    } as TopicMessage<T>;
  }

  /**
   * Subscribe a typed handler to a topic, with error wrapping and message parsing.
   */
  public async subscribeHandler<T extends TopicMessageSchema>(
    name: string,
    schema: T["payload"],
    handler: TopicHandler<T>,
    options?: TopicSubscribeOptions,
  ): Promise<UnSubscribeFn> {
    const parser = new TemplatedPathParser(name);
    const subscribeTopic = parser.hasParams
      ? parser.wildcardize(this.wildcardChar())
      : name;

    return this.subscribe(
      subscribeTopic,
      async (message, receivedTopic?: string) => {
        try {
          const parsed = this.parseMessage<T>(schema, message);

          if (parser.hasParams && receivedTopic) {
            const params = parser.extract(receivedTopic) ?? {};
            await handler({ ...parsed, params } as TopicMessage<T>);
          } else {
            await handler(parsed);
          }
        } catch (error) {
          this.log.error("Message processing has failed", error);
        }
      },
      options,
    );
  }

  /**
   * Wait for a single message matching an optional filter, with timeout.
   */
  public async waitForMessage<T extends TopicMessageSchema>(
    name: string,
    schema: T["payload"],
    options: TopicWaitOptions<T> = {},
  ): Promise<TopicMessage<T>> {
    const filter = options.filter ?? (() => true);

    return new Promise((resolve, reject) => {
      const ref: { timeout?: Timeout; clear?: () => void } = {};

      (async () => {
        const clear = await this.subscribe(name, (raw) => {
          const message = this.parseMessage<T>(schema, raw);
          if (!filter(message)) {
            return;
          }

          ref.timeout?.clear();
          if (ref.clear) {
            ref.clear();
          }
          resolve(message);
        });

        ref.clear = clear;

        const timeoutDuration = options.timeout ?? [10, "seconds"];

        ref.timeout = this.dateTimeProvider.createTimeout(() => {
          clear();
          reject(
            new TopicTimeoutError(
              name,
              this.dateTimeProvider.duration(timeoutDuration).asMilliseconds(),
            ),
          );
        }, timeoutDuration);
      })();
    });
  }

  /**
   * The wildcard character used for pattern subscriptions.
   * Override in subclasses for provider-specific wildcards.
   */
  protected wildcardChar(): string {
    return "+";
  }

  /**
   * Returns the list of $subscribers for this provider.
   */
  protected subscribers(): Array<() => Promise<unknown>> {
    const handlers: Array<() => Promise<unknown>> = [];

    const topics = this.alepha.primitives($topic);

    for (const topic of topics) {
      if (topic.provider !== this) {
        continue;
      }

      const handler = topic.options.handler;
      if (handler && topic.provider === this) {
        handlers.push(() => topic.subscribe(handler));
      }
    }

    const subscribers = this.alepha.primitives($subscriber);
    for (const subscriber of subscribers) {
      if (subscriber.options.topic.provider !== this) {
        continue;
      }

      const handler = subscriber.handler.run.bind(subscriber.handler);
      handlers.push(() => subscriber.options.topic.subscribe(handler));
    }

    return handlers;
  }
}

export type SubscribeCallback = (
  message: string,
  topic?: string,
) => Promise<void> | void;

export type UnSubscribeFn = () => Promise<void>;

export interface TopicPublishOptions {
  retain?: boolean;
  params?: Record<string, string>;
}

// biome-ignore lint/suspicious/noEmptyInterface: augmented by provider-specific modules (e.g. MqttTopicProvider)
export interface TopicSubscribeOptions {}
