import type {
  InboundMessage,
  OutboundMessage,
  ChannelAdapter,
  ChannelStatus,
} from '../types.js';
import {
  allowTrustedOrPairedSender,
  buildResetInboundMessage,
  isAllowedByAllowlist,
  normalizeResetCommandText,
  shouldIgnoreForMissingMention,
  splitMessage,
} from '../utils.js';
import type { PairingManager } from '../pairing.js';

export interface MattermostAdapterConfig {
  serverUrl: string;
  botToken: string;
  /** Allowed channel IDs for inbound processing. Empty/undefined = allow all channel IDs. */
  allowedChannelIds?: string[];
  /** Require mention in non-DM channels (default: true). */
  requireMention?: boolean;
  /** Mention token used for mention detection/stripping (default: flynn). */
  mentionName?: string;
  /** Poll interval for inbound post polling (default: 3000ms). */
  pollIntervalMs?: number;
  /** Optional pairing manager for untrusted senders. */
  pairingManager?: PairingManager;
}

interface MattermostUser {
  id: string;
  username: string;
}

interface MattermostChannel {
  id: string;
  type?: string;
}

interface MattermostPost {
  id: string;
  channel_id: string;
  user_id: string;
  message?: string;
  create_at: number;
  type?: string;
}

interface MattermostPostsResponse {
  order?: string[];
  posts?: Record<string, MattermostPost>;
}

interface MattermostUploadResponse {
  file_infos?: Array<{ id?: string }>;
}

const DEFAULT_POLL_INTERVAL_MS = 3000;
const MAX_MESSAGE_LENGTH = 3500;

export class MattermostAdapter implements ChannelAdapter {
  readonly name = 'mattermost';

  private _status: ChannelStatus = 'disconnected';
  private messageHandler?: (msg: InboundMessage) => void;
  private readonly config: MattermostAdapterConfig;
  private pollTimer: NodeJS.Timeout | null = null;
  private polling = false;
  private botUserId = '';
  private botUsername = '';
  private channelTypes = new Map<string, string>();
  private channelCursorMs = new Map<string, number>();

  get status(): ChannelStatus {
    return this._status;
  }

  constructor(config: MattermostAdapterConfig) {
    this.config = config;
  }

  onMessage(handler: (msg: InboundMessage) => void): void {
    this.messageHandler = handler;
  }

  async connect(): Promise<void> {
    this._status = 'connecting';
    try {
      const me = await this.apiGet<MattermostUser>('/api/v4/users/me');
      this.botUserId = me.id;
      this.botUsername = me.username;

      const now = Date.now();
      for (const channelId of this.config.allowedChannelIds ?? []) {
        this.channelCursorMs.set(channelId, now);
        try {
          const channel = await this.apiGet<MattermostChannel>(`/api/v4/channels/${channelId}`);
          if (channel.type) {
            this.channelTypes.set(channelId, channel.type);
          }
        } catch {
          // Channel metadata fetch is best-effort; continue polling messages.
        }
      }

      const pollIntervalMs = this.config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
      this.pollTimer = setInterval(() => {
        void this.pollOnce();
      }, pollIntervalMs);
      void this.pollOnce();

      this._status = 'connected';
      console.log(`Mattermost adapter connected as ${this.botUsername}`);
    } catch (error) {
      this._status = 'error';
      throw error;
    }
  }

  async disconnect(): Promise<void> {
    if (this.pollTimer) {
      clearInterval(this.pollTimer);
      this.pollTimer = null;
    }
    this.polling = false;
    this.botUserId = '';
    this.botUsername = '';
    this.channelTypes.clear();
    this.channelCursorMs.clear();
    this._status = 'disconnected';
  }

  async send(peerId: string, message: OutboundMessage): Promise<void> {
    if (this._status !== 'connected') {
      throw new Error('Mattermost adapter not connected');
    }

    const text = (message.text ?? '').trim();
    if (text) {
      const chunks = text.length > MAX_MESSAGE_LENGTH ? splitMessage(text, MAX_MESSAGE_LENGTH) : [text];
      for (const chunk of chunks) {
        await this.postMessage(peerId, chunk, message.replyTo);
      }
    }

    if (message.attachments && message.attachments.length > 0) {
      for (const a of message.attachments) {
        if (a.url) {
          const line = a.filename ? `${a.filename}: ${a.url}` : a.url;
          await this.postMessage(peerId, line);
        } else if (a.data) {
          const fileId = await this.uploadAttachment(peerId, a.data, a.filename, a.mimeType);
          await this.postMessageWithFiles(peerId, a.filename ?? 'Attachment uploaded', [fileId]);
        }
      }
    }
  }

  private async postMessage(channelId: string, text: string, rootId?: string): Promise<void> {
    if (!text.trim()) {
      return;
    }
    await this.apiPost('/api/v4/posts', {
      channel_id: channelId,
      message: text,
      ...(rootId ? { root_id: rootId } : {}),
    });
  }

  private async postMessageWithFiles(channelId: string, text: string, fileIds: string[]): Promise<void> {
    await this.apiPost('/api/v4/posts', {
      channel_id: channelId,
      message: text,
      file_ids: fileIds,
    });
  }

  private async uploadAttachment(
    channelId: string,
    base64Data: string,
    filename?: string,
    mimeType?: string,
  ): Promise<string> {
    const form = new FormData();
    const blob = new Blob([Buffer.from(base64Data, 'base64')], {
      type: mimeType || 'application/octet-stream',
    });
    form.append('channel_id', channelId);
    form.append('files', blob, filename ?? 'attachment.bin');

    const res = await fetch(this.makeUrl('/api/v4/files'), {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.config.botToken}`,
      },
      body: form,
    });
    if (!res.ok) {
      throw new Error(`Mattermost POST /api/v4/files failed (${res.status}): ${await res.text()}`);
    }
    const json = await res.json() as MattermostUploadResponse;
    const fileId = json.file_infos?.[0]?.id;
    if (!fileId) {
      throw new Error('Mattermost upload response missing file id');
    }
    return fileId;
  }

  private async pollOnce(): Promise<void> {
    if (this.polling || !this.messageHandler || this._status !== 'connected') {
      return;
    }

    const channels = this.config.allowedChannelIds ?? [];
    if (channels.length === 0) {
      return;
    }

    this.polling = true;
    try {
      for (const channelId of channels) {
        const since = this.channelCursorMs.get(channelId) ?? Date.now();
        const posts = await this.apiGet<MattermostPostsResponse>(`/api/v4/channels/${channelId}/posts?since=${since}`);
        await this.processPosts(channelId, posts);
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      console.warn(`Mattermost polling failed: ${message}`);
    } finally {
      this.polling = false;
    }
  }

  private async processPosts(channelId: string, response: MattermostPostsResponse): Promise<void> {
    if (!this.messageHandler) {
      return;
    }

    const order = response.order ?? [];
    const posts = response.posts ?? {};
    const sorted = order
      .map((id) => posts[id])
      .filter((post): post is MattermostPost => Boolean(post))
      .sort((a, b) => a.create_at - b.create_at);

    let cursor = this.channelCursorMs.get(channelId) ?? Date.now();
    for (const post of sorted) {
      cursor = Math.max(cursor, post.create_at + 1);
      if (post.user_id === this.botUserId) {
        continue;
      }

      const rawText = (post.message ?? '').trim();
      if (!rawText || post.type?.startsWith('system_')) {
        continue;
      }

      const allowedByChannel = isAllowedByAllowlist(post.channel_id, this.config.allowedChannelIds);
      const allowMessage = allowedByChannel || await allowTrustedOrPairedSender({
        pairingManager: this.config.pairingManager,
        channel: 'mattermost',
        senderId: post.user_id,
        text: rawText,
        isTrusted: false,
      });
      if (!allowMessage) {
        continue;
      }

      const isDm = this.isDirectChannel(channelId);
      const mentionsBot = this.isBotMentioned(rawText);
      if (!isDm && shouldIgnoreForMissingMention({
        requireMention: this.config.requireMention,
        defaultRequireMention: true,
        mentionsBot,
      })) {
        continue;
      }

      const text = normalizeResetCommandText(this.stripMention(rawText).trim());
      if (!text) {
        continue;
      }

      if (text === '!reset') {
        this.messageHandler(buildResetInboundMessage({
          id: post.id,
          channel: 'mattermost',
          senderId: post.channel_id,
          senderName: post.user_id,
          timestamp: post.create_at,
        }));
        continue;
      }

      this.messageHandler({
        id: post.id,
        channel: 'mattermost',
        senderId: post.channel_id,
        senderName: post.user_id,
        text,
        timestamp: post.create_at,
        metadata: {
          channelId: post.channel_id,
          senderUserId: post.user_id,
        },
      });
    }

    this.channelCursorMs.set(channelId, cursor);
  }

  private isDirectChannel(channelId: string): boolean {
    const t = this.channelTypes.get(channelId);
    return t === 'D' || t === 'G';
  }

  private isBotMentioned(text: string): boolean {
    const lower = text.toLowerCase();
    const mentionName = (this.config.mentionName ?? 'flynn').trim().toLowerCase();
    if (mentionName && lower.includes(`@${mentionName}`)) {
      return true;
    }
    if (this.botUsername && lower.includes(`@${this.botUsername.toLowerCase()}`)) {
      return true;
    }
    return false;
  }

  private stripMention(text: string): string {
    const mentionNames = [this.config.mentionName ?? 'flynn', this.botUsername].filter(Boolean);
    let cleaned = text;
    for (const name of mentionNames) {
      const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      cleaned = cleaned.replace(new RegExp(`^\\s*@${escaped}(?::|\\b)\\s*`, 'i'), '');
    }
    return cleaned;
  }

  private makeUrl(path: string): string {
    const base = this.config.serverUrl.replace(/\/+$/, '');
    const suffix = path.startsWith('/') ? path : `/${path}`;
    return `${base}${suffix}`;
  }

  private async apiGet<T>(path: string): Promise<T> {
    const res = await fetch(this.makeUrl(path), {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${this.config.botToken}`,
        'Content-Type': 'application/json',
      },
    });
    if (!res.ok) {
      throw new Error(`Mattermost GET ${path} failed (${res.status}): ${await res.text()}`);
    }
    return await res.json() as T;
  }

  private async apiPost(path: string, body: Record<string, unknown>): Promise<void> {
    const res = await fetch(this.makeUrl(path), {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.config.botToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    if (!res.ok) {
      throw new Error(`Mattermost POST ${path} failed (${res.status}): ${await res.text()}`);
    }
  }
}
