# -*- coding: utf-8 -*-
# Part of ds_amcs_extract. See LICENSE file for full copyright and licensing details.

"""
HR Expense Extension
--------------------
Extends hr.expense with OCR processing capabilities using the OCR mixin.
"""

import json
import logging
import uuid
from odoo import _, api, fields, models
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)

# Cache for synchronous OCR results (server processes during parse, no get_result endpoint)
_ocr_parse_cache = {}


class HrExpense(models.Model):
    """
    Override hr.expense to use custom OCR service.
    """

    _inherit = "hr.expense"

    # OCR fields (consolidated from ocr.mixin)
    ocr_extract_state = fields.Selection(
        [
            ("no_extract_requested", "No Extract Requested"),
            ("waiting_extraction", "Waiting Extraction"),
            ("extract_not_ready", "Extract Not Ready"),
            ("waiting_validation", "Waiting Validation"),
            ("to_validate", "To Validate"),
            ("done", "Done"),
        ],
        string="OCR State",
        default="no_extract_requested",
        tracking=True,
    )
    ocr_extract_request_uuid = fields.Char(string="Request UUID", copy=False)
    ocr_extract_document_id = fields.Many2one("ir.attachment", string="OCR Document", copy=False)
    ocr_extract_last_check = fields.Datetime(string="Last Status Check", copy=False)
    ocr_extract_result_json = fields.Text(string="OCR Result", copy=False)
    ocr_extract_validated_on = fields.Datetime(string="Validated On", copy=False)

    @api.constrains("ocr_extract_state")
    def _check_ocr_extract_state_transition(self):
        """Validate that state transitions follow the defined order."""
        VALID_TRANSITIONS = {
            "no_extract_requested": ["no_extract_requested", "waiting_extraction"],
            "waiting_extraction": ["waiting_extraction", "extract_not_ready", "waiting_validation"],
            "extract_not_ready": ["extract_not_ready", "waiting_validation"],
            "waiting_validation": ["waiting_validation", "to_validate"],
            "to_validate": ["to_validate", "done"],
            "done": ["done"],
        }
        for record in self:
            if not record._origin.ocr_extract_state:
                continue
            current_state = record._origin.ocr_extract_state
            if current_state not in VALID_TRANSITIONS:
                continue
            allowed = VALID_TRANSITIONS[current_state]
            if record.ocr_extract_state not in allowed:
                raise UserError(
                    _('Invalid state transition from "%s" to "%s"')
                    % (current_state, record.ocr_extract_state)
                )

    def _get_nested_value(self, data, key, default=None):
        if not isinstance(data, dict):
            return default
        value = data.get(key, default)
        if isinstance(value, dict):
            if "selected_value" in value:
                return value.get("selected_value", {}).get("content", default)
            if "value" in value:
                return value.get("value", default)
        return value if value is not None else default

    def _get_ocr_client(self):
        from .ocr_client import OCRClientFactory

        return OCRClientFactory.create(self.env)

    def send_for_ocr_extraction(self):
        """Send the document for OCR processing."""
        self.ensure_one()
        if self.ocr_extract_state != "no_extract_requested":
            raise UserError(_("Document already sent for extraction"))
        doc_b64 = self._get_document_base64()
        if not doc_b64:
            raise UserError(_("No document attached to this record"))
        try:
            client = self._get_ocr_client()
            result = client.parse(doc_b64, service_name=self._get_ocr_service_name())
            self.write({
                "ocr_extract_state": "waiting_extraction",
                "ocr_extract_request_uuid": result.get("document_token", str(uuid.uuid4()) if "uuid" not in locals() else str(uuid.uuid4())),
                "ocr_extract_last_check": fields.Datetime.now(),
            })
            if result.get("status") == "success":
                extraction_data = result.get("result") or result.get("results", [{}])[0]
                self.ocr_extract_result_json = json.dumps(extraction_data)
                self.ocr_extract_state = "waiting_validation"
                self._process_ocr_result(extraction_data)
        except Exception as e:
            _logger.error("Failed to send for OCR: %s", str(e))
            raise UserError(_("Failed to send document for OCR: %s") % str(e))
        return True

    def check_ocr_status(self):
        """Check the status of a pending OCR extraction.
        
        Returns the standard format expected by extract.mixin JS:
        [(state, error_message)]
        """
        for expense in self:
            if expense.ocr_extract_state not in ["waiting_extraction", "extract_not_ready", "waiting_validation"]:
                continue
            # Skip waiting_validation if amount is already filled (safety net)
            if expense.ocr_extract_state == "waiting_validation" and expense.total_amount_currency:
                continue
            if not expense.ocr_extract_request_uuid:
                continue
            try:
                doc_uuid = expense.ocr_extract_request_uuid
                # Check memory cache first (fast, same worker)
                cached = _ocr_parse_cache.get(doc_uuid) if doc_uuid else None
                if not cached and doc_uuid:
                    # Check database cache (shared across workers)
                    cached = expense._ocr_db_cache_get(doc_uuid)
                    if cached:
                        # Promote to memory cache
                        _ocr_parse_cache[doc_uuid] = cached
                if cached:
                    result = {
                        "status": "ready",
                        "result": cached.get("extraction_data", cached["result"]),
                    }
                else:
                    client = expense._get_ocr_client()
                    try:
                        result = client.get_result(doc_uuid)
                    except Exception:
                        _logger.warning(
                            "check_ocr_status: get_result failed for %s, treating as pending",
                            doc_uuid,
                        )
                        result = {"status": "pending"}
                expense.ocr_extract_last_check = fields.Datetime.now()
                if result.get("status") in ("ready", "success"):
                    extraction_data = result.get("result", {})
                    expense.ocr_extract_result_json = json.dumps(extraction_data)
                    expense.ocr_extract_state = "waiting_validation"
                    if expense.extract_state != "waiting_validation":
                        expense.extract_state = "waiting_validation"
                    expense._process_ocr_result(extraction_data)
                elif result.get("status") == "pending":
                    if expense.ocr_extract_state == "waiting_extraction":
                        expense.ocr_extract_state = "extract_not_ready"
                        if expense.extract_state != "extract_not_ready":
                            expense.extract_state = "extract_not_ready"
            except Exception as e:
                _logger.exception("Error checking OCR status for %s: %s", expense.id, str(e))
        
        return [
            (rec.ocr_extract_state, rec.extract_error_message if hasattr(rec, 'extract_error_message') else None)
            for rec in self
        ]

    def _get_document_base64(self):
        """Get document data from expense attachment as base64 string."""
        attachment = self.message_main_attachment_id
        if attachment:
            datas = attachment.datas
            if isinstance(datas, bytes):
                return datas.decode("utf-8")
            return datas

        for att in self.attachment_ids:
            datas = att.datas
            if isinstance(datas, bytes):
                return datas.decode("utf-8")
            return datas

        return None

    def _get_ocr_service_name(self):
        """Get the OCR service name for expenses."""
        return "expense_ocr"

    @api.model_create_multi
    def create(self, vals_list):
        """Override create to set default employee if not provided."""
        for vals in vals_list:
            if not vals.get("employee_id"):
                employee = self.env["hr.employee"].search(
                    [("user_id", "=", self.env.uid)], limit=1
                )
                if employee:
                    vals["employee_id"] = employee.id
                else:
                    _logger.warning(
                        "OCR Expense: No employee found for user %s", self.env.uid
                    )

        return super(HrExpense, self).create(vals_list)

    def write(self, vals):
        """Sync ocr_extract_state with extract_state (from extract.mixin)."""
        if "ocr_extract_state" in vals and "extract_state" not in vals:
            vals["extract_state"] = vals["ocr_extract_state"]
        if "extract_state" in vals and "ocr_extract_state" not in vals:
            vals["ocr_extract_state"] = vals["extract_state"]
        if "ocr_extract_request_uuid" in vals and "extract_document_uuid" not in vals:
            vals["extract_document_uuid"] = vals["ocr_extract_request_uuid"]
        if "extract_document_uuid" in vals and "ocr_extract_request_uuid" not in vals:
            vals["ocr_extract_request_uuid"] = vals["extract_document_uuid"]
        return super().write(vals)

    # ---
    # CALLBACK: sync ocr_extract_state after extract.mixin sets extract_state
    # ---
    def _upload_to_extract_success_callback(self):
        """Called after _upload_to_extract succeeds. Syncs our fields."""
        super()._upload_to_extract_success_callback()
        for expense in self:
            if expense.extract_state and expense.extract_state != expense.ocr_extract_state:
                expense.write({
                    "ocr_extract_state": expense.extract_state,
                    "ocr_extract_request_uuid": expense.extract_document_uuid,
                    "ocr_extract_last_check": fields.Datetime.now(),
                })

    def _process_ocr_result(self, result):
        """
        Process OCR result and apply to expense.
        """
        _logger.info("Processing OCR result for expense %s", self.id)

        if isinstance(result, dict) and "results" in result:
            if isinstance(result["results"], list) and result["results"]:
                result = result["results"][0]

        # Custom OCR server nests extracted fields under "fields"
        if isinstance(result, dict) and "fields" in result and isinstance(result["fields"], dict):
            for k, v in result["fields"].items():
                if k not in result:
                    result[k] = v

        for expense in self:
            vals = {}

            date_val = self._get_nested_value(result, "date")
            if date_val and not expense.date:
                vals["date"] = date_val

            total_val = self._get_nested_value(result, "total")
            if not total_val:
                total_val = self._get_nested_value(result, "total_ttc") or self._get_nested_value(result, "total_ht")
            if total_val and not expense.total_amount_currency:
                try:
                    # Handle both comma and dot as decimal separator
                    cleaned = str(total_val).replace(",", ".").replace(" ", "")
                    vals["total_amount_currency"] = float(cleaned)
                except (ValueError, TypeError):
                    pass

            supplier = self._get_nested_value(
                result, "supplier_name"
            ) or self._get_nested_value(result, "supplier")
            ref = self._get_nested_value(result, "invoice_id")

            if not expense.name or expense.name == "/":
                if supplier and ref:
                    vals["name"] = f"{supplier} - {ref}"
                elif supplier:
                    vals["name"] = supplier
                elif ref:
                    vals["name"] = ref

            currency_code = self._get_nested_value(result, "currency")
            if currency_code and not expense.currency_id:
                currency = self.env["res.currency"].search(
                    [("name", "=", currency_code)], limit=1
                )
                if currency:
                    vals["currency_id"] = currency.id

            if not expense.employee_id:
                employee = self.env["hr.employee"].search(
                    [("user_id", "=", self.env.uid)], limit=1
                )
                if employee:
                    vals["employee_id"] = employee.id

            if vals:
                _logger.info(
                    "OCR Expense Mapping: Writing to expense %s: %s", expense.id, vals
                )
                expense.write(vals)

    def _save_form(self, *args, **kwargs):
        """Override _save_form for expense OCR handling."""
        res = super()._save_form(*args, **kwargs)

        try:
            ocr_results = args[0] if args else kwargs.get("ocr_results", {})
            if not ocr_results:
                return res

            self._process_ocr_result(ocr_results)

        except Exception as e:
            _logger.error("OCR Expense: Failed _save_form processing: %s", e)

        return res

    @api.model
    def _contact_iap_extract(self, pathinfo, params):
        """
        Redirect standard Odoo OCR calls to the custom local server.
        """
        client = self._get_ocr_client()
        if pathinfo == "parse":
            if not params.get("documents"):
                return {"status": "error_no_document"}
            doc_b64 = params["documents"][0]
            user_infos = {
                "company_name": self.env.company.name if self.env.company else "",
                "currency": self.env.company.currency_id.name if self.env.company else "",
            }
            result = client.parse(doc_b64, service_name=self._get_ocr_service_name(), user_infos=user_infos)
            if result.get("status") == "success":
                # Generate a UUID if server doesn't return one (it has no get_result endpoint)
                doc_uuid = result.get("uuid") or result.get("document_token") or str(uuid.uuid4())
                # Cache in memory (fast, per-worker) AND in database (shared across workers)
                cache_entry = {
                    "done": True,
                    "result": result,
                    "extraction_data": result.get("result", {}),
                }
                _ocr_parse_cache[doc_uuid] = cache_entry
                self._ocr_db_cache_set(doc_uuid, cache_entry)
                # Ensure uuid is in response for standard mixin flow
                result["uuid"] = doc_uuid
                return result
            return {"status": "error", "message": result.get("message", "Unknown error")}
        elif pathinfo == "get_result":
            # Accept both 'uuid' (from our code) and 'document_token' (from standard _check_ocr_status)
            doc_uuid = params.get("uuid") or params.get("document_token", "")
            if not doc_uuid:
                return {"status": "error_no_uuid"}
            # Check memory cache first (fast)
            cached = _ocr_parse_cache.get(doc_uuid)
            if not cached:
                # Check database cache (shared across workers)
                cached = self._ocr_db_cache_get(doc_uuid)
                if cached:
                    # Promote to memory cache
                    _ocr_parse_cache[doc_uuid] = cached
            if cached:
                _logger.debug("get_result: returning cached result for %s", doc_uuid)
                # Transform extraction data to format expected by _fill_document_with_results
                raw_data = cached.get("extraction_data", cached["result"])
                fields_data = raw_data.get("fields", {}) if isinstance(raw_data, dict) else {}
                transformed = {}
                # Map our field names to standard _get_ocr_selected_value format
                raw_total = fields_data.get("total_ttc") or fields_data.get("total_ht") or fields_data.get("total")
                if raw_total:
                    # Sanitize decimal separator (handle both comma and dot)
                    try:
                        cleaned = str(raw_total).replace(",", ".").replace(" ", "")
                        transformed["total"] = {"selected_value": {"content": float(cleaned)}}
                    except (ValueError, TypeError):
                        transformed["total"] = {"selected_value": {"content": raw_total}}
                description = fields_data.get("supplier_name") or fields_data.get("supplier", "")
                if description:
                    transformed["description"] = {"selected_value": {"content": description}}
                raw_date = fields_data.get("date")
                if raw_date:
                    transformed["date"] = {"selected_value": {"content": raw_date}}
                raw_currency = fields_data.get("currency")
                if raw_currency:
                    transformed["currency"] = {"selected_value": {"content": raw_currency}}
                return {
                    "status": "success",
                    "uuid": doc_uuid,
                    "result": transformed,
                }
            # Fallback: try server (may fail)
            _logger.warning("get_result: no cache entry for %s, trying server", doc_uuid)
            try:
                return client.get_result(doc_uuid)
            except Exception as e:
                _logger.warning("get_result: server failed for %s: %s", doc_uuid, e)
                return {"status": "pending", "uuid": doc_uuid}
        return {"status": "success"}

    @api.model
    def _ocr_db_cache_set(self, doc_uuid, cache_entry):
        """Store OCR cache entry in database (shared across workers)."""
        try:
            self.env['ir.config_parameter'].sudo().set_param(
                'ocr.cache.' + doc_uuid,
                json.dumps(cache_entry, default=str)
            )
        except Exception as e:
            _logger.warning("Failed to store OCR DB cache for %s: %s", doc_uuid, e)

    @api.model
    def _ocr_db_cache_get(self, doc_uuid):
        """Retrieve OCR cache entry from database."""
        try:
            cached_json = self.env['ir.config_parameter'].sudo().get_param(
                'ocr.cache.' + doc_uuid, ''
            )
            if cached_json:
                return json.loads(cached_json)
        except Exception as e:
            _logger.warning("Failed to read OCR DB cache for %s: %s", doc_uuid, e)
        return None

    @api.model
    def check_all_expense_status(self):
        """
        Cron job method to check status of pending expense extractions.
        """
        expenses = self.search(
            [
                ("ocr_extract_state", "in", ["waiting_extraction", "extract_not_ready"]),
            ],
            limit=20,
        )

        if not expenses:
            return 0

        _logger.info("OCR cron: checking %d expense(s)", len(expenses))

        processed = 0
        for expense in expenses:
            try:
                if expense.check_ocr_status():
                    processed += 1
            except Exception as e:
                _logger.exception(
                    "OCR cron: error checking expense %s: %s", expense.id, e
                )

        _logger.info("OCR cron: processed %d expense extractions", processed)
        return processed
