import json
import logging
import math
import base64

from odoo import http, fields
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__)

LEARNING_RATE = 0.3
SEM_THRESHOLD = 0.3
MAX_QUESTIONS = 40

THETA_CEFR = [
    (-3.0, 'pre_a1'), (-2.0, 'a1'), (-1.0, 'a2'),
    (0.0, 'b1'), (1.0, 'b2'), (2.0, 'c1'), (3.0, 'c2'),
]


def _theta_to_cefr(theta):
    for boundary, level in THETA_CEFR:
        if theta <= boundary:
            return level
    return 'c2'


def _irt_probability(theta, a, b, c):
    """3PL IRT probability."""
    exp_val = -a * (theta - b)
    exp_val = max(min(exp_val, 500), -500)
    return c + (1.0 - c) / (1.0 + math.exp(exp_val))


def _fisher_info(theta, a, b, c):
    """Fisher information for a 3PL item."""
    p = _irt_probability(theta, a, b, c)
    q = 1.0 - p
    if (1.0 - c) == 0 or p == 0:
        return 0.0
    numerator = (a ** 2) * ((p - c) ** 2) * q
    denominator = ((1.0 - c) ** 2) * p
    return numerator / denominator if denominator else 0.0


def _select_next(theta, available_qs, answered_ids):
    """Pick the item with max Fisher information from unanswered pool."""
    best, best_info = None, -1.0
    for q in available_qs:
        if q['id'] in answered_ids:
            continue
        info = _fisher_info(theta, q['irt_a'], q['irt_b'], q['irt_c'])
        if info > best_info:
            best_info = info
            best = q
    return best


def _question_to_dict(rec):
    options = None
    if rec.options:
        try:
            options = json.loads(rec.options)
        except (json.JSONDecodeError, TypeError):
            options = rec.options
    return {
        'id': rec.id,
        'stem': rec.stem,
        'options': options,
        'question_type': rec.question_type,
        'skill': rec.skill,
    }


class EncoachPlacementController(http.Controller):

    # ------------------------------------------------------------------
    # POST /api/placement/start
    # ------------------------------------------------------------------
    @http.route('/api/placement/start', type='http', auth='none',
                methods=['POST'], csrf=False)
    @jwt_required
    def start(self, **kw):
        try:
            user = request.env.user

            active_session = request.env['encoach.cat.session'].sudo().search([
                ('student_id', '=', user.id),
                ('status', '=', 'active'),
            ], limit=1)
            if active_session:
                active_session.write({'status': 'abandoned'})

            session = request.env['encoach.cat.session'].sudo().create({
                'student_id': user.id,
                'status': 'active',
                'current_theta': 0.0,
                'current_sem': 1.0,
                'questions_answered': 0,
            })

            questions = request.env['encoach.question'].sudo().search(
                [('status', '=', 'active')])
            q_dicts = [{
                'id': q.id, 'irt_a': q.irt_a,
                'irt_b': q.irt_b, 'irt_c': q.irt_c,
            } for q in questions]

            first = _select_next(0.0, q_dicts, set())
            first_question = None
            if first:
                rec = request.env['encoach.question'].sudo().browse(first['id'])
                first_question = _question_to_dict(rec)

            return _json_response({
                'session_id': session.id,
                'first_question': first_question,
            })

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

    # ------------------------------------------------------------------
    # POST /api/placement/answer
    # ------------------------------------------------------------------
    @http.route('/api/placement/answer', type='http', auth='none',
                methods=['POST'], csrf=False)
    @jwt_required
    def answer(self, **kw):
        try:
            body = _get_json_body()
            session_id = body.get('session_id')
            question_id = body.get('question_id')
            answer = body.get('answer')

            if not session_id or not question_id:
                return _error_response('session_id and question_id are required', 400)

            session = request.env['encoach.cat.session'].sudo().browse(int(session_id))
            if not session.exists() or session.status != 'active':
                return _error_response('Invalid or inactive session', 400)

            question = request.env['encoach.question'].sudo().browse(int(question_id))
            if not question.exists():
                return _error_response('Question not found', 404)

            correct_answer = (question.correct_answer or '').strip()
            given_answer = str(answer or '').strip()
            correct = given_answer.lower() == correct_answer.lower()

            theta = session.current_theta
            a, b, c = question.irt_a, question.irt_b, question.irt_c
            p = _irt_probability(theta, a, b, c)
            theta += LEARNING_RATE * ((1.0 if correct else 0.0) - p)
            theta = max(-4.0, min(4.0, theta))

            answered_ids = set()
            autosave = session.autosave_data
            if autosave:
                try:
                    saved = json.loads(autosave)
                    answered_ids = set(saved.get('answered_ids', []))
                except (json.JSONDecodeError, TypeError):
                    pass
            answered_ids.add(question_id)

            all_qs = request.env['encoach.question'].sudo().search(
                [('status', '=', 'active')])
            total_info = 0.0
            for q in all_qs:
                if q.id in answered_ids:
                    total_info += _fisher_info(theta, q.irt_a, q.irt_b, q.irt_c)
            sem = 1.0 / math.sqrt(total_info) if total_info > 0 else 1.0

            questions_answered = session.questions_answered + 1
            completed = sem < SEM_THRESHOLD or questions_answered >= MAX_QUESTIONS

            write_vals = {
                'current_theta': theta,
                'current_sem': sem,
                'questions_answered': questions_answered,
                'autosave_data': json.dumps({
                    'answered_ids': list(answered_ids),
                }),
            }

            result = {
                'correct': correct,
                'new_theta': round(theta, 4),
                'new_sem': round(sem, 4),
                'completed': completed,
            }

            if completed:
                cefr = _theta_to_cefr(theta)
                write_vals['status'] = 'completed'
                write_vals['completed_at'] = fields.Datetime.now()
                result['cefr_level'] = cefr
                result['next_question'] = None

                Ability = request.env['encoach.student.ability.model'].sudo()
                ability = Ability.search([
                    ('student_id', '=', session.student_id.id),
                    ('skill', '=', question.skill),
                ], limit=1)
                if ability:
                    ability.write({'theta': theta, 'sem': sem,
                                   'last_updated': fields.Datetime.now()})
                else:
                    Ability.create({
                        'student_id': session.student_id.id,
                        'skill': question.skill,
                        'theta': theta,
                        'sem': sem,
                    })
            else:
                q_dicts = [{
                    'id': q.id, 'irt_a': q.irt_a,
                    'irt_b': q.irt_b, 'irt_c': q.irt_c,
                } for q in all_qs]
                nxt = _select_next(theta, q_dicts, answered_ids)
                if nxt:
                    rec = request.env['encoach.question'].sudo().browse(nxt['id'])
                    result['next_question'] = _question_to_dict(rec)
                else:
                    result['next_question'] = None

            session.write(write_vals)
            return _json_response(result)

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

    # ------------------------------------------------------------------
    # POST /api/placement/autosave
    # ------------------------------------------------------------------
    @http.route('/api/placement/autosave', type='http', auth='none',
                methods=['POST'], csrf=False)
    @jwt_required
    def autosave(self, **kw):
        try:
            body = _get_json_body()
            session_id = body.get('session_id')
            autosave_data = body.get('autosave_data')

            if not session_id:
                return _error_response('session_id is required', 400)

            session = request.env['encoach.cat.session'].sudo().browse(int(session_id))
            if not session.exists():
                return _error_response('Session not found', 404)

            session.write({
                'autosave_data': json.dumps(autosave_data) if not isinstance(
                    autosave_data, str) else autosave_data,
            })
            return _json_response({'saved': True})

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

    # ------------------------------------------------------------------
    # POST /api/placement/speaking-upload
    # ------------------------------------------------------------------
    @http.route('/api/placement/speaking-upload', type='http', auth='none',
                methods=['POST'], csrf=False)
    @jwt_required
    def speaking_upload(self, **kw):
        try:
            audio_file = request.httprequest.files.get('audio')
            if not audio_file:
                return _error_response('No audio file provided', 400)

            file_data = audio_file.read()
            attachment = request.env['ir.attachment'].sudo().create({
                'name': audio_file.filename or 'speaking_upload.webm',
                'type': 'binary',
                'datas': base64.b64encode(file_data),
                'res_model': 'encoach.cat.session',
                'mimetype': audio_file.content_type or 'audio/webm',
            })

            return _json_response({
                'upload_id': attachment.id,
                'status': 'processing',
            })

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

    # ------------------------------------------------------------------
    # GET /api/placement/speaking-status
    # ------------------------------------------------------------------
    @http.route('/api/placement/speaking-status', type='http', auth='none',
                methods=['GET'], csrf=False)
    @jwt_required
    def speaking_status(self, **kw):
        try:
            upload_id = kw.get('upload_id')
            if not upload_id:
                return _error_response('upload_id is required', 400)

            attachment = request.env['ir.attachment'].sudo().browse(int(upload_id))
            if not attachment.exists():
                return _error_response('Upload not found', 404)

            return _json_response({
                'status': 'completed',
                'transcription': None,
            })

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

    # ------------------------------------------------------------------
    # GET /api/placement/results
    # ------------------------------------------------------------------
    @http.route('/api/placement/results', type='http', auth='none',
                methods=['GET'], csrf=False)
    @jwt_required
    def results(self, **kw):
        try:
            user = request.env.user
            session = request.env['encoach.cat.session'].sudo().search([
                ('student_id', '=', user.id),
                ('status', '=', 'completed'),
            ], limit=1, order='completed_at desc')

            if not session:
                return _error_response('No completed placement found', 404)

            abilities = request.env['encoach.student.ability.model'].sudo().search([
                ('student_id', '=', user.id),
            ])

            skills = [{
                'skill': ab.skill,
                'theta': round(ab.theta, 4),
                'cefr_level': _theta_to_cefr(ab.theta),
            } for ab in abilities]

            return _json_response({
                'cefr_level': _theta_to_cefr(session.current_theta),
                'skills': skills,
                'placement_date': session.completed_at,
            })

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

    # ------------------------------------------------------------------
    # GET /api/placement/learning-path
    # ------------------------------------------------------------------
    @http.route('/api/placement/learning-path', type='http', auth='none',
                methods=['GET'], csrf=False)
    @jwt_required
    def learning_path(self, **kw):
        try:
            user = request.env.user
            abilities = request.env['encoach.student.ability.model'].sudo().search([
                ('student_id', '=', user.id),
            ])

            gap_areas = []
            for ab in abilities:
                if ab.theta < 0.0:
                    gap_areas.append({
                        'skill': ab.skill,
                        'current_level': _theta_to_cefr(ab.theta),
                        'theta': round(ab.theta, 4),
                    })

            recommended = []
            cefr_map = {}
            for ab in abilities:
                cefr_map[ab.skill] = _theta_to_cefr(ab.theta)

            weakest_skills = sorted(abilities, key=lambda a: a.theta)
            for ab in weakest_skills[:3]:
                recommended.append({
                    'skill': ab.skill,
                    'course_type': 'remedial',
                    'suggested_level': _theta_to_cefr(ab.theta),
                    'priority': 'high' if ab.theta < -1.0 else 'medium',
                })

            return _json_response({
                'recommended_courses': recommended,
                'gap_areas': gap_areas,
            })

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