import { Injectable, Logger } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { TelegramApiService, TgMessage, TgCallbackQuery } from './telegram-api.service';
import { SubscriberService } from './subscriber.service';
import { CrmIntegrationService } from './crm-integration.service';
import {
  BotScenario,
  MessageNode,
  ConditionNode,
  ActionNode,
  InputNode,
  DelayNode,
  ReferralNode,
  EngineContext,
} from '../interfaces/scenario.interface';

/**
 * Scenario Engine — executes JSON bot scenarios.
 *
 * Flow:
 *  1. Resolve current node from subscriber state
 *  2. Execute node → get next nodeId
 *  3. Recurse through action/condition nodes (no user input needed)
 *  4. Stop at message/input nodes (wait for user interaction)
 *  5. Save state (currentNodeId + variables)
 */
@Injectable()
export class ScenarioEngineService {
  private readonly logger = new Logger(ScenarioEngineService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly telegramApi: TelegramApiService,
    private readonly subscribers: SubscriberService,
    private readonly crmIntegration: CrmIntegrationService,
    @InjectQueue('telegram-bot-delay')
    private readonly delayQueue: Queue,
    @InjectQueue('telegram-bot-webhook')
    private readonly webhookQueue: Queue,
  ) {}

  // ─── Entry points ──────────────────────────────────────────────

  /**
   * Process an incoming text message or /start command.
   */
  async processMessage(
    bot: { id: string; clubId: string; token: string; username: string | null },
    scenarioJson: unknown,
    subscriberId: string,
    chatId: string,
    currentNodeId: string | null,
    variables: Record<string, unknown>,
    message: TgMessage,
  ): Promise<void> {
    const scenario = scenarioJson as BotScenario;
    if (!scenario?.nodes || !scenario.startNodeId) return;

    const ctx = this.buildContext(bot, scenario, subscriberId, chatId, variables);
    const text = message.text ?? '';

    // If /start — reset to startNode (or a specific node if provided as payload)
    if (text.startsWith('/start')) {
      const payload = text.replace('/start', '').trim();
      // Allow direct node navigation (e.g. friendStartNode)
      const targetNode = payload && scenario.nodes[payload] ? payload : scenario.startNodeId;
      await this.executeFromNode(ctx, targetNode);
      return;
    }

    // If subscriber is waiting at an input node
    if (currentNodeId) {
      const node = scenario.nodes[currentNodeId];
      if (node?.type === 'input') {
        await this.handleInputResponse(ctx, node, message);
        return;
      }
    }

    // If subscriber is at a message node with reply buttons — try matching
    if (currentNodeId) {
      const node = scenario.nodes[currentNodeId];
      if (node?.type === 'message' && node.data.buttonsLayout === 'reply') {
        const matched = node.data.buttons?.find((b) => b.text === text);
        if (matched?.action === 'goto' && matched.target) {
          await this.executeFromNode(ctx, matched.target);
          return;
        }
      }
    }

    // Default: re-execute current node or start from beginning
    await this.executeFromNode(ctx, currentNodeId ?? scenario.startNodeId);
  }

  /**
   * Process a callback_query (inline button press).
   */
  async processCallback(
    bot: { id: string; clubId: string; token: string; username: string | null },
    scenarioJson: unknown,
    subscriberId: string,
    chatId: string,
    variables: Record<string, unknown>,
    query: TgCallbackQuery,
  ): Promise<void> {
    const scenario = scenarioJson as BotScenario;
    if (!scenario?.nodes || !query.data) return;

    const ctx = this.buildContext(bot, scenario, subscriberId, chatId, variables);

    // callback_data format: "goto:nodeId" or "value:varName:val"
    const parts = query.data.split(':');

    if (parts[0] === 'goto' && parts[1]) {
      await this.executeFromNode(ctx, parts[1]);
    } else if (parts[0] === 'value' && parts[1] && parts[2]) {
      ctx.variables[parts[1]] = parts[2];
      // Find current input node's next
      const currentSub = await this.prisma.tgBotSubscriber.findUnique({
        where: { id: subscriberId },
        select: { currentNodeId: true },
      });
      const node = currentSub?.currentNodeId ? scenario.nodes[currentSub.currentNodeId] : null;
      if (node?.next) {
        await this.executeFromNode(ctx, node.next);
      }
    }
  }

  // ─── Core execution loop ──────────────────────────────────────

  /**
   * Execute nodes starting from nodeId. Recurses through
   * action/condition nodes, stops at message/input/delay.
   */
  private async executeFromNode(ctx: EngineContext, nodeId: string, depth = 0): Promise<void> {
    if (depth > 50) {
      this.logger.warn(`Max depth reached for subscriber ${ctx.subscriberId}`);
      return;
    }

    // Don't send messages to unsubscribed users
    if (depth === 0) {
      const sub = await this.prisma.tgBotSubscriber.findUnique({
        where: { id: ctx.subscriberId },
        select: { isSubscribed: true },
      });
      if (sub && !sub.isSubscribed) return;
    }

    const node = ctx.scenario.nodes[nodeId];
    if (!node) {
      this.logger.warn(`Node "${nodeId}" not found in scenario`);
      await this.saveState(ctx, null);
      return;
    }

    switch (node.type) {
      case 'message':
        await this.executeMessage(ctx, node);
        break;
      case 'condition':
        await this.executeCondition(ctx, node, depth);
        break;
      case 'action':
        await this.executeAction(ctx, node, depth);
        break;
      case 'input':
        await this.executeInput(ctx, node);
        break;
      case 'delay':
        await this.executeDelay(ctx, node);
        break;
      case 'referral':
        await this.executeReferral(ctx, node, depth);
        break;
    }
  }

  // ─── Node handlers ─────────────────────────────────────────────

  private async executeMessage(ctx: EngineContext, node: MessageNode): Promise<void> {
    const text = this.interpolate(node.data.text, ctx);
    const buttons = node.data.buttons ?? [];
    const layout = node.data.buttonsLayout ?? 'inline';

    let reply_markup: Record<string, unknown> | undefined;

    if (buttons.length > 0) {
      if (layout === 'inline') {
        reply_markup = this.telegramApi.buildInlineKeyboard(
          buttons.map((b) => ({
            text: this.interpolate(b.text, ctx),
            ...(b.action === 'goto' ? { callback_data: `goto:${b.target}` } : {}),
            ...(b.action === 'url' ? { url: this.interpolate(b.target ?? '', ctx) } : {}),
          })),
        );
      } else if (layout === 'reply') {
        reply_markup = this.telegramApi.buildReplyKeyboard(
          buttons.map((b) => ({
            text: this.interpolate(b.text, ctx),
            ...(b.action === 'share_contact' ? { request_contact: true } : {}),
          })),
        );
      } else if (layout === 'remove') {
        reply_markup = this.telegramApi.removeKeyboard();
      }
    }

    const sent = await this.telegramApi.sendMessage(ctx.botToken, {
      chat_id: ctx.chatId,
      text,
      parse_mode: 'HTML',
      reply_markup,
    });

    await this.logMessage(ctx, 'OUT', text, node.id, sent.message_id);

    // If no buttons or buttons with auto-next → save state and wait
    // If node.next is set and no buttons, auto-advance
    if (node.next && buttons.length === 0) {
      await this.executeFromNode(ctx, node.next);
    } else {
      // Wait for user interaction
      await this.saveState(ctx, node.id);
    }
  }

  private async executeCondition(
    ctx: EngineContext,
    node: ConditionNode,
    depth: number,
  ): Promise<void> {
    for (const rule of node.data.rules) {
      if (this.evalCondition(rule.condition, ctx.variables)) {
        await this.executeFromNode(ctx, rule.target, depth + 1);
        return;
      }
    }

    // No rule matched — use default
    if (node.data.default) {
      await this.executeFromNode(ctx, node.data.default, depth + 1);
    } else if (node.next) {
      await this.executeFromNode(ctx, node.next, depth + 1);
    } else {
      await this.saveState(ctx, null);
    }
  }

  private async executeAction(ctx: EngineContext, node: ActionNode, depth: number): Promise<void> {
    for (const action of node.data.actions) {
      switch (action.type) {
        case 'set_variable':
          ctx.variables[action.variable] = action.value;
          break;

        case 'webhook': {
          const url = this.interpolate(action.url, ctx);
          const body: Record<string, unknown> = action.body
            ? (JSON.parse(this.interpolate(JSON.stringify(action.body), ctx)) as Record<
                string,
                unknown
              >)
            : {};
          await this.webhookQueue.add(
            'deliver',
            {
              url,
              method: action.method ?? 'POST',
              body,
              headers: action.headers ?? {},
            } as Record<string, unknown>,
            { attempts: 3, backoff: { type: 'exponential', delay: 5000 } },
          );
          break;
        }

        case 'grant_bonus':
          await this.prisma.tgBotSubscriber.update({
            where: { id: ctx.subscriberId },
            data: { bonusGranted: true },
          });
          break;

        case 'set_consent':
          await this.prisma.tgBotSubscriber.update({
            where: { id: ctx.subscriberId },
            data: { mailingConsent: action.mailingConsent },
          });
          break;

        case 'crm_deal': {
          const dealFields: Record<string, string> = {};
          for (const [k, v] of Object.entries(action.fields)) {
            dealFields[k] = this.interpolate(String(v), ctx);
          }
          const deal = await this.crmIntegration.createDeal(ctx.clubId, {
            title: dealFields.title ?? dealFields.name,
            contactName: dealFields.contactName ?? dealFields.name,
            contactPhone: dealFields.phone ?? String(ctx.variables['phone'] ?? ''),
            contactTelegram: ctx.chatId,
            pipelineId: action.pipeline ? this.interpolate(action.pipeline, ctx) : undefined,
            source: 'telegram_bot',
            metadata: { botId: ctx.botId, subscriberId: ctx.subscriberId },
          });
          if (deal) {
            ctx.variables['crm_deal_id'] = deal.id;
            ctx.variables['crm_deal_number'] = deal.number;
          }
          break;
        }
      }
    }

    // Auto-advance to next node
    if (node.next) {
      await this.executeFromNode(ctx, node.next, depth + 1);
    } else {
      await this.saveState(ctx, null);
    }
  }

  private async executeInput(ctx: EngineContext, node: InputNode): Promise<void> {
    const prompt = this.interpolate(node.data.prompt, ctx);

    let reply_markup: Record<string, unknown> | undefined;

    if (node.data.requestContact) {
      reply_markup = this.telegramApi.buildReplyKeyboard(
        [{ text: '📱 Поделиться контактом', request_contact: true }],
        { one_time_keyboard: true },
      );
    } else if (node.data.choices?.length) {
      reply_markup = this.telegramApi.buildInlineKeyboard(
        node.data.choices.map((c) => ({
          text: c.text,
          callback_data: `value:${node.data.saveAs}:${c.value}`,
        })),
      );
    }

    const sent = await this.telegramApi.sendMessage(ctx.botToken, {
      chat_id: ctx.chatId,
      text: prompt,
      parse_mode: 'HTML',
      reply_markup,
    });

    await this.logMessage(ctx, 'OUT', prompt, node.id, sent.message_id);
    await this.saveState(ctx, node.id);
  }

  private async handleInputResponse(
    ctx: EngineContext,
    node: InputNode,
    message: TgMessage,
  ): Promise<void> {
    let value: string;

    if (node.data.inputType === 'phone' && message.contact?.phone_number) {
      value = message.contact.phone_number;
      await this.subscribers.updatePhone(ctx.subscriberId, value);
    } else {
      value = message.text ?? '';
    }

    // Validate
    if (node.data.validation) {
      const regex = new RegExp(node.data.validation);
      if (!regex.test(value)) {
        const errorMsg = node.data.errorMessage ?? 'Неверный формат. Попробуйте ещё раз.';
        await this.telegramApi.sendMessage(ctx.botToken, {
          chat_id: ctx.chatId,
          text: errorMsg,
        });
        return; // Stay on same node
      }
    }

    // Save variable
    ctx.variables[node.data.saveAs] = value;

    // Advance to next node
    if (node.next) {
      await this.executeFromNode(ctx, node.next);
    } else {
      await this.saveState(ctx, null);
    }
  }

  private async executeDelay(ctx: EngineContext, node: DelayNode): Promise<void> {
    const delayMs = this.parseDuration(node.data.duration);

    await this.delayQueue.add(
      'delayed-node',
      {
        botId: ctx.botId,
        subscriberId: ctx.subscriberId,
        chatId: ctx.chatId,
        nodeId: node.id,
        nextNodeId: node.next ?? null,
        cancelOn: node.data.cancelOn ?? null,
        message: node.data.message ?? null,
        variables: ctx.variables,
      },
      {
        delay: delayMs,
        attempts: 1,
        removeOnComplete: true,
      },
    );

    // Save state pointing to delay node
    await this.saveState(ctx, node.id);
  }

  private async executeReferral(
    ctx: EngineContext,
    node: ReferralNode,
    depth: number,
  ): Promise<void> {
    if (node.data.generateLink) {
      const link = `https://t.me/${ctx.botUsername}?start=ref_${ctx.subscriberId}`;
      ctx.variables['referral_link'] = link;
    }

    // Check if target already reached
    const currentCount = Number(ctx.variables[node.data.trackVariable] ?? 0);
    if (currentCount >= node.data.targetCount && node.data.onReach) {
      await this.executeFromNode(ctx, node.data.onReach, depth + 1);
      return;
    }

    // Continue to next node (typically: send referral link message)
    if (node.next) {
      await this.executeFromNode(ctx, node.next, depth + 1);
    } else {
      await this.saveState(ctx, node.id);
    }
  }

  // ─── Helpers ───────────────────────────────────────────────────

  private buildContext(
    bot: { id: string; clubId: string; token: string; username: string | null },
    scenario: BotScenario,
    subscriberId: string,
    chatId: string,
    rawVariables: Record<string, unknown>,
  ): EngineContext {
    // Merge defaults with current variables
    const variables: Record<string, string | number | boolean> = {};
    for (const [key, def] of Object.entries(scenario.variables ?? {})) {
      variables[key] = def.default;
    }
    for (const [key, val] of Object.entries(rawVariables)) {
      if (val !== null && val !== undefined) {
        variables[key] = val as string | number | boolean;
      }
    }

    return {
      botId: bot.id,
      botToken: bot.token,
      botUsername: bot.username ?? '',
      clubId: bot.clubId,
      subscriberId,
      chatId,
      variables,
      scenario,
    };
  }

  /** Replace {{variable}} placeholders in text */
  private interpolate(text: string, ctx: EngineContext): string {
    return text.replace(/\{\{(\w+)\}\}/g, (_, key: string) => {
      if (key in ctx.variables) return String(ctx.variables[key]);
      return `{{${key}}}`;
    });
  }

  /** Evaluate a simple condition expression against variables */
  private evalCondition(expr: string, vars: Record<string, string | number | boolean>): boolean {
    try {
      // Parse simple expressions: "var >= 2", "var == 'value'", "var != false"
      const match = expr.match(/^(\w+)\s*(==|!=|>=|<=|>|<)\s*(.+)$/);
      if (!match) return false;

      const [, varName, op, rawRight] = match;
      const left = vars[varName];
      if (left === undefined) return false;

      // Parse right side
      let right: string | number | boolean = rawRight.trim();
      if (right === 'true') right = true;
      else if (right === 'false') right = false;
      else if (/^['"].*['"]$/.test(right)) right = right.slice(1, -1);
      else if (!isNaN(Number(right))) right = Number(right);

      const leftNum = typeof left === 'number' ? left : Number(left);
      const rightNum = typeof right === 'number' ? right : Number(right);

      switch (op) {
        case '==':
          return left == right;
        case '!=':
          return left != right;
        case '>=':
          return leftNum >= rightNum;
        case '<=':
          return leftNum <= rightNum;
        case '>':
          return leftNum > rightNum;
        case '<':
          return leftNum < rightNum;
        default:
          return false;
      }
    } catch {
      return false;
    }
  }

  /** Parse duration string to milliseconds */
  private parseDuration(duration: string): number {
    const match = duration.match(/^(\d+)(m|h|d)$/);
    if (!match) return 60000; // default 1 min

    const amount = parseInt(match[1], 10);
    switch (match[2]) {
      case 'm':
        return amount * 60 * 1000;
      case 'h':
        return amount * 60 * 60 * 1000;
      case 'd':
        return amount * 24 * 60 * 60 * 1000;
      default:
        return 60000;
    }
  }

  private async saveState(ctx: EngineContext, nodeId: string | null): Promise<void> {
    await this.subscribers.updateState(ctx.subscriberId, nodeId, ctx.variables);
  }

  private async logMessage(
    ctx: EngineContext,
    direction: 'IN' | 'OUT',
    text: string,
    nodeId: string | null,
    telegramMsgId?: number,
  ): Promise<void> {
    await this.prisma.tgBotMessage.create({
      data: {
        botId: ctx.botId,
        subscriberId: ctx.subscriberId,
        direction,
        text,
        nodeId,
        telegramMsgId: telegramMsgId ?? null,
        payload: Prisma.JsonNull,
      },
    });
  }
}
