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

_logger = logging.getLogger(__name__)

AUTO_SCORABLE_TYPES = {'mcq', 'mcq_multi', 'gap_fill', 'tfng', 'ynng', 'numerical',
                       'short_answer', 'form_completion', 'note_completion',
                       'matching', 'summary_completion', 'heading_matching',
                       'matching_features', 'map_labelling'}

SUBJECTIVE_SKILLS = {'writing', 'speaking'}

BAND_TO_CEFR = [
    (8.5, 'c2'), (7.0, 'c1'), (5.5, 'b2'),
    (4.5, 'b1'), (3.5, 'a2'), (2.5, 'a1'), (0.0, 'pre_a1'),
]


def _band_to_cefr(band):
    for threshold, level in BAND_TO_CEFR:
        if band >= threshold:
            return level
    return 'pre_a1'


def _question_to_student_dict(q):
    return {
        'id': q.id,
        'skill': q.skill or '',
        'question_type': q.question_type or '',
        'stem': q.stem or '',
        'options': json.loads(q.options) if q.options else [],
        'marks': q.marks,
        'difficulty': q.difficulty or '',
        'source_type': q.source_type or '',
        'source_id': q.source_id or 0,
    }


def _score_objective(question, student_answer):
    """Compare student answer to correct_answer for objective question types."""
    correct = (question.correct_answer or '').strip().lower()
    given = (student_answer or '').strip().lower()
    if not correct:
        return False, 0.0
    if question.question_type in ('mcq_multi', 'matching', 'matching_features'):
        try:
            correct_set = set(json.loads(question.correct_answer))
            given_set = set(json.loads(student_answer)) if student_answer else set()
            correct_set = {str(v).strip().lower() for v in correct_set}
            given_set = {str(v).strip().lower() for v in given_set}
            is_correct = correct_set == given_set
        except (json.JSONDecodeError, TypeError):
            is_correct = correct == given
    else:
        is_correct = correct == given
    return is_correct, question.marks if is_correct else 0.0


def _compute_bands(attempt, Answer, Score):
    """Compute per-skill band scores and overall band for an attempt."""
    skill_totals = {}
    skill_max = {}

    for ans in Answer.search([('attempt_id', '=', attempt.id)]):
        q = ans.question_id
        skill = q.skill or 'general'
        skill_totals.setdefault(skill, 0.0)
        skill_max.setdefault(skill, 0.0)
        skill_totals[skill] += ans.score or 0.0
        skill_max[skill] += q.marks or 1.0

    band_map = {}
    for skill, total in skill_totals.items():
        mx = skill_max.get(skill, 1.0) or 1.0
        raw_pct = total / mx
        band = round(raw_pct * 9.0 * 2) / 2  # round to nearest 0.5
        band = min(band, 9.0)
        band_map[skill] = band

        existing = Score.search([
            ('attempt_id', '=', attempt.id), ('skill', '=', skill),
        ], limit=1)
        score_vals = {
            'band_score': band,
            'raw_score': total,
            'max_score': mx,
            'cefr_level': _band_to_cefr(band),
            'entity_id': attempt.entity_id.id if attempt.entity_id else False,
        }
        if existing:
            existing.write(score_vals)
        else:
            score_vals.update({
                'attempt_id': attempt.id,
                'skill': skill,
            })
            Score.create(score_vals)

    skill_field_map = {
        'listening': 'listening_band',
        'reading': 'reading_band',
        'writing': 'writing_band',
        'speaking': 'speaking_band',
    }
    write_vals = {}
    for skill, field in skill_field_map.items():
        if skill in band_map:
            write_vals[field] = band_map[skill]

    counted = [b for s, b in band_map.items() if s in skill_field_map]
    if counted:
        overall = round(sum(counted) / len(counted) * 2) / 2
        overall = min(overall, 9.0)
    else:
        overall = 0.0

    write_vals['overall_band'] = overall
    write_vals['cefr_level'] = _band_to_cefr(overall)

    existing_overall = Score.search([
        ('attempt_id', '=', attempt.id), ('skill', '=', 'overall'),
    ], limit=1)
    overall_vals = {
        'band_score': overall,
        'raw_score': sum(skill_totals.values()),
        'max_score': sum(skill_max.values()),
        'cefr_level': _band_to_cefr(overall),
        'entity_id': attempt.entity_id.id if attempt.entity_id else False,
    }
    if existing_overall:
        existing_overall.write(overall_vals)
    else:
        overall_vals.update({'attempt_id': attempt.id, 'skill': 'overall'})
        Score.create(overall_vals)

    attempt.write(write_vals)
    return band_map, overall


class EncoachExamSessionController(http.Controller):

    # ------------------------------------------------------------------
    # GET /api/exam/<int:exam_id>/session
    # ------------------------------------------------------------------
    @http.route('/api/exam/<int:exam_id>/session', type='http', auth='none',
                methods=['GET'], csrf=False)
    @jwt_required
    def get_session(self, exam_id, **kw):
        try:
            Exam = request.env['encoach.exam.custom'].sudo()
            exam = Exam.browse(exam_id)
            if not exam.exists():
                return _error_response('Exam not found', 404)

            uid = request.env.user.id
            Attempt = request.env['encoach.student.attempt'].sudo()
            attempt = Attempt.search([
                ('student_id', '=', uid),
                ('exam_id', '=', exam.id),
                ('status', 'in', ['in_progress', 'scoring']),
            ], limit=1, order='id desc')

            if not attempt:
                attempt = Attempt.create({
                    'student_id': uid,
                    'exam_id': exam.id,
                    'status': 'in_progress',
                    'entity_id': exam.entity_id.id if exam.entity_id else False,
                })

            Answer = request.env['encoach.student.answer'].sudo()
            saved_answers = {}
            for ans in Answer.search([('attempt_id', '=', attempt.id)]):
                saved_answers[ans.question_id.id] = ans.answer or ''

            sections = []
            for sec in exam.section_ids.sorted('sequence'):
                questions = []
                for q in sec.question_ids:
                    q_dict = _question_to_student_dict(q)
                    q_dict['saved_answer'] = saved_answers.get(q.id, '')
                    questions.append(q_dict)
                sections.append({
                    'id': sec.id,
                    'title': sec.title,
                    'skill': sec.skill or '',
                    'time_limit_min': sec.time_limit_min or 0,
                    'scoring_method': sec.scoring_method or 'auto',
                    'sequence': sec.sequence,
                    'questions': questions,
                })

            return _json_response({
                'attempt_id': attempt.id,
                'exam_id': exam.id,
                'exam_title': exam.title,
                'total_time_min': exam.total_time_min or 0,
                'status': attempt.status,
                'started_at': attempt.started_at,
                'sections': sections,
            })

        except Exception as e:
            _logger.exception('get_session failed')
            return _error_response(str(e), 500)

    # ------------------------------------------------------------------
    # POST /api/exam/<int:exam_id>/autosave
    # ------------------------------------------------------------------
    @http.route('/api/exam/<int:exam_id>/autosave', type='http', auth='none',
                methods=['POST'], csrf=False)
    @jwt_required
    def autosave(self, exam_id, **kw):
        try:
            body = _get_json_body()
            attempt_id = body.get('attempt_id')
            if not attempt_id:
                return _error_response('attempt_id is required', 400)

            Attempt = request.env['encoach.student.attempt'].sudo()
            attempt = Attempt.browse(int(attempt_id))
            if not attempt.exists():
                return _error_response('Attempt not found', 404)
            if attempt.student_id.id != request.env.user.id:
                return _error_response('Forbidden', 403)
            if attempt.status != 'in_progress':
                return _error_response('Attempt is not in progress', 400)

            answers = body.get('answers', [])
            Answer = request.env['encoach.student.answer'].sudo()

            for ans_data in answers:
                q_id = ans_data.get('question_id')
                if not q_id:
                    continue
                existing = Answer.search([
                    ('attempt_id', '=', attempt.id),
                    ('question_id', '=', int(q_id)),
                ], limit=1)
                if existing:
                    existing.write({'answer': ans_data.get('answer', '')})
                else:
                    Answer.create({
                        'attempt_id': attempt.id,
                        'question_id': int(q_id),
                        'answer': ans_data.get('answer', ''),
                    })

            from odoo.fields import Datetime as DT
            now = DT.now()
            autosave_meta = json.dumps({
                'current_section': body.get('current_section', ''),
                'time_remaining_sec': body.get('time_remaining_sec', 0),
                'last_autosave': str(now),
            })
            attempt.write({'autosave_data': autosave_meta})

            return _json_response({'saved': True, 'timestamp': str(now)})

        except Exception as e:
            _logger.exception('autosave failed')
            return _error_response(str(e), 500)

    # ------------------------------------------------------------------
    # POST /api/exam/<int:exam_id>/submit
    # ------------------------------------------------------------------
    @http.route('/api/exam/<int:exam_id>/submit', type='http', auth='none',
                methods=['POST'], csrf=False)
    @jwt_required
    def submit(self, exam_id, **kw):
        try:
            body = _get_json_body()
            attempt_id = body.get('attempt_id')
            if not attempt_id:
                return _error_response('attempt_id is required', 400)

            Attempt = request.env['encoach.student.attempt'].sudo()
            attempt = Attempt.browse(int(attempt_id))
            if not attempt.exists():
                return _error_response('Attempt not found', 404)
            if attempt.student_id.id != request.env.user.id:
                return _error_response('Forbidden', 403)
            if attempt.status not in ('in_progress',):
                return _error_response('Attempt cannot be submitted', 400)

            answers = body.get('answers', [])
            Answer = request.env['encoach.student.answer'].sudo()
            Question = request.env['encoach.question'].sudo()
            Score = request.env['encoach.score'].sudo()

            has_subjective = False

            for ans_data in answers:
                q_id = ans_data.get('question_id')
                if not q_id:
                    continue
                q_id = int(q_id)
                q = Question.browse(q_id)
                if not q.exists():
                    continue

                existing = Answer.search([
                    ('attempt_id', '=', attempt.id),
                    ('question_id', '=', q_id),
                ], limit=1)

                student_answer = ans_data.get('answer', '')
                is_correct = False
                score_val = 0.0

                if q.question_type in AUTO_SCORABLE_TYPES and q.skill not in SUBJECTIVE_SKILLS:
                    is_correct, score_val = _score_objective(q, student_answer)
                elif q.skill in SUBJECTIVE_SKILLS:
                    has_subjective = True

                vals = {
                    'answer': student_answer,
                    'is_correct': is_correct,
                    'score': score_val,
                }
                if existing:
                    existing.write(vals)
                else:
                    vals.update({
                        'attempt_id': attempt.id,
                        'question_id': q_id,
                    })
                    Answer.create(vals)

            from odoo.fields import Datetime as DT
            attempt.write({
                'completed_at': DT.now(),
                'status': 'scoring' if has_subjective else 'scored',
            })

            band_map, overall = _compute_bands(attempt, Answer, Score)

            exam = attempt.exam_id
            if not has_subjective:
                release_mode = exam.results_release_mode if exam else 'auto'
                if release_mode == 'auto':
                    attempt.write({'status': 'released', 'released_at': DT.now()})
                elif release_mode == 'manual_approval':
                    attempt.write({'status': 'pending_approval'})

            result = {
                'attempt_id': attempt.id,
                'status': attempt.status,
            }
            if attempt.status == 'released':
                result['scores'] = {
                    'listening': attempt.listening_band,
                    'reading': attempt.reading_band,
                    'writing': attempt.writing_band,
                    'speaking': attempt.speaking_band,
                    'overall': attempt.overall_band,
                    'cefr': attempt.cefr_level or '',
                }

            return _json_response(result)

        except Exception as e:
            _logger.exception('submit failed')
            return _error_response(str(e), 500)

    # ------------------------------------------------------------------
    # GET /api/exam/<int:exam_id>/results
    # ------------------------------------------------------------------
    @http.route('/api/exam/<int:exam_id>/results', type='http', auth='none',
                methods=['GET'], csrf=False)
    @jwt_required
    def get_results(self, exam_id, **kw):
        try:
            uid = request.env.user.id
            Attempt = request.env['encoach.student.attempt'].sudo()
            attempt = Attempt.search([
                ('student_id', '=', uid),
                ('exam_id', '=', int(exam_id)),
                ('status', '=', 'released'),
            ], limit=1, order='id desc')

            if not attempt:
                return _error_response('Results not yet released', 403)

            scores = []
            for s in attempt.score_ids:
                scores.append({
                    'skill': s.skill,
                    'band_score': s.band_score,
                    'raw_score': s.raw_score,
                    'max_score': s.max_score,
                    'cefr_level': s.cefr_level or '',
                })

            feedback_list = []
            for fb in attempt.feedback_ids:
                feedback_list.append({
                    'question_id': fb.question_id.id if fb.question_id else None,
                    'feedback_text': fb.feedback_text or '',
                    'source': fb.source,
                })

            return _json_response({
                'attempt_id': attempt.id,
                'exam_id': attempt.exam_id.id if attempt.exam_id else None,
                'status': attempt.status,
                'completed_at': attempt.completed_at,
                'released_at': attempt.released_at,
                'listening_band': attempt.listening_band,
                'reading_band': attempt.reading_band,
                'writing_band': attempt.writing_band,
                'speaking_band': attempt.speaking_band,
                'overall_band': attempt.overall_band,
                'cefr_level': attempt.cefr_level or '',
                'scores': scores,
                'feedback': feedback_list,
            })

        except Exception as e:
            _logger.exception('get_results failed')
            return _error_response(str(e), 500)
