import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
import type { ClawdbotConfig } from "../config/types.js";
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import {
  readProviderAllowFromStore,
  upsertProviderPairingRequest,
} from "../pairing/pairing-store.js";
import { resolveAgentRoute } from "../routing/resolve-route.js";
import type { RuntimeEnv } from "../runtime.js";
import {
  buildMSTeamsAttachmentPlaceholder,
  buildMSTeamsGraphMessageUrls,
  buildMSTeamsMediaPayload,
  downloadMSTeamsGraphMedia,
  downloadMSTeamsImageAttachments,
  type MSTeamsAttachmentLike,
  summarizeMSTeamsHtmlAttachments,
} from "./attachments.js";
import type {
  MSTeamsConversationStore,
  StoredConversationReference,
} from "./conversation-store.js";
import { formatUnknownError } from "./errors.js";
import {
  extractMSTeamsConversationMessageId,
  normalizeMSTeamsConversationId,
  parseMSTeamsActivityTimestamp,
  stripMSTeamsMentionTags,
  wasMSTeamsBotMentioned,
} from "./inbound.js";
import type { MSTeamsAdapter } from "./messenger.js";
import type { MSTeamsMonitorLogger } from "./monitor-types.js";
import {
  resolveMSTeamsReplyPolicy,
  resolveMSTeamsRouteConfig,
} from "./policy.js";
import { extractMSTeamsPollVote, type MSTeamsPollStore } from "./polls.js";
import { createMSTeamsReplyDispatcher } from "./reply-dispatcher.js";
import type { MSTeamsTurnContext } from "./sdk-types.js";

export type MSTeamsAccessTokenProvider = {
  getAccessToken: (scope: string) => Promise<string>;
};

export type MSTeamsActivityHandler = {
  onMessage: (
    handler: (context: unknown, next: () => Promise<void>) => Promise<void>,
  ) => MSTeamsActivityHandler;
  onMembersAdded: (
    handler: (context: unknown, next: () => Promise<void>) => Promise<void>,
  ) => MSTeamsActivityHandler;
};

export type MSTeamsMessageHandlerDeps = {
  cfg: ClawdbotConfig;
  runtime: RuntimeEnv;
  appId: string;
  adapter: MSTeamsAdapter;
  tokenProvider: MSTeamsAccessTokenProvider;
  textLimit: number;
  mediaMaxBytes: number;
  conversationStore: MSTeamsConversationStore;
  pollStore: MSTeamsPollStore;
  log: MSTeamsMonitorLogger;
};

export function registerMSTeamsHandlers<T extends MSTeamsActivityHandler>(
  handler: T,
  deps: MSTeamsMessageHandlerDeps,
): T {
  const handleTeamsMessage = createMSTeamsMessageHandler(deps);
  handler.onMessage(async (context, next) => {
    try {
      await handleTeamsMessage(context as MSTeamsTurnContext);
    } catch (err) {
      deps.runtime.error?.(danger(`msteams handler failed: ${String(err)}`));
    }
    await next();
  });

  handler.onMembersAdded(async (context, next) => {
    const membersAdded =
      (context as MSTeamsTurnContext).activity?.membersAdded ?? [];
    for (const member of membersAdded) {
      if (
        member.id !== (context as MSTeamsTurnContext).activity?.recipient?.id
      ) {
        deps.log.debug("member added", { member: member.id });
        // Don't send welcome message - let the user initiate conversation.
      }
    }
    await next();
  });

  return handler;
}

function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
  const {
    cfg,
    runtime,
    appId,
    adapter,
    tokenProvider,
    textLimit,
    mediaMaxBytes,
    conversationStore,
    pollStore,
    log,
  } = deps;
  const msteamsCfg = cfg.msteams;

  return async function handleTeamsMessage(context: MSTeamsTurnContext) {
    const activity = context.activity;
    const rawText = activity.text?.trim() ?? "";
    const text = stripMSTeamsMentionTags(rawText);
    const attachments = Array.isArray(activity.attachments)
      ? (activity.attachments as unknown as MSTeamsAttachmentLike[])
      : [];
    const attachmentPlaceholder =
      buildMSTeamsAttachmentPlaceholder(attachments);
    const rawBody = text || attachmentPlaceholder;
    const from = activity.from;
    const conversation = activity.conversation;

    const attachmentTypes = attachments
      .map((att) =>
        typeof att.contentType === "string" ? att.contentType : undefined,
      )
      .filter(Boolean)
      .slice(0, 3);
    const htmlSummary = summarizeMSTeamsHtmlAttachments(attachments);

    log.info("received message", {
      rawText: rawText.slice(0, 50),
      text: text.slice(0, 50),
      attachments: attachments.length,
      attachmentTypes,
      from: from?.id,
      conversation: conversation?.id,
    });
    if (htmlSummary) {
      log.debug("html attachment summary", htmlSummary);
    }

    if (!from?.id) {
      log.debug("skipping message without from.id");
      return;
    }

    // Teams conversation.id may include ";messageid=..." suffix - strip it for session key
    const rawConversationId = conversation?.id ?? "";
    const conversationId = normalizeMSTeamsConversationId(rawConversationId);
    const conversationMessageId =
      extractMSTeamsConversationMessageId(rawConversationId);
    const conversationType = conversation?.conversationType ?? "personal";
    const isGroupChat =
      conversationType === "groupChat" || conversation?.isGroup === true;
    const isChannel = conversationType === "channel";
    const isDirectMessage = !isGroupChat && !isChannel;

    const senderName = from.name ?? from.id;
    const senderId = from.aadObjectId ?? from.id;

    // Check DM policy for direct messages
    if (isDirectMessage && msteamsCfg) {
      const dmPolicy = msteamsCfg.dmPolicy ?? "pairing";
      const allowFrom = msteamsCfg.allowFrom ?? [];

      if (dmPolicy === "disabled") {
        log.debug("dropping dm (dms disabled)");
        return;
      }

      if (dmPolicy !== "open") {
        // Check allowlist - look up from config and pairing store
        const storedAllowFrom = await readProviderAllowFromStore("msteams");
        const effectiveAllowFrom = [
          ...allowFrom.map((v) => String(v).toLowerCase()),
          ...storedAllowFrom,
        ];

        const senderLower = senderId.toLowerCase();
        const senderNameLower = senderName.toLowerCase();
        const allowed =
          effectiveAllowFrom.includes("*") ||
          effectiveAllowFrom.includes(senderLower) ||
          effectiveAllowFrom.includes(senderNameLower);

        if (!allowed) {
          if (dmPolicy === "pairing") {
            const request = await upsertProviderPairingRequest({
              provider: "msteams",
              id: senderId,
              meta: { name: senderName },
            });
            if (request) {
              log.info("msteams pairing request created", {
                sender: senderId,
                label: senderName,
              });
            }
          }
          log.debug("dropping dm (not allowlisted)", {
            sender: senderId,
            label: senderName,
          });
          return;
        }
      }
    }

    // Build conversation reference for proactive replies
    const agent = activity.recipient;
    const teamId = activity.channelData?.team?.id;
    const conversationRef: StoredConversationReference = {
      activityId: activity.id,
      user: { id: from.id, name: from.name, aadObjectId: from.aadObjectId },
      agent,
      bot: agent ? { id: agent.id, name: agent.name } : undefined,
      conversation: {
        id: conversationId,
        conversationType,
        tenantId: conversation?.tenantId,
      },
      teamId,
      channelId: activity.channelId,
      serviceUrl: activity.serviceUrl,
      locale: activity.locale,
    };
    conversationStore.upsert(conversationId, conversationRef).catch((err) => {
      log.debug("failed to save conversation reference", {
        error: formatUnknownError(err),
      });
    });

    const pollVote = extractMSTeamsPollVote(activity);
    if (pollVote) {
      try {
        const poll = await pollStore.recordVote({
          pollId: pollVote.pollId,
          voterId: senderId,
          selections: pollVote.selections,
        });
        if (!poll) {
          log.debug("poll vote ignored (poll not found)", {
            pollId: pollVote.pollId,
          });
        } else {
          log.info("recorded poll vote", {
            pollId: pollVote.pollId,
            voter: senderId,
            selections: pollVote.selections,
          });
        }
      } catch (err) {
        log.error("failed to record poll vote", {
          pollId: pollVote.pollId,
          error: formatUnknownError(err),
        });
      }
      return;
    }

    if (!rawBody) {
      log.debug("skipping empty message after stripping mentions");
      return;
    }

    // Build Teams-specific identifiers
    const teamsFrom = isDirectMessage
      ? `msteams:${senderId}`
      : isChannel
        ? `msteams:channel:${conversationId}`
        : `msteams:group:${conversationId}`;
    const teamsTo = isDirectMessage
      ? `user:${senderId}`
      : `conversation:${conversationId}`;

    // Resolve routing
    const route = resolveAgentRoute({
      cfg,
      provider: "msteams",
      peer: {
        kind: isDirectMessage ? "dm" : isChannel ? "channel" : "group",
        id: isDirectMessage ? senderId : conversationId,
      },
    });

    const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
    const inboundLabel = isDirectMessage
      ? `Teams DM from ${senderName}`
      : `Teams message in ${conversationType} from ${senderName}`;

    enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
      sessionKey: route.sessionKey,
      contextKey: `msteams:message:${conversationId}:${activity.id ?? "unknown"}`,
    });

    // Resolve team/channel config for channels and group chats
    const channelId = conversationId;
    const { teamConfig, channelConfig } = resolveMSTeamsRouteConfig({
      cfg: msteamsCfg,
      teamId,
      conversationId: channelId,
    });
    const { requireMention, replyStyle } = resolveMSTeamsReplyPolicy({
      isDirectMessage,
      globalConfig: msteamsCfg,
      teamConfig,
      channelConfig,
    });

    // Check requireMention for channels and group chats
    if (!isDirectMessage) {
      const mentioned = wasMSTeamsBotMentioned(activity);

      if (requireMention && !mentioned) {
        log.debug("skipping message (mention required)", {
          teamId,
          channelId,
          requireMention,
          mentioned,
        });
        return;
      }
    }

    // Format the message body with envelope
    const timestamp = parseMSTeamsActivityTimestamp(activity.timestamp);
    let mediaList = await downloadMSTeamsImageAttachments({
      attachments,
      maxBytes: mediaMaxBytes,
      tokenProvider: {
        getAccessToken: (scope) => tokenProvider.getAccessToken(scope),
      },
      allowHosts: msteamsCfg?.mediaAllowHosts,
    });
    if (mediaList.length === 0) {
      const onlyHtmlAttachments =
        attachments.length > 0 &&
        attachments.every((att) =>
          String(att.contentType ?? "").startsWith("text/html"),
        );
      if (onlyHtmlAttachments) {
        const messageUrls = buildMSTeamsGraphMessageUrls({
          conversationType,
          conversationId,
          messageId: activity.id ?? undefined,
          replyToId: activity.replyToId ?? undefined,
          conversationMessageId,
          channelData: activity.channelData,
        });
        if (messageUrls.length === 0) {
          log.debug("graph message url unavailable", {
            conversationType,
            hasChannelData: Boolean(activity.channelData),
            messageId: activity.id ?? undefined,
            replyToId: activity.replyToId ?? undefined,
          });
        } else {
          const attempts: Array<{
            url: string;
            hostedStatus?: number;
            attachmentStatus?: number;
            hostedCount?: number;
            attachmentCount?: number;
            tokenError?: boolean;
          }> = [];
          for (const messageUrl of messageUrls) {
            const graphMedia = await downloadMSTeamsGraphMedia({
              messageUrl,
              tokenProvider: {
                getAccessToken: (scope) => tokenProvider.getAccessToken(scope),
              },
              maxBytes: mediaMaxBytes,
              allowHosts: msteamsCfg?.mediaAllowHosts,
            });
            attempts.push({
              url: messageUrl,
              hostedStatus: graphMedia.hostedStatus,
              attachmentStatus: graphMedia.attachmentStatus,
              hostedCount: graphMedia.hostedCount,
              attachmentCount: graphMedia.attachmentCount,
              tokenError: graphMedia.tokenError,
            });
            if (graphMedia.media.length > 0) {
              mediaList = graphMedia.media;
              break;
            }
            if (graphMedia.tokenError) break;
          }
          if (mediaList.length === 0) {
            log.debug("graph media fetch empty", { attempts });
          }
        }
      }
    }
    if (mediaList.length > 0) {
      log.debug("downloaded image attachments", { count: mediaList.length });
    } else if (htmlSummary?.imgTags) {
      log.debug("inline images detected but none downloaded", {
        imgTags: htmlSummary.imgTags,
        srcHosts: htmlSummary.srcHosts,
        dataImages: htmlSummary.dataImages,
        cidImages: htmlSummary.cidImages,
      });
    }
    const mediaPayload = buildMSTeamsMediaPayload(mediaList);
    const body = formatAgentEnvelope({
      provider: "Teams",
      from: senderName,
      timestamp,
      body: rawBody,
    });

    // Build context payload for agent
    const ctxPayload = {
      Body: body,
      From: teamsFrom,
      To: teamsTo,
      SessionKey: route.sessionKey,
      AccountId: route.accountId,
      ChatType: isDirectMessage ? "direct" : isChannel ? "room" : "group",
      GroupSubject: !isDirectMessage ? conversationType : undefined,
      SenderName: senderName,
      SenderId: senderId,
      Provider: "msteams" as const,
      Surface: "msteams" as const,
      MessageSid: activity.id,
      Timestamp: timestamp?.getTime() ?? Date.now(),
      WasMentioned: isDirectMessage || wasMSTeamsBotMentioned(activity),
      CommandAuthorized: true,
      OriginatingChannel: "msteams" as const,
      OriginatingTo: teamsTo,
      ...mediaPayload,
    };

    if (shouldLogVerbose()) {
      logVerbose(
        `msteams inbound: from=${ctxPayload.From} preview="${preview}"`,
      );
    }

    // Create reply dispatcher
    const { dispatcher, replyOptions, markDispatchIdle } =
      createMSTeamsReplyDispatcher({
        cfg,
        agentId: route.agentId,
        runtime,
        log,
        adapter,
        appId,
        conversationRef,
        context,
        replyStyle,
        textLimit,
      });

    // Dispatch to agent
    log.info("dispatching to agent", { sessionKey: route.sessionKey });
    try {
      const { queuedFinal, counts } = await dispatchReplyFromConfig({
        ctx: ctxPayload,
        cfg,
        dispatcher,
        replyOptions,
      });

      markDispatchIdle();
      log.info("dispatch complete", { queuedFinal, counts });

      if (!queuedFinal) return;
      if (shouldLogVerbose()) {
        const finalCount = counts.final;
        logVerbose(
          `msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`,
        );
      }
    } catch (err) {
      log.error("dispatch failed", { error: String(err) });
      runtime.error?.(danger(`msteams dispatch failed: ${String(err)}`));
      // Try to send error message back to Teams.
      try {
        await context.sendActivity(
          `⚠️ Agent failed: ${err instanceof Error ? err.message : String(err)}`,
        );
      } catch {
        // Best effort.
      }
    }
  };
}
