import { execFile } from 'node:child_process';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import type { IncomingMessage, ServerResponse } from 'http';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';

import type {
  InboundMessage,
  OutboundMessage,
  ChannelAdapter,
  ChannelStatus,
} from '../types.js';
import { shouldIgnoreForMissingMention, splitMessage } from '../utils.js';
import { readRequestBody } from '../../utils/httpBody.js';
import { backupInternals } from '../../backup/index.js';

const execFileAsync = promisify(execFile);

type ExecRunner = (file: string, args: string[], options?: { env?: NodeJS.ProcessEnv }) => Promise<{ stdout: string; stderr: string }>;

export interface MinioShareConfig {
  enabled: boolean;
  endpoint: string;
  accessKey: string;
  secretKey: string;
  bucket: string;
  prefix?: string;
  secure?: boolean;
  expires?: string;
  mcPath?: string;
}

export interface ZaloAdapterConfig {
  oaAccessToken: string;
  endpoint?: string;
  webhookToken?: string;
  allowedUserIds?: string[];
  requireMention?: boolean;
  mentionName?: string;
  minio?: MinioShareConfig;
  minioExecRunner?: ExecRunner;
}

interface ZaloWebhookEvent {
  token?: string;
  sender?: { id?: string };
  recipient?: { id?: string };
  message?: {
    msg_id?: string;
    text?: string;
  };
  timestamp?: number;
}

const MAX_MESSAGE_LENGTH = 3500;

export class ZaloAdapter implements ChannelAdapter {
  readonly name = 'zalo';
  private _status: ChannelStatus = 'disconnected';
  private messageHandler?: (msg: InboundMessage) => void;

  constructor(private readonly config: ZaloAdapterConfig) {}

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

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

  async connect(): Promise<void> {
    this._status = 'connected';
  }

  async disconnect(): Promise<void> {
    this._status = 'disconnected';
  }

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

    const text = message.text.trim();
    const attachments = message.attachments ?? [];
    if (!text && attachments.length === 0) {
      return;
    }

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

    for (const attachment of attachments) {
      if (attachment.url) {
        const line = attachment.filename ? `${attachment.filename}: ${attachment.url}` : attachment.url;
        await this.sendText(peerId, line);
        continue;
      }
      if (attachment.data) {
        const minioUrl = await this.uploadBinaryAttachmentToMinio(attachment.data, attachment.filename, attachment.mimeType);
        if (minioUrl) {
          const line = attachment.filename ? `${attachment.filename}: ${minioUrl}` : minioUrl;
          await this.sendText(peerId, line);
        } else {
          console.warn(`Zalo: skipping attachment data (${attachment.mimeType}) — upload not implemented`);
          await this.sendText(peerId, formatBinaryAttachmentNotice('Zalo', attachment.filename, attachment.mimeType));
        }
      }
    }
  }

  private async uploadBinaryAttachmentToMinio(base64Data: string, filename?: string, mimeType?: string): Promise<string | null> {
    const minio = this.config.minio;
    if (!minio?.enabled || !minio.endpoint || !minio.accessKey || !minio.secretKey || !minio.bucket) {
      return null;
    }

    const tempDir = await mkdtemp(join(tmpdir(), 'flynn-zalo-'));
    const safeName = sanitizeFilename(filename) || inferFilenameFromMimeType(mimeType);
    const localPath = join(tempDir, `${Date.now()}_${safeName}`);
    const alias = 'flynnzalo';
    const objectKey = backupInternals.buildObjectKey(minio.prefix ?? 'channels/zalo', `${Date.now()}_${safeName}`);
    const remotePath = `${alias}/${minio.bucket}/${objectKey}`;
    const host = backupInternals.buildMinioHost({
      endpoint: minio.endpoint,
      accessKey: minio.accessKey,
      secretKey: minio.secretKey,
      secure: minio.secure ?? true,
    });
    const env = { ...process.env, [`MC_HOST_${alias}`]: host };
    const runner = this.config.minioExecRunner ?? (async (file: string, args: string[], options?: { env?: NodeJS.ProcessEnv }) => {
      return execFileAsync(file, args, options);
    });

    try {
      await writeFile(localPath, Buffer.from(base64Data, 'base64'));
      await runner(minio.mcPath ?? 'mc', ['mb', '--ignore-existing', `${alias}/${minio.bucket}`], { env });
      await runner(minio.mcPath ?? 'mc', ['cp', localPath, remotePath], { env });
      const { stdout } = await runner(
        minio.mcPath ?? 'mc',
        ['share', 'download', '--json', '--expire', minio.expires ?? '24h', remotePath],
        { env },
      );
      const shareUrl = parseShareUrl(typeof stdout === 'string' ? stdout : stdout.toString('utf-8'));
      if (!shareUrl) {
        return null;
      }
      return shareUrl;
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      console.warn(`Zalo: MinIO upload failed (${message})`);
      return null;
    } finally {
      await rm(tempDir, { recursive: true, force: true });
    }
  }

  async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
    let body = '';
    try {
      body = await readRequestBody(req, { maxBytes: 1_048_576 });
    } catch {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Invalid request body' }));
      return;
    }

    let payload: ZaloWebhookEvent;
    try {
      payload = JSON.parse(body) as ZaloWebhookEvent;
    } catch {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Invalid JSON' }));
      return;
    }

    if (this.config.webhookToken) {
      const headerToken = req.headers['x-zalo-token'];
      const token = typeof headerToken === 'string' ? headerToken : payload.token;
      if (token !== this.config.webhookToken) {
        res.writeHead(401, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Invalid webhook token' }));
        return;
      }
    }

    await this.handleEvent(payload);
    res.writeHead(202, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ accepted: true }));
  }

  async handleEvent(payload: ZaloWebhookEvent): Promise<void> {
    if (!this.messageHandler) {
      return;
    }
    const senderId = payload.sender?.id?.trim();
    if (!senderId) {
      return;
    }

    if (this.config.allowedUserIds && this.config.allowedUserIds.length > 0) {
      if (!this.config.allowedUserIds.includes(senderId)) {
        return;
      }
    }

    const text = (payload.message?.text ?? '').trim();
    if (!text) {
      return;
    }

    const mentionName = this.config.mentionName ?? 'flynn';
    const mentionRegex = new RegExp(`(?:^|\\s)@?${escapeRegex(mentionName)}(?:\\b|:)`, 'i');
    if (shouldIgnoreForMissingMention({
      requireMention: this.config.requireMention,
      defaultRequireMention: true,
      mentionsBot: mentionRegex.test(text),
    })) {
      return;
    }

    const cleaned = text.replace(new RegExp(`^\\s*@?${escapeRegex(mentionName)}(?:\\b|:)\\s*`, 'i'), '').trim();
    if (!cleaned) {
      return;
    }

    this.messageHandler({
      id: payload.message?.msg_id ?? `zalo-${Date.now()}`,
      channel: 'zalo',
      senderId,
      text: cleaned,
      timestamp: typeof payload.timestamp === 'number' ? payload.timestamp : Date.now(),
      metadata: {
        replyPeerId: senderId,
        recipientId: payload.recipient?.id,
      },
    });
  }

  private async sendText(userId: string, text: string): Promise<void> {
    const endpoint = `${(this.config.endpoint ?? 'https://openapi.zalo.me').replace(/\/+$/, '')}/v3.0/oa/message/cs`;
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        access_token: this.config.oaAccessToken,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        recipient: { user_id: userId },
        message: { text },
      }),
    });
    if (!response.ok) {
      const body = await response.text().catch(() => '');
      throw new Error(`Zalo send failed (${response.status}): ${body}`);
    }
  }
}

function escapeRegex(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function formatBinaryAttachmentNotice(channel: string, filename?: string, mimeType?: string): string {
  const name = filename || 'attachment';
  const type = mimeType || 'application/octet-stream';
  return `[${channel}] Binary attachment not uploaded yet: ${name} (${type}).`;
}

function sanitizeFilename(filename?: string): string {
  if (!filename) {
    return '';
  }
  return filename.replace(/[^a-zA-Z0-9._-]/g, '_');
}

function inferFilenameFromMimeType(mimeType?: string): string {
  if (!mimeType) {
    return 'attachment.bin';
  }
  if (mimeType.startsWith('image/jpeg')) {
    return 'attachment.jpg';
  }
  if (mimeType.startsWith('image/png')) {
    return 'attachment.png';
  }
  if (mimeType.startsWith('application/pdf')) {
    return 'attachment.pdf';
  }
  const [, subtype] = mimeType.split('/');
  if (!subtype) {
    return 'attachment.bin';
  }
  return `attachment.${subtype.split('+')[0]}`;
}

function parseShareUrl(stdout: string): string | null {
  const lines = stdout.split('\n').map((line) => line.trim()).filter(Boolean);
  for (const line of lines) {
    try {
      const parsed = JSON.parse(line) as Record<string, unknown>;
      const share = typeof parsed.share === 'string' ? parsed.share : undefined;
      const url = typeof parsed.url === 'string' ? parsed.url : undefined;
      if (share) {return share;}
      if (url) {return url;}
    } catch {
      const match = line.match(/https?:\/\/\S+/);
      if (match) {
        return match[0];
      }
    }
  }
  return null;
}
