"""Reports API.

Serves the admin "Reports" section in the sidebar:
  * Student Performance  -> /api/reports/student-performance
  * Stats Corporate      -> /api/reports/stats-corporate
  * Record               -> /api/reports/record

All endpoints aggregate from ``encoach.student.attempt`` (exam attempts with
per-skill band scores and CEFR level) and its surrounding records
(``res.users`` for student names, ``encoach.entity`` for corporate context,
``encoach.exam.custom`` for exam metadata).

A single attempt is "reportable" when it has a completed/scored/released
status AND at least one non-null band. Everything else is ignored so the
Reports pages never surface misleading zeros for in-progress work.
"""
import logging
from collections import defaultdict
from datetime import datetime, timedelta

from odoo import http
from odoo.http import request
from odoo.addons.encoach_api.controllers.base import (
    jwt_required, _json_response, _error_response, _paginate,
)

_logger = logging.getLogger(__name__)

REPORTABLE_STATUSES = ('completed', 'scoring', 'scored', 'released')

# CEFR ordering + band ranges used when an attempt didn't store cefr_level.
_CEFR_BY_BAND = [
    (9.0, 'C2'), (7.0, 'C1'), (5.5, 'B2'),
    (4.0, 'B1'), (3.0, 'A2'), (2.0, 'A1'), (0.0, 'Pre-A1'),
]


def _cefr_for_band(band):
    if band is None:
        return None
    for threshold, label in _CEFR_BY_BAND:
        if band >= threshold:
            return label
    return 'Pre-A1'


def _normalize_cefr(label):
    if not label:
        return None
    s = str(label).strip().lower().replace('-', '_')
    return {
        'pre_a1': 'Pre-A1', 'a1': 'A1', 'a2': 'A2',
        'b1': 'B1', 'b2': 'B2', 'c1': 'C1', 'c2': 'C2',
    }.get(s, label)


def _duration_minutes(att):
    start = att.started_at
    end = None
    if hasattr(att, 'completed_at'):
        end = att.completed_at
    if not end and hasattr(att, 'finished_at'):
        end = att.finished_at
    if not start or not end:
        return None
    return int(max(0, (end - start).total_seconds() // 60))


def _attempt_completed_at(att):
    if hasattr(att, 'completed_at') and att.completed_at:
        return att.completed_at
    if hasattr(att, 'finished_at') and att.finished_at:
        return att.finished_at
    return None


def _build_attempt_domain(kw, reportable=True):
    """Common filter reader used by all three endpoints."""
    domain = []
    if reportable:
        domain.append(('status', 'in', list(REPORTABLE_STATUSES)))
    entity_id = kw.get('entity_id')
    if entity_id:
        try:
            domain.append(('entity_id', '=', int(entity_id)))
        except (TypeError, ValueError):
            pass
    user_id = kw.get('user_id') or kw.get('student_id')
    if user_id:
        try:
            domain.append(('student_id', '=', int(user_id)))
        except (TypeError, ValueError):
            pass
    since = kw.get('since')
    if since:
        try:
            domain.append(('started_at', '>=', since))
        except Exception:
            pass
    period = (kw.get('period') or '').lower()
    if period in ('day', 'week', 'month'):
        delta = {'day': 1, 'week': 7, 'month': 30}[period]
        cutoff = datetime.now() - timedelta(days=delta)
        domain.append(('started_at', '>=', cutoff))
    return domain


class ReportsController(http.Controller):

    # ── 1. Student Performance ───────────────────────────────────────

    @http.route('/api/reports/student-performance', type='http',
                auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def student_performance(self, **kw):
        """Return one row per student with averaged bands per skill.

        Query params: ``entity_id``, ``level`` (CEFR code, case-insensitive),
        ``search`` (matches student name/login), ``since`` (ISO date).
        """
        try:
            Att = request.env['encoach.student.attempt'].sudo()
            domain = _build_attempt_domain(kw)
            attempts = Att.search(domain, order='started_at desc')

            per_student = defaultdict(lambda: {
                'attempts': [],
                'reading_sum': 0.0, 'reading_n': 0,
                'listening_sum': 0.0, 'listening_n': 0,
                'writing_sum': 0.0, 'writing_n': 0,
                'speaking_sum': 0.0, 'speaking_n': 0,
                'overall_sum': 0.0, 'overall_n': 0,
                'last_at': None, 'last_cefr': None,
                'entity_id': None, 'entity_name': None,
            })

            def _accum(agg, key, value):
                if value is not None and value > 0:
                    agg[f'{key}_sum'] += float(value)
                    agg[f'{key}_n'] += 1

            for att in attempts:
                if not att.student_id:
                    continue
                agg = per_student[att.student_id.id]
                agg['attempts'].append(att.id)
                _accum(agg, 'reading', att.reading_band)
                _accum(agg, 'listening', att.listening_band)
                _accum(agg, 'writing', att.writing_band)
                _accum(agg, 'speaking', att.speaking_band)
                _accum(agg, 'overall', att.overall_band)
                ct = _attempt_completed_at(att) or att.started_at
                if ct and (agg['last_at'] is None or ct > agg['last_at']):
                    agg['last_at'] = ct
                    agg['last_cefr'] = _normalize_cefr(att.cefr_level) \
                                       or _cefr_for_band(att.overall_band)
                if att.entity_id and not agg['entity_id']:
                    agg['entity_id'] = att.entity_id.id
                    agg['entity_name'] = att.entity_id.name

            search_lc = (kw.get('search') or '').strip().lower()
            level_filter = (kw.get('level') or '').strip().upper().replace('-', '_')

            rows = []
            for student_id, agg in per_student.items():
                user = request.env['res.users'].sudo().browse(student_id)
                if not user.exists():
                    continue
                name = user.name or user.login or f'User #{student_id}'
                if search_lc and search_lc not in (name or '').lower() \
                        and search_lc not in (user.login or '').lower():
                    continue

                def _avg(key):
                    n = agg[f'{key}_n']
                    if not n:
                        return None
                    return round(agg[f'{key}_sum'] / n, 2)

                overall = _avg('overall')
                if overall is None:
                    skill_avgs = [v for v in (
                        _avg('reading'), _avg('listening'),
                        _avg('writing'), _avg('speaking'),
                    ) if v is not None]
                    overall = round(sum(skill_avgs) / len(skill_avgs), 2) \
                        if skill_avgs else None

                level = agg['last_cefr'] or _cefr_for_band(overall)
                if level_filter and (level or '').upper().replace('-', '_') \
                        != level_filter:
                    continue

                rows.append({
                    'student_id': student_id,
                    'student_name': name,
                    'login': user.login or '',
                    'entity_id': agg['entity_id'],
                    'entity_name': agg['entity_name'] or '',
                    'reading': _avg('reading'),
                    'listening': _avg('listening'),
                    'writing': _avg('writing'),
                    'speaking': _avg('speaking'),
                    'overall': overall,
                    'level': level,
                    'attempts_count': len(agg['attempts']),
                    'last_attempt_at': agg['last_at'].isoformat()
                                         if agg['last_at'] else None,
                })

            rows.sort(key=lambda r: (-(r['overall'] or 0.0),
                                     r['student_name'].lower()))

            return _json_response({
                'items': rows,
                'total': len(rows),
            })
        except Exception as e:
            _logger.exception('student-performance report failed')
            return _error_response(str(e), 500)

    # ── 2. Stats Corporate ───────────────────────────────────────────

    @http.route('/api/reports/stats-corporate', type='http', auth='public',
                methods=['GET'], csrf=False)
    @jwt_required
    def stats_corporate(self, **kw):
        """Corporate rollups: avg by module, monthly trend, level distribution,
        entity comparison.

        Query params: ``entity_id``, ``threshold`` (0..100, minimum band*10 to
        include), ``months`` (trend horizon; default 6).
        """
        try:
            Att = request.env['encoach.student.attempt'].sudo()
            domain = _build_attempt_domain(kw)

            try:
                threshold = float(kw.get('threshold') or 0)
            except (TypeError, ValueError):
                threshold = 0.0
            if threshold > 0:
                domain.append(('overall_band', '>=', threshold / 10.0))

            attempts = Att.search(domain)

            skill_totals = {k: [0.0, 0] for k in
                            ('reading', 'listening', 'writing', 'speaking')}
            for att in attempts:
                for skill, field in (('reading', att.reading_band),
                                     ('listening', att.listening_band),
                                     ('writing', att.writing_band),
                                     ('speaking', att.speaking_band)):
                    if field is not None and field > 0:
                        skill_totals[skill][0] += field
                        skill_totals[skill][1] += 1

            by_module = []
            for skill in ('Reading', 'Listening', 'Writing', 'Speaking'):
                total, n = skill_totals[skill.lower()]
                avg = round((total / n) * 10, 1) if n else 0
                by_module.append({'module': skill, 'score': avg, 'n': n})

            try:
                months = max(1, min(24, int(kw.get('months') or 6)))
            except (TypeError, ValueError):
                months = 6

            month_buckets = defaultdict(lambda: [0.0, 0])
            for att in attempts:
                ct = _attempt_completed_at(att) or att.started_at
                if not ct or att.overall_band is None:
                    continue
                key = ct.strftime('%Y-%m')
                month_buckets[key][0] += att.overall_band
                month_buckets[key][1] += 1

            trend = []
            today = datetime.now().replace(day=1)
            for i in range(months - 1, -1, -1):
                ref = (today - timedelta(days=31 * i)).replace(day=1)
                key = ref.strftime('%Y-%m')
                total, n = month_buckets.get(key, (0.0, 0))
                avg = round((total / n) * 10, 1) if n else 0
                trend.append({'month': ref.strftime('%b'), 'avg': avg,
                              'period': key, 'n': n})

            level_buckets = defaultdict(int)
            for att in attempts:
                label = _normalize_cefr(att.cefr_level) \
                        or _cefr_for_band(att.overall_band)
                if label:
                    level_buckets[label] += 1
            palette = {
                'Pre-A1': 'hsl(0, 0%, 55%)',
                'A1': 'hsl(0, 72%, 51%)',
                'A2': 'hsl(38, 92%, 50%)',
                'B1': 'hsl(199, 89%, 48%)',
                'B2': 'hsl(243, 75%, 59%)',
                'C1': 'hsl(142, 71%, 45%)',
                'C2': 'hsl(280, 65%, 50%)',
            }
            distribution = [
                {'name': lvl, 'value': level_buckets.get(lvl, 0),
                 'color': palette[lvl]}
                for lvl in ('A1', 'A2', 'B1', 'B2', 'C1', 'C2')
                if level_buckets.get(lvl)
            ] or [
                {'name': lvl, 'value': 0, 'color': palette[lvl]}
                for lvl in ('A1', 'A2', 'B1', 'B2', 'C1', 'C2')
            ]

            entity_buckets = defaultdict(lambda: [0.0, 0, 'Unassigned'])
            for att in attempts:
                if att.overall_band is None:
                    continue
                eid = att.entity_id.id or 0
                entity_buckets[eid][0] += att.overall_band
                entity_buckets[eid][1] += 1
                entity_buckets[eid][2] = att.entity_id.name or 'Unassigned'
            comparison = []
            for eid, (total, n, name) in entity_buckets.items():
                comparison.append({
                    'entity_id': eid or None,
                    'entity_name': name,
                    'avg': round((total / n) * 10, 1) if n else 0,
                    'attempts': n,
                })
            comparison.sort(key=lambda r: -r['avg'])

            return _json_response({
                'by_module': by_module,
                'trend': trend,
                'distribution': distribution,
                'comparison': comparison,
                'meta': {
                    'attempts_considered': len(attempts),
                    'threshold': threshold,
                    'months': months,
                },
            })
        except Exception as e:
            _logger.exception('stats-corporate report failed')
            return _error_response(str(e), 500)

    # ── 3. Record ────────────────────────────────────────────────────

    @http.route('/api/reports/record', type='http', auth='public',
                methods=['GET'], csrf=False)
    @jwt_required
    def record(self, **kw):
        """Per-user attempt history.

        Query params: ``user_id`` (required for focused view; omit to see all),
        ``entity_id``, ``period`` (day|week|month), ``status``, pagination.
        """
        try:
            Att = request.env['encoach.student.attempt'].sudo()
            domain = _build_attempt_domain(kw, reportable=False)
            if kw.get('status'):
                domain.append(('status', '=', kw['status']))

            offset, limit, page = _paginate(kw)
            total = Att.search_count(domain)
            attempts = Att.search(domain, offset=offset, limit=limit,
                                  order='started_at desc')

            items = []
            for att in attempts:
                exam = att.exam_id
                exam_title = ''
                if exam:
                    # encoach.exam.custom labels its record with `title`,
                    # not `name`. Fall back to display_name if the field
                    # is ever renamed upstream.
                    exam_title = getattr(exam, 'title', None) \
                                  or getattr(exam, 'display_name', '') or ''
                assignment = request.env['encoach.exam.assignment'].sudo() \
                    .search([('exam_id', '=', exam.id),
                             ('student_id', '=', att.student_id.id)],
                            limit=1) if exam and att.student_id else None
                duration_min = _duration_minutes(att)
                ct = _attempt_completed_at(att) or att.started_at
                items.append({
                    'id': att.id,
                    'student_id': att.student_id.id or None,
                    'student_name': att.student_id.name if att.student_id else '',
                    'assignment': exam_title,
                    'assignment_id': assignment.id if assignment else None,
                    'exam': exam_title,
                    'exam_code': f'EX-{exam.id:03d}' if exam else '',
                    'exam_id': exam.id or None,
                    'entity_id': att.entity_id.id or None,
                    'entity_name': att.entity_id.name if att.entity_id else '',
                    'date': ct.strftime('%Y-%m-%d') if ct else None,
                    'started_at': att.started_at.isoformat()
                                    if att.started_at else None,
                    'completed_at': (_attempt_completed_at(att).isoformat()
                                     if _attempt_completed_at(att) else None),
                    'score': att.overall_band if att.overall_band else None,
                    'duration_min': duration_min,
                    'duration': f'{duration_min} min' if duration_min is not None
                                  else '—',
                    'status': att.status or 'in_progress',
                    'status_label': (att.status or 'in_progress').replace('_', ' ').title(),
                    'cefr_level': _normalize_cefr(att.cefr_level)
                                    or _cefr_for_band(att.overall_band),
                })

            return _json_response({
                'items': items,
                'total': total,
                'page': page,
                'size': limit,
            })
        except Exception as e:
            _logger.exception('record report failed')
            return _error_response(str(e), 500)

    # ── Shared picker: entities and students for report filters ──────

    @http.route('/api/reports/filters', type='http', auth='public',
                methods=['GET'], csrf=False)
    @jwt_required
    def filters(self, **kw):
        """Lightweight list of entities + students/users we have attempts for.
        Used by the filter dropdowns on all three Reports pages."""
        try:
            Ent = request.env['encoach.entity'].sudo()
            entities = [{'id': e.id, 'name': e.name or ''}
                        for e in Ent.search([], order='name')]

            Att = request.env['encoach.student.attempt'].sudo()
            att_students = Att.search([]).mapped('student_id')
            students = [{'id': u.id, 'name': u.name or u.login or '',
                         'login': u.login or ''}
                        for u in att_students.sorted('name')
                        if u and u.id > 0]

            return _json_response({
                'entities': entities,
                'students': students,
            })
        except Exception as e:
            _logger.exception('reports filters failed')
            return _error_response(str(e), 500)
