import type {
  Attachment,
  CompleteAttachment,
  CreateAttachment,
  PendingAttachment,
} from "../../types/attachment";
import type { MessageRole, AppendMessage } from "../../types/message";
import type { QuoteInfo } from "../../types/quote";
import type { Unsubscribe } from "../../types/unsubscribe";
import type { RunConfig } from "../../types/message";
import { BaseSubscribable } from "../../subscribable/subscribable";
import {
  type AttachmentAdapter,
  fileMatchesAccept,
} from "../../adapters/attachment";
import type {
  AttachmentAddErrorReason,
  ComposerRuntimeCore,
  ComposerRuntimeEventCallback,
  ComposerRuntimeEventPayload,
  ComposerRuntimeEventType,
  DictationState,
  SendOptions,
} from "../interfaces/composer-runtime-core";
import type { DictationAdapter } from "../../adapters/speech";
import { generateId } from "../../utils/id";

const isAttachmentComplete = (a: Attachment): a is CompleteAttachment =>
  a.status.type === "complete";

export abstract class BaseComposerRuntimeCore
  extends BaseSubscribable
  implements ComposerRuntimeCore
{
  public readonly isEditing = true;

  protected abstract getAttachmentAdapter(): AttachmentAdapter | undefined;
  protected abstract getDictationAdapter(): DictationAdapter | undefined;

  public get attachmentAccept(): string {
    return this.getAttachmentAdapter()?.accept ?? "*";
  }

  private _attachments: readonly Attachment[] = [];
  public get attachments() {
    return this._attachments;
  }

  protected setAttachments(value: readonly Attachment[]) {
    this._attachments = value;
    this._notifySubscribers();
  }

  public abstract get canCancel(): boolean;

  public get isEmpty() {
    return !this.text.trim() && !this.attachments.length;
  }

  private _text = "";

  get text() {
    return this._text;
  }

  private _role: MessageRole = "user";

  get role() {
    return this._role;
  }

  private _runConfig: RunConfig = {};

  get runConfig() {
    return this._runConfig;
  }

  private _quote: QuoteInfo | undefined = undefined;

  get quote() {
    return this._quote;
  }

  public setQuote(quote: QuoteInfo | undefined) {
    if (this._quote === quote) return;

    this._quote = quote;
    this._notifySubscribers();
  }

  public setText(value: string) {
    if (this._text === value) return;

    this._text = value;
    if (this._dictation) {
      this._dictationBaseText = value;
      this._currentInterimText = "";
      const { status, inputDisabled } = this._dictation;
      this._dictation = inputDisabled ? { status, inputDisabled } : { status };
    }
    this._notifySubscribers();
  }

  public setRole(role: MessageRole) {
    if (this._role === role) return;

    this._role = role;
    this._notifySubscribers();
  }

  public setRunConfig(runConfig: RunConfig) {
    if (this._runConfig === runConfig) return;

    this._runConfig = runConfig;
    this._notifySubscribers();
  }

  private _emptyTextAndAttachments() {
    this._attachments = [];
    this._text = "";
    this._notifySubscribers();
  }

  private async _onClearAttachments() {
    const adapter = this.getAttachmentAdapter();
    if (adapter) {
      const pending = this._attachments.filter((a) => !isAttachmentComplete(a));
      await Promise.all(pending.map((a) => adapter.remove(a)));
    }
  }

  public async reset() {
    if (
      this._attachments.length === 0 &&
      this._text === "" &&
      this._role === "user" &&
      Object.keys(this._runConfig).length === 0 &&
      this._quote === undefined
    ) {
      return;
    }

    this._role = "user";
    this._runConfig = {};
    this._quote = undefined;

    const task = this._onClearAttachments();
    this._emptyTextAndAttachments();
    await task;
  }

  public async clearAttachments() {
    const task = this._onClearAttachments();
    this.setAttachments([]);

    await task;
  }

  public async send(options?: SendOptions) {
    if (this.isEmpty) return;

    if (this._dictationSession) {
      this._dictationSession.cancel();
      this._cleanupDictation();
    }

    const adapter = this.getAttachmentAdapter();
    const attachments =
      this.attachments.length > 0
        ? Promise.all(
            this.attachments.map(async (a) => {
              if (isAttachmentComplete(a)) return a;
              if (!adapter) throw new Error("Attachments are not supported");
              const result = await adapter.send(a);
              return result as CompleteAttachment;
            }),
          )
        : [];

    const text = this.text;
    const quote = this._quote;
    this._quote = undefined;
    this._emptyTextAndAttachments();

    const message: Omit<AppendMessage, "parentId" | "sourceId"> = {
      createdAt: new Date(),
      role: this.role,
      content: text ? [{ type: "text", text }] : [],
      attachments: await attachments,
      runConfig: this.runConfig,
      metadata: { custom: { ...(quote ? { quote } : {}) } },
    };

    this.handleSend(message, options);
    this._notifyEventSubscribers("send", {});
  }

  public cancel() {
    this.handleCancel();
  }

  protected abstract handleSend(
    message: Omit<AppendMessage, "parentId" | "sourceId">,
    options?: SendOptions,
  ): void;
  protected abstract handleCancel(): void;

  async addAttachment(fileOrAttachment: File | CreateAttachment) {
    if (!(fileOrAttachment instanceof File)) {
      const adapter = this.getAttachmentAdapter();
      if (
        adapter &&
        !fileMatchesAccept(
          {
            name: fileOrAttachment.name,
            type: fileOrAttachment.contentType ?? "",
          },
          adapter.accept,
        )
      ) {
        const message = `File type ${fileOrAttachment.contentType || "unknown"} is not accepted. Accepted types: ${adapter.accept}`;
        const err = new Error(message);
        this._safeEmitAttachmentAddError(
          "not-accepted",
          message,
          undefined,
          err,
        );
        throw err;
      }

      const a: CompleteAttachment = {
        id: fileOrAttachment.id ?? generateId(),
        type: fileOrAttachment.type ?? "document",
        name: fileOrAttachment.name,
        contentType: fileOrAttachment.contentType,
        content: fileOrAttachment.content,
        status: { type: "complete" },
      };
      this._attachments = [...this._attachments, a];
      this._notifySubscribers();
      this._notifyEventSubscribers("attachmentAdd", {});
      return;
    }

    const upsertAttachment = (a: PendingAttachment) => {
      const idx = this._attachments.findIndex(
        (attachment) => attachment.id === a.id,
      );
      if (idx !== -1)
        this._attachments = [
          ...this._attachments.slice(0, idx),
          a,
          ...this._attachments.slice(idx + 1),
        ];
      else {
        this._attachments = [...this._attachments, a];
      }

      this._notifySubscribers();
    };

    const adapter = this.getAttachmentAdapter();
    if (!adapter) {
      const message = "Attachments are not supported";
      const err = new Error(message);
      this._safeEmitAttachmentAddError("no-adapter", message, undefined, err);
      throw err;
    }

    if (
      !fileMatchesAccept(
        { name: fileOrAttachment.name, type: fileOrAttachment.type },
        adapter.accept,
      )
    ) {
      const message = `File type ${fileOrAttachment.type || "unknown"} is not accepted. Accepted types: ${adapter.accept}`;
      const err = new Error(message);
      this._safeEmitAttachmentAddError("not-accepted", message, undefined, err);
      throw err;
    }

    let lastAttachment: PendingAttachment | undefined;
    try {
      const promiseOrGenerator = adapter.add({ file: fileOrAttachment });
      if (Symbol.asyncIterator in promiseOrGenerator) {
        for await (const r of promiseOrGenerator) {
          lastAttachment = r;
          upsertAttachment(r);
        }
      } else {
        lastAttachment = await promiseOrGenerator;
        upsertAttachment(lastAttachment);
      }
    } catch (e) {
      if (lastAttachment) {
        upsertAttachment({
          ...lastAttachment,
          status: { type: "incomplete", reason: "error" },
        });
      }
      this._safeEmitAttachmentAddError(
        "adapter-error",
        e instanceof Error ? e.message : String(e),
        lastAttachment?.id,
        e instanceof Error ? e : undefined,
      );
      throw e;
    }

    const hasError =
      lastAttachment?.status.type === "incomplete" &&
      lastAttachment.status.reason === "error";
    if (hasError) {
      this._safeEmitAttachmentAddError(
        "adapter-error",
        "Attachment upload did not complete successfully.",
        lastAttachment?.id,
      );
    } else {
      this._notifyEventSubscribers("attachmentAdd", {});
    }
  }

  private _safeEmitAttachmentAddError(
    reason: AttachmentAddErrorReason,
    message: string,
    attachmentId?: string,
    error?: Error,
  ) {
    try {
      this._notifyEventSubscribers("attachmentAddError", {
        reason,
        message,
        ...(attachmentId !== undefined && { attachmentId }),
        ...(error !== undefined && { error }),
      });
    } catch (subscriberError) {
      console.error(
        "[assistant-ui] attachmentAddError subscriber threw:",
        subscriberError,
      );
    }
  }

  async removeAttachment(attachmentId: string) {
    const index = this._attachments.findIndex((a) => a.id === attachmentId);
    if (index === -1) throw new Error("Attachment not found");
    const attachment = this._attachments[index]!;

    if (!isAttachmentComplete(attachment)) {
      const adapter = this.getAttachmentAdapter();
      if (!adapter) throw new Error("Attachments are not supported");
      await adapter.remove(attachment);
    }

    this._attachments = this._attachments.filter((a) => a.id !== attachmentId);
    this._notifySubscribers();
  }

  private _dictation: DictationState | undefined;
  private _dictationSession: DictationAdapter.Session | undefined;
  private _dictationUnsubscribes: Unsubscribe[] = [];
  private _dictationBaseText = "";
  private _currentInterimText = "";
  private _dictationSessionIdCounter = 0;
  private _activeDictationSessionId: number | undefined;
  private _isCleaningDictation = false;

  public get dictation(): DictationState | undefined {
    return this._dictation;
  }

  private _isActiveSession(
    sessionId: number,
    session: DictationAdapter.Session,
  ): boolean {
    return (
      this._activeDictationSessionId === sessionId &&
      this._dictationSession === session
    );
  }

  public startDictation(): void {
    const adapter = this.getDictationAdapter();
    if (!adapter) {
      throw new Error("Dictation adapter not configured");
    }

    if (this._dictationSession) {
      for (const unsub of this._dictationUnsubscribes) {
        unsub();
      }
      this._dictationUnsubscribes = [];
      const oldSession = this._dictationSession;
      oldSession.stop().catch(() => {});
      this._dictationSession = undefined;
    }

    const inputDisabled = adapter.disableInputDuringDictation ?? false;

    this._dictationBaseText = this._text;
    this._currentInterimText = "";

    const session = adapter.listen();
    this._dictationSession = session;
    const sessionId = ++this._dictationSessionIdCounter;
    this._activeDictationSessionId = sessionId;
    this._dictation = { status: session.status, inputDisabled };
    this._notifySubscribers();

    const unsubSpeech = session.onSpeech((result) => {
      if (!this._isActiveSession(sessionId, session)) return;
      const isFinal = result.isFinal !== false;

      const needsSeparator =
        this._dictationBaseText &&
        !this._dictationBaseText.endsWith(" ") &&
        result.transcript;
      const separator = needsSeparator ? " " : "";

      if (isFinal) {
        this._dictationBaseText =
          this._dictationBaseText + separator + result.transcript;
        this._currentInterimText = "";
        this._text = this._dictationBaseText;

        if (this._dictation) {
          const { transcript: _, ...rest } = this._dictation;
          this._dictation = rest;
        }
        this._notifySubscribers();
      } else {
        this._currentInterimText = separator + result.transcript;
        this._text = this._dictationBaseText + this._currentInterimText;

        if (this._dictation) {
          this._dictation = {
            ...this._dictation,
            transcript: result.transcript,
          };
        }
        this._notifySubscribers();
      }
    });
    this._dictationUnsubscribes.push(unsubSpeech);

    const unsubStart = session.onSpeechStart(() => {
      if (!this._isActiveSession(sessionId, session)) return;

      this._dictation = {
        status: { type: "running" },
        inputDisabled,
        ...(this._dictation?.transcript && {
          transcript: this._dictation.transcript,
        }),
      };
      this._notifySubscribers();
    });
    this._dictationUnsubscribes.push(unsubStart);

    const unsubEnd = session.onSpeechEnd(() => {
      this._cleanupDictation({ sessionId });
    });
    this._dictationUnsubscribes.push(unsubEnd);

    const statusInterval = setInterval(() => {
      if (!this._isActiveSession(sessionId, session)) return;

      if (session.status.type === "ended") {
        this._cleanupDictation({ sessionId });
      }
    }, 100);
    this._dictationUnsubscribes.push(() => clearInterval(statusInterval));
  }

  public stopDictation(): void {
    if (!this._dictationSession) return;

    const session = this._dictationSession;
    const sessionId = this._activeDictationSessionId;
    session.stop().finally(() => {
      this._cleanupDictation({ sessionId });
    });
  }

  private _cleanupDictation(options?: { sessionId: number | undefined }): void {
    const isStaleSession =
      options?.sessionId !== undefined &&
      options.sessionId !== this._activeDictationSessionId;
    if (isStaleSession || this._isCleaningDictation) return;

    this._isCleaningDictation = true;
    try {
      for (const unsub of this._dictationUnsubscribes) {
        unsub();
      }
      this._dictationUnsubscribes = [];
      this._dictationSession = undefined;
      this._activeDictationSessionId = undefined;
      this._dictation = undefined;
      this._dictationBaseText = "";
      this._currentInterimText = "";
      this._notifySubscribers();
    } finally {
      this._isCleaningDictation = false;
    }
  }

  private _eventSubscribers = new Map<
    ComposerRuntimeEventType,
    Set<(payload?: unknown) => void>
  >();

  protected _notifyEventSubscribers<E extends ComposerRuntimeEventType>(
    event: E,
    payload: ComposerRuntimeEventPayload[E],
  ) {
    const subscribers = this._eventSubscribers.get(event);
    if (!subscribers) return;

    for (const callback of subscribers) callback(payload);
  }

  public unstable_on<E extends ComposerRuntimeEventType>(
    event: E,
    callback: ComposerRuntimeEventCallback<E>,
  ) {
    const wrapped = callback as (payload?: unknown) => void;
    let subscribers = this._eventSubscribers.get(event);
    if (!subscribers) {
      subscribers = new Set();
      this._eventSubscribers.set(event, subscribers);
    }
    subscribers.add(wrapped);

    return () => {
      this._eventSubscribers.get(event)?.delete(wrapped);
    };
  }
}
