/**
 * WhatsApp Nurturing AI Pipeline
 * Multi-stage pipeline for generating WhatsApp nurturing sequences
 */

import { getModels } from '../../models';
import { generateWithAI } from '../../utils/aiProvider';
import { buildSchedule, reconcilePlanToSchedule, applyScheduleToMessages, scheduleCountMismatch } from './whatsappSchedule';
import {
  buildContextString,
  buildSequencePlanPrompt,
  buildMessageGenerationPrompt,
  buildOptimizationPrompt,
  buildRegenerateMessagePrompt,
  buildMultiChannelPrompt,
  enforceWhatsAppMessageLength,
} from './whatsappNurturingPrompts';

// ============================================
// TYPES
// ============================================

export interface WhatsAppPipelineResult {
  success: boolean;
  stage: string;
  sequencePlan?: any[];
  messages?: any[];
  optimization?: any;
  tokensUsed: number;
  inputTokens: number;
  outputTokens: number;
  model: string;
  provider: string;
  latencyMs: number;
  error?: string;
  fallbackUsed?: boolean;         // true if a different provider was used than the requested one
  requestedProvider?: string;    // the provider originally requested by the user
}

// ============================================
// PIPELINE STAGES
// ============================================

/**
 * Stage 1: Context Assembly
 * Fetches all selected data sources from the database and builds a context string.
 * Now properly filters by linked IDs (icpIds, personaIds, etc.) for relevant content.
 */
async function assembleContext(
  companyId: string,
  dataSources: string[],
  linkedData?: Record<string, string[] | string | undefined>
): Promise<string> {
  const {
    BusinessProfile, Founder, Product, ICP, Persona, Competitor,
    Brand, Testimonial, CaseStudy, FAQ, Blog, LandingPage,
    Course, Event,
  } = getModels();

  const companyData: Record<string, any> = {};

  // Helper to filter by linked IDs
  const filterByIds = <T extends { _id: any; id?: string }>(items: T[], ids: string[] | undefined): T[] => {
    if (!ids || ids.length === 0) return items;
    return items.filter((item) => {
      const itemId = item._id?.toString() || item.id;
      return ids.includes(itemId);
    });
  };

  const fetchMap: Record<string, () => Promise<any>> = {
    'business-profile': async () => {
      const data = await BusinessProfile.findOne({ companyId });
      return { businessProfile: data };
    },
    'founder': async () => {
      const items = await Founder.find({ companyId });
      const linkedIds = linkedData?.founderIds as string[] | undefined;
      return { founders: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'product': async () => {
      const items = await Product.find({ companyId });
      const linkedIds = linkedData?.productIds as string[] | undefined;
      return { products: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'icp': async () => {
      const items = await ICP.find({ companyId });
      const linkedIds = linkedData?.icpIds as string[] | undefined;
      return { icps: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'persona': async () => {
      const items = await Persona.find({ companyId });
      const linkedIds = linkedData?.personaIds as string[] | undefined;
      return { personas: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'competitor': async () => {
      const items = await Competitor.find({ companyId });
      const linkedIds = linkedData?.competitorIds as string[] | undefined;
      return { competitors: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'brand': async () => {
      const data = await Brand.findOne({ companyId });
      return { brand: data };
    },
    'testimonial': async () => {
      const items = await Testimonial.find({ companyId });
      const linkedIds = linkedData?.testimonialIds as string[] | undefined;
      return { testimonials: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'case-study': async () => {
      const items = await CaseStudy.find({ companyId });
      const linkedIds = linkedData?.caseStudyIds as string[] | undefined;
      return { caseStudies: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'faq': async () => {
      const items = await FAQ.find({ companyId });
      const linkedIds = linkedData?.faqIds as string[] | undefined;
      return { faqs: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'blog': async () => {
      const items = await Blog.find({ companyId });
      const linkedIds = linkedData?.blogIds as string[] | undefined;
      return { blogs: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'landing-page': async () => {
      const items = await LandingPage.find({ companyId });
      const linkedIds = linkedData?.landingPageIds as string[] | undefined;
      return { landingPages: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'courses': async () => {
      const items = await Course.find({ companyId });
      const linkedIds = linkedData?.courseIds as string[] | undefined;
      return { courses: linkedIds ? filterByIds(items, linkedIds) : items };
    },
    'events': async () => {
      const items = await Event.find({ companyId });
      const linkedIds = linkedData?.eventIds as string[] | undefined;
      return { events: linkedIds ? filterByIds(items, linkedIds) : items };
    },
  };

  // Fetch data sources in parallel
  const results = await Promise.all(
    dataSources
      .filter((ds) => fetchMap[ds])
      .map(async (ds) => {
        try {
          return await fetchMap[ds]();
        } catch {
          return {};
        }
      })
  );

  // Merge all data
  for (const result of results) {
    Object.assign(companyData, result);
  }

  return buildContextString(dataSources, companyData);
}

/**
 * Stage 2: Sequence Plan Generation
 * Generates a day-by-day sequence plan based on the framework and campaign settings.
 */
async function generateSequencePlan(
  campaign: any,
  contextString: string,
  preferredProvider?: string,
  providerLock?: boolean
): Promise<{ plan: any[]; tokensUsed: number; inputTokens: number; outputTokens: number; model: string; provider: string; fallbackUsed?: boolean; requestedProvider?: string }> {
  const prompt = buildSequencePlanPrompt(campaign, contextString);

  const result = await generateWithAI(
    prompt,
    'You are an expert WhatsApp marketing strategist. Generate only valid JSON arrays. No markdown, no explanation.',
    4000,
    0.7,
    'json',
    preferredProvider as any,
    undefined,
    undefined,
    undefined,
    providerLock
  );

  if (!result || !result.content) {
    throw new Error('Failed to generate sequence plan: No AI response');
  }

  // Parse the JSON response
  // AI may return a bare array [...] or a wrapped object like { plan: [...] } or { sequencePlan: [...] }
  let plan: any[];
  try {
    const cleaned = result.content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
    const parsed = JSON.parse(cleaned);
    if (Array.isArray(parsed)) {
      plan = parsed;
    } else if (parsed && typeof parsed === 'object') {
      // Try common wrapper keys
      plan = parsed.plan || parsed.sequencePlan || parsed.sequence || parsed.days || [];
      if (!Array.isArray(plan)) plan = [];
    } else {
      plan = [];
    }
  } catch {
    throw new Error('Failed to parse sequence plan from AI response');
  }

  if (!Array.isArray(plan) || plan.length === 0) {
    throw new Error('AI returned invalid sequence plan format');
  }

  return {
    plan,
    tokensUsed: result.tokenUsage?.totalTokens || 0,
    inputTokens: result.tokenUsage?.inputTokens || 0,
    outputTokens: result.tokenUsage?.outputTokens || 0,
    model: result.model || 'unknown',
    provider: result.provider || 'unknown',
    fallbackUsed: result.fallbackUsed || false,
    requestedProvider: result.requestedProvider,
  };
}

/**
 * Stage 3: Message Generation
 * Generates WhatsApp messages for the sequence, batching to avoid context window limits.
 */
async function generateMessages(
  campaign: any,
  sequencePlan: any[],
  contextString: string,
  preferredProvider?: string,
  providerLock?: boolean
): Promise<{ messages: any[]; tokensUsed: number; inputTokens: number; outputTokens: number; model: string; provider: string; fallbackUsed?: boolean; requestedProvider?: string }> {
  const totalDays = sequencePlan.length;
  const batchSize = 10;
  const allMessages: any[] = [];
  let totalTokens = 0;
  let totalInputTokens = 0;
  let totalOutputTokens = 0;
  let lastModel = 'unknown';
  let lastProvider = 'unknown';
  let anyFallbackUsed = false;
  let requestedProvider: string | undefined;

  // Generate in batches to avoid context window limits
  for (let i = 0; i < totalDays; i += batchSize) {
    const batchEnd = Math.min(i + batchSize, totalDays);
    const dayRange = { start: i + 1, end: batchEnd };

    const prompt = buildMessageGenerationPrompt(
      campaign,
      sequencePlan,
      contextString,
      dayRange
    );

    const result = await generateWithAI(
      prompt,
      'You are an expert WhatsApp copywriter. Generate only valid JSON arrays. No markdown, no explanation. Make messages feel personal, conversational, and engaging.',
      8000,
      0.7,
      'json',
      preferredProvider as any,
      undefined,
      undefined,
      undefined,
      providerLock
    );

    if (!result || !result.content) {
      throw new Error(`Failed to generate messages for days ${dayRange.start}-${dayRange.end}`);
    }

    // Parse messages — AI may return a bare array [...] or a wrapped object like { messages: [...] }
    let batchMessages: any[];
    try {
      const cleaned = result.content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
      const parsed = JSON.parse(cleaned);
      if (Array.isArray(parsed)) {
        batchMessages = parsed;
      } else if (parsed && typeof parsed === 'object') {
        // Try common wrapper keys
        batchMessages = parsed.messages || parsed.data || parsed.items || [];
        if (!Array.isArray(batchMessages)) batchMessages = [];
      } else {
        batchMessages = [];
      }
    } catch {
      throw new Error(`Failed to parse messages for days ${dayRange.start}-${dayRange.end}`);
    }

    if (Array.isArray(batchMessages) && batchMessages.length > 0) {
      // Safeguard: guarantee no message exceeds the hard character ceiling,
      // trimming at a natural boundary if the model overshot the prompt target.
      for (const m of batchMessages) {
        if (m && typeof m.copy === 'string') {
          m.copy = enforceWhatsAppMessageLength(m.copy);
        }
      }
      allMessages.push(...batchMessages);
    }

    totalTokens += result.tokenUsage?.totalTokens || 0;
    totalInputTokens += result.tokenUsage?.inputTokens || 0;
    totalOutputTokens += result.tokenUsage?.outputTokens || 0;
    lastModel = result.model || 'unknown';
    lastProvider = result.provider || 'unknown';
    if (result.fallbackUsed) anyFallbackUsed = true;
    if (result.requestedProvider) requestedProvider = result.requestedProvider;
  }

  return {
    messages: allMessages,
    tokensUsed: totalTokens,
    inputTokens: totalInputTokens,
    outputTokens: totalOutputTokens,
    model: lastModel,
    provider: lastProvider,
    fallbackUsed: anyFallbackUsed,
    requestedProvider,
  };
}

/**
 * Stage 4: Optimization
 * Scores and optimizes the generated messages for engagement and conversion.
 */
async function optimizeMessages(
  campaign: any,
  messages: any[],
  preferredProvider?: string,
  providerLock?: boolean
): Promise<{ optimization: any; tokensUsed: number; inputTokens: number; outputTokens: number; model: string; provider: string; fallbackUsed?: boolean; requestedProvider?: string }> {
  const prompt = buildOptimizationPrompt(campaign, messages);

  const result = await generateWithAI(
    prompt,
    'You are an expert WhatsApp marketing optimizer. Analyze the sequence and provide scores and suggestions. Generate only valid JSON.',
    3000,
    0.7,
    'json',
    preferredProvider as any,
    undefined,
    undefined,
    undefined,
    providerLock
  );

  if (!result || !result.content) {
    // Optimization failure is non-critical, return default
    return {
      optimization: {
        openRateScore: 70,
        responseRateScore: 65,
        engagementScore: 72,
        conversionScore: 68,
        suggestions: ['Consider adding more personalization', 'Try varying message lengths'],
        optimizedCopy: undefined,
      },
      tokensUsed: 0,
      inputTokens: 0,
      outputTokens: 0,
      model: 'unknown',
      provider: 'unknown',
    };
  }

  let optimization: any;
  try {
    const cleaned = result.content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
    optimization = JSON.parse(cleaned);
  } catch {
    optimization = {
      openRateScore: 70,
      responseRateScore: 65,
      engagementScore: 72,
      conversionScore: 68,
      suggestions: [],
    };
  }

  return {
    optimization,
    tokensUsed: result.tokenUsage?.totalTokens || 0,
    inputTokens: result.tokenUsage?.inputTokens || 0,
    outputTokens: result.tokenUsage?.outputTokens || 0,
    model: result.model || 'unknown',
    provider: result.provider || 'unknown',
    fallbackUsed: result.fallbackUsed || false,
    requestedProvider: result.requestedProvider,
  };
}

// ============================================
// MAIN PIPELINE
// ============================================

/**
 * Runs the full WhatsApp Nurturing AI pipeline.
 * 1. Assembles context from selected data sources
 * 2. Generates sequence plan
 * 3. Generates messages
 * 4. Optimizes messages
 */
export async function runWhatsAppNurturingPipeline(
  companyId: string,
  campaign: any,
  preferredProvider?: string,
  providerLock?: boolean
): Promise<WhatsAppPipelineResult> {
  const startTime = Date.now();

  try {
    // Stage 1: Context Assembly - pass linkedData for filtering specific entities
    const contextString = await assembleContext(
      companyId,
      campaign.dataSources || [],
      campaign.linkedData || {}
    );

    // The schedule is recomputed here rather than trusted from the request: the
    // plan length IS the message count (generateMessages emits one message per
    // entry), so this is the point that decides how many messages exist. An
    // empty schedule means no date range was sent — a campaign from the wizard,
    // or one created before this change — and every step below then behaves
    // exactly as it did, driven by the plan the caller supplied.
    const schedule = buildSchedule(
      campaign.startDate,
      campaign.endDate,
      campaign.messageFrequency,
    );

    // Stage 2: Sequence Plan Generation
    let sequencePlan = campaign.sequencePlan;
    let planTokensUsed = 0;
    let planInputTokens = 0;
    let planOutputTokens = 0;
    let planModel = 'unknown';
    let planProvider = 'unknown';
    let planFallbackUsed = false;
    let planRequestedProvider: string | undefined;

    if (!sequencePlan || sequencePlan.length === 0) {
      const planResult = await generateSequencePlan(campaign, contextString, preferredProvider, providerLock);
      sequencePlan = planResult.plan;
      planTokensUsed = planResult.tokensUsed;
      planInputTokens = planResult.inputTokens;
      planOutputTokens = planResult.outputTokens;
      planModel = planResult.model;
      planProvider = planResult.provider;
      planFallbackUsed = planResult.fallbackUsed || false;
      planRequestedProvider = planResult.requestedProvider;
    }

    // Force the plan to exactly one entry per scheduled send date and stamp each
    // with its date. A stale or tampered client payload therefore cannot make the
    // pipeline generate the wrong number of messages.
    sequencePlan = reconcilePlanToSchedule(sequencePlan, schedule);

    // Stage 3: Message Generation
    const messageResult = await generateMessages(campaign, sequencePlan, contextString, preferredProvider, providerLock);
    // Carry each send date onto its message, and drop any surplus the model
    // returned beyond the schedule.
    messageResult.messages = applyScheduleToMessages(messageResult.messages, schedule);

    // Surplus is trimmed above, but a short generation would leave scheduled
    // dates with no message. Fail rather than save an incomplete sequence.
    const countError = scheduleCountMismatch(messageResult.messages, schedule);
    if (countError) {
      return {
        success: false,
        stage: 'message-count-mismatch',
        error: countError,
        tokensUsed: planTokensUsed + messageResult.tokensUsed,
        inputTokens: planInputTokens + messageResult.inputTokens,
        outputTokens: planOutputTokens + messageResult.outputTokens,
        model: messageResult.model || planModel,
        provider: messageResult.provider || planProvider,
        latencyMs: Date.now() - startTime,
      };
    }

    // Stage 4: Optimization (non-critical, don't fail on error)
    let optimization;
    let optTokensUsed = 0;
    let optInputTokens = 0;
    let optOutputTokens = 0;
    try {
      const optResult = await optimizeMessages(campaign, messageResult.messages, preferredProvider, providerLock);
      optimization = optResult.optimization;
      optTokensUsed = optResult.tokensUsed;
      optInputTokens = optResult.inputTokens;
      optOutputTokens = optResult.outputTokens;
    } catch {
      optimization = {
        openRateScore: 70,
        responseRateScore: 65,
        engagementScore: 72,
        conversionScore: 68,
        suggestions: [],
      };
    }

    return {
      success: true,
      stage: 'complete',
      sequencePlan,
      messages: messageResult.messages,
      optimization,
      tokensUsed: planTokensUsed + messageResult.tokensUsed + optTokensUsed,
      inputTokens: planInputTokens + messageResult.inputTokens + optInputTokens,
      outputTokens: planOutputTokens + messageResult.outputTokens + optOutputTokens,
      model: messageResult.model || planModel,
      provider: messageResult.provider || planProvider,
      latencyMs: Date.now() - startTime,
      fallbackUsed: planFallbackUsed || messageResult.fallbackUsed || false,
      requestedProvider: planRequestedProvider || messageResult.requestedProvider,
    };
  } catch (error: any) {
    return {
      success: false,
      stage: 'pipeline-error',
      tokensUsed: 0,
      inputTokens: 0,
      outputTokens: 0,
      model: 'unknown',
      provider: 'unknown',
      latencyMs: Date.now() - startTime,
      error: error.message || 'Unknown pipeline error',
    };
  }
}

/**
 * Generates only a sequence plan (without messages).
 * Used when the user wants to review/approve the plan before generating messages.
 */
export async function generateSequencePlanOnly(
  companyId: string,
  campaign: any
): Promise<{ plan: any[]; tokensUsed: number; model: string; provider: string }> {
  const contextString = await assembleContext(
    companyId,
    campaign.dataSources || [],
    campaign.linkedData || {}
  );
  return generateSequencePlan(campaign, contextString);
}

/**
 * Regenerates a single message.
 */
export async function regenerateSingleMessage(
  companyId: string,
  campaign: any,
  message: any,
  dayPlan?: any
): Promise<{ message: any; tokensUsed: number; model: string; provider: string }> {
  const contextString = await assembleContext(
    companyId,
    campaign.dataSources || [],
    campaign.linkedData || {}
  );
  const prompt = buildRegenerateMessagePrompt(campaign, message, dayPlan);

  const result = await generateWithAI(
    prompt,
    'You are an expert WhatsApp copywriter. Generate only valid JSON. No markdown, no explanation.',
    2000,
    0.8,
    'json'
  );

  if (!result || !result.content) {
    throw new Error('Failed to regenerate message: No AI response');
  }

  let regeneratedMessage: any;
  try {
    const cleaned = result.content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
    regeneratedMessage = JSON.parse(cleaned);
  } catch {
    throw new Error('Failed to parse regenerated message from AI response');
  }

  // Safeguard: enforce the hard character ceiling on the regenerated copy.
  if (regeneratedMessage && typeof regeneratedMessage.copy === 'string') {
    regeneratedMessage.copy = enforceWhatsAppMessageLength(regeneratedMessage.copy);
  }

  return {
    message: regeneratedMessage,
    tokensUsed: result.tokenUsage?.totalTokens || 0,
    model: result.model || 'unknown',
    provider: result.provider || 'unknown',
  };
}

/**
 * Generates multi-channel assets (email, landing page, social, ads).
 */
export async function generateMultiChannelAssets(
  companyId: string,
  campaign: any,
  messages: any[],
  channel: 'email' | 'landing-page' | 'social-post' | 'ad-copy'
): Promise<{ asset: any; tokensUsed: number; model: string; provider: string }> {
  const contextString = await assembleContext(
    companyId,
    campaign.dataSources || [],
    campaign.linkedData || {}
  );
  const prompt = buildMultiChannelPrompt(campaign, messages, channel);

  const result = await generateWithAI(
    prompt,
    'You are an expert multi-channel marketer. Generate only valid JSON. No markdown, no explanation.',
    3000,
    0.7,
    'json'
  );

  if (!result || !result.content) {
    throw new Error(`Failed to generate ${channel} asset: No AI response`);
  }

  let asset: any;
  try {
    const cleaned = result.content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
    asset = JSON.parse(cleaned);
  } catch {
    throw new Error(`Failed to parse ${channel} asset from AI response`);
  }

  // Ensure the asset has required fields
  asset.id = `mca-${channel}-${Date.now()}`;
  asset.channel = channel;
  asset.status = 'generated';

  return {
    asset,
    tokensUsed: result.tokenUsage?.totalTokens || 0,
    model: result.model || 'unknown',
    provider: result.provider || 'unknown',
  };
}