var _useState_AP = React.useState;
var _useCallback_AP = React.useCallback;
var _useEffect_AP = React.useEffect;
var _useRef_AP = React.useRef;

/**
 * AnalysisPage — Two-phase page: Image Upload → AI Analysis Results.
 *
 * Engine selection via AIEngineAdapter.resolve() — reads AI_ENGINE flag value
 * and returns the active strategy (LocalCNNStrategy | HybridStrategy |
 * NyckelStrategy | CloudStrategy | NoneStrategy). No engine branching here.
 *
 * Phases: idle | loading_model | preview | analyzing | results | error
 *
 * Progressive disclosure for Hybrid and Local engines:
 *   1. Confidence ring + severity alert (immediate — Nyckel or Local CNN)
 *   2. Frequency spectrum bar         (progressive — enrichment pending)
 *   3. Heatmap panel                  (progressive — enrichment pending)
 *   4. Species panel                  (progressive — enrichment pending, SPECIES_LIST flag)
 *
 * Enrichment state machine: idle → pending → ready | failed
 *   pending : skeleton loaders shown in place of enrichment panels
 *   ready   : panels replace skeletons (scan record patched via updateScan)
 *   failed  : panels hidden, Nyckel verdict stands
 *
 * On successful analysis, saves scan record to ScanStore.
 * Hybrid enrichment patches the saved record via updateScan when complete.
 */
var AnalysisPage = function () {
    var navigate = ReactRouterDOM.useNavigate();
    var scanStore = useScanStore();

    var phaseState = _useState_AP('idle');
    var phase = phaseState[0]; var setPhase = phaseState[1];

    var imageState = _useState_AP(null);
    var imageData = imageState[0]; var setImageData = imageState[1];

    var resultState = _useState_AP(null);
    var result = resultState[0]; var setResult = resultState[1];

    var errorState = _useState_AP('');
    var errorMsg = errorState[0]; var setErrorMsg = errorState[1];

    var statusState = _useState_AP('');
    var statusMsg = statusState[0]; var setStatusMsg = statusState[1];

    var modelReadyState = _useState_AP(false);
    var modelReady = modelReadyState[0]; var setModelReady = modelReadyState[1];

    // Analysis state machine — prevents concurrent operations
    var analysisState = _useState_AP('idle'); // idle | running | completed | failed
    var analysisRunning = analysisState[0]; var setAnalysisRunning = analysisState[1];

    // Enrichment state machine for Hybrid mode progressive disclosure
    // idle → pending (Nyckel done, enrichment running) → ready | failed
    var enrichmentStateArr = _useState_AP('idle');
    var enrichmentState = enrichmentStateArr[0]; var setEnrichmentState = enrichmentStateArr[1];

    // Refs for cancellation and enrichment persistence
    var analysisRef = _useRef_AP(null);
    var analysisAbortRef = _useRef_AP(null);
    var savedScanIdRef = _useRef_AP(null);
    var previewImgRef = _useRef_AP(null);

    // Cleanup on unmount
    _useEffect_AP(function () {
        return function () {
            if (analysisAbortRef.current !== null) {
                analysisAbortRef.current = true;
            }
            savedScanIdRef.current  = null;
            previewImgRef.current   = null; // LR-04 FIX: release detached DOM node ref
        };
    }, []);

    // Read the active engine value once per render — drives all engine-aware UI copy
    var _aiEngine = useFeatureFlagValue('AI_ENGINE') || 'nyckel';
    // Convenience flag for UI copy — not used for engine dispatch (that's AIEngineAdapter's job)
    var useLocalCNN = (_aiEngine === 'local');
    // Hoisted at component level — hooks must not be called inside nested functions
    var speciesEnabled = useFeatureFlag('SPECIES_LIST');

    // ── Model readiness — poll until the active engine's models are ready ──────
    // local  : LocalCNNService must reach 'ready' state
    // hybrid : LocalCNNEnrichmentService must reach 'ready' (same models, lighter)
    // others : cloud APIs need no pre-load, mark ready immediately
    _useEffect_AP(function () {
        var cancelled = false;

        // aws_vision loads the local models because it uses them for ENRICHMENT.
        // aws_lambda_only deliberately does not — it is cloud-verdict-only, and loading
        // ~16MB of weights it will never call is pure cost on a phone.
        if (_aiEngine !== 'local' && _aiEngine !== 'hybrid' && _aiEngine !== 'aws_vision') {
            setModelReady(true);
            return;
        }

        // MR-08 FIX: both 'local' and 'hybrid' share LocalCNNService model state —
        // LocalCNNEnrichmentService.isReady() delegates to LocalCNNService.getLoadState()
        // so reading it directly is correct and clearer for both engine values.
        var state = LocalCNNService.getLoadState();

        if (state === 'ready') {
            setModelReady(true);
            return;
        }
        if (state === 'failed') {
            console.warn('[AnalysisPage] Engine pre-load failed — proceeding with available fallback.');
            setModelReady(true);
            return;
        }
        if (state === 'idle') {
            setPhase('loading_model');
            setStatusMsg('Loading AI Vision engine…');
            var loadFn = ((_aiEngine === 'hybrid' || _aiEngine === 'aws_vision'))
                ? LocalCNNEnrichmentService.loadForEnrichment
                : LocalCNNService.load;
            loadFn(function (msg) { if (!cancelled) setStatusMsg(msg); })
                .then(function () {
                    if (cancelled) return;
                    setModelReady(true);
                    setPhase('idle');
                    setStatusMsg('');
                })
                ['catch'](function (err) {
                    if (cancelled) return;
                    console.warn('[AnalysisPage] On-demand load failed:', err.message);
                    setModelReady(true);
                    setPhase('idle');
                    setStatusMsg('');
                });
            return function () { cancelled = true; };
        }
        // 'loading' — boot pre-load still in progress, poll until done
        var pollInterval = setInterval(function () {
            var s = LocalCNNService.getLoadState();
            if (s === 'ready' || s === 'failed') {
                clearInterval(pollInterval);
                if (s === 'failed') {
                    console.warn('[AnalysisPage] Engine load failed (polled) — proceeding with fallback.');
                }
                setModelReady(true);
                setPhase('idle');
                setStatusMsg('');
            }
        }, 500);
        setPhase('loading_model');
        setStatusMsg('Finalising AI engine…');
        return function () { clearInterval(pollInterval); };
    }, [_aiEngine]);

    var handleImageReady = _useCallback_AP(function (data) {
        setImageData(data);
        setResult(null);
        setErrorMsg('');
        setStatusMsg('');
        setPhase('preview');
    }, []);

    // Builds the persisted capture-metadata block from whatever ImageUpload
    // recovered. Returns null when the scan carries no metadata at all, keeping
    // the stored record clean rather than writing a shell of nulls.
    var _captureContext = function () {
        if (!imageData || !imageData.exif || typeof ExifService === 'undefined') return null;
        var fields = imageData.exif.fields;
        if (!ExifService.hasAnyData(fields)) return null;
        return ExifService.toCaptureContext(fields, 'web_upload');
    };

    // _genusSnapshot — normalises the engine's raw `genus` block (mould genus
    // classifier plan §5) into the persisted genusSnapshot field. Follows the
    // weatherSnapshot/airSnapshot data-moat contract exactly: versioned v:1,
    // JSON primitives only (string/number/boolean/null, never undefined) so it
    // rides the future opaque S3 cloud sync unchanged, and captured
    // UNCONDITIONALLY — no tier or feature-flag gate; only DISPLAY is gated.
    // Returns null when the engine produced no genus block at all (negative
    // verdicts, genus not requested, or a genus failure — which by contract
    // never fails a scan).
    var _genusSnapshot = function (genus) {
        if (!genus || typeof genus !== 'object') return null;
        var abstained = genus.abstained === true;
        var candidates = [];
        var src = genus.candidates;
        // Abstention means the genus model stood down — no candidates are
        // persisted even if a malformed response carried some. Confidences
        // persist on the WIRE scale (0..1), not the display percentage, so
        // the stored record mirrors what the model actually said.
        if (!abstained && Array.isArray(src)) {
            for (var i = 0; i < src.length && i < 3; i++) {
                var c = src[i] || {};
                candidates.push({
                    label:      (typeof c.label === 'string') ? c.label : '',
                    confidence: (typeof c.confidence === 'number' && isFinite(c.confidence)) ? c.confidence : 0,
                });
            }
        }
        return {
            v: 1,
            modelVersion: (typeof genus.model_version === 'string') ? genus.model_version : null,
            abstained: abstained,
            candidates: candidates,
        };
    };

    // _caSnapshot — the Cloud Assembly engine's own output, normalised into the
    // persisted caSnapshot field. Same data-moat contract as weatherSnapshot /
    // airSnapshot / genusSnapshot: versioned v:1, JSON primitives only (string /
    // number / boolean / null, never undefined) so it rides the future opaque S3
    // cloud sync unchanged, and captured UNCONDITIONALLY — no tier or flag gate;
    // only display is gated.
    //
    // Deliberately SEPARATE from genusSnapshot rather than folded into it. That
    // block is a candidate list from the genus classifier; this is the inference
    // API's own model version, confidence tier, saliency score and INDICATIVE
    // prose narrative. Different producers, different meanings, and merging them
    // would make "which model said this?" unanswerable on a stored record.
    //
    // Returns null when the engine produced none of these fields — every other
    // engine, and any Cloud Assembly response that carried nothing worth storing.
    var _caSnapshot = function (res) {
        if (!res) return null;
        var modelVersion      = (typeof res.modelVersion === 'string') ? res.modelVersion : null;
        var confidenceTier    = (typeof res.confidenceTier === 'string') ? res.confidenceTier : null;
        var saliencyScore     = (typeof res.saliencyScore === 'number' && isFinite(res.saliencyScore)) ? res.saliencyScore : null;
        var speciesSuggestion = (typeof res.speciesSuggestion === 'string') ? res.speciesSuggestion : null;
        var speciesConfidence = (typeof res.speciesConfidence === 'string') ? res.speciesConfidence : null;
        if (!modelVersion && !confidenceTier && saliencyScore === null &&
            !speciesSuggestion && !speciesConfidence) {
            return null;
        }
        return {
            v: 1,
            modelVersion:      modelVersion,
            confidenceTier:    confidenceTier,
            saliencyScore:     saliencyScore,
            speciesSuggestion: speciesSuggestion,
            speciesConfidence: speciesConfidence,
        };
    };

    // Shared scan save helper — avoids duplication across engine branches.
    // Captures the new scan id into savedScanIdRef so Hybrid enrichment
    // can patch the record via updateScan when the Local CNN pass completes.
    //
    // Returns a Promise (rather than saving synchronously) so it can resolve
    // WeatherCache.getSnapshot() and AirCache.getSnapshot() first and attach them as
    // weatherSnapshot / airSnapshot. Both lookups are IndexedDB-backed, asynchronous, and
    // — like every WeatherCache/AirCache method — never reject (each cache swallows its
    // own storage errors), so running them via Promise.all is safe: it cannot itself
    // become a rejected chain, and the two independent reads run in parallel rather than
    // serially. Neither meaningfully delays scan saving; a missing/broken cache degrades
    // to that snapshot being null rather than blocking the save.
    var _saveScan = function (res, thumbnail) {
        AnalyticsService.scanSaved({
            severity:     res.severity,
            has_location: false,
            has_property: false,
        });
        var weatherSnapshotPromise = (typeof WeatherCache !== 'undefined' && WeatherCache.getSnapshot)
            ? WeatherCache.getSnapshot()
            : Promise.resolve(null);
        var airSnapshotPromise = (typeof AirCache !== 'undefined' && AirCache.getSnapshot)
            ? AirCache.getSnapshot()
            : Promise.resolve(null);

        return Promise.all([weatherSnapshotPromise, airSnapshotPromise]).then(function (snapshots) {
            var weatherSnapshot = snapshots[0];
            var airSnapshot = snapshots[1];
            var record = scanStore.addScan({
                fileName:    imageData.file ? imageData.file.name : 'scan',
                thumbnail:   thumbnail || '',
                previewURL:  imageData.previewURL,
                percentage:  res.percentage,
                verdict:     res.verdict,
                severity:    res.severity,
                label:       res.label,
                insight:     res.insight,
                aiEngine:    res.aiEngine || null,
                rawScore:              res.rawScore              || null,
                freqSpectrum:          res.freqSpectrum          || null,
                saliencyLevel:         res.saliencyLevel         || null,
                saliencyPeakToMean:    res.saliencyPeakToMean    || null,
                isShortcut:            res.isShortcut            || false,
                heatmapDataURL:        res.heatmapDataURL        || null,
                speciesTop:            res.speciesTop            || null,
                speciesRanked:         res.speciesRanked         || [],
                speciesIsUncertain:    res.speciesIsUncertain    || false,
                speciesSummary:        res.speciesSummary        || '',
                darkMouldConsensus:    res.darkMouldConsensus    || false,
                patchContribution:     res.patchContribution     || null,
                textureIrregularity:   res.textureIrregularity   || null,
                textureIrregularityLevel: res.textureIrregularityLevel || null,
                interChannelDiff:      res.interChannelDiff      || null,
                // Capture metadata read from the original file at upload time.
                // Stored even when only pixel dimensions survived, so a report can
                // distinguish "no metadata in this photo" from "scan predates the
                // feature" — the latter has no captureContext at all.
                captureContext: _captureContext(),
                // Outdoor weather at capture time — unconditional on every scan, distinct
                // from the manual environmentalReadings enterprise field. null when no
                // cached weather reading exists yet (e.g. first-ever scan before the
                // weather panel has fetched anything); _backfillEnvironmentSnapshots below
                // upgrades that in the background rather than delaying the save.
                weatherSnapshot: weatherSnapshot || null,
                // Outdoor air quality at capture time — same automatic-capture contract as
                // weatherSnapshot immediately above, independent field, independent cache.
                airSnapshot: airSnapshot || null,
                // Genus classifier candidates at analysis time — normalised by
                // _genusSnapshot above. Same data-moat contract as the two environment
                // snapshots: unconditional capture on every scan the server answered for,
                // never gated by tier or flag (only display is). null until the classifier
                // ships, and thereafter on negative verdicts and genus failures.
                genusSnapshot: _genusSnapshot(res.genus),
                // The Cloud Assembly engine's own model version, confidence tier,
                // saliency score and INDICATIVE species narrative — normalised by
                // _caSnapshot above, same unconditional-capture contract as the
                // three snapshots before it. null for every other engine.
                caSnapshot: _caSnapshot(res),
            });
            // Capture scan id for Hybrid enrichment patch
            savedScanIdRef.current = record ? record.id : null;

            _backfillEnvironmentSnapshots(savedScanIdRef.current, weatherSnapshot, airSnapshot);

            return record;
        });
    };

    // ENV_BACKFILL_MAX_AGE_MS / _envSnapshotIsStale — shared staleness rule for BOTH
    // weatherSnapshot and airSnapshot. 60 minutes, not either cache's own 24h MAX_AGE_MS
    // (see WeatherCache/AirCache header docs for why that longer window is honest for
    // passive display): a scan record's snapshot, once attached, carries no live
    // "updated Xh ago" indicator anywhere in the current UI — it is a fixed point-in-time
    // fact about that scan, and a scan taken during a sudden weather/air change an hour
    // after the last panel load should not silently inherit hours-old conditions as if
    // they were current at capture time.
    var ENV_BACKFILL_MAX_AGE_MS = 60 * 60 * 1000;
    var _envSnapshotIsStale = function (snapshot) {
        return !snapshot ||
            typeof snapshot.capturedAt !== 'number' ||
            (Date.now() - snapshot.capturedAt) > ENV_BACKFILL_MAX_AGE_MS;
    };

    // _backfillEnvironmentSnapshots — upgrades a missing or ageing weatherSnapshot AND/OR
    // airSnapshot in the background, after the scan is already saved. Mirrors the
    // _applyEnrichment precedent: fire-and-forget, patches via ONE scanStore.updateScan
    // call, and any failure is silent — weather/air are context for a scan, never a
    // dependency of saving one. Generalised from the former _backfillWeatherSnapshot to
    // cover both environment snapshots under the same rule, sharing a single coordinate
    // resolution rather than resolving location twice.
    //
    // Only fetches whichever snapshot is actually stale/missing. The weather and air
    // fetches run CONCURRENTLY (both kicked off in the same tick, off the one resolved
    // coords value) and each is caught independently, resolving null on its own failure
    // rather than rejecting — so Promise.all below can never reject, and one fetch failing
    // can never block or lose the other's patch. That is also why this needs no
    // Promise.allSettled: each promise is already made "settled-shaped" by its own catch.
    var _backfillEnvironmentSnapshots = function (scanId, weatherSnapshot, airSnapshot) {
        if (!scanId) return;

        var weatherStale = _envSnapshotIsStale(weatherSnapshot);
        var airStale = _envSnapshotIsStale(airSnapshot);
        if (!weatherStale && !airStale) return; // both fresh — nothing to do

        var resolveCoords = function () {
            if (!window.GeoLocationService || typeof GeoLocationService.getCurrentPosition !== 'function') {
                return Promise.resolve(AppConstants.FALLBACK_COORDS);
            }
            return GeoLocationService.getCurrentPosition()
                .then(function (c) {
                    return (c && typeof c.lat === 'number' && typeof c.lon === 'number') ? c : AppConstants.FALLBACK_COORDS;
                })
                ['catch'](function () { return AppConstants.FALLBACK_COORDS; });
        };

        resolveCoords()
            .then(function (coords) {
                var latParam = encodeURIComponent(coords.lat.toFixed(4));
                var lonParam = encodeURIComponent(coords.lon.toFixed(4));

                // Each branch resolves its OWN small snapshot (never the raw payload) and
                // lets `raw` fall out of scope as soon as that snapshot is built — the
                // full multi-day hourly/daily arrays are never retained beyond this one
                // .then callback, and never propagate into the Promise.all below.
                var weatherPromise = !weatherStale ? Promise.resolve(null) :
                    fetch('/api/weather?lat=' + latParam + '&lon=' + lonParam, { headers: { 'Accept': 'application/json' } })
                        .then(function (res) {
                            if (!res.ok) throw new Error('weather ' + res.status);
                            return res.json();
                        })
                        .then(function (raw) {
                            if (typeof WeatherCache === 'undefined') return null;
                            var snapshot = WeatherCache.buildSnapshot(coords.lat, coords.lon, null, raw, 'live');
                            WeatherCache.save(coords.lat, coords.lon, null, raw);
                            return snapshot;
                        })
                        ['catch'](function (err) {
                            console.warn('[AnalysisPage] Weather snapshot backfill failed (non-fatal):', err && err.message);
                            return null;
                        });

                var airPromise = !airStale ? Promise.resolve(null) :
                    fetch('/api/air?lat=' + latParam + '&lon=' + lonParam, { headers: { 'Accept': 'application/json' } })
                        .then(function (res) {
                            if (!res.ok) throw new Error('air ' + res.status);
                            return res.json();
                        })
                        .then(function (raw) {
                            if (typeof AirCache === 'undefined') return null;
                            var snapshot = AirCache.buildSnapshot(coords.lat, coords.lon, null, raw, 'live');
                            AirCache.save(coords.lat, coords.lon, null, raw);
                            return snapshot;
                        })
                        ['catch'](function (err) {
                            console.warn('[AnalysisPage] Air snapshot backfill failed (non-fatal):', err && err.message);
                            return null;
                        });

                return Promise.all([weatherPromise, airPromise]);
            })
            .then(function (results) {
                var patch = {};
                if (results[0]) patch.weatherSnapshot = results[0];
                if (results[1]) patch.airSnapshot = results[1];
                // ONE updateScan call carries whichever of the two actually upgraded —
                // never two separate patches racing each other into the same record.
                if (patch.weatherSnapshot || patch.airSnapshot) {
                    scanStore.updateScan(scanId, patch);
                }
            })
            ['catch'](function (err) {
                // Belt-and-braces only: resolveCoords and both fetch branches above already
                // catch everything they can throw, so this guards against something
                // unexpected (e.g. a synchronous throw) rather than an anticipated path.
                console.warn('[AnalysisPage] Environment snapshot backfill failed (non-fatal):', err && err.message);
            });
    };

    // _applyEnrichment — called when Hybrid enrichment resolves.
    // Patches the saved scan record and updates UI result state in one pass.
    // isAnalysisCancelled is passed in to guard against stale updates after reset.
    var _applyEnrichment = function (enrichedResult, isAnalysisCancelled) {
        if (isAnalysisCancelled()) {
            console.log('[AnalysisPage] Enrichment arrived after cancellation — discarding.');
            return;
        }
        // Update the persisted scan record with enrichment fields
        if (savedScanIdRef.current) {
            scanStore.updateScan(savedScanIdRef.current, {
                // HR-05 FIX: ensure aiEngine identity is persisted as 'hybrid'
                // so ScanDetails can correctly identify enriched scans after reload
                aiEngine:              'hybrid',
                freqSpectrum:          enrichedResult.freqSpectrum          || null,
                saliencyLevel:         enrichedResult.saliencyLevel         || null,
                saliencyPeakToMean:    enrichedResult.saliencyPeakToMean    || null,
                isShortcut:            enrichedResult.isShortcut            || false,
                heatmapDataURL:        enrichedResult.heatmapDataURL        || null,
                speciesTop:            enrichedResult.speciesTop            || null,
                speciesRanked:         enrichedResult.speciesRanked         || [],
                speciesIsUncertain:    enrichedResult.speciesIsUncertain    || false,
                speciesSummary:        enrichedResult.speciesSummary        || '',
            });
        }
        // Merge enrichment fields into the displayed result and reveal panels
        setResult(enrichedResult);
        setEnrichmentState('ready');
        console.info('[AnalysisPage] Hybrid enrichment applied.');
    };

        // HR-02 FIX: Map raw engine/network errors to plain-English user messages.
    // Called from the catch block so users never see API error codes or
    // TF.js internal strings.
    var _friendlyError = function (err) {
        // The two "not a verdict" outcomes carry copy that is ALREADY user-facing and
        // says something the generic fallback cannot: what to do differently. An
        // uncertain result means the model could not judge the photo (never that the
        // surface is clean), and a rejected one quotes the server's own input-quality
        // reason ("too blurry", …). Passing them through verbatim is the whole point of
        // having the third and fourth states at all — collapsing them into "something
        // went wrong" throws away the retake instruction.
        if (err && (err.code === 'uncertain_verdict' || err.code === 'rejected_verdict')) {
            return err.message;
        }
        var msg = (err && err.message) ? err.message.toLowerCase() : '';
        if (msg === 'offline'
            || msg.indexOf('failed to fetch') !== -1
            || msg.indexOf('networkerror') !== -1
            || msg.indexOf('network request failed') !== -1
            || msg.indexOf('load failed') !== -1) {
            return 'No internet connection. Please check your connection and try again.';
        }
        if (msg.indexOf('aborted') !== -1 || msg.indexOf('timeout') !== -1) {
            return 'The analysis took too long. Please try again.';
        }
        if (msg.indexOf('401') !== -1 || msg.indexOf('403') !== -1 || msg.indexOf('auth failed') !== -1) {
            return 'The analysis service is temporarily unavailable. Please try again later.';
        }
        if (msg.indexOf('image not ready') !== -1 || msg.indexOf('image element') !== -1) {
            return 'Please go back and select your photo again.';
        }
        if (msg.indexOf('not loaded') !== -1 || msg.indexOf('call load()') !== -1) {
            return 'The AI engine is still loading. Please wait a moment and try again.';
        }
        if (msg.indexOf('unrecognised api') !== -1) {
            return 'The analysis service returned an unexpected result. Please try again.';
        }
        return 'Something went wrong. Please try again. If the problem persists, try a different photo.';
    };

    var handleAnalyze = _useCallback_AP(function () {
        if (!imageData || !imageData.dataURL) return;
        if (analysisRunning === 'running') {
            console.warn('[AnalysisPage] Analysis already running, ignoring request');
            return;
        }

        var analysisId = Date.now() + Math.random();
        analysisRef.current = analysisId;
        analysisAbortRef.current = false;

        setAnalysisRunning('running');
        setEnrichmentState('idle');
        setPhase('analyzing');
        setStatusMsg('Analysing image…');
        setResult(null);
        setErrorMsg('');

        var strategy = AIEngineAdapter.resolve();

        // HR-01 FIX: Check connectivity before attempting any network-dependent engine.
        // Avoids 60-90s timeout on mobile when offline.
        // Every engine that needs the network. aws_lambda_only and cloud_assembly are
        // included: unlike the others they have no local fallback at all, so offline is
        // fatal rather than degraded.
        var needsNetwork = (strategy.engineId === 'nyckel' || strategy.engineId === 'hybrid' ||
                            strategy.engineId === 'aws_vision' || strategy.engineId === 'aws_lambda_only' ||
                            strategy.engineId === 'cloud_assembly');
        if (needsNetwork && typeof navigator !== 'undefined' && navigator.onLine === false) {
            setErrorMsg('No internet connection. Please check your connection and try again.');
            setPhase('error');
            setAnalysisRunning('failed');
            return;
        }

        console.info('[AnalysisPage] Starting analysis — engine:', strategy.engineId, 'id:', analysisId);
        AnalyticsService.scanInitiated({ ai_engine: strategy.engineId, image_source: 'upload' });

        var isAnalysisCancelled = function () {
            return analysisAbortRef.current || analysisRef.current !== analysisId;
        };

        var imagePayload = {
            dataURL:    imageData.dataURL,
            previewURL: imageData.previewURL,
            file:       imageData.file || null,
            imgElement: previewImgRef.current || null
        };

        strategy.analyze(imagePayload, function (msg) {
            if (!isAnalysisCancelled()) setStatusMsg(msg);
        })
        .then(function (res) {
            if (isAnalysisCancelled()) return;
            if (!!res._enrichmentPromise) {
                res.enrichmentPending = true;
                res.hasEnrichment = false;
            }
            setResult(res);
            return ThumbnailService.generate(imageData.previewURL || imageData.dataURL)['catch'](function () { return ''; })
                .then(function (thumb) {
                    if (!isAnalysisCancelled()) return _saveScan(res, thumb);
                })
                .then(function () {
                    if (isAnalysisCancelled()) return;
                    // MR-06 FIX: clear large original base64 — only previewURL needed
                    // for display from this point. dataURL can be 10-20MB for DSLR images.
                    // `exif` is carried through deliberately: it is a small flat
                    // object (bytes, not megabytes) and the capture-details panel
                    // renders from it after the analysis completes.
                    setImageData(function (prev) {
                        if (!prev) return prev;
                        return {
                            file:       prev.file,
                            previewURL: prev.previewURL,
                            dataURL:    null,
                            exif:       prev.exif || null,
                        };
                    });
                    setStatusMsg('');
                    setPhase('results');
                    setAnalysisRunning('completed');
                    AnalyticsService.scanCompleted({
                        ai_engine:      strategy.engineId,
                        verdict:        res.verdict,
                        severity:       res.severity,
                        confidence_pct: res.percentage,
                        species:        res.speciesTop ? res.speciesTop.label : 'none',
                        is_mould:       res.percentage >= 50
                    });
                    if (!!res._enrichmentPromise && res._enrichmentPromise) {
                        setEnrichmentState('pending');
                        setStatusMsg('Generating visual insights…');
                        res._enrichmentPromise
                            .then(function (enrichedResult) {
                                if (isAnalysisCancelled()) return;
                                setStatusMsg('');
                                if (enrichedResult && enrichedResult.hasEnrichment) {
                                    _applyEnrichment(enrichedResult, isAnalysisCancelled);
                                } else {
                                    setEnrichmentState('failed');
                                }
                            })
                            ['catch'](function (err) {
                                if (isAnalysisCancelled()) return;
                                setStatusMsg('');
                                setEnrichmentState('failed');
                                console.warn('[AnalysisPage] Hybrid enrichment failed:', err.message);
                            });
                    }
                });
        })
        ['catch'](function (err) {
            if (isAnalysisCancelled()) return;
            console.error('[AnalysisPage] Analysis failed (' + strategy.engineId + '):', err.message);
            AnalyticsService.scanError({
                ai_engine:     strategy.engineId,
                error_type:    'analysis_failure',
                error_message: err.message
            });
            // HR-02 FIX: map raw error to plain-English user message
            setErrorMsg(_friendlyError(err));
            setPhase('error');
            setAnalysisRunning('failed');
        });
    }, [imageData, scanStore, analysisRunning]);

    var handleReset = _useCallback_AP(function () {
        if (analysisRef.current) {
            analysisAbortRef.current = true;
            console.log('[AnalysisPage] Cancelling analysis on reset');
        }
        setPhase('idle');
        setImageData(null);
        setResult(null);
        setErrorMsg('');
        setStatusMsg('');
        setAnalysisRunning('idle');
        setEnrichmentState('idle');
        analysisRef.current     = null;
        analysisAbortRef.current = false;
        savedScanIdRef.current  = null;
        previewImgRef.current   = null; // LR-04 FIX: release detached DOM node ref
    }, []);

    var headerTitle = (phase === 'results') ? 'Analysis Results' : 'Mould Detect';

    var severityStyles = {
        critical: { bg: 'bg-warning-light', border: 'border-warning/20', text: 'text-warning', icon: 'emergency' },
        high:     { bg: 'bg-warning-light', border: 'border-warning/20', text: 'text-warning', icon: 'warning' },
        moderate: { bg: 'bg-[#FFF8E1]', border: 'border-[#d4a373]/20', text: 'text-[#d4a373]', icon: 'info' },
        low:      { bg: 'bg-[#E8F5E9]', border: 'border-primary/20', text: 'text-primary', icon: 'check_circle' },
    };

    // Engine badge shown in results
    var renderEngineBadge = function () {
        if (!result) return null;
        var isLocal = result.aiEngine === 'local_cnn';
        return (
            <div className={'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider ' + (isLocal ? 'bg-primary/15 text-forest' : 'bg-forest/10 text-forest')}>
                <span className="material-symbols-outlined text-sm">{isLocal ? 'memory' : 'cloud'}</span>
                {isLocal ? 'Analysed on device' : 'AI vision analysis'}
            </div>
        );
    };

    var renderResults = function () {
        var sev = severityStyles[result.severity] || severityStyles.low;
        // hasEnrichment: true for local_cnn always; true for hybrid once enrichment resolves
        var hasEnrichment = result.hasEnrichment || result.aiEngine === 'local_cnn';
        var enrichmentPending = enrichmentState === 'pending';

        // Skeleton loader — reused for each pending enrichment panel
        var renderEnrichmentSkeleton = function (icon, label) {
            return (
                <div className="bio-bg bio-bg-20 rounded-xl border border-stone-100/50 shadow-soft p-4 flex items-center gap-3">
                    <div className="relative w-8 h-8 shrink-0">
                        <div className="absolute inset-0 rounded-full border-2 border-primary/20"></div>
                        <div className="absolute inset-0 rounded-full border-2 border-transparent border-t-primary animate-spin"></div>
                        <div className="absolute inset-0 flex items-center justify-center">
                            <span className="material-symbols-outlined text-primary text-sm">{icon}</span>
                        </div>
                    </div>
                    <div>
                        <p className="text-[11px] font-black text-forest uppercase tracking-[0.12em]">{label}</p>
                        <p className="text-[11px] font-medium text-forest/60 mt-0.5">Generating visual insights…</p>
                    </div>
                </div>
            );
        };

        return (
            <div className="mt-4 space-y-4">
                {/* Scan image */}
                {imageData && imageData.previewURL && (
                    <div className="aspect-video w-full rounded-xl overflow-hidden shadow-soft border-4 border-surface">
                        <img className="w-full h-full object-cover" src={imageData.previewURL} alt="Analysed image" />
                    </div>
                )}

                {/* Verdict + confidence ring */}
                <div className="bio-bg bio-bg-10 rounded-xl shadow-soft p-5 border border-stone-100/50">
                    <div className="text-center mb-3">
                        <h2 className="text-2xl font-extrabold text-forest">{result.verdict}</h2>
                        {result.label && (
                            <p className="text-forest font-bold text-sm mt-1">{result.label}</p>
                        )}
                    </div>
                    <div className="flex flex-col items-center">
                        <ConfidenceRing percentage={result.percentage} severity={result.severity} />
                    </div>

                    {/* Frequency spectrum — present when enrichment ready */}
                    {hasEnrichment && result.freqSpectrum && (
                        <FrequencySpectrumBar freqSpectrum={result.freqSpectrum} className="mt-4" />
                    )}
                    {/* Frequency spectrum skeleton — shown while enrichment is pending */}
                    {enrichmentPending && !result.freqSpectrum && (
                        <div className="mt-4">{renderEnrichmentSkeleton('graphic_eq', 'Frequency Spectrum')}</div>
                    )}

                    {/* Severity alert */}
                    <div className={sev.bg + ' rounded-xl mt-4 p-5 flex items-start gap-3 border ' + sev.border}>
                        <span className={'material-symbols-outlined ' + sev.text + ' mt-0.5'}>{sev.icon}</span>
                        <div className="flex-1">
                            <h3 className={'font-extrabold ' + sev.text + ' mb-1'}>{result.verdict}</h3>
                            <p className="text-sm font-medium text-forest leading-relaxed">{result.insight}</p>
                        </div>
                    </div>
                </div>

                {/* Heatmap panel — present when enrichment ready */}
                {hasEnrichment && result.heatmapDataURL && (
                    <LocalHeatmapPanel
                        heatmapDataURL={result.heatmapDataURL}
                        originalDataURL={imageData && imageData.previewURL}
                        isMould={result.percentage >= 50}
                        saliencyLevel={result.saliencyLevel}
                        isShortcut={result.isShortcut}
                        speciesLabel={result.speciesTop ? result.speciesTop.label : ''}
                    />
                )}
                {/* Heatmap skeleton — shown while enrichment is pending */}
                {enrichmentPending && !result.heatmapDataURL && (
                    renderEnrichmentSkeleton('visibility', 'AI Attention Heatmap')
                )}

                {/* Species panel — gated by SPECIES_LIST flag and the presence of real
                    species data. Deliberately NOT gated on hasEnrichment: the species
                    fields are only ever populated from genuine classifier output, and in
                    aws_lambda_only mode hasEnrichment tracks the heat-map overlay — a
                    flat server map must not hide a valid genus result. */}
                {speciesEnabled && result.speciesTop && result.percentage >= 50 && (
                    <LocalSpeciesPanel
                        speciesTop={result.speciesTop}
                        speciesRanked={result.speciesRanked}
                        speciesSummary={result.speciesSummary}
                        isUncertain={result.speciesIsUncertain}
                    />
                )}
                {/* Genus abstention — VISIBLE, not silent. The genus model abstains
                    (calibrated confidence threshold) on a large share of real-world
                    photos, and an empty space where a panel sometimes appears reads as
                    "feature broken" — the owner hit exactly this while testing
                    (2026-08-11). Abstention is the model being honest, so say so. */}
                {speciesEnabled && !result.speciesTop && result.genus && result.genus.abstained === true && result.percentage >= 50 && (
                    <div className="bio-bg bio-bg-30 rounded-xl border border-stone-100/50 shadow-soft px-4 py-4">
                        <div className="flex items-center justify-center gap-2 mb-2">
                            <span className="text-[10px] font-black text-forest uppercase tracking-[0.15em]">Mould Genus</span>
                            <span className="text-[8px] font-black uppercase tracking-[0.1em] px-1.5 py-0.5 rounded-full bg-[#FDECE8] text-[#a1543c]">Experimental</span>
                        </div>
                        <p className="text-[12px] font-medium text-forest leading-relaxed text-center">
                            The genus model reviewed this detection, but the visual signals were
                            inconclusive — so no genus suggestion is offered rather than an
                            unreliable one. Independent laboratory testing can identify the genus,
                            and is always recommended by the Mould Detect team.
                        </p>
                    </div>
                )}
                {/* Genus skeleton — shown while enrichment is pending and SPECIES_LIST
                    enabled. enrichmentPending is only ever true for the two-phase
                    hybrid/aws_vision engines (set off _enrichmentPromise);
                    aws_lambda_only resolves in a single pass, so no skeleton ever
                    appears there. */}
                {speciesEnabled && enrichmentPending && !result.speciesTop && result.percentage >= 50 && (
                    renderEnrichmentSkeleton('biotech', 'Mould Genus')
                )}

                {/* Photo capture metadata — read from the original file at upload */}
                {imageData && imageData.exif && (
                    <CaptureDetailsPanel fields={imageData.exif.fields} variant="analysis" />
                )}

                <HealthRiskInfo />

                <FindSpecialistPanel context="analysis" severity={result.severity} />

                <button
                    onClick={function () { navigate('/location-details', { state: { fromScan: true } }); }}
                    className="w-full h-14 bg-primary text-white rounded-full font-extrabold text-lg shadow-clay flex items-center justify-center gap-2 btn-nature hover:brightness-110 transition-all"
                >
                    <span className="material-symbols-outlined">add_location</span>
                    Save Scan Details
                </button>
                <button
                    onClick={handleReset}
                    className="w-full h-12 bg-surface text-forest rounded-full font-bold text-sm border border-stone-100/50 shadow-soft flex items-center justify-center gap-2 btn-nature hover:bg-forest hover:text-white transition-all duration-300"
                >
                    <span className="material-symbols-outlined text-lg">add_a_photo</span>
                    Scan Another Image
                </button>

                <p className="text-[11px] text-forest text-center leading-relaxed">
                    AI-assisted visual analysis for decision-support only. Results should not replace professional inspection.
                </p>
            </div>
        );
    };

    return (
        <Layout>
            <main className="flex-1 overflow-y-auto overflow-x-hidden px-6 pb-36">
                <PageHeader
                    title={headerTitle}
                    showMenu={true}
                    titleClass="font-extrabold text-xl text-forest tracking-tight"
                />

                {/* Model loading state */}
                {phase === 'loading_model' && (
                    <div className="bio-bg bio-bg-30 mt-4 rounded-xl shadow-soft p-5 border border-stone-100/50">
                        <div className="mt-8 flex flex-col items-center gap-5">
                            <div className="relative w-20 h-20">
                                <div className="absolute inset-0 rounded-full border-4 border-primary/20"></div>
                                <div className="absolute inset-0 rounded-full border-4 border-transparent border-t-primary animate-spin"></div>
                                <div className="absolute inset-0 flex items-center justify-center">
                                    <span className="material-symbols-outlined text-primary text-2xl">memory</span>
                                </div>
                            </div>
                            <div className="text-center">
                                <h2 className="text-base font-extrabold text-forest">Loading Local AI</h2>
                                <p className="text-sm font-medium text-forest mt-1">{statusMsg || 'Preparing on-device models…'}</p>
                                {/* <p className="text-[11px] font-medium text-forest mt-2">First load downloads MobileNet (~16MB) and caches it for future visits.</p> */}
                            </div>
                            <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-primary/10">
                                <span className="material-symbols-outlined text-primary text-sm">lock</span>
                                <span className="text-[11px] font-bold text-primary">No data leaves your device</span>
                            </div>
                        </div>
                    </div>
                )}

                {phase === 'idle' && (
                    <div className="mt-4 space-y-5">
                        <div className="text-center">
                            <h2 className="text-xl font-extrabold text-forest">Upload a Photo</h2>
                            {/* One line, the same whichever engine is active. The copy used
                                to branch on _aiEngine and name the vendor and topology
                                outright — implementation detail that is ours, not the
                                reader's, and nothing a customer benefits from knowing.
                                The engine indicator pill below it was removed for the same
                                reason: it was developer diagnostics rendered as UI. */}
                            <p className="text-sm text-forest font-medium mt-1 leading-relaxed">
                                AI vision technology designed to support human decision-making
                                when identifying mould in homes and commercial properties.
                            </p>
                        </div>
                        <ImageUpload onImageReady={handleImageReady} />
                        <p className="text-[11px] text-forest text-center leading-relaxed">
                            AI-assisted visual analysis for decision-support only. Results should not replace professional inspection.
                        </p>
                    </div>
                )}

                {phase === 'preview' && imageData && (
                    <div className="mt-4 space-y-5">
                        <div className="aspect-video w-full rounded-xl overflow-hidden shadow-soft border-4 border-surface">
                            <img
                                ref={previewImgRef}
                                className="w-full h-full object-cover"
                                src={imageData.previewURL}
                                alt="Upload preview"
                                crossOrigin="anonymous"
                            />
                        </div>
                        <div className="text-center">
                            <p className="text-sm font-extrabold text-forest">{imageData.file.name}</p>
                            <p className="text-xs font-semibold text-muted">{(imageData.file.size / 1024).toFixed(1)} KB</p>
                        </div>
                        <button
                            onClick={handleAnalyze}
                            disabled={!modelReady}
                            className="w-full h-14 bg-forest text-white rounded-full font-extrabold text-lg shadow-clay flex items-center justify-center gap-2 btn-nature hover:brightness-110 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
                        >
                            <span className="material-symbols-outlined">biotech</span>
                            Check for Mould
                        </button>
                        <button
                            onClick={handleReset}
                            className="w-full h-12 bg-surface text-forest rounded-full font-bold text-sm border border-stone-100/50 shadow-soft flex items-center justify-center gap-2 btn-nature hover:bg-forest hover:text-white transition-all duration-300"
                        >
                            <span className="material-symbols-outlined text-lg">refresh</span>
                            Choose Different Image
                        </button>
                    </div>
                )}

                {phase === 'analyzing' && (
                    <div className="bio-bg bio-bg-30 mt-4 rounded-xl shadow-soft p-5 border border-stone-100/50">
                        <div className="mt-12 flex flex-col items-center gap-6">
                            <div className="relative w-24 h-24">
                                <div className="absolute inset-0 rounded-full border-4 border-primary/20"></div>
                                <div className="absolute inset-0 rounded-full border-4 border-transparent border-t-primary animate-spin"></div>
                                <div className="absolute inset-0 flex items-center justify-center">
                                    <span className="material-symbols-outlined text-primary text-3xl">biotech</span>
                                </div>
                            </div>
                            <div className="text-center">
                                <h2 className="text-lg font-extrabold text-forest">Analysing Image</h2>
                                <p className="text-sm font-medium text-forest mt-1">{statusMsg || 'Examining your photo for mould indicators…'}</p>
                            </div>
                            {(useLocalCNN || (_aiEngine === 'hybrid' || _aiEngine === 'aws_vision')) && (
                                <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-primary/10">
                                    <span className="material-symbols-outlined text-primary text-sm">lock</span>
                                    <span className="text-[11px] font-bold text-primary">
                                        {(_aiEngine === 'hybrid' || _aiEngine === 'aws_vision') ? 'Visual insights processed on your device' : 'Processing on your device'}
                                    </span>
                                </div>
                            )}
                        </div>
                    </div>
                )}

                {phase === 'results' && result && renderResults()}

                {phase === 'error' && (
                    <div className="mt-8 space-y-5">
                        <div className="bg-warning-light rounded-xl p-5 flex items-start gap-3 border border-warning/20">
                            <span className="material-symbols-outlined text-warning mt-0.5">error</span>
                            <div className="flex-1">
                                <h3 className="font-extrabold text-warning mb-1">Analysis Failed</h3>
                                <p className="text-sm font-medium text-forest leading-relaxed">{errorMsg}</p>
                            </div>
                        </div>
                        <button
                            onClick={handleReset}
                            className="w-full h-14 bg-forest text-white rounded-full font-extrabold text-lg shadow-clay flex items-center justify-center gap-2 btn-nature hover:brightness-110 transition-all"
                        >
                            <span className="material-symbols-outlined">refresh</span>
                            Try Again
                        </button>
                    </div>
                )}

            </main>
        </Layout>
    );
};

window.AnalysisPage = AnalysisPage;
