// src/index.ts
import { createHmac, timingSafeEqual } from "crypto";
import { extractCard, ValidationError } from "@chat-adapter/shared";
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import { ConsoleLogger, convertEmojiPlaceholders, Message } from "chat";

// src/cards.ts
import { renderGfmTable } from "@chat-adapter/shared";
import { cardChildToFallbackText } from "chat";
function cardToGitHubMarkdown(card) {
  const lines = [];
  if (card.title) {
    lines.push(`**${escapeMarkdown(card.title)}**`);
  }
  if (card.subtitle) {
    lines.push(escapeMarkdown(card.subtitle));
  }
  if ((card.title || card.subtitle) && card.children.length > 0) {
    lines.push("");
  }
  if (card.imageUrl) {
    lines.push(`![](${card.imageUrl})`);
    lines.push("");
  }
  for (let i = 0; i < card.children.length; i++) {
    const child = card.children[i];
    const childLines = renderChild(child);
    if (childLines.length > 0) {
      lines.push(...childLines);
      if (i < card.children.length - 1) {
        lines.push("");
      }
    }
  }
  return lines.join("\n");
}
function renderChild(child) {
  switch (child.type) {
    case "text":
      return renderText(child);
    case "fields":
      return renderFields(child);
    case "actions":
      return renderActions(child);
    case "section":
      return child.children.flatMap(renderChild);
    case "image":
      if (child.alt) {
        return [`![${escapeMarkdown(child.alt)}](${child.url})`];
      }
      return [`![](${child.url})`];
    case "link":
      return [`[${escapeMarkdown(child.label)}](${child.url})`];
    case "divider":
      return ["---"];
    case "table":
      return renderTable(child);
    default: {
      const text = cardChildToFallbackText(child);
      if (text) {
        return [text];
      }
      return [];
    }
  }
}
function renderText(text) {
  const content = text.content;
  switch (text.style) {
    case "bold":
      return [`**${content}**`];
    case "muted":
      return [`_${content}_`];
    default:
      return [content];
  }
}
function renderFields(fields) {
  return fields.children.map(
    (field) => `**${escapeMarkdown(field.label)}:** ${escapeMarkdown(field.value)}`
  );
}
function renderTable(table) {
  return renderGfmTable(table);
}
function renderActions(actions) {
  const buttonTexts = actions.children.map((button) => {
    if (button.type === "link-button") {
      return `[${escapeMarkdown(button.label)}](${button.url})`;
    }
    return `**[${escapeMarkdown(button.label)}]**`;
  });
  return [buttonTexts.join(" \u2022 ")];
}
function escapeMarkdown(text) {
  return text.replace(/\\/g, "\\\\").replace(/\*/g, "\\*").replace(/_/g, "\\_").replace(/\[/g, "\\[").replace(/\]/g, "\\]");
}

// src/markdown.ts
import {
  BaseFormatConverter,
  parseMarkdown,
  stringifyMarkdown
} from "chat";
var GitHubFormatConverter = class extends BaseFormatConverter {
  /**
   * GitHub uses standard GFM, so we can use remark-stringify directly.
   * We just need to ensure @mentions are preserved.
   */
  fromAst(ast) {
    return stringifyMarkdown(ast).trim();
  }
  /**
   * Parse GitHub markdown into an AST.
   * GitHub uses standard GFM, so we use the standard parser.
   */
  toAst(markdown) {
    return parseMarkdown(markdown);
  }
  /**
   * Override renderPostable to handle @mentions in plain strings.
   * GitHub @mentions are already in the correct format (@username).
   */
  renderPostable(message) {
    if (typeof message === "string") {
      return message;
    }
    if ("raw" in message) {
      return message.raw;
    }
    if ("markdown" in message) {
      return this.fromMarkdown(message.markdown);
    }
    if ("ast" in message) {
      return this.fromAst(message.ast);
    }
    return super.renderPostable(message);
  }
};

// src/index.ts
var REVIEW_COMMENT_THREAD_PATTERN = /^([^/]+)\/([^:]+):(\d+):rc:(\d+)$/;
var PR_THREAD_PATTERN = /^([^/]+)\/([^:]+):(\d+)$/;
var GitHubAdapter = class {
  name = "github";
  userName;
  // Single Octokit instance for PAT or single-tenant app mode
  octokit = null;
  // App credentials for multi-tenant mode
  appCredentials = null;
  // Cache of Octokit instances per installation (for multi-tenant)
  installationClients = /* @__PURE__ */ new Map();
  webhookSecret;
  chat = null;
  logger;
  _botUserId = null;
  formatConverter = new GitHubFormatConverter();
  /** Bot user ID (numeric) used for self-message detection */
  get botUserId() {
    return this._botUserId?.toString();
  }
  /** Whether this adapter is in multi-tenant mode (no fixed installation ID) */
  get isMultiTenant() {
    return this.appCredentials !== null && this.octokit === null;
  }
  constructor(config = {}) {
    const webhookSecret = config.webhookSecret ?? process.env.GITHUB_WEBHOOK_SECRET;
    if (!webhookSecret) {
      throw new ValidationError(
        "github",
        "webhookSecret is required. Set GITHUB_WEBHOOK_SECRET or provide it in config."
      );
    }
    this.webhookSecret = webhookSecret;
    this.logger = config.logger ?? new ConsoleLogger("info").child("github");
    this.userName = config.userName ?? process.env.GITHUB_BOT_USERNAME ?? "github-bot";
    this._botUserId = config.botUserId ?? null;
    const hasExplicitAuth = !!("token" in config && config.token || "appId" in config && config.appId || "privateKey" in config && config.privateKey);
    if ("token" in config && config.token) {
      this.octokit = new Octokit({ auth: config.token });
    } else if ("appId" in config && config.appId && "privateKey" in config && config.privateKey) {
      if ("installationId" in config && config.installationId) {
        this.octokit = new Octokit({
          authStrategy: createAppAuth,
          auth: {
            appId: config.appId,
            privateKey: config.privateKey,
            installationId: config.installationId
          }
        });
      } else {
        this.appCredentials = {
          appId: config.appId,
          privateKey: config.privateKey
        };
        this.logger.info(
          "GitHub adapter initialized in multi-tenant mode (installation ID will be extracted from webhooks)"
        );
      }
    } else if (hasExplicitAuth) {
      throw new ValidationError(
        "github",
        "Authentication is required. Set GITHUB_TOKEN or GITHUB_APP_ID/GITHUB_PRIVATE_KEY, or provide token/appId+privateKey in config."
      );
    } else {
      const token = process.env.GITHUB_TOKEN;
      if (token) {
        this.octokit = new Octokit({ auth: token });
      } else {
        const appId = process.env.GITHUB_APP_ID;
        const privateKey = process.env.GITHUB_PRIVATE_KEY;
        if (appId && privateKey) {
          const installationIdRaw = process.env.GITHUB_INSTALLATION_ID ? Number.parseInt(process.env.GITHUB_INSTALLATION_ID, 10) : void 0;
          if (installationIdRaw) {
            this.octokit = new Octokit({
              authStrategy: createAppAuth,
              auth: { appId, privateKey, installationId: installationIdRaw }
            });
          } else {
            this.appCredentials = { appId, privateKey };
            this.logger.info(
              "GitHub adapter initialized in multi-tenant mode (installation ID will be extracted from webhooks)"
            );
          }
        } else {
          throw new ValidationError(
            "github",
            "Authentication is required. Set GITHUB_TOKEN or GITHUB_APP_ID/GITHUB_PRIVATE_KEY, or provide token/appId+privateKey in config."
          );
        }
      }
    }
  }
  /**
   * Get or create an Octokit instance for a specific installation.
   * For single-tenant mode, returns the single instance.
   * For multi-tenant mode, creates/caches instances per installation.
   */
  getOctokit(installationId) {
    if (this.octokit) {
      return this.octokit;
    }
    if (!this.appCredentials) {
      throw new ValidationError("github", "Adapter not properly configured");
    }
    if (!installationId) {
      throw new ValidationError(
        "github",
        "Installation ID required for multi-tenant mode. This usually means you're trying to make an API call outside of a webhook context. For proactive messages, use thread IDs from previous webhook interactions."
      );
    }
    let client = this.installationClients.get(installationId);
    if (!client) {
      client = new Octokit({
        authStrategy: createAppAuth,
        auth: {
          appId: this.appCredentials.appId,
          privateKey: this.appCredentials.privateKey,
          installationId
        }
      });
      this.installationClients.set(installationId, client);
      this.logger.debug("Created Octokit client for installation", {
        installationId
      });
    }
    return client;
  }
  async initialize(chat) {
    this.chat = chat;
    if (!this._botUserId && this.octokit) {
      try {
        const { data: user } = await this.octokit.users.getAuthenticated();
        this._botUserId = user.id;
        this.logger.info("GitHub auth completed", {
          botUserId: this._botUserId,
          login: user.login
        });
      } catch (error) {
        this.logger.warn("Could not fetch bot user ID", { error });
      }
    }
  }
  /**
   * Get the state key for storing installation ID for a repository.
   */
  getInstallationKey(owner, repo) {
    return `github:install:${owner}/${repo}`;
  }
  /**
   * Store the installation ID for a repository (for multi-tenant mode).
   */
  async storeInstallationId(owner, repo, installationId) {
    if (!(this.chat && this.isMultiTenant)) {
      return;
    }
    const key = this.getInstallationKey(owner, repo);
    await this.chat.getState().set(key, installationId);
    this.logger.debug("Stored installation ID", {
      owner,
      repo,
      installationId
    });
  }
  /**
   * Get the installation ID for a repository (for multi-tenant mode).
   */
  async getInstallationId(owner, repo) {
    if (!(this.chat && this.isMultiTenant)) {
      return void 0;
    }
    const key = this.getInstallationKey(owner, repo);
    return await this.chat.getState().get(key) ?? void 0;
  }
  /**
   * Handle incoming webhook from GitHub.
   */
  async handleWebhook(request, options) {
    const body = await request.text();
    this.logger.debug("GitHub webhook raw body", {
      body: body.substring(0, 500)
    });
    const signature = request.headers.get("x-hub-signature-256");
    if (!this.verifySignature(body, signature)) {
      return new Response("Invalid signature", { status: 401 });
    }
    const eventType = request.headers.get("x-github-event");
    this.logger.debug("GitHub webhook event type", { eventType });
    if (eventType === "ping") {
      this.logger.info("GitHub webhook ping received");
      return new Response("pong", { status: 200 });
    }
    let payload;
    try {
      payload = JSON.parse(body);
    } catch {
      this.logger.error("GitHub webhook invalid JSON", {
        contentType: request.headers.get("content-type"),
        bodyPreview: body.substring(0, 200)
      });
      return new Response(
        "Invalid JSON. Make sure webhook Content-Type is set to application/json",
        { status: 400 }
      );
    }
    const installationId = payload.installation?.id;
    if (installationId && this.isMultiTenant) {
      const repo = payload.repository;
      await this.storeInstallationId(
        repo.owner.login,
        repo.name,
        installationId
      );
    }
    if (eventType === "issue_comment") {
      const issuePayload = payload;
      if (issuePayload.action === "created" && issuePayload.issue.pull_request) {
        this.handleIssueComment(issuePayload, installationId, options);
      }
    } else if (eventType === "pull_request_review_comment") {
      const reviewPayload = payload;
      if (reviewPayload.action === "created") {
        this.handleReviewComment(reviewPayload, installationId, options);
      }
    }
    return new Response("ok", { status: 200 });
  }
  /**
   * Verify GitHub webhook signature using HMAC-SHA256.
   */
  verifySignature(body, signature) {
    if (!signature) {
      return false;
    }
    const expected = `sha256=${createHmac("sha256", this.webhookSecret).update(body).digest("hex")}`;
    try {
      return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    } catch {
      return false;
    }
  }
  /**
   * Handle issue_comment webhook (PR-level comments in Conversation tab).
   */
  handleIssueComment(payload, _installationId, options) {
    if (!this.chat) {
      this.logger.warn("Chat instance not initialized, ignoring comment");
      return;
    }
    const { comment, issue, repository, sender } = payload;
    const threadId = this.encodeThreadId({
      owner: repository.owner.login,
      repo: repository.name,
      prNumber: issue.number
    });
    const message = this.parseIssueComment(
      comment,
      repository,
      issue.number,
      threadId
    );
    if (sender.id === this._botUserId) {
      this.logger.debug("Ignoring message from self", {
        messageId: comment.id
      });
      return;
    }
    this.chat.processMessage(this, threadId, message, options);
  }
  /**
   * Handle pull_request_review_comment webhook (line-specific comments).
   */
  handleReviewComment(payload, _installationId, options) {
    if (!this.chat) {
      this.logger.warn("Chat instance not initialized, ignoring comment");
      return;
    }
    const { comment, pull_request, repository, sender } = payload;
    const rootCommentId = comment.in_reply_to_id ?? comment.id;
    const threadId = this.encodeThreadId({
      owner: repository.owner.login,
      repo: repository.name,
      prNumber: pull_request.number,
      reviewCommentId: rootCommentId
    });
    const message = this.parseReviewComment(
      comment,
      repository,
      pull_request.number,
      threadId
    );
    if (sender.id === this._botUserId) {
      this.logger.debug("Ignoring message from self", {
        messageId: comment.id
      });
      return;
    }
    this.chat.processMessage(this, threadId, message, options);
  }
  /**
   * Parse an issue comment into a normalized Message.
   */
  parseIssueComment(comment, repository, prNumber, threadId) {
    const author = this.parseAuthor(comment.user);
    return new Message({
      id: comment.id.toString(),
      threadId,
      text: this.formatConverter.extractPlainText(comment.body),
      formatted: this.formatConverter.toAst(comment.body),
      raw: {
        type: "issue_comment",
        comment,
        repository: {
          id: 0,
          // Not needed for raw storage
          name: repository.name,
          full_name: `${repository.owner.login}/${repository.name}`,
          owner: repository.owner
        },
        prNumber
      },
      author,
      metadata: {
        dateSent: new Date(comment.created_at),
        edited: comment.created_at !== comment.updated_at,
        editedAt: comment.created_at !== comment.updated_at ? new Date(comment.updated_at) : void 0
      },
      attachments: []
    });
  }
  /**
   * Parse a review comment into a normalized Message.
   */
  parseReviewComment(comment, repository, prNumber, threadId) {
    const author = this.parseAuthor(comment.user);
    return new Message({
      id: comment.id.toString(),
      threadId,
      text: this.formatConverter.extractPlainText(comment.body),
      formatted: this.formatConverter.toAst(comment.body),
      raw: {
        type: "review_comment",
        comment,
        repository: {
          id: 0,
          name: repository.name,
          full_name: `${repository.owner.login}/${repository.name}`,
          owner: repository.owner
        },
        prNumber
      },
      author,
      metadata: {
        dateSent: new Date(comment.created_at),
        edited: comment.created_at !== comment.updated_at,
        editedAt: comment.created_at !== comment.updated_at ? new Date(comment.updated_at) : void 0
      },
      attachments: []
    });
  }
  /**
   * Parse a GitHub user into an Author.
   */
  parseAuthor(user) {
    return {
      userId: user.id.toString(),
      userName: user.login,
      fullName: user.login,
      // GitHub doesn't always expose real names
      isBot: user.type === "Bot",
      isMe: user.id === this._botUserId
    };
  }
  /**
   * Get the Octokit client for a specific thread.
   * In multi-tenant mode, looks up the installation ID from state.
   */
  async getOctokitForThread(owner, repo) {
    const installationId = await this.getInstallationId(owner, repo);
    return this.getOctokit(installationId);
  }
  /**
   * Post a message to a thread.
   */
  async postMessage(threadId, message) {
    const { owner, repo, prNumber, reviewCommentId } = this.decodeThreadId(threadId);
    const octokit = await this.getOctokitForThread(owner, repo);
    let body;
    const card = extractCard(message);
    if (card) {
      body = cardToGitHubMarkdown(card);
    } else {
      body = this.formatConverter.renderPostable(message);
    }
    body = convertEmojiPlaceholders(body, "github");
    if (reviewCommentId) {
      const { data: comment2 } = await octokit.pulls.createReplyForReviewComment(
        {
          owner,
          repo,
          pull_number: prNumber,
          comment_id: reviewCommentId,
          body
        }
      );
      return {
        id: comment2.id.toString(),
        threadId,
        raw: {
          type: "review_comment",
          comment: comment2,
          repository: {
            id: 0,
            name: repo,
            full_name: `${owner}/${repo}`,
            owner: { id: 0, login: owner, type: "User" }
          },
          prNumber
        }
      };
    }
    const { data: comment } = await octokit.issues.createComment({
      owner,
      repo,
      issue_number: prNumber,
      body
    });
    return {
      id: comment.id.toString(),
      threadId,
      raw: {
        type: "issue_comment",
        comment,
        repository: {
          id: 0,
          name: repo,
          full_name: `${owner}/${repo}`,
          owner: { id: 0, login: owner, type: "User" }
        },
        prNumber
      }
    };
  }
  /**
   * Edit an existing message.
   */
  async editMessage(threadId, messageId, message) {
    const { owner, repo, prNumber, reviewCommentId } = this.decodeThreadId(threadId);
    const commentId = Number.parseInt(messageId, 10);
    const octokit = await this.getOctokitForThread(owner, repo);
    let body;
    const card = extractCard(message);
    if (card) {
      body = cardToGitHubMarkdown(card);
    } else {
      body = this.formatConverter.renderPostable(message);
    }
    body = convertEmojiPlaceholders(body, "github");
    if (reviewCommentId) {
      const { data: comment2 } = await octokit.pulls.updateReviewComment({
        owner,
        repo,
        comment_id: commentId,
        body
      });
      return {
        id: comment2.id.toString(),
        threadId,
        raw: {
          type: "review_comment",
          comment: comment2,
          repository: {
            id: 0,
            name: repo,
            full_name: `${owner}/${repo}`,
            owner: { id: 0, login: owner, type: "User" }
          },
          prNumber
        }
      };
    }
    const { data: comment } = await octokit.issues.updateComment({
      owner,
      repo,
      comment_id: commentId,
      body
    });
    return {
      id: comment.id.toString(),
      threadId,
      raw: {
        type: "issue_comment",
        comment,
        repository: {
          id: 0,
          name: repo,
          full_name: `${owner}/${repo}`,
          owner: { id: 0, login: owner, type: "User" }
        },
        prNumber
      }
    };
  }
  /**
   * Stream a message by accumulating text and posting once.
   *
   * The default fallback posts a placeholder then edits it every 500ms.
   * GitHub rejects these edits with 422 because `body` is empty before
   * the first token arrives, and rapid edits risk secondary rate limits.
   */
  async stream(threadId, textStream, _options) {
    let text = "";
    for await (const chunk of textStream) {
      if (typeof chunk === "string") {
        text += chunk;
      } else if (chunk.type === "markdown_text") {
        text += chunk.text;
      }
    }
    return this.postMessage(threadId, { markdown: text });
  }
  /**
   * Delete a message.
   */
  async deleteMessage(threadId, messageId) {
    const { owner, repo, reviewCommentId } = this.decodeThreadId(threadId);
    const commentId = Number.parseInt(messageId, 10);
    const octokit = await this.getOctokitForThread(owner, repo);
    if (reviewCommentId) {
      await octokit.pulls.deleteReviewComment({
        owner,
        repo,
        comment_id: commentId
      });
    } else {
      await octokit.issues.deleteComment({
        owner,
        repo,
        comment_id: commentId
      });
    }
  }
  /**
   * Add a reaction to a message.
   */
  async addReaction(threadId, messageId, emoji) {
    const { owner, repo, reviewCommentId } = this.decodeThreadId(threadId);
    const commentId = Number.parseInt(messageId, 10);
    const octokit = await this.getOctokitForThread(owner, repo);
    const content = this.emojiToGitHubReaction(emoji);
    if (reviewCommentId) {
      await octokit.reactions.createForPullRequestReviewComment({
        owner,
        repo,
        comment_id: commentId,
        content
      });
    } else {
      await octokit.reactions.createForIssueComment({
        owner,
        repo,
        comment_id: commentId,
        content
      });
    }
  }
  /**
   * Remove a reaction from a message.
   */
  async removeReaction(threadId, messageId, emoji) {
    const { owner, repo, reviewCommentId } = this.decodeThreadId(threadId);
    const commentId = Number.parseInt(messageId, 10);
    const content = this.emojiToGitHubReaction(emoji);
    const octokit = await this.getOctokitForThread(owner, repo);
    if (!this._botUserId) {
      try {
        const { data: user } = await octokit.users.getAuthenticated();
        this._botUserId = user.id;
      } catch {
        this.logger.warn("Could not detect bot user ID for reaction removal");
      }
    }
    const reactions = reviewCommentId ? (await octokit.reactions.listForPullRequestReviewComment({
      owner,
      repo,
      comment_id: commentId
    })).data : (await octokit.reactions.listForIssueComment({
      owner,
      repo,
      comment_id: commentId
    })).data;
    const reaction = reactions.find(
      (r) => r.content === content && r.user?.id === this._botUserId
    );
    if (reaction) {
      if (reviewCommentId) {
        await octokit.reactions.deleteForPullRequestComment({
          owner,
          repo,
          comment_id: commentId,
          reaction_id: reaction.id
        });
      } else {
        await octokit.reactions.deleteForIssueComment({
          owner,
          repo,
          comment_id: commentId,
          reaction_id: reaction.id
        });
      }
    }
  }
  /**
   * Convert SDK emoji to GitHub reaction content.
   */
  emojiToGitHubReaction(emoji) {
    const emojiName = typeof emoji === "string" ? emoji : emoji.name;
    const mapping = {
      thumbs_up: "+1",
      "+1": "+1",
      thumbs_down: "-1",
      "-1": "-1",
      laugh: "laugh",
      smile: "laugh",
      confused: "confused",
      thinking: "confused",
      heart: "heart",
      love_eyes: "heart",
      hooray: "hooray",
      party: "hooray",
      confetti: "hooray",
      rocket: "rocket",
      eyes: "eyes"
    };
    return mapping[emojiName] || "+1";
  }
  /**
   * Show typing indicator (no-op for GitHub).
   */
  async startTyping(_threadId, _status) {
  }
  /**
   * Fetch messages from a thread.
   */
  async fetchMessages(threadId, options) {
    const { owner, repo, prNumber, reviewCommentId } = this.decodeThreadId(threadId);
    const limit = options?.limit ?? 100;
    const direction = options?.direction ?? "backward";
    const octokit = await this.getOctokitForThread(owner, repo);
    let messages;
    if (reviewCommentId) {
      const { data: allComments } = await octokit.pulls.listReviewComments({
        owner,
        repo,
        pull_number: prNumber,
        per_page: 100
        // Fetch more to filter
      });
      const threadComments = allComments.filter(
        (c) => c.id === reviewCommentId || c.in_reply_to_id === reviewCommentId
      );
      messages = threadComments.map(
        (comment) => this.parseReviewComment(
          comment,
          {
            owner: { id: 0, login: owner, type: "User", avatar_url: "" },
            name: repo
          },
          prNumber,
          threadId
        )
      );
    } else {
      const { data: comments } = await octokit.issues.listComments({
        owner,
        repo,
        issue_number: prNumber,
        per_page: limit
      });
      messages = comments.map(
        (comment) => this.parseIssueComment(
          comment,
          {
            owner: { id: 0, login: owner, type: "User", avatar_url: "" },
            name: repo
          },
          prNumber,
          threadId
        )
      );
    }
    messages.sort(
      (a, b) => a.metadata.dateSent.getTime() - b.metadata.dateSent.getTime()
    );
    if (direction === "backward" && messages.length > limit) {
      messages = messages.slice(-limit);
    } else if (direction === "forward" && messages.length > limit) {
      messages = messages.slice(0, limit);
    }
    return {
      messages,
      nextCursor: void 0
      // Simplified pagination for now
    };
  }
  /**
   * Fetch thread metadata.
   */
  async fetchThread(threadId) {
    const { owner, repo, prNumber, reviewCommentId } = this.decodeThreadId(threadId);
    const octokit = await this.getOctokitForThread(owner, repo);
    const { data: pr } = await octokit.pulls.get({
      owner,
      repo,
      pull_number: prNumber
    });
    return {
      id: threadId,
      channelId: `${owner}/${repo}`,
      channelName: `${repo} #${prNumber}`,
      isDM: false,
      metadata: {
        owner,
        repo,
        prNumber,
        prTitle: pr.title,
        prState: pr.state,
        reviewCommentId
      }
    };
  }
  /**
   * Encode platform data into a thread ID string.
   *
   * Thread ID formats:
   * - PR-level: `github:{owner}/{repo}:{prNumber}`
   * - Review comment: `github:{owner}/{repo}:{prNumber}:rc:{reviewCommentId}`
   */
  encodeThreadId(platformData) {
    const { owner, repo, prNumber, reviewCommentId } = platformData;
    if (reviewCommentId) {
      return `github:${owner}/${repo}:${prNumber}:rc:${reviewCommentId}`;
    }
    return `github:${owner}/${repo}:${prNumber}`;
  }
  /**
   * Decode thread ID string back to platform data.
   */
  decodeThreadId(threadId) {
    if (!threadId.startsWith("github:")) {
      throw new ValidationError(
        "github",
        `Invalid GitHub thread ID: ${threadId}`
      );
    }
    const withoutPrefix = threadId.slice(7);
    const rcMatch = withoutPrefix.match(REVIEW_COMMENT_THREAD_PATTERN);
    if (rcMatch) {
      return {
        owner: rcMatch[1],
        repo: rcMatch[2],
        prNumber: Number.parseInt(rcMatch[3], 10),
        reviewCommentId: Number.parseInt(rcMatch[4], 10)
      };
    }
    const prMatch = withoutPrefix.match(PR_THREAD_PATTERN);
    if (prMatch) {
      return {
        owner: prMatch[1],
        repo: prMatch[2],
        prNumber: Number.parseInt(prMatch[3], 10)
      };
    }
    throw new ValidationError(
      "github",
      `Invalid GitHub thread ID format: ${threadId}`
    );
  }
  /**
   * Derive channel ID from a GitHub thread ID.
   * github:{owner}/{repo}:{prNumber}... -> github:{owner}/{repo}
   */
  channelIdFromThreadId(threadId) {
    const { owner, repo } = this.decodeThreadId(threadId);
    return `github:${owner}/${repo}`;
  }
  /**
   * List threads (PRs) in a GitHub repository.
   * Each open PR is treated as a thread.
   */
  async listThreads(channelId, options = {}) {
    const withoutPrefix = channelId.slice(7);
    const slashIndex = withoutPrefix.indexOf("/");
    if (slashIndex === -1) {
      throw new ValidationError(
        "github",
        `Invalid GitHub channel ID: ${channelId}`
      );
    }
    const owner = withoutPrefix.slice(0, slashIndex);
    const repo = withoutPrefix.slice(slashIndex + 1);
    const octokit = await this.getOctokitForThread(owner, repo);
    const limit = options.limit || 30;
    this.logger.debug("GitHub API: pulls.list", { owner, repo, limit });
    const { data: pulls } = await octokit.pulls.list({
      owner,
      repo,
      state: "open",
      sort: "updated",
      direction: "desc",
      per_page: limit,
      page: options.cursor ? Number.parseInt(options.cursor, 10) : 1
    });
    const threads = pulls.map(
      (pr) => {
        const threadId = this.encodeThreadId({
          owner,
          repo,
          prNumber: pr.number
        });
        const rootMessage = new Message({
          id: pr.number.toString(),
          threadId,
          text: pr.title,
          formatted: this.formatConverter.toAst(pr.title),
          raw: {
            type: "issue_comment",
            comment: {
              id: pr.number,
              body: pr.body || pr.title,
              user: pr.user,
              created_at: pr.created_at,
              updated_at: pr.updated_at,
              html_url: pr.html_url
            },
            repository: {
              id: 0,
              name: repo,
              full_name: `${owner}/${repo}`,
              owner: { id: 0, login: owner, type: "User" }
            },
            prNumber: pr.number
          },
          author: this.parseAuthor(pr.user),
          metadata: {
            dateSent: new Date(pr.created_at),
            edited: pr.created_at !== pr.updated_at
          },
          attachments: [],
          links: []
        });
        return {
          id: threadId,
          rootMessage,
          lastReplyAt: new Date(pr.updated_at)
        };
      }
    );
    const currentPage = options.cursor ? Number.parseInt(options.cursor, 10) : 1;
    const nextCursor = pulls.length === limit ? String(currentPage + 1) : void 0;
    return { threads, nextCursor };
  }
  /**
   * Fetch GitHub repository info as channel metadata.
   */
  async fetchChannelInfo(channelId) {
    const withoutPrefix = channelId.slice(7);
    const slashIndex = withoutPrefix.indexOf("/");
    if (slashIndex === -1) {
      throw new ValidationError(
        "github",
        `Invalid GitHub channel ID: ${channelId}`
      );
    }
    const owner = withoutPrefix.slice(0, slashIndex);
    const repo = withoutPrefix.slice(slashIndex + 1);
    const octokit = await this.getOctokitForThread(owner, repo);
    this.logger.debug("GitHub API: repos.get", { owner, repo });
    const { data: repoData } = await octokit.repos.get({ owner, repo });
    return {
      id: channelId,
      name: repoData.full_name,
      isDM: false,
      metadata: {
        owner,
        repo,
        description: repoData.description,
        visibility: repoData.visibility,
        defaultBranch: repoData.default_branch,
        openIssuesCount: repoData.open_issues_count
      }
    };
  }
  /**
   * Parse a raw message into normalized format.
   */
  parseMessage(raw) {
    if (raw.type === "issue_comment") {
      const threadId2 = this.encodeThreadId({
        owner: raw.repository.owner.login,
        repo: raw.repository.name,
        prNumber: raw.prNumber
      });
      return this.parseIssueComment(
        raw.comment,
        { owner: raw.repository.owner, name: raw.repository.name },
        raw.prNumber,
        threadId2
      );
    }
    const rootCommentId = raw.comment.in_reply_to_id ?? raw.comment.id;
    const threadId = this.encodeThreadId({
      owner: raw.repository.owner.login,
      repo: raw.repository.name,
      prNumber: raw.prNumber,
      reviewCommentId: rootCommentId
    });
    return this.parseReviewComment(
      raw.comment,
      { owner: raw.repository.owner, name: raw.repository.name },
      raw.prNumber,
      threadId
    );
  }
  /**
   * Render formatted content to GitHub markdown.
   */
  renderFormatted(content) {
    return this.formatConverter.fromAst(content);
  }
};
function createGitHubAdapter(config) {
  return new GitHubAdapter(config);
}
export {
  GitHubAdapter,
  createGitHubAdapter
};
//# sourceMappingURL=index.js.map