// 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/>.

/**
 * @module     block_mwa_dashboard/dashboardapp
 * @copyright  2026 Bruno Porto
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

/**
 * Note: This module generates HTML markup directly in JavaScript strings for
 * performance reasons — the dashboard renders large dynamic datasets (student lists,
 * heatmaps, charts) that require frequent partial updates. All user-supplied data is
 * escaped via the esc() helper before insertion into the DOM.
 * See: https://docs.moodle.org/dev/JavaScript_Modules#HTML_generation
 */
define([
    'block_mwa_dashboard/dashboardstore',
    'block_mwa_dashboard/actioncenter',
    'block_mwa_dashboard/alerts',
    'block_mwa_dashboard/classlist',
    'block_mwa_dashboard/studentprofile',
    'block_mwa_dashboard/activities',
    'block_mwa_dashboard/grades',
    'block_mwa_dashboard/interventions',
    'block_mwa_dashboard/engagement',
    'block_mwa_dashboard/content',
    'block_mwa_dashboard/personalised',
    'block_mwa_dashboard/chat',
    'core/templates'
], function(Store, ActionCenter, Alerts, ClassList, Profile, Activities, Grades, Interventions, Engagement, Content, Personalised, Chat, Templates) {

    'use strict';

    var Modules = {
        ActionCenter: ActionCenter,
        Alerts: Alerts,
        ClassList: ClassList,
        Profile: Profile,
        Activities: Activities,
        Grades: Grades,
        Interventions: Interventions
    };
    var eventChart = null;

    var init = function(config) {
        var STRINGS = Store.getStrings();
        function tr(key, fallback){
          var value = Object.prototype.hasOwnProperty.call(STRINGS, key) ? STRINGS[key] : '';
          if (typeof value === 'string' && value && !/^\[\[.*\]\]$/.test(value)) { return value; }
          return (fallback !== undefined && fallback !== null) ? fallback : key;
        }
        function renderInlineStatus(el, message) {
          if (!el) { return; }
          Templates.render('block_mwa_dashboard/content_inline_status', {
            color: 'var(--muted)',
            message: message
          }).then(function(html) {
            Templates.replaceNodeContents(el, html, '');
          });
        }
        function applyTranslations(){
          document.querySelectorAll('[data-i18n]').forEach(function(el){
            var key = el.getAttribute('data-i18n');
            el.textContent = tr(key, el.textContent);
          });
          document.querySelectorAll('[data-i18n-attr]').forEach(function(el){
            var spec = el.getAttribute('data-i18n-attr').split(':');
            var attr = spec[0], key = spec[1];
            if (attr && key) { el.setAttribute(attr, tr(key, el.getAttribute(attr) || '')); }
          });
        }
        applyTranslations();
        document.addEventListener('click', function(e){
          // Button with data-bulk-kind only (no data-action-page) → opens modal
          var bulkBtn = e.target.closest('[data-bulk-kind]');
          if (bulkBtn) {
            e.preventDefault();
            e.stopPropagation();
            var ac = window.MWAActionCenter;
            if (ac && typeof ac.openBulkForKind === 'function') {
              ac.openBulkForKind(bulkBtn.getAttribute('data-bulk-kind'));
            }
            return;
          }
          // Navigation buttons (data-action-page, without data-bulk-kind)
          var btn = e.target.closest('[data-action-page]');
          if (btn) {
            e.preventDefault();
            showPage(btn.getAttribute('data-action-page'));
          }
        });
        
        var state={logs:[],grades:[],students:[],activities:[],seen:false};
        var COLORS=['#4f8ef7','#2fb579','#d9962c','#8b6bd6','#e05a5a','#14b8a6'];
        function $(id){return document.getElementById(id)}
        function norm(v){return (v===undefined||v===null)?'':String(v).trim()}
        function lower(v){return norm(v).toLowerCase()}
        function parseDate(log){if(log._ts){return new Date(Number(log._ts)*1000)}var s=norm(log.hora);var m=s.match(/(\d{2})\/(\d{2})\/(\d{2}),\s*(\d{2}):(\d{2})/);if(m){return new Date(2000+Number(m[3]),Number(m[2])-1,Number(m[1]),Number(m[4]),Number(m[5]))}return null}
        function studentKey(name,email){return lower(email)||lower(name)}
        function gradeEmail(row){var k=Object.keys(row).find(function(x){return lower(x)==='email'||lower(x).includes('email')});return k?norm(row[k]):''}
        function gradeName(row){var f=Object.keys(row).find(function(x){return lower(x).includes('first')||lower(x).includes('nome')});var l=Object.keys(row).find(function(x){return lower(x).includes('last')||lower(x).includes('sobrenome')});return [f?row[f]:'',l?row[l]:''].join(' ').trim()}
        function gradeTotal(row){var keys=Object.keys(row);var k=keys.find(function(x){var lx=lower(x);return lx.includes('course total')||lx.includes('total do curso')||lx==='total'});var v=k?row[k]:null;var n=parseFloat(String(v).replace(',','.'));return isNaN(n)?null:n}
        function isSubmission(log){var text=lower([log.nomedoevento,log.action,log.componente,log.component].join(' '));return text.includes('submit')||text.includes('submission')||text.includes('submitted')||text.includes('upload')||text.includes('graded')}
        function activityName(log){return norm(log.contextodoevento)||norm(log.context)||norm(log._modtype)||null}
        function componentName(log){return norm(log.componente)||norm(log.component)||norm(log._modtype)||tr('other','Other')}
        function buildModel(){var by={};var activitiesMap={};state.logs.forEach(function(l){var name=norm(l.nomecompleto);if(!name)return;var email=norm(l.email);var key=studentKey(name,email);if(!by[key])by[key]={name:name,email:email,interactions:0,contexts:{},submissions:0,last:null,grade:null,score:0};by[key].interactions++;var a=activityName(l);if(a){by[key].contexts[a]=true;activitiesMap[a]=activitiesMap[a]||{name:a,type:componentName(l),accesses:0,students:{}};activitiesMap[a].accesses++;activitiesMap[a].students[key]=true;}if(isSubmission(l))by[key].submissions++;var d=parseDate(l);if(d&&(!by[key].last||d>by[key].last))by[key].last=d});state.grades.forEach(function(g){var email=gradeEmail(g), name=gradeName(g);if(!name&&!email)return;var key=studentKey(name,email);if(!by[key])by[key]={name:name||email,email:email,interactions:0,contexts:{},submissions:0,last:null,grade:null,score:0};var gt=gradeTotal(g);if(gt!==null)by[key].grade=gt;by[key].gradeRow=g});var totalActivities=Math.max(1,Object.keys(activitiesMap).length);state.students=Object.keys(by).map(function(k){var s=by[k];var coverage=Object.keys(s.contexts).length/totalActivities*100;var accessScore=Math.min(100,s.interactions*10);var submitScore=s.submissions>0?100:0;var grade=(s.grade===null?null:s.grade);var score;if(grade!==null){score=Math.round(grade*.5+coverage*.3+submitScore*.2)}else{score=Math.round(accessScore*.6+coverage*.4)}s.coverage=Math.round(coverage);s.score=score;s.risk=score<40?'High':score<70?'Medium':'Low';return s}).sort(function(a,b){return a.score-b.score});state.activities=Object.keys(activitiesMap).map(function(k){var a=activitiesMap[k];a.unique=Object.keys(a.students).length;return a}).sort(function(a,b){return b.accesses-a.accesses})}
        /* ── Delta chip ▲▼▬ reusable across all tabs ── */
        window.MWADeltaChip = function(cur, prv, invertColors) {
          if (prv === null || prv === undefined || cur === null || cur === undefined) return '';
          var d = cur - prv;
          var pos = invertColors ? d < 0 : d > 0;
          var neg = invertColors ? d > 0 : d < 0;
          var color  = d === 0 ? '#8a94a8' : pos ? '#13794c' : '#b42318';
          var bg     = d === 0 ? '#f0f2f7' : pos ? '#e8f7ef' : '#fdecec';
          var arrow  = d === 0 ? '▬' : d > 0 ? '▲' : '▼';
          var sign   = d > 0 ? '+' : '';
          return '<span style="background:'+bg+';color:'+color+';font-size:.65rem;font-weight:800;padding:2px 7px;border-radius:99px;float:right;margin-left:4px;">'+arrow+' '+sign+d+'</span>';
        };

        /* ── Split logs into recent half vs earlier half ── */
        function splitLogsHalf() {
          var dated = [];
          state.logs.forEach(function(l) {
            var d = parseDate(l);
            if (d) dated.push({ l: l, t: d.getTime() });
          });
          dated.sort(function(a,b){ return a.t - b.t; });
          if (dated.length < 4) return { cur: dated, prv: [] };
          var mid = Math.floor(dated.length / 2);
          return { cur: dated.slice(mid), prv: dated.slice(0, mid) };
        }

        function statsOf(arr) {
          var names = {}, nameSet = {}, ri = 0;
          arr.forEach(function(x){ var n=norm(x.l.nomecompleto); if(n){ names[n]=true; nameSet[n]=true; }});
          state.students.forEach(function(st){ if(nameSet[norm(st.name)]&&st.score<40) ri++; });
          var s = Object.keys(names).length;
          return { students: s, logs: arr.length, avgInt: s?Math.round(arr.length/s):0, atRisk: ri };
        }

        /* ── Renderiza um KPI inteligente ── */
        function kpiSmart(label, value, sub, delta, invertDelta) {
          // delta: number or null
          var chipHtml = '';
          if (delta !== null && delta !== undefined && !isNaN(Number(delta))) {
            var pos = invertDelta ? delta < 0 : delta > 0; // invertido para "em risco"
            var neg = invertDelta ? delta > 0 : delta < 0;
            var chipClr = delta === 0 ? '#8a94a8' : pos ? '#13794c' : '#b42318';
            var chipBg  = delta === 0 ? '#f0f2f7' : pos ? '#e8f7ef' : '#fdecec';
            var arrow   = delta === 0 ? '▬' : delta > 0 ? '▲' : '▼';
            var sign    = delta > 0 ? '+' : '';
            chipHtml = '<span style="background:'+chipBg+';color:'+chipClr+';font-size:.68rem;font-weight:800;padding:2px 8px;border-radius:99px;letter-spacing:.02em;">'+arrow+' '+sign+delta+'</span>';
          }
          return '<div class="kpi">'            + '<div class="kpi-label" style="display:flex;justify-content:space-between;align-items:center;">'            + '<span>'+label+'</span>'+chipHtml            + '</div>'            + '<div class="kpi-value">'+value+'</div>'            + '<div class="kpi-sub">'+sub+'</div>'            + '</div>';
        }



        function kpi(label,value,cls,sub){return '<div class="kpi"><div class="kpi-label">'+label+'</div><div class="kpi-value '+(cls||'')+'">'+value+'</div><div class="kpi-sub">'+(sub||'')+'</div></div>'}
        
        
        var dashboard={state:state,$:$,tr:tr,esc:esc,norm:norm,lower:lower,parseDate:parseDate,studentKey:studentKey,componentName:componentName,callAction:function(method,args){return Store.callAction(method,args||{});},receiveData:receiveData,receiveError:receiveError};
        Store.setDashboard(dashboard);
        function renderKPIs(){
          var avgInt = state.students.length ? Math.round(state.logs.length / state.students.length) : 0;
          var atRisk = state.students.filter(function(s){return s.score<40;}).length;
          var grades = state.students.map(function(s){return s.grade;}).filter(function(v){return v!==null;});
          var avgGrade = grades.length ? Math.round(grades.reduce(function(a,b){return a+b;},0)/grades.length) : null;

          // Compare recent half vs metade anterior dos logs
          var split    = splitLogsHalf();
          var cur      = statsOf(split.cur);
          var prv      = statsOf(split.prv);
          var hasPrev  = prv.logs > 0;

          var dStudents = hasPrev ? (cur.students - prv.students) : null;
          var dLogs     = hasPrev ? (cur.logs     - prv.logs)     : null;
          var dAvgInt   = hasPrev ? (cur.avgInt   - prv.avgInt)   : null;
          var dRisk     = hasPrev ? (cur.atRisk   - prv.atRisk)   : null;

          // Grade delta: average grade of active students in recent vs earlier period
          var dGrade = null;
          if (hasPrev && avgGrade !== null) {
            var prvNames = {};
            split.prv.forEach(function(x){ var n=norm(x.l.nomecompleto); if(n) prvNames[n]=true; });
            var prvGrades = state.students.filter(function(s){ return prvNames[norm(s.name)] && s.grade!==null; }).map(function(s){ return s.grade; });
            if (prvGrades.length) {
              var prvAvgGrade = Math.round(prvGrades.reduce(function(a,b){return a+b;},0)/prvGrades.length);
              dGrade = avgGrade - prvAvgGrade;
            }
          }

          Store.renderHtml($('kpis'), kpiSmart(tr('students_in_log','Estudantes no log'),     state.students.length, tr('active_students','estudantes ativos'),   dStudents, false)
          + kpiSmart(tr('total_interactions','Total de interações'), state.logs.length,     tr('moodle_events','eventos do Moodle'),     dLogs,     false)
          + kpiSmart(tr('average_student','Média por estudante'),    avgInt,                tr('interactions','interações'),             dAvgInt,   false)
          + kpiSmart(tr('at_risk','Em risco'),                       atRisk,                tr('score_below_40','score abaixo de 40'),   dRisk,     true)
          + (avgGrade!==null ? kpiSmart(tr('grade_average','Média das notas'), avgGrade, tr('of_100_points','de 100 pontos'), dGrade, false) : ''));
        }
        
        function renderChart(){var SKIP={System:1,Login:1,system:1,login:1,outro:1,Outro:1};var counts={};state.logs.forEach(function(l){var c=componentName(l);if(SKIP[c])return;counts[c]=(counts[c]||0)+1});var labels=Object.keys(counts);var data=labels.map(function(k){return counts[k]});var ctx=$('eventChart');if(!ctx||!window.Chart)return; if(eventChart)eventChart.destroy();eventChart=new Chart(ctx,{type:'doughnut',data:{labels:labels,datasets:[{data:data,backgroundColor:COLORS,borderWidth:0,hoverOffset:4}]},options:{cutout:'62%',plugins:{legend:{position:'bottom',labels:{usePointStyle:false,boxWidth:12,padding:14,font:{size:12}}}},maintainAspectRatio:false}})}
        function badge(risk){var c=risk==='High'?'low':risk==='Medium'?'medium':'high';return '<span class="badge '+c+'">'+(risk==='High'?tr('risk_high','High'):risk==='Medium'?tr('risk_medium','Medium'):tr('risk_low','Low'))+'</span>'}
        function renderTables(){var list=state.students;var q=lower($('search').value);if(q){list=list.filter(function(s){return lower(s.name).includes(q)||lower(s.email).includes(q)})}var st=$('studentsTable');if(st)Store.renderHtml(st, '<thead><tr><th>'+tr('student','Student')+'</th><th>'+tr('email','Email')+'</th><th>'+tr('interactions','Interactions')+'</th><th>'+tr('coverage','Coverage')+'</th><th>'+tr('grade','Grade')+'</th><th>'+tr('score','Score')+'</th><th>'+tr('risk','Risk')+'</th></tr></thead><tbody>'+list.map(function(s){return '<tr><td>'+esc(s.name)+'</td><td>'+esc(s.email)+'</td><td>'+s.interactions+'</td><td>'+s.coverage+'%</td><td>'+(s.grade===null?'-':s.grade)+'</td><td>'+s.score+'%</td><td>'+badge(s.risk)+'</td></tr>'}).join('')+'</tbody>');var at=$('activitiesTable');if(at)Store.renderHtml(at, '<thead><tr><th>'+tr('activity','Activity')+'</th><th>'+tr('type','Type')+'</th><th>'+tr('accesses','Accesses')+'</th><th>'+tr('unique_students','Unique students')+'</th></tr></thead><tbody>'+state.activities.map(function(a){return '<tr><td>'+esc(a.name)+'</td><td>'+esc(a.type)+'</td><td>'+a.accesses+'</td><td>'+a.unique+'</td></tr>'}).join('')+'</tbody>');renderGradesTable()}
        function renderGradesTable(){var gt=$('gradesTable');if(!gt)return;if(!state.grades.length){Store.renderHtml(gt, '<tbody><tr><td class="empty">'+tr('no_grade_data_available','No grade data available.')+'</td></tr></tbody>');return}var keys=Object.keys(state.grades[0]).slice(0,12);Store.renderHtml(gt, '<thead><tr>'+keys.map(function(k){return '<th>'+esc(k)+'</th>'}).join('')+'</tr></thead><tbody>'+state.grades.map(function(r){return '<tr>'+keys.map(function(k){return '<td>'+esc(r[k])+'</td>'}).join('')+'</tr>'}).join('')+'</tbody>')}
        function renderAlerts(){if(Modules.Alerts){Modules.Alerts.reset();Modules.Alerts.render();}}
        function alertHtml(s){var cl=s.risk==='High'?'':'medium';var msg=s.risk==='High'?tr('immediate_intervention','Immediate intervention recommended'):tr('monitor_preventive','Monitor and send a preventive message');return '<div class="alert-card '+cl+'"><strong>'+esc(s.name)+'</strong><div class="muted">'+msg+' · '+tr('score','Score')+' '+s.score+'% · '+s.interactions+' '+tr('interactions','interactions')+' · '+tr('grade','Grade')+' '+(s.grade===null?'-':s.grade)+'</div></div>'}
        function renderHeatmap() {
          var DAYS = [tr('sun','Dom'),tr('mon','Seg'),tr('tue','Ter'),tr('wed','Qua'),
                      tr('thu','Qui'),tr('fri','Sex'),tr('sat','Sáb')];

          /* ── read UI filters ── */
          var selStudent = document.getElementById('heatmapStudentSel');
          var selMod     = document.getElementById('heatmapModSel');
          var selMode    = document.getElementById('heatmapModeSel');
          var studentFilter = selStudent ? selStudent.value : '';
          var modFilter     = selMod     ? selMod.value     : '';
          var mode          = selMode    ? selMode.value    : 'access';

          /* ── preenche selects com tr() (garante i18n ao trocar idioma) ── */
          if (selStudent && selStudent.options.length === 0) {
            var optAll = document.createElement('option');
            optAll.value = ''; optAll.textContent = '👥 ' + tr('hm_filter_all_students','Turma toda');
            selStudent.appendChild(optAll);
            state.students.forEach(function(s) {
              var opt = document.createElement('option');
              opt.value = norm(s.name); opt.textContent = s.name;
              selStudent.appendChild(opt);
            });
          }
          if (selMod && selMod.options.length === 0) {
            [
              {v:'',  l:'📚 '+tr('hm_filter_all_resources','Todos os recursos')},
              {v:'quiz',        l:'❓ '+tr('hm_filter_quiz','Questionários')},
              {v:'forum',       l:'💬 '+tr('hm_filter_forum','Fóruns')},
              {v:'resource',    l:'📄 '+tr('hm_filter_resource','Arquivos')},
              {v:'url',         l:'🔗 '+tr('hm_filter_url','URLs')},
              {v:'page',        l:'📖 '+tr('hm_filter_page','Páginas')},
              {v:'h5pactivity', l:'🎮 '+tr('hm_filter_h5p','H5P')},
              {v:'scorm',       l:'📦 '+tr('hm_filter_scorm','SCORM')},
              {v:'video',       l:'🎥 '+tr('hm_filter_video','Vídeos')}
            ].forEach(function(o){
              var opt=document.createElement('option');
              opt.value=o.v; opt.textContent=o.l;
              selMod.appendChild(opt);
            });
          }
          if (selMode && selMode.options.length === 0) {
            [{v:'access',l:'🟢 '+tr('hm_mode_access','Acessos')},
             {v:'dropout',l:'🔴 '+tr('hm_mode_dropout','Abandono')}]
            .forEach(function(o){
              var opt=document.createElement('option');
              opt.value=o.v; opt.textContent=o.l;
              selMode.appendChild(opt);
            });
          }

          /* ── filtra logs ── */
          var logs = state.logs.filter(function(l) {
            if (studentFilter && norm(l.nomecompleto) !== studentFilter) return false;
            if (modFilter && (norm(l._modtype)||'').toLowerCase().indexOf(modFilter) < 0) return false;
            return true;
          });

          /* ── grid principal ── */
          var grid = {};      // day:hour → count
          var lastGrid = {};  // day:hour → count of last accesses (dropout mode)
          var weekTrend = [{},{}]; // [semana anterior, esta semana]
          var now = new Date();
          var weekAgo  = new Date(now - 7*86400000);
          var twoWeeks = new Date(now - 14*86400000);

          logs.forEach(function(l) {
            var d = parseDate(l);
            if (!d) return;
            var key = d.getDay() + ':' + d.getHours();

            if (mode === 'access') {
              grid[key] = (grid[key]||0) + 1;
            } else {
              /* dropout: count only last access per student per slot */
              var uid = norm(l.nomecompleto);
              if (!lastGrid[uid] || d > lastGrid[uid].date) {
                lastGrid[uid] = {date: d, key: key};
              }
            }

            /* weekly trend */
            if (d >= twoWeeks && d < weekAgo) {
              weekTrend[0][key] = (weekTrend[0][key]||0) + 1;
            } else if (d >= weekAgo) {
              weekTrend[1][key] = (weekTrend[1][key]||0) + 1;
            }
          });

          /* modo abandono: converte lastGrid → grid */
          if (mode === 'dropout') {
            Object.keys(lastGrid).forEach(function(uid) {
              var k = lastGrid[uid].key;
              grid[k] = (grid[k]||0) + 1;
            });
          }

          var max = Math.max(1, Math.max.apply(null, Object.values(grid).concat([0])));

          /* ── heat bands (5 levels) ── */
          function heatClass(v) {
            if (!v) return '';
            if (v/max > 0.75) return 'v5';
            if (v/max > 0.50) return 'v4';
            if (v/max > 0.25) return 'v3';
            if (v/max > 0.08) return 'v2';
            return 'v1';
          }

          /* ── deadlines detectados (prazos: horas com pico isolado) ── */
          var deadlines = {};
          for (var d2=0; d2<7; d2++) {
            for (var h=0; h<24; h++) {
              var v    = grid[d2+':'+h]     || 0;
              var vPre = grid[d2+':'+(h-1)] || 0;
              var vNxt = grid[d2+':'+(h+1)] || 0;
              if (v > 0 && v/max > 0.7 && v > vPre * 2 && v > vNxt * 2) {
                deadlines[d2+':'+h] = true;
              }
            }
          }

          /* ── renderiza o grid ── */
          var html = '<div class="h"></div>';
          for (var h2=0; h2<24; h2++) html += '<div class="h">' + h2 + 'h</div>';

          for (var d3=0; d3<7; d3++) {
            html += '<div class="h">' + DAYS[d3] + '</div>';
            for (var h3=0; h3<24; h3++) {
              var v3   = grid[d3+':'+h3] || 0;
              var cls  = heatClass(v3);
              var prev = weekTrend[0][d3+':'+h3] || 0;
              var curr = weekTrend[1][d3+':'+h3] || 0;
              var trend = (prev > 0 && curr > prev*1.3) ? ' ▲' : (prev > 0 && curr < prev*0.7) ? ' ▼' : '';
              var deadline = deadlines[d3+':'+h3] ? ' 🔴' : '';
              var label = v3 ? (v3 + trend + deadline) : '';
              var tip = DAYS[d3] + ' ' + h3 + 'h: ' + v3 + ' '+(v3===1?tr('hm_access_count','{n} acesso').replace('{n}',''):tr('hm_accesses_count','{n} acessos').replace('{n}','')) +
                        (trend === ' ▲' ? ' (+'+tr('hm_trend_up','tendência de alta')+')' : trend === ' ▼' ? ' (-'+tr('hm_trend_down','tendência de baixa')+')' : '') +
                        (deadline ? ' — '+tr('hm_possible_deadline','possível prazo!') : '');
              html += '<div class="' + cls + (deadlines[d3+':'+h3]?' hm-deadline':'') +
                      '" data-tip="' + tip + '">' + label + '</div>';
            }
          }

          var box = $('heatmap');
          if (box) Store.renderHtml(box, html);

          /* ── tooltip on hover ── */
          if (box) {
            box.onmouseover = function(e) {
              var cell = e.target.closest('[data-tip]');
              var tip  = document.getElementById('heatmapTooltip');
              if (cell && tip) {
                tip.textContent = cell.getAttribute('data-tip');
                tip.style.display = 'block';
              }
            };
            box.onmouseout = function() {
              var tip = document.getElementById('heatmapTooltip');
              if (tip) tip.style.display = 'none';
            };
          }

          /* ── quick KPIs ── */
          var total = Object.values(grid).reduce(function(a,b){return a+b;},0);
          var afterHours = 0;
          for (var d4=0; d4<7; d4++) {
            for (var h4=18; h4<24; h4++) afterHours += (grid[d4+':'+h4]||0);
          }
          var pctAfter = total > 0 ? Math.round(afterHours/total*100) : 0;

          /* Best time slot (highest sum across all days) */
          var hourTotals = Array(24).fill(0);
          for (var d5=0; d5<7; d5++) {
            for (var h5=0; h5<24; h5++) hourTotals[h5] += (grid[d5+':'+h5]||0);
          }
          var bestH = hourTotals.indexOf(Math.max.apply(null, hourTotals));

          /* Best 2-hour window for intervention */
          var best2h = 0, best2hStart = 0;
          for (var h6=0; h6<23; h6++) {
            var sum2 = hourTotals[h6] + hourTotals[h6+1];
            if (sum2 > best2h) { best2h = sum2; best2hStart = h6; }
          }

          var kpiBox = document.getElementById('heatmapKpis');
          if (kpiBox) {
            Store.renderHtml(kpiBox, '<div class="hm-kpi"><div class="hm-kpi-val">' + pctAfter + '%</div>'
              + '<div class="hm-kpi-lbl">'+tr('hm_kpi_after_hours','acessos após 18h')+'</div></div>'
              + '<div class="hm-kpi"><div class="hm-kpi-val">' + bestH + 'h</div>'
              + '<div class="hm-kpi-lbl">'+tr('hm_kpi_peak_hour','horário de pico')+'</div></div>'
              + '<div class="hm-kpi"><div class="hm-kpi-val">' + total + '</div>'
              + '<div class="hm-kpi-lbl">'+tr('hm_kpi_filtered','acessos filtrados')+'</div></div>'
              + '<div class="hm-kpi"><div class="hm-kpi-val">' + state.logs.length + '</div>'
              + '<div class="hm-kpi-lbl">'+tr('hm_kpi_total_logs','total de logs')+'</div></div>');
          }

          /* ── Automatic insights ── */
          var insights = [];
          var peakDay = 0, peakDaySum = 0;
          for (var d6=0; d6<7; d6++) {
            var ds = 0;
            for (var h7=0; h7<24; h7++) ds += (grid[d6+':'+h7]||0);
            if (ds > peakDaySum) { peakDaySum = ds; peakDay = d6; }
          }
          if (total > 0) {
            insights.push('✓ '+tr('hm_peak_insight','Pico: {day} às {hour}h').replace('{day}',DAYS[peakDay]).replace('{hour}',String(bestH)) + DAYS[peakDay] + ' às ' + bestH + 'h</strong>.');
            insights.push('✓ '+tr('hm_after18_insight','{pct}% dos acessos ocorreram após as 18h.').replace('{pct}',String(pctAfter)));
            insights.push('✓ '+tr('hm_besttime_insight','Melhor horário: {start}h–{end}h.').replace('{start}',String(best2hStart)).replace('{end}',String(best2hStart+2)));
            /* Detecta se alunos acessam perto de prazos */
            var deadlineCount = Object.keys(deadlines).length;
            if (deadlineCount > 0) {
              insights.push('⚠️ '+tr('hm_deadline_insight','Detectados {n} possíveis prazos com pico de acesso (células 🔴).').replace('{n}',String(deadlineCount)));
            }
            /* Overall trend */
            var prevTotal = Object.values(weekTrend[0]).reduce(function(a,b){return a+b;},0);
            var currTotal = Object.values(weekTrend[1]).reduce(function(a,b){return a+b;},0);
            if (prevTotal > 0) {
              var chg = Math.round((currTotal-prevTotal)/prevTotal*100);
              insights.push((chg>=0?'▲':'▼')+' '+tr('hm_trend_insight','Tendência semanal: {pct}% em relação à semana anterior.').replace('{pct}',(chg>=0?'+':'')+String(chg)));
            }
          } else {
            insights.push(tr('hm_no_data','Nenhum dado para o filtro selecionado.'));
          }
          var insBox = document.getElementById('heatmapInsights');
          if (insBox) Store.renderHtml(insBox, insights.map(function(i){ return '<p style="margin:0 0 8px;font-size:.85rem;line-height:1.5;">'+i+'</p>'; }).join(''));

          /* ── Grade × time-slot correlation ── */
          var hourGrades = {};
          state.students.forEach(function(s) {
            if (s.grade === null) return;
            /* hora favorita deste aluno */
            var uHours = Array(24).fill(0);
            state.logs.filter(function(l){ return norm(l.nomecompleto) === norm(s.name); })
              .forEach(function(l){ var d=parseDate(l); if(d) uHours[d.getHours()]++; });
            var uBest = uHours.indexOf(Math.max.apply(null,uHours));
            var bucket = uBest<12?tr('hm_morning','Manhã (6h–12h)'):uBest<18?tr('hm_afternoon','Tarde (12h–18h)'):tr('hm_evening','Noite (18h–24h)');
            if (!hourGrades[bucket]) hourGrades[bucket] = [];
            hourGrades[bucket].push(s.grade);
          });
          var gcHTML = '';
          Object.keys(hourGrades).sort().forEach(function(bucket) {
            var grades = hourGrades[bucket];
            var avg = Math.round(grades.reduce(function(a,b){return a+b;},0)/grades.length*10)/10;
            gcHTML += '<div style="display:flex;justify-content:space-between;align-items:center;'
              + 'padding:7px 0;border-bottom:1px solid var(--line);font-size:.82rem;">'
              + '<span>' + bucket + '</span>'
              + '<strong style="color:' + (avg>=70?'var(--green)':avg>=50?'var(--amber)':'var(--red)') + ';">'
              + tr('hm_grade_avg','Média') + ' ' + avg + '</strong></div>';
          });
          var gcBox = document.getElementById('heatmapGradeCorr');
          if (gcBox) Store.renderHtml(gcBox, gcHTML || '<p style="color:var(--muted);font-size:.82rem;">'+tr('hm_insufficient_data','Dados insuficientes.')+'</p>');

          /* ── Best time to intervene ── */
          var btBox = document.getElementById('heatmapBestTime');
          if (btBox && total > 0) {
            var pct2h = total > 0 ? Math.round(best2h/total*100) : 0;
            Store.renderHtml(btBox, '<div style="font-size:2rem;font-weight:900;color:var(--blue);letter-spacing:-.04em;">'
              + best2hStart + 'h–' + (best2hStart+2) + 'h</div>'
              + '<p style="margin:6px 0 0;font-size:.82rem;color:var(--muted);">'
              + '<strong>' + pct2h + '%</strong> '+tr('hm_besttime_desc','dos alunos costumam acessar nesse período.')+'<br>'
              + tr('hm_besttime_tip','Envie mensagens antes desse horário para maior chance de leitura.')+'</p>');
          } else if (btBox) {
            Store.renderHtml(btBox, '<p style="color:var(--muted);font-size:.82rem;">'+tr('hm_no_data_simple','Sem dados.')+'</p>');
          }

          /* ── legenda de cores ── */
          var leg = document.getElementById('heatmapLegend');
          if (leg) {
            Store.renderHtml(leg, '<span class="hm-leg-item hm-leg-0">0</span>'
              + '<span class="hm-leg-item v1">1–5</span>'
              + '<span class="hm-leg-item v2">6–15</span>'
              + '<span class="hm-leg-item v3">16–30</span>'
              + '<span class="hm-leg-item v4">31–60</span>'
              + '<span class="hm-leg-item v5">60+</span>');
          }
        }

        function renderReport(){var el=$('reportBody');if(!el)return;var risk=state.students.filter(function(s){return s.score<40}).length;var medium=state.students.filter(function(s){return s.score>=40&&s.score<70}).length;var high=state.students.filter(function(s){return s.score>=70}).length;Store.renderHtml(el, '<p><strong>'+tr('total_students','Total students')+':</strong> '+state.students.length+'</p><p><strong>'+tr('total_interactions','Total interactions')+':</strong> '+state.logs.length+'</p><p><strong>'+tr('activities_detected','Activities detected')+':</strong> '+state.activities.length+'</p><p><strong>'+tr('engagement_groups','Engagement groups')+':</strong> '+risk+' '+tr('low','low')+', '+medium+' '+tr('average','average')+', '+high+' '+tr('high','high')+'.</p><p class=\"muted\">'+tr('report_note','This report uses stable Moodle data fields.')+'</p>');}
        function renderAll(){buildModel();dashboard.state=state;Store.setDashboard(dashboard);renderKPIs();if(Modules.ActionCenter){Modules.ActionCenter.renderEngagement();Modules.ActionCenter.render();}if(Engagement){Engagement.render();}if(Content){Content.render();}if(Personalised){Personalised.render();}if(Chat){Chat.render();}renderChart();renderTables();renderAlerts();renderHeatmap();renderReport();if(Modules.ClassList)Modules.ClassList.render();if(Modules.Profile)Modules.Profile.render();if(Modules.Activities)Modules.Activities.render();if(Modules.Grades)Modules.Grades.render();if(Modules.Interventions)Modules.Interventions.render();$('loading').style.display='none'}
        function esc(v){return String(v===undefined||v===null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]})}
        function showPage(p){document.querySelectorAll('.page').forEach(function(x){x.classList.remove('active')});var page=$('page-'+p);if(page)page.classList.add('active');document.querySelectorAll('.nav-item').forEach(function(x){x.classList.toggle('active',x.getAttribute('data-page')===p)});if(p==='alerts'&&Modules.Alerts&&state.logs.length){Modules.Alerts.reset();Modules.Alerts.render();}if(p==='classlist'&&Modules.ClassList&&state.logs.length){Modules.ClassList.render();}if(p==='studentprofile'&&Modules.Profile&&state.logs.length){Modules.Profile.render();}if(p==='activities'&&Modules.Activities&&state.logs.length){Modules.Activities.render();}if(p==='grades'&&Modules.Grades){Modules.Grades.render();}if(p==='interventions'&&Modules.Interventions){Modules.Interventions.render();}if(p==='engagement'&&state.logs.length&&Engagement){Engagement.render();}if(p==='personalised'&&state.logs.length&&Personalised){Personalised.render();}if(p==='chat'&&Chat){Chat.render();}if(p==='contentanalysis'&&Content){var ctEl=document.getElementById('prescriptiveContent');renderInlineStatus(ctEl,'⏳ '+tr('ct_updated','Updating...'));var _cfg=typeof Store!=='undefined'&&Store.getConfig?Store.getConfig():{};var _cid=parseInt(_cfg.courseid||0,10);if(!_cid){var _m=(window.location.search||'').match(/[?&]id=(\d+)/);if(_m)_cid=parseInt(_m[1],10);}if(_cid&&typeof window.MWAReloadData==='function'){window.MWAReloadData().then(function(){Content.render();}).catch(function(){Content.render();});}else{Content.render();}var periodBar=document.getElementById('contentPeriodBar');if(periodBar&&!periodBar.dataset.ctInit){periodBar.dataset.ctInit='1';periodBar.addEventListener('click',function(ev){var btn=ev.target.closest('[data-ct-period]');if(btn)Content.setPeriod(Number(btn.getAttribute('data-ct-period')),btn);});}}if(p==='heatmap'&&state.logs.length){renderHeatmap();['heatmapStudentSel','heatmapModSel','heatmapModeSel'].forEach(function(id){var el=document.getElementById(id);if(el&&!el.dataset.hmInit){el.dataset.hmInit='1';el.addEventListener('change',renderHeatmap);}});}}
        function markSeen(){state.seen=true;alert(tr('actions_marked_seen','Actions marked as seen.'));}
        Store.setHandler('showPage',showPage);
        Store.setHandler('markSeen',markSeen);
        $('todayLabel').textContent=new Date().toLocaleDateString((config.language&&config.language.indexOf('pt')===0)?'pt-BR':'en-US',{weekday:'long',year:'numeric',month:'long',day:'numeric'});$('search').addEventListener('input',renderTables);
        function receiveData(data){try{state.logs=JSON.parse(data.logs||'[]');state.grades=JSON.parse(data.grades||'[]')}catch(err){$('loadStatus').textContent=tr('could_not_parse','Could not parse Moodle data.');return}renderAll()}function receiveError(message){$('loadStatus').textContent=message||tr('data_load_failed','Data load failed.')}dashboard.receiveData=receiveData;dashboard.receiveError=receiveError;Store.setDashboard(dashboard);
        Store.setHandler('showPage', showPage);
        Store.setHandler('markSeen', markSeen);
        Store.setHandler('toggleSelectPriority', function() { if (Modules.ActionCenter) { Modules.ActionCenter.toggleSelectPriority(); } });
        Store.setHandler('openBulkEmail', function() { if (Modules.ActionCenter) { Modules.ActionCenter.openBulkEmail(); } });
        Store.bindDomEvents(document.getElementById('block-mwa-dashboard-app'));
        return dashboard;
    };

    return {
        init: init
    };
});
