import type { DestinationAdapter } from "./dispatchAdapters";
import {
  postChatMessage as defaultPostChatMessage,
  type SlackChatPostMessageResult,
} from "./slackClient";

export interface SlackWorkspaceForAdapter {
  status: string;
  teamId: string;
}

export interface SecretManagerClient {
  getSecret(vault: string, key: string): Promise<string | undefined>;
}

export interface CreateSlackDestinationAdapterDeps {
  loadConnection: () => Promise<SlackWorkspaceForAdapter | null>;
  secretManager: SecretManagerClient;
  markConnectionRevoked?: (reason: string) => Promise<void>;
  postChatMessage?: (
    botToken: string,
    channel: string,
    text: string,
  ) => Promise<SlackChatPostMessageResult>;
}

// Slack `error` values that mean the stored bot token is no longer usable.
const TOKEN_REVOKED_ERRORS = new Set(["invalid_auth", "token_revoked", "account_inactive"]);

// Slack `error` values that mean the target channel is unreachable: the bot is
// not a member, or the externalChannelRef is invalid. Split out from the generic
// class because the operator action is specific and common (invite the bot to
// the channel, or fix the binding).
const CHANNEL_UNREACHABLE_ERRORS = new Set(["not_in_channel", "channel_not_found"]);

function redact(result: { ok: boolean; ts?: string; channel?: string; error?: string }): string {
  // Persist only delivery-trace essentials — never the posted text/blocks or
  // Slack internal metadata.
  return JSON.stringify({
    ok: result.ok,
    ts: result.ts ?? null,
    channel: result.channel ?? null,
    error: result.ok ? null : (result.error ?? null),
  });
}

export function createSlackDestinationAdapter(
  deps: CreateSlackDestinationAdapterDeps,
): DestinationAdapter {
  const postFn = deps.postChatMessage ?? defaultPostChatMessage;

  return {
    async send({ channelId, externalChannelRef, subject, body }) {
      if (channelId !== "SLACK") {
        return {
          ok: false,
          errorClass: "ChannelMismatch",
          errorDetail: channelId,
          providerResponse: redact({ ok: false, error: "channel_mismatch" }),
        };
      }

      const conn = await deps.loadConnection();
      if (!conn || conn.status !== "ACTIVE") {
        return {
          ok: false,
          errorClass: "WorkspaceNotConnected",
          errorDetail: "workspace_not_connected",
          providerResponse: redact({ ok: false, error: "workspace_not_connected" }),
        };
      }

      const botToken = await deps.secretManager.getSecret("slack-tokens", conn.teamId);
      if (!botToken) {
        return {
          ok: false,
          errorClass: "WorkspaceNotConnected",
          errorDetail: "workspace_not_connected",
          providerResponse: redact({ ok: false, error: "workspace_not_connected" }),
        };
      }

      const text = subject || body;
      let result: SlackChatPostMessageResult;
      try {
        result = await postFn(botToken, externalChannelRef, text);
      } catch (e) {
        if (e instanceof Error && e.name === "TimeoutError") {
          return {
            ok: false,
            errorClass: "SlackTimeout",
            errorDetail: "timeout",
            providerResponse: redact({ ok: false, error: "timeout" }),
          };
        }
        const detail = e instanceof Error ? e.message : "post_failed";
        return {
          ok: false,
          errorClass: "SlackPostThrew",
          errorDetail: detail,
          providerResponse: redact({ ok: false, error: detail }),
        };
      }

      const providerResponse = redact(result);
      if (result.ok) {
        return {
          ok: true,
          ...(result.ts ? { externalMessageId: result.ts } : {}),
          providerResponse,
        };
      }

      const slackError = result.error ?? "unknown";

      // Token revoked / auth dead: the connection is unusable until a
      // re-install. Optionally flip the stored row to REVOKED (best-effort)
      // so later sends fail fast as workspace_not_connected.
      if (TOKEN_REVOKED_ERRORS.has(slackError)) {
        if (deps.markConnectionRevoked) {
          try {
            await deps.markConnectionRevoked(slackError);
          } catch {
            // Best-effort: the delivery outcome below already records the
            // revocation cause.
          }
        }
        return {
          ok: false,
          errorClass: "SlackTokenRevoked",
          errorDetail: slackError,
          providerResponse,
        };
      }

      // Rate limited (HTTP 429): keep the Retry-After hint in the failure
      // detail so it lands on DestinationDeliveryLog.failureReason.
      if (slackError === "ratelimited") {
        return {
          ok: false,
          errorClass: "SlackRateLimited",
          errorDetail:
            result.retryAfterSeconds !== undefined
              ? `ratelimited retry_after=${result.retryAfterSeconds}`
              : "ratelimited",
          providerResponse,
        };
      }

      // Bot not in channel / channel gone: a specific, common operator fix.
      if (CHANNEL_UNREACHABLE_ERRORS.has(slackError)) {
        return {
          ok: false,
          errorClass: "SlackChannelUnreachable",
          errorDetail: slackError,
          providerResponse,
        };
      }

      // Everything else stays a plain Slack API error.
      return {
        ok: false,
        errorClass: "SlackApiError",
        errorDetail: slackError,
        providerResponse,
      };
    },
  };
}
