import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { ConfigService } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { IntegrationSession } from '@prisma/client';

/**
 * Base class for all chatbot service implementations
 * Contains common methods shared across different chatbot integrations
 */
export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
  protected readonly logger: Logger;
  protected readonly waMonitor: WAMonitoringService;
  protected readonly prismaRepository: PrismaRepository;
  protected readonly configService?: ConfigService;

  constructor(
    waMonitor: WAMonitoringService,
    prismaRepository: PrismaRepository,
    loggerName: string,
    configService?: ConfigService,
  ) {
    this.waMonitor = waMonitor;
    this.prismaRepository = prismaRepository;
    this.logger = new Logger(loggerName);
    this.configService = configService;
  }

  /**
   * Check if a message contains an image
   */
  protected isImageMessage(content: string): boolean {
    return content.includes('imageMessage');
  }

  /**
   * Check if a message contains audio
   */
  protected isAudioMessage(content: string): boolean {
    return content.includes('audioMessage');
  }

  /**
   * Check if a string is valid JSON
   */
  protected isJSON(str: string): boolean {
    try {
      JSON.parse(str);
      return true;
    } catch (e) {
      return false;
    }
  }

  /**
   * Determine the media type from a URL based on its extension
   */
  protected getMediaType(url: string): string | null {
    const extension = url.split('.').pop()?.toLowerCase();
    const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
    const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
    const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
    const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];

    if (imageExtensions.includes(extension || '')) return 'image';
    if (audioExtensions.includes(extension || '')) return 'audio';
    if (videoExtensions.includes(extension || '')) return 'video';
    if (documentExtensions.includes(extension || '')) return 'document';
    return null;
  }

  /**
   * Create a new chatbot session
   */
  public async createNewSession(instance: InstanceDto | any, data: any, type: string) {
    try {
      // Extract pushName safely - if data.pushName is an object with a pushName property, use that
      const pushNameValue =
        typeof data.pushName === 'object' && data.pushName?.pushName
          ? data.pushName.pushName
          : typeof data.pushName === 'string'
            ? data.pushName
            : null;

      // Extract remoteJid safely
      const remoteJidValue =
        typeof data.remoteJid === 'object' && data.remoteJid?.remoteJid ? data.remoteJid.remoteJid : data.remoteJid;

      const session = await this.prismaRepository.integrationSession.create({
        data: {
          remoteJid: remoteJidValue,
          pushName: pushNameValue,
          sessionId: remoteJidValue,
          status: 'opened',
          awaitUser: false,
          botId: data.botId,
          instanceId: instance.instanceId,
          type: type,
        },
      });

      return { session };
    } catch (error) {
      this.logger.error(error);
      return;
    }
  }

  /**
   * Standard implementation for processing incoming messages
   * This handles the common workflow across all chatbot types:
   * 1. Check for existing session or create new one
   * 2. Handle message based on session state
   */
  public async process(
    instance: any,
    remoteJid: string,
    bot: BotType,
    session: IntegrationSession,
    settings: SettingsType,
    content: string,
    pushName?: string,
    msg?: any,
  ): Promise<void> {
    try {
      // For new sessions or sessions awaiting initialization
      if (!session) {
        await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName, msg);
        return;
      }

      // If session is paused, ignore the message
      if (session.status === 'paused') {
        return;
      }

      // For existing sessions, keywords might indicate the conversation should end
      const keywordFinish = (settings as any)?.keywordFinish || '';
      const normalizedContent = content.toLowerCase().trim();
      if (keywordFinish.length > 0 && normalizedContent === keywordFinish.toLowerCase()) {
        // Update session to closed and return
        await this.prismaRepository.integrationSession.update({
          where: {
            id: session.id,
          },
          data: {
            status: 'closed',
          },
        });
        return;
      }

      // Forward the message to the chatbot API
      await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);

      // Update session to indicate we're waiting for user response
      await this.prismaRepository.integrationSession.update({
        where: {
          id: session.id,
        },
        data: {
          status: 'opened',
          awaitUser: true,
        },
      });
    } catch (error) {
      this.logger.error(`Error in process: ${error}`);
      return;
    }
  }

  /**
   * Standard implementation for sending messages to WhatsApp
   * This handles common patterns like markdown links and formatting
   */
  protected async sendMessageWhatsApp(
    instance: any,
    remoteJid: string,
    message: string,
    settings: SettingsType,
  ): Promise<void> {
    if (!message) return;

    const linkRegex = /!?\[(.*?)\]\((.*?)\)/g;
    let textBuffer = '';
    let lastIndex = 0;
    let match: RegExpExecArray | null;

    const splitMessages = (settings as any)?.splitMessages ?? false;

    while ((match = linkRegex.exec(message)) !== null) {
      const [fullMatch, altText, url] = match;
      const mediaType = this.getMediaType(url);
      const beforeText = message.slice(lastIndex, match.index);

      if (beforeText) {
        textBuffer += beforeText;
      }

      if (mediaType) {
        // Send accumulated text before sending media
        if (textBuffer.trim()) {
          await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
          textBuffer = '';
        }

        // Handle sending the media
        try {
          if (mediaType === 'audio') {
            await instance.audioWhatsapp({
              number: remoteJid.split('@')[0],
              delay: (settings as any)?.delayMessage || 1000,
              audio: url,
              caption: altText,
            });
          } else {
            await instance.mediaMessage(
              {
                number: remoteJid.split('@')[0],
                delay: (settings as any)?.delayMessage || 1000,
                mediatype: mediaType,
                media: url,
                caption: altText,
                fileName: mediaType === 'document' ? altText || 'document' : undefined,
              },
              null,
              false,
            );
          }
        } catch (error) {
          this.logger.error(`Error sending media: ${error}`);
          // If media fails, at least send the alt text and URL
          textBuffer += `${altText}: ${url}`;
        }
      } else {
        // It's a regular link, keep it in the text
        textBuffer += fullMatch;
      }

      lastIndex = linkRegex.lastIndex;
    }

    // Add any remaining text after the last match
    if (lastIndex < message.length) {
      const remainingText = message.slice(lastIndex);
      if (remainingText.trim()) {
        textBuffer += remainingText;
      }
    }

    // Send any remaining text
    if (textBuffer.trim()) {
      await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
    }
  }

  /**
   * Helper method to send formatted text with proper typing indicators and delays
   */
  private async sendFormattedText(
    instance: any,
    remoteJid: string,
    text: string,
    settings: any,
    splitMessages: boolean,
  ): Promise<void> {
    const timePerChar = settings?.timePerChar ?? 0;
    const minDelay = 1000;
    const maxDelay = 20000;

    if (splitMessages) {
      const multipleMessages = text.split('\n\n');
      for (let index = 0; index < multipleMessages.length; index++) {
        const message = multipleMessages[index];
        if (!message.trim()) continue;

        const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);

        if (instance.integration === Integration.WHATSAPP_BAILEYS) {
          await instance.client.presenceSubscribe(remoteJid);
          await instance.client.sendPresenceUpdate('composing', remoteJid);
        }

        await new Promise<void>((resolve) => {
          setTimeout(async () => {
            await instance.textMessage(
              {
                number: remoteJid.split('@')[0],
                delay: settings?.delayMessage || 1000,
                text: message,
              },
              false,
            );
            resolve();
          }, delay);
        });

        if (instance.integration === Integration.WHATSAPP_BAILEYS) {
          await instance.client.sendPresenceUpdate('paused', remoteJid);
        }
      }
    } else {
      const delay = Math.min(Math.max(text.length * timePerChar, minDelay), maxDelay);

      if (instance.integration === Integration.WHATSAPP_BAILEYS) {
        await instance.client.presenceSubscribe(remoteJid);
        await instance.client.sendPresenceUpdate('composing', remoteJid);
      }

      await new Promise<void>((resolve) => {
        setTimeout(async () => {
          await instance.textMessage(
            {
              number: remoteJid.split('@')[0],
              delay: settings?.delayMessage || 1000,
              text: text,
            },
            false,
          );
          resolve();
        }, delay);
      });

      if (instance.integration === Integration.WHATSAPP_BAILEYS) {
        await instance.client.sendPresenceUpdate('paused', remoteJid);
      }
    }
  }

  /**
   * Standard implementation for initializing a new session
   * This method should be overridden if a subclass needs specific initialization
   */
  protected async initNewSession(
    instance: any,
    remoteJid: string,
    bot: BotType,
    settings: SettingsType,
    session: IntegrationSession,
    content: string,
    pushName?: string | any,
    msg?: any,
  ): Promise<void> {
    // Create a session if none exists
    if (!session) {
      // Extract pushName properly - if it's an object with pushName property, use that
      const pushNameValue =
        typeof pushName === 'object' && pushName?.pushName
          ? pushName.pushName
          : typeof pushName === 'string'
            ? pushName
            : null;

      const sessionResult = await this.createNewSession(
        {
          instanceName: instance.instanceName,
          instanceId: instance.instanceId,
        },
        {
          remoteJid,
          pushName: pushNameValue,
          botId: (bot as any).id,
        },
        this.getBotType(),
      );

      if (!sessionResult || !sessionResult.session) {
        this.logger.error('Failed to create new session');
        return;
      }

      session = sessionResult.session;
    }

    // Update session status to opened
    await this.prismaRepository.integrationSession.update({
      where: {
        id: session.id,
      },
      data: {
        status: 'opened',
        awaitUser: false,
      },
    });

    // Forward the message to the chatbot
    await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
  }

  /**
   * Get the bot type identifier (e.g., 'dify', 'n8n', 'evoai')
   * This should match the type field used in the IntegrationSession
   */
  protected abstract getBotType(): string;

  /**
   * Send a message to the chatbot API
   * This is specific to each chatbot integration
   */
  protected abstract sendMessageToBot(
    instance: any,
    session: IntegrationSession,
    settings: SettingsType,
    bot: BotType,
    remoteJid: string,
    pushName: string,
    content: string,
    msg?: any,
  ): Promise<void>;
}
