"""Log approved Odoo leave into SAP B1 as an employee absence record.

B1 has no leave-request/approval workflow (no such entities in the Service
Layer) and only a flat absence log (EmployeeAbsenceInfoLines: FromDate, ToDate,
Reason, ApprovedBy). So Odoo owns the workflow (request + approval + per-type
balances) and, on approval, writes the result into B1 so B1 holds the
authoritative absence log for whoever uses it (payroll/accounting).

Push is best-effort: a B1 outage never blocks the Odoo approval. Unsynced
approved leaves are retried by a cron.

Note: write() fires this on ANY code path that sets state='validate' --
normal approval, HR force-approve, a bulk edit, a future feature, an
import. There's no separate opt-in per caller, which is exactly why the
production-write guard below has to live here rather than in each caller.
"""
import logging

from odoo import fields, models

from .sapb1_client import SAPB1Client, check_production_write_allowed

_logger = logging.getLogger(__name__)


class HrLeave(models.Model):
    _inherit = "hr.leave"

    b1_absence_synced = fields.Datetime(string="Logged to SAP B1", readonly=True, copy=False)

    def write(self, vals):
        res = super().write(vals)
        if vals.get("state") == "validate":
            self.filtered(lambda l: not l.b1_absence_synced)._push_absence_to_b1()
        return res

    def _push_absence_to_b1(self):
        """Append an absence line to each employee's B1 record. Best-effort."""
        pushable = self.filtered(
            lambda l: l.state == "validate"
            and l.employee_id.b1_employee_id
            and not l.b1_absence_synced
        )
        if not pushable:
            return
        if not check_production_write_allowed(self.env, soft=True):
            return
        try:
            client = SAPB1Client.from_env(self.env)
        except Exception as exc:  # not configured -> skip silently
            _logger.warning("B1 absence push skipped: %s", exc)
            return
        try:
            with client as b1:
                for leave in pushable:
                    try:
                        leave._push_one_absence(b1)
                        leave.b1_absence_synced = fields.Datetime.now()
                    except Exception as exc:
                        _logger.exception(
                            "B1 absence push failed for leave %s: %s", leave.id, exc
                        )
        except Exception as exc:
            _logger.warning("B1 absence push connection failed: %s", exc)

    def _push_one_absence(self, b1):
        self.ensure_one()
        bid = self.employee_id.b1_employee_id
        approver = self.first_approver_id.name or self.env.user.name
        reason = self.holiday_status_id.name or "Leave"
        if self.name:
            reason = f"{reason}: {self.name}"
        line = {
            "FromDate": f"{self.request_date_from}T00:00:00Z",
            "ToDate": f"{self.request_date_to}T00:00:00Z",
            "Reason": reason[:100],
            "ApprovedBy": (approver or "")[:100],
        }
        # B1 replaces the child collection on PATCH -> send existing + new.
        cur = b1.get(
            f"EmployeesInfo({bid})", params={"$select": "EmployeeAbsenceInfoLines"}
        )
        existing = (cur or {}).get("EmployeeAbsenceInfoLines", []) or []
        clean = [
            {k: ln.get(k) for k in ("FromDate", "ToDate", "Reason", "ApprovedBy")}
            for ln in existing
        ]
        b1.patch(
            f"EmployeesInfo({bid})",
            json={"EmployeeAbsenceInfoLines": clean + [line]},
        )

    def _cron_push_absences(self):
        """Retry approved leaves not yet logged to B1."""
        pending = self.search([
            ("state", "=", "validate"),
            ("b1_absence_synced", "=", False),
            ("employee_id.b1_employee_id", "!=", False),
        ])
        if pending:
            pending._push_absence_to_b1()
