# -*- coding: utf-8 -*-
import logging
from datetime import timedelta

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


class DoCronHealthLog(models.Model):
    """One row per scheduled-action execution.

    Rows are written by the ``ir.cron`` wrapper on a dedicated cursor that
    commits independently of the job's own transaction, so a run is still
    recorded even when the job rolls back or the worker crashes mid-run.
    Read-only to users (see ir.model.access.csv); only the monitor writes here.
    """
    _name = 'do.cron.health.log'
    _description = 'Cron Run Health Log'
    _order = 'start_datetime desc, id desc'
    _rec_name = 'name'

    cron_id = fields.Many2one(
        'ir.cron', string='Scheduled Action',
        ondelete='cascade', required=True, index=True)
    name = fields.Char(
        string='Cron', readonly=True,
        help="Snapshot of the scheduled action's name at run time.")
    start_datetime = fields.Datetime(string='Started', index=True, readonly=True)
    end_datetime = fields.Datetime(string='Ended', readonly=True)
    duration = fields.Float(
        string='Duration (s)', readonly=True, aggregator='avg',
        help="Wall-clock seconds the run took.")
    state = fields.Selection(
        selection=[
            ('running', 'Running'),
            ('success', 'Success'),
            ('partial', 'Partially Done'),
            ('failed', 'Failed'),
            ('stale', 'Stale'),
        ],
        string='Outcome', default='running', required=True, index=True, readonly=True)
    error_message = fields.Text(
        string='Traceback', readonly=True,
        help="Formatted traceback captured when the run failed.")
    trigger = fields.Selection(
        selection=[('scheduled', 'Scheduled'), ('manual', 'Manual')],
        string='Trigger', default='scheduled', readonly=True)
    db_name = fields.Char(string='Database', readonly=True)

    # ------------------------------------------------------------------
    # Watchdog + alerting (runs from its own ir.cron, in a normal cursor)
    # ------------------------------------------------------------------
    @api.model
    def _cron_watchdog(self):
        """Mark overdue 'running' rows 'stale' (worker crash / overlap), then fire
        throttled health alerts. Runs in a normal cron transaction."""
        icp = self.env['ir.config_parameter'].sudo()
        if str(icp.get_param('do_cron_health.enabled', 'True')).strip().lower() in ('false', '0', '', 'none'):
            return
        stale_minutes = int(icp.get_param('do_cron_health.stale_minutes', '60') or 60)
        cutoff = fields.Datetime.now() - timedelta(minutes=max(stale_minutes, 1))
        stale = self.sudo().search([('state', '=', 'running'), ('start_datetime', '<', cutoff)])
        if stale:
            stale.write({'state': 'stale', 'end_datetime': fields.Datetime.now()})
            _logger.warning("do_cron_health: flagged %s stale run(s)", len(stale))
        self._dch_run_alerts()

    @api.model
    def _dch_run_alerts(self):
        """Throttled per-cron alerts from the latest run + recent failures.
        Dedup/throttling is handled by ops.alert.mixin (alert on state change,
        otherwise suppress until the throttle window elapses)."""
        Alert = self.env['ops.alert.mixin']
        State = self.env['ops.alert.state'].sudo()
        recent = fields.Datetime.now() - timedelta(days=7)
        # active_test=False so we still alert on crons Odoo auto-deactivated
        # after repeated failures -- those are the ones that matter most.
        crons = self.env['ir.cron'].sudo().with_context(active_test=False).search(
            [('do_health_log_ids', '!=', False)])
        for cron in crons:
            if cron.do_last_run and cron.do_last_run < recent:
                continue
            key = 'cron_health:%s' % cron.id
            label = cron.cron_name or ('cron #%s' % cron.id)
            if cron.do_is_overrunning:
                Alert._ops_notify(
                    key=key, state='overrun', level='warning',
                    subject="Cron overrunning: %s" % label,
                    body="'%s' has been running far longer than its average of %.1fs." % (
                        label, cron.do_avg_duration))
            elif cron.do_health_status in ('warning', 'critical'):
                last_log = self.sudo().search([('cron_id', '=', cron.id)], limit=1)
                Alert._ops_notify(
                    key=key, state=cron.do_health_status,
                    level='error' if cron.do_health_status == 'critical' else 'warning',
                    subject="Cron %s: %s" % (cron.do_health_status, label),
                    body=(last_log.error_message or
                          "Last run outcome: %s (%s failed/stale run(s))." % (
                              cron.do_last_state, cron.do_recent_failure_count)))
            elif cron.do_health_status == 'good':
                row = State.search([('key', '=', key)], limit=1)
                if row and row.last_state in ('warning', 'critical', 'overrun', 'failed', 'stale'):
                    Alert._ops_notify(
                        key=key, state='good', level='info',
                        subject="Cron recovered: %s" % label,
                        body="'%s' is healthy again." % label)

    @api.autovacuum
    def _gc_cron_health_logs(self):
        """Retention: drop rows older than the configured window. Hooks the daily
        base auto-vacuum cron, so no dedicated retention cron is needed."""
        days = int(self.env['ir.config_parameter'].sudo().get_param(
            'do_cron_health.retention_days', '30') or 30)
        if days <= 0:
            return
        cutoff = fields.Datetime.now() - timedelta(days=days)
        old = self.sudo().search([('start_datetime', '<', cutoff)])
        if old:
            count = len(old)
            old.unlink()
            _logger.info("do_cron_health: vacuumed %s old log row(s)", count)
