import { NextResponse } from 'next/server';
import { withErrorHandler } from '@server/middleware/withErrorHandler';
import { childLogger } from '@server/logger';
const log = childLogger('route.widget.message');

import {
  verifyWidgetToken,
  appendWidgetMessage,
  getWidgetSettings,
  updateWidgetConversationStatus,
} from '@server/services/widget.service';
import { sendAgentMessage } from '@server/integrations/ai/restaurantChatAgent';
import { filterOrderableItems } from '@server/services/menu.service';
import { resolveApiKey } from '@server/services/provider-keys.service';
import { db, conversations, aiAgents, llmModels, aiAgentConfigs, menuItems as menuItemsTable, restaurants, branches } from '@server/db/drizzle';
import { eq, and, sql } from 'drizzle-orm';
import { initDatabase } from '@server/db/init';
import { DEFAULT_LLM_MODEL } from '@server/config/ai-defaults';
import { withWidgetCors, widgetOptionsResponse } from '../_cors';
import { createNotification } from '@server/services/notifications.service';
import { broadcastEscalation } from '@server/utils/escalationBus';
import { hasPlanFeature, getPlanLimits } from '@server/utils/features';
import { getUsage } from '@server/services/billing.service';
import { withRateLimit, getClientIp } from '@server/middleware/withRateLimit';

export async function OPTIONS() { return widgetOptionsResponse(); }

const WIDGET_MAX_MESSAGES_PER_CONVERSATION = 50;

export const POST = withWidgetCors(
  withRateLimit(
    [
      {
        scope: 'widget:message:ip',
        limit: 120,
        windowMs: 60 * 60 * 1000,
        keyOf: (req) => getClientIp(req),
      },
      {
        scope: 'widget:message:ip:short',
        limit: 20,
        windowMs: 60 * 1000,
        keyOf: (req) => getClientIp(req),
      },
    ],
    withErrorHandler(async (req: Request) => {
  const body = await req.json().catch(() => ({}));
  const { sessionToken, message, history } = body as {
    sessionToken?: string;
    message?: string;
    history?: Array<{ role: string; content: string }>;
  };

  if (!sessionToken || !message) {
    return NextResponse.json({ error: 'sessionToken and message are required' }, { status: 400 });
  }

  const session = verifyWidgetToken(sessionToken);
  if (!session) {
    return NextResponse.json({ error: 'Invalid or expired session token' }, { status: 401 });
  }

  const { restaurantId, conversationId } = session;

  await initDatabase();

  // Enforce chat_agent plan feature and conversations_per_month cap on every
  // message so a token minted before the limit was reached cannot be reused
  // after the restaurant has hit its monthly ceiling.
  const [chatEnabled, limits] = await Promise.all([
    hasPlanFeature(restaurantId, 'chat_agent'),
    getPlanLimits(restaurantId),
  ]);
  if (!chatEnabled) {
    return NextResponse.json(
      { error: 'Chat agent is not included in this restaurant\'s current plan.' },
      { status: 402 }
    );
  }
  if (limits.conversations_per_month !== null) {
    const usage = await getUsage(restaurantId);
    // Use strict greater-than (not >=) so a conversation created at the
    // exact plan limit (count === limit) can still exchange messages.
    // The /start route already enforced >= on creation so no new session
    // can be started once the cap is reached — this check only catches
    // tokens minted before the ceiling was hit but used after further
    // conversations pushed the count strictly over the limit.
    if (usage.conversations > limits.conversations_per_month) {
      return NextResponse.json(
        {
          error: `Monthly conversation limit of ${limits.conversations_per_month} reached.`,
          code: 'PLAN_LIMIT_EXCEEDED',
        },
        { status: 402 }
      );
    }
  }

  // Enforce per-conversation message cap to prevent a single session from
  // generating unlimited AI completions (unbounded cost / spam abuse).
  const { rows: convCountRows } = await db.execute(sql`
    SELECT jsonb_array_length(COALESCE(messages, '[]'::jsonb)) AS msg_count
    FROM conversations
    WHERE id = ${conversationId} AND restaurant_id = ${restaurantId}
  `);
  const msgCount = Number((convCountRows[0] as { msg_count?: number } | undefined)?.msg_count ?? 0);
  if (msgCount >= WIDGET_MAX_MESSAGES_PER_CONVERSATION) {
    return NextResponse.json(
      {
        error: 'This conversation has reached its message limit. Please start a new chat.',
        code: 'MESSAGE_LIMIT_EXCEEDED',
      },
      { status: 429 }
    );
  }

  const settings = await getWidgetSettings(restaurantId);
  if (!settings?.is_enabled) {
    return NextResponse.json({ error: 'Chat widget is not enabled' }, { status: 403 });
  }

  await appendWidgetMessage(conversationId, restaurantId, 'customer', message);

  interface AgentRow {
    agent_id: string;
    system_prompt: string | null;
    greeting_script: string | null;
    closing_script: string | null;
    fallback_rules: Array<{ trigger: string; action: string; priority: string }> | null;
    model_id: string | null;
    agent_name: string | null;
    branch_id: string | null;
    capabilities: Record<string, boolean> | null;
    menu_category_ids: string[] | null;
  }

  /* raw: SELECT agent from ai_agents with JOIN on llm_models — conditional on widget agent_id */
  const agentResult = settings.agent_id
    ? await db.execute(sql`
        SELECT a.id AS agent_id, a.system_prompt, a.greeting_script, a.closing_script, a.fallback_rules,
               lm.model_id, a.name as agent_name, a.branch_id, a.capabilities, a.menu_category_ids
        FROM ai_agents a
        LEFT JOIN llm_models lm ON lm.id = a.llm_model_id
        WHERE a.id = ${settings.agent_id} AND a.restaurant_id = ${restaurantId} AND a.is_active = true
      `)
    : await db.execute(sql`
        SELECT a.id AS agent_id, a.system_prompt, a.greeting_script, a.closing_script, a.fallback_rules,
               lm.model_id, a.name as agent_name, a.branch_id, a.capabilities, a.menu_category_ids
        FROM ai_agents a
        LEFT JOIN llm_models lm ON lm.id = a.llm_model_id
        WHERE a.restaurant_id = ${restaurantId} AND a.is_active = true AND 'chat' = ANY(a.channels)
        ORDER BY a.is_default DESC, a.created_at LIMIT 1
      `);

  const chatAgentRow = (agentResult.rows[0] as unknown as AgentRow | undefined) ?? null;

  const [totalAgentsResult, legacyConfigResult, menuResult, restaurantResult, branchResult] = await Promise.all([
    db.execute(sql`SELECT COUNT(*) AS count FROM ai_agents WHERE restaurant_id = ${restaurantId} AND is_active = true`),
    db.execute(sql`SELECT * FROM ai_agent_configs WHERE restaurant_id = ${restaurantId} LIMIT 1`),
    db.execute(sql`SELECT mi.id, mi.name, mi.price, mi.description, mi.category_id, mc.name AS category FROM menu_items mi LEFT JOIN menu_categories mc ON mc.id = mi.category_id WHERE mi.restaurant_id = ${restaurantId} AND mi.is_available = true LIMIT 80`),
    db.execute(sql`SELECT name FROM restaurants WHERE id = ${restaurantId} LIMIT 1`),
    db.execute(sql`SELECT name, phone, address, hours, timezone FROM branches WHERE restaurant_id = ${restaurantId} AND is_active = true LIMIT 5`),
  ]);

  const totalAgentsRow = totalAgentsResult.rows[0] as { count: string } | undefined;
  const legacyConfig = legacyConfigResult.rows[0] as Record<string, unknown> | undefined;
  const hasAnyAgents = parseInt(totalAgentsRow?.count ?? '0', 10) > 0;

  let agentConfig: {
    model: string;
    systemPrompt?: string;
    responseStyle?: string;
    greetingScript?: string;
    closingScript?: string;
    fallbackRules?: Array<{ trigger: string; action: string; priority: string }>;
  } | null = null;

  if (chatAgentRow) {
    agentConfig = {
      model: chatAgentRow.model_id || (legacyConfig?.model as string) || DEFAULT_LLM_MODEL,
      systemPrompt: chatAgentRow.system_prompt ?? (legacyConfig?.system_prompt as string),
      responseStyle: (legacyConfig?.response_style as string) || 'friendly',
      greetingScript: chatAgentRow.greeting_script ?? (legacyConfig?.greeting_script as string),
      closingScript: chatAgentRow.closing_script ?? (legacyConfig?.closing_script as string),
      fallbackRules: Array.isArray(chatAgentRow.fallback_rules)
        ? chatAgentRow.fallback_rules
        : (Array.isArray(legacyConfig?.fallback_rules) ? legacyConfig.fallback_rules as Array<{ trigger: string; action: string; priority: string }> : []),
    };
  } else if (!hasAnyAgents && legacyConfig) {
    agentConfig = {
      model: (legacyConfig.model as string) || DEFAULT_LLM_MODEL,
      systemPrompt: legacyConfig.system_prompt as string,
      responseStyle: (legacyConfig.response_style as string) || 'friendly',
      greetingScript: legacyConfig.greeting_script as string,
      closingScript: legacyConfig.closing_script as string,
      fallbackRules: Array.isArray(legacyConfig.fallback_rules) ? legacyConfig.fallback_rules as Array<{ trigger: string; action: string; priority: string }> : [],
    };
  } else {
    agentConfig = { model: DEFAULT_LLM_MODEL, responseStyle: 'friendly' };
  }

  interface MenuItemRow { id: string; name: string; price: number; description: string; category_id: string | null; category: string | null }
  interface BranchRow {
    name: string;
    phone: string | null;
    address: string | null;
    hours: Record<string, { open: string; close: string; closed?: boolean }> | null;
    timezone: string | null;
  }

  const DAY_NAMES: Record<string, string> = {
    monday: 'Monday', tuesday: 'Tuesday', wednesday: 'Wednesday',
    thursday: 'Thursday', friday: 'Friday', saturday: 'Saturday', sunday: 'Sunday',
  };

  function formatBranchHours(hours: BranchRow['hours'], timezone: string | null): string | null {
    if (!hours || typeof hours !== 'object') return null;
    const lines: string[] = [];
    for (const day of ['monday','tuesday','wednesday','thursday','friday','saturday','sunday']) {
      const slot = hours[day];
      if (!slot) continue;
      if (slot.closed) {
        lines.push(`${DAY_NAMES[day]}: Closed`);
      } else if (slot.open && slot.close) {
        lines.push(`${DAY_NAMES[day]}: ${slot.open} – ${slot.close}`);
      }
    }
    if (lines.length === 0) return null;
    const tzNote = timezone ? ` (${timezone})` : '';
    return `Working Hours${tzNote}:\n${lines.join(', ')}`;
  }

  const branchData = branchResult.rows as unknown as BranchRow[];
  let contactInfo: string | undefined;
  if (branchData.length > 0) {
    const lines = ['--- Restaurant Contact Info ---'];
    branchData.forEach(b => {
      const parts = [`Branch: ${b.name}`];
      if (b.phone) parts.push(`Phone: ${b.phone}`);
      if (b.address) parts.push(`Address: ${b.address}`);
      lines.push(parts.join(' | '));
      const hoursStr = formatBranchHours(b.hours, b.timezone);
      if (hoursStr) lines.push(hoursStr);
    });
    lines.push('--- End Contact Info ---');
    contactInfo = lines.join('\n');
  }

  // Filter the menu blob the LLM sees so disabled / out-of-stock /
  // off-schedule / wrong-branch items disappear automatically. Single batch
  // DB query; runs against the chat agent's branch (or restaurant scope when
  // no branch is configured).
  const branchIdForAgent = chatAgentRow?.branch_id ?? null;
  const allowedCats = Array.isArray(chatAgentRow?.menu_category_ids) && chatAgentRow!.menu_category_ids!.length
    ? new Set(chatAgentRow!.menu_category_ids!) : null;
  const allItems = (menuResult.rows as unknown as MenuItemRow[])
    .filter(r => !allowedCats || (r.category_id && allowedCats.has(r.category_id)))
    .map(r => ({
      id: r.id,
      name: r.name,
      price: r.price,
      description: r.description,
      category: r.category ?? undefined,
    }));
  const orderableItems = await filterOrderableItems(allItems, branchIdForAgent);
  const menuContext = {
    items: orderableItems,
    contactInfo,
  };

  const restaurantName = (restaurantResult.rows[0] as { name: string } | undefined)?.name || 'Our Restaurant';
  const chatHistory: Array<{ role: 'user' | 'assistant'; content: string }> = [
    ...(Array.isArray(history) ? history.map(m => ({
      role: m.role as 'user' | 'assistant',
      content: m.content,
    })) : []),
    { role: 'user', content: message },
  ];

  const openaiApiKey = await resolveApiKey(restaurantId, 'openai');

  const agentLabel = chatAgentRow?.agent_name || (settings.agent_id ? 'pinned-agent' : 'auto-selected');
  log.info({ restaurantId, agent: agentLabel, model: agentConfig.model, historyLen: chatHistory.length, menuItems: menuContext.items?.length ?? 0 }, 'widget chat request');

  let agentResponse: Awaited<ReturnType<typeof sendAgentMessage>>;
  try {
    agentResponse = await sendAgentMessage(
      chatHistory,
      agentConfig,
      restaurantName,
      menuContext,
      restaurantId,
      conversationId,
      openaiApiKey ?? undefined,
      branchIdForAgent ?? undefined,
      'widget',
      undefined,
      {
        agentId: chatAgentRow?.agent_id ?? settings.agent_id ?? null,
        capabilities: chatAgentRow?.capabilities ?? null,
        menuCategoryIds: Array.isArray(chatAgentRow?.menu_category_ids) ? chatAgentRow!.menu_category_ids : null,
      }
    );
  } catch (err) {
    const errMsg = err instanceof Error ? err.message : String(err);
    log.error({ restaurantId, agent: agentLabel, model: agentConfig.model, errMsg }, 'Widget Chat sendAgentMessage failed');
    throw err;
  }

  log.info({ intent: agentResponse.detectedIntent, escalate: agentResponse.shouldEscalate, replyLen: agentResponse.content?.length ?? 0 }, 'widget chat reply');

  let displayContent = agentResponse.content || '';
  let quickReplies: string[] = [];
  const qrMatch = displayContent.match(/\{"quickReplies"\s*:\s*(\[[^\]]*\])\s*\}/);
  if (qrMatch) {
    try {
      const parsed = JSON.parse(qrMatch[0]) as { quickReplies?: unknown };
      if (Array.isArray(parsed.quickReplies)) {
        quickReplies = parsed.quickReplies.filter((r): r is string => typeof r === 'string').slice(0, 4);
      }
    } catch {
    }
    displayContent = displayContent.replace(/\{"quickReplies"\s*:\s*\[[^\]]*\]\s*\}/g, '').trim();
  }

  await appendWidgetMessage(conversationId, restaurantId, 'ai', displayContent);

  if (agentResponse.shouldEscalate) {
    await updateWidgetConversationStatus(conversationId, restaurantId, 'human');
    try {
      await createNotification(restaurantId, {
        type: 'escalation',
        title: 'Customer requesting human agent',
        message: agentResponse.escalationReason || 'A chat customer has asked to speak with a staff member.',
        actionHref: '/conversations',
      });
    } catch { /* Notification failure must never block the response */ }
    broadcastEscalation({
      restaurantId,
      conversationId,
      reason: agentResponse.escalationReason || 'Customer requested human agent',
    });
  }

  const topicMap: Record<string, string> = {
    order: 'Food Order',
    booking: 'Table Booking',
    menu_query: 'Menu Inquiry',
    complaint: 'Customer Complaint',
    escalation: 'Human Agent Request',
    general: 'General Inquiry',
  };

  if (agentResponse.detectedIntent && agentResponse.detectedIntent !== 'general') {
    await updateWidgetConversationStatus(
      conversationId,
      restaurantId,
      agentResponse.shouldEscalate ? 'human' : 'ai',
      topicMap[agentResponse.detectedIntent]
    );
  }

  const collectedCustomerName =
    agentResponse.orderCreated?.customer_name ||
    agentResponse.bookingCreated?.guest_name;
  if (collectedCustomerName && typeof collectedCustomerName === 'string') {
    await db.update(conversations)
      .set({ customerName: collectedCustomerName, updatedAt: sql`NOW()` })
      .where(and(
        eq(conversations.id, conversationId),
        eq(conversations.restaurantId, restaurantId),
        eq(conversations.customerName, 'Website Visitor'),
      ));
  }

  return NextResponse.json({
    reply: displayContent,
    quickReplies: quickReplies,
    shouldEscalate: agentResponse.shouldEscalate,
    escalationReason: agentResponse.escalationReason,
    detectedIntent: agentResponse.detectedIntent,
    orderCreated: agentResponse.orderCreated || null,
    bookingCreated: agentResponse.bookingCreated || null,
    suggestedItems: agentResponse.suggestedItems || null,
    orderRejections: agentResponse.orderRejections || null,
  });
})));
