import json
import logging

from odoo import api, models

_logger = logging.getLogger(__name__)

# Mirrors XENHR's Auditable trait default: ['password', 'remember_token', 'updated_at']
_ALWAYS_EXCLUDED = {"write_date", "write_uid", "__last_update"}


def _jsonable(value):
    if isinstance(value, models.BaseModel):
        return value.ids if len(value) != 1 else value.id
    if hasattr(value, "isoformat"):
        return value.isoformat()
    return value


class XenoAuditMixin(models.AbstractModel):
    _name = "xeno.audit.mixin"
    _description = "Adds automatic create/write/unlink audit logging (see xeno_audit_log)"

    # Override per-model to exclude extra fields (e.g. sensitive ones),
    # mirroring XENHR's per-model $auditExclude property.
    _audit_exclude_fields = ()

    def _xeno_audit_excluded_fields(self):
        return _ALWAYS_EXCLUDED | set(self._audit_exclude_fields)

    def _xeno_audit_stored_field_names(self):
        return {
            name for name, field in self._fields.items()
            if field.store and field.type not in ("one2many", "many2many")
            and name not in self._xeno_audit_excluded_fields()
        }

    def _xeno_audit_ip(self):
        try:
            from odoo.http import request
            return request.httprequest.remote_addr if request else False
        except RuntimeError:
            return False

    def _xeno_audit_write(self, action, old_values, new_values, res_id, display_name):
        try:
            self.env["xeno.audit.log"].sudo().create({
                "model_name": self._name,
                "res_id": res_id,
                "record_display_name": display_name or "",
                "action": action,
                "old_values": json.dumps(old_values, default=str) if old_values else False,
                "new_values": json.dumps(new_values, default=str) if new_values else False,
                "user_id": self.env.uid,
                "ip_address": self._xeno_audit_ip(),
            })
        except Exception:
            # Never let audit logging break the underlying operation.
            _logger.exception("xeno_audit_log: failed to write audit entry for %s", self._name)

    @api.model_create_multi
    def create(self, vals_list):
        records = super().create(vals_list)
        stored = self._xeno_audit_stored_field_names()
        for record in records:
            record_sudo = record.sudo()
            new_values = {f: _jsonable(record_sudo[f]) for f in stored}
            record._xeno_audit_write("created", None, new_values, record.id, record_sudo.display_name)
        return records

    def write(self, vals):
        stored = self._xeno_audit_stored_field_names()
        changed_fields = [f for f in vals if f in stored]
        pre_state = {}
        if changed_fields:
            for record in self:
                record_sudo = record.sudo()
                pre_state[record.id] = (
                    {f: _jsonable(record_sudo[f]) for f in changed_fields},
                    record_sudo.display_name,
                )
        result = super().write(vals)
        if changed_fields:
            for record in self:
                record_sudo = record.sudo()
                old_values, display_name = pre_state.get(record.id, ({}, ""))
                new_values = {f: _jsonable(record_sudo[f]) for f in changed_fields}
                if old_values != new_values:
                    record._xeno_audit_write(
                        "updated", old_values, new_values, record.id,
                        display_name or record_sudo.display_name,
                    )
        return result

    def unlink(self):
        stored = self._xeno_audit_stored_field_names()
        pre_state = [
            (record.id, {f: _jsonable(record.sudo()[f]) for f in stored}, record.sudo().display_name)
            for record in self
        ]
        result = super().unlink()
        for res_id, old_values, display_name in pre_state:
            self._xeno_audit_write("deleted", old_values, None, res_id, display_name)
        return result
