/**
 * Progressive message updating for channel adapters that support it.
 *
 * Two "thinking" strategies, chosen by whether a `setStatus` callback is
 * supplied:
 *
 *   - Native status (Slack AI apps): show `assistant.threads.setStatus`
 *     ("is thinking…", "is using WebSearch…") while the agent works, then post
 *     the reply as a single clean message once text starts streaming. Posting a
 *     message clears the status automatically.
 *   - Placeholder fallback (everything else, or when setStatus is unavailable):
 *     post a "_Thinking…_" message up front and edit it in place as text
 *     streams in.
 *
 * Used by the gateway to give Slack (and similar platforms) a real-time
 * streaming experience instead of waiting for the full response.
 */

import type { AgentEvent } from "./types.ts";

const MAX_LENGTH = 4000;

type PostFn = (text: string) => Promise<string | undefined>;
type UpdateFn = (messageId: string, text: string) => Promise<void>;
type DeleteFn = (messageId: string) => Promise<void>;
/** Set the native working status; empty string clears it. Rejects if unavailable. */
type SetStatusFn = (status: string) => Promise<void>;

/** Strip the `mcp__server__` prefix so tool names read cleanly. */
function toolShortName(name: string): string {
  return name.startsWith("mcp__") ? (name.split("__").pop() ?? name) : name;
}

/** Status fragment for the native indicator: "using WebSearch". */
function humanizeTool(name: string): string {
  return `using ${toolShortName(name)}`;
}

export class StreamingResponder {
  private post: PostFn;
  private update: UpdateFn;
  private deleteFn: DeleteFn | undefined;
  private setStatusFn: SetStatusFn | undefined;

  private messageId: string | undefined;
  private buffer = "";
  private lastSent = "";
  private timer: ReturnType<typeof setInterval> | null = null;
  private intervalMs: number;
  private toolInProgress: string | null = null;
  private failed = false;
  private started = false;
  /** Guards against concurrent post() calls (placeholder or first stream chunk). */
  private posting = false;
  /**
   * The in-flight placeholder/first-chunk post, if any. `finalize()` awaits it
   * so `messageId`/`usingStatus`/`failed` are settled before it decides how to
   * deliver -- otherwise a result arriving mid-post double-posts (native mode)
   * or orphans the placeholder (fallback mode). Never rejects.
   */
  private pendingPost: Promise<void> | null = null;

  /** Using the native status indicator (setStatus supplied and still working). */
  private usingStatus = false;
  /** setStatus rejected once -- stop trying, fall back to a placeholder message. */
  private statusFailed = false;
  private lastStatus = "";

  constructor(
    post: PostFn,
    update: UpdateFn,
    deleteFn?: DeleteFn,
    setStatus?: SetStatusFn,
    intervalMs = 1500,
  ) {
    this.post = post;
    this.update = update;
    this.deleteFn = deleteFn;
    this.setStatusFn = setStatus;
    this.intervalMs = intervalMs;
  }

  /** Pass as the `emit` callback to the message queue. */
  handleEvent = (event: AgentEvent): void => {
    if (this.failed) return;

    switch (event.type) {
      case "stream_event": {
        const sdkMsg = event.event as {
          type: string;
          event?: {
            type: string;
            delta?: { type: string; text?: string };
            content_block?: { type: string; name?: string };
          };
        };

        if (sdkMsg.type === "stream_event" && sdkMsg.event) {
          const inner = sdkMsg.event;
          if (inner.type === "content_block_delta") {
            const delta = inner.delta;
            if (delta?.type === "text_delta" && delta.text) {
              this.toolInProgress = null;
              this.buffer += delta.text;
              this.ensureTimer();
            }
          }
        }
        break;
      }

      case "tool_use_summary": {
        this.toolInProgress = event.tool_name;
        // Before any text: reflect the tool in the native status if we have one,
        // otherwise let the placeholder message show "_Using …_" via the timer.
        if (this.usingStatus && !this.messageId) {
          this.applyStatus(`is ${humanizeTool(event.tool_name)}…`);
        } else {
          this.ensureTimer();
        }
        break;
      }

      case "system": {
        // On processing start: show the "thinking" indicator.
        if (event.subtype === "status" && !this.started) {
          this.started = true;
          if (this.setStatusFn) {
            this.usingStatus = true;
            this.applyStatus("is thinking…");
          } else {
            this.postPlaceholder();
          }
        }
        break;
      }

      default:
        break;
    }
  };

  /**
   * Finalize the streaming message with the complete response.
   * Returns `true` if the streamer handled delivery (caller should skip normal send).
   * Returns `false` if the message is too long — caller should fall back to chunked send.
   */
  async finalize(fullText: string): Promise<boolean> {
    this.stopTimer();

    // Wait for any in-flight placeholder/first-chunk post to settle so
    // messageId and the mode flags are final before we decide how to deliver.
    // (stopTimer() above guarantees no new post can start after this point.)
    if (this.pendingPost) await this.pendingPost;

    if (this.failed) {
      this.clearStatus();
      return false;
    }

    // A message already exists (placeholder, or first streamed chunk): edit it.
    // Slack's chat.update supports up to ~40k chars -- no need to delete + repost.
    if (this.messageId) {
      try {
        await this.update(this.messageId, fullText);
        return true;
      } catch {
        // Update failed (e.g., message was deleted) -- fall back to normal send
        return false;
      }
    }

    // Native-status mode with nothing posted yet (short reply, or reply that
    // arrived faster than the flush tick): post the final text. The post clears
    // the status automatically.
    if (this.usingStatus) {
      try {
        const id = await this.post(fullText);
        if (id) return true;
        this.clearStatus();
        return false;
      } catch {
        this.clearStatus();
        return false;
      }
    }

    return false;
  }

  private postPlaceholder(): void {
    if (this.posting || this.messageId) return;
    this.posting = true;
    this.pendingPost = this.post("_Thinking…_")
      .then((id) => {
        if (id) {
          this.messageId = id;
        } else {
          this.failed = true;
        }
      })
      .catch(() => {
        this.failed = true;
      })
      .finally(() => {
        this.posting = false;
        this.pendingPost = null;
      });
  }

  /** Best-effort native status update; on first failure, fall back to a placeholder. */
  private applyStatus(status: string): void {
    if (!this.setStatusFn || this.statusFailed || status === this.lastStatus) return;
    this.lastStatus = status;
    this.setStatusFn(status).catch(() => {
      this.statusFailed = true;
      this.usingStatus = false;
      // Nothing shown yet -- degrade to the placeholder-message strategy.
      if (!this.messageId) this.postPlaceholder();
    });
  }

  private clearStatus(): void {
    if (!this.setStatusFn || this.statusFailed || !this.usingStatus) return;
    this.usingStatus = false;
    this.setStatusFn("").catch(() => {
      // Best-effort clear; ignore failures.
    });
  }

  private ensureTimer(): void {
    if (this.timer) return;
    this.timer = setInterval(() => this.flush(), this.intervalMs);
  }

  private stopTimer(): void {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }

  private flush(): void {
    if (this.failed) return;

    // Native-status mode, no message posted yet: post the first real chunk once
    // we have text (this clears the status). With no text yet, keep the status.
    if (this.usingStatus && !this.messageId) {
      if (this.buffer.trim()) {
        this.postFirstChunk();
      }
      return;
    }

    if (!this.messageId) return;

    let text: string;
    if (this.buffer) {
      // Real content: show it, with a tool footnote while one is running.
      text = this.buffer;
      if (this.toolInProgress) text += `\n\n_Using ${toolShortName(this.toolInProgress)}…_`;
    } else {
      // No text yet (placeholder mode): keep a clean phase-aware status line
      // in place of the static "_Thinking…_" — "Using WebSearch…" etc.
      text = this.toolInProgress ? `_Using ${toolShortName(this.toolInProgress)}…_` : "_Thinking…_";
    }

    // Nothing new to send
    if (text === this.lastSent) return;

    // Truncate if too long for an update (show the tail end)
    if (text.length > MAX_LENGTH) {
      text = "…" + text.slice(text.length - MAX_LENGTH + 1);
    }

    this.lastSent = text;
    this.update(this.messageId, text).catch(() => {
      // Rate-limited or failed — skip this tick, try again next interval
    });
  }

  /** Post the first streamed chunk as a fresh message (native-status mode). */
  private postFirstChunk(): void {
    if (this.posting || this.messageId) return;
    this.posting = true;
    let text = this.buffer;
    if (text.length > MAX_LENGTH) {
      text = "…" + text.slice(text.length - MAX_LENGTH + 1);
    }
    const sent = text;
    this.pendingPost = this.post(text)
      .then((id) => {
        if (id) {
          this.messageId = id;
          this.lastSent = sent;
          this.usingStatus = false; // status is cleared by the post
        } else {
          this.failed = true;
        }
      })
      .catch(() => {
        this.failed = true;
      })
      .finally(() => {
        this.posting = false;
        this.pendingPost = null;
      });
  }
}
