import { randomBytes } from 'node:crypto';
import type {
  AssetPack,
  CharacterSpec,
  ExportManifest,
  GenImage,
  GenSession,
  PackKind,
  ToyAsset,
} from './types';
import { detectProvider, generateGptImageEdit, generateGptJson, generateMock, inferCharacterSpecFromImage } from './providers';
import { saveAnchorImage, saveExportManifest, savePackImage } from './storage';
import { FILENAME_SCHEMA, getPackTemplates, PACK_LABELS, renderCharacterSummary, TEMPLATE_FREEZE_VERSION } from './templates';

function slugify(input: string): string {
  const ascii = input
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '');
  return ascii || 'toy';
}

function pickPalette(prompt: string): string[] {
  const colors = ['橙白', '粉白', '蓝白', '黑金', '紫白', '绿色', '米色'];
  const found = colors.filter(c => prompt.includes(c));
  if (found.length > 0) return found;
  return ['主色待定', '辅色待定', '强调色待定'];
}

function buildFallbackCharacterSpec(session: GenSession, sourceImage: GenImage): CharacterSpec {
  const prompt = sourceImage.prompt || session.prompt;
  const name = prompt.includes('AI') ? 'AI 陪伴玩具' : '未命名玩具 IP';
  const materials = prompt.includes('毛绒') ? ['短毛绒/超柔面料', '刺绣五官', 'PP 棉填充'] : ['主体材料待定', '表面工艺待定'];
  const accessories = ['标志性配件待拆解', '吊牌/水洗标预留'];
  if (prompt.includes('机甲')) accessories.unshift('机甲头盔');
  if (prompt.includes('M logo') || prompt.includes('M logo'.toLowerCase())) accessories.unshift('胸前 M 标识');

  return {
    name,
    oneLiner: prompt,
    targetUser: '潮玩/礼品/品牌联名用户',
    speciesShape: prompt.includes('机甲') ? '圆胖机甲陪伴娃娃' : '圆胖毛绒陪伴娃娃',
    bodyRatio: '大头圆胖体型，建议头身比 1:1.2，坐姿或轻微站姿均可打样',
    faceFeatures: '圆润亲和表情，眼睛、嘴巴、腮红需保持与源图一致',
    colorPalette: pickPalette(prompt),
    materials,
    accessories,
    signatureElements: ['圆胖轮廓', '高识别五官', '胸前/身体标志元素', '可被专利图清楚表达的配件轮廓'],
    manufacturingNotes: ['避免过细尖角和过多小件', '配件连接处需预留缝线或固定方式', '刺绣/印花边界需单独放大确认'],
    patentFocus: ['整体轮廓', '头身比例', '五官组合', '配件造型', '正背侧轮廓差异'],
    marketingAngle: ['AI 陪伴感', '治愈礼物属性', '可收藏 IP 角色', '柔软可抱材质'],
    negativePrompt: '不要改变角色五官、主配色、核心配件；不要加入无关品牌、水印、价格和营销贴纸',
    sourceImageId: sourceImage.id,
    sourceImageUrl: sourceImage.url,
    lockedAt: Date.now(),
  };
}

function asStringArray(value: unknown, fallback: string[]): string[] {
  if (Array.isArray(value)) {
    const items = value.map(item => String(item).trim()).filter(Boolean);
    return items.length > 0 ? items : fallback;
  }
  if (typeof value === 'string' && value.trim()) return [value.trim()];
  return fallback;
}

function normalizeCharacterSpec(spec: CharacterSpec, fallback: CharacterSpec, sourceImage: GenImage): CharacterSpec {
  return {
    ...fallback,
    ...spec,
    name: spec.name || fallback.name,
    oneLiner: spec.oneLiner || fallback.oneLiner,
    targetUser: spec.targetUser || fallback.targetUser,
    speciesShape: spec.speciesShape || fallback.speciesShape,
    bodyRatio: spec.bodyRatio || fallback.bodyRatio,
    faceFeatures: spec.faceFeatures || fallback.faceFeatures,
    colorPalette: asStringArray(spec.colorPalette, fallback.colorPalette),
    materials: asStringArray(spec.materials, fallback.materials),
    accessories: asStringArray(spec.accessories, fallback.accessories),
    signatureElements: asStringArray(spec.signatureElements, fallback.signatureElements),
    manufacturingNotes: asStringArray(spec.manufacturingNotes, fallback.manufacturingNotes),
    patentFocus: asStringArray(spec.patentFocus, fallback.patentFocus),
    marketingAngle: asStringArray(spec.marketingAngle, fallback.marketingAngle),
    negativePrompt: spec.negativePrompt || fallback.negativePrompt,
    sourceImageId: sourceImage.id,
    sourceImageUrl: sourceImage.url,
    lockedAt: typeof spec.lockedAt === 'number' ? spec.lockedAt : Date.now(),
  };
}

export async function buildCharacterSpec(session: GenSession, sourceImage: GenImage): Promise<CharacterSpec> {
  const fallback = buildFallbackCharacterSpec(session, sourceImage);
  if (session.inputMode === 'replicate' || session.inputMode === 'extend' || sourceImage.meta?.source === 'upload') {
    const inferred = await inferCharacterSpecFromImage({
      imageUrl: sourceImage.url,
      userHint: session.prompt,
      fallback,
    });
    return normalizeCharacterSpec(inferred, fallback, sourceImage);
  }
  const generated = await generateGptJson<CharacterSpec>({
    fallback,
    prompt: [
      '你是资深毛绒玩具产品经理、外观专利素材规划师和工厂打样顾问。',
      '请根据用户 prompt 与选中图片 URL，输出严格 JSON。不要 markdown，不要解释。',
      '字段必须完整匹配：name, oneLiner, targetUser, speciesShape, bodyRatio, faceFeatures, colorPalette, materials, accessories, signatureElements, manufacturingNotes, patentFocus, marketingAngle, negativePrompt, sourceImageId, sourceImageUrl, lockedAt。',
      '数组字段必须为字符串数组。lockedAt 使用给定时间戳。',
      `当前时间戳：${Date.now()}`,
      `用户 prompt：${session.prompt}`,
      `源图 ID：${sourceImage.id}`,
      `源图 URL：${sourceImage.url}`,
      `兜底 JSON：${JSON.stringify(fallback)}`,
    ].join('\n'),
  });
  return normalizeCharacterSpec(generated, fallback, sourceImage);
}

function renderPrompt(template: string, spec: CharacterSpec, sourceImageUrl: string): string {
  return [
    template.replace('{character}', renderCharacterSummary(spec)),
    `参考源图：${sourceImageUrl}`,
    '必须保持同一角色身份、五官、配色、主体比例和核心配件一致。',
  ].join('\n');
}

function sizeForAspect(aspectRatio: '1:1' | '3:4' | '4:5' | '9:16' | '16:9' | 'long'): '1024x1024' | '1024x1536' | '1536x1024' {
  if (aspectRatio === '16:9') return '1536x1024';
  if (aspectRatio === '1:1') return '1024x1024';
  return '1024x1536';
}

function resolveRootAnchor(characterSpec: CharacterSpec, sourceImage: GenImage): string {
  return characterSpec.cleanReferenceImageUrl || characterSpec.sourceImageUrl || sourceImage.url;
}

function sortTemplatesByAnchor<T extends { id: string; anchorTemplateId?: string }>(templates: T[]): T[] {
  const remaining = [...templates];
  const done = new Set<string>();
  const sorted: T[] = [];
  while (remaining.length > 0) {
    const index = remaining.findIndex(template => !template.anchorTemplateId || done.has(template.anchorTemplateId));
    if (index === -1) {
      throw new Error(`template anchor cycle or missing root: ${remaining.map(template => template.id).join(', ')}`);
    }
    const [template] = remaining.splice(index, 1);
    sorted.push(template);
    done.add(template.id);
  }
  return sorted;
}

export async function cleanupCharacterAnchor(opts: {
  session: GenSession;
  sourceImage: GenImage;
  characterSpec?: CharacterSpec;
  force?: boolean;
  preserveLevel?: 'normal' | 'strict';
}): Promise<{ characterSpec: CharacterSpec; cleanReferenceImageUrl: string; provider: 'mock' | 'gpt' }> {
  const provider = detectProvider();
  const characterSpec = opts.characterSpec ?? opts.session.characterSpec ?? await buildCharacterSpec(opts.session, opts.sourceImage);

  if (!opts.force && characterSpec.cleanReferenceImageUrl) {
    return { characterSpec, cleanReferenceImageUrl: characterSpec.cleanReferenceImageUrl, provider };
  }

  const strictPrompt = [
    '保持原图中的核心玩具角色完全一致，仅做产品图净化和主体抽取。',
    '',
    '如果参考图是多宫格提案板、品牌手册、包装展示页、海报拼图或包含多个小图的 collage：',
    '1. 只抽取最大、最完整、最清楚的正面全身主角色玩具，通常来自“核心形象 / IP形象设定 / 01”区域',
    '2. 不要保留提案板版式、分割线、标题编号、说明文字、证书、真人展示、包装盒、配件平铺图或其它小场景图',
    '3. 不要把多张小图重新拼成一张板；最终只能有一个主体玩具',
    '',
    '背景和构图要求：',
    '1. 把背景换成纯白色',
    '2. 去除水印、价格标签、网页 UI 元素、海报标题和版面说明文字',
    '3. 居中并适当裁剪到正方形构图，完整保留头、身体、四肢和主要随身配件',
    '',
    '绝对不要修改：',
    '- 角色五官、表情、姿态',
    '- 主体配色、材质、纹理',
    '- 配件位置、轮廓、细节',
    '- 玩具本体上的设计标识和识别符号，例如衣服图案、帽标、面罩声波图案',
    '',
    '输出风格：商业产品图，柔和均匀打光，无阴影。',
    `角色设定：${renderCharacterSummary(characterSpec)}`,
  ].join('\n');

  const normalPrompt = [
    '保持参考图中的玩具角色完全一致，只做产品图净化。',
    '把背景换成纯白色，产品居中，正面或轻微正面视角，光线均匀。',
    '不要改变五官、主配色、身体比例、毛绒材质、核心配件和识别元素。',
    '不要文字、水印、logo、场景道具、价格、促销贴纸。',
    `角色设定：${renderCharacterSummary(characterSpec)}`,
  ].join('\n');

  const prompt = opts.preserveLevel === 'strict' ? strictPrompt : normalPrompt;

  const image = provider === 'gpt'
    ? await generateGptImageEdit({
      sessionId: `${opts.session.id}_clean`,
      prompt,
      anchorImage: opts.sourceImage.url,
      size: '1024x1024',
    })
    : (await generateMock({
      sessionId: `${opts.session.id}_clean`,
      prompt,
      count: 1,
    }))[0];

  if (!image) throw new Error('clean anchor generation failed');
  const cleanReferenceImageUrl = image.url.startsWith('data:')
    ? await saveAnchorImage(opts.session.id, opts.sourceImage.id, image.url)
    : image.url;

  return {
    characterSpec: {
      ...characterSpec,
      sourceImageId: opts.sourceImage.id,
      sourceImageUrl: opts.sourceImage.url,
      cleanReferenceImageUrl,
    },
    cleanReferenceImageUrl,
    provider,
  };
}

async function generateAssetImage(opts: {
  packId: string;
  assetId: string;
  prompt: string;
  anchorImageUrl: string;
  aspectRatio: '1:1' | '3:4' | '4:5' | '9:16' | '16:9' | 'long';
}): Promise<{ url: string; provider: 'mock' | 'gpt'; raw?: unknown }> {
  const provider = detectProvider();
  const images = provider === 'gpt'
    ? [await generateGptImageEdit({
      sessionId: `${opts.packId}_${opts.assetId}`,
      prompt: opts.prompt,
      anchorImage: opts.anchorImageUrl,
      size: sizeForAspect(opts.aspectRatio),
    })]
    : await generateMock({
      sessionId: `${opts.packId}_${opts.assetId}`,
      prompt: opts.prompt,
      count: 1,
    });

  const image = images[0];
  if (!image) throw new Error('pack asset generation failed');
  if (image.url.startsWith('data:')) {
    return { url: await savePackImage(opts.packId, opts.assetId, image.url), provider, raw: image.meta };
  }
  return { url: image.url, provider, raw: image.meta };
}

function buildFilename(opts: {
  sessionId: string;
  characterName: string;
  kind: PackKind;
  view: string;
  version: string;
  url: string;
}): string {
  const extMatch = opts.url.match(/\.([a-z0-9]+)(?:\?|$)/i);
  const ext = extMatch?.[1] || 'png';
  return `${opts.sessionId}_${slugify(opts.characterName)}_${opts.kind}_${opts.view}_${opts.version}.${ext}`;
}

export async function generateAssetPack(opts: {
  session: GenSession;
  sourceImage: GenImage;
  kind: PackKind;
  onProgress?: (pack: AssetPack) => Promise<void>;
}): Promise<{ pack: AssetPack; manifest: ExportManifest; provider: 'mock' | 'gpt' }> {
  const templates = sortTemplatesByAnchor(getPackTemplates(opts.kind));
  const initialCharacterSpec = opts.session.characterSpec?.sourceImageId === opts.sourceImage.id
    ? opts.session.characterSpec
    : await buildCharacterSpec(opts.session, opts.sourceImage);
  const cleaned = await cleanupCharacterAnchor({
    session: opts.session,
    sourceImage: opts.sourceImage,
    characterSpec: initialCharacterSpec,
  });
  const characterSpec = cleaned.characterSpec;
  const version = 'v01';
  const packId = `pack_${opts.kind}_${Date.now().toString(36)}_${randomBytes(3).toString('hex')}`;
  const createdAt = Date.now();
  const provider = detectProvider();

  const assets: ToyAsset[] = [];
  const pack: AssetPack = {
    id: packId,
    kind: opts.kind,
    sessionId: opts.session.id,
    sourceImageId: opts.sourceImage.id,
    sourceImageUrl: opts.sourceImage.url,
    characterSpec,
    assets,
    manifestId: `manifest_${packId}`,
    createdAt,
    version,
    status: 'draft',
  };

  for (const template of templates) {
    const assetId = `${opts.kind}_${template.filenamePart}_${randomBytes(3).toString('hex')}`;
    const anchorAsset = template.anchorTemplateId
      ? assets.find(asset => asset.templateId === template.anchorTemplateId)
      : undefined;
    if (template.anchorTemplateId && !anchorAsset) {
      throw new Error(`anchor ${template.anchorTemplateId} not generated yet`);
    }
    const anchorImageUrl = anchorAsset?.url ?? resolveRootAnchor(characterSpec, opts.sourceImage);
    const prompt = renderPrompt(template.promptTemplate, characterSpec, anchorImageUrl);
    const preFilledSlot = opts.session.preFilledSlots?.find(slot => slot.templateId === template.id);
    if (preFilledSlot) {
      assets.push({
        id: assetId,
        templateId: template.id,
        kind: opts.kind,
        view: template.view,
        title: template.title,
        description: template.description,
        url: preFilledSlot.url,
        prompt: [
          `用户上传图已占用槽位：${template.id}`,
          prompt,
        ].join('\n'),
        status: 'draft',
        version,
        aspectRatio: template.aspectRatio,
        required: template.required,
        createdAt: Date.now(),
        anchorAssetId: anchorAsset?.id,
        anchorImageUrl,
        derivationLevel: anchorAsset ? 3 : 2,
        meta: {
          provider: 'upload',
          uploadedImageId: preFilledSlot.uploadedImageId,
          uploadRole: preFilledSlot.role,
          packLabel: PACK_LABELS[opts.kind],
          templateFreezeVersion: TEMPLATE_FREEZE_VERSION,
          anchorTemplateId: template.anchorTemplateId,
        },
      });
      await opts.onProgress?.(pack);
      continue;
    }
    const generated = await generateAssetImage({
      packId,
      assetId,
      prompt,
      anchorImageUrl,
      aspectRatio: template.aspectRatio,
    });
    assets.push({
      id: assetId,
      templateId: template.id,
      kind: opts.kind,
      view: template.view,
      title: template.title,
      description: template.description,
      url: generated.url,
      prompt,
      status: 'draft',
      version,
      aspectRatio: template.aspectRatio,
      required: template.required,
      createdAt: Date.now(),
      anchorAssetId: anchorAsset?.id,
      anchorImageUrl,
      derivationLevel: anchorAsset ? 3 : 2,
      meta: {
        provider: generated.provider,
        packLabel: PACK_LABELS[opts.kind],
        templateFreezeVersion: TEMPLATE_FREEZE_VERSION,
        anchorTemplateId: template.anchorTemplateId,
        raw: generated.raw,
      },
    });
    await opts.onProgress?.(pack);
  }

  pack.status = 'complete';

  const manifest: ExportManifest = {
    id: pack.manifestId,
    sessionId: opts.session.id,
    packId,
    packKind: opts.kind,
    version,
    createdAt,
    filenameSchema: FILENAME_SCHEMA,
    templateFreezeVersion: TEMPLATE_FREEZE_VERSION,
    source: {
      prompt: opts.session.prompt,
      sourceImageId: opts.sourceImage.id,
      sourceImageUrl: opts.sourceImage.url,
    },
    characterSpec,
    files: assets.map(asset => {
      const template = templates.find(t => t.id === asset.templateId);
      return {
        assetId: asset.id,
        templateId: asset.templateId,
        filename: buildFilename({
          sessionId: opts.session.id,
          characterName: characterSpec.name,
          kind: opts.kind,
          view: asset.view,
          version: asset.version,
          url: asset.url,
        }),
        url: asset.url,
        kind: asset.kind,
        view: asset.view,
        title: asset.title,
        required: asset.required,
        aspectRatio: asset.aspectRatio,
        prompt: asset.prompt,
        anchorAssetId: asset.anchorAssetId,
        anchorImageUrl: asset.anchorImageUrl,
        derivationLevel: asset.derivationLevel,
        checklist: template?.checklist ?? [],
      };
    }),
    exportTargets: ['zip', 'pdf', 'manifest-json'],
  };

  await saveExportManifest(manifest);
  return { pack, manifest, provider };
}

export async function regeneratePackAsset(opts: {
  session: GenSession;
  assetId: string;
  userRefinement?: string;
}): Promise<{ pack: AssetPack; asset: ToyAsset; provider: 'mock' | 'gpt' }> {
  const pack = opts.session.packs?.find(candidate => candidate.assets.some(asset => asset.id === opts.assetId));
  if (!pack) throw new Error('asset pack not found');
  const assetIndex = pack.assets.findIndex(asset => asset.id === opts.assetId);
  const asset = pack.assets[assetIndex];
  if (!asset) throw new Error('asset not found');
  const template = getPackTemplates(pack.kind).find(candidate => candidate.id === asset.templateId);
  if (!template) throw new Error(`template not found: ${asset.templateId}`);

  const anchorAsset = template.anchorTemplateId
    ? pack.assets.find(candidate => candidate.templateId === template.anchorTemplateId)
    : undefined;
  if (template.anchorTemplateId && !anchorAsset) {
    throw new Error(`anchor ${template.anchorTemplateId} not generated yet`);
  }

  const sourceImage = opts.session.images.find(image => image.id === pack.sourceImageId);
  const anchorImageUrl = anchorAsset?.url
    ?? pack.characterSpec.cleanReferenceImageUrl
    ?? pack.characterSpec.sourceImageUrl
    ?? sourceImage?.url;
  if (!anchorImageUrl) throw new Error('anchor image not found');

  const prompt = [
    renderPrompt(template.promptTemplate, pack.characterSpec, anchorImageUrl),
    opts.userRefinement?.trim() ? `用户重做要求：${opts.userRefinement.trim()}` : '',
  ].filter(Boolean).join('\n');

  const generated = await generateAssetImage({
    packId: pack.id,
    assetId: asset.id,
    prompt,
    anchorImageUrl,
    aspectRatio: template.aspectRatio,
  });

  const updatedAsset: ToyAsset = {
    ...asset,
    url: generated.url,
    prompt,
    status: 'draft',
    createdAt: Date.now(),
    anchorAssetId: anchorAsset?.id,
    anchorImageUrl,
    derivationLevel: anchorAsset ? 3 : 2,
    meta: {
      ...(asset.meta ?? {}),
      provider: generated.provider,
      anchorTemplateId: template.anchorTemplateId,
      regeneratedAt: Date.now(),
      raw: generated.raw,
    },
  };
  pack.assets[assetIndex] = updatedAsset;
  return { pack, asset: updatedAsset, provider: detectProvider() };
}
