const fs = require("fs");
const path = require("path");
const os = require("os");
const mime = require("mime");
const { SystemSettings } = require("../../../../../models/systemSettings");
const { CollectorApi } = require("../../../../collectorApi");
const { humanFileSize } = require("../../../../helpers");
const { safeJsonParse } = require("../../../../http");

const MAX_TOTAL_ATTACHMENT_SIZE = 25 * 1024 * 1024; // 25MB limit for Outlook

/**
 * Parses a comma-separated email string into Graph API recipient format.
 * @param {string} emailString - Comma-separated email addresses
 * @returns {Array<{emailAddress: {address: string}}>}
 */
function parseEmailRecipients(emailString) {
  if (!emailString) return [];
  return emailString.split(",").map((email) => ({
    emailAddress: { address: email.trim() },
  }));
}

/**
 * Validates organization auth type configuration.
 * @param {Object} config - The Outlook configuration object
 * @returns {{valid: boolean, error?: string}}
 */
function validateOrganizationAuth(config) {
  const authType = config.authType || AUTH_TYPES.common;
  if (authType === AUTH_TYPES.organization && !config.tenantId) {
    return {
      valid: false,
      error: "Tenant ID is required for organization-only authentication.",
    };
  }
  return { valid: true };
}

/**
 * Maps a Microsoft Graph message object to a simplified format.
 * @param {Object} msg - The Graph API message object
 * @param {Object} options - Mapping options
 * @param {boolean} options.includeBody - Include full body content
 * @param {boolean} options.includeAttachments - Include attachment details
 * @returns {Object} Simplified message object
 */
function mapGraphMessage(msg, options = {}) {
  const base = {
    id: msg.id,
    conversationId: msg.conversationId,
    subject: msg.subject,
    from: msg.from?.emailAddress?.address || "Unknown",
    fromName: msg.from?.emailAddress?.name || "",
    to: msg.toRecipients?.map((r) => r.emailAddress?.address).join(", ") || "",
    cc: msg.ccRecipients?.map((r) => r.emailAddress?.address).join(", ") || "",
    isRead: msg.isRead,
    hasAttachments: msg.hasAttachments,
  };

  if (msg.receivedDateTime) {
    base.receivedDateTime = msg.receivedDateTime;
  }
  if (msg.bodyPreview !== undefined) {
    base.preview = msg.bodyPreview;
  }

  if (options.includeBody && msg.body) {
    base.date = msg.receivedDateTime;
    base.body = msg.body?.content || "";
    base.bodyType = msg.body?.contentType || "text";
  }

  if (options.includeAttachments && msg.attachments) {
    base.attachments = (msg.attachments || []).map((att) => ({
      id: att.id,
      name: att.name,
      contentType: att.contentType,
      size: att.size,
      contentBytes: att.contentBytes,
    }));
  }

  return base;
}

/**
 * Formats an array of messages into a human-readable summary string.
 * @param {Array} messages - Array of simplified message objects
 * @returns {string} Formatted summary
 */
function formatMessageSummary(messages) {
  return messages
    .map(
      (m, i) =>
        `${i + 1}. [${m.isRead ? "READ" : "UNREAD"}] "${m.subject}" from ${m.fromName || m.from} (${new Date(m.receivedDateTime).toLocaleString()})${m.hasAttachments ? " 📎" : ""}\n   ID: ${m.id}\n   Conversation ID: ${m.conversationId}`
    )
    .join("\n\n");
}

/**
 * Handles errors in Outlook skill handlers with consistent logging and messaging.
 * @param {Object} context - The handler context (this) from the aibitat function
 * @param {string} skillName - The name of the skill (e.g., "outlook-get-inbox")
 * @param {Error} error - The error object
 * @returns {string} User-friendly error message
 */
function handleSkillError(context, skillName, error) {
  context.super.handlerProps.log(`${skillName} error: ${error.message}`);
  context.super.introspect(`Error: ${error.message}`);
  return `Error in ${skillName}: ${error.message}`;
}

/**
 * Normalizes a token expiry value to a number.
 * @param {number|string|null|undefined} expiry - The token expiry value
 * @returns {number|null} The normalized expiry as a number, or null if invalid
 */
function normalizeTokenExpiry(expiry) {
  if (expiry === null || expiry === undefined) return null;
  return typeof expiry === "number" ? expiry : parseInt(expiry, 10);
}

/**
 * Prepares and validates attachments for email sending/drafting.
 * @param {Object} context - The handler context with introspect and caller
 * @param {Array<string>} attachmentPaths - Array of absolute file paths
 * @param {Object} options - Options for attachment handling
 * @param {boolean} options.requireApprovalPerFile - Request approval for each file
 * @param {string} options.recipientInfo - Recipient info for approval message
 * @returns {Promise<{success: boolean, attachments?: Array, summaries?: Array, totalSize?: number, error?: string}>}
 */
async function prepareAttachmentsWithValidation(
  context,
  attachmentPaths,
  options = {}
) {
  if (!Array.isArray(attachmentPaths) || attachmentPaths.length === 0) {
    return { success: true, attachments: [], summaries: [], totalSize: 0 };
  }

  const preparedAttachments = [];
  const attachmentSummaries = [];
  let totalAttachmentSize = 0;

  context.super.introspect(
    `${context.caller}: Validating ${attachmentPaths.length} attachment(s)...`
  );

  for (const filePath of attachmentPaths) {
    const result = prepareAttachment(filePath);
    if (!result.success) {
      context.super.introspect(
        `${context.caller}: Attachment validation failed - ${result.error}`
      );
      return { success: false, error: result.error };
    }

    totalAttachmentSize += result.fileInfo.size;
    if (totalAttachmentSize > MAX_TOTAL_ATTACHMENT_SIZE) {
      const totalFormatted = humanFileSize(totalAttachmentSize, true);
      context.super.introspect(
        `${context.caller}: Total attachment size (${totalFormatted}) exceeds 25MB limit`
      );
      return {
        success: false,
        error: `Total attachment size (${totalFormatted}) exceeds the 25MB limit.`,
      };
    }

    if (options.requireApprovalPerFile && context.super.requestToolApproval) {
      const approval = await context.super.requestToolApproval({
        skillName: context.name,
        payload: {
          fileName: result.fileInfo.name,
          fileSize: result.fileInfo.sizeFormatted,
          filePath: result.fileInfo.path,
        },
        description:
          `Attach file "${result.fileInfo.name}" (${result.fileInfo.sizeFormatted}) to email? ` +
          `This file will be sent to ${options.recipientInfo || "recipients"}.`,
      });

      if (!approval.approved) {
        context.super.introspect(
          `${context.caller}: User rejected attaching "${result.fileInfo.name}"`
        );
        return {
          success: false,
          error: `Attachment rejected by user: ${result.fileInfo.name}. ${approval.message || ""}`,
        };
      }
    }

    preparedAttachments.push(result.attachment);
    attachmentSummaries.push(
      `${result.fileInfo.name} (${result.fileInfo.sizeFormatted})`
    );
    context.super.introspect(
      `${context.caller}: Prepared attachment "${result.fileInfo.name}"`
    );
  }

  return {
    success: true,
    attachments: preparedAttachments,
    summaries: attachmentSummaries,
    totalSize: totalAttachmentSize,
  };
}

// ============================================================================
// Attachment Functions
// ============================================================================

/**
 * Validates and prepares a file attachment for email.
 * @param {string} filePath - Absolute path to the file
 * @returns {{success: boolean, attachment?: object, error?: string, fileInfo?: object}}
 */
function prepareAttachment(filePath) {
  if (process.env.ANYTHING_LLM_RUNTIME === "docker") {
    return {
      success: false,
      error: "File attachments are not supported in Docker environments.",
    };
  }

  if (!path.isAbsolute(filePath)) {
    return { success: false, error: `Path must be absolute: ${filePath}` };
  }

  if (!fs.existsSync(filePath)) {
    return { success: false, error: `File does not exist: ${filePath}` };
  }

  const stats = fs.statSync(filePath);
  if (!stats.isFile()) {
    return { success: false, error: `Path is not a file: ${filePath}` };
  }

  if (stats.size === 0) {
    return { success: false, error: `File is empty: ${filePath}` };
  }

  try {
    const fileBuffer = fs.readFileSync(filePath);
    const base64Data = fileBuffer.toString("base64");
    const fileName = path.basename(filePath);
    const contentType = mime.getType(filePath) || "application/octet-stream";

    return {
      success: true,
      attachment: {
        "@odata.type": "#microsoft.graph.fileAttachment",
        name: fileName,
        contentType,
        contentBytes: base64Data,
      },
      fileInfo: {
        path: filePath,
        name: fileName,
        size: stats.size,
        sizeFormatted: humanFileSize(stats.size, true),
        contentType,
      },
    };
  } catch (e) {
    return { success: false, error: `Failed to read file: ${e.message}` };
  }
}

/**
 * Parse an attachment using the CollectorApi for secure content extraction.
 * @param {Object} attachment - The attachment object with name, contentType, size, contentBytes (base64)
 * @returns {Promise<{success: boolean, content: string|null, error: string|null}>}
 */
async function parseAttachment(attachment) {
  const tempDir = os.tmpdir();
  const safeFilename = attachment.name.replace(/[^a-zA-Z0-9._-]/g, "_");
  const tempFilePath = path.join(
    tempDir,
    `outlook-attachment-${Date.now()}-${safeFilename}`
  );

  try {
    const buffer = Buffer.from(attachment.contentBytes, "base64");
    fs.writeFileSync(tempFilePath, buffer);

    const collector = new CollectorApi();
    const result = await collector.parseDocument(safeFilename, {
      absolutePath: tempFilePath,
    });

    if (fs.existsSync(tempFilePath)) {
      fs.unlinkSync(tempFilePath);
    }

    if (!result.success) {
      return {
        success: false,
        content: null,
        error: result.reason || "Failed to parse attachment",
      };
    }

    const textContent = result.documents
      ?.map((doc) => doc.pageContent || doc.content || "")
      .filter(Boolean)
      .join("\n\n");

    return {
      success: true,
      content: textContent || "(No text content extracted)",
      error: null,
    };
  } catch (e) {
    if (fs.existsSync(tempFilePath)) {
      try {
        fs.unlinkSync(tempFilePath);
      } catch {}
    }
    return { success: false, content: null, error: e.message };
  }
}

/**
 * MIME types that can be parsed by the collector to extract text content.
 * These are a subset of ACCEPTED_MIMES from collector/utils/constants.js
 * that are suitable for attachment parsing (excludes audio/video and images).
 * Images are excluded because they're typically signature images in emails.
 */
const PARSEABLE_ATTACHMENT_MIMES = [
  "text/plain",
  "text/html",
  "text/csv",
  "application/json",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .docx
  "application/vnd.openxmlformats-officedocument.presentationml.presentation", // .pptx
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx
  "application/vnd.oasis.opendocument.text", // .odt
  "application/vnd.oasis.opendocument.presentation", // .odp
  "application/pdf",
  "application/epub+zip",
];

/**
 * Checks if an attachment's MIME type can be parsed for text extraction.
 * @param {string} contentType - The MIME type of the attachment
 * @returns {boolean}
 */
function isParseableMimeType(contentType) {
  if (!contentType) return false;
  const baseMime = contentType.split(";")[0].trim().toLowerCase();
  return PARSEABLE_ATTACHMENT_MIMES.includes(baseMime);
}

/**
 * Collect attachments from messages and optionally parse them with user approval.
 * Only attachments with parseable MIME types will be offered for parsing.
 * If two attachments have the same name, only the first one will be kept (handling fwd emails)
 * @param {Object} context - The handler context (this) from the aibitat function
 * @param {Array} messages - Array of message objects
 * @returns {Promise<{allAttachments: Array, parsedContent: string}>}
 */
async function handleAttachments(context, messages) {
  const allAttachments = [];
  const uniqueAttachments = new Set();
  messages.forEach((msg, msgIndex) => {
    if (msg.attachments?.length > 0) {
      msg.attachments.forEach((att) => {
        if (uniqueAttachments.has(att.name)) return;
        uniqueAttachments.add(att.name);
        allAttachments.push({
          ...att,
          messageIndex: msgIndex + 1,
          messageId: msg.id,
        });
      });
    }
  });

  const parseableAttachments = allAttachments.filter((att) =>
    isParseableMimeType(att.contentType)
  );

  let parsedContent = "";
  const citations = [];
  if (parseableAttachments.length > 0 && context.super.requestToolApproval) {
    const attachmentNames = parseableAttachments
      .map((a) => `${a.name} (${a.contentType})`)
      .join(", ");

    const approval = await context.super.requestToolApproval({
      skillName: context.name,
      payload: { attachments: attachmentNames },
      description: `Download and parse ${parseableAttachments.length} attachment(s) to extract text content? (${attachmentNames})`,
    });

    if (approval.approved) {
      context.super.introspect(
        `${context.caller}: Downloading and parsing ${parseableAttachments.length} attachment(s)...`
      );

      const parsedResults = [];
      for (const attachment of parseableAttachments) {
        if (!attachment.contentBytes) {
          context.super.introspect(
            `${context.caller}: Skipping "${attachment.name}" - no content available`
          );
          continue;
        }
        context.super.introspect(
          `${context.caller}: Parsing "${attachment.name}"...`
        );
        const parseResult = await parseAttachment(attachment);
        if (!parseResult.success) {
          context.super.introspect(
            `${context.caller}: Failed to parse "${attachment.name}": ${parseResult.error}`
          );
          continue;
        }

        citations.push({
          id: `outlook-attachment-${attachment.messageId}-${attachment.name}`,
          title: attachment.name,
          text: parseResult.content,
          chunkSource: "outlook-attachment://" + attachment.name,
          score: null,
        });
        parsedResults.push({
          name: attachment.name,
          messageIndex: attachment.messageIndex,
          ...parseResult,
        });
      }

      if (parsedResults.length > 0) {
        parsedContent =
          "\n\n--- Parsed Attachment Content ---\n" +
          parsedResults
            .map(
              (r) => `\n[Message ${r.messageIndex}: ${r.name}]\n${r.content}`
            )
            .join("\n");
      }

      context.super.introspect(
        `${context.caller}: Finished parsing attachments (${parsedResults.length}/${parseableAttachments.length} successful)`
      );
    } else {
      context.super.introspect(
        `${context.caller}: User declined to parse attachments`
      );
    }
  }

  citations.forEach((c) => context.super.addCitation?.(c));
  return { allAttachments, parsedContent };
}

/**
 * Microsoft Graph API OAuth2 Configuration
 * Uses Authorization Code Flow with PKCE
 */
const MICROSOFT_AUTH_URL = "https://login.microsoftonline.com";
const GRAPH_API_URL = "https://graph.microsoft.com/v1.0";
const SCOPES = [
  "offline_access",
  "Mail.Read",
  "Mail.ReadWrite",
  "Mail.Send",
  "User.Read",
].join(" ");

/**
 * Authentication types for Microsoft OAuth2.
 * - "organization": Use tenant ID endpoint (work/school accounts from a specific tenant only)
 * - "common": Use /common endpoint (both personal and work/school accounts)
 * - "consumers": Use /consumers endpoint (personal Microsoft accounts only)
 */
const AUTH_TYPES = {
  organization: "organization",
  common: "common",
  consumers: "consumers",
};

/**
 * Gets the appropriate OAuth2 authority endpoint based on auth type.
 * @param {string} authType - The authentication type
 * @param {string} tenantId - The tenant ID (used only for "organization" type)
 * @returns {string} The authority path segment
 */
function getAuthority(authType, tenantId) {
  switch (authType) {
    case AUTH_TYPES.consumers:
      return "consumers";
    case AUTH_TYPES.organization:
      return tenantId || "common";
    case AUTH_TYPES.common:
    default:
      return "common";
  }
}

/**
 * Outlook Bridge Library
 * Handles OAuth2 authentication and Microsoft Graph API communication for Outlook mail.
 */
class OutlookBridge {
  #accessToken = null;
  #isInitialized = false;

  #log(text, ...args) {
    console.log(`\x1b[35m[OutlookBridge]\x1b[0m ${text}`, ...args);
  }

  /**
   * Decodes a JWT token and logs relevant info.
   * @param {string} token - The JWT token to decode
   * @param {string} context - Context label for logging (e.g., "NEW token", "Token")
   * @returns {Object|null} The decoded payload or null if decoding fails
   */
  #decodeAndLogToken(token, context = "Token") {
    try {
      const parts = token.split(".");
      if (parts.length !== 3) return null;
      const payload = JSON.parse(Buffer.from(parts[1], "base64").toString());
      this.#log(
        `${context} for: ${payload.upn || payload.email || payload.unique_name || "unknown"}`
      );
      return payload;
    } catch {
      this.#log(`Could not decode ${context.toLowerCase()}`);
      return null;
    }
  }

  /**
   * Resets the bridge state, forcing re-initialization on next use.
   */
  reset() {
    this.#accessToken = null;
    this.#isInitialized = false;
  }

  /**
   * Gets the current Outlook agent configuration from system settings.
   * @returns {Promise<{clientId?: string, tenantId?: string, clientSecret?: string, authType?: string, accessToken?: string, refreshToken?: string, tokenExpiry?: number}>}
   */
  static async getConfig() {
    const configJson = await SystemSettings.getValueOrFallback(
      { label: "outlook_agent_config" },
      "{}"
    );
    return safeJsonParse(configJson, {});
  }

  /**
   * Updates the Outlook agent configuration in system settings.
   * @param {Object} updates - Fields to update
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  static async updateConfig(updates) {
    try {
      await SystemSettings.updateSettings({
        outlook_agent_config: JSON.stringify(updates),
      });
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  /**
   * Generates the OAuth2 authorization URL for the user to authenticate.
   * @param {string} redirectUri - The callback URL for OAuth
   * @returns {Promise<{success: boolean, url?: string, error?: string}>}
   */
  async getAuthUrl(redirectUri) {
    const config = await OutlookBridge.getConfig();

    if (!config.clientId) {
      return {
        success: false,
        error: "Outlook configuration incomplete. Please set Client ID.",
      };
    }

    const orgAuth = validateOrganizationAuth(config);
    if (!orgAuth.valid) {
      return { success: false, error: orgAuth.error };
    }

    const authType = config.authType || AUTH_TYPES.common;
    const authority = getAuthority(authType, config.tenantId);
    const params = new URLSearchParams({
      client_id: config.clientId,
      response_type: "code",
      redirect_uri: redirectUri,
      response_mode: "query",
      scope: SCOPES,
    });

    const url = `${MICROSOFT_AUTH_URL}/${authority}/oauth2/v2.0/authorize?${params.toString()}`;
    this.#log(`Auth URL using authType: ${authType}, authority: ${authority}`);
    return { success: true, url };
  }

  /**
   * Exchanges the authorization code for access and refresh tokens.
   * @param {string} code - The authorization code from OAuth callback
   * @param {string} redirectUri - The callback URL used in the initial auth request
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async exchangeCodeForToken(code, redirectUri) {
    const config = await OutlookBridge.getConfig();

    if (!config.clientId || !config.clientSecret) {
      return {
        success: false,
        error: "Outlook configuration incomplete.",
      };
    }

    const orgAuth = validateOrganizationAuth(config);
    if (!orgAuth.valid) {
      return { success: false, error: orgAuth.error };
    }

    try {
      const authType = config.authType || AUTH_TYPES.common;
      const authority = getAuthority(authType, config.tenantId);
      const tokenUrl = `${MICROSOFT_AUTH_URL}/${authority}/oauth2/v2.0/token`;
      this.#log(
        `Token exchange using authType: ${authType}, authority: ${authority}`
      );

      const params = new URLSearchParams({
        client_id: config.clientId,
        client_secret: config.clientSecret,
        code,
        redirect_uri: redirectUri,
        grant_type: "authorization_code",
        scope: SCOPES,
      });

      const response = await fetch(tokenUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: params.toString(),
      });

      const data = await response.json();

      if (!response.ok) {
        this.#log("Token exchange failed:", data);
        return {
          success: false,
          error:
            data.error_description || data.error || "Token exchange failed",
        };
      }

      const expiresAt = Date.now() + (data.expires_in - 60) * 1000;
      this.#decodeAndLogToken(data.access_token, "NEW token received");
      await OutlookBridge.updateConfig({
        ...config,
        accessToken: data.access_token,
        refreshToken: data.refresh_token,
        tokenExpiry: expiresAt,
      });

      this.#accessToken = data.access_token;
      this.#isInitialized = false; // Force re-initialization

      this.#log("Successfully obtained tokens");
      return { success: true };
    } catch (error) {
      this.#log("Token exchange error:", error.message);
      return { success: false, error: error.message };
    }
  }

  /**
   * Refreshes the access token using the refresh token.
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async #refreshAccessToken() {
    const config = await OutlookBridge.getConfig();

    if (!config.clientId || !config.clientSecret || !config.refreshToken) {
      return {
        success: false,
        error: "Cannot refresh token. Missing configuration or refresh token.",
      };
    }

    const orgAuth = validateOrganizationAuth(config);
    if (!orgAuth.valid) {
      return { success: false, error: orgAuth.error };
    }

    try {
      const authType = config.authType || AUTH_TYPES.common;
      const authority = getAuthority(authType, config.tenantId);
      const tokenUrl = `${MICROSOFT_AUTH_URL}/${authority}/oauth2/v2.0/token`;
      this.#log(
        `Token refresh using authType: ${authType}, authority: ${authority}`
      );

      const params = new URLSearchParams({
        client_id: config.clientId,
        client_secret: config.clientSecret,
        refresh_token: config.refreshToken,
        grant_type: "refresh_token",
        scope: SCOPES,
      });

      const response = await fetch(tokenUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: params.toString(),
      });

      const data = await response.json();

      if (!response.ok) {
        this.#log("Token refresh failed:", data);
        return {
          success: false,
          error: data.error_description || data.error || "Token refresh failed",
        };
      }

      const expiresAt = Date.now() + (data.expires_in - 60) * 1000;

      await OutlookBridge.updateConfig({
        ...config,
        accessToken: data.access_token,
        refreshToken: data.refresh_token || config.refreshToken,
        tokenExpiry: expiresAt,
      });

      this.#accessToken = data.access_token;

      this.#log("Successfully refreshed access token");
      return { success: true };
    } catch (error) {
      this.#log("Token refresh error:", error.message);
      return { success: false, error: error.message };
    }
  }

  /**
   * Ensures we have a valid access token, refreshing if necessary.
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async #ensureValidToken() {
    const config = await OutlookBridge.getConfig();

    if (!config.accessToken || !config.tokenExpiry) {
      this.#log("No access token or expiry found in config");
      return {
        success: false,
        error: "Outlook is not authenticated. Please complete the OAuth flow.",
      };
    }

    const expiryTime = normalizeTokenExpiry(config.tokenExpiry);

    const now = Date.now();
    const timeUntilExpiry = expiryTime - now;
    this.#log(
      `Token check: expires in ${Math.round(timeUntilExpiry / 1000)}s (at ${new Date(expiryTime).toISOString()})`
    );

    const payload = this.#decodeAndLogToken(config.accessToken, "Token check");
    if (payload) {
      this.#log(`Token aud: ${payload.aud}`);
      this.#log(`Token scp: ${payload.scp}`);
    }

    if (now >= expiryTime) {
      this.#log("Access token expired, refreshing...");
      return this.#refreshAccessToken();
    }

    this.#accessToken = config.accessToken;
    return { success: true };
  }

  /**
   * Initializes the Outlook bridge by fetching configuration from system settings.
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async initialize() {
    if (this.#isInitialized) return { success: true };

    try {
      const isMultiUser = await SystemSettings.isMultiUserMode();
      if (isMultiUser) {
        return {
          success: false,
          error:
            "Outlook integration is not available in multi-user mode for security reasons.",
        };
      }

      const config = await OutlookBridge.getConfig();

      if (!config.clientId || !config.clientSecret) {
        return {
          success: false,
          error:
            "Outlook integration is not configured. Please set Client ID and Client Secret in the agent settings.",
        };
      }

      const orgAuth = validateOrganizationAuth(config);
      if (!orgAuth.valid) {
        return { success: false, error: orgAuth.error };
      }

      this.#log(
        `Initializing with authType: ${config.authType || AUTH_TYPES.common}`
      );

      if (!config.accessToken) {
        return {
          success: false,
          error:
            "Outlook is not authenticated. Please complete the OAuth authorization flow.",
        };
      }

      const tokenResult = await this.#ensureValidToken();
      if (!tokenResult.success) {
        return tokenResult;
      }

      this.#isInitialized = true;
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  /**
   * Checks if the Outlook bridge is properly configured and available.
   * @returns {Promise<boolean>}
   */
  async isAvailable() {
    const result = await this.initialize();
    return result.success;
  }

  /**
   * Checks if Outlook tools are available (not in multi-user mode and has configuration).
   * @returns {Promise<boolean>}
   */
  static async isToolAvailable() {
    const isMultiUser = await SystemSettings.isMultiUserMode();
    if (isMultiUser) return false;

    const config = await OutlookBridge.getConfig();

    if (!config.clientId || !config.clientSecret || !config.accessToken) {
      return false;
    }

    const orgAuth = validateOrganizationAuth(config);
    return orgAuth.valid;
  }

  /**
   * Makes a request to the Microsoft Graph API.
   * @param {string} endpoint - The API endpoint (relative to /v1.0)
   * @param {object} options - Fetch options
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async request(endpoint, options = {}) {
    const initResult = await this.initialize();
    if (!initResult.success) {
      this.#log(`Initialize failed: ${initResult.error}`);
      return { success: false, error: initResult.error };
    }

    const tokenResult = await this.#ensureValidToken();
    if (!tokenResult.success) {
      this.#log(`Token validation failed: ${tokenResult.error}`);
      return { success: false, error: tokenResult.error };
    }

    try {
      const url = endpoint.startsWith("http")
        ? endpoint
        : `${GRAPH_API_URL}${endpoint}`;

      this.#log(`Making request to: ${url}`);

      const response = await fetch(url, {
        ...options,
        headers: {
          Authorization: `Bearer ${this.#accessToken}`,
          "Content-Type": "application/json",
          ...options.headers,
        },
      });

      if (!response.ok) {
        const errorText = await response.text();
        let errorData = {};
        try {
          errorData = JSON.parse(errorText);
        } catch {
          errorData = { raw: errorText };
        }
        this.#log(
          `API request failed: ${response.status} ${response.statusText}`,
          `\n  Endpoint: ${endpoint}`,
          `\n  Headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`,
          `\n  Error: ${JSON.stringify(errorData, null, 2)}`
        );
        return {
          success: false,
          error:
            errorData.error?.message ||
            `Request failed with status ${response.status}`,
        };
      }

      // Handle responses with no content (204 No Content, 202 Accepted with empty body, etc.)
      if (response.status === 204 || response.status === 202) {
        return { success: true, data: {} };
      }

      const text = await response.text();
      if (!text || text.trim() === "") {
        return { success: true, data: {} };
      }

      const data = JSON.parse(text);
      return { success: true, data };
    } catch (error) {
      return {
        success: false,
        error: `Outlook API request failed: ${error.message}`,
      };
    }
  }

  /**
   * Search emails using OData filter syntax.
   * Note: Microsoft Graph API does not support $skip with $search, so pagination
   * is only available when no query is provided.
   * @param {string} query - Search query (uses Microsoft Search syntax)
   * @param {number} limit - Maximum results to return
   * @param {number} skip - Number of results to skip for pagination (ignored when query is provided)
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async search(query = "", limit = 10, skip = 0) {
    let endpoint;

    if (query) {
      // $skip is not supported with $search in Microsoft Graph API
      // $orderby is also not supported with $search - results are ordered by relevance
      endpoint = `/me/messages?$top=${limit}&$select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,isRead,hasAttachments,bodyPreview,conversationId&$search="${encodeURIComponent(query)}"`;
    } else {
      endpoint = `/me/messages?$top=${limit}&$skip=${skip}&$orderby=receivedDateTime desc&$select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,isRead,hasAttachments,bodyPreview,conversationId`;
    }

    const result = await this.request(endpoint);
    if (!result.success) return result;

    const messages = result.data.value || [];
    return {
      success: true,
      data: {
        messages: messages.map((msg) => mapGraphMessage(msg)),
        resultCount: messages.length,
        hasMore: !!result.data["@odata.nextLink"],
      },
    };
  }

  /**
   * Get inbox messages (only from the Inbox folder, not archived/other folders).
   * @param {number} limit - Maximum results to return
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async getInbox(limit = 25) {
    const endpoint = `/me/mailFolders/inbox/messages?$top=${limit}&$orderby=receivedDateTime desc&$select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,isRead,hasAttachments,bodyPreview,conversationId`;

    const result = await this.request(endpoint);
    if (!result.success) return result;

    const messages = result.data.value || [];
    return {
      success: true,
      data: {
        messages: messages.map((msg) => mapGraphMessage(msg)),
        resultCount: messages.length,
        hasMore: !!result.data["@odata.nextLink"],
      },
    };
  }

  /**
   * Read a full conversation thread by conversation ID.
   * Note: We avoid combining $filter with $orderby due to Microsoft Graph API
   * "InefficientFilter" errors. Instead, we fetch without orderby and sort client-side.
   * @param {string} conversationId - The conversation ID
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async readThread(conversationId) {
    const endpoint = `/me/messages?$filter=conversationId eq '${conversationId}'&$select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,isRead,hasAttachments,body,attachments&$expand=attachments`;

    const result = await this.request(endpoint);
    if (!result.success) return result;

    let messages = result.data.value || [];
    if (messages.length === 0) {
      return {
        success: false,
        error: "No messages found in this conversation.",
      };
    }

    // Sort by receivedDateTime ascending (oldest first) client-side
    messages.sort(
      (a, b) => new Date(a.receivedDateTime) - new Date(b.receivedDateTime)
    );

    return {
      success: true,
      data: {
        conversationId,
        subject: messages[0]?.subject || "No Subject",
        messageCount: messages.length,
        messages: messages.map((msg) =>
          mapGraphMessage(msg, { includeBody: true, includeAttachments: true })
        ),
      },
    };
  }

  /**
   * Read a single message by ID.
   * @param {string} messageId - The message ID
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async readMessage(messageId) {
    const endpoint = `/me/messages/${messageId}?$select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,isRead,hasAttachments,body,conversationId&$expand=attachments`;

    const result = await this.request(endpoint);
    if (!result.success) return result;

    return {
      success: true,
      data: mapGraphMessage(result.data, {
        includeBody: true,
        includeAttachments: true,
      }),
    };
  }

  /**
   * Create a new draft email.
   * @param {string} to - Recipient email
   * @param {string} subject - Email subject
   * @param {string} body - Email body
   * @param {object} options - Additional options (cc, bcc, isHtml, attachments)
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async createDraft(to, subject, body, options = {}) {
    const message = {
      subject,
      body: {
        contentType: options.isHtml ? "HTML" : "Text",
        content: body,
      },
      toRecipients: parseEmailRecipients(to),
    };

    if (options.cc) {
      message.ccRecipients = parseEmailRecipients(options.cc);
    }

    if (options.bcc) {
      message.bccRecipients = parseEmailRecipients(options.bcc);
    }

    const result = await this.request("/me/messages", {
      method: "POST",
      body: JSON.stringify(message),
    });

    if (!result.success) return result;

    const draftId = result.data.id;

    if (options.attachments && options.attachments.length > 0) {
      for (const attachment of options.attachments) {
        const attachResult = await this.request(
          `/me/messages/${draftId}/attachments`,
          {
            method: "POST",
            body: JSON.stringify(attachment),
          }
        );
        if (!attachResult.success) {
          this.#log(`Failed to add attachment: ${attachResult.error}`);
        }
      }
    }

    return {
      success: true,
      data: {
        draftId: result.data.id,
        subject: result.data.subject,
        to,
        webLink: result.data.webLink,
      },
    };
  }

  /**
   * Create a draft reply to an existing message.
   * @param {string} messageId - The message ID to reply to
   * @param {string} body - Reply body
   * @param {boolean} replyAll - Whether to reply all
   * @param {object} options - Additional options
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async createDraftReply(messageId, body, replyAll = false, _options = {}) {
    const endpoint = replyAll
      ? `/me/messages/${messageId}/createReplyAll`
      : `/me/messages/${messageId}/createReply`;

    const result = await this.request(endpoint, {
      method: "POST",
      body: JSON.stringify({
        comment: body,
      }),
    });

    if (!result.success) return result;

    return {
      success: true,
      data: {
        draftId: result.data.id,
        subject: result.data.subject,
        webLink: result.data.webLink,
      },
    };
  }

  /**
   * Get a specific draft by ID.
   * @param {string} draftId - The draft ID
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async getDraft(draftId) {
    return this.readMessage(draftId);
  }

  /**
   * List all drafts.
   * @param {number} limit - Maximum drafts to return
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async listDrafts(limit = 25) {
    const endpoint = `/me/mailFolders/drafts/messages?$top=${limit}&$orderby=lastModifiedDateTime desc&$select=id,subject,toRecipients,lastModifiedDateTime,bodyPreview`;

    const result = await this.request(endpoint);
    if (!result.success) return result;

    const drafts = result.data.value || [];
    return {
      success: true,
      data: {
        drafts: drafts.map((draft) => ({
          id: draft.id,
          subject: draft.subject,
          to:
            draft.toRecipients
              ?.map((r) => r.emailAddress?.address)
              .join(", ") || "",
          lastModified: draft.lastModifiedDateTime,
          preview: draft.bodyPreview,
        })),
        count: drafts.length,
      },
    };
  }

  /**
   * Update an existing draft.
   * @param {string} draftId - The draft ID
   * @param {object} updates - Fields to update
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async updateDraft(draftId, updates) {
    const message = {};

    if (updates.subject) message.subject = updates.subject;
    if (updates.body) {
      message.body = {
        contentType: updates.isHtml ? "HTML" : "Text",
        content: updates.body,
      };
    }
    if (updates.to) {
      message.toRecipients = parseEmailRecipients(updates.to);
    }
    if (updates.cc) {
      message.ccRecipients = parseEmailRecipients(updates.cc);
    }

    const result = await this.request(`/me/messages/${draftId}`, {
      method: "PATCH",
      body: JSON.stringify(message),
    });

    if (!result.success) return result;

    return {
      success: true,
      data: {
        draftId: result.data.id,
        subject: result.data.subject,
      },
    };
  }

  /**
   * Delete a draft.
   * @param {string} draftId - The draft ID
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async deleteDraft(draftId) {
    return this.request(`/me/messages/${draftId}`, {
      method: "DELETE",
    });
  }

  /**
   * Send an existing draft.
   * @param {string} draftId - The draft ID
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async sendDraft(draftId) {
    return this.request(`/me/messages/${draftId}/send`, {
      method: "POST",
    });
  }

  /**
   * Send an email immediately.
   * @param {string} to - Recipient email
   * @param {string} subject - Email subject
   * @param {string} body - Email body
   * @param {object} options - Additional options
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async sendEmail(to, subject, body, options = {}) {
    const message = {
      subject,
      body: {
        contentType: options.isHtml ? "HTML" : "Text",
        content: body,
      },
      toRecipients: parseEmailRecipients(to),
    };

    if (options.cc) {
      message.ccRecipients = parseEmailRecipients(options.cc);
    }

    if (options.bcc) {
      message.bccRecipients = parseEmailRecipients(options.bcc);
    }

    if (options.attachments && options.attachments.length > 0) {
      message.attachments = options.attachments;
    }

    return this.request("/me/sendMail", {
      method: "POST",
      body: JSON.stringify({
        message,
        saveToSentItems: true,
      }),
    });
  }

  /**
   * Reply to a message immediately.
   * @param {string} messageId - The message ID to reply to
   * @param {string} body - Reply body
   * @param {boolean} replyAll - Whether to reply all
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async replyToMessage(messageId, body, replyAll = false) {
    const endpoint = replyAll
      ? `/me/messages/${messageId}/replyAll`
      : `/me/messages/${messageId}/reply`;

    return this.request(endpoint, {
      method: "POST",
      body: JSON.stringify({
        comment: body,
      }),
    });
  }

  /**
   * Mark a message as read.
   * @param {string} messageId - The message ID
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async markRead(messageId) {
    return this.request(`/me/messages/${messageId}`, {
      method: "PATCH",
      body: JSON.stringify({ isRead: true }),
    });
  }

  /**
   * Mark a message as unread.
   * @param {string} messageId - The message ID
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async markUnread(messageId) {
    return this.request(`/me/messages/${messageId}`, {
      method: "PATCH",
      body: JSON.stringify({ isRead: false }),
    });
  }

  /**
   * Move a message to trash.
   * @param {string} messageId - The message ID
   * @returns {Promise<{success: boolean, error?: string}>}
   */
  async moveToTrash(messageId) {
    return this.request(`/me/messages/${messageId}/move`, {
      method: "POST",
      body: JSON.stringify({
        destinationId: "deleteditems",
      }),
    });
  }

  /**
   * Get mailbox folder statistics.
   * @returns {Promise<{success: boolean, data?: object, error?: string}>}
   */
  async getMailboxStats() {
    const folders = ["inbox", "drafts", "sentitems", "deleteditems"];
    const stats = {};

    for (const folder of folders) {
      const result = await this.request(
        `/me/mailFolders/${folder}?$select=displayName,totalItemCount,unreadItemCount`
      );
      if (result.success) {
        stats[folder] = {
          name: result.data.displayName,
          total: result.data.totalItemCount,
          unread: result.data.unreadItemCount,
        };
      }
    }

    const profileResult = await this.request("/me?$select=displayName,mail");

    return {
      success: true,
      data: {
        email: profileResult.data?.mail || "Unknown",
        displayName: profileResult.data?.displayName || "Unknown",
        folders: stats,
      },
    };
  }
}

module.exports = new OutlookBridge();
module.exports.OutlookBridge = OutlookBridge;
module.exports.prepareAttachment = prepareAttachment;
module.exports.parseAttachment = parseAttachment;
module.exports.handleAttachments = handleAttachments;
module.exports.isParseableMimeType = isParseableMimeType;
module.exports.PARSEABLE_ATTACHMENT_MIMES = PARSEABLE_ATTACHMENT_MIMES;
module.exports.MAX_TOTAL_ATTACHMENT_SIZE = MAX_TOTAL_ATTACHMENT_SIZE;
module.exports.AUTH_TYPES = AUTH_TYPES;
module.exports.formatMessageSummary = formatMessageSummary;
module.exports.mapGraphMessage = mapGraphMessage;
module.exports.parseEmailRecipients = parseEmailRecipients;
module.exports.validateOrganizationAuth = validateOrganizationAuth;
module.exports.prepareAttachmentsWithValidation =
  prepareAttachmentsWithValidation;
module.exports.handleSkillError = handleSkillError;
module.exports.normalizeTokenExpiry = normalizeTokenExpiry;
