/**
 * Brand Assets AI Context Routes — Primary Logo guided generation
 *
 * Background-queue endpoints that generate a PRIMARY logo after studying the
 * business profile, brand strategy, and competitor landscape. Mirrors the
 * aiContextBrand.ts pattern: createJob → 202 { jobId } → setImmediate runs
 * the work → completeJob with a result the frontend polls.
 *
 *   POST /primary-logo/generate    — fresh generation from current context
 *   POST /primary-logo/regenerate  — distinctly different direction, same context
 *   POST /primary-logo/refine      — incorporate user feedback ("make it bolder")
 *   GET  /status/:jobId            — poll job status
 *
 * The image itself is produced by the same providers as /image-generations
 * (generateImageWithOpenAI with Zhipu CogView fallback) and stored as an
 * ImageGeneration doc with versions[] so the existing review/history UI works.
 * "Use this logo" is handled by the existing POST /image-generations/save-to-brand-assets/:id.
 */

import express, { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import { body, validationResult } from 'express-validator';
import { authenticate } from '../middleware/auth';
import { requirePermission } from '../middleware/permissions';
import { createJob, updateJobProgress, completeJob, failJob, getJob } from '../services/aiContext/aiJobManager';
import { buildPrimaryLogoPrompt, PrimaryLogoPromptInputs } from '../services/aiContext/primaryLogoPrompts';
import { buildWatermarkPrompt, WatermarkPromptInputs } from '../services/aiContext/watermarkPrompts';
import { buildBackdropPrompt, BackdropPromptInputs } from '../services/aiContext/backdropPrompts';
import multer from 'multer';
import { generateImageWithZhipuCogView, generateImageForProvider } from './imageGenerations';
import { getAIConfig, getImageProviderForModel, IMAGE_PROVIDER_NAMES, type ImageProvider } from '../utils/aiProvider';
import { stripReasoning } from '../utils/stripReasoning';
import { getModels } from '../models';

const router = express.Router();
router.use(authenticate);

// ============================================
// HELPERS
// ============================================

/** Authorise company access — admin bypasses all checks (mirrors imageGenerations). */
const authorizeCompany = (req: Request, companyId: string): boolean => {
  return !!companyId && (req.user?.companyIds?.includes(companyId) || req.user?.role === 'admin');
};

/** Hard outer timeout so a hung provider call can't hold the job forever. */
const PROVIDER_TIMEOUT_MS = 240000;
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
  let timer: NodeJS.Timeout | undefined;
  const timeout = new Promise<T>((_, reject) => {
    timer = setTimeout(() => reject(new Error(`${label} timed out after ${Math.round(ms / 1000)}s`)), ms);
  });
  return Promise.race([promise, timeout]).finally(() => {
    if (timer) clearTimeout(timer);
  });
}

/**
 * The ImageGeneration schema caps `originalPrompt` at 3000 chars, but the
 * composed logo prompt (brand + competitor + guidance) is usually longer.
 * Truncate the stored field to fit the schema while the FULL prompt is still
 * sent to the image provider and preserved in `ollamaEnhancedPrompt` (15k cap).
 */
function truncateForStorage(text: string, limit: number): string {
  if (text.length <= limit) return text;
  const marker = '…[truncated for storage; full prompt sent to generator]';
  return text.slice(0, Math.max(0, limit - marker.length)) + marker;
}

/**
 * Direct OpenAI Images call using gpt-image-2 at HIGH quality. Shared by the
 * guided Primary Logo and Watermark flows. This bypasses the shared
 * generateImageWithOpenAI provider, whose per-request timeout (config.AI_TIMEOUT,
 * ~120s) + retry loop is too short for gpt-image-2 high-quality generation,
 * which routinely exceeds 120s and would otherwise be aborted on every attempt.
 *
 * Single attempt, 300s timeout, no model fallback (we want gpt-image-2
 * specifically). The Zhipu CogView fallback in the caller handles total
 * failure. `logLabel` only affects console logs.
 */
async function generateBrandImageWithOpenAI(prompt: string, logLabel = 'PrimaryLogo', userId?: string): Promise<{
  base64Data: string;
  revisedPrompt: string;
  model: string;
  provider: string;
  tokenUsage: { inputTokens: number; outputTokens: number; totalTokens: number };
  latencyMs: number;
}> {
  const config = await getAIConfig(userId);
  const openaiKey = config.OPENAI_API_KEY;
  if (!openaiKey) {
    throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY in .env or add a key in Super Admin settings.');
  }
  const openaiBaseUrl = (config.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions')
    .replace('/chat/completions', '')
    .replace(/\/$/, '');
  const imagesUrl = `${openaiBaseUrl}/images/generations`;
  const size = '1024x1024';
  const model = 'gpt-image-2';
  const body = { model, prompt, size, quality: 'high' };

  const callStartTime = Date.now();
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 300000); // 300s — gpt-image-2 high can be slow

  console.log(`[${logLabel}] Calling OpenAI Images API: ${imagesUrl} | model=${model} | size=${size} | quality=high (300s timeout)`);

  try {
    const response = await fetch(imagesUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${openaiKey}`,
      },
      body: JSON.stringify(body),
      signal: controller.signal,
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`OpenAI Images API error (${response.status}) with ${model}: ${errorText}`);
    }

    const data: any = await response.json();
    const latencyMs = Date.now() - callStartTime;
    const usage = data.usage;
    const tokenUsage = {
      inputTokens: usage?.input_tokens || usage?.prompt_tokens || 0,
      outputTokens: usage?.output_tokens || usage?.completion_tokens || 0,
      totalTokens:
        (usage?.input_tokens || usage?.prompt_tokens || 0) +
        (usage?.output_tokens || usage?.completion_tokens || 0),
    };

    // Extract base64. gpt-image-2 via Images API returns { data: [{ b64_json }] }.
    let base64 = '';
    let revisedPrompt = '';
    const image = data.data?.[0];
    if (image) {
      base64 = image.b64_json || image.base64_data || image.base64Data || image.data || '';
      revisedPrompt = image.revised_prompt || '';
    }
    // Chat-style: { output: [{ content: [{ type: 'image_url', url: 'data:image/...;base64,...' }] }] }
    if (!base64 && Array.isArray(data.output)) {
      for (const out of data.output) {
        for (const c of out.content || []) {
          if (c.type === 'image_url' && typeof c.url === 'string' && c.url.startsWith('data:image')) {
            const m = c.url.match(/^data:image\/\w+;base64,(.+)$/);
            if (m) base64 = m[1];
          }
        }
      }
    }
    // URL response — download and convert.
    if (!base64 && image?.url) {
      const urlResp = await fetch(image.url, { signal: AbortSignal.timeout(60000) });
      if (urlResp.ok) {
        base64 = Buffer.from(await urlResp.arrayBuffer()).toString('base64');
      }
    }

    if (!base64) {
      throw new Error(`OpenAI returned no image data. Response keys: ${Object.keys(data).join(', ')}`);
    }

    console.log(`[${logLabel}] Image generated with ${model} in ${latencyMs}ms | base64 length=${base64.length}`);
    return { base64Data: base64, revisedPrompt, model, provider: 'openai', tokenUsage, latencyMs };
  } finally {
    clearTimeout(timeoutId);
  }
}

// ============================================
// SECONDARY LOGO — image-to-image from the primary logo
// ============================================

/**
 * Variation → { directive, assetType, label }. The directive is folded into the
 * prompt sent with the reference image; assetType is the BrandAsset type used
 * when the user clicks "Use this logo". The roadmap's Secondary Logo stage is
 * considered done when any of these types exists.
 */
const SECONDARY_LOGO_VARIATIONS: Record<string, { directive: string; assetType: string; label: string }> = {
  wordmark: {
    directive:
      'wordmark (text only): render ONLY the brand name in the SAME type style, weight and colour as the ' +
      'attached primary logo, with no icon/mark. Keep the typography identical to the reference.',
    assetType: 'wordmark',
    label: 'Wordmark',
  },
  horizontal: {
    directive:
      'horizontal lockup: place the icon/mark to the LEFT of the wordmark, aligned on a single baseline, side ' +
      'by side. Preserve the exact mark and wordmark from the reference — do not restyle them.',
    assetType: 'logoHorizontal',
    label: 'Horizontal lockup',
  },
  stacked: {
    directive:
      'stacked / vertical lockup: place the icon/mark ABOVE the wordmark, both centred as a vertical stack. ' +
      'Preserve the exact mark and wordmark from the reference — do not restyle them.',
    assetType: 'logoVertical',
    label: 'Stacked / vertical lockup',
  },
  'icon-only': {
    directive:
      'icon-only mark: show ONLY the symbol/mark from the attached primary logo, centred, with no wordmark. ' +
      'Keep the mark identical to the reference.',
    assetType: 'logoIconOnly',
    label: 'Icon-only mark',
  },
};

/**
 * Logo VARIATION definitions — treatments/layouts derived from the primary logo
 * (the Logo Variations roadmap stage). Each is generated image-to-image from the
 * approved primary logo so it stays visually aligned. `assetType` is the
 * BrandAsset type used when the user saves a variation; the roadmap's Logo
 * Variations stage is considered done when any of these types exists.
 */
const LOGO_VARIATION_DEFINITIONS: Record<string, { directive: string; assetType: string; label: string }> = {
  logoMarkLight: {
    directive:
      'light mark: render the logo/mark in a single light, light-background-friendly monochrome treatment (e.g. ' +
      'white or pale tint on a neutral light backdrop). Preserve the mark shape and proportions from the reference ' +
      'exactly — only change the colour treatment.',
    assetType: 'logoMarkLight',
    label: 'Light mark',
  },
  logoMarkDark: {
    directive:
      'dark mark: render the logo/mark in a single dark monochrome treatment (e.g. black or deep tint on a neutral ' +
      'backdrop) suitable for dark backgrounds. Preserve the mark shape and proportions from the reference exactly ' +
      '— only change the colour treatment.',
    assetType: 'logoMarkDark',
    label: 'Dark mark',
  },
  'logo-icon': {
    directive:
      'icon-only mark: show ONLY the symbol/mark from the attached primary logo, centred, with no wordmark. ' +
      'Keep the mark identical to the reference.',
    assetType: 'logoIconOnly',
    label: 'Icon-only mark',
  },
  horizontal: {
    directive:
      'horizontal lockup: place the icon/mark to the LEFT of the wordmark, aligned on a single baseline, side ' +
      'by side. Preserve the exact mark and wordmark from the reference — do not restyle them.',
    assetType: 'logoHorizontal',
    label: 'Horizontal lockup',
  },
  stacked: {
    directive:
      'stacked / vertical lockup: place the icon/mark ABOVE the wordmark, both centred as a vertical stack. ' +
      'Preserve the exact mark and wordmark from the reference — do not restyle them.',
    assetType: 'logoVertical',
    label: 'Stacked / vertical lockup',
  },
};

/**
 * Brand PATTERN definitions — geometric/abstract/logo-inspired surface patterns
 * derived image-to-image from the approved primary logo. Each is generated from
 * the primary logo as a reference image so the pattern stays visually tied to
 * the brand mark. Saved as BrandAsset type `brandPattern` (which the website &
 * landing-page generators already consume as a decorative overlay). The roadmap's
 * Brand Patterns stage is considered done when any `brandPattern` asset exists.
 */
const BRAND_PATTERN_VARIATIONS: Record<string, { directive: string; assetType: string; label: string }> = {
  geometric: {
    directive:
      'geometric pattern: derive a crisp, structured geometric tile from the shapes and silhouette of the ' +
      'attached PRIMARY logo. Extract the mark\'s core geometric forms (circles, squares, triangles, lines, ' +
      'grids) and repeat them in a clean, rhythmic layout. Precise, architectural, modern.',
    assetType: 'brandPattern',
    label: 'Geometric',
  },
  abstract: {
    directive:
      'abstract pattern: create a fluid, expressive abstract motif inspired by the forms, movement and ' +
      'character of the attached PRIMARY logo. Loose shapes and gestures echoing the mark, arranged as a ' +
      'balanced surface pattern. Artistic and contemporary.',
    assetType: 'brandPattern',
    label: 'Abstract',
  },
  'seamless-tile': {
    directive:
      'seamless tileable pattern: design a pattern that tiles seamlessly edge-to-edge in ALL directions ' +
      '(top/bottom and left/right must match perfectly with no visible seams), derived from the mark or ' +
      'monogram of the attached PRIMARY logo. Repeatable as a background fill. Subtle enough to sit behind ' +
      'content while retaining brand recognition.',
    assetType: 'brandPattern',
    label: 'Seamless tile',
  },
  'packaging-motif': {
    directive:
      'packaging motif: a denser, more decorative surface pattern tuned for product packaging, derived from ' +
      'the attached PRIMARY logo\'s mark. Richer texture and visual rhythm than a background pattern, with ' +
      'layered elements and depth — still unmistakably on-brand.',
    assetType: 'brandPattern',
    label: 'Packaging motif',
  },
  'presentation-backdrop': {
    directive:
      'presentation backdrop: a very subtle, low-contrast pattern suitable as a slide background behind text, ' +
      'derived from the attached PRIMARY logo. Mostly empty space, minimal motif, brand colours at reduced ' +
      'saturation and low visual weight so it never competes with foreground content.',
    assetType: 'brandPattern',
    label: 'Presentation backdrop',
  },
};

/**
 * Read the primary logo's image bytes from disk (or decode its base64Data) so
 * it can be sent as the reference image to /images/edits. Returns null if no
 * usable image is found.
 */
async function readPrimaryLogoImage(asset: any): Promise<{ buffer: Buffer; mimeType: string } | null> {
  try {
    if (asset.base64Data) {
      const dataUriMatch = String(asset.base64Data).match(/^data:([^;]+);base64,(.+)$/s);
      if (dataUriMatch) {
        return { buffer: Buffer.from(dataUriMatch[2], 'base64'), mimeType: dataUriMatch[1] };
      }
      return { buffer: Buffer.from(String(asset.base64Data), 'base64'), mimeType: 'image/png' };
    }
    if (asset.url && typeof asset.url === 'string') {
      // url is a web path like /uploads/brand-assets/<file>.png — resolve from cwd.
      const filePath = asset.url.startsWith('/uploads/brand-assets/')
        ? path.resolve(process.cwd(), 'uploads', 'brand-assets', path.basename(asset.url))
        : path.resolve(process.cwd(), asset.url.replace(/^\//, ''));
      if (!fs.existsSync(filePath)) {
        console.warn(`[SecondaryLogo] Primary logo file not found at ${filePath}`);
        return null;
      }
      const buffer = await fs.promises.readFile(filePath);
      const ext = path.extname(filePath).toLowerCase();
      const mimeType =
        ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg'
        : ext === '.webp' ? 'image/webp'
        : ext === '.svg' ? 'image/svg+xml'
        : 'image/png';
      return { buffer, mimeType };
    }
    return null;
  } catch (e) {
    console.warn('[SecondaryLogo] Failed to read primary logo image:', (e as Error).message);
    return null;
  }
}

/**
 * Call OpenAI /images/edits with a reference image (e.g. the primary logo) and
 * a prompt, using gpt-image-2 at high quality. Single attempt, 300s timeout.
 * This is the shared image-to-image mechanism that keeps derived brand assets
 * (secondary logos, logo variations, watermark) visually aligned with the
 * approved primary logo — the model sees the reference image. No Zhipu
 * fallback — CogView cannot take a reference image, so a fallback would lose
 * alignment; callers decide how to handle a failed edit (e.g. fall back to a
 * from-scratch generation). `logLabel` only affects console logs.
 */
export async function generateBrandImageEditWithOpenAI(
  prompt: string,
  imageBuffer: Buffer,
  imageMime: string,
  logLabel = 'ImageEdit',
  userId?: string
): Promise<{
  base64Data: string;
  revisedPrompt: string;
  model: string;
  provider: string;
  tokenUsage: { inputTokens: number; outputTokens: number; totalTokens: number };
  latencyMs: number;
}> {
  const config = await getAIConfig(userId);
  const openaiKey = config.OPENAI_API_KEY;
  if (!openaiKey) {
    throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY in .env or add a key in Super Admin settings.');
  }
  const openaiBaseUrl = (config.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions')
    .replace('/chat/completions', '')
    .replace(/\/$/, '');
  const editsUrl = `${openaiBaseUrl}/images/edits`;
  const model = 'gpt-image-2';

  const form = new FormData();
  form.append('model', model);
  form.append('image', new Blob([imageBuffer], { type: imageMime }), 'primary-logo.png');
  form.append('prompt', prompt);
  form.append('size', '1024x1024');
  form.append('quality', 'high');

  const callStartTime = Date.now();
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 300000); // 300s

  console.log(`[${logLabel}] Calling OpenAI Images Edits: ${editsUrl} | model=${model} | ref mime=${imageMime} (300s timeout)`);

  try {
    const response = await fetch(editsUrl, {
      method: 'POST',
      headers: { Authorization: `Bearer ${openaiKey}` },
      body: form,
      signal: controller.signal,
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`OpenAI Images Edits error (${response.status}) with ${model}: ${errorText}`);
    }

    const data: any = await response.json();
    const latencyMs = Date.now() - callStartTime;
    const usage = data.usage;
    const tokenUsage = {
      inputTokens: usage?.input_tokens || usage?.prompt_tokens || 0,
      outputTokens: usage?.output_tokens || usage?.completion_tokens || 0,
      totalTokens:
        (usage?.input_tokens || usage?.prompt_tokens || 0) +
        (usage?.output_tokens || usage?.completion_tokens || 0),
    };

    let base64 = '';
    let revisedPrompt = '';
    const image = data.data?.[0];
    if (image) {
      base64 = image.b64_json || image.base64_data || image.base64Data || image.data || '';
      revisedPrompt = image.revised_prompt || '';
    }
    if (!base64 && Array.isArray(data.output)) {
      for (const out of data.output) {
        for (const c of out.content || []) {
          if (c.type === 'image_url' && typeof c.url === 'string' && c.url.startsWith('data:image')) {
            const m = c.url.match(/^data:image\/\w+;base64,(.+)$/);
            if (m) base64 = m[1];
          }
        }
      }
    }
    if (!base64 && image?.url) {
      const urlResp = await fetch(image.url, { signal: AbortSignal.timeout(60000) });
      if (urlResp.ok) {
        base64 = Buffer.from(await urlResp.arrayBuffer()).toString('base64');
      }
    }
    if (!base64) {
      throw new Error(`OpenAI returned no image data. Response keys: ${Object.keys(data).join(', ')}`);
    }

    console.log(`[${logLabel}] Reference-image edit generated with ${model} in ${latencyMs}ms | base64 length=${base64.length}`);
    return { base64Data: base64, revisedPrompt, model, provider: 'openai', tokenUsage, latencyMs };
  } finally {
    clearTimeout(timeoutId);
  }
}

// ============================================
// GET /status/:jobId
// ============================================

router.get('/status/:jobId', async (req: Request, res: Response) => {
  const { jobId } = req.params;
  const job = getJob(jobId);
  if (!job) {
    res.status(404).json({ error: 'Job not found' });
    return;
  }
  res.json({
    jobId: job.jobId,
    status: job.status,
    progress: job.progress,
    step: job.step,
    result: job.result,
    error: job.error,
  });
});

// ============================================
// Shared generation handler
// ============================================

interface GenerateBody {
  companyId: string;
  refineFeedback?: string;
  regenerateFeedback?: string;
  previousPrompt?: string;
  previousConceptSummary?: string;
  style?: string;
  aspectRatio?: string;
  model?: string;
  // User configuration fields
  description?: string;
  requirements?: string;
  stylePreferences?: string[];
  additionalNotes?: string;
  logoType?: string;
}

/**
 * Run the full Primary Logo generation in the background, updating the job as
 * it goes. Resolves when completeJob/failJob has been called.
 *
 * `kind` only affects the progress label and the `source` reported on the job.
 */
async function runPrimaryLogoGeneration(
  jobId: string,
  req: Request,
  body: GenerateBody,
  kind: 'generate' | 'regenerate' | 'refine'
): Promise<void> {
  const { ImageGeneration } = getModels();
  const { companyId } = body;

  updateJobProgress(jobId, 10, 'Analyzing business profile & brand strategy...');
  const promptInputs: PrimaryLogoPromptInputs = {
    companyId,
    refineFeedback: kind === 'refine' ? body.refineFeedback : undefined,
    regenerateFeedback: kind === 'regenerate' ? body.regenerateFeedback : undefined,
    previousPrompt: kind !== 'generate' ? body.previousPrompt : undefined,
    previousConceptSummary: kind !== 'generate' ? body.previousConceptSummary : undefined,
    style: body.style || 'Minimalist',
    aspectRatio: body.aspectRatio || '1:1',
    // User configuration
    description: body.description,
    requirements: body.requirements,
    stylePreferences: body.stylePreferences,
    additionalNotes: body.additionalNotes,
  };
  const { systemPrompt: _systemPrompt, userPrompt, conceptSummary } = await buildPrimaryLogoPrompt(promptInputs);

  updateJobProgress(jobId, 40, 'Analyzing competitor landscape & composing logo prompt...');

  // Create / fetch an ImageGeneration doc to accumulate versions.
  // For generate we create a new doc; for regenerate/refine we append a version
  // to the existing doc identified by previousPrompt's generation if available,
  // but simplest correct behaviour: create a new doc per request so each
  // version is traceable. The frontend keeps the latest imageGenerationId.
  const generation = new ImageGeneration({
    companyId,
    name: 'Primary Logo',
    description: 'Primary brand logo (guided flow)',
    style: promptInputs.style || 'Minimalist',
    platform: 'Brand',
    aspectRatio: promptInputs.aspectRatio || '1:1',
    // Schema caps originalPrompt at 3000 chars; full prompt is sent to the
    // provider and preserved in ollamaEnhancedPrompt below.
    originalPrompt: truncateForStorage(userPrompt, 3000),
    status: 'generating',
    createdBy: req.user?.id,
  });
  await generation.save();

  updateJobProgress(jobId, 55, 'Generating primary logo...');

  const size = '1024x1024';
  // Determine model and provider from the request body, defaulting to gpt-image-2.
  const model = body.model || 'gpt-image-2';
  const imageProvider: ImageProvider = getImageProviderForModel(model);
  // Primary logos use gpt-image-2 at high quality via a dedicated direct call
  // (300s timeout) — the shared provider's 120s per-request timeout aborts
  // gpt-image-2 high-quality generation, which routinely exceeds 120s. If the
  // direct OpenAI call fails, fall back to Zhipu CogView.
  const PRIMARY_MODEL = model;
  const userId = req.user?._id?.toString();
  let imageResult;
  let providerUsed: string;
  let modelUsed: string;
  try {
    if (imageProvider === 'openai') {
      // Use the dedicated high-quality call for OpenAI (300s timeout for gpt-image-2)
      imageResult = await generateBrandImageWithOpenAI(userPrompt, 'PrimaryLogo', userId);
    } else {
      // Use the unified provider router for non-OpenAI providers
      imageResult = await withTimeout(
        generateImageForProvider(imageProvider, model, userPrompt, size, 'standard', 'vivid'),
        PROVIDER_TIMEOUT_MS,
        `Primary logo generation (${IMAGE_PROVIDER_NAMES[imageProvider] || imageProvider})`
      );
    }
    providerUsed = imageResult.provider;
    modelUsed = imageResult.model;
  } catch (primaryErr) {
    // Only attempt CogView-3 fallback when the primary provider is OpenAI
    if (imageProvider === 'openai') {
      console.warn(`[PrimaryLogo] ${PRIMARY_MODEL} unavailable, trying Zhipu CogView fallback:`, (primaryErr as Error).message);
      updateJobProgress(jobId, 70, 'Primary provider unavailable; trying fallback...');
      try {
        imageResult = await withTimeout(
          generateImageWithZhipuCogView(userPrompt, size),
          PROVIDER_TIMEOUT_MS,
          'Primary logo generation (fallback)'
        );
        providerUsed = imageResult.provider;
        modelUsed = imageResult.model;
      } catch (fallbackErr) {
        generation.status = 'failed';
        await generation.save().catch(() => {});
        throw new Error(
          `Logo generation failed: ${(fallbackErr as Error).message || (primaryErr as Error).message || 'all image providers failed'}`
        );
      }
    } else {
      generation.status = 'failed';
      await generation.save().catch(() => {});
      throw new Error(
        `Logo generation failed with ${IMAGE_PROVIDER_NAMES[imageProvider] || imageProvider}: ${(primaryErr as Error).message || 'image generation failed'}`
      );
    }
  }

  // Push the new version (mirror imageGenerations.ts /generate/:id logic).
  for (const v of generation.versions) v.isCurrent = false;
  const versionNumber = generation.versions.length + 1;
  generation.versions.push({
    version: versionNumber,
    prompt: generation.originalPrompt,
    enhancedPrompt: stripReasoning(userPrompt),
    imageUrl: '',
    base64Data: imageResult.base64Data,
    generationProvider: providerUsed,
    generationModel: modelUsed,
    aspectRatio: generation.aspectRatio,
    size,
    quality: 'standard',
    style: 'vivid',
    revisedPrompt: imageResult.revisedPrompt,
    tokenUsage: imageResult.tokenUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
    latencyMs: imageResult.latencyMs || 0,
    isCurrent: true,
  } as any);
  generation.currentVersion = versionNumber;
  generation.totalVersions = versionNumber;
  generation.generationProvider = providerUsed;
  generation.generationModel = modelUsed;
  if (!generation.ollamaEnhancedPrompt) {
    generation.ollamaEnhancedPrompt = truncateForStorage(stripReasoning(userPrompt), 15000);
  }
  generation.status = 'completed';
  await generation.save();

  updateJobProgress(jobId, 90, 'Finalizing...');
  completeJob(
    jobId,
    {
      imageGenerationId: String(generation._id),
      brandAssetId: null, // not saved as a BrandAsset until "Use this logo"
      prompt: userPrompt,
      conceptSummary,
      model: modelUsed,
      provider: providerUsed,
      versionNumber,
    },
    kind === 'regenerate' ? 'regenerated' : kind === 'refine' ? 'refined' : 'generated'
  );
}

// ============================================
// Route validation helper
// ============================================

function validateCompany(req: Request, res: Response): string | null {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    res.status(400).json({ errors: errors.array() });
    return null;
  }
  const companyId = req.body.companyId as string;
  if (!authorizeCompany(req, companyId)) {
    res.status(403).json({ error: 'Not authorised for this company' });
    return null;
  }
  return companyId;
}

const generateValidations = [
  body('companyId').trim().notEmpty().withMessage('Company ID is required'),
];

// ============================================
// POST /primary-logo/generate
// ============================================

router.post(
  '/primary-logo/generate',
  requirePermission('brand-assets', 'ai-generate'),
  generateValidations,
  async (req: Request, res: Response) => {
    const companyId = validateCompany(req, res);
    if (!companyId) return;
    const job = createJob('brand-assets', companyId, 'brand-assets');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runPrimaryLogoGeneration(job.jobId, req, req.body, 'generate');
      } catch (err) {
        console.error('[PrimaryLogo] generate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Primary logo generation failed');
      }
    });
  }
);

// ============================================
// POST /primary-logo/regenerate
// ============================================

router.post(
  '/primary-logo/regenerate',
  requirePermission('brand-assets', 'ai-generate'),
  [
    ...generateValidations,
    body('regenerateFeedback').trim().notEmpty().withMessage('Regenerate feedback is required'),
  ],
  async (req: Request, res: Response) => {
    const companyId = validateCompany(req, res);
    if (!companyId) return;
    const job = createJob('brand-assets', companyId, 'brand-assets');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runPrimaryLogoGeneration(job.jobId, req, req.body, 'regenerate');
      } catch (err) {
        console.error('[PrimaryLogo] regenerate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Primary logo regeneration failed');
      }
    });
  }
);

// ============================================
// POST /primary-logo/refine
// ============================================

router.post(
  '/primary-logo/refine',
  requirePermission('brand-assets', 'ai-generate'),
  [
    ...generateValidations,
    body('refineFeedback').trim().notEmpty().withMessage('Refine feedback is required'),
  ],
  async (req: Request, res: Response) => {
    const companyId = validateCompany(req, res);
    if (!companyId) return;
    const job = createJob('brand-assets', companyId, 'brand-assets');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runPrimaryLogoGeneration(job.jobId, req, req.body, 'refine');
      } catch (err) {
        console.error('[PrimaryLogo] refine failed:', err);
        failJob(job.jobId, (err as Error).message || 'Primary logo refinement failed');
      }
    });
  }
);

// ============================================
// SECONDARY LOGO — guided, image-to-image from the primary logo
// ============================================

/**
 * Shared reference-image variation generator. Reads the approved primary logo,
 * then for each chosen variation sends it to gpt-image-2 /images/edits (image-
 * to-image) with that variation's prompt, stores one ImageGeneration doc per
 * variation, and completes the job with a `generations[]` array. Each variation
 * stays visually aligned with the primary because the model sees the reference
 * image. Used by both the Secondary Logo and Logo Variations guided flows.
 */
interface ReferenceVariationConfig {
  variationMap: Record<string, { directive: string; assetType: string; label: string }>;
  /** Build the per-variation prompt from its definition. */
  buildPrompt: (def: { directive: string; assetType: string; label: string }) => string;
  /** ImageGeneration doc name for a given variation label. */
  docName: (label: string) => string;
  /** ImageGeneration doc description for a given variation label. */
  docDescription: (label: string) => string;
  /** Console-log label. */
  jobLabel: string;
  /** Noun used in the "failed at <noun>" error message. */
  errorNoun: string;
}

async function runReferenceVariationGeneration(
  jobId: string,
  req: Request,
  body: {
    companyId: string;
    primaryLogoAssetId: string;
    variations: string[];
    model?: string;
    // User configuration
    description?: string;
    requirements?: string;
    stylePreferences?: string[];
    additionalNotes?: string;
  },
  config: ReferenceVariationConfig
): Promise<void> {
  const { ImageGeneration, BrandAsset } = getModels();
  const { companyId, primaryLogoAssetId, variations, model: requestedModel, description, requirements, stylePreferences, additionalNotes } = body;

  // Determine the image generation provider from the model parameter
  const model = requestedModel || 'gpt-image-2';
  const provider: ImageProvider = getImageProviderForModel(model);
  const isEditCapable = provider === 'openai'; // Only OpenAI supports image edits (/images/edits)

  if (!Array.isArray(variations) || variations.length === 0) {
    throw new Error('At least one variation is required.');
  }
  // Validate + de-duplicate variations up front.
  const uniqueVariations = Array.from(new Set(variations));
  for (const v of uniqueVariations) {
    if (!config.variationMap[v]) {
      throw new Error(`Unknown variation: ${v}. Expected one of ${Object.keys(config.variationMap).join(', ')}`);
    }
  }

  updateJobProgress(jobId, 5, 'Loading your primary logo…');
  const primaryAsset = await (BrandAsset as any).findOne({ _id: primaryLogoAssetId, companyId });
  if (!primaryAsset) {
    throw new Error('Primary logo not found. Save a primary logo first.');
  }
  const primaryImage = await readPrimaryLogoImage(primaryAsset);
  if (!primaryImage) {
    throw new Error('Could not load the primary logo image file. Try regenerating the primary logo first.');
  }

  const generations: Array<{
    imageGenerationId: string;
    variation: string;
    variationLabel: string;
    assetType: string;
  }> = [];
  let lastModel = 'gpt-image-2';
  let lastProvider = 'openai';
  const size = '1024x1024';
  const total = uniqueVariations.length;

  for (let i = 0; i < total; i++) {
    const variation = uniqueVariations[i];
    const variationDef = config.variationMap[variation];
    const baseProgress = Math.round((i / total) * 100);
    updateJobProgress(
      jobId,
      10 + baseProgress,
      `Generating ${variationDef.label} (${i + 1}/${total}) from your primary logo…`
    );

    // Build prompt with user configuration
    let userPrompt = config.buildPrompt(variationDef);

    // Add user configuration to the prompt
    const userConfigParts: string[] = [];
    if (description) {
      userConfigParts.push(`USER REQUIREMENTS: ${description.trim()}`);
    }
    if (requirements) {
      userConfigParts.push(`SPECIFIC CONSTRAINTS: ${requirements.trim()}`);
    }
    if (stylePreferences && stylePreferences.length > 0) {
      userConfigParts.push(`STYLE DIRECTION: ${stylePreferences.join(', ')}`);
    }
    if (additionalNotes) {
      userConfigParts.push(`ADDITIONAL NOTES: ${additionalNotes.trim()}`);
    }
    if (userConfigParts.length > 0) {
      userPrompt = userConfigParts.join('\n\n') + '\n\n' + userPrompt;
    }

    const generation = new ImageGeneration({
      companyId,
      name: config.docName(variationDef.label),
      description: config.docDescription(variationDef.label),
      style: 'Minimalist',
      platform: 'Brand',
      aspectRatio: '1:1',
      originalPrompt: truncateForStorage(userPrompt, 3000),
      status: 'generating',
      createdBy: req.user?.id,
    });
    await generation.save();

    let imageResult;
    try {
      if (isEditCapable && primaryImage) {
        // OpenAI supports image-to-image edits with a reference image
        imageResult = await generateBrandImageEditWithOpenAI(
          userPrompt,
          primaryImage.buffer,
          primaryImage.mimeType,
          config.jobLabel
        );
      } else {
        // Non-OpenAI providers: use text-to-image (no reference image support)
        imageResult = await generateImageForProvider(provider, model, userPrompt, size, 'standard', 'vivid');
      }
      lastModel = imageResult.model;
      lastProvider = imageResult.provider;
    } catch (err) {
      generation.status = 'failed';
      await generation.save().catch(() => {});
      throw new Error(
        `${config.errorNoun} generation failed at ${variationDef.label}: ${(err as Error).message || 'image generation failed'}`
      );
    }

    for (const v of generation.versions) v.isCurrent = false;
    const versionNumber = generation.versions.length + 1;
    generation.versions.push({
      version: versionNumber,
      prompt: generation.originalPrompt,
      enhancedPrompt: stripReasoning(userPrompt),
      imageUrl: '',
      base64Data: imageResult.base64Data,
      generationProvider: imageResult.provider,
      generationModel: imageResult.model,
      aspectRatio: generation.aspectRatio,
      size,
      quality: 'standard',
      style: 'vivid',
      revisedPrompt: imageResult.revisedPrompt,
      tokenUsage: imageResult.tokenUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
      latencyMs: imageResult.latencyMs || 0,
      isCurrent: true,
    } as any);
    generation.currentVersion = versionNumber;
    generation.totalVersions = versionNumber;
    generation.generationProvider = imageResult.provider;
    generation.generationModel = imageResult.model;
    if (!generation.ollamaEnhancedPrompt) {
      generation.ollamaEnhancedPrompt = truncateForStorage(stripReasoning(userPrompt), 15000);
    }
    generation.status = 'completed';
    await generation.save();

    generations.push({
      imageGenerationId: String(generation._id),
      variation,
      variationLabel: variationDef.label,
      assetType: variationDef.assetType,
    });
  }

  updateJobProgress(jobId, 95, 'Finalizing…');
  completeJob(
    jobId,
    {
      generations,
      model: lastModel,
      provider: lastProvider,
    },
    'generated'
  );
}

/**
 * Secondary Logo flow — wordmark / lockups / icon-only, derived image-to-image
 * from the approved primary logo.
 */
async function runSecondaryLogoGeneration(
  jobId: string,
  req: Request,
  body: { companyId: string; primaryLogoAssetId: string; variations: string[] }
): Promise<void> {
  return runReferenceVariationGeneration(jobId, req, body, {
    variationMap: SECONDARY_LOGO_VARIATIONS,
    buildPrompt: (def) =>
      'You are creating a SECONDARY logo variation based on the attached PRIMARY logo (reference image).\n' +
      `Variation required: ${def.directive}\n\n` +
      'Keep the SAME mark, colours, type style and visual language as the attached primary logo — do NOT ' +
      'redesign it or change the brand identity. Only rearrange or extract the elements as the variation ' +
      'requires. Clean, neutral background. Output the logo centred with clear space around it. Scalable ' +
      'vector-style, strong silhouette, at most 2–3 brand colours.',
    docName: (label) => `Secondary Logo — ${label}`,
    docDescription: (label) => `Secondary logo variation (${label}) derived from the primary logo`,
    jobLabel: 'SecondaryLogo',
    errorNoun: 'Secondary logo',
  });
}

/**
 * Logo Variations flow — light/dark/icon-only/lockup treatments, derived
 * image-to-image from the approved primary logo so every variation stays on-brand.
 */
async function runLogoVariationsGeneration(
  jobId: string,
  req: Request,
  body: { companyId: string; primaryLogoAssetId: string; variations: string[] }
): Promise<void> {
  return runReferenceVariationGeneration(jobId, req, body, {
    variationMap: LOGO_VARIATION_DEFINITIONS,
    buildPrompt: (def) =>
      'You are creating a LOGO VARIATION based on the attached PRIMARY logo (reference image).\n' +
      `Variation required: ${def.directive}\n\n` +
      'Keep the SAME mark, colours, type style and visual language as the attached primary logo — do NOT ' +
      'redesign it or change the brand identity. Only adapt the colour treatment or layout as the variation ' +
      'requires. Clean, neutral background. Output the logo centred with clear space around it. Scalable ' +
      'vector-style, strong silhouette, at most 2–3 brand colours.',
    docName: (label) => `Logo Variation — ${label}`,
    docDescription: (label) => `Logo variation (${label}) derived from the primary logo`,
    jobLabel: 'LogoVariation',
    errorNoun: 'Logo variation',
  });
}

/**
 * Logo Variants Batch — combines ALL logo variants (secondary logo types +
 * logo variations) into a single batch generation from the primary logo.
 * This is the unified flow that runs after the user approves their primary logo,
 * generating wordmark, horizontal, stacked, icon-only, light mark, and dark mark
 * all at once.
 */
const LOGO_VARIANTS_BATCH_DEFINITIONS: Record<string, { directive: string; assetType: string; label: string }> = {
  wordmark: {
    directive:
      'wordmark (text only): render ONLY the brand name in the SAME type style, weight and colour as the ' +
      'attached primary logo, with no icon/mark. Keep the typography identical to the reference.',
    assetType: 'wordmark',
    label: 'Wordmark',
  },
  horizontal: {
    directive:
      'horizontal lockup: place the icon/mark to the LEFT of the wordmark, aligned on a single baseline, side ' +
      'by side. Preserve the exact mark and wordmark from the reference — do not restyle them.',
    assetType: 'logoHorizontal',
    label: 'Horizontal lockup',
  },
  stacked: {
    directive:
      'stacked / vertical lockup: place the icon/mark ABOVE the wordmark, both centred as a vertical stack. ' +
      'Preserve the exact mark and wordmark from the reference — do not restyle them.',
    assetType: 'logoVertical',
    label: 'Stacked / vertical lockup',
  },
  'icon-only': {
    directive:
      'icon-only mark: show ONLY the symbol/mark from the attached primary logo, centred, with no wordmark. ' +
      'Keep the mark identical to the reference.',
    assetType: 'logoIconOnly',
    label: 'Icon-only mark',
  },
  logoMarkLight: {
    directive:
      'light mark: render the logo/mark in a single light, light-background-friendly monochrome treatment (e.g. ' +
      'white or pale tint on a neutral light backdrop). Preserve the mark shape and proportions from the reference ' +
      'exactly — only change the colour treatment.',
    assetType: 'logoMarkLight',
    label: 'Light mark',
  },
  logoMarkDark: {
    directive:
      'dark mark: render the logo/mark in a single dark monochrome treatment (e.g. black or deep tint on a neutral ' +
      'backdrop) suitable for dark backgrounds. Preserve the mark shape and proportions from the reference exactly ' +
      '— only change the colour treatment.',
    assetType: 'logoMarkDark',
    label: 'Dark mark',
  },
  favicon: {
    directive:
      'favicon: create a small, square favicon (1:1 aspect ratio) derived from the primary logo\'s icon/mark. ' +
      'Simplify and refine the mark so it remains clear, recognisable and legible at 16×16 px and 32×32 px sizes. ' +
      'Remove fine detail, thin strokes and small text — the favicon must be a bold, simplified silhouette of the ' +
      'brand mark on a transparent or solid background. Preserve the core shape and colour identity from the reference.',
    assetType: 'favicon',
    label: 'Favicon',
  },
};

async function runLogoVariantsBatchGeneration(
  jobId: string,
  req: Request,
  body: { companyId: string; primaryLogoAssetId: string; variations: string[] }
): Promise<void> {
  return runReferenceVariationGeneration(jobId, req, body, {
    variationMap: LOGO_VARIANTS_BATCH_DEFINITIONS,
    buildPrompt: (def) =>
      'You are creating a LOGO VARIANT based on the attached PRIMARY logo (reference image).\n' +
      `Variant required: ${def.directive}\n\n` +
      'Keep the SAME mark, colours, type style and visual language as the attached primary logo — do NOT ' +
      'redesign it or change the brand identity. Only rearrange, extract, or adapt the elements as the variant ' +
      'requires. Clean, neutral background. Output the logo centred with clear space around it. Scalable ' +
      'vector-style, strong silhouette, at most 2–3 brand colours.',
    docName: (label) => `Logo Variant — ${label}`,
    docDescription: (label) => `Logo variant (${label}) derived from the primary logo`,
    jobLabel: 'LogoVariantsBatch',
    errorNoun: 'Logo variant',
  });
}

/**
 * Brand Patterns flow — geometric / abstract / seamless / packaging / backdrop
 * patterns derived image-to-image from the approved primary logo so every
 * pattern stays visually tied to the brand mark. Saved as `brandPattern`.
 */
async function runBrandPatternGeneration(
  jobId: string,
  req: Request,
  body: { companyId: string; primaryLogoAssetId: string; variations: string[]; model?: string }
): Promise<void> {
  return runReferenceVariationGeneration(jobId, req, body, {
    variationMap: BRAND_PATTERN_VARIATIONS,
    buildPrompt: (def) =>
      'You are creating a BRAND PATTERN based on the attached PRIMARY logo (reference image).\n' +
      `Pattern required: ${def.directive}\n\n` +
      'Draw the pattern\'s motifs DIRECTLY from the attached primary logo — its mark, shapes, silhouette and ' +
      'visual language — do NOT introduce unrelated decoration or redesign the brand identity. Use ONLY the ' +
      'brand\'s colours (at most 2–3). The output is a SURFACE PATTERN, not a logo: it must read as a repeating ' +
      'or fillable texture, not as a single centred emblem. Seamless/tileable where the variation calls for it. ' +
      'Clean edges, professional refinement, no photographic noise or illegible small text.',
    docName: (label) => `Brand Pattern — ${label}`,
    docDescription: (label) => `Brand pattern (${label}) derived from the primary logo`,
    jobLabel: 'BrandPattern',
    errorNoun: 'Brand pattern',
  });
}

router.post(
  '/secondary-logo/generate',
  requirePermission('brand-assets', 'ai-generate'),
  [
    body('companyId').trim().notEmpty().withMessage('Company ID is required'),
    body('primaryLogoAssetId').trim().notEmpty().withMessage('Primary logo asset ID is required'),
    body('variations').isArray({ min: 1 }).withMessage('At least one variation is required'),
  ],
  async (req: Request, res: Response) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      res.status(400).json({ errors: errors.array() });
      return;
    }
    const companyId = req.body.companyId as string;
    if (!authorizeCompany(req, companyId)) {
      res.status(403).json({ error: 'Not authorised for this company' });
      return;
    }
    // Distinct job module so the secondary flow doesn't collide with the
    // primary-logo flow's useJobResult('brand-assets', companyId) state.
    const job = createJob('brand-assets-secondary', companyId, 'brand-assets-secondary');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runSecondaryLogoGeneration(job.jobId, req, req.body);
      } catch (err) {
        console.error('[SecondaryLogo] generate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Secondary logo generation failed');
      }
    });
  }
);

// ============================================
// LOGO VARIANTS BATCH — all logo variants in a single step
// ============================================

router.post(
  '/logo-variants-batch/generate',
  requirePermission('brand-assets', 'ai-generate'),
  [
    body('companyId').trim().notEmpty().withMessage('Company ID is required'),
    body('primaryLogoAssetId').trim().notEmpty().withMessage('Primary logo asset ID is required'),
    body('variations').isArray({ min: 1 }).withMessage('At least one variation is required'),
  ],
  async (req: Request, res: Response) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      res.status(400).json({ errors: errors.array() });
      return;
    }
    const companyId = req.body.companyId as string;
    if (!authorizeCompany(req, companyId)) {
      res.status(403).json({ error: 'Not authorised for this company' });
      return;
    }
    const job = createJob('brand-assets-variants-batch', companyId, 'brand-assets-variants-batch');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runLogoVariantsBatchGeneration(job.jobId, req, req.body);
      } catch (err) {
        console.error('[LogoVariantsBatch] generate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Logo variants batch generation failed');
      }
    });
  }
);

// ============================================
// LOGO VARIATIONS — guided, image-to-image from the primary logo
// ============================================

router.post(
  '/logo-variations/generate',
  requirePermission('brand-assets', 'ai-generate'),
  [
    body('companyId').trim().notEmpty().withMessage('Company ID is required'),
    body('primaryLogoAssetId').trim().notEmpty().withMessage('Primary logo asset ID is required'),
    body('variations').isArray({ min: 1 }).withMessage('At least one variation is required'),
  ],
  async (req: Request, res: Response) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      res.status(400).json({ errors: errors.array() });
      return;
    }
    const companyId = req.body.companyId as string;
    if (!authorizeCompany(req, companyId)) {
      res.status(403).json({ error: 'Not authorised for this company' });
      return;
    }
    // Distinct job module so the variations flow doesn't collide with the
    // secondary or primary flows' useJobResult state.
    const job = createJob('brand-assets-variations', companyId, 'brand-assets-variations');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runLogoVariationsGeneration(job.jobId, req, req.body);
      } catch (err) {
        console.error('[LogoVariations] generate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Logo variation generation failed');
      }
    });
  }
);

// ============================================
// BRAND PATTERNS — guided, image-to-image from the primary logo
// ============================================

router.post(
  '/brand-patterns/generate',
  requirePermission('brand-assets', 'ai-generate'),
  [
    body('companyId').trim().notEmpty().withMessage('Company ID is required'),
    body('primaryLogoAssetId').trim().notEmpty().withMessage('Primary logo asset ID is required'),
    body('variations').isArray({ min: 1 }).withMessage('At least one variation is required'),
  ],
  async (req: Request, res: Response) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      res.status(400).json({ errors: errors.array() });
      return;
    }
    const companyId = req.body.companyId as string;
    if (!authorizeCompany(req, companyId)) {
      res.status(403).json({ error: 'Not authorised for this company' });
      return;
    }
    // Distinct job module so the patterns flow doesn't collide with the
    // secondary/variations/watermark flows' useJobResult state.
    const job = createJob('brand-assets-patterns', companyId, 'brand-assets-patterns');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runBrandPatternGeneration(job.jobId, req, req.body);
      } catch (err) {
        console.error('[BrandPattern] generate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Brand pattern generation failed');
      }
    });
  }
);

// ============================================
// WATERMARK — guided flow (mirrors the Primary Logo flow)
// ============================================

/**
 * Run the guided Watermark generation in the background. Same shape as
 * runPrimaryLogoGeneration but uses the watermark prompt builder and saves the
 * ImageGeneration doc as a watermark. Distinct job module
 * 'brand-assets-watermark' so it doesn't collide with the primary or secondary
 * flows' useJobResult state.
 */
async function runWatermarkGeneration(
  jobId: string,
  req: Request,
  body: {
    companyId: string;
    /** Approved primary logo to distil the watermark from (reference image). */
    primaryLogoAssetId?: string;
    refineFeedback?: string;
    regenerateFeedback?: string;
    previousPrompt?: string;
    previousConceptSummary?: string;
    style?: string;
    aspectRatio?: string;
    // User configuration
    description?: string;
    requirements?: string;
    stylePreferences?: string[];
    additionalNotes?: string;
  },
  kind: 'generate' | 'regenerate' | 'refine'
): Promise<void> {
  const { ImageGeneration, BrandAsset } = getModels();
  const { companyId } = body;

  updateJobProgress(jobId, 10, 'Analyzing business profile & brand strategy...');
  const promptInputs: WatermarkPromptInputs = {
    companyId,
    refineFeedback: kind === 'refine' ? body.refineFeedback : undefined,
    regenerateFeedback: kind === 'regenerate' ? body.regenerateFeedback : undefined,
    previousPrompt: kind !== 'generate' ? body.previousPrompt : undefined,
    previousConceptSummary: kind !== 'generate' ? body.previousConceptSummary : undefined,
    style: body.style || 'Minimalist',
    aspectRatio: body.aspectRatio || '1:1',
    // User configuration
    description: body.description,
    requirements: body.requirements,
    stylePreferences: body.stylePreferences,
    additionalNotes: body.additionalNotes,
  };
  const { systemPrompt: _systemPrompt, userPrompt, conceptSummary } = await buildWatermarkPrompt(promptInputs);

  updateJobProgress(jobId, 40, 'Analyzing competitor landscape & composing watermark prompt...');

  const generation = new ImageGeneration({
    companyId,
    name: 'Watermark',
    description: 'Brand watermark (guided flow)',
    style: promptInputs.style || 'Minimalist',
    platform: 'Brand',
    aspectRatio: promptInputs.aspectRatio || '1:1',
    originalPrompt: truncateForStorage(userPrompt, 3000),
    status: 'generating',
    createdBy: req.user?.id,
  });
  await generation.save();

  // ── Reference chain: distil the watermark from the approved primary logo ──
  // When a primary logo is available, send it as the reference image to
  // /images/edits so the watermark stays visually tied to the brand mark. If the
  // edit call fails (or no reference is available), fall back to from-scratch
  // generation, then to Zhipu CogView — the watermark is still produced.
  let referenceImage: { buffer: Buffer; mimeType: string } | null = null;
  if (body.primaryLogoAssetId) {
    try {
      const primaryAsset = await (BrandAsset as any).findOne({ _id: body.primaryLogoAssetId, companyId });
      if (primaryAsset) {
        referenceImage = await readPrimaryLogoImage(primaryAsset);
      }
    } catch (e) {
      console.warn('[Watermark] Could not load primary logo reference:', (e as Error).message);
    }
  }
  const referenceNote = referenceImage
    ? 'The attached image is the brand\'s approved PRIMARY logo (reference image). Distil its core mark or ' +
      'monogram into the watermark — preserve the mark\'s silhouette and visual identity; do not redesign it.\n\n'
    : '';
  const generationPrompt = referenceNote + userPrompt;

  updateJobProgress(jobId, 55, referenceImage ? 'Generating watermark from your primary logo…' : 'Generating watermark...');
  const size = '1024x1024';
  const WATERMARK_MODEL = 'gpt-image-2';
  const userId = req.user?._id?.toString();
  let imageResult;
  let provider: string;
  let model: string;
  try {
    if (referenceImage) {
      // Image-to-image from the primary logo — keeps the watermark on-brand.
      imageResult = await generateBrandImageEditWithOpenAI(
        generationPrompt,
        referenceImage.buffer,
        referenceImage.mimeType,
        'Watermark',
        userId
      );
    } else {
      imageResult = await generateBrandImageWithOpenAI(generationPrompt, 'Watermark', userId);
    }
    provider = imageResult.provider;
    model = imageResult.model;
  } catch (primaryErr) {
    // If the reference-image edit failed, retry from-scratch before giving up.
    if (referenceImage) {
      console.warn('[Watermark] Reference-image edit failed, retrying from-scratch:', (primaryErr as Error).message);
      updateJobProgress(jobId, 70, 'Reference edit failed; generating from-scratch…');
    } else {
      console.warn(`[Watermark] ${WATERMARK_MODEL} unavailable, trying Zhipu CogView fallback:`, (primaryErr as Error).message);
      updateJobProgress(jobId, 70, 'Primary provider unavailable; trying fallback...');
    }
    try {
      imageResult = await generateBrandImageWithOpenAI(userPrompt, 'Watermark', userId);
      provider = imageResult.provider;
      model = imageResult.model;
    } catch (openAiErr) {
      try {
        imageResult = await withTimeout(
          generateImageWithZhipuCogView(userPrompt, size),
          PROVIDER_TIMEOUT_MS,
          'Watermark generation (fallback)'
        );
        provider = imageResult.provider;
        model = imageResult.model;
      } catch (fallbackErr) {
        generation.status = 'failed';
        await generation.save().catch(() => {});
        throw new Error(
          `Watermark generation failed: ${(fallbackErr as Error).message || (openAiErr as Error).message || (primaryErr as Error).message || 'all image providers failed'}`
        );
      }
    }
  }

  for (const v of generation.versions) v.isCurrent = false;
  const versionNumber = generation.versions.length + 1;
  generation.versions.push({
    version: versionNumber,
    prompt: generation.originalPrompt,
    enhancedPrompt: stripReasoning(userPrompt),
    imageUrl: '',
    base64Data: imageResult.base64Data,
    generationProvider: provider,
    generationModel: model,
    aspectRatio: generation.aspectRatio,
    size,
    quality: 'standard',
    style: 'vivid',
    revisedPrompt: imageResult.revisedPrompt,
    tokenUsage: imageResult.tokenUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
    latencyMs: imageResult.latencyMs || 0,
    isCurrent: true,
  } as any);
  generation.currentVersion = versionNumber;
  generation.totalVersions = versionNumber;
  generation.generationProvider = provider;
  generation.generationModel = model;
  if (!generation.ollamaEnhancedPrompt) {
    generation.ollamaEnhancedPrompt = truncateForStorage(stripReasoning(userPrompt), 15000);
  }
  generation.status = 'completed';
  await generation.save();

  updateJobProgress(jobId, 90, 'Finalizing...');
  completeJob(
    jobId,
    {
      imageGenerationId: String(generation._id),
      brandAssetId: null, // not saved as a BrandAsset until "Use this watermark"
      prompt: userPrompt,
      conceptSummary,
      model,
      provider,
      versionNumber,
    },
    kind === 'regenerate' ? 'regenerated' : kind === 'refine' ? 'refined' : 'generated'
  );
}

router.post(
  '/watermark/generate',
  requirePermission('brand-assets', 'ai-generate'),
  generateValidations,
  async (req: Request, res: Response) => {
    const companyId = validateCompany(req, res);
    if (!companyId) return;
    const job = createJob('brand-assets-watermark', companyId, 'brand-assets-watermark');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runWatermarkGeneration(job.jobId, req, req.body, 'generate');
      } catch (err) {
        console.error('[Watermark] generate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Watermark generation failed');
      }
    });
  }
);

router.post(
  '/watermark/regenerate',
  requirePermission('brand-assets', 'ai-generate'),
  [
    ...generateValidations,
    body('regenerateFeedback').trim().notEmpty().withMessage('Regenerate feedback is required'),
  ],
  async (req: Request, res: Response) => {
    const companyId = validateCompany(req, res);
    if (!companyId) return;
    const job = createJob('brand-assets-watermark', companyId, 'brand-assets-watermark');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runWatermarkGeneration(job.jobId, req, req.body, 'regenerate');
      } catch (err) {
        console.error('[Watermark] regenerate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Watermark regeneration failed');
      }
    });
  }
);

router.post(
  '/watermark/refine',
  requirePermission('brand-assets', 'ai-generate'),
  [
    ...generateValidations,
    body('refineFeedback').trim().notEmpty().withMessage('Refine feedback is required'),
  ],
  async (req: Request, res: Response) => {
    const companyId = validateCompany(req, res);
    if (!companyId) return;
    const job = createJob('brand-assets-watermark', companyId, 'brand-assets-watermark');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runWatermarkGeneration(job.jobId, req, req.body, 'refine');
      } catch (err) {
        console.error('[Watermark] refine failed:', err);
        failJob(job.jobId, (err as Error).message || 'Watermark refinement failed');
      }
    });
  }
);

// ============================================
// BACKDROP — branded backdrop with single or multiple logo composition
// ============================================

// Multer for handling multiple logo file uploads in backdrop generation
const backdropLogoUpload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 10 * 1024 * 1024 }, // 10MB per file
  fileFilter: (_req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
    const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
    if (allowedMimeTypes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Invalid file format. Supported: JPG, PNG, GIF, WebP, SVG'));
    }
  },
});

/**
 * Run backdrop generation — single-logo (image-to-image from primary logo) or
 * multiple-logos (compositing uploaded logos). The prompt builder handles both
 * modes; the only difference is how the reference image(s) reach the provider.
 */
async function runBackdropGeneration(
  jobId: string,
  req: Request,
  body: {
    companyId: string;
    mode: 'single-logo' | 'multiple-logos';
    /** Approved primary logo to use as reference (single-logo mode). */
    primaryLogoAssetId?: string;
    /** Uploaded logo files (multiple-logos mode) — set by the route handler from multer. */
    logoFiles?: Express.Multer.File[];
    refineFeedback?: string;
    regenerateFeedback?: string;
    previousPrompt?: string;
    previousConceptSummary?: string;
    style?: string;
    aspectRatio?: string;
    // User configuration
    description?: string;
    requirements?: string;
    stylePreferences?: string[];
    additionalNotes?: string;
  },
  kind: 'generate' | 'regenerate' | 'refine'
): Promise<void> {
  const { ImageGeneration, BrandAsset } = getModels();
  const { companyId } = body;

  updateJobProgress(jobId, 10, 'Analyzing business profile & brand strategy...');
  const promptInputs: BackdropPromptInputs = {
    companyId,
    mode: body.mode || 'single-logo',
    refineFeedback: kind === 'refine' ? body.refineFeedback : undefined,
    regenerateFeedback: kind === 'regenerate' ? body.regenerateFeedback : undefined,
    previousPrompt: kind !== 'generate' ? body.previousPrompt : undefined,
    previousConceptSummary: kind !== 'generate' ? body.previousConceptSummary : undefined,
    style: body.style || 'Abstract',
    aspectRatio: body.aspectRatio || '16:9',
    // User configuration
    description: body.description,
    requirements: body.requirements,
    stylePreferences: body.stylePreferences,
    additionalNotes: body.additionalNotes,
  };
  const { systemPrompt: _systemPrompt, userPrompt, conceptSummary } = await buildBackdropPrompt(promptInputs);

  updateJobProgress(jobId, 40, 'Analyzing competitor landscape & composing backdrop prompt...');

  const generation = new ImageGeneration({
    companyId,
    name: 'Backdrop',
    description: body.mode === 'multiple-logos' ? 'Branded backdrop (multiple logos)' : 'Branded backdrop (single logo)',
    style: promptInputs.style || 'Abstract',
    platform: 'Brand',
    aspectRatio: promptInputs.aspectRatio || '16:9',
    originalPrompt: truncateForStorage(userPrompt, 3000),
    status: 'generating',
    createdBy: req.user?.id,
  });
  await generation.save();

  // ── Reference images ──
  // Single-logo mode: distil the backdrop from the approved primary logo.
  // Multiple-logos mode: compose uploaded logos into the backdrop.
  let referenceImage: { buffer: Buffer; mimeType: string } | null = null;

  if (body.mode === 'multiple-logos' && body.logoFiles && body.logoFiles.length > 0) {
    // Multiple-logos mode: use the first uploaded file as the primary reference
    // (OpenAI /images/edits accepts a single image, so we use the first file).
    // Future enhancement could composite all files into a reference collage.
    try {
      const firstFile = body.logoFiles[0];
      referenceImage = {
        buffer: firstFile.buffer,
        mimeType: firstFile.mimetype,
      };
      console.log(`[Backdrop] Using uploaded file "${firstFile.originalname}" as reference (${body.logoFiles.length} total files uploaded)`);
    } catch (e) {
      console.warn('[Backdrop] Could not read uploaded logo file:', (e as Error).message);
    }
  } else if (body.primaryLogoAssetId) {
    try {
      const primaryAsset = await (BrandAsset as any).findOne({ _id: body.primaryLogoAssetId, companyId });
      if (primaryAsset) {
        referenceImage = await readPrimaryLogoImage(primaryAsset);
      }
    } catch (e) {
      console.warn('[Backdrop] Could not load primary logo reference:', (e as Error).message);
    }
  }

  const modeNote = body.mode === 'multiple-logos'
    ? 'The attached image is one of multiple brand logos that need to be composed together in a branded backdrop. Arrange all logos in a balanced, harmonious layout on a branded background. Each logo should remain recognisable but work together as a cohesive visual.\n\n'
    : referenceImage
      ? 'The attached image is the brand\'s approved PRIMARY logo. Create a BACKDROP that features or is inspired by this logo. The logo should be the central visual element, arranged in a visually compelling way on a branded background. Preserve the logo\'s identity — do not redesign it.\n\n'
      : '';
  const generationPrompt = modeNote + userPrompt;

  updateJobProgress(jobId, 55, referenceImage ? 'Generating backdrop from reference logo…' : 'Generating backdrop...');
  const size = '1792x1024'; // 16:9 aspect ratio for backdrop
  const userId = req.user?._id?.toString();
  let imageResult;
  let provider: string;
  let model: string;
  try {
    if (referenceImage) {
      imageResult = await generateBrandImageEditWithOpenAI(
        generationPrompt,
        referenceImage.buffer,
        referenceImage.mimeType,
        'Backdrop',
        userId
      );
    } else {
      imageResult = await generateBrandImageWithOpenAI(generationPrompt, 'Backdrop', userId);
    }
    provider = imageResult.provider;
    model = imageResult.model;
  } catch (primaryErr) {
    if (referenceImage) {
      console.warn('[Backdrop] Reference-image edit failed, retrying from-scratch:', (primaryErr as Error).message);
      updateJobProgress(jobId, 70, 'Reference edit failed; generating from-scratch…');
    } else {
      console.warn('[Backdrop] Primary provider unavailable, trying fallback:', (primaryErr as Error).message);
      updateJobProgress(jobId, 70, 'Primary provider unavailable; trying fallback...');
    }
    try {
      imageResult = await generateBrandImageWithOpenAI(userPrompt, 'Backdrop', userId);
      provider = imageResult.provider;
      model = imageResult.model;
    } catch (openAiErr) {
      try {
        imageResult = await withTimeout(
          generateImageWithZhipuCogView(userPrompt, size),
          PROVIDER_TIMEOUT_MS,
          'Backdrop generation (fallback)'
        );
        provider = imageResult.provider;
        model = imageResult.model;
      } catch (fallbackErr) {
        generation.status = 'failed';
        await generation.save().catch(() => {});
        throw new Error(
          `Backdrop generation failed: ${(fallbackErr as Error).message || (openAiErr as Error).message || (primaryErr as Error).message || 'all image providers failed'}`
        );
      }
    }
  }

  for (const v of generation.versions) v.isCurrent = false;
  const versionNumber = generation.versions.length + 1;
  generation.versions.push({
    version: versionNumber,
    prompt: generation.originalPrompt,
    enhancedPrompt: stripReasoning(userPrompt),
    imageUrl: '',
    base64Data: imageResult.base64Data,
    generationProvider: provider,
    generationModel: model,
    aspectRatio: generation.aspectRatio,
    size,
    quality: 'standard',
    style: 'vivid',
    revisedPrompt: imageResult.revisedPrompt,
    tokenUsage: imageResult.tokenUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
    latencyMs: imageResult.latencyMs || 0,
    isCurrent: true,
  } as any);
  generation.currentVersion = versionNumber;
  generation.totalVersions = versionNumber;
  generation.generationProvider = provider;
  generation.generationModel = model;
  if (!generation.ollamaEnhancedPrompt) {
    generation.ollamaEnhancedPrompt = truncateForStorage(stripReasoning(userPrompt), 15000);
  }
  generation.status = 'completed';
  await generation.save();

  updateJobProgress(jobId, 90, 'Finalizing...');
  completeJob(
    jobId,
    {
      imageGenerationId: String(generation._id),
      brandAssetId: null,
      prompt: userPrompt,
      conceptSummary,
      model,
      provider,
      versionNumber,
    },
    kind === 'regenerate' ? 'regenerated' : kind === 'refine' ? 'refined' : 'generated'
  );
}

// ── Backdrop endpoints ──

// POST /backdrop/generate — multipart for multiple-logos, JSON for single-logo
router.post(
  '/backdrop/generate',
  requirePermission('brand-assets', 'ai-generate'),
  backdropLogoUpload.array('logoFiles', 5),
  (req: Request, res: Response, next: Function) => {
    // If multipart, multer parsed the fields; if JSON, they're already in req.body.
    // Normalise the mode field from the parsed multipart fields.
    next();
  },
  async (req: Request, res: Response) => {
    const companyId = req.body.companyId;
    if (!companyId) {
      res.status(400).json({ error: 'Company ID is required' });
      return;
    }
    if (!authorizeCompany(req, companyId)) {
      res.status(403).json({ error: 'Not authorised for this company' });
      return;
    }

    // Parse stylePreferences if sent as a JSON string (multipartFormData)
    let stylePreferences: string[] = [];
    if (req.body.stylePreferences) {
      if (typeof req.body.stylePreferences === 'string') {
        try { stylePreferences = JSON.parse(req.body.stylePreferences); } catch { stylePreferences = []; }
      } else if (Array.isArray(req.body.stylePreferences)) {
        stylePreferences = req.body.stylePreferences;
      }
    }

    // Attach multer files to body for the handler
    const body = {
      ...req.body,
      companyId,
      mode: (req.body.mode || 'single-logo') as 'single-logo' | 'multiple-logos',
      stylePreferences,
      logoFiles: ((req as any).files || []) as Express.Multer.File[],
    };

    const job = createJob('brand-assets-backdrop', companyId, 'brand-assets-backdrop');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runBackdropGeneration(job.jobId, req, body, 'generate');
      } catch (err) {
        console.error('[Backdrop] generate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Backdrop generation failed');
      }
    });
  }
);

router.post(
  '/backdrop/regenerate',
  requirePermission('brand-assets', 'ai-generate'),
  [
    ...generateValidations,
    body('regenerateFeedback').trim().notEmpty().withMessage('Regenerate feedback is required'),
  ],
  async (req: Request, res: Response) => {
    const companyId = validateCompany(req, res);
    if (!companyId) return;
    const job = createJob('brand-assets-backdrop', companyId, 'brand-assets-backdrop');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runBackdropGeneration(job.jobId, req, req.body, 'regenerate');
      } catch (err) {
        console.error('[Backdrop] regenerate failed:', err);
        failJob(job.jobId, (err as Error).message || 'Backdrop regeneration failed');
      }
    });
  }
);

router.post(
  '/backdrop/refine',
  requirePermission('brand-assets', 'ai-generate'),
  [
    ...generateValidations,
    body('refineFeedback').trim().notEmpty().withMessage('Refine feedback is required'),
  ],
  async (req: Request, res: Response) => {
    const companyId = validateCompany(req, res);
    if (!companyId) return;
    const job = createJob('brand-assets-backdrop', companyId, 'brand-assets-backdrop');
    res.status(202).json({ jobId: job.jobId, status: 'processing' });
    setImmediate(async () => {
      try {
        await runBackdropGeneration(job.jobId, req, req.body, 'refine');
      } catch (err) {
        console.error('[Backdrop] refine failed:', err);
        failJob(job.jobId, (err as Error).message || 'Backdrop refinement failed');
      }
    });
  }
);

export default router;