declare const process: {
  env: {
    EXPO_PUBLIC_PROXY_URL?: string;
    EXPO_PUBLIC_APP_TOKEN?: string;
  };
};

declare const __DEV__: boolean;

import type { TreatmentPlan, TreatmentStep } from './investigation/types';
import { runAiCall } from './orchestration/aiQueue';

// ─── Types ────────────────────────────────────────────────────────────────────

export type ProblemScanContextType = 'existingPlant' | 'unknownPlant';

export type ProblemScanInput = {
  imageBase64: string;
  additionalImagesBase64?: string[];
  symptoms: string[];
  /** Per-photo breakdown for true multimodal diagnosis: each photo's own
   *  symptoms (and optionally category). Index 0 = primary image, then
   *  additional photos in order. Lets Pixie connect which issue lives in
   *  which photo. */
  photoSymptoms?: Array<{ symptoms: string[]; category?: string; label?: string }>;
  userNote?: string;
  contextType: ProblemScanContextType;
  plantContext?: {
    name: string;
    scientificName?: string;
    environment?: string;
    setup?: string;
    recentMemories?: string[];
  };
  /** Optional full plant memory block (from buildPlantMemoryContext) — the AI reads this before diagnosing. */
  plantMemory?: string;
  /** User's care preferences — refine diagnosis/advice (organic-first, pet-safe, edible caution). */
  carePreferences?: {
    organicFirst?: boolean;
    petSafeCaution?: boolean;
    ediblePlantCaution?: boolean;
    shoppingStyle?: 'diy_first' | 'store_bought' | 'either';
  };
};

export type ProblemScanResult = {
  summary: string;
  possibleCauses: string[];
  noticed: string[];
  suggestedChecks: string[];
  confidence: 'low' | 'medium' | 'high';
  uncertaintyNote: string;
  recommendedMemoryTitle: string;
  recommendedMemoryNotes: string;
  /** Safety/treatment warnings — e.g. "neem oil — don't harvest or eat for 72h after application". */
  treatmentWarnings?: string[];
  /** The structured treatment plan the gardener should DO (Option A — one call). */
  treatmentPlan?: TreatmentPlan;
};

export type ProblemScanError = {
  message: string;
  fallbackAvailable: true;
};

// ─── Constants ────────────────────────────────────────────────────────────────

const proxyEndpoint = process.env.EXPO_PUBLIC_PROXY_URL || 'https://pixiesprout-ai.46-4-121-190.sslip.io/v1/chat/completions';
const xiaomiModel = 'qwen3.5:397b';
const requestTimeoutMs = 45000;

const symptomLabelMap: Record<string, string> = {
  yellow_leaves: 'yellow leaves',
  brown_tips: 'brown tips',
  drooping: 'drooping',
  spots_patches: 'spots or patches',
  bugs_pests: 'bugs or pests',
  webbing: 'webbing',
  mold_fungus: 'mold or fungus',
  wilting: 'wilting',
  slow_growth: 'slow growth',
  leaf_damage: 'leaf damage',
  other: 'other issue',
};

// ─── Helpers ──────────────────────────────────────────────────────────────────

function isDevRuntime() {
  return typeof __DEV__ !== 'undefined' && __DEV__;
}

function logDev(message: string, details: Record<string, unknown>) {
  if (isDevRuntime()) {
    console.log(message, details);
  }
}

function warnDev(message: string, details: Record<string, unknown>) {
  if (isDevRuntime()) {
    console.warn(message, details);
  }
}

function getProxyToken() {
  return (process.env.EXPO_PUBLIC_APP_TOKEN || '').trim();
}

function mapSymptoms(symptoms: string[]): string {
  return symptoms
    .map(s => symptomLabelMap[s] || s.replace(/_/g, ' '))
    .join(', ');
}

function buildPrompt(input: ProblemScanInput): string {
  const symptomText = mapSymptoms(input.symptoms);
  const parts: string[] = [
    'You are Pixie, a warm and supportive plant companion. A gardener is worried about their plant and has shared photo(s) with you.',
    '',
    `Observed symptoms: ${symptomText}.`,
  ];

  const totalPhotos = 1 + (input.additionalImagesBase64?.length || 0);
  if (totalPhotos > 1) {
    parts.push(`The gardener has provided ${totalPhotos} photos. The first photo shows the primary area of concern. Additional photos show the whole plant, soil, setup, or other angles.`);
  }

  // Per-photo multimodal breakdown — tell Pixie which symptoms belong to WHICH
  // photo, so it can connect each issue to the right image instead of guessing
  // across a blur. This is the true multimodal upgrade.
  if (input.photoSymptoms && input.photoSymptoms.length > 0) {
    parts.push('');
    parts.push('Here is which symptom belongs to which photo (numbered in the order you received them):');
    input.photoSymptoms.forEach((p, idx) => {
      const label = p.label ? ` (${p.label})` : '';
      const cat = p.category && p.category !== 'area_of_concern' ? ` [${p.category}]` : '';
      const syms = p.symptoms.length > 0 ? mapSymptoms(p.symptoms) : 'none specified';
      parts.push(`- Photo ${idx + 1}${label}${cat}: ${syms}`);
    });
    parts.push('Diagnose holistically: connect related symptoms across photos, and call out where each issue appears.');
  }

  if (input.userNote) {
    parts.push(`The gardener added this note: "${input.userNote}".`);
  }

  if (input.carePreferences) {
    const cp = input.carePreferences;
    const prefs: string[] = [];
    if (cp.organicFirst) prefs.push('prefers organic-first care/products');
    if (cp.petSafeCaution) prefs.push('needs pet-safe plants (household has pets)');
    if (cp.ediblePlantCaution) prefs.push('needs edible/kid-safe plants (household has children)');
    if (cp.shoppingStyle === 'diy_first') prefs.push('prefers DIY/home remedies over store-bought');
    if (cp.shoppingStyle === 'store_bought') prefs.push('prefers store-bought products');
    if (prefs.length > 0) {
      parts.push(`The gardener's care preferences: ${prefs.join('; ')}. Tailor your advice accordingly.`);
    }
  }

  if (input.plantContext) {
    const ctx = input.plantContext;
    parts.push('');
    parts.push('Background on this plant:');
    parts.push(`- Name: ${ctx.name}`);
    if (ctx.scientificName) parts.push(`- Scientific name: ${ctx.scientificName}`);
    if (ctx.environment) parts.push(`- Environment: ${ctx.environment}`);
    if (ctx.setup) parts.push(`- Setup: ${ctx.setup}`);
    if (ctx.recentMemories && ctx.recentMemories.length > 0) {
      parts.push(`- Recent history: ${ctx.recentMemories.join('; ')}`);
    }
  } else {
    parts.push('');
    parts.push('This plant has not been identified yet — the gardener is not sure what species it is.');
  }

  // Full plant memory — the accumulated history the AI reads before diagnosing
  if (input.plantMemory) {
    parts.push('');
    parts.push(input.plantMemory);
  }

  parts.push('');
  parts.push('Please look at the attached photo(s) carefully and respond with ONLY a JSON object matching this schema:');
  parts.push(`{
  "summary": "A gentle 1-2 sentence summary of what you think is happening (warm, supportive tone — not clinical)",
  "possibleCauses": ["cause 1", "cause 2", "..."],
  "noticed": ["specific thing you noticed in the photo 1", "thing 2", "..."],
  "suggestedChecks": ["actionable check 1", "check 2", "..."],
  "confidence": "low" | "medium" | "high",
  "uncertaintyNote": "a brief honest note about what you're uncertain about",
  "recommendedMemoryTitle": "a short title for saving this observation (e.g. 'Yellow leaves with brown tips')",
  "recommendedMemoryNotes": "a concise note summarizing findings for the plant's memory log",
  "treatmentWarnings": ["array of safety warnings for any treatment you suggest — e.g. 'Neem oil: don't harvest or eat for 72 hours after application', 'Keep treated plant away from pets/kids until dry'. Empty array if no warnings apply."],
  "treatmentPlan": {
    "steps": [
      {
        "action": "Apply neem oil to affected leaves",
        "instructions": ["step 1", "step 2", "..."],
        "dueOffsetDays": 0,
        "safetyWarnings": ["warning 1", "..."],
        "materials": ["what supplies the gardener needs for this step — e.g. 'neem oil', 'spray bottle'. List common household items the gardener may or may not already have."],
        "chips": ["2-5 short quick-pick options for 'what did you use for this step?' — e.g. for a neem step: 'neem oil', 'insecticidal soap', 'rubbing alcohol', 'pruned the leaves'. Plain product/action names the user can tap; no brands. These appear as tappable chips in the case-treatment modal. Include the most common real products/remedies a gardener would use, incl. a DIY/home option if the gardener prefers DIY. Never include brand names. Empty array if no sensible quick-picks (then the modal shows only free text)."],
        "kind": "treatment" | "recheck"
      }
    ],
    "recheckDays": 7,
    "generalNote": "Give it neem, see me next week."
  }
}`);
  parts.push('');
  parts.push('Important guidelines:');
  parts.push('- Be warm and supportive, not medical or alarmist.');
  parts.push('- Do NOT recommend specific products or brands.');
  parts.push('- If you are unsure, say so honestly and set confidence to "low".');
  parts.push('- Keep responses concise and actionable.');
  parts.push('- The "treatmentPlan" is what the gardener should DO. Provide 1-3 concrete, sequenced steps (treat now with dueOffsetDays 0, plus a recheck step with kind "recheck"). Set recheckDays to how many days until the gardener should check back (e.g. 7 for "see me next week"). If no treatment is needed, return an empty steps array and no recheckDays.');
  parts.push('- Each step action should be specific and safe; instructions should be beginner-friendly.');
  parts.push('- Return ONLY the JSON object, no other text.');
  parts.push('');
  parts.push('Treatment philosophy (follow the gardener\'s preferences):');
  parts.push('- If the gardener prefers DIY/home remedies, suggest a DIY or home remedy FIRST, and only fall back to a store-bought option if a DIY approach genuinely won\'t work — and say so honestly.');
  parts.push('- If the gardener has kids and/or pets, prefer non-toxic, organic solutions. If a non-toxic option won\'t work, be honest about it and include the necessary safety disclaimers for kids/pets.');
  parts.push('- If the plant is edible (herbs, vegetables, fruit), include treatment warnings about harvest/consumption safety — e.g. how long to wait after applying a treatment before eating.');
  parts.push('- ALWAYS include a safe-application heads-up for any treatment applied outdoors or that could affect the environment or your home (e.g. avoid spraying on windy days, keep away from beneficial insects, wash hands after).');
  parts.push('- Prioritize organic and environmentally-safe options whenever possible. Only resort to stronger chemical options if needed to save the plant, and include proper safety warnings and disclaimers.');

  return parts.join('\n');
}

function extractJsonText(content: string): string {
  const trimmed = content.trim();
  if (trimmed.startsWith('{') && trimmed.endsWith('}')) return trimmed;

  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
  if (fenced?.[1]) return fenced[1].trim();

  const firstBrace = trimmed.indexOf('{');
  const lastBrace = trimmed.lastIndexOf('}');
  if (firstBrace >= 0 && lastBrace > firstBrace) {
    return trimmed.slice(firstBrace, lastBrace + 1);
  }

  return trimmed;
}

function readString(value: unknown): string | undefined {
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}

function readStringArray(value: unknown): string[] {
  if (!Array.isArray(value)) return [];
  return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0);
}

function readConfidence(value: unknown): 'low' | 'medium' | 'high' {
  if (typeof value === 'string') {
    const lower = value.toLowerCase().trim();
    if (lower === 'high') return 'high';
    if (lower === 'medium') return 'medium';
  }
  return 'low';
}

/** Parse the AI's treatmentPlan JSON into a typed TreatmentPlan (or undefined). */
function readTreatmentPlan(value: unknown): TreatmentPlan | undefined {
  if (!value || typeof value !== 'object') return undefined;
  const obj = value as Record<string, unknown>;
  const stepsRaw = Array.isArray(obj.steps) ? obj.steps : [];
  const steps: TreatmentStep[] = [];
  for (const sRaw of stepsRaw) {
    if (!sRaw || typeof sRaw !== 'object') continue;
    const s = sRaw as Record<string, unknown>;
    const action = readString(s.action);
    if (!action) continue;
    steps.push({
      stepId: `ts_${Date.now()}_${steps.length}_${Math.round(Math.random() * 10000)}`,
      action,
      instructions: readStringArray(s.instructions),
      dueOffsetDays: typeof s.dueOffsetDays === 'number' ? Math.max(0, Math.floor(s.dueOffsetDays)) : 0,
      safetyWarnings: readStringArray(s.safetyWarnings),
      materials: readStringArray(s.materials),
      chips: readStringArray(s.chips),
      kind: s.kind === 'recheck' ? 'recheck' : 'treatment',
    });
  }
  if (steps.length === 0) return undefined;
  const recheck = typeof obj.recheckDays === 'number' ? Math.max(1, Math.floor(obj.recheckDays)) : undefined;
  return {
    planId: `tp_${Date.now()}_${Math.round(Math.random() * 10000)}`,
    generatedAt: new Date().toISOString(),
    steps,
    recheckDays: recheck,
    generalNote: readString(obj.generalNote),
  };
}

function parseResult(raw: string): ProblemScanResult {
  const jsonText = extractJsonText(raw);

  let parsed: Record<string, unknown>;
  try {
    parsed = JSON.parse(jsonText);
  } catch {
    // If JSON parsing fails, try to extract what we can from the text
    warnDev('[ProblemScan] JSON parse failed, building fallback from raw text', { rawLength: raw.length });
    return buildFallbackResult(raw);
  }

  return {
    summary: readString(parsed.summary) || 'Pixie had a look at your photo. Here is what she noticed.',
    possibleCauses: readStringArray(parsed.possibleCauses),
    noticed: readStringArray(parsed.noticed),
    suggestedChecks: readStringArray(parsed.suggestedChecks),
    confidence: readConfidence(parsed.confidence),
    uncertaintyNote: readString(parsed.uncertaintyNote) || 'Photo-based assessments are always approximate.',
    recommendedMemoryTitle: readString(parsed.recommendedMemoryTitle) || 'Problem scan observation',
    recommendedMemoryNotes: readString(parsed.recommendedMemoryNotes) || '',
    treatmentWarnings: readStringArray(parsed.treatmentWarnings),
    treatmentPlan: readTreatmentPlan(parsed.treatmentPlan),
  };
}

function buildFallbackResult(raw: string): ProblemScanResult {
  // Try to extract at least a summary from free-form text
  const sentences = raw
    .replace(/```[\s\S]*?```/g, '')
    .split(/[.!]\s+/)
    .map(s => s.trim())
    .filter(s => s.length > 10);

  const summary = sentences.slice(0, 2).join('. ') || 'Pixie took a look but could not form a clear picture this time.';

  return {
    summary,
    possibleCauses: [],
    noticed: [],
    suggestedChecks: [
      'Check the soil moisture level',
      'Look closely at the undersides of leaves',
      'Note if the issue is spreading or stable',
    ],
    confidence: 'low',
    uncertaintyNote: 'The response could not be fully parsed. These are best-guess observations.',
    recommendedMemoryTitle: 'Problem scan observation',
    recommendedMemoryNotes: summary,
  };
}

// ─── Main API ─────────────────────────────────────────────────────────────────

export async function analyzeProblem(input: ProblemScanInput): Promise<ProblemScanResult> {
  const apiKey = getProxyToken();
  if (!apiKey) {
    throw createProblemScanError('API key is missing. Cannot contact Pixie.');
  }

  const prompt = buildPrompt(input);

  // Build content array with all images
  const messageContent: Array<{ type: string; text?: string; image_url?: { url: string } }> = [
    { type: 'text', text: prompt },
    { type: 'image_url', image_url: { url: `data:image/jpeg;base64,${input.imageBase64}` } },
  ];

  // Add additional investigation photos if present
  if (input.additionalImagesBase64 && input.additionalImagesBase64.length > 0) {
    for (const imgBase64 of input.additionalImagesBase64) {
      messageContent.push({ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${imgBase64}` } });
    }
  }

  const requestBody = {
    model: xiaomiModel,
    messages: [{
      role: 'user',
      content: messageContent,
    }],
    temperature: 0.2,
    max_tokens: 1500,
    response_format: { type: 'json_object' },
  };

  logDev('[ProblemScan] request started', {
    endpoint: proxyEndpoint,
    model: xiaomiModel,
    symptomCount: input.symptoms.length,
    contextType: input.contextType,
    hasPlantContext: Boolean(input.plantContext),
    imageCount: 1 + (input.additionalImagesBase64?.length || 0),
    imageBase64Length: input.imageBase64.length,
  });

  let response: Response;
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
    try {
      response = await runAiCall(() => fetch(proxyEndpoint, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestBody),
        signal: controller.signal,
      }), 'high'); // problem scan is user-initiated — never wait behind background
    } finally {
      clearTimeout(timeoutId);
    }
  } catch {
    warnDev('[ProblemScan] network failure', {});
    throw createProblemScanError('Could not reach Pixie. Please check your connection and try again.');
  }

  if (!response.ok) {
    const status = response.status;
    warnDev('[ProblemScan] API error', { status });
    if (status === 401 || status === 403) {
      throw createProblemScanError('Pixie could not authenticate. Please check your settings.');
    }
    throw createProblemScanError('Pixie is temporarily unavailable. Please try again in a moment.');
  }

  let data: { choices?: Array<{ message?: { content?: string }; finish_reason?: string }> };
  try {
    data = await response.json();
  } catch {
    warnDev('[ProblemScan] response parse failure', {});
    throw createProblemScanError('Pixie returned something unexpected. Please try again.');
  }

  const content = data.choices?.[0]?.message?.content;
  if (!content) {
    warnDev('[ProblemScan] empty response content', {});
    throw createProblemScanError('Pixie did not have anything to say this time. Please try again.');
  }
  // Truncation detection (M11): a 'length' finish_reason means the JSON is
  // incomplete — fail loudly rather than persisting a partial diagnosis.
  if (data.choices?.[0]?.finish_reason === 'length') {
    warnDev('[ProblemScan] response truncated (finish_reason=length)', {});
    throw createProblemScanError('Pixie ran out of room answering. Please try again.');
  }

  logDev('[ProblemScan] response received', { contentLength: content.length });

  try {
    return parseResult(content);
  } catch {
    warnDev('[ProblemScan] result parse failure', {});
    // Last resort: build a fallback from the raw text
    return buildFallbackResult(content);
  }
}

export type ProgressAssessment = {
  summary: string;
  working: 'improving' | 'no_change' | 'worsening' | 'unknown';
  nextStep?: TreatmentStep;
  planAdjusted: boolean;
  recheckDays?: number;
};

export type EvaluateProgressInput = {
  imageBase64: string;
  referenceImageBase64?: string;
  betterOrWorse: 'better' | 'same' | 'worse';
  entryType: 'scheduled' | 'early';
  note?: string;
  optionalChips?: string[];
  plantName?: string;
  treatmentContext?: string;   // what was prescribed (the prior treatment plan)
};

/**
 * Evaluate a progress photo against the treatment plan. Called on each check-in
 * (Add Progress Update or Get a Second Look). Returns whether it's working and
 * the refined next step.
 */
export async function evaluateProgress(input: EvaluateProgressInput): Promise<ProgressAssessment> {
  const apiKey = getProxyToken();
  if (!apiKey) {
    throw createProblemScanError('API key is missing. Cannot contact Pixie.');
  }

  const entryTypeLine = input.entryType === 'early'
    ? 'The gardener came back EARLY, before the scheduled recheck, because they are worried or noticed something. Look carefully for whether the condition is WORSENING or the treatment backfired.'
    : 'The gardener came back at the scheduled recheck. Assess progress against the baseline.';

  const prompt = [
    `You are Pixie, a warm, supportive plant-care companion. The gardener has been treating their plant and is checking in with a progress photo.`,
    '',
    `How the gardener says it's going: ${input.betterOrWorse}`,
    input.note ? `Their note: ${input.note}` : '',
    input.optionalChips && input.optionalChips.length ? `New/noticed things: ${input.optionalChips.join(', ')}` : '',
    '',
    `Context: ${entryTypeLine}`,
    input.plantName ? `Plant: ${input.plantName}` : '',
    input.treatmentContext ? `What was prescribed: ${input.treatmentContext}` : '',
    '',
    'Look at the progress photo (and the original intake photo if provided) and respond with ONLY JSON:',
    `{
  "summary": "A gentle 1-2 sentence assessment — is the treatment working? (warm, supportive tone)",
  "working": "improving" | "no_change" | "worsening" | "unknown",
  "nextStep": {
    "action": "the refined next step, or same/keep going",
    "instructions": ["step 1", "step 2", "..."],
    "dueOffsetDays": 0,
    "safetyWarnings": ["warning 1", "..."],
    "kind": "treatment" | "recheck"
  },
  "planAdjusted": true,
  "recheckDays": 7
}`,
    '',
    'Guidelines:',
    '- If it is improving, reassure warmly and either keep the same step or recommend continuing.',
    '- If it is not changing, suggest keeping the current treatment a bit longer or a small adjustment.',
    '- If it is worsening, acknowledge honestly and adjust the plan — possibly a different approach or a sooner recheck. Do not alarm.',
    '- Respect safety: if the plant is edible, include harvest/consumption warnings in nextStep.safetyWarnings.',
    '- If no change to the plan is needed, set planAdjusted to false and still give a nextStep (e.g. "Keep going with the current treatment").',
    '- Return ONLY the JSON object, no other text.',
  ].filter(line => line !== '').join('\n');

  const messageContent: Array<{ type: string; text?: string; image_url?: { url: string } }> = [
    { type: 'text', text: prompt },
    { type: 'image_url', image_url: { url: `data:image/jpeg;base64,${input.imageBase64}` } },
  ];
  if (input.referenceImageBase64) {
    messageContent.push({ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${input.referenceImageBase64}` } });
  }

  const requestBody = {
    model: xiaomiModel,
    messages: [{ role: 'user', content: messageContent }],
    temperature: 0.2,
    max_tokens: 1500,
    response_format: { type: 'json_object' },
  };

  let response: Response;
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
    try {
      response = await runAiCall(() => fetch(proxyEndpoint, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestBody),
        signal: controller.signal,
      }), 'high'); // problem scan is user-initiated — never wait behind background
    } finally {
      clearTimeout(timeoutId);
    }
  } catch {
    warnDev('[Progress] network failure', {});
    throw createProblemScanError('Could not reach Pixie. Please check your connection and try again.');
  }

  if (!response.ok) {
    const status = response.status;
    warnDev('[Progress] API error', { status });
    if (status === 401 || status === 403) {
      throw createProblemScanError('Pixie could not authenticate. Please check your settings.');
    }
    throw createProblemScanError('Pixie is temporarily unavailable. Please try again in a moment.');
  }

  let data: { choices?: Array<{ message?: { content?: string }; finish_reason?: string }> };
  try {
    data = await response.json();
  } catch {
    warnDev('[Progress] response parse failure', {});
    throw createProblemScanError('Pixie returned something unexpected. Please try again.');
  }

  const content = data.choices?.[0]?.message?.content;
  if (!content) {
    warnDev('[Progress] empty response content', {});
    throw createProblemScanError('Pixie did not have anything to say this time. Please try again.');
  }
  // Truncation detection (M11): a 'length' finish_reason means the JSON is
  // incomplete — fail loudly rather than persisting a partial assessment.
  if (data.choices?.[0]?.finish_reason === 'length') {
    warnDev('[Progress] response truncated (finish_reason=length)', {});
    throw createProblemScanError('Pixie ran out of room answering. Please try again.');
  }

  try {
    const parsed = JSON.parse(extractJsonText(content)) as Record<string, unknown>;
    const workingRaw = String(parsed.working || 'unknown').toLowerCase();
    const working: ProgressAssessment['working'] =
      workingRaw === 'improving' || workingRaw === 'no_change' || workingRaw === 'worsening'
        ? workingRaw
        : 'unknown';
    const nextStep = readTreatmentPlan({ steps: parsed.nextStep ? [parsed.nextStep] : [] })?.steps?.[0];
    return {
      summary: readString(parsed.summary) || 'Pixie looked at the progress photo.',
      working,
      nextStep,
      planAdjusted: parsed.planAdjusted === true,
      recheckDays: typeof parsed.recheckDays === 'number' ? Math.max(1, Math.floor(parsed.recheckDays)) : undefined,
    };
  } catch {
    warnDev('[Progress] result parse failure', {});
    return {
      summary: 'Pixie could not fully read the progress photo this time.',
      working: 'unknown',
      planAdjusted: false,
    };
  }
}

function createProblemScanError(message: string): ProblemScanError {
  return { message, fallbackAvailable: true };
}
