"""Reference source attached to a course plan.

Each source is one of: an uploaded file (PDF, DOCX, TXT), a URL the
agent should crawl, or a chunk of inline text. Indexing extracts the
text, chunks it, and pushes the chunks into ``encoach.embedding`` so
the LangGraph ``course_planner`` and ``course_week_materials`` agents
can ground their generation on these sources via the existing
``resources.search`` tool, scoped to the plan.

We deliberately use the existing pgvector store rather than a new one:
that way the same retrieval pipeline (embedding model, chunker, cosine
similarity ranker) is shared, and admins can look up "what did the AI
read" with the same dashboards.
"""

import base64
import logging

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


SOURCE_KIND_SELECTION = [
    ('file', 'File'),
    ('url', 'URL'),
    ('text', 'Inline text'),
    # ``resource`` is a soft-link to the central ``encoach.resource``
    # library so the admin can re-use a PDF / DOCX / link they already
    # uploaded under /admin/resources without re-uploading the binary.
    # The indexer dereferences the link at extraction time; the binary
    # itself stays in the library record so a single edit / rotation
    # propagates to every plan that grounds on it.
    ('resource', 'Library resource'),
]

SOURCE_STATUS_SELECTION = [
    ('pending', 'Pending'),
    ('indexing', 'Indexing'),
    ('indexed', 'Indexed'),
    ('failed', 'Failed'),
]


class CoursePlanSource(models.Model):
    _name = 'encoach.course.plan.source'
    _description = 'Course Plan Reference Source'
    _order = 'sequence asc, id asc'

    plan_id = fields.Many2one(
        'encoach.course.plan', required=True, ondelete='cascade', index=True,
    )
    sequence = fields.Integer(default=10)
    name = fields.Char(required=True, help='Human-readable label.')
    kind = fields.Selection(SOURCE_KIND_SELECTION, required=True, default='file')

    # Filled when ``kind == 'file'`` — base64 payload mirrors how
    # encoach.resource and ir.attachment already do it.
    file = fields.Binary(string='Uploaded file', attachment=True)
    file_name = fields.Char(string='File name')
    mime_type = fields.Char(string='MIME type')

    url = fields.Char(string='Source URL')
    inline_text = fields.Text(string='Inline text')

    # Optional pointer to the central /admin/resources library. When set
    # the indexer pulls the binary / URL / inline text from the linked
    # ``encoach.resource`` instead of re-storing it on this row, which
    # avoids duplicating large PDFs across every plan that grounds on
    # the same library item.
    resource_id = fields.Many2one(
        'encoach.resource',
        string='Library resource',
        ondelete='set null',
        help='Link to a resource already uploaded under /admin/resources. '
             'The indexer reads the binary from the library at extraction '
             'time so updates to the library propagate to every plan.',
    )

    auto_index = fields.Boolean(
        default=True,
        help='If true, indexing runs automatically on create. '
             'Disable to upload first, review, then index manually.',
    )
    status = fields.Selection(SOURCE_STATUS_SELECTION, default='pending')
    error = fields.Char()
    chunks_count = fields.Integer(default=0, string='Chunks')
    extracted_chars = fields.Integer(default=0, string='Extracted chars')
    indexed_at = fields.Datetime()

    @api.model_create_multi
    def create(self, vals_list):
        records = super().create(vals_list)
        for rec in records:
            if rec.auto_index:
                try:
                    rec.action_index()
                except Exception as exc:
                    # If indexing crashes before SourceIndexer has a chance
                    # to mark the row as ``failed`` (e.g. an unexpected
                    # import or DB error) we MUST still set the status to
                    # ``failed``; otherwise the source sits in ``pending``
                    # forever and the deliverables UI can't show progress.
                    _logger.exception('Auto-index failed for source %s', rec.id)
                    try:
                        rec.write({
                            'status': 'failed',
                            'error': str(exc)[:500],
                        })
                    except Exception:
                        pass
        return records

    def action_index(self):
        """(Re-)extract text and push chunks to the vector store.

        Wraps each per-record indexing call so a failure on one source
        doesn't abort the loop and leave later records orphaned.
        """
        from odoo.addons.encoach_ai_course.services.source_indexer import (
            SourceIndexer,
        )
        indexer = SourceIndexer(self.env)
        for rec in self:
            try:
                indexer.index(rec)
            except Exception as exc:
                _logger.exception('Index failed for source %s', rec.id)
                try:
                    rec.write({
                        'status': 'failed',
                        'error': str(exc)[:500],
                    })
                except Exception:
                    pass
        return True

    def unlink(self):
        try:
            from odoo.addons.encoach_vector.services.embedding_service import (
                EmbeddingService,
            )
            svc = EmbeddingService(self.env)
            for rec in self:
                svc.delete('course_plan_source', rec.id)
        except Exception:
            _logger.exception('Failed to delete embeddings for sources')
        return super().unlink()

    def to_api_dict(self):
        self.ensure_one()
        return {
            'id': self.id,
            'plan_id': self.plan_id.id,
            'name': self.name or '',
            'kind': self.kind or 'file',
            'file_name': self.file_name or '',
            'mime_type': self.mime_type or '',
            'url': self.url or '',
            'has_inline_text': bool(self.inline_text),
            'resource_id': self.resource_id.id if self.resource_id else None,
            'resource_name': self.resource_id.name if self.resource_id else '',
            'resource_type': (
                self.resource_id.type if self.resource_id else ''
            ),
            'status': self.status or 'pending',
            'error': self.error or '',
            'chunks_count': self.chunks_count or 0,
            'extracted_chars': self.extracted_chars or 0,
            'indexed_at': self.indexed_at.isoformat() if self.indexed_at else None,
            'created_at': self.create_date.isoformat() if self.create_date else None,
        }

    def get_decoded_file(self):
        """Return raw bytes of the uploaded file, or ``b''`` if none."""
        self.ensure_one()
        if not self.file:
            return b''
        try:
            return base64.b64decode(self.file)
        except Exception:
            return b''
