// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * SSE lifecycle event handlers: status, error, review_needed, completed, failed.
 *
 * Each handler signature: (data, ctx) => void | Promise<void>
 *
 * @module     local_coursegen/local/courseai/stream/handlers-lifecycle
 * @copyright  2026 Wilber Narvaez <https://datacurso.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

import {
    hideFeedbackThinking,
    showWorkingIndicator,
    leftHasRealContent,
} from 'local_coursegen/local/courseai/ui/feedback-progress';
import {
    transcriptHasContent,
    finalizeTranscript,
    rebuildTranscriptFromPlan,
} from 'local_coursegen/local/courseai/ui/plan-transcript';
import {finalizeRegen, rebuildRegenFromPlan} from 'local_coursegen/local/courseai/ui/regen-block';
import {addedActivityTurn} from 'local_coursegen/local/courseai/ui/added-turn';
import {getDecisionOverlay} from 'local_coursegen/local/courseai/ui/decision-overlay';

/**
 * Handle 'status' event: localize message, update UI text, advance heuristic progress.
 *
 * @param {Object} data
 * @param {Object} ctx
 * @returns {Promise<void>}
 */
export const handleStatus = async(data, ctx) => {
    const {
        state, stepsUi, texts, streamMode, syncTrackerFromStatus,
        ensureStreamContentVisible, localizeMessage,
        prvHeaderSub, pcSubtitle,
    } = ctx;

    const statusText = data.message ? await localizeMessage(data.message) : (data.text || '');
    const heuristicText = (data.message && data.message.string) || data.text || '';

    // During STRUCTURED generation the per-activity cards (spinner→check) are the
    // single source of truth. Suppress the raw per-activity narration ("Assembling
    // final Quiz package…", "Generating Chapter…") in the header/working indicator:
    // it fires per activity and, with parallel generation, contradicts the cards
    // (one card checked while the line narrates another). The structured handlers
    // own the header text/state instead, so both panels stay perfectly in sync.
    const suppressRaw = streamMode === 'generating' && state.structuredActivityProgress;

    // Surface transient progress as the SINGLE live "working" indicator in the
    // thread (never one turn per status event) so the panel never looks frozen
    // while the server streams. It is cleared when content/sections land or on a
    // terminal lifecycle event.
    if (statusText && !suppressRaw) {
        showWorkingIndicator(texts, statusText);
    }

    if (streamMode === 'generating') {
        ensureStreamContentVisible();
        if (!state.structuredActivityProgress) {
            syncTrackerFromStatus(heuristicText);
        }
    }

    const loadingTextEl = document.querySelector('.planning-loading-text');
    if (loadingTextEl && statusText && !suppressRaw) {
        loadingTextEl.textContent = statusText;
    }

    const totalActivities = state.phase4TotalActivities || ctx.preservedPhase4Total();

    if (prvHeaderSub && !suppressRaw) {
        prvHeaderSub.textContent = statusText;
    }
    if (pcSubtitle && !suppressRaw) {
        pcSubtitle.textContent = statusText;
    }

    if (state.currentStage !== 'generating' || totalActivities <= 0) {
        return;
    }

    const startPattern = /^(Designing|Generating Assignment content)/i;
    const completePattern = /ready|Assembling final|configuration ready|with \d+ discussion/i;

    if (startPattern.test(heuristicText)) {
        state.contentGenerationStarted = (state.contentGenerationStarted || 0) + 1;
        const startProgress = Math.min(30, (state.contentGenerationStarted / totalActivities) * 30);
        stepsUi.setProgress(Math.round(startProgress));
        return;
    }
    if (completePattern.test(heuristicText)) {
        state.contentGenerationCurrent = (state.contentGenerationCurrent || 0) + 1;
        const completeProgress = 30 + Math.min(60, (state.contentGenerationCurrent / totalActivities) * 60);
        stepsUi.setProgress(Math.round(completeProgress));
    }
};

/**
 * Handle 'error' event: non-fatal; show localized message in header and progress card.
 *
 * @param {Object} data
 * @param {Object} ctx
 * @returns {Promise<void>}
 */
export const handleError = async(data, ctx) => {
    const errorText = await ctx.localizeMessage(data.message);
    // 'error' is NON-FATAL and can arrive mid-early-phase before any section has
    // landed. Clearing the live working indicator here used to leave the LEFT
    // panel blank for a long stretch until the next status/section. Instead, keep
    // the single live indicator visible and track the error text on it — but only
    // while the LEFT still has no real content (checklist/structure). Once real
    // content exists, the indicator is no longer the left's only signal, so drop
    // it as before (a terminal lifecycle event will manage it from there).
    if (leftHasRealContent()) {
        hideFeedbackThinking();
    } else if (errorText) {
        showWorkingIndicator(ctx.texts, errorText);
    }
    if (ctx.prvHeaderSub && errorText) {
        ctx.prvHeaderSub.textContent = errorText;
    }
    if (ctx.pcSubtitle && errorText) {
        ctx.pcSubtitle.textContent = errorText;
    }
    // Nothing the server streams is silent: surface the error as a turn. 'error'
    // is non-fatal and may repeat within a round, so dedup consecutive identical
    // messages to avoid stacking the same turn.
    if (typeof ctx.emitLog === 'function' && errorText && ctx.state.lastErrorLogged !== errorText) {
        ctx.state.lastErrorLogged = errorText;
        ctx.emitLog({actor: 'ai', kind: 'danger', message: errorText});
    }
};

/**
 * Handle 'review_needed' event: reconcile the plan diff, show review actions,
 * and close the stream to prevent EventSource auto-reconnect (which would
 * trigger an endless review_needed storm).
 *
 * Uses the diff-based reconciler so only changed elements animate; unchanged
 * rows are never re-rendered or cleared.
 *
 * @param {Object} data
 * @param {Object} ctx
 * @returns {Promise<void>}
 */
export const handleReviewNeeded = async(data, ctx) => {
    const {
        state, stepsUi, planningUi, detailedUi, proposalsUi, texts, emitLog,
        ensureStreamContentVisible, hideStreamBar, closeStream,
    } = ctx;

    state.isStreaming = false;
    // The AI responded — drop the live "working" indicator.
    hideFeedbackThinking();

    // Cache the latest fully-detailed plan so that, when the user approves, the
    // accept handler can render the complete approved-plan detail summary without
    // waiting for a round-trip (the persisted approved snapshot drives reload).
    if (Array.isArray(data.current_plan) && data.current_plan.length) {
        state.lastReviewedPlan = data.current_plan;
    }

    // The section checklist stays as-is (name + spinner→check). Its per-section
    // Markdown detail (description + activities) sits below each item:
    //  - initial round: the live accumulator already filled each detail in real
    //    time → just settle them (clamp long ones with "Show more"/"Show less").
    //  - keepPlan adjust: the live detail fill is skipped during the re-stream, so
    //    rebuild the checklist + details from the authoritative current_plan.
    // Activity regeneration: the regenerated activity streamed into its OWN block
    // (regen-block) — finalize it (clamp) and leave the top "structure I planned"
    // checklist FROZEN. Do NOT rebuild the top here, otherwise it would overwrite
    // the initial snapshot. reorder/initial (regenScope null) keep the old path.
    const isActivityRegen = state.regenScope && state.regenScope.action === 'replan_activity';
    const isSectionRegen = state.regenScope && state.regenScope.action === 'replan_section';
    const isAddSection = state.addScope && state.addScope.action === 'add_section';
    if (isActivityRegen || isSectionRegen || isAddSection) {
        // Activity regen, section regen and section add ALL streamed live into their
        // OWN bottom block (regen-block) as their events arrived — just settle it
        // (clamp long details). The top "structure I planned" checklist stays FROZEN;
        // rebuilding it here would overwrite the initial snapshot and re-touch what
        // is already above (a chat appends at the end, it never rewrites the past).
        finalizeRegen();
    } else if (state.addScope) {
        // add_activity: shown below at review time (it has no live section block).
        // (no top rebuild)
    } else if (ctx.keepPlan) {
        // DEFAULT for ANY keepPlan change: the top "structure I planned" checklist is
        // FROZEN history — reorder, delete, adjust, replace, or any future action may
        // NOT rewrite it (that pulled later additions up into the original snapshot).
        // The change shows as a turn below and the centre already reflects it, so just
        // settle. The SOLE exception is a full course regeneration, which the user
        // explicitly asked to rebuild the WHOLE plan — there the top is replaced.
        if (state.keepPlanAction === 'full_regeneration'
            && Array.isArray(data.current_plan) && data.current_plan.length) {
            rebuildTranscriptFromPlan(data.current_plan);
        } else {
            finalizeTranscript();
        }
    } else if (transcriptHasContent()) {
        finalizeTranscript();
    } else if (Array.isArray(data.current_plan) && data.current_plan.length) {
        rebuildTranscriptFromPlan(data.current_plan);
    }
    // Consume the regeneration scope for this round (set in runPlanAction). Cleared
    // here AND on failure/error handlers so a later reorder/accept is never mistaken
    // for a regen.
    state.regenScope = null;
    state.keepPlanAction = null;

    // Adding an element: the click-time turn was intentionally silent (the name
    // didn't exist yet).
    //  - add_section already named its "You added section: X" turn AND streamed its
    //    detail block live as the events arrived (handleSection / regen-block), so
    //    there is nothing to render here — the live block was just finalized above.
    //  - add_activity has no live block, so name it and show its detail block now,
    //    from the settled plan (the element whose id was not present before the add).
    if (state.addScope) {
        if (state.addScope.action !== 'add_section') {
            const beforeIds = state.addScope.beforeIds || [];
            const added = addedActivityTurn(
                texts, data.current_plan, state.addScope.parentSectionId, beforeIds
            );
            if (added && typeof emitLog === 'function') {
                emitLog({actor: 'user', kind: 'success', message: added});
            }
            const plan = Array.isArray(data.current_plan) ? data.current_plan : [];
            // The parent may be a SUBSECTION: check top-level ids first, then
            // each section's subsections.
            let parent = plan.find((s) => s && s.id === state.addScope.parentSectionId);
            if (!parent) {
                plan.some((s) => {
                    const sub = ((s && s.subsections) || []).find(
                        (x) => x && x.id === state.addScope.parentSectionId
                    );
                    if (sub) {
                        parent = sub;
                        return true;
                    }
                    return false;
                });
            }
            const newAct = ((parent && parent.activities) || [])
                .find((a) => a && !a.deleted && beforeIds.indexOf(a.id) === -1);
            if (newAct) {
                rebuildRegenFromPlan({action: 'replan_activity', targetIds: [newAct.id], plan});
            }
        }
        state.addScope = null;
    }

    // The plan has settled: from now on log entries flow BELOW the section checklist.
    // Set this BEFORE emitting the milestone so the AI's "review the plan" message
    // lands AFTER the planned-structure group (chronological: plan first, then the
    // prompt to review), not above it.
    // Capture BEFORE flipping the flag: the FIRST review follows the initial
    // planning ("I finished planning…"); every later review follows a user
    // adjustment, so it must read "I applied your changes…" instead.
    const firstReview = !state.planEverReviewed;
    state.planEverReviewed = true;
    // Meaningful milestone: the AI finished this round and is awaiting review. Phrased
    // as the assistant talking to the user (first person, no dashes).
    if (typeof emitLog === 'function') {
        const hasProposals = Array.isArray(data.proposals) && data.proposals.length > 0;
        let message;
        if (hasProposals) {
            message = (texts && texts.courseai_log_ai_proposals_ready)
                || 'I prepared a few suggestions for you. Review them and choose how you want to continue.';
        } else if (firstReview) {
            message = (texts && texts.courseai_log_ai_review_ready)
                || 'I finished planning your course. Take a look at the plan and tell me if you want any changes.';
        } else {
            message = (texts && texts.courseai_log_ai_review_updated)
                || 'I applied your changes. Take a look and tell me if you want anything else.';
        }
        emitLog({actor: 'ai', kind: 'ai', message});
    }
    if (typeof ctx.onStreamEnd === 'function') {
        ctx.onStreamEnd();
    }
    ensureStreamContentVisible();
    if (typeof hideStreamBar === 'function') {
        hideStreamBar();
    }
    stepsUi.setStepState('planning', 'done');
    state.currentStage = 'planning';
    stepsUi.updateFlowNav();

    // Show the bottom review UI (decision card) FIRST — BEFORE the async center
    // reconcile below. reconcilePlan awaits ~1s, and during that gap the composer
    // (Stop/input) was left visible and flashed for a beat before the decision card
    // finally replaced it. Rendering the decision card now claims the bottom slot
    // immediately (it hides the composer), so nothing flashes while the center
    // reconciles in the background.
    document.body.classList.add('cg-plan-reviewed');
    // Claim the bottom slot for the decision card SYNCHRONOUSLY first: this hides the
    // composer immediately. Otherwise showReviewActions → setCompactChatState('enabled')
    // shows the composer (the overlay isn't visible yet) and renderProposals (async,
    // localizes) only hides it a beat later → the composer flashes for ~0.3s.
    getDecisionOverlay().show();
    planningUi.showReviewActions(state.planningMode === 'detailed' ? 'detailed' : 'markdown');
    if (proposalsUi && typeof proposalsUi.renderProposals === 'function') {
        proposalsUi.renderProposals(data);
    }

    if (Array.isArray(data.current_plan) && data.current_plan.length > 0) {
        await detailedUi.reconcilePlan(data.current_plan);
    }

    if (typeof detailedUi.finalizePlanView === 'function') {
        detailedUi.finalizePlanView();
    }
    if (typeof detailedUi.enableAllActionControls === 'function') {
        detailedUi.enableAllActionControls();
    }
    // Do NOT enable/show the composer here: at review the decision card owns the
    // bottom slot. The composer reappears only when the user clicks "Adjust".
    // review_needed is a terminal pause — close so EventSource does NOT auto-reconnect.
    closeStream();
};

/**
 * Handle 'completed' event: finalize tracker, set stage, close stream, create course.
 *
 * @param {Object} data
 * @param {Object} ctx
 * @returns {Promise<void>}
 */
export const handleCompleted = async(data, ctx) => {
    const {
        state, stepsUi, streamMode, markAllDone, texts, emitLog,
        setCompletionStatsFromGeneratedResult, closeStream, createCourseFromSession,
        hideStreamBar,
    } = ctx;
    hideFeedbackThinking();
    if (typeof hideStreamBar === 'function') {
        hideStreamBar();
    }
    // Header: NOW the whole flow is truly done (activities + images + config + cleanup),
    // so swap the still-running spinner for the check. The spinner was intentionally
    // kept spinning through the tail phases (see resolveGenerationHeaderIfDone) so the
    // final ~minute never looked frozen.
    const prvHeader = document.getElementById('prvHeader');
    const prvSpinner = document.getElementById('prvSpinnerIcon');
    const prvCheck = document.getElementById('prvCheckIcon');
    if (prvHeader) { prvHeader.classList.add('prv-header--done'); }
    if (prvSpinner) { prvSpinner.style.display = 'none'; }
    if (prvCheck) { prvCheck.style.display = ''; }
    // Meaningful milestone: generation finished → one permanent success turn.
    if (typeof emitLog === 'function') {
        emitLog({
            actor: 'ai',
            kind: 'success',
            message: (texts && texts.courseai_log_ai_completed) || 'Course generated',
        });
    }
    if (streamMode === 'generating') {
        markAllDone();
    }
    setCompletionStatsFromGeneratedResult(data.result || []);
    stepsUi.setStepState('planning', 'done');
    stepsUi.setStepState('generating', 'active');
    state.currentStage = 'generating';
    stepsUi.updateFlowNav();
    closeStream();
    if (typeof ctx.onStreamEnd === 'function') {
        ctx.onStreamEnd();
    }
    await createCourseFromSession();
};

/**
 * Handle 'failed' event: mark stream done, show error state, re-enable chat.
 *
 * @param {Object} data
 * @param {Object} ctx
 * @returns {Promise<void>}
 */
export const handleFailed = async(data, ctx) => {
    const {
        state, stepsUi, detailedUi, texts, streamMode, emitLog,
        markAllDone, closeStream, setCompactChatState, deps,
        localizeMessage, planningSpinner, pcStep, pcSubtitle, hideStreamBar,
    } = ctx;
    state.isStreaming = false;
    hideFeedbackThinking();
    // A failed replan must not leave the regen scope set (a later reorder/accept
    // would be misrouted) — drop the half-built block and clear the flag.
    finalizeRegen();
    state.regenScope = null;
    state.addScope = null;
    if (typeof ctx.onStreamEnd === 'function') {
        ctx.onStreamEnd();
    }
    // Localize once and reuse for both the chat turn and the subtitle.
    let failedText = data.message
        ? await localizeMessage(data.message)
        : (texts && texts.courseai_error_generic) || 'Generation failed';
    if (data.detail) {
        failedText += '\n' + String(data.detail);
    }
    // Meaningful (fatal) milestone: surface failure as a permanent error turn.
    if (typeof emitLog === 'function') {
        emitLog({actor: 'ai', kind: 'danger', message: failedText});
    }
    state.currentStage = 'failed';
    // Retryable failure (the model gave up after exhausting its automatic
    // retries): offer a manual "Retry" button that re-opens the stream so the
    // graph re-runs the failed step — a fresh run restarts the retry counter.
    if (data && data.retryable && state.streamingurl && typeof ctx.openSSEStream === 'function') {
        const feed = document.getElementById('cgLogAfter') || document.getElementById('cgLog');
        if (feed && !feed.querySelector('.cg-retry-btn')) {
            const btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'cg-retry-btn btn btn-warning btn-sm mt-2';
            btn.textContent = texts.courseai_retry || 'Retry';
            btn.addEventListener('click', () => {
                btn.remove();
                state.isStreaming = true;
                ctx.openSSEStream(state.streamingurl, 0, streamMode, true);
            });
            feed.appendChild(btn);
        }
    }
    if (typeof hideStreamBar === 'function') {
        hideStreamBar();
    }
    if (streamMode === 'generating') {
        markAllDone();
    }
    stepsUi.setStepState('planning', 'active');
    closeStream();
    if (planningSpinner) {
        planningSpinner.classList.add('done');
    }
    if (pcStep) {
        pcStep.textContent = texts.courseai_state_error;
    }
    if (pcSubtitle) {
        pcSubtitle.textContent = failedText;
    }
    if (typeof detailedUi.enableAllActionControls === 'function') {
        detailedUi.enableAllActionControls();
    }
    setCompactChatState(deps, 'enabled');
};
