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__)


def _ser_exam(e):
    return {
        'id': e.id,
        'name': e.name or '',
        'session_id': e.session_id.id if hasattr(e, 'session_id') and e.session_id else 0,
        'subject_id': e.subject_id.id if hasattr(e, 'subject_id') and e.subject_id else 0,
        'subject_name': e.subject_id.name if hasattr(e, 'subject_id') and e.subject_id else '',
        'exam_code': getattr(e, 'exam_code', '') or '',
        'start_time': str(e.start_time) if hasattr(e, 'start_time') and e.start_time else '',
        'end_time': str(e.end_time) if hasattr(e, 'end_time') and e.end_time else '',
        'total_marks': getattr(e, 'total_marks', 0) or 0,
        'min_marks': getattr(e, 'min_marks', 0) or 0,
        'state': getattr(e, 'state', 'draft') or 'draft',
        'responsible_names': [p.name for p in (getattr(e, 'responsible_ids', False) or [])],
        'attendee_count': len(getattr(e, 'attendees_line', False) or []),
    }


def _ser_session(r, *, with_exams=False):
    exams = []
    if with_exams and hasattr(r, 'exam_ids'):
        exams = [_ser_exam(e) for e in r.exam_ids]
    exam_count = len(getattr(r, 'exam_ids', False) or [])
    exam_type = getattr(r, 'exam_type', False)
    return {
        'id': r.id,
        'name': r.name or '',
        'course_id': r.course_id.id if hasattr(r, 'course_id') and r.course_id else 0,
        'course_name': r.course_id.name if hasattr(r, 'course_id') and r.course_id else '',
        'batch_id': r.batch_id.id if hasattr(r, 'batch_id') and r.batch_id else 0,
        'batch_name': r.batch_id.name if hasattr(r, 'batch_id') and r.batch_id else '',
        'exam_code': getattr(r, 'exam_code', '') or '',
        'start_date': str(r.start_date) if hasattr(r, 'start_date') and r.start_date else '',
        'end_date': str(r.end_date) if hasattr(r, 'end_date') and r.end_date else '',
        'exam_type_id': exam_type.id if exam_type else 0,
        'exam_type_name': exam_type.name if exam_type else '',
        'evaluation_type': getattr(r, 'evaluation_type', 'normal') or 'normal',
        'venue': getattr(r, 'venue', '') or '',
        'state': getattr(r, 'state', 'draft') or 'draft',
        'exam_count': exam_count,
        'exams': exams,
    }


def _ser_template(r):
    exam_session = getattr(r, 'exam_session_id', False)
    grade_ids = []
    if hasattr(r, 'grade_ids'):
        grade_ids = r.grade_ids.ids
    return {
        'id': r.id,
        'name': r.name or '',
        'exam_session_id': exam_session.id if exam_session else 0,
        'exam_session_name': exam_session.name if exam_session else '',
        'evaluation_type': getattr(r, 'evaluation_type', 'normal') or 'normal',
        'result_date': str(r.result_date) if hasattr(r, 'result_date') and r.result_date else '',
        'grade_ids': grade_ids,
        'state': getattr(r, 'state', 'draft') or 'draft',
    }


def _ser_marksheet_line(l):
    return {
        'id': l.id,
        'student_id': l.student_id.id if hasattr(l, 'student_id') and l.student_id else 0,
        'student_name': l.student_id.name if hasattr(l, 'student_id') and l.student_id else '',
        'total_marks': getattr(l, 'total_marks', 0) or 0,
        'percentage': getattr(l, 'percentage', 0) or 0,
        'grade': getattr(l, 'grade', '') or '',
        'status': getattr(l, 'status', 'fail') or 'fail',
        'results': [],
    }


def _ser_marksheet(r, *, with_lines=False):
    exam_session = getattr(r, 'exam_session_id', False)
    line_recs = getattr(r, 'marksheet_line', False) or []
    lines = [_ser_marksheet_line(l) for l in line_recs] if with_lines else []
    return {
        'id': r.id,
        'name': r.name or '',
        'exam_session_id': exam_session.id if exam_session else 0,
        'exam_session_name': exam_session.name if exam_session else '',
        'result_template_id': r.result_template_id.id if hasattr(r, 'result_template_id') and r.result_template_id else 0,
        'generated_date': str(r.generated_date) if hasattr(r, 'generated_date') and r.generated_date else '',
        'generated_by_name': r.generated_by.name if hasattr(r, 'generated_by') and r.generated_by else '',
        'state': getattr(r, 'state', 'draft') or 'draft',
        'total_pass': getattr(r, 'total_pass', 0) or 0,
        'total_failed': getattr(r, 'total_failed', 0) or 0,
        'lines': lines,
    }


class InstitutionalExamsController(http.Controller):

    # ── Exam Sessions (op.exam.session) ──────────────────────────────

    @http.route('/api/inst-exam-sessions', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def list_sessions(self, **kw):
        try:
            M = request.env['op.exam.session'].sudo()
            domain = []
            if kw.get('course_id'):
                domain.append(('course_id', '=', int(kw['course_id'])))
            if kw.get('batch_id'):
                domain.append(('batch_id', '=', int(kw['batch_id'])))
            if kw.get('state'):
                domain.append(('state', '=', kw['state']))
            q = (kw.get('q') or '').strip()
            if q:
                domain.append(('name', 'ilike', q))
            offset, limit, page = _paginate(kw)
            total = M.search_count(domain)
            recs = M.search(domain, offset=offset, limit=limit, order='id desc')
            items = [_ser_session(r) for r in recs]
            return _json_response({'items': items, 'data': items, 'total': total, 'page': page, 'size': limit})
        except Exception as e:
            _logger.exception('list_sessions')
            return _error_response(str(e), 500)

    @http.route('/api/inst-exam-sessions/<int:sid>', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def get_session(self, sid, **kw):
        try:
            r = request.env['op.exam.session'].sudo().browse(sid)
            if not r.exists():
                return _error_response('Not found', 404)
            return _json_response(_ser_session(r, with_exams=True))
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/inst-exam-sessions', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def create_session(self, **kw):
        try:
            body = _get_json_body()
            Session = request.env['op.exam.session'].sudo()
            vals = {'name': body.get('name', '')}
            for fk in ('course_id', 'batch_id'):
                if body.get(fk):
                    vals[fk] = int(body[fk])
            if body.get('exam_type_id') and 'exam_type' in Session._fields:
                vals['exam_type'] = int(body['exam_type_id'])
            if body.get('evaluation_type') and 'evaluation_type' in Session._fields:
                vals['evaluation_type'] = body['evaluation_type']
            if body.get('venue') and 'venue' in Session._fields:
                vals['venue'] = body['venue']
            if body.get('exam_code') and 'exam_code' in Session._fields:
                vals['exam_code'] = body['exam_code']
            for dt in ('start_date', 'end_date'):
                if body.get(dt):
                    vals[dt] = body[dt]
            rec = Session.create(vals)
            return _json_response(_ser_session(rec, with_exams=True))
        except Exception as e:
            _logger.exception('create_session')
            return _error_response(str(e), 500)

    @http.route('/api/inst-exam-sessions/<int:sid>', type='http', auth='public', methods=['PATCH', 'PUT'], csrf=False)
    @jwt_required
    def update_session(self, sid, **kw):
        try:
            rec = request.env['op.exam.session'].sudo().browse(sid)
            if not rec.exists():
                return _error_response('Not found', 404)
            body = _get_json_body()
            vals = {}
            for k in ('name', 'start_date', 'end_date', 'venue', 'exam_code', 'evaluation_type'):
                if k in body and k in rec._fields:
                    vals[k] = body[k]
            for fk in ('course_id', 'batch_id'):
                if fk in body and fk in rec._fields:
                    vals[fk] = int(body[fk]) if body[fk] else False
            if 'exam_type_id' in body and 'exam_type' in rec._fields:
                vals['exam_type'] = int(body['exam_type_id']) if body['exam_type_id'] else False
            if vals:
                rec.write(vals)
            return _json_response(_ser_session(rec, with_exams=True))
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/inst-exam-sessions/<int:sid>', type='http', auth='public', methods=['DELETE'], csrf=False)
    @jwt_required
    def delete_session(self, sid, **kw):
        try:
            rec = request.env['op.exam.session'].sudo().browse(sid)
            if rec.exists():
                rec.unlink()
            return _json_response({'success': True})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/inst-exam-sessions/<int:sid>/schedule', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def schedule_session(self, sid, **kw):
        try:
            rec = request.env['op.exam.session'].sudo().browse(sid)
            if not rec.exists():
                return _error_response('Not found', 404)
            rec.write({'state': 'schedule'})
            return _json_response({'id': rec.id, 'state': 'schedule'})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/inst-exam-sessions/<int:sid>/done', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def done_session(self, sid, **kw):
        try:
            rec = request.env['op.exam.session'].sudo().browse(sid)
            if not rec.exists():
                return _error_response('Not found', 404)
            rec.write({'state': 'done'})
            return _json_response({'id': rec.id, 'state': 'done'})
        except Exception as e:
            return _error_response(str(e), 500)

    # ── Exams (op.exam) ──────────────────────────────────────────────

    @http.route('/api/inst-exams', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def create_exam(self, **kw):
        try:
            body = _get_json_body()
            vals = {'name': body.get('name', '')}
            for fk in ('session_id', 'subject_id', 'course_id'):
                if body.get(fk):
                    vals[fk] = int(body[fk])
            if body.get('total_marks'):
                vals['total_marks'] = float(body['total_marks'])
            rec = request.env['op.exam'].sudo().create(vals)
            return _json_response({'id': rec.id, 'name': rec.name})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/inst-exams/<int:eid>', type='http', auth='public', methods=['PATCH', 'PUT'], csrf=False)
    @jwt_required
    def update_exam(self, eid, **kw):
        try:
            rec = request.env['op.exam'].sudo().browse(eid)
            if not rec.exists():
                return _error_response('Not found', 404)
            body = _get_json_body()
            vals = {}
            for k in ('name', 'total_marks'):
                if k in body:
                    vals[k] = body[k]
            if vals:
                rec.write(vals)
            return _json_response({'id': rec.id})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/inst-exams/<int:eid>/attendees', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def add_attendees(self, eid, **kw):
        try:
            body = _get_json_body()
            student_ids = body.get('student_ids', [])
            Att = request.env['op.exam.attendees'].sudo()
            for sid in student_ids:
                Att.create({'exam_id': eid, 'student_id': int(sid)})
            return _json_response({'success': True})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/inst-exams/<int:eid>/marks', type='http', auth='public', methods=['PATCH'], csrf=False)
    @jwt_required
    def enter_marks(self, eid, **kw):
        try:
            body = _get_json_body()
            marks = body.get('marks', [])
            Att = request.env['op.exam.attendees'].sudo()
            for m in marks:
                att = Att.search([('exam_id', '=', eid), ('student_id', '=', int(m['student_id']))], limit=1)
                if att:
                    att.write({'marks': float(m.get('score', 0))})
            return _json_response({'success': True})
        except Exception as e:
            return _error_response(str(e), 500)

    # ── Exam Types ───────────────────────────────────────────────────

    @http.route('/api/exam-types', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def list_exam_types(self, **kw):
        try:
            M = request.env['op.exam.type'].sudo()
            offset, limit, page = _paginate(kw)
            total = M.search_count([])
            recs = M.search([], offset=offset, limit=limit)
            items = [{'id': r.id, 'name': r.name or ''} for r in recs]
            return _json_response({'items': items, 'data': items, 'total': total, 'page': page, 'size': limit})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/exam-types', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def create_exam_type(self, **kw):
        try:
            body = _get_json_body()
            rec = request.env['op.exam.type'].sudo().create({'name': body.get('name', '')})
            return _json_response({'id': rec.id, 'name': rec.name})
        except Exception as e:
            return _error_response(str(e), 500)

    # ── Grade Configurations ─────────────────────────────────────────

    @http.route('/api/grade-config', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def list_grade_config(self, **kw):
        try:
            M = request.env['op.grade.configuration'].sudo()
            offset, limit, page = _paginate(kw)
            total = M.search_count([])
            recs = M.search([], offset=offset, limit=limit)
            items = [{
                'id': r.id,
                'name': r.name or '',
                'min_per': getattr(r, 'min_per', 0),
                'max_per': getattr(r, 'max_per', 0),
                'result': getattr(r, 'result', '') or '',
            } for r in recs]
            return _json_response({'items': items, 'data': items, 'total': total, 'page': page, 'size': limit})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/grade-config', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def create_grade_config(self, **kw):
        try:
            body = _get_json_body()
            vals = {'name': body.get('name', '')}
            for k in ('min_per', 'max_per'):
                if body.get(k) is not None:
                    vals[k] = float(body[k])
            if body.get('result'):
                vals['result'] = body['result']
            rec = request.env['op.grade.configuration'].sudo().create(vals)
            return _json_response({'id': rec.id, 'name': rec.name})
        except Exception as e:
            return _error_response(str(e), 500)

    # ── Result Templates ─────────────────────────────────────────────

    @http.route('/api/result-templates', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def list_result_templates(self, **kw):
        try:
            M = request.env['op.result.template'].sudo()
            domain = []
            if kw.get('exam_session_id'):
                domain.append(('exam_session_id', '=', int(kw['exam_session_id'])))
            offset, limit, page = _paginate(kw)
            total = M.search_count(domain)
            recs = M.search(domain, offset=offset, limit=limit, order='id desc')
            items = [_ser_template(r) for r in recs]
            return _json_response({'items': items, 'data': items, 'total': total, 'page': page, 'size': limit})
        except Exception as e:
            _logger.exception('list_result_templates')
            return _error_response(str(e), 500)

    @http.route('/api/result-templates', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def create_result_template(self, **kw):
        try:
            body = _get_json_body()
            Tpl = request.env['op.result.template'].sudo()
            vals = {'name': body.get('name', '')}
            if body.get('exam_session_id') and 'exam_session_id' in Tpl._fields:
                vals['exam_session_id'] = int(body['exam_session_id'])
            if body.get('evaluation_type') and 'evaluation_type' in Tpl._fields:
                vals['evaluation_type'] = body['evaluation_type']
            if body.get('result_date') and 'result_date' in Tpl._fields:
                vals['result_date'] = body['result_date']
            grade_ids = body.get('grade_ids') or []
            if grade_ids and 'grade_ids' in Tpl._fields:
                vals['grade_ids'] = [(6, 0, [int(g) for g in grade_ids])]
            rec = Tpl.create(vals)
            return _json_response(_ser_template(rec))
        except Exception as e:
            _logger.exception('create_result_template')
            return _error_response(str(e), 500)

    @http.route(['/api/result-templates/<int:tid>/generate',
                 '/api/result-templates/<int:tid>/generate-marksheets'],
                type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def generate_marksheets(self, tid, **kw):
        try:
            rec = request.env['op.result.template'].sudo().browse(tid)
            if not rec.exists():
                return _error_response('Not found', 404)
            # OpenEduCat 19 names the method `generate_result`;
            # older versions used `action_generate_marksheet`.
            if hasattr(rec, 'generate_result'):
                rec.generate_result()
            elif hasattr(rec, 'action_generate_marksheet'):
                rec.action_generate_marksheet()
            else:
                return _error_response(
                    'No marksheet-generation method on op.result.template '
                    '(expected generate_result or action_generate_marksheet).',
                    400,
                )
            return _json_response(_ser_template(rec))
        except Exception as e:
            _logger.exception('generate_marksheets')
            return _error_response(str(e), 500)

    @http.route('/api/result-templates/<int:tid>', type='http', auth='public', methods=['DELETE'], csrf=False)
    @jwt_required
    def delete_result_template(self, tid, **kw):
        try:
            rec = request.env['op.result.template'].sudo().browse(tid)
            if rec.exists():
                # If this template already generated marksheet registers,
                # unlink them first so the FK RESTRICT doesn't block us.
                session = rec.exam_session_id
                if session:
                    regs = request.env['op.marksheet.register'].sudo().search([
                        ('result_template_id', '=', rec.id),
                    ])
                    if regs:
                        # Unlink the lines + registers (CASCADE ok on lines).
                        regs.sudo().unlink()
                rec.unlink()
            return _json_response({'success': True})
        except Exception as e:
            _logger.exception('delete_result_template')
            return _error_response(str(e), 500)

    @http.route('/api/result-templates/<int:tid>', type='http', auth='public', methods=['PATCH', 'PUT'], csrf=False)
    @jwt_required
    def update_result_template(self, tid, **kw):
        try:
            rec = request.env['op.result.template'].sudo().browse(tid)
            if not rec.exists():
                return _error_response('Not found', 404)
            body = _get_json_body()
            vals = {}
            for k in ('name', 'result_date', 'evaluation_type'):
                if k in body and k in rec._fields:
                    vals[k] = body[k]
            if 'grade_ids' in body and 'grade_ids' in rec._fields:
                vals['grade_ids'] = [(6, 0, [int(g) for g in body['grade_ids']])]
            if vals:
                rec.write(vals)
            return _json_response(_ser_template(rec))
        except Exception as e:
            return _error_response(str(e), 500)

    # ── Marksheets ───────────────────────────────────────────────────

    @http.route('/api/marksheets', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def list_marksheets(self, **kw):
        try:
            M = request.env['op.marksheet.register'].sudo()
            domain = []
            if kw.get('exam_session_id'):
                domain.append(('exam_session_id', '=', int(kw['exam_session_id'])))
            if kw.get('state'):
                domain.append(('state', '=', kw['state']))
            offset, limit, page = _paginate(kw)
            total = M.search_count(domain)
            recs = M.search(domain, offset=offset, limit=limit, order='id desc')
            items = [_ser_marksheet(r) for r in recs]
            return _json_response({'items': items, 'data': items, 'total': total, 'page': page, 'size': limit})
        except Exception as e:
            _logger.exception('list_marksheets')
            return _error_response(str(e), 500)

    @http.route('/api/marksheets/<int:mid>', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def get_marksheet(self, mid, **kw):
        try:
            rec = request.env['op.marksheet.register'].sudo().browse(mid)
            if not rec.exists():
                return _error_response('Not found', 404)
            return _json_response(_ser_marksheet(rec, with_lines=True))
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/marksheets/<int:mid>/validate', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def validate_marksheet(self, mid, **kw):
        try:
            rec = request.env['op.marksheet.register'].sudo().browse(mid)
            if not rec.exists():
                return _error_response('Not found', 404)
            if hasattr(rec, 'action_validate'):
                rec.action_validate()
            else:
                rec.write({'state': 'validated'})
            return _json_response(_ser_marksheet(rec, with_lines=True))
        except Exception as e:
            return _error_response(str(e), 500)

    # ── Course Assignments (OpenEduCat op.assignment) ─────────────────

    @http.route('/api/course-assignments', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def list_course_assignments(self, **kw):
        try:
            M = request.env['op.assignment'].sudo()
            domain = []
            if kw.get('course_id'):
                domain.append(('course_id', '=', int(kw['course_id'])))
            if kw.get('batch_id'):
                domain.append(('batch_id', '=', int(kw['batch_id'])))
            if kw.get('state'):
                domain.append(('state', '=', kw['state']))
            offset, limit, page = _paginate(kw)
            total = M.search_count(domain)
            recs = M.search(domain, offset=offset, limit=limit, order='id desc')
            items = [{
                'id': r.id,
                'name': r.name or '',
                'course_id': r.course_id.id if hasattr(r, 'course_id') and r.course_id else 0,
                'course_name': r.course_id.name if hasattr(r, 'course_id') and r.course_id else '',
                'batch_id': r.batch_id.id if hasattr(r, 'batch_id') and r.batch_id else 0,
                'subject_id': r.subject_id.id if hasattr(r, 'subject_id') and r.subject_id else 0,
                'state': getattr(r, 'state', 'draft') or 'draft',
                'issued_date': str(r.issued_date) if hasattr(r, 'issued_date') and r.issued_date else '',
            } for r in recs]
            return _json_response({'items': items, 'data': items, 'total': total, 'page': page, 'size': limit})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/course-assignments', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def create_course_assignment(self, **kw):
        try:
            body = _get_json_body()
            vals = {'name': body.get('name', '')}
            for fk in ('course_id', 'batch_id', 'subject_id'):
                if body.get(fk):
                    vals[fk] = int(body[fk])
            if body.get('issued_date'):
                vals['issued_date'] = body['issued_date']
            rec = request.env['op.assignment'].sudo().create(vals)
            return _json_response({'id': rec.id, 'name': rec.name})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/course-assignments/<int:aid>', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def get_course_assignment(self, aid, **kw):
        try:
            rec = request.env['op.assignment'].sudo().browse(aid)
            if not rec.exists():
                return _error_response('Not found', 404)
            return _json_response({'id': rec.id, 'name': rec.name or '', 'state': getattr(rec, 'state', 'draft')})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/course-assignments/<int:aid>', type='http', auth='public', methods=['PATCH', 'PUT'], csrf=False)
    @jwt_required
    def update_course_assignment(self, aid, **kw):
        try:
            rec = request.env['op.assignment'].sudo().browse(aid)
            if not rec.exists():
                return _error_response('Not found', 404)
            body = _get_json_body()
            vals = {}
            if 'name' in body:
                vals['name'] = body['name']
            if vals:
                rec.write(vals)
            return _json_response({'id': rec.id})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/course-assignments/<int:aid>/publish', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def publish_assignment(self, aid, **kw):
        try:
            rec = request.env['op.assignment'].sudo().browse(aid)
            if not rec.exists():
                return _error_response('Not found', 404)
            rec.write({'state': 'publish'})
            return _json_response({'id': rec.id, 'state': 'publish'})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/course-assignments/<int:aid>/finish', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def finish_assignment(self, aid, **kw):
        try:
            rec = request.env['op.assignment'].sudo().browse(aid)
            if not rec.exists():
                return _error_response('Not found', 404)
            rec.write({'state': 'finish'})
            return _json_response({'id': rec.id, 'state': 'finish'})
        except Exception as e:
            return _error_response(str(e), 500)

    # ── Assignment Types ─────────────────────────────────────────────

    @http.route('/api/assignment-types', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def list_assignment_types(self, **kw):
        try:
            M = request.env['grading.assignment.type'].sudo()
            offset, limit, page = _paginate(kw)
            total = M.search_count([])
            recs = M.search([], offset=offset, limit=limit)
            items = [{'id': r.id, 'name': r.name or ''} for r in recs]
            return _json_response({'items': items, 'data': items, 'total': total, 'page': page, 'size': limit})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/assignment-types', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def create_assignment_type(self, **kw):
        try:
            body = _get_json_body()
            rec = request.env['grading.assignment.type'].sudo().create({'name': body.get('name', '')})
            return _json_response({'id': rec.id, 'name': rec.name})
        except Exception as e:
            return _error_response(str(e), 500)

    # ── Subject Registrations (op.subject.registration) ──────────────

    @http.route('/api/subject-registrations', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def list_subject_registrations(self, **kw):
        try:
            M = request.env['op.subject.registration'].sudo()
            domain = []
            if kw.get('student_id'):
                domain.append(('student_id', '=', int(kw['student_id'])))
            if kw.get('course_id'):
                domain.append(('course_id', '=', int(kw['course_id'])))
            if kw.get('state'):
                domain.append(('state', '=', kw['state']))
            offset, limit, page = _paginate(kw)
            total = M.search_count(domain)
            recs = M.search(domain, offset=offset, limit=limit, order='id desc')
            items = [{
                'id': r.id,
                'student_id': r.student_id.id if hasattr(r, 'student_id') and r.student_id else 0,
                'course_id': r.course_id.id if hasattr(r, 'course_id') and r.course_id else 0,
                'state': getattr(r, 'state', 'draft') or 'draft',
            } for r in recs]
            return _json_response({'items': items, 'data': items, 'total': total, 'page': page, 'size': limit})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/subject-registrations', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def create_subject_registration(self, **kw):
        try:
            body = _get_json_body()
            vals = {}
            for fk in ('student_id', 'course_id'):
                if body.get(fk):
                    vals[fk] = int(body[fk])
            rec = request.env['op.subject.registration'].sudo().create(vals)
            return _json_response({'id': rec.id})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/subject-registrations/<int:rid>', type='http', auth='public', methods=['PATCH', 'PUT'], csrf=False)
    @jwt_required
    def update_subject_registration(self, rid, **kw):
        try:
            rec = request.env['op.subject.registration'].sudo().browse(rid)
            if not rec.exists():
                return _error_response('Not found', 404)
            return _json_response({'id': rec.id})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/subject-registrations/<int:rid>/confirm', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def confirm_registration(self, rid, **kw):
        try:
            rec = request.env['op.subject.registration'].sudo().browse(rid)
            if not rec.exists():
                return _error_response('Not found', 404)
            rec.write({'state': 'confirm'})
            return _json_response({'id': rec.id, 'state': 'confirm'})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/subject-registrations/<int:rid>/reject', type='http', auth='public', methods=['POST'], csrf=False)
    @jwt_required
    def reject_registration(self, rid, **kw):
        try:
            rec = request.env['op.subject.registration'].sudo().browse(rid)
            if not rec.exists():
                return _error_response('Not found', 404)
            rec.write({'state': 'reject'})
            return _json_response({'id': rec.id, 'state': 'reject'})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/subject-registrations/available', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def available_subjects(self, **kw):
        try:
            return _json_response([])
        except Exception as e:
            return _error_response(str(e), 500)

    # ── Submissions ──────────────────────────────────────────────────

    @http.route('/api/course-assignments/<int:aid>/submissions', type='http', auth='public', methods=['GET'], csrf=False)
    @jwt_required
    def list_submissions(self, aid, **kw):
        try:
            M = request.env['op.assignment.sub.line'].sudo()
            domain = [('assignment_id', '=', aid)]
            offset, limit, page = _paginate(kw)
            total = M.search_count(domain)
            recs = M.search(domain, offset=offset, limit=limit, order='id desc')
            items = [{
                'id': r.id,
                'student_id': r.student_id.id if hasattr(r, 'student_id') and r.student_id else 0,
                'state': getattr(r, 'state', 'draft') or 'draft',
            } for r in recs]
            return _json_response({'items': items, 'data': items, 'total': total, 'page': page, 'size': limit})
        except Exception as e:
            return _error_response(str(e), 500)

    @http.route('/api/submissions/<int:sid>', type='http', auth='public', methods=['PATCH'], csrf=False)
    @jwt_required
    def grade_submission(self, sid, **kw):
        try:
            rec = request.env['op.assignment.sub.line'].sudo().browse(sid)
            if not rec.exists():
                return _error_response('Not found', 404)
            body = _get_json_body()
            vals = {}
            if 'marks' in body:
                vals['marks'] = float(body['marks'])
            if vals:
                rec.write(vals)
            return _json_response({'id': rec.id})
        except Exception as e:
            return _error_response(str(e), 500)
