/**
 * Mimecast API 2.0 Client
 * Auth: OAuth2 Client Credentials — POST /oauth/token
 * Base: https://api.services.mimecast.com
 *
 * Endpoints sourced from official Postman collection (mimecast-api-v2-collection.json):
 *  - Message logs:   POST /api/message-finder/search  (trackedEmails with pagination)
 *  - SIEM batch:     GET  /siem/v1/batch/events/cg    (returns pre-signed URLs → download JSON)
 *  - Threat events:  GET  /threats/v1/events
 *  - Message info:   POST /api/message-finder/get-message-info
 *  - Account:        POST /api/account/get-account
 */

export interface MimecastConfig {
  clientId: string;
  clientSecret: string;
  baseUrl?: string;
  accountCode?: string;
}

interface TokenResponse {
  access_token: string;
  token_type: string;
  expires_in: number;
}

interface TokenCache {
  token: string;
  expiresAt: number;
}

export interface MimecastMessage {
  id: string;
  senderAddress: string;
  recipientAddress: string;
  subject: string;
  direction: string;
  status: string;
  action?: string;
  spamScore?: number;
  sizeBytes?: number;
  attachmentCount?: boolean | number;
  sentDateTime?: string;
  receivedDateTime?: string;
  route?: string;
  rejectReason?: string;
  heldReason?: string;
  sourceIp?: string;
  [key: string]: any;
}

export interface MimecastThreatEvent {
  id: string;
  messageId?: string;
  eventType: string;
  threatLevel?: string;
  url?: string;
  fileName?: string;
  verdict?: string;
  actorEmail?: string;
  eventDateTime?: string;
  analysis?: string[];
  source?: string[];
  direction?: string[];
  status?: string[];
  [key: string]: any;
}

export interface MimecastMessageInfo {
  messageId: string;
  bodyText?: string;
  bodyHtml?: string;
  headers?: Record<string, string>;
}

export interface MimecastHeldMessage {
  id: string;
  subject: string;
  from: string;
  fromDisplay: string;
  to: string;
  toDisplay: string;
  dateReceived: string;
  reason: string;
  reasonCode: string;
  policyInfo: string;
  route: string;
  hasAttachments: boolean;
  size: number;
}

export interface MimecastDeliveredMessage {
  id: string;
  status: string;
  subject: string;
  from: string;
  fromEnv: string;
  to: string;
  toDisplay: string;
  received: string;
  senderIP: string;
  spamScore: number;
  detectionLevel: string;
  attachments: boolean;
  route: string;
  info: string;
}

export interface MimecastCloudUser {
  emailAddress: string;
  domain: string;
  lockedOut: boolean;
  status?: string;
  name?: string;
  alias?: string[];
  [key: string]: any;
}

export interface PaginatedResult<T> {
  items: T[];
  nextCursor: string | null;
  totalCount?: number;
}

export class MimecastClient {
  private readonly clientId: string;
  private readonly clientSecret: string;
  private readonly baseUrl: string;
  private readonly accountCode: string;
  private tokenCache: TokenCache | null = null;

  constructor(config: MimecastConfig) {
    this.clientId = config.clientId;
    this.clientSecret = config.clientSecret;
    this.baseUrl = (config.baseUrl ?? 'https://api.services.mimecast.com').replace(/\/$/, '');
    this.accountCode = config.accountCode ?? '';
  }

  /**
   * Format a Date to the Mimecast-required format: yyyy-MM-ddTHH:mm:ss+0000
   * Note: NO milliseconds — the API rejects .SSS variants
   */
  static formatDate(d: Date): string {
    const pad = (n: number) => String(n).padStart(2, '0');
    return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}` +
      `T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}+0000`;
  }

  // ── Auth ─────────────────────────────────────────────────────────────────

  private async getToken(): Promise<string> {
    if (this.tokenCache && Date.now() < this.tokenCache.expiresAt - 60_000) {
      return this.tokenCache.token;
    }

    const body = new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: this.clientId,
      client_secret: this.clientSecret,
    });

    const res = await fetch(`${this.baseUrl}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body.toString(),
    });

    if (!res.ok) {
      const text = await res.text();
      throw new Error(`Mimecast auth failed (${res.status}): ${text}`);
    }

    const data: TokenResponse = await res.json();
    this.tokenCache = {
      token: data.access_token,
      expiresAt: Date.now() + data.expires_in * 1000,
    };
    return data.access_token;
  }

  private async request<T>(
    method: 'GET' | 'POST',
    path: string,
    body?: Record<string, any>,
    params?: Record<string, string>,
    timeoutMs = 20_000
  ): Promise<T> {
    const token = await this.getToken();

    let url = `${this.baseUrl}${path}`;
    if (params && Object.keys(params).length > 0) {
      url = `${url}?${new URLSearchParams(params).toString()}`;
    }

    const headers: Record<string, string> = {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      Accept: 'application/json',
    };
    if (this.accountCode) {
      headers['x-mc-account'] = this.accountCode;
    }

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);

    const res = await fetch(url, {
      method,
      signal: controller.signal,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    clearTimeout(timer);

    if (!res.ok) {
      const text = await res.text();
      throw new Error(`Mimecast ${method} ${path} failed (${res.status}): ${text}`);
    }

    return res.json() as T;
  }

  // ── Message Tracking ─────────────────────────────────────────────────────

  /**
   * Search tracked emails via POST /api/message-finder/search
   * Response: { data: [{ trackedEmails: [...], pageToken: string }] }
   */
  async getMessageLogs(options: {
    from: Date;
    to: Date;
    cursor?: string;
    pageSize?: number;
    fromFilter?: string;
    toFilter?: string;
  }): Promise<PaginatedResult<MimecastMessage>> {
    const atOptions: Record<string, string> = {};
    if (options.fromFilter) atOptions.from = options.fromFilter;
    if (options.toFilter)   atOptions.to   = options.toFilter;

    const reqData: Record<string, any> = {
      accountCode: this.accountCode,
      advancedTrackAndTraceOptions: atOptions,
      start: MimecastClient.formatDate(options.from),
      end: MimecastClient.formatDate(options.to),
    };
    if (options.cursor) reqData.pageToken = options.cursor;

    const data = await this.request<any>('POST', '/api/message-finder/search', { data: [reqData] });

    const block = data.data?.[0] ?? {};
    const tracked: any[] = block.trackedEmails ?? [];

    return {
      items: tracked.map((m: any) => this.normalizeTrackedEmail(m)),
      nextCursor: block.pageToken ?? null,
      totalCount: tracked.length,
    };
  }

  private normalizeTrackedEmail(m: any): MimecastMessage {
    const sender = m.fromEnv?.emailAddress ?? m.fromHdr?.emailAddress ?? m.sender ?? '';
    const recipients: string[] = (m.to ?? []).map((t: any) =>
      typeof t === 'string' ? t : t.emailAddress ?? ''
    );

    return {
      id: m.id,
      senderAddress: sender,
      recipientAddress: recipients.join(', '),
      subject: m.subject ?? '',
      direction: m.route?.toLowerCase().includes('inbound') ? 'inbound' : 'outbound',
      status: m.status ?? '',
      action: m.detectionLevel ?? undefined,
      spamScore: m.spamScore ?? undefined,
      sizeBytes: undefined,
      attachmentCount: m.attachments ?? 0,
      sentDateTime: m.sent ?? undefined,
      receivedDateTime: m.received ?? undefined,
      route: m.route ?? undefined,
      rejectReason: m.info ?? undefined,
      sourceIp: m.senderIP ?? undefined,
      _raw: m,
    };
  }

  // ── SIEM Batch Events ─────────────────────────────────────────────────────

  /**
   * GET /siem/v1/batch/events/cg — returns pre-signed download URLs for batch NDJSON files
   * Each URL points to a compressed file; we download and parse each one.
   * Response: { value: [{url, expiry, size}], "@nextPage": string }
   */
  async getSiemBatchEventUrls(options: {
    from: string;  // date only: YYYY-MM-DD
    to: string;
    type?: string;
    cursor?: string;
    pageSize?: number;
  }): Promise<{ urls: Array<{ url: string; expiry: string; size: number }>; nextCursor: string | null }> {
    const params: Record<string, string> = {
      type: options.type ?? 'mailflow',
      dateRangeStartsAt: options.from,
      dateRangeEndsAt: options.to,
      pageSize: String(options.pageSize ?? 500),
    };
    if (options.cursor) params.pageToken = options.cursor;

    const data = await this.request<any>('GET', '/siem/v1/batch/events/cg', undefined, params);

    return {
      urls: data.value ?? [],
      nextCursor: data['@nextPage'] ?? null,
    };
  }

  /**
   * Download and parse a SIEM batch event file (NDJSON, possibly gzipped)
   */
  async downloadSiemBatchFile(url: string): Promise<MimecastMessage[]> {
    const res = await fetch(url);
    if (!res.ok) return [];

    const text = await res.text();
    const lines = text.split('\n').filter(l => l.trim());
    const messages: MimecastMessage[] = [];

    for (const line of lines) {
      try {
        const event = JSON.parse(line);
        messages.push(this.normalizeSiemEvent(event));
      } catch {
        // skip malformed lines
      }
    }

    return messages;
  }

  private normalizeSiemEvent(e: any): MimecastMessage {
    return {
      id: e.messageId ?? e.id ?? e.Messageid ?? '',
      senderAddress: e.senderAddress ?? e.Sender ?? e.sender ?? '',
      recipientAddress: e.recipientAddress ?? e.Recipient ?? e.recipient ?? '',
      subject: e.subject ?? e.Subject ?? '',
      direction: (e.Dir ?? e.direction ?? '').toLowerCase() === 'inbound' ? 'inbound' : 'outbound',
      status: e.Act ?? e.action ?? e.status ?? '',
      action: e.RejType ?? e.rejectionType ?? undefined,
      spamScore: e.SpamScore ?? e.spamScore ?? undefined,
      sizeBytes: e.MsgSize ?? e.messageSize ?? e.size ?? undefined,
      attachmentCount: e.AttCnt ?? e.attachmentCount ?? 0,
      sentDateTime: e.Datetime ?? e.datetime ?? e.timestamp ?? undefined,
      receivedDateTime: e.Datetime ?? undefined,
      route: e.Route ?? e.route ?? undefined,
      rejectReason: e.RejCode ?? e.rejectionCode ?? undefined,
      heldReason: e.HeldGroup ?? undefined,
      sourceIp: e.IP ?? e.senderIp ?? undefined,
      _raw: e,
    };
  }

  // ── Threat Events ─────────────────────────────────────────────────────────

  /**
   * GET /threats/v1/events
   * Response: { value: [{timestamp, analysis, source, details, status, direction, subject, sender, ...}], "@nextPage": string }
   */
  async getThreatEvents(options: {
    cursor?: string;
    pageSize?: number;
  } = {}): Promise<PaginatedResult<MimecastThreatEvent>> {
    const params: Record<string, string> = {
      pageSize: String(options.pageSize ?? 500),
    };
    if (options.cursor) params.pageToken = options.cursor;

    try {
      const data = await this.request<any>('GET', '/threats/v1/events', undefined, params);
      const items = (data.value ?? []).map((e: any) => this.normalizeThreatEvent(e));
      return {
        items,
        nextCursor: data['@nextPage'] ?? null,
      };
    } catch {
      return { items: [], nextCursor: null };
    }
  }

  private normalizeThreatEvent(e: any): MimecastThreatEvent {
    const analysis: string[] = Array.isArray(e.analysis) ? e.analysis : [e.analysis].filter(Boolean);
    const eventType = analysis[0] ?? 'unknown';

    const statusArr: string[] = Array.isArray(e.status) ? e.status : [e.status].filter(Boolean);
    const verdict = statusArr[0] ?? undefined;

    const threatLevel = analysis.includes('malware') ? 'high'
      : analysis.includes('phishing') ? 'high'
      : analysis.includes('spam') ? 'medium'
      : 'info';

    return {
      id: e.id ?? `threat_${e.sender ?? ''}_${e.timestamp ?? Date.now()}`,
      messageId: e.messageId ?? undefined,
      eventType,
      threatLevel,
      url: e.url ?? undefined,
      fileName: undefined,
      verdict,
      actorEmail: e.sender ?? undefined,
      eventDateTime: e.timestamp ?? undefined,
      analysis,
      source: Array.isArray(e.source) ? e.source : [e.source].filter(Boolean),
      direction: Array.isArray(e.direction) ? e.direction : [e.direction].filter(Boolean),
      status: statusArr,
      _raw: e,
    };
  }

  // ── Message Info (body/headers) ────────────────────────────────────────────

  /**
   * POST /api/message-finder/get-message-info
   * Returns delivered message details including parts
   */
  async getMessageInfo(messageId: string): Promise<MimecastMessageInfo | null> {
    try {
      const data = await this.request<any>('POST', '/api/message-finder/get-message-info', {
        data: [{ id: messageId }],
      });

      const delivered = data.data?.[0]?.deliveredMessage;
      if (!delivered) return null;

      const entries = Object.values(delivered) as any[];
      if (!entries.length) return null;

      const info = entries[0]?.messageInfo ?? entries[0];

      return {
        messageId,
        bodyText: info?.textBody ?? info?.body ?? undefined,
        bodyHtml: info?.htmlBody ?? undefined,
        headers: info?.headers ?? undefined,
      };
    } catch {
      return null;
    }
  }

  // ── Cloud Gateway ─────────────────────────────────────────────────────────

  /**
   * GET /user/cloud-gateway/v1/users?emailAddress=...&domain=...
   */
  async getCloudUser(emailAddress: string, domain: string): Promise<MimecastCloudUser | null> {
    const data = await this.request<any>('GET', '/user/cloud-gateway/v1/users', undefined, {
      emailAddress,
      domain,
    });
    const user = data?.value?.[0] ?? data?.data?.[0] ?? null;
    if (!user) return null;
    return {
      emailAddress: user.emailAddress ?? emailAddress,
      domain: user.domain ?? domain,
      lockedOut: user.lockedOut ?? false,
      status: user.status ?? undefined,
      name: user.name ?? undefined,
      alias: user.alias ?? undefined,
      _raw: user,
    };
  }

  // ── Held Messages ──────────────────────────────────────────────────────────

  /**
   * POST /api/gateway/get-hold-message-list
   * admin: true = see user-level hold queues as an admin
   * pageSize is ignored by the API (always returns 10); paginate via meta.pagination.next
   * Fetches ALL pages up to maxMessages limit.
   */
  async getHeldMessages(options: {
    recipient?: string;
    maxMessages?: number;
  } = {}): Promise<{ messages: MimecastHeldMessage[]; totalCount: number }> {
    const maxMessages = options.maxMessages ?? 100;
    const all: MimecastHeldMessage[] = [];
    let cursor: string | null = null;
    let totalCount = 0;

    do {
      const reqBody: any = { admin: true };
      if (options.recipient) {
        reqBody.searchBy = { fieldName: 'recipient', value: options.recipient };
      }

      const body: any = { data: [reqBody] };
      if (cursor) {
        body.meta = { pagination: { pageToken: cursor } };
      }

      let result: any;
      try {
        result = await this.request<any>('POST', '/api/gateway/get-hold-message-list', body);
      } catch (err: any) {
        if (err.message?.includes('403') && !cursor) {
          // Retry without admin: true for tenants that lack the permission
          const fallbackBody: any = { data: [{ ...reqBody }] };
          delete fallbackBody.data[0].admin;
          if (cursor) fallbackBody.meta = { pagination: { pageToken: cursor } };
          result = await this.request<any>('POST', '/api/gateway/get-hold-message-list', fallbackBody);
        } else {
          throw err;
        }
      }

      const pagination = result?.meta?.pagination ?? {};
      const msgs: any[] = result?.data ?? [];

      if (totalCount === 0) totalCount = pagination.totalCount ?? msgs.length;

      for (const m of msgs) {
        all.push({
          id: m.id,
          subject: m.subject ?? '',
          from: m.fromHeader?.emailAddress ?? m.from?.emailAddress ?? '',
          fromDisplay: m.fromHeader?.displayableName ?? m.from?.displayableName ?? '',
          to: m.to?.emailAddress ?? '',
          toDisplay: m.to?.displayableName ?? '',
          dateReceived: m.dateReceived ?? '',
          reason: m.reason ?? '',
          reasonCode: m.reasonCode ?? '',
          policyInfo: m.policyInfo ?? '',
          route: m.route ?? '',
          hasAttachments: m.hasAttachments ?? false,
          size: m.size ?? 0,
        });
      }

      cursor = pagination.next ?? null;
    } while (cursor && all.length < maxMessages);

    return { messages: all, totalCount };
  }

  /**
   * POST /api/message-finder/search
   * Search delivered/accepted inbound messages. Requires at least one of: to, from, subject, senderIP, url.
   */
  async searchDeliveredMessages(options: {
    to?: string;
    from?: string;
    subject?: string;
    startHours?: number;
    start?: string;
    end?: string;
    route?: string;
  }): Promise<{ messages: MimecastDeliveredMessage[]; error?: string }> {
    const now = new Date();
    const startDate = options.start
      ? options.start
      : new Date(now.getTime() - (options.startHours ?? 24) * 60 * 60 * 1000)
          .toISOString()
          .replace(/\.\d{3}Z$/, '+0000');
    const endDate = options.end ?? now.toISOString().replace(/\.\d{3}Z$/, '+0000');

    const opts: Record<string, any> = {};
    if (options.to) opts.to = options.to;
    if (options.from) opts.from = options.from;
    if (options.subject) opts.subject = options.subject;
    if (options.route) opts.route = options.route;

    try {
      const result = await this.request<any>('POST', '/api/message-finder/search', {
        data: [{
          start: startDate,
          end: endDate,
          advancedTrackAndTraceOptions: opts,
        }],
      });
      const emails: any[] = result?.data?.[0]?.trackedEmails ?? [];
      return {
        messages: emails.map(e => ({
          id: e.id,
          status: e.status,
          subject: e.subject ?? '',
          from: e.fromHdr?.emailAddress ?? e.fromEnv?.emailAddress ?? '',
          fromEnv: e.fromEnv?.emailAddress ?? '',
          to: e.to?.[0]?.emailAddress ?? '',
          toDisplay: e.to?.[0]?.displayableName ?? '',
          received: e.received,
          senderIP: e.senderIP ?? '',
          spamScore: e.spamScore ?? 0,
          detectionLevel: e.detectionLevel ?? '',
          attachments: e.attachments ?? false,
          route: e.route ?? '',
          info: e.info ?? '',
        })),
      };
    } catch (err: any) {
      return { messages: [], error: err.message };
    }
  }

  /**
   * POST /api/gateway/hold-release
   * Releases a held message by ID. Returns true if released successfully.
   */
  async releaseHeldMessage(id: string): Promise<{ released: boolean; error?: string }> {
    try {
      const result = await this.request<any>('POST', '/api/gateway/hold-release', {
        data: [{ id }],
      });
      // API returns 200 with fail[] empty on success; release field may be false for already-released msgs
      const hasFail = result?.fail?.length > 0;
      if (hasFail) {
        return { released: false, error: result.fail[0]?.message ?? 'Release rejected by Mimecast' };
      }
      return { released: true };
    } catch (err: any) {
      return { released: false, error: err.message };
    }
  }

  // ── Account ────────────────────────────────────────────────────────────────

  /**
   * POST /api/account/get-account
   */
  async testConnection(): Promise<{ ok: boolean; accountName?: string; packageName?: string; error?: string }> {
    try {
      const data = await this.request<any>('POST', '/api/account/get-account', { data: [{ accountCode: this.accountCode }] });
      const account = data.data?.[0] ?? {};
      return {
        ok: true,
        accountName: account.accountName ?? account.accountCode ?? 'Connected',
        packageName: account.packageName ?? undefined,
      };
    } catch (err: any) {
      return { ok: false, error: err.message };
    }
  }
}

let _client: MimecastClient | null = null;

export function getMimecastClient(): MimecastClient {
  if (!_client) {
    const clientId = process.env.MIMECAST_CLIENT_ID;
    const clientSecret = process.env.MIMECAST_CLIENT_SECRET;
    if (!clientId || !clientSecret) {
      throw new Error('MIMECAST_CLIENT_ID and MIMECAST_CLIENT_SECRET must be set');
    }
    _client = new MimecastClient({
      clientId,
      clientSecret,
      baseUrl: process.env.MIMECAST_BASE_URL ?? 'https://api.services.mimecast.com',
      accountCode: process.env.MIMECAST_ACCOUNT_CODE ?? '',
    });
  }
  return _client;
}

export function getMimecastClientForTenant(tenant: {
  client_id: string;
  client_secret: string;
  base_url?: string;
}): MimecastClient {
  return new MimecastClient({
    clientId: tenant.client_id,
    clientSecret: tenant.client_secret,
    baseUrl: tenant.base_url ?? 'https://api.services.mimecast.com',
    accountCode: '',
  });
}
