import { env } from '$env/dynamic/private';
import { json } from '@sveltejs/kit';
import { createHash } from 'node:crypto';
import type { RequestHandler } from './$types';

type ChatRole = 'user' | 'assistant';
type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack';

type Draft = {
	food_name?: string;
	meal_type?: MealType;
	entry_date?: string;
	quantity?: number;
	unit?: string;
	calories?: number;
	protein?: number;
	carbs?: number;
	fat?: number;
	sugar?: number;
	fiber?: number;
	note?: string;
	default_serving_label?: string;
};

type DraftBundle = Draft[];

type ChatMessage = {
	role?: ChatRole;
	content?: string;
};

type ResolvedFood = {
	id: string;
	name: string;
	status?: string;
	base_unit?: string;
	calories_per_base?: number;
	protein_per_base?: number;
	carbs_per_base?: number;
	fat_per_base?: number;
	sugar_per_base?: number;
	fiber_per_base?: number;
	servings?: Array<{ id?: string; name?: string; amount_in_base?: number; is_default?: boolean }>;
};

function todayIso(): string {
	const now = new Date();
	const year = now.getFullYear();
	const month = String(now.getMonth() + 1).padStart(2, '0');
	const day = String(now.getDate()).padStart(2, '0');
	return `${year}-${month}-${day}`;
}

function toNumber(value: unknown, fallback = 0): number {
	if (typeof value === 'number' && Number.isFinite(value)) return value;
	if (typeof value === 'string') {
		const parsed = Number(value);
		if (Number.isFinite(parsed)) return parsed;
	}
	return fallback;
}

function clampDraft(input: Record<string, unknown> | null | undefined): Draft {
	const meal = input?.meal_type;
	const normalizedMeal: MealType =
		meal === 'breakfast' || meal === 'lunch' || meal === 'dinner' || meal === 'snack'
			? meal
			: 'snack';

	return {
		food_name: typeof input?.food_name === 'string' ? input.food_name.trim() : '',
		meal_type: normalizedMeal,
		entry_date: typeof input?.entry_date === 'string' && input.entry_date ? input.entry_date : todayIso(),
		quantity: Math.max(toNumber(input?.quantity, 1), 0),
		unit: typeof input?.unit === 'string' && input.unit ? input.unit.trim() : 'serving',
		calories: Math.max(toNumber(input?.calories), 0),
		protein: Math.max(toNumber(input?.protein), 0),
		carbs: Math.max(toNumber(input?.carbs), 0),
		fat: Math.max(toNumber(input?.fat), 0),
		sugar: Math.max(toNumber(input?.sugar), 0),
		fiber: Math.max(toNumber(input?.fiber), 0),
		note: typeof input?.note === 'string' ? input.note.trim() : '',
		default_serving_label:
			typeof input?.default_serving_label === 'string' ? input.default_serving_label.trim() : ''
	};
}

function parseLeadingQuantity(name: string): { quantity: number | null; cleanedName: string } {
	const trimmed = name.trim();
	const match = trimmed.match(/^(\d+(?:\.\d+)?)\s+(.+)$/);
	if (!match) {
		return { quantity: null, cleanedName: trimmed };
	}
	return {
		quantity: Number(match[1]),
		cleanedName: match[2].trim()
	};
}

function canonicalFoodName(name: string): string {
	const cleaned = name
		.trim()
		.replace(/^(?:a|an|the)\s+/i, '')
		.replace(/\s+/g, ' ');

	return cleaned
		.split(' ')
		.filter(Boolean)
		.map((part) => {
			if (/[A-Z]{2,}/.test(part)) return part;
			return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
		})
		.join(' ');
}

function normalizedFoodKey(name: string): string {
	return canonicalFoodName(name)
		.toLowerCase()
		.replace(/[^a-z0-9\s]/g, ' ')
		.replace(/\b(\d+(?:\.\d+)?)\b/g, ' ')
		.replace(/\s+/g, ' ')
		.trim();
}

function shouldReuseResolvedFood(canonicalName: string, resolved: ResolvedFood | null, confidence: number): boolean {
	if (!resolved?.name) return false;
	if (confidence < 0.9) return false;

	const draftKey = normalizedFoodKey(canonicalName);
	const resolvedKey = normalizedFoodKey(resolved.name);
	if (!draftKey || !resolvedKey) return false;

	return draftKey === resolvedKey;
}

function foodBaseUnit(draft: Draft): string {
	const unit = (draft.unit || 'serving').trim().toLowerCase();
	if (unit === 'piece' || unit === 'slice' || unit === 'cup' || unit === 'scoop' || unit === 'serving') {
		return unit;
	}
	return 'serving';
}

function defaultServingName(baseUnit: string): string {
	return `1 ${baseUnit}`;
}

function hasMaterialNutritionMismatch(draft: Draft, matchedFood: ResolvedFood, entryQuantity: number): boolean {
	const draftPerBase = {
		calories: Math.max((draft.calories || 0) / entryQuantity, 0),
		protein: Math.max((draft.protein || 0) / entryQuantity, 0),
		carbs: Math.max((draft.carbs || 0) / entryQuantity, 0),
		fat: Math.max((draft.fat || 0) / entryQuantity, 0),
		sugar: Math.max((draft.sugar || 0) / entryQuantity, 0),
		fiber: Math.max((draft.fiber || 0) / entryQuantity, 0)
	};

	const currentPerBase = {
		calories: matchedFood.calories_per_base || 0,
		protein: matchedFood.protein_per_base || 0,
		carbs: matchedFood.carbs_per_base || 0,
		fat: matchedFood.fat_per_base || 0,
		sugar: matchedFood.sugar_per_base || 0,
		fiber: matchedFood.fiber_per_base || 0
	};

	return (Object.keys(draftPerBase) as Array<keyof typeof draftPerBase>).some((key) => {
		const next = draftPerBase[key];
		const current = currentPerBase[key];
		if (Math.abs(next - current) >= 5) return true;
		if (Math.max(next, current) <= 0) return false;
		return Math.abs(next - current) / Math.max(next, current) >= 0.12;
	});
}

function entryIdempotencyKey(draft: Draft, index = 0): string {
	return createHash('sha256')
		.update(
			JSON.stringify({
				index,
				food_name: draft.food_name || '',
				meal_type: draft.meal_type || 'snack',
				entry_date: draft.entry_date || todayIso(),
				quantity: draft.quantity || 1,
				unit: draft.unit || 'serving',
				calories: draft.calories || 0,
				protein: draft.protein || 0,
				carbs: draft.carbs || 0,
				fat: draft.fat || 0,
				sugar: draft.sugar || 0,
				fiber: draft.fiber || 0,
				note: draft.note || ''
			})
		)
		.digest('hex');
}

function hasCompleteDraft(draft: Draft): boolean {
	return !!draft.food_name && !!draft.meal_type && Number.isFinite(draft.calories ?? NaN);
}

function hasCompleteDrafts(drafts: DraftBundle | null | undefined): drafts is DraftBundle {
	return Array.isArray(drafts) && drafts.length > 0 && drafts.every((draft) => hasCompleteDraft(draft));
}

function isExplicitConfirmation(message: string): boolean {
	const text = message.trim().toLowerCase();
	if (!text) return false;

	return [
		/^add it[.!]?$/,
		/^log it[.!]?$/,
		/^save it[.!]?$/,
		/^looks good[.!]?$/,
		/^looks good add it[.!]?$/,
		/^that looks good[.!]?$/,
		/^that looks good add it[.!]?$/,
		/^confirm[.!]?$/,
		/^go ahead[.!]?$/,
		/^yes add it[.!]?$/,
		/^yes log it[.!]?$/,
		/^yes save it[.!]?$/
	].some((pattern) => pattern.test(text));
}

function isRetryRequest(message: string): boolean {
	const text = message.trim().toLowerCase();
	if (!text) return false;

	return [
		"that's not right",
		"that is not right",
		'wrong item',
		'wrong food',
		'wrong meal',
		'try again',
		'search again',
		'guess again',
		'redo that',
		'start over',
		'that is wrong',
		"that's wrong"
	].some((phrase) => text.includes(phrase));
}

function draftForRetry(draft: Draft): Draft {
	return {
		meal_type: draft.meal_type,
		entry_date: draft.entry_date || todayIso(),
		quantity: draft.quantity || 1,
		unit: draft.unit || 'serving',
		food_name: '',
		calories: 0,
		protein: 0,
		carbs: 0,
		fat: 0,
		sugar: 0,
		fiber: 0,
		note: draft.note || ''
	};
}

async function applyDraft(fetchFn: typeof fetch, draft: Draft, index = 0) {
	const parsedName = parseLeadingQuantity(draft.food_name || '');
	const entryQuantity = Math.max(
		draft.quantity && draft.quantity > 0 ? draft.quantity : parsedName.quantity || 1,
		0.1
	);
	const canonicalName = canonicalFoodName(parsedName.cleanedName || draft.food_name || 'Quick add');
	const baseUnit = foodBaseUnit(draft);

	const resolveResponse = await fetchFn('/api/fitness/foods/resolve', {
		method: 'POST',
		headers: { 'content-type': 'application/json' },
		body: JSON.stringify({
			raw_phrase: canonicalName,
			meal_type: draft.meal_type || 'snack',
			entry_date: draft.entry_date || todayIso(),
			source: 'assistant'
		})
	});

	const resolveBody = await resolveResponse.json().catch(() => ({}));
	let matchedFood: ResolvedFood | null =
		resolveResponse.ok && shouldReuseResolvedFood(canonicalName, resolveBody?.matched_food ?? null, Number(resolveBody?.confidence || 0))
			? (resolveBody?.matched_food ?? null)
			: null;

	if (!matchedFood) {
		const perBaseCalories = Math.max((draft.calories || 0) / entryQuantity, 0);
		const perBaseProtein = Math.max((draft.protein || 0) / entryQuantity, 0);
		const perBaseCarbs = Math.max((draft.carbs || 0) / entryQuantity, 0);
		const perBaseFat = Math.max((draft.fat || 0) / entryQuantity, 0);
		const perBaseSugar = Math.max((draft.sugar || 0) / entryQuantity, 0);
		const perBaseFiber = Math.max((draft.fiber || 0) / entryQuantity, 0);

		const createFoodResponse = await fetchFn('/api/fitness/foods', {
			method: 'POST',
			headers: { 'content-type': 'application/json' },
			body: JSON.stringify({
				name: canonicalName,
				calories_per_base: perBaseCalories,
				protein_per_base: perBaseProtein,
				carbs_per_base: perBaseCarbs,
				fat_per_base: perBaseFat,
				sugar_per_base: perBaseSugar,
				fiber_per_base: perBaseFiber,
				base_unit: baseUnit,
				status: 'assistant_created',
				notes: `Assistant created from chat draft: ${draft.food_name || canonicalName}`,
				servings: [
					{
						name: draft.default_serving_label?.trim() || defaultServingName(baseUnit),
						amount_in_base: 1.0,
						is_default: true
					}
				]
			})
		});

		const createdFoodBody = await createFoodResponse.json().catch(() => ({}));
		if (!createFoodResponse.ok) {
			return { ok: false, status: createFoodResponse.status, body: createdFoodBody };
		}
		matchedFood = createdFoodBody as ResolvedFood;
	} else if (
		(matchedFood.status === 'assistant_created' || matchedFood.status === 'ai_created') &&
		hasMaterialNutritionMismatch(draft, matchedFood, entryQuantity)
	) {
		const updateFoodResponse = await fetchFn(`/api/fitness/foods/${matchedFood.id}`, {
			method: 'PATCH',
			headers: { 'content-type': 'application/json' },
			body: JSON.stringify({
				calories_per_base: Math.max((draft.calories || 0) / entryQuantity, 0),
				protein_per_base: Math.max((draft.protein || 0) / entryQuantity, 0),
				carbs_per_base: Math.max((draft.carbs || 0) / entryQuantity, 0),
				fat_per_base: Math.max((draft.fat || 0) / entryQuantity, 0),
				sugar_per_base: Math.max((draft.sugar || 0) / entryQuantity, 0),
				fiber_per_base: Math.max((draft.fiber || 0) / entryQuantity, 0)
			})
		});
		const updatedFoodBody = await updateFoodResponse.json().catch(() => ({}));
		if (updateFoodResponse.ok) {
			matchedFood = updatedFoodBody as ResolvedFood;
		}
	}

	const entryPayload = {
		food_id: matchedFood.id,
		quantity: entryQuantity,
		unit: baseUnit,
		serving_id: matchedFood.servings?.find((serving) => serving.is_default)?.id,
		meal_type: draft.meal_type || 'snack',
		entry_date: draft.entry_date || todayIso(),
		entry_method: 'assistant',
		source: 'assistant',
		note: draft.note || undefined,
		idempotency_key: entryIdempotencyKey(draft, index),
		snapshot_food_name_override:
			draft.food_name && draft.food_name.trim() !== canonicalName ? draft.food_name.trim() : undefined
	};

	const response = await fetchFn('/api/fitness/entries', {
		method: 'POST',
		headers: { 'content-type': 'application/json' },
		body: JSON.stringify(entryPayload)
	});

	const body = await response.json().catch(() => ({}));
	return { ok: response.ok, status: response.status, body };
}

async function splitInputItems(fetchFn: typeof fetch, phrase: string): Promise<string[]> {
	try {
		const response = await fetchFn('/api/fitness/foods/split', {
			method: 'POST',
			headers: { 'content-type': 'application/json' },
			body: JSON.stringify({ phrase })
		});
		const body = await response.json().catch(() => ({}));
		if (response.ok && Array.isArray(body?.items) && body.items.length > 0) {
			return body.items.map((item: unknown) => String(item).trim()).filter(Boolean);
		}
	} catch {
		// fall back below
	}

	return phrase
		.split(/,/)
		.map((item) => item.trim())
		.filter(Boolean);
}

function draftFromResolvedItem(resolved: Record<string, unknown>, entryDate: string): Draft {
	const parsed = (resolved?.parsed as Record<string, unknown> | undefined) || {};
	const matchedFood = (resolved?.matched_food as Record<string, unknown> | undefined) || null;
	const aiEstimate = (resolved?.ai_estimate as Record<string, unknown> | undefined) || null;
	const quantity = Math.max(toNumber(parsed.quantity, 1), 0.1);
	const unit = typeof parsed.unit === 'string' && parsed.unit ? parsed.unit : 'serving';
	const meal = parsed.meal_type;
	const mealType: MealType =
		meal === 'breakfast' || meal === 'lunch' || meal === 'dinner' || meal === 'snack'
			? meal
			: 'snack';

	let calories = 0;
	let protein = 0;
	let carbs = 0;
	let fat = 0;
	let sugar = 0;
	let fiber = 0;

	if (matchedFood) {
		calories = toNumber(matchedFood.calories_per_base) * quantity;
		protein = toNumber(matchedFood.protein_per_base) * quantity;
		carbs = toNumber(matchedFood.carbs_per_base) * quantity;
		fat = toNumber(matchedFood.fat_per_base) * quantity;
		sugar = toNumber(matchedFood.sugar_per_base) * quantity;
		fiber = toNumber(matchedFood.fiber_per_base) * quantity;
	} else if (aiEstimate) {
		calories = toNumber(aiEstimate.calories_per_base) * quantity;
		protein = toNumber(aiEstimate.protein_per_base) * quantity;
		carbs = toNumber(aiEstimate.carbs_per_base) * quantity;
		fat = toNumber(aiEstimate.fat_per_base) * quantity;
		sugar = toNumber(aiEstimate.sugar_per_base) * quantity;
		fiber = toNumber(aiEstimate.fiber_per_base) * quantity;
	} else if (resolved?.resolution_type === 'quick_add') {
		calories = toNumber(parsed.quantity);
	}

	const foodName =
		(typeof resolved?.snapshot_name_override === 'string' && resolved.snapshot_name_override) ||
		(typeof matchedFood?.name === 'string' && matchedFood.name) ||
		(typeof aiEstimate?.food_name === 'string' && aiEstimate.food_name) ||
		(typeof resolved?.raw_text === 'string' && resolved.raw_text) ||
		'Quick add';

	return clampDraft({
		food_name: foodName,
		meal_type: mealType,
		entry_date: entryDate,
		quantity,
		unit,
		calories: Math.round(calories),
		protein: Math.round(protein),
		carbs: Math.round(carbs),
		fat: Math.round(fat),
		sugar: Math.round(sugar),
		fiber: Math.round(fiber),
		note: typeof resolved?.note === 'string' ? resolved.note : '',
		default_serving_label:
			(typeof aiEstimate?.serving_description === 'string' && aiEstimate.serving_description) ||
			(quantity > 0 && unit ? `${quantity} ${unit}` : '')
	});
}

async function reviseDraftBundle(
	fetchFn: typeof fetch,
	messages: Array<{ role: 'user' | 'assistant'; content: string }>,
	drafts: DraftBundle,
	imageDataUrl: string | null
): Promise<{ reply: string; drafts: DraftBundle } | null> {
	if (!env.OPENAI_API_KEY || drafts.length === 0) return null;

	const systemPrompt = `You are revising a bundled food draft inside a fitness app.

Return ONLY JSON like:
{
  "reply": "short assistant reply",
  "drafts": [
    {
      "food_name": "string",
      "meal_type": "breakfast|lunch|dinner|snack",
      "entry_date": "YYYY-MM-DD",
      "quantity": 1,
      "unit": "serving",
      "calories": 0,
      "protein": 0,
      "carbs": 0,
      "fat": 0,
      "sugar": 0,
      "fiber": 0,
      "note": "",
      "default_serving_label": ""
    }
  ]
}

Rules:
- Update only the item or items the user is correcting.
- Keep untouched items unchanged.
- If the user says one item is wrong, replace that item without collapsing the bundle into one merged food.
- Preserve meal and entry date unless the user changes them.
- Keep replies brief and natural.
- If a photo is attached, you may use it again for corrections.`;

	const userMessages = messages.map((message) => ({
		role: message.role,
		content: message.content
	}));

	if (imageDataUrl) {
		const latestUserText = [...messages].reverse().find((message) => message.role === 'user')?.content || 'Revise these items.';
		userMessages.push({
			role: 'user',
			content: [
				{ type: 'text', text: latestUserText },
				{ type: 'image_url', image_url: { url: imageDataUrl } }
			]
		} as unknown as { role: ChatRole; content: string });
	}

	const response = await fetch('https://api.openai.com/v1/chat/completions', {
		method: 'POST',
		headers: {
			'content-type': 'application/json',
			authorization: `Bearer ${env.OPENAI_API_KEY}`
		},
		body: JSON.stringify({
			model: env.OPENAI_MODEL || 'gpt-5.2',
			response_format: { type: 'json_object' },
			temperature: 0.2,
			max_completion_tokens: 1000,
			messages: [
				{
					role: 'system',
					content: `${systemPrompt}\n\nCurrent bundle:\n${JSON.stringify(drafts, null, 2)}`
				},
				...userMessages
			]
		})
	});

	if (!response.ok) return null;
	const raw = await response.json().catch(() => ({}));
	const content = raw?.choices?.[0]?.message?.content;
	if (typeof content !== 'string') return null;

	try {
		const parsed = JSON.parse(content);
		const nextDrafts = Array.isArray(parsed?.drafts)
			? parsed.drafts.map((draft: Record<string, unknown>) => clampDraft(draft))
			: [];
		if (!hasCompleteDrafts(nextDrafts)) return null;
		return {
			reply: typeof parsed?.reply === 'string' ? parsed.reply : 'I updated those items.',
			drafts: nextDrafts
		};
	} catch {
		return null;
	}
}

async function buildDraftBundle(fetchFn: typeof fetch, phrase: string, entryDate: string): Promise<DraftBundle | null> {
	const parts = await splitInputItems(fetchFn, phrase);
	if (parts.length < 2) return null;

	const results = await Promise.all(
		parts.map(async (part) => {
			const response = await fetchFn('/api/fitness/foods/resolve', {
				method: 'POST',
				headers: { 'content-type': 'application/json' },
				body: JSON.stringify({
					raw_phrase: part,
					entry_date: entryDate,
					source: 'assistant'
				})
			});
			if (!response.ok) {
				throw new Error(`Failed to resolve "${part}"`);
			}
			const body = await response.json().catch(() => ({}));
			return draftFromResolvedItem(body, entryDate);
		})
	);

	return results.filter((draft) => hasCompleteDraft(draft));
}

export const POST: RequestHandler = async ({ request, fetch, cookies }) => {
	if (!cookies.get('platform_session')) {
		return json({ error: 'Unauthorized' }, { status: 401 });
	}

	const { messages = [], draft = null, drafts = null, action = 'chat', imageDataUrl = null, entryDate = null } = await request
		.json()
		.catch(() => ({}));
	const requestedDate =
		typeof entryDate === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(entryDate) ? entryDate : todayIso();
	const currentDraft = clampDraft({
		entry_date: requestedDate,
		...(draft && typeof draft === 'object' ? draft : {})
	});
	const currentDrafts = Array.isArray(drafts)
		? drafts
				.filter((item) => !!item && typeof item === 'object')
				.map((item) =>
					clampDraft({
						entry_date: requestedDate,
						...(item as Record<string, unknown>)
					})
				)
		: [];

	if (action === 'apply') {
		if (hasCompleteDrafts(currentDrafts)) {
			const results = await Promise.all(currentDrafts.map((item, index) => applyDraft(fetch, item, index)));
			const failed = results.find((result) => !result.ok);
			if (failed) {
				return json(
					{
						reply: 'I couldn’t add all of those entries yet. Try again in a moment.',
						drafts: currentDrafts,
						applied: false,
						error: failed.body?.error || `Fitness API returned ${failed.status}`
					},
					{ status: 500 }
				);
			}

			return json({
				reply: `Added ${currentDrafts.length} items to ${currentDrafts[0]?.meal_type || 'your log'}.`,
				drafts: currentDrafts,
				applied: true,
				entries: results.map((result) => result.body)
			});
		}

		if (!hasCompleteDraft(currentDraft)) {
			return json({
				reply: 'I still need a food and calories before I can add it.',
				draft: currentDraft,
				drafts: currentDrafts,
				applied: false
			});
		}

		const result = await applyDraft(fetch, currentDraft);
		if (!result.ok) {
			return json(
				{
					reply: 'I couldn’t add that entry yet. Try again in a moment.',
					draft: currentDraft,
					applied: false,
					error: result.body?.error || `Fitness API returned ${result.status}`
				},
				{ status: 500 }
			);
		}

		return json({
			reply: `Added ${currentDraft.food_name} to ${currentDraft.meal_type}.`,
			draft: currentDraft,
			drafts: [],
			applied: true,
			entry: result.body
		});
	}

	const recentMessages = (Array.isArray(messages)
		? messages
				.filter((m: unknown) => !!m && typeof m === 'object')
				.map((m: ChatMessage) => ({
					role: m.role === 'assistant' ? 'assistant' : 'user',
					content: typeof m.content === 'string' ? m.content.slice(0, 2000) : ''
				}))
					.filter((m) => m.content.trim())
					.slice(-10)
		: []) as Array<{ role: 'user' | 'assistant'; content: string }>;

	const lastUserMessage =
		[...recentMessages].reverse().find((message) => message.role === 'user')?.content || '';
	const allowApply = isExplicitConfirmation(lastUserMessage);
	const retryRequested = isRetryRequest(lastUserMessage);
	const hasPhoto =
		typeof imageDataUrl === 'string' &&
		imageDataUrl.startsWith('data:image/') &&
		imageDataUrl.length < 8_000_000;

	if (!allowApply && currentDrafts.length > 1 && lastUserMessage.trim()) {
		const revisedBundle = await reviseDraftBundle(
			fetch,
			recentMessages,
			currentDrafts,
			hasPhoto && typeof imageDataUrl === 'string' ? imageDataUrl : null
		);
		if (revisedBundle) {
			return json({
				reply: revisedBundle.reply,
				drafts: revisedBundle.drafts,
				draft: null,
				applied: false
			});
		}
	}

	if (!hasPhoto && !retryRequested && !allowApply && lastUserMessage.trim()) {
		const bundle = await buildDraftBundle(fetch, lastUserMessage, requestedDate);
		if (bundle && bundle.length > 1) {
			const meal = bundle[0]?.meal_type || 'snack';
			return json({
				reply: `I split that into ${bundle.length} items for ${meal}: ${bundle.map((item) => item.food_name).join(', ')}. Add them when this looks right.`,
				drafts: bundle,
				draft: null,
				applied: false
			});
		}
	}

	const systemPrompt = `You are a conversational fitness logging assistant inside a personal app.

Your job:
- read the chat plus the current draft food entry
- update the draft naturally
- keep the reply short, plain, and useful
- do not add an entry on the first food message
- only set apply_now=true if the latest user message is a pure confirmation like "add it", "log it", "save it", or "looks good add it"
- never say an item was added, logged, or saved unless apply_now=true for that response
- if a food photo is attached, identify the likely food, portion, and meal context before drafting
- if the user says the current guess is wrong, treat that as authoritative and replace the draft instead of defending the previous guess

Return ONLY JSON with this shape:
{
  "reply": "short assistant reply",
  "draft": {
    "food_name": "string",
    "meal_type": "breakfast|lunch|dinner|snack",
    "entry_date": "YYYY-MM-DD",
    "quantity": 1,
    "unit": "serving",
    "calories": 0,
    "protein": 0,
    "carbs": 0,
    "fat": 0,
    "sugar": 0,
    "fiber": 0,
    "note": "",
    "default_serving_label": ""
  },
  "apply_now": false
}

Rules:
- Preserve the current draft unless the user changes something.
- If the latest user message says the guess is wrong, try again, or search again, do not cling to the old food guess. Replace the draft with a new best guess.
- If the user says "make it 150 calories", update calories and keep the rest unless another field should obviously move with it.
- If the user says a meal, move it to that meal.
- Default meal_type to snack if not specified.
- Default entry_date to today unless the user specifies another date.
- Estimate realistic nutrition when needed.
- Always include sugar and fiber estimates, even if rough.
- Keep food_name human and concise, for example "2 boiled eggs".
- If the photo is a nutrition label or package nutrition panel, extract the serving size from the label and put it in default_serving_label.
- If the user gives a product name plus a nutrition label photo, use the label values instead of guessing from memory.
- If the photo is ambiguous, briefly mention up to 2 likely alternatives instead of sounding overconfident.
- After drafting or revising, summarize the draft with calories and key macros, then ask for confirmation.
- If a photo is unclear, say what you think it is and mention the uncertainty briefly.
- If retrying from a photo, use the image again and produce a different best guess or ask one short clarifying question.
- If details are missing, ask one short follow-up instead of overexplaining.
- When the user confirms, keep the reply brief because the app will add the entry next.

Today is ${todayIso()}.
Current draft:
${JSON.stringify(retryRequested ? draftForRetry(currentDraft) : currentDraft, null, 2)}`;

	if (!env.OPENAI_API_KEY) {
		return json(
			{
				reply: 'Assistant is not configured yet.',
				draft: currentDraft,
				drafts: currentDrafts,
				applied: false
			},
			{ status: 500 }
		);
	}

	const userMessages = recentMessages.map((message) => ({
		role: message.role,
		content: message.content
	}));

	if (hasPhoto) {
		const latestUserText = lastUserMessage || 'Analyze this food photo and draft a fitness entry.';
		userMessages.push({
			role: 'user',
			content: [
				{ type: 'text', text: latestUserText },
				{
					type: 'image_url',
					image_url: {
						url: imageDataUrl
					}
				}
			]
		} as unknown as { role: ChatRole; content: string });
	}

	const openAiResponse = await fetch('https://api.openai.com/v1/chat/completions', {
		method: 'POST',
		headers: {
			'content-type': 'application/json',
			authorization: `Bearer ${env.OPENAI_API_KEY}`
		},
		body: JSON.stringify({
			model: env.OPENAI_MODEL || 'gpt-5.2',
			response_format: { type: 'json_object' },
			temperature: 0.2,
			max_completion_tokens: 900,
			messages: [
				{ role: 'system', content: systemPrompt },
				...userMessages
			]
		})
	});

	if (!openAiResponse.ok) {
		const errorText = await openAiResponse.text();
		return json(
			{
				reply: 'The assistant did not respond cleanly.',
				draft: currentDraft,
				drafts: currentDrafts,
				applied: false,
				error: errorText
			},
			{ status: 500 }
		);
	}

	const raw = await openAiResponse.json();
	const content = raw?.choices?.[0]?.message?.content;
	if (typeof content !== 'string') {
		return json(
			{
				reply: 'The assistant response was empty.',
				draft: currentDraft,
				drafts: currentDrafts,
				applied: false
			},
			{ status: 500 }
		);
	}

	let parsed: { reply?: string; draft?: Draft; apply_now?: boolean };
	try {
		parsed = JSON.parse(content);
	} catch {
		return json(
			{
				reply: 'The assistant response could not be parsed.',
				draft: currentDraft,
				drafts: currentDrafts,
				applied: false
			},
			{ status: 500 }
		);
	}

	const nextDraft = clampDraft(parsed.draft || currentDraft);

	if (parsed.apply_now && allowApply && hasCompleteDraft(nextDraft)) {
		const result = await applyDraft(fetch, nextDraft);
		if (result.ok) {
			return json({
				reply: `Added ${nextDraft.food_name} to ${nextDraft.meal_type}.`,
				draft: nextDraft,
				drafts: [],
				applied: true,
				entry: result.body
			});
		}
	}

	return json({
		reply:
			parsed.reply ||
			(hasCompleteDraft(nextDraft)
				? `${nextDraft.food_name} is staged at ${Math.round(nextDraft.calories || 0)} calories.`
				: 'I updated the draft.'),
		draft: nextDraft,
		drafts: [],
		applied: false
	});
};
