/**
 * MouldDetectService — Encapsulated Nyckel AI mould detection API client.
 *
 * Handles: OAuth token management, image compression, API invocation,
 * result parsing, verdict generation, and persona response text.
 *
 * SECURITY: Credentials are read from window.MOULD_DETECT_CONFIG.
 * For production, replace with a backend proxy that holds the secret server-side.
 * See: CR-13 in CODE-REVIEW-2025-07-17.md
 */
var MouldDetectService = (function () {
    var config = window.MOULD_DETECT_CONFIG || {};
    var TOKEN_URL = config.tokenUrl || 'https://www.nyckel.com/connect/token';
    var INVOKE_URL = config.invokeUrl || 'https://www.nyckel.com/v1/functions/if-theres-mold/invoke';
    var CLIENT_ID = config.clientId || '';
    var CLIENT_SECRET = config.clientSecret || '';

    var cachedToken = null;
    var tokenExpiresAt = 0;
    var tokenRequestInFlight = null;

    // --- Auth ---
    async function _requestToken(now) {
        var controller = new AbortController();
        var timeout = setTimeout(function () { controller.abort(); }, 30000);

        try {
            var res = await fetch(TOKEN_URL, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: 'grant_type=client_credentials&client_id=' + encodeURIComponent(CLIENT_ID) + '&client_secret=' + encodeURIComponent(CLIENT_SECRET),
                signal: controller.signal,
            });

            if (!res.ok) throw new Error('Auth failed: ' + res.status);
            var data = await res.json();
            cachedToken = data.access_token;
            tokenExpiresAt = now + (data.expires_in || 3600);
            return cachedToken;
        } finally {
            clearTimeout(timeout);
        }
    }

    // getToken — dedups concurrent callers onto a single in-flight token request
    // so a burst of calls doesn't fire N parallel OAuth requests.
    function getToken() {
        // MR-09 FIX: Fail fast when offline rather than hanging for 30s timeout
        if (typeof navigator !== 'undefined' && navigator.onLine === false) {
            return Promise.reject(new Error('offline'));
        }
        var now = Date.now() / 1000;
        if (cachedToken && tokenExpiresAt > now + 30) return Promise.resolve(cachedToken);

        if (tokenRequestInFlight) return tokenRequestInFlight;

        tokenRequestInFlight = _requestToken(now)
            .then(function (token) {
                tokenRequestInFlight = null;
                return token;
            })
            ['catch'](function (err) {
                tokenRequestInFlight = null;
                throw err;
            });
        return tokenRequestInFlight;
    }

    // --- Image compression ---
    // MR-01 FIX: delegates to shared ImageUtils.compressToJpeg — single source of truth
    function compressImage(dataURL, maxDim, quality) {
        return ImageUtils.compressToJpeg(dataURL, maxDim || 200, quality || 0.7);
    }

    // --- Detection API ---
    async function detect(imageDataURL) {
        var token = await getToken();
        var controller = new AbortController();
        var timeout = setTimeout(function () { controller.abort(); }, 60000);

        try {
            var res = await fetch(INVOKE_URL, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': 'Bearer ' + token,
                },
                body: JSON.stringify({ data: imageDataURL }),
                signal: controller.signal,
            });

            if (!res.ok) throw new Error('Detection API failed: ' + res.status);
            return res.json();
        } finally {
            clearTimeout(timeout);
        }
    }

    // --- Result parsing (handles all Nyckel response shapes) ---
    function parseResult(response) {
        var score = null;
        var label = null;

        if (response && response.result) {
            score = (response.result.confidence != null) ? response.result.confidence : response.result.score;
            label = response.result.label || response.result.prediction;
        }
        if (score == null && response && response.output) {
            score = (response.output.confidence != null) ? response.output.confidence : response.output.score;
            label = label || response.output.label;
        }
        if (score == null) score = (response.confidence != null) ? response.confidence : response.score;
        if (score == null && Array.isArray(response) && response.length > 0) {
            score = (response[0].confidence != null) ? response[0].confidence : response[0].score;
            label = label || response[0].label;
        }
        if (typeof score === 'string') score = parseFloat(score);
        if (typeof score === 'number' && score > 1) score = score / 100;
        if (score == null || isNaN(score)) {
            // MR-04 FIX: throw instead of fabricating a score from keyword scanning.
            // A malformed/empty/HTML error response must surface as an error rather
            // than presenting an invented confidence value as a real detection result.
            throw new Error('Unrecognised API response. Please try again.');
        }

        // Invert score for "No Mold" label
        var percentage = Math.round(score * 100);
        if (response.labelName === 'No Mold') {
            percentage = Math.round((1 - score) * 100);
        }

        return { percentage: Math.max(0, Math.min(100, percentage)), label: label || response.labelName };
    }

    // --- Verdict & persona ---
    function getVerdict(pct) {
        if (pct >= 80) return 'Very Likely Mould';
        if (pct >= 60) return 'Possible Mould';
        if (pct >= 40) return 'Uncertain';
        return 'Unlikely Mould';
    }

    function getSeverity(pct) {
        return SeverityConfig.fromPercentage(pct).severity;
    }

    function getPersonaResponse(pct) {
        if (pct >= 80)
            return 'This image shows strong visual indicators consistent with mould (' + pct + '% confidence). Check for moisture sources such as leaks, condensation, or poor ventilation. Professional inspection is recommended.';
        if (pct >= 60)
            return 'There are visible patterns that may be consistent with mould (' + pct + '% confidence). Monitor this area closely. Improving airflow and addressing possible moisture sources may help.';
        if (pct >= 40)
            return 'The result is inconclusive (' + pct + '% confidence). This could be surface staining, dirt, or early-stage mould. Keep an eye on changes over time and check for dampness or odours.';
        return 'This image does not show strong visual indicators of mould (' + pct + '% confidence). It may be surface marks or lighting effects. If you experience musty smells or moisture issues, further investigation is worthwhile.';
    }

    // --- Public API ---
    async function analyze(imageDataURL) {
        var compressed = await compressImage(imageDataURL);
        var response = await detect(compressed);
        var parsed = parseResult(response);
        return {
            percentage: parsed.percentage,
            label: parsed.label,
            verdict: getVerdict(parsed.percentage),
            severity: getSeverity(parsed.percentage),
            insight: getPersonaResponse(parsed.percentage),
            raw: response,
        };
    }

    // Also expose compression for preview use
    function compressForPreview(dataURL) {
        return compressImage(dataURL, 800, 0.8);
    }

    return { analyze: analyze, compressForPreview: compressForPreview };
})();

window.MouldDetectService = MouldDetectService;
