"""Course Plan models.

A *course plan* is the AI-generated, structured outline of a full course
(similar to the UTAS GE1 outline: objectives, per-skill learning outcomes,
grammar scope, assessment split, and a week-by-week delivery plan).

Distinct from the existing exam / exercise generation pipeline:

* ``encoach.ai.generation.log``  generates **exam questions**.
* ``encoach.course.plan``         generates **teaching content** — weeks,
  reading texts, listening scripts, speaking prompts, grammar lessons, etc.

Large, loosely-structured JSON (objectives, learning outcomes grouped by
skill, grammar topics, assessment breakdown, learning resources) lives on
the header as ``Text`` columns to keep the schema boring. Per-week rows
and per-week materials each get their own table because they are
generated incrementally and users want to drill into them.
"""

import json
import logging

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


SKILL_SELECTION = [
    ('reading', 'Reading'),
    ('writing', 'Writing'),
    ('listening', 'Listening'),
    ('speaking', 'Speaking'),
    ('grammar', 'Grammar'),
    ('vocabulary', 'Vocabulary'),
    ('integrated', 'Integrated'),
]


MATERIAL_TYPE_SELECTION = [
    ('reading_text', 'Reading Text'),
    ('listening_script', 'Listening Script'),
    ('speaking_prompt', 'Speaking Prompt'),
    ('writing_prompt', 'Writing Prompt'),
    ('grammar_lesson', 'Grammar Lesson'),
    ('vocabulary_list', 'Vocabulary List'),
    ('practice', 'Practice Exercises'),
    ('interactive_workbook', 'Interactive Workbook'),
    ('other', 'Other'),
]


class CoursePlan(models.Model):
    _name = 'encoach.course.plan'
    _description = 'AI-generated Course Plan'
    _order = 'create_date desc, id desc'

    name = fields.Char(required=True)
    entity_id = fields.Many2one(
        'encoach.entity',
        string='Entity',
        ondelete='set null',
        index=True,
        help='Owning entity/organization for LMS isolation.',
    )
    course_id = fields.Many2one('op.course', ondelete='set null', string='Linked course')
    cefr_level = fields.Selection([
        ('pre_a1', 'Pre-A1'),
        ('a1', 'A1'),
        ('a2', 'A2'),
        ('b1', 'B1'),
        ('b2', 'B2'),
        ('c1', 'C1'),
        ('c2', 'C2'),
    ], default='a2')

    total_weeks = fields.Integer(default=12, string='Total weeks')
    contact_hours_per_week = fields.Integer(default=18, string='Contact hours / week')

    # The "Reading & Writing = 10 hrs/wk, Listening & Speaking = 8 hrs/wk"
    # breakdown is a free-form label so AI can propose any split.
    skills_division = fields.Char(
        string='Skills division',
        help='Free-form label describing how hours are split across skill '
             'tracks, e.g. "10 hrs/wk Reading & Writing + 8 hrs/wk '
             'Listening & Speaking".',
    )

    description = fields.Text()
    objectives_json = fields.Text(
        help='JSON array of high-level course objectives.',
    )
    outcomes_json = fields.Text(
        help='JSON object keyed by skill (reading/writing/listening/speaking/'
             'vocabulary/grammar). Each value is an ordered list of '
             '{code, description} learning outcome rows — code is e.g. '
             '"RLO1", "WLO3", "GLO2a".',
    )
    grammar_json = fields.Text(
        help='JSON array of grammar topics in the order they should be '
             'taught. Each item is {code, label, sub_items: []}.',
    )
    assessment_json = fields.Text(
        help='JSON object describing the CA/FE split and component weights.',
    )
    resources_json = fields.Text(
        help='JSON array of textbooks / URLs / materials referenced by the '
             'AI when planning content.',
    )

    status = fields.Selection([
        ('draft', 'Draft'),
        ('generated', 'Generated'),
        ('approved', 'Approved'),
        ('archived', 'Archived'),
    ], default='draft')

    brief_json = fields.Text(
        help='Original brief that was sent to the AI — kept for audit and '
             'so the user can re-generate if the first pass disappoints.',
    )

    week_ids = fields.One2many(
        'encoach.course.plan.week', 'plan_id', string='Weeks',
    )
    material_ids = fields.One2many(
        'encoach.course.plan.material', 'plan_id', string='Materials',
    )
    source_ids = fields.One2many(
        'encoach.course.plan.source', 'plan_id', string='Reference sources',
    )
    media_ids = fields.One2many(
        'encoach.course.plan.media', 'plan_id', string='Generated media',
    )
    assignment_ids = fields.One2many(
        'encoach.course.plan.assignment', 'plan_id', string='Assignments',
    )

    week_count = fields.Integer(compute='_compute_counts', store=False)
    material_count = fields.Integer(compute='_compute_counts', store=False)
    source_count = fields.Integer(compute='_compute_counts', store=False)
    media_count = fields.Integer(compute='_compute_counts', store=False)
    assignment_count = fields.Integer(compute='_compute_counts', store=False)

    @api.depends('week_ids', 'material_ids', 'source_ids', 'media_ids', 'assignment_ids')
    def _compute_counts(self):
        for rec in self:
            rec.week_count = len(rec.week_ids)
            rec.material_count = len(rec.material_ids)
            rec.source_count = len(rec.source_ids)
            rec.media_count = len(rec.media_ids)
            rec.assignment_count = len(rec.assignment_ids)

    @api.model_create_multi
    def create(self, vals_list):
        user = self.env.user.sudo()
        default_entity = user.entity_ids[:1].id if hasattr(user, 'entity_ids') else False
        for vals in vals_list:
            if vals.get('entity_id') is None and default_entity:
                vals['entity_id'] = default_entity
        return super().create(vals_list)

    # ------------------------------------------------------------------
    # Serialisation helpers — used by the REST controller so payload
    # shape stays in a single, obvious place.
    # ------------------------------------------------------------------
    def _loads(self, raw, default):
        if not raw:
            return default
        try:
            return json.loads(raw)
        except (TypeError, ValueError):
            return default

    def to_api_dict(self, *, include_weeks=True, include_materials=False,
                    include_sources=False, include_media=False,
                    include_assignments=False):
        self.ensure_one()
        data = {
            'id': self.id,
            'name': self.name,
            'entity_id': self.entity_id.id if self.entity_id else None,
            'entity_name': self.entity_id.name if self.entity_id else '',
            'course_id': self.course_id.id if self.course_id else None,
            'course_name': self.course_id.name if self.course_id else '',
            'cefr_level': self.cefr_level or '',
            'total_weeks': self.total_weeks or 0,
            'contact_hours_per_week': self.contact_hours_per_week or 0,
            'skills_division': self.skills_division or '',
            'description': self.description or '',
            'status': self.status or 'draft',
            'objectives': self._loads(self.objectives_json, []),
            'outcomes': self._loads(self.outcomes_json, {}),
            'grammar': self._loads(self.grammar_json, []),
            'assessment': self._loads(self.assessment_json, {}),
            'resources': self._loads(self.resources_json, []),
            'week_count': len(self.week_ids),
            'material_count': len(self.material_ids),
            'source_count': len(self.source_ids),
            'media_count': len(self.media_ids),
            'assignment_count': len(self.assignment_ids),
            'created_at': self.create_date.isoformat() if self.create_date else None,
        }
        if include_weeks:
            data['weeks'] = [w.to_api_dict() for w in self.week_ids.sorted('week_number')]
        if include_materials:
            data['materials'] = [m.to_api_dict() for m in self.material_ids]
        if include_sources:
            data['sources'] = [s.to_api_dict() for s in self.source_ids]
        if include_media:
            data['media'] = [m.to_api_dict() for m in self.media_ids]
        if include_assignments:
            data['assignments'] = [a.to_api_dict() for a in self.assignment_ids]
        return data


class CoursePlanWeek(models.Model):
    _name = 'encoach.course.plan.week'
    _description = 'Course Plan Week'
    _order = 'week_number asc, id asc'

    plan_id = fields.Many2one(
        'encoach.course.plan', required=True, ondelete='cascade', index=True,
    )
    week_number = fields.Integer(required=True)
    date_label = fields.Char(
        help='Human-readable date range, e.g. "7-11 Sep. 2025".',
    )
    unit = fields.Char(help='Textbook unit / theme for the week.')
    focus = fields.Char(help='Short focus headline for the week.')
    items_json = fields.Text(
        help='JSON array of per-skill rows for this week: '
             '[{skill, outcome_codes: [...], remarks}]. Mirrors the '
             'GE1 delivery plan table.',
    )

    material_ids = fields.One2many(
        'encoach.course.plan.material', 'week_id', string='Materials',
    )
    material_count = fields.Integer(compute='_compute_material_count', store=False)

    @api.depends('material_ids')
    def _compute_material_count(self):
        for rec in self:
            rec.material_count = len(rec.material_ids)

    def _loads(self, raw, default):
        if not raw:
            return default
        try:
            return json.loads(raw)
        except (TypeError, ValueError):
            return default

    def to_api_dict(self):
        self.ensure_one()
        return {
            'id': self.id,
            'week_number': self.week_number or 0,
            'date_label': self.date_label or '',
            'unit': self.unit or '',
            'focus': self.focus or '',
            'items': self._loads(self.items_json, []),
            'material_count': len(self.material_ids),
        }


class CoursePlanMaterial(models.Model):
    _name = 'encoach.course.plan.material'
    _description = 'Course Plan Teaching Material'
    _order = 'week_id, skill, id'

    plan_id = fields.Many2one(
        'encoach.course.plan', required=True, ondelete='cascade', index=True,
    )
    week_id = fields.Many2one(
        'encoach.course.plan.week', ondelete='cascade', index=True,
    )
    week_number = fields.Integer(
        related='week_id.week_number', store=True, string='Week #',
    )
    skill = fields.Selection(SKILL_SELECTION, required=True)
    material_type = fields.Selection(
        MATERIAL_TYPE_SELECTION, required=True, default='other',
    )
    title = fields.Char(required=True)
    is_static = fields.Boolean(
        default=False,
        help='When enabled, regenerate-week keeps this material untouched.',
    )
    share_date = fields.Date(
        help='Optional date when the material becomes visible/shared.',
    )
    summary = fields.Text(
        help='Short blurb — purpose / learning outcomes targeted / how to use.',
    )
    body_json = fields.Text(
        help='Structured payload. Shape depends on material_type: '
             'reading_text → {text, questions[]}, '
             'listening_script → {script, comprehension_questions[]}, '
             'grammar_lesson → {explanation, examples[], practice[]}, etc.',
    )
    body_text = fields.Text(
        help='Plain-text rendering for easy preview / copy-paste when the '
             'structured body is not needed.',
    )
    grounded_on_json = fields.Text(
        help='JSON list of source citations used to ground this material via '
             'RAG: [{source_id, title, chunks_used}]. Surfaced in the UI as '
             'a "Grounded on N references" badge.',
    )
    extracted_from_json = fields.Text(
        help='JSON {source_id, page_hint, …} when the material was mined '
             'from an indexed PDF/DOCX by ExerciseExtractor. Distinct from '
             'grounded_on_json which records *retrieval* rather than '
             '*extraction* provenance.',
    )

    media_ids = fields.One2many(
        'encoach.course.plan.media', 'material_id', string='Generated media',
    )

    def _loads(self, raw, default):
        if not raw:
            return default
        try:
            return json.loads(raw)
        except (TypeError, ValueError):
            return default

    def to_api_dict(self, include_media=True):
        self.ensure_one()
        grounded = self._loads(self.grounded_on_json, [])
        extracted = self._loads(self.extracted_from_json, None)
        out = {
            'id': self.id,
            'plan_id': self.plan_id.id,
            'week_id': self.week_id.id if self.week_id else None,
            'week_number': self.week_number or 0,
            'skill': self.skill or '',
            'material_type': self.material_type or 'other',
            'title': self.title or '',
            'is_static': bool(self.is_static),
            'share_date': self.share_date.isoformat() if self.share_date else None,
            'summary': self.summary or '',
            'body': self._loads(self.body_json, {}),
            'body_text': self.body_text or '',
            'grounded_on': grounded if isinstance(grounded, list) else [],
            'extracted_from': extracted if isinstance(extracted, dict) else None,
        }
        if include_media:
            out['media'] = [m.to_api_dict() for m in self.media_ids]
        return out
