// src/adapter.ts
import { ValidationError } from "@chat-adapter/shared";
import { ConsoleLogger, Message, stringifyMarkdown as stringifyMarkdown2 } from "chat";

// src/api.ts
import { AuthenticationError, NetworkError, ResourceNotFoundError } from "@chat-adapter/shared";
var TIMEOUT_MS = 3e4;
var ADAPTER_NAME = "asana";
function createAsanaApiClient(accessToken, baseUrl) {
  function headers() {
    return {
      "Authorization": `Bearer ${accessToken}`,
      "Accept": "application/json",
      "Content-Type": "application/json"
    };
  }
  async function request(method, path, body) {
    const url = `${baseUrl}${path}`;
    let response;
    try {
      const ctrl = new AbortController();
      const timeout = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
      response = await fetch(url, {
        method,
        headers: headers(),
        body: body ? JSON.stringify(body) : void 0,
        signal: ctrl.signal
      });
      clearTimeout(timeout);
    } catch (cause) {
      throw new NetworkError(ADAPTER_NAME, `Request failed: ${method} ${path}`, cause instanceof Error ? cause : void 0);
    }
    if (response.status === 401 || response.status === 403) {
      throw new AuthenticationError(ADAPTER_NAME, `${response.status} on ${method} ${path}`);
    }
    if (response.status === 404) {
      throw new ResourceNotFoundError(ADAPTER_NAME, "resource", path);
    }
    if (!response.ok) {
      const text = await response.text().catch(() => "");
      throw new NetworkError(ADAPTER_NAME, `${response.status} on ${method} ${path}: ${text}`);
    }
    if (response.status === 204 || method === "DELETE") {
      return void 0;
    }
    const json = await response.json();
    return json;
  }
  return {
    async fetchTask(taskGid) {
      const result = await request("GET", `/tasks/${taskGid}?opt_fields=gid,name,notes,assignee,assignee.name,created_at,modified_at`);
      return result.data;
    },
    async fetchStories(taskGid, options) {
      const limit = options?.limit ?? 50;
      let path = `/tasks/${taskGid}/stories?opt_fields=gid,text,html_text,type,created_at,created_by,created_by.name,is_edited,likes,likes.user,likes.user.name,num_likes,target&limit=${limit}`;
      if (options?.offset) {
        path += `&offset=${encodeURIComponent(options.offset)}`;
      }
      const result = await request("GET", path);
      return { data: result.data, next_page: result.next_page };
    },
    async postStory(taskGid, text, options) {
      const result = await request("POST", `/tasks/${taskGid}/stories`, {
        data: { text, is_pinned: options?.isPinned ?? false }
      });
      return result.data;
    },
    async updateStory(storyGid, text) {
      const result = await request("PUT", `/stories/${storyGid}`, {
        data: { text }
      });
      return result.data;
    },
    async deleteStory(storyGid) {
      await request("DELETE", `/stories/${storyGid}`);
    },
    async likeStory(storyGid) {
      await request("POST", `/stories/${storyGid}/addHeart`, { data: {} });
    },
    async unlikeStory(storyGid) {
      await request("POST", `/stories/${storyGid}/removeHeart`, { data: {} });
    }
  };
}

// src/format-converter.ts
import { extractCard } from "@chat-adapter/shared";
import { parseMarkdown, stringifyMarkdown } from "chat";
function toAst(text) {
  return parseMarkdown(text);
}
function fromAst(ast) {
  return stringifyMarkdown(ast);
}
function renderPostable(message) {
  if (typeof message === "string") {
    return message;
  }
  const card = extractCard(message);
  if (card) {
    const lines = [];
    if (card.title)
      lines.push(card.title);
    return lines.join("\n");
  }
  if ("markdown" in message && typeof message.markdown === "string") {
    return message.markdown;
  }
  if ("ast" in message && message.ast) {
    return fromAst(message.ast);
  }
  if ("text" in message && typeof message.text === "string") {
    return message.text;
  }
  return "";
}

// src/webhook.ts
import { createHmac } from "crypto";
function isHandshake(request) {
  return request.headers.get("x-hook-secret");
}
function createHandshakeResponse(secret) {
  return new Response("", {
    status: 200,
    headers: { "X-Hook-Secret": secret }
  });
}
function verifySignature(body, signature, secret) {
  const hmac = createHmac("sha256", secret);
  hmac.update(body);
  const expected = hmac.digest("hex");
  return expected === signature;
}
function extractStoryEvents(payload) {
  if (!Array.isArray(payload.events))
    return [];
  return payload.events.filter(
    (e) => e.resource.resource_type === "story" && e.action === "added" && e.parent?.resource_type === "task"
  );
}

// src/adapter.ts
var AsanaAdapter = class {
  name = "asana";
  userName;
  chat = null;
  logger;
  api;
  webhookSecret = null;
  botUserId;
  constructor(config) {
    this.userName = config.userName ?? "asana-bot";
    this.logger = config.logger ?? new ConsoleLogger();
    this.api = createAsanaApiClient(
      config.accessToken,
      config.baseUrl ?? "https://app.asana.com/api/1.0"
    );
  }
  async initialize(chat) {
    this.chat = chat;
    this.logger = chat.getLogger("asana");
  }
  encodeThreadId(data) {
    return `asana:${data.taskGid}`;
  }
  decodeThreadId(threadId) {
    const parts = threadId.split(":");
    if (parts.length !== 2 || parts[0] !== "asana" || !parts[1]) {
      throw new ValidationError("asana", `Invalid Asana thread ID: ${threadId}`);
    }
    return { taskGid: parts[1] };
  }
  channelIdFromThreadId(threadId) {
    return threadId;
  }
  async handleWebhook(request, options) {
    const hookSecret = isHandshake(request);
    if (hookSecret) {
      this.webhookSecret = hookSecret;
      this.logger.info("webhook handshake completed");
      return createHandshakeResponse(hookSecret);
    }
    const signature = request.headers.get("x-hook-signature");
    if (!signature) {
      return new Response("Missing signature", { status: 401 });
    }
    const body = await request.text();
    if (this.webhookSecret && !verifySignature(body, signature, this.webhookSecret)) {
      return new Response("Invalid signature", { status: 401 });
    }
    let payload;
    try {
      payload = JSON.parse(body);
    } catch {
      return new Response("Invalid JSON", { status: 400 });
    }
    const storyEvents = extractStoryEvents(payload);
    for (const event of storyEvents) {
      if (!event.parent?.gid || !this.chat)
        continue;
      const taskGid = event.parent.gid;
      const storyGid = event.resource.gid;
      const threadId = this.encodeThreadId({ taskGid });
      const factory = async () => {
        const stories = await this.api.fetchStories(taskGid, { limit: 1 });
        const story = stories.data.find((s) => s.gid === storyGid);
        if (!story) {
          throw new Error(`Story ${storyGid} not found on task ${taskGid}`);
        }
        return this.parseMessage(story);
      };
      this.chat.processMessage(this, threadId, factory, options);
    }
    return new Response("OK", { status: 200 });
  }
  parseMessage(raw) {
    const threadId = raw.target?.gid ? this.encodeThreadId({ taskGid: raw.target.gid }) : "asana:unknown";
    const isBotMessage = raw.created_by?.gid === this.botUserId;
    return new Message({
      id: raw.gid,
      threadId,
      text: raw.text ?? "",
      formatted: toAst(raw.text ?? ""),
      raw,
      author: {
        userId: raw.created_by?.gid ?? "unknown",
        userName: raw.created_by?.name ?? "unknown",
        fullName: raw.created_by?.name ?? "",
        isBot: isBotMessage,
        isMe: isBotMessage
      },
      metadata: {
        dateSent: new Date(raw.created_at),
        edited: raw.is_edited ?? false
      },
      attachments: []
    });
  }
  async postMessage(threadId, message) {
    const { taskGid } = this.decodeThreadId(threadId);
    const text = renderPostable(message);
    const story = await this.api.postStory(taskGid, text);
    return { id: story.gid, threadId, raw: story };
  }
  async editMessage(threadId, messageId, message) {
    const text = renderPostable(message);
    const story = await this.api.updateStory(messageId, text);
    return { id: story.gid, threadId, raw: story };
  }
  async deleteMessage(_threadId, messageId) {
    await this.api.deleteStory(messageId);
  }
  async fetchMessages(threadId, options) {
    const { taskGid } = this.decodeThreadId(threadId);
    const limit = options?.limit ?? 50;
    const offset = options?.cursor;
    const result = await this.api.fetchStories(taskGid, { limit, offset });
    const comments = result.data.filter((s) => s.type === "comment");
    const messages = comments.map((s) => this.parseMessage(s));
    return {
      messages,
      nextCursor: result.next_page?.offset
    };
  }
  async fetchThread(threadId) {
    const { taskGid } = this.decodeThreadId(threadId);
    const task = await this.api.fetchTask(taskGid);
    return {
      id: threadId,
      channelId: threadId,
      channelName: task.name,
      metadata: {
        taskGid: task.gid,
        taskName: task.name
      }
    };
  }
  async addReaction(threadId, messageId, _emoji) {
    await this.api.likeStory(messageId);
  }
  async removeReaction(threadId, messageId, _emoji) {
    await this.api.unlikeStory(messageId);
  }
  async startTyping(_threadId) {
  }
  renderFormatted(content) {
    return stringifyMarkdown2(content);
  }
};

// src/factory.ts
import process from "process";
function createAsanaAdapter(config) {
  const accessToken = config?.accessToken ?? process.env.ASANA_ACCESS_TOKEN;
  if (!accessToken) {
    throw new Error("Asana adapter requires an access token. Set ASANA_ACCESS_TOKEN or pass accessToken in config.");
  }
  return new AsanaAdapter({
    accessToken,
    baseUrl: config?.baseUrl,
    userName: config?.userName,
    logger: config?.logger
  });
}
export {
  AsanaAdapter,
  createAsanaAdapter,
  createAsanaApiClient
};
//# sourceMappingURL=index.js.map