# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)

import base64
import io
import re
import time
import logging
from datetime import datetime, timedelta

import requests as py_requests

from odoo import api, fields, models, _

RC_MEDIA_DELAY = 10
RC_MEDIA_RETRY_WAIT = 65
RC_MEDIA_MAX_RETRIES = 5

OPENAI_WHISPER_URL = 'https://api.openai.com/v1/audio/transcriptions'

_logger = logging.getLogger(__name__)


class RcVoicemail(models.Model):
    _name = 'rc.voicemail'
    _description = 'RingCentral Voicemail'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'received_date desc'
    _rec_name = 'name'

    name = fields.Char(
        string='Reference', required=True, copy=False, readonly=True,
        default=lambda self: _('New'),
    )
    rc_message_id = fields.Char(
        string='RC Message ID', readonly=True, copy=False, index=True,
    )
    caller_number = fields.Char(string='Caller Number', readonly=True)
    caller_name = fields.Char(string='Caller Name', readonly=True)
    direction = fields.Selection([
        ('inbound', 'Inbound'),
        ('outbound', 'Outbound'),
    ], string='Direction', default='inbound', readonly=True)

    received_date = fields.Datetime(string='Received At', readonly=True)
    duration = fields.Integer(string='Duration (sec)', readonly=True)
    duration_display = fields.Char(
        string='Duration', compute='_compute_duration_display',
    )

    read_status = fields.Selection([
        ('Read', 'Read'),
        ('Unread', 'Unread'),
    ], string='Read Status', readonly=True)

    transcription_status = fields.Char(
        string='Transcription Status', readonly=True,
    )
    transcription_text = fields.Text(string='Transcription', readonly=True)
    transcription_timeline = fields.Text(
        string='Timeline', readonly=True,
        help='Timestamped transcription with silence gaps.',
    )
    transcription_language = fields.Char(
        string='Language', readonly=True,
    )
    has_transcription = fields.Boolean(
        compute='_compute_has_transcription', store=True,
    )

    partner_id = fields.Many2one(
        'res.partner', string='Contact', tracking=True,
    )
    audio_attachment_id = fields.Many2one(
        'ir.attachment', string='Audio File', readonly=True,
    )

    sale_order_count = fields.Integer(compute='_compute_partner_counts')
    invoice_count = fields.Integer(compute='_compute_partner_counts')

    @api.depends('duration')
    def _compute_duration_display(self):
        for rec in self:
            if rec.duration:
                mins, secs = divmod(rec.duration, 60)
                rec.duration_display = f'{mins}m {secs}s' if mins else f'{secs}s'
            else:
                rec.duration_display = ''

    @api.depends('transcription_text')
    def _compute_has_transcription(self):
        for rec in self:
            rec.has_transcription = bool(rec.transcription_text)

    @api.depends('partner_id')
    def _compute_partner_counts(self):
        for rec in self:
            if rec.partner_id:
                partner = rec.partner_id.commercial_partner_id or rec.partner_id
                partner_ids = (partner | partner.child_ids).ids
                rec.sale_order_count = self.env['sale.order'].search_count(
                    [('partner_id', 'in', partner_ids)],
                )
                rec.invoice_count = self.env['account.move'].search_count(
                    [('partner_id', 'in', partner_ids),
                     ('move_type', 'in', ('out_invoice', 'out_refund'))],
                )
            else:
                rec.sale_order_count = 0
                rec.invoice_count = 0

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get('name', _('New')) == _('New'):
                vals['name'] = (
                    self.env['ir.sequence'].next_by_code('rc.voicemail')
                    or _('New')
                )
            if not vals.get('partner_id') and vals.get('caller_number'):
                partner = self._match_voicemail_partner(vals['caller_number'])
                if partner:
                    vals['partner_id'] = partner.id
        return super().create(vals_list)

    # ──────────────────────────────────────────────────────────
    # Contact matching
    # ──────────────────────────────────────────────────────────

    @api.model
    def _match_voicemail_partner(self, number):
        if not number:
            return False
        cleaned = self._normalize_phone(number)
        if not cleaned or len(cleaned) < 7:
            return False

        Partner = self.env['res.partner']
        phone_fields = [f for f in ('phone', 'mobile') if f in Partner._fields]
        if not phone_fields:
            return False

        domain = ['|'] * (len(phone_fields) - 1)
        for f in phone_fields:
            domain.append((f, '!=', False))
        partners = Partner.search(domain, order='customer_rank desc, write_date desc')

        for p in partners:
            for f in phone_fields:
                val = p[f]
                if val and self._normalize_phone(val) == cleaned:
                    return p
        return False

    @staticmethod
    def _normalize_phone(number):
        return re.sub(r'\D', '', number or '')[-10:] if number else ''

    # ──────────────────────────────────────────────────────────
    # Smart button actions
    # ──────────────────────────────────────────────────────────

    def action_view_contact(self):
        self.ensure_one()
        if not self.partner_id:
            return
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'res.partner',
            'res_id': self.partner_id.id,
            'view_mode': 'form',
            'target': 'current',
        }

    def action_view_sale_orders(self):
        self.ensure_one()
        if not self.partner_id:
            return
        partner = self.partner_id.commercial_partner_id or self.partner_id
        partner_ids = (partner | partner.child_ids).ids
        return {
            'type': 'ir.actions.act_window',
            'name': _('Sales Orders - %s') % self.partner_id.name,
            'res_model': 'sale.order',
            'view_mode': 'list,form',
            'domain': [('partner_id', 'in', partner_ids)],
            'target': 'current',
        }

    def action_view_invoices(self):
        self.ensure_one()
        if not self.partner_id:
            return
        partner = self.partner_id.commercial_partner_id or self.partner_id
        partner_ids = (partner | partner.child_ids).ids
        return {
            'type': 'ir.actions.act_window',
            'name': _('Invoices - %s') % self.partner_id.name,
            'res_model': 'account.move',
            'view_mode': 'list,form',
            'domain': [('partner_id', 'in', partner_ids),
                        ('move_type', 'in', ('out_invoice', 'out_refund'))],
            'target': 'current',
        }

    # ──────────────────────────────────────────────────────────
    # Call / Fax buttons (stay on same page via act_window_close)
    # ──────────────────────────────────────────────────────────

    def action_make_call(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window_close',
            'infos': {
                'rc_action': 'call',
                'rc_phone_number': self.caller_number or '',
            },
        }

    def action_send_sms(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window_close',
            'infos': {
                'rc_action': 'sms',
                'rc_phone_number': self.caller_number or '',
            },
        }

    def action_send_fax(self):
        self.ensure_one()
        ctx = {}
        if self.partner_id:
            ctx['default_partner_id'] = self.partner_id.id
            fax_num = getattr(self.partner_id, 'x_ff_fax_number', '')
            if fax_num:
                ctx['default_fax_number'] = fax_num
        return {
            'type': 'ir.actions.act_window',
            'name': _('Send Fax'),
            'res_model': 'fusion_faxes.send.fax.wizard',
            'view_mode': 'form',
            'target': 'new',
            'context': ctx,
        }

    # ──────────────────────────────────────────────────────────
    # Cron: incremental voicemail sync
    # ──────────────────────────────────────────────────────────

    @api.model
    def _cron_sync_voicemails(self):
        """Incremental sync: fetch voicemails since last sync."""
        config = self.env['rc.config']._get_active_config()
        if not config:
            return

        ICP = self.env['ir.config_parameter'].sudo()
        last_sync = ICP.get_param('fusion_rc.last_voicemail_sync', '')
        if not last_sync:
            two_days_ago = datetime.utcnow() - timedelta(days=2)
            last_sync = two_days_ago.strftime('%Y-%m-%dT%H:%M:%S.000Z')

        now_str = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.000Z')
        total = self._sync_voicemails_from_date(config, last_sync, now_str)
        ICP.set_param('fusion_rc.last_voicemail_sync', now_str)

        if total:
            _logger.info("RC Voicemail Sync: %d new voicemails imported.", total)

    @api.model
    def _cron_daily_voicemail_catchup(self):
        """Re-scan past 3 days for any missed voicemails."""
        config = self.env['rc.config']._get_active_config()
        if not config:
            return

        three_days_ago = datetime.utcnow() - timedelta(days=3)
        date_from = three_days_ago.strftime('%Y-%m-%dT%H:%M:%S.000Z')
        now_str = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.000Z')

        total = self._sync_voicemails_from_date(config, date_from, now_str)
        _logger.info("RC Voicemail Catchup: %d new voicemails (past 3 days).", total)

    # ──────────────────────────────────────────────────────────
    # Backfill: re-download audio/transcripts for existing records
    # ──────────────────────────────────────────────────────────

    @api.model
    def _run_backfill_voicemail_media(self):
        """Re-fetch audio and transcription for records missing them.

        Called via cron after the initial import that missed audio due
        to rate limiting. Queries the RC API for each voicemail's
        message data and downloads any missing media.
        """
        config = self.env['rc.config']._get_active_config()
        if not config:
            _logger.warning("RC VM Backfill: No connected config.")
            return

        try:
            config._ensure_token()
        except Exception:
            _logger.exception("RC VM Backfill: Token invalid.")
            return

        missing = self.search([
            ('audio_attachment_id', '=', False),
            ('rc_message_id', '!=', False),
        ], order='received_date asc')

        _logger.info("RC VM Backfill: %d voicemails need audio.", len(missing))
        success = 0
        for vm in missing:
            try:
                data = config._api_get(
                    f'/restapi/v1.0/account/~/extension/~/message-store/{vm.rc_message_id}',
                )
                attachments = data.get('attachments', [])
                audio_uri = ''
                audio_content_type = 'audio/x-wav'
                transcript_uri = ''
                for att in attachments:
                    att_type = att.get('type', '')
                    if att_type == 'AudioRecording':
                        audio_uri = att.get('uri', '')
                        audio_content_type = att.get('contentType', 'audio/x-wav')
                    elif att_type == 'AudioTranscription':
                        transcript_uri = att.get('uri', '')

                if not vm.transcription_text and transcript_uri:
                    text = self._download_transcription(transcript_uri, config)
                    if text:
                        vm.sudo().write({'transcription_text': text})

                if audio_uri:
                    self._download_and_attach_audio(
                        vm, audio_uri, audio_content_type, config,
                    )
                    success += 1

                if not vm.transcription_text and vm.audio_attachment_id:
                    self._try_openai_transcribe(vm)
            except Exception:
                _logger.exception(
                    "RC VM Backfill: Failed for %s (msg %s).",
                    vm.name, vm.rc_message_id,
                )

        _logger.info(
            "RC VM Backfill complete: %d/%d audio files downloaded.",
            success, len(missing),
        )

    # ──────────────────────────────────────────────────────────
    # Historical import (all available voicemails, 12 months)
    # ──────────────────────────────────────────────────────────

    @api.model
    def _run_historical_voicemail_import(self):
        """Background job: import up to 12 months of voicemails in monthly chunks.

        Tracks which monthly chunks completed successfully via
        ir.config_parameter so it never re-queries months already done.
        Rate-limit safe: _api_get already paces at ~6 req/min with
        auto-retry on 429.
        """
        config = self.env['rc.config']._get_active_config()
        if not config:
            _logger.warning("RC VM Historical Import: No connected config.")
            return

        try:
            config._ensure_token()
        except Exception:
            _logger.exception("RC VM Historical Import: Token invalid.")
            return

        ICP = self.env['ir.config_parameter'].sudo()
        now = datetime.utcnow()
        total_imported = 0
        total_skipped = 0
        failed_chunks = 0

        for months_back in range(12, 0, -1):
            chunk_start = now - timedelta(days=months_back * 30)
            chunk_end = now - timedelta(days=(months_back - 1) * 30)
            chunk_num = 13 - months_back

            date_from = chunk_start.strftime('%Y-%m-%dT%H:%M:%S.000Z')
            date_to = chunk_end.strftime('%Y-%m-%dT%H:%M:%S.000Z')

            chunk_key = (
                f'fusion_rc.vm_import_done_{chunk_start.strftime("%Y%m")}'
            )
            if ICP.get_param(chunk_key, ''):
                _logger.info(
                    "RC VM Import [%d/12]: %s to %s -- already done, skipping.",
                    chunk_num, date_from[:10], date_to[:10],
                )
                total_skipped += 1
                continue

            _logger.info(
                "RC VM Import [%d/12]: %s to %s ...",
                chunk_num, date_from[:10], date_to[:10],
            )
            try:
                chunk_count = self._sync_voicemails_from_date(
                    config, date_from, date_to,
                )
                total_imported += chunk_count
                ICP.set_param(chunk_key, fields.Datetime.now().isoformat())
                _logger.info(
                    "RC VM Import [%d/12]: %d voicemails imported.",
                    chunk_num, chunk_count,
                )
            except Exception:
                failed_chunks += 1
                _logger.exception(
                    "RC VM Import [%d/12]: Failed, will retry next run.",
                    chunk_num,
                )

        _logger.info(
            "RC VM Historical Import complete: %d imported, %d skipped, %d failed.",
            total_imported, total_skipped, failed_chunks,
        )

    # ──────────────────────────────────────────────────────────
    # Core sync logic
    # ──────────────────────────────────────────────────────────

    def _sync_voicemails_from_date(self, config, date_from, date_to=None):
        """Fetch voicemails from RC Message Store for a date range."""
        total_imported = 0
        try:
            params = {
                'messageType': 'VoiceMail',
                'dateFrom': date_from,
                'perPage': 100,
            }
            if date_to:
                params['dateTo'] = date_to

            data = config._api_get(
                '/restapi/v1.0/account/~/extension/~/message-store',
                params=params,
            )
            total_imported += self._process_voicemail_page(data, config)

            nav = data.get('navigation', {})
            while nav.get('nextPage', {}).get('uri'):
                next_uri = nav['nextPage']['uri']
                data = config._api_get(next_uri)
                total_imported += self._process_voicemail_page(data, config)
                nav = data.get('navigation', {})

        except Exception:
            _logger.exception("RC Voicemail: Error syncing voicemails.")
        return total_imported

    def _process_voicemail_page(self, data, config):
        """Process one page of voicemail records with bulk dedup."""
        records = data.get('records', [])
        if not records:
            return 0

        page_ids = [str(r.get('id', '')) for r in records if r.get('id')]
        if page_ids:
            existing = set(
                r.rc_message_id
                for r in self.search([('rc_message_id', 'in', page_ids)])
            )
        else:
            existing = set()

        imported = 0
        for msg in records:
            msg_id = str(msg.get('id', ''))
            if not msg_id or msg_id in existing:
                continue
            if self._import_voicemail(msg, config):
                existing.add(msg_id)
                imported += 1
        return imported

    def _import_voicemail(self, msg, config):
        """Import a single voicemail message and download its audio."""
        try:
            msg_id = str(msg.get('id', ''))
            from_info = msg.get('from', {})
            caller_number = from_info.get('phoneNumber', '')
            caller_name = from_info.get('name', '')

            creation_time = msg.get('creationTime', '')
            received_dt = False
            if creation_time:
                try:
                    clean = creation_time.replace('Z', '+00:00')
                    received_dt = datetime.fromisoformat(clean).strftime(
                        '%Y-%m-%d %H:%M:%S'
                    )
                except (ValueError, AttributeError):
                    pass

            direction = (msg.get('direction', '') or '').lower()
            if direction not in ('inbound', 'outbound'):
                direction = 'inbound'

            attachments = msg.get('attachments', [])
            vm_duration = 0
            audio_uri = ''
            audio_content_type = 'audio/x-wav'
            transcript_uri = ''
            for att in attachments:
                att_type = att.get('type', '')
                if att_type == 'AudioRecording':
                    vm_duration = att.get('vmDuration', 0)
                    audio_uri = att.get('uri', '')
                    audio_content_type = att.get('contentType', 'audio/x-wav')
                elif att_type == 'AudioTranscription':
                    transcript_uri = att.get('uri', '')

            transcription_text = ''
            if transcript_uri:
                transcription_text = self._download_transcription(
                    transcript_uri, config,
                )

            partner = self._match_voicemail_partner(caller_number)

            vm = self.sudo().create({
                'rc_message_id': msg_id,
                'caller_number': caller_number,
                'caller_name': caller_name,
                'direction': direction,
                'received_date': received_dt,
                'duration': vm_duration,
                'read_status': msg.get('readStatus', 'Unread'),
                'transcription_status': 'Completed' if transcription_text else '',
                'transcription_text': transcription_text,
                'partner_id': partner.id if partner else False,
            })

            if audio_uri:
                self._download_and_attach_audio(vm, audio_uri, audio_content_type, config)

            if not vm.transcription_text and vm.audio_attachment_id:
                self._try_openai_transcribe(vm)

            return True
        except Exception:
            _logger.exception(
                "RC Voicemail: Failed to import message %s",
                msg.get('id', ''),
            )
            return False

    def _rc_media_get(self, uri, config):
        """Download binary content from RC with rate-limit retry."""
        config._ensure_token()
        for attempt in range(1, RC_MEDIA_MAX_RETRIES + 1):
            resp = py_requests.get(
                uri,
                headers={'Authorization': f'Bearer {config.access_token}'},
                timeout=30,
                verify=config.ssl_verify,
                proxies=config._get_proxies(),
            )
            if resp.status_code == 429:
                wait = int(resp.headers.get('Retry-After', RC_MEDIA_RETRY_WAIT))
                _logger.warning(
                    "RC media 429. Waiting %ds (attempt %d/%d)...",
                    wait, attempt, RC_MEDIA_MAX_RETRIES,
                )
                time.sleep(wait)
                config._ensure_token()
                continue
            if resp.status_code == 401 and attempt < RC_MEDIA_MAX_RETRIES:
                _logger.warning(
                    "RC media 401. Refreshing token, waiting 60s (attempt %d/%d)...",
                    attempt, RC_MEDIA_MAX_RETRIES,
                )
                time.sleep(60)
                try:
                    config._refresh_token()
                except Exception:
                    pass
                continue
            resp.raise_for_status()
            time.sleep(RC_MEDIA_DELAY)
            return resp
        resp.raise_for_status()

    def _download_transcription(self, transcript_uri, config):
        """Download voicemail transcription text from RC."""
        try:
            resp = self._rc_media_get(transcript_uri, config)
            return (resp.text or '').strip()
        except Exception:
            _logger.warning("RC Voicemail: Could not fetch transcription.")
            return ''

    def _download_and_attach_audio(self, vm, audio_uri, content_type, config):
        """Download the voicemail audio and post it to the record's chatter."""
        try:
            resp = self._rc_media_get(audio_uri, config)
            audio_data = resp.content
            if not audio_data:
                return

            ext = 'wav' if 'wav' in content_type else 'mp3'
            filename = f'{vm.name}.{ext}'

            attachment = self.env['ir.attachment'].sudo().create({
                'name': filename,
                'type': 'binary',
                'datas': base64.b64encode(audio_data),
                'mimetype': content_type,
                'res_model': 'rc.voicemail',
                'res_id': vm.id,
            })
            vm.sudo().write({'audio_attachment_id': attachment.id})
            vm.sudo().message_post(
                body=_('Voicemail audio (%ss)') % vm.duration,
                attachment_ids=[attachment.id],
                message_type='notification',
            )
        except Exception:
            _logger.exception(
                "RC Voicemail: Failed to download audio for %s", vm.name,
            )

    # ──────────────────────────────────────────────────────────
    # OpenAI Whisper transcription
    # ──────────────────────────────────────────────────────────

    def _get_openai_key(self):
        return (
            self.env['ir.config_parameter']
            .sudo()
            .get_param('fusion_rc.openai_api_key', '')
        )

    def _try_openai_transcribe(self, vm):
        """Transcribe a voicemail if OpenAI key is configured. Non-fatal."""
        api_key = self._get_openai_key()
        if not api_key:
            return
        try:
            result = self._transcribe_audio_openai(vm.audio_attachment_id, api_key)
            if result:
                vm.sudo().write(result)
        except Exception:
            _logger.warning(
                "RC Voicemail: OpenAI transcription failed for %s", vm.name,
            )

    def _transcribe_audio_openai(self, attachment, api_key):
        """Send audio to OpenAI Whisper verbose_json and return structured data.

        Returns a dict with transcription_text (English), language tag,
        and a timeline string showing silence gaps between segments.
        """
        audio_data = base64.b64decode(attachment.datas)
        if not audio_data:
            return {}

        ext = 'wav'
        if attachment.mimetype and 'mp3' in attachment.mimetype:
            ext = 'mp3'

        resp = py_requests.post(
            OPENAI_WHISPER_URL,
            headers={'Authorization': f'Bearer {api_key}'},
            files={
                'file': (
                    f'voicemail.{ext}',
                    io.BytesIO(audio_data),
                    attachment.mimetype or 'audio/wav',
                ),
            },
            data={
                'model': 'whisper-1',
                'response_format': 'verbose_json',
                'timestamp_granularities[]': 'segment',
            },
            timeout=120,
        )
        resp.raise_for_status()
        data = resp.json()

        language = (data.get('language') or '').strip()
        full_text = (data.get('text') or '').strip()
        segments = data.get('segments', [])

        english_text = full_text
        if language and language != 'english':
            english_text = self._translate_to_english(full_text, api_key)

        timeline = self._build_timeline(segments)

        return {
            'transcription_text': english_text,
            'transcription_language': language.title() if language else '',
            'transcription_timeline': timeline,
            'transcription_status': 'Completed',
        }

    def _translate_to_english(self, text, api_key):
        """Translate text to English using OpenAI chat completions."""
        try:
            resp = py_requests.post(
                'https://api.openai.com/v1/chat/completions',
                headers={
                    'Authorization': f'Bearer {api_key}',
                    'Content-Type': 'application/json',
                },
                json={
                    'model': 'gpt-4o-mini',
                    'messages': [
                        {
                            'role': 'system',
                            'content': 'Translate the following text to English. '
                                       'Return only the translation, nothing else.',
                        },
                        {'role': 'user', 'content': text},
                    ],
                    'temperature': 0.1,
                },
                timeout=60,
            )
            resp.raise_for_status()
            return (
                resp.json()
                .get('choices', [{}])[0]
                .get('message', {})
                .get('content', text)
                .strip()
            )
        except Exception:
            _logger.warning("RC Voicemail: Translation failed, using original.")
            return text

    @staticmethod
    def _build_timeline(segments):
        """Build a timeline string from Whisper segments showing silence gaps.

        Example output:
            .....3s..... Do you have apartments? .....6s..... Please call back.
        """
        if not segments:
            return ''

        parts = []
        prev_end = 0.0

        for seg in segments:
            seg_start = seg.get('start', 0.0)
            seg_end = seg.get('end', 0.0)
            seg_text = (seg.get('text') or '').strip()

            gap = seg_start - prev_end
            if gap >= 1.0:
                parts.append(f'.....{int(gap)}s.....')

            if seg_text:
                parts.append(seg_text)

            prev_end = seg_end

        return ' '.join(parts)

    def action_transcribe(self):
        """Manual button: transcribe this voicemail with OpenAI Whisper."""
        self.ensure_one()
        api_key = self._get_openai_key()
        if not api_key:
            return {
                'type': 'ir.actions.client',
                'tag': 'display_notification',
                'params': {
                    'title': _('No API Key'),
                    'message': _('Set your OpenAI API key in Settings > Fusion RingCentral.'),
                    'type': 'warning',
                    'sticky': False,
                },
            }
        if not self.audio_attachment_id:
            return {
                'type': 'ir.actions.client',
                'tag': 'display_notification',
                'params': {
                    'title': _('No Audio'),
                    'message': _('This voicemail has no audio file to transcribe.'),
                    'type': 'warning',
                    'sticky': False,
                },
            }
        try:
            result = self._transcribe_audio_openai(self.audio_attachment_id, api_key)
            if result:
                self.sudo().write(result)
        except Exception:
            _logger.exception("RC Voicemail: Manual transcription failed for %s", self.name)
            return {
                'type': 'ir.actions.client',
                'tag': 'display_notification',
                'params': {
                    'title': _('Transcription Failed'),
                    'message': _('Could not transcribe. Check logs for details.'),
                    'type': 'danger',
                    'sticky': False,
                },
            }

    @api.model
    def _cron_transcribe_voicemails(self):
        """Batch-transcribe voicemails that have audio but no transcript."""
        api_key = self._get_openai_key()
        if not api_key:
            return

        pending = self.search([
            ('audio_attachment_id', '!=', False),
            ('transcription_text', '=', False),
        ], limit=50, order='received_date desc')

        if not pending:
            return

        _logger.info("RC VM Transcribe: %d voicemails to process.", len(pending))
        success = 0
        for vm in pending:
            try:
                result = self._transcribe_audio_openai(vm.audio_attachment_id, api_key)
                if result:
                    vm.sudo().write(result)
                    success += 1
            except Exception:
                _logger.warning(
                    "RC VM Transcribe: Failed for %s, skipping.", vm.name,
                )
            time.sleep(1)

        _logger.info("RC VM Transcribe: %d/%d completed.", success, len(pending))
