import { z } from "zod";
import type { A2AAttachment } from "@/agents/a2a-executor";

/**
 * ChatOps provider types enum
 * Used for PG ENUM in database schema
 */
export const ChatOpsProviderTypeSchema = z.enum(["ms-teams", "slack"]);
export type ChatOpsProviderType = z.infer<typeof ChatOpsProviderTypeSchema>;

export const ChatOpsConnectionModeSchema = z.enum(["webhook", "socket"]);
export type ChatOpsConnectionMode = z.infer<typeof ChatOpsConnectionModeSchema>;

export const ChatOpsStatusSchema = z.enum(["configured", "unassigned"]);
export type ChatOpsStatus = z.infer<typeof ChatOpsStatusSchema>;

/** Credentials shape returned by the chatops status endpoint */
export const ChatOpsProviderCredentialsSchema = z
  .object({
    botToken: z.string().optional(),
    appId: z.string().optional(),
    appSecret: z.string().optional(),
    tenantId: z.string().optional(),
    signingSecret: z.string().optional(),
    appLevelToken: z.string().optional(),
    connectionMode: ChatOpsConnectionModeSchema.optional(),
  })
  .optional();

/** DM info returned by the chatops status endpoint */
export const ChatOpsDmInfoSchema = z
  .object({
    botUserId: z.string().optional(),
    teamId: z.string().optional(),
    appId: z.string().optional(),
  })
  .optional();

/** Single provider entry in the chatops status response */
export const ChatOpsProviderInfoSchema = z.object({
  id: ChatOpsProviderTypeSchema,
  displayName: z.string(),
  configured: z.boolean(),
  credentials: ChatOpsProviderCredentialsSchema,
  dmInfo: ChatOpsDmInfoSchema,
});

/** Full chatops status response schema */
export const ChatOpsStatusResponseSchema = z.object({
  providers: z.array(ChatOpsProviderInfoSchema),
});

/**
 * Why an attached file was not included in the message sent to the model.
 * - `too_large`: exceeded the per-file size limit (and, for images, could not
 *   be shrunk under the model's inline-image limit).
 * - `download_failed`: the provider download failed (auth, network, or a
 *   non-file response such as an HTML login page).
 * - `total_limit_reached`: the combined attachment budget was already spent.
 * - `too_many`: more files than the per-message cap were attached.
 */
export type SkippedAttachmentReason =
  | "too_large"
  | "download_failed"
  | "total_limit_reached"
  | "too_many";

/**
 * A file that was attached to a message but not delivered to the model. Carried
 * so the chatops layer can tell the model, in-context, that a file was dropped
 * and why — otherwise the model has no idea the file existed and denies it.
 */
export interface SkippedAttachment {
  /** Filename from the provider, when known. */
  name?: string;
  /** Size in bytes from provider metadata, when known. */
  sizeBytes?: number;
  reason: SkippedAttachmentReason;
}

/**
 * Per-input-file result of downloading thread-history files. The returned
 * array is positionally aligned with the input: `outcomes[i]` describes
 * `files[i]`, so callers can attribute every skip back to the history turn
 * the file came from.
 */
export type ThreadFileOutcome =
  | { status: "delivered"; attachment: A2AAttachment }
  | { status: "skipped"; skipped: SkippedAttachment };

/**
 * Represents an incoming chat message from a chatops provider
 */
export interface IncomingChatMessage {
  /** Unique message ID from the provider */
  messageId: string;
  /** The channel where the message was sent */
  channelId: string;
  /** The workspace/team ID (e.g., Teams team ID) */
  workspaceId: string | null;
  /** Thread/conversation ID for fetching history */
  threadId?: string;
  /** The sender's ID in the provider's system */
  senderId: string;
  /** Pre-resolved sender email (from Bot Framework TeamsInfo, avoids Graph API call) */
  senderEmail?: string;
  /** The sender's display name */
  senderName: string;
  /** The message text (with bot mentions cleaned) */
  text: string;
  /** Raw message text before cleaning */
  rawText: string;
  /** When the message was sent */
  timestamp: Date;
  /** Whether this is a reply to a thread */
  isThreadReply: boolean;
  /** Provider-specific metadata */
  metadata?: Record<string, unknown>;
  /** Attachments from the message (images, files, etc.) */
  attachments?: A2AAttachment[];
  /** Files that were attached but could not be delivered to the model. */
  skippedAttachments?: SkippedAttachment[];
}

/**
 * Options for sending a chat reply
 */
export interface ChatReplyOptions {
  /** The original message to reply to */
  originalMessage: IncomingChatMessage;
  /** The reply text */
  text: string;
  /** Optional: Reply in thread (if supported) */
  replyInThread?: boolean;
  /** Optional: Footer text to append (e.g. agent name) */
  footer?: string;
  /**
   * Optional: an even-more-subtle hint rendered on its own line below the
   * footer (e.g. the one-time "you can mute me" tip on a thread's first reply).
   */
  hint?: string;
  /** Provider-specific conversation reference for reply routing */
  conversationReference?: unknown;
}

export interface AddApprovalRequestFormOptions {
  channelId: string;
  threadId?: string;
  approvalId: string;
  taskId: string;
  /**
   * The tool the user is approving. For a `run_tool` dispatch this is the
   * underlying target tool, not the `run_tool` wrapper.
   */
  toolName: string;
  /**
   * The arguments the tool will be invoked with, rendered as a code block in
   * the approval prompt. Undefined/empty when there is nothing to show.
   */
  toolArgs?: Record<string, unknown>;
  originalMessage: IncomingChatMessage;
}

export interface UpdateApprovalRequestOptions {
  channelId: string;
  messageKey: string;
  toolName: string;
  approved: boolean;
}

/**
 * Approval decision payload shared by chatops providers and approval handlers.
 */
export interface ChatOpsApprovalDecision {
  taskId: string;
  approvalId: string;
  approved: boolean;
  toolName: string;
  messageTs: string;
  channelId: string;
  workspaceId: string | null;
  originalMessage: IncomingChatMessage;
  threadTs?: string;
  userId: string;
  userName: string;
  responseUrl: string;
  approverEmail?: string;
}

/**
 * File metadata from a thread history message (not yet downloaded).
 * Used to carry attachment info from provider-specific history APIs
 * so the manager can download them for LLM context.
 */
export interface ChatThreadMessageFile {
  /** Download URL for the file */
  url: string;
  /** MIME type of the file */
  mimetype: string;
  /** Optional filename */
  name?: string;
  /** Optional file size in bytes (from provider metadata) */
  size?: number;
}

/**
 * A message in a chat thread history
 */
export interface ChatThreadMessage {
  /** Unique message ID */
  messageId: string;
  /** The sender's ID */
  senderId: string;
  /** The sender's display name */
  senderName: string;
  /** The message text */
  text: string;
  /** When the message was sent */
  timestamp: Date;
  /** Whether this message was from the bot */
  isFromBot: boolean;
  /** File attachments from this message (metadata only, not downloaded) */
  files?: ChatThreadMessageFile[];
}

/**
 * Parameters for fetching thread history
 */
export interface ThreadHistoryParams {
  /** The channel ID */
  channelId: string;
  /** The workspace/team ID */
  workspaceId: string | null;
  /** The thread/conversation ID */
  threadId: string;
  /** Maximum number of messages to fetch */
  limit?: number;
  /** Exclude this message ID from results */
  excludeMessageId?: string;
}

/**
 * Result of processing a chatops message
 */
export interface ChatOpsProcessingResult {
  success: boolean;
  /** The agent response (if successful) */
  agentResponse?: string;
  /** Error message (if failed) */
  error?: string;
  /** The interaction ID for tracking */
  interactionId?: string;
}

/**
 * A channel discovered by a chatops provider.
 * Used to auto-populate channel bindings so admins can assign agents from the UI.
 */
export interface DiscoveredChannel {
  channelId: string;
  channelName: string | null;
  workspaceId: string;
  workspaceName: string | null;
}

/**
 * Interface for chatops providers (MS Teams, Slack, Discord, etc.)
 *
 * Implementations should:
 * 1. Handle webhook validation and JWT verification
 * 2. Parse incoming activities/events into IncomingChatMessage
 * 3. Send replies using provider-specific APIs
 * 4. Fetch thread history for conversation context
 * 5. Discover available channels for auto-populating bindings
 */
export interface ChatOpsProvider {
  /** Provider identifier */
  readonly providerId: ChatOpsProviderType;

  /** Display name for UI */
  readonly displayName: string;

  /**
   * Check if the provider is properly configured
   */
  isConfigured(): boolean;

  /**
   * Initialize the provider (setup adapters, clients, etc.)
   * Called once when the server starts if the provider is configured
   */
  initialize(): Promise<void>;

  /**
   * Clean up resources
   * Called on graceful shutdown
   */
  cleanup(): Promise<void>;

  /**
   * Validate a webhook request (signature/JWT verification)
   * @param payload - The raw webhook payload
   * @param headers - HTTP headers from the webhook request
   * @returns true if the request is valid
   */
  validateWebhookRequest(
    payload: unknown,
    headers: Record<string, string | string[] | undefined>,
  ): Promise<boolean>;

  /**
   * Handle webhook validation challenge (for initial setup)
   * @param payload - The challenge payload
   * @returns Response to send back, or null if not a validation request
   */
  handleValidationChallenge(payload: unknown): unknown | null;

  /**
   * Parse a webhook notification into an IncomingChatMessage
   * @param payload - The raw webhook payload
   * @param headers - HTTP headers from the webhook request
   * @returns Parsed message or null if not a processable message
   */
  parseWebhookNotification(
    payload: unknown,
    headers: Record<string, string | string[] | undefined>,
  ): Promise<IncomingChatMessage | null>;

  /**
   * Send a reply to a chat message
   * @param options - Reply options including original message and response text
   * @returns The message ID of the sent reply
   */
  sendReply(options: ChatReplyOptions): Promise<string>;

  /**
   * Send a message with Approve/Decline buttons for a single approval request
   */
  addApprovalRequestForm(options: AddApprovalRequestFormOptions): Promise<void>;

  /**
   * Update the status of an existing approval request to Approved/Declined
   */
  updateApprovalRequest(options: UpdateApprovalRequestOptions): Promise<void>;

  /**
   * Send an ephemeral message visible only to a specific user.
   * Used for welcome messages to auto-provisioned users.
   * Falls back to a regular reply if ephemeral messaging is not supported.
   */
  sendEphemeralMessage?(params: {
    channelId: string;
    userId: string;
    text: string;
    threadId?: string;
  }): Promise<void>;

  /**
   * Send a direct message (DM) to a user.
   * Used for welcome messages to auto-provisioned users.
   * @param params.userId - The user's ID in the provider's system
   * @param params.text - The message text (markdown)
   * @param params.actionUrl - Optional URL for an action button
   * @param params.actionLabel - Optional label for the action button
   */
  sendDirectMessage?(params: {
    userId: string;
    text: string;
    actionUrl?: string;
    actionLabel?: string;
    /** When provided, post to this channel instead of opening a new DM via conversations.open.
     *  Useful for replying inside an existing DM without routing to the History tab. */
    channelId?: string;
    /** When provided, thread the message as a reply to this timestamp.
     *  Required in DMs so the reply appears in Chat tab instead of History. */
    threadId?: string;
  }): Promise<void>;

  /**
   * Handle a provider-specific interactive payload.
   * Slack uses this to route approval clicks and agent selections.
   */
  handleInteractivePayload?(payload: unknown): Promise<void>;

  /**
   * Set a typing/loading status indicator (optional, provider-specific).
   * For Slack: shows "App is thinking..." in the assistant thread.
   * For Teams: sends a typing activity indicator in DMs/group chats,
   *   or a placeholder "Thinking..." message in channels (which sendReply
   *   later updates with the real response).
   * Non-fatal if unsupported or not configured.
   *
   * Implementations may mutate `metadata` to pass state to a subsequent
   * `sendReply` call (e.g., storing a placeholder message ID). The caller
   * passes `message.metadata` by reference, so mutations are visible when
   * `sendReply` reads `options.originalMessage.metadata`.
   */
  setTypingStatus?(
    channelId: string,
    threadTs: string,
    metadata?: Record<string, unknown>,
  ): Promise<void>;

  /**
   * Clear a transient "thinking" indicator without posting a message.
   * Needed when the agent deliberately stays silent: providers like Slack
   * only auto-clear the status once a message is posted to the thread.
   */
  clearTypingStatus?(channelId: string, threadTs: string): Promise<void>;

  /**
   * Get thread/conversation history for context
   * @param params - Parameters including channel, thread ID, and limit
   * @returns Array of previous messages, oldest first
   */
  getThreadHistory(params: ThreadHistoryParams): Promise<ChatThreadMessage[]>;

  /**
   * Get a permalink to a specific message in the provider's web UI.
   * Used to surface a clickable thread URL in the LLM context so tools
   * can reference the originating conversation.
   * @param params.channelId - The channel ID containing the message
   * @param params.messageId - The message ID (Slack ts) to link to
   * @returns Permalink URL, or null if unavailable
   */
  getMessagePermalink?(params: {
    channelId: string;
    messageId: string;
  }): Promise<string | null>;

  /**
   * Get user's email address from their provider-specific ID
   * Used for security validation to verify the user exists in Archestra
   * @param userId - The user's ID in the provider's system (e.g., AAD Object ID for MS Teams)
   * @returns The user's email address, or null if not available
   */
  getUserEmail(userId: string): Promise<string | null>;

  /**
   * Get user's display name from their provider-specific ID.
   * Used for auto-provisioning to set a meaningful user name.
   * @param userId - The user's ID in the provider's system
   * @returns The user's display name, or null if not available
   */
  getUserName?(userId: string): Promise<string | null>;

  /**
   * Get a channel's display name from its provider-specific ID.
   * Used when creating early bindings for channels not yet in the discovery cache.
   * @param channelId - The channel ID in the provider's system
   * @returns The channel name, or null if not available
   */
  getChannelName(channelId: string): Promise<string | null>;

  /**
   * Parse an interactive payload (e.g. button click) into a structured selection.
   * Each provider implements its own payload parsing (Block Kit for Slack, Adaptive Card for MS Teams).
   * @param payload - The raw interactive payload from the provider
   * @returns Parsed selection or null if not a valid agent selection
   */
  parseInteractivePayload(payload: unknown): {
    agentId: string;
    channelId: string;
    workspaceId: string | null;
    threadTs?: string;
    userId: string;
    userName: string;
    responseUrl: string;
  } | null;

  /**
   * Send an agent selection card/message to a channel.
   * Each provider renders the card in its native format (Adaptive Card for MS Teams, Block Kit for Slack).
   * @param params.message - The incoming message that triggered the selection
   * @param params.agents - Available agents to choose from
   * @param params.isWelcome - Whether this is a first-time welcome (true) or a change-agent request (false)
   * @param params.providerContext - Provider-specific context (e.g., TurnContext for MS Teams)
   */
  sendAgentSelectionCard(params: {
    message: IncomingChatMessage;
    agents: { id: string; name: string }[];
    isWelcome: boolean;
    providerContext?: unknown;
  }): Promise<void>;

  /**
   * Get the workspace/team ID for this provider, if known without an incoming message.
   * Used for eager channel discovery on startup.
   * Returns null if the workspace ID can only be determined from incoming messages
   * (e.g., MS Teams requires a TurnContext to know which team).
   */
  getWorkspaceId(): string | null;

  /**
   * Get the workspace/team display name for this provider, if known.
   * Used to populate workspaceName on channel bindings (including DMs).
   */
  getWorkspaceName(): string | null;

  /**
   * Check whether the provider is missing any required scopes/permissions.
   * Used to trigger rate-limited user notifications about scope drift.
   */
  hasMissingScopes(): boolean;

  /**
   * Send a rate-limited notification to the user about missing scopes/permissions.
   * Implementations should throttle notifications (e.g., once per 30 days per workspace).
   * No-op if the provider has no missing scopes or doesn't support scope detection.
   * @param message - The incoming message to reply in-thread to
   */
  notifyMissingScopes(message: IncomingChatMessage): Promise<void>;

  /**
   * Download files from thread history messages.
   * Reuses the provider's existing download logic (auth headers, SSRF protection, etc.).
   * @param files - File metadata from thread history messages
   * @returns One outcome per input file, positionally aligned with `files`:
   * either the downloaded attachment (base64-encoded A2A format) or the
   * skip record explaining why it was not delivered
   */
  downloadFiles(files: ChatThreadMessageFile[]): Promise<ThreadFileOutcome[]>;

  /**
   * Discover all channels in a workspace/team.
   * Used to auto-populate channel bindings so admins can assign agents from the UI.
   * @param context - Provider-specific context (e.g., TurnContext for MS Teams)
   * @returns Discovered channels, or null if context doesn't support discovery
   */
  discoverChannels(context: unknown): Promise<DiscoveredChannel[] | null>;
}

/**
 * Callback interface for socket-mode providers to delegate events
 * back to the ChatOpsManager without depending on it directly.
 */
export interface ChatOpsEventHandler {
  handleIncomingMessage(
    provider: ChatOpsProvider,
    body: unknown,
  ): Promise<void>;
  handleInteractiveApprovalDecision(
    provider: ChatOpsProvider,
    decision: ChatOpsApprovalDecision,
    updateApprovalRequestCallback?: () => Promise<void> | void,
  ): Promise<void>;
  handleInteractiveSelection(
    provider: ChatOpsProvider,
    payload: unknown,
  ): Promise<void>;
  getAccessibleChatopsAgents({
    senderEmail,
    isDm,
  }: {
    senderEmail?: string;
    isDm: boolean;
  }): Promise<{ id: string; name: string }[]>;
}

/**
 * MS Teams specific configuration from environment variables
 */
export interface MSTeamsConfig {
  enabled: boolean;
  /** Azure Bot App ID */
  appId: string;
  /** Azure Bot App Secret (Client Secret) */
  appSecret: string;
  /** Optional Graph API configuration for thread history */
  graph?: {
    tenantId: string;
    clientId: string;
    clientSecret: string;
  };
}

/** MS Teams config stored as a DB secret */
export interface MsTeamsDbConfig {
  enabled: boolean;
  appId: string;
  appSecret: string;
  tenantId: string;
  graphTenantId: string;
  graphClientId: string;
  graphClientSecret: string;
}

/** Slack config stored as a DB secret */
export interface SlackDbConfig {
  enabled: boolean;
  botToken: string;
  signingSecret: string;
  appId: string;
  connectionMode?: ChatOpsConnectionMode;
  appLevelToken?: string;
}

/** ngrok tunnel config stored as a DB secret */
export interface NgrokDbConfig {
  authToken: string;
  /** Optional reserved domain for a stable public URL across restarts. */
  domain: string;
  /**
   * False when the user explicitly stopped the tunnel — credentials are kept
   * for reconnecting, but the tunnel must not come back up on restart.
   * Missing (older rows) means enabled.
   */
  enabled?: boolean;
}
