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

"""
OCR Service Client
-------------------
JSON-RPC 2.0 client for communication with the custom OCR server.
"""

import json
import logging
import uuid
import base64
import requests
from typing import Optional, Dict, Any

from odoo import _, api
from odoo.exceptions import UserError

from .exceptions import (
    OCRConnectionError,
    OCRTimeoutError,
    OCRAuthenticationError,
    OCRProcessingError,
    OCRQuotaExceededError,
    OCRDocumentTooLargeError,
    OCRUnsupportedFormatError,
)

_logger = logging.getLogger(__name__)

OCR_VERSION = 123
MAX_FILE_SIZE_MB = 20
SUPPORTED_MIME_TYPES = {
    "application/pdf",
    "image/png",
    "image/jpeg",
    "image/tiff",
}


class OCRClient:
    """
    JSON-RPC 2.0 client for OCR server communication.

    Usage:
        client = OCRClient(env)
        result = client.parse(document_base64, user_infos)
    """

    def __init__(self, env):
        self.env = env
        self._endpoint = None
        self._token = None

    @property
    def endpoint(self) -> str:
        """Get OCR server endpoint from configuration."""
        if self._endpoint is None:
            # Check for internal endpoint first for server-to-server calls
            endpoint = self._get_config_param("iap_extract_endpoint_internal", "")
            if not endpoint:
                endpoint = self._get_config_param("iap_extract_endpoint", "")
            
            if not endpoint:
                raise UserError(_("OCR endpoint is not configured. Please check System Parameters."))
            self._endpoint = endpoint.rstrip("/")
        return self._endpoint

    @property
    def token(self) -> Optional[str]:
        """Get IAP authentication token."""
        if self._token is None:
            self._token = self._get_iap_token()
        return self._token

    def _get_config_param(self, key: str, default: str = "") -> str:
        """Retrieve configuration parameter."""
        return self.env["ir.config_parameter"].sudo().get_param(key, default)

    def _get_iap_token(self) -> Optional[str]:
        """Retrieve IAP token from configuration or IAP account."""
        # Prioritize IAP account token
        account = self.env["iap.account"].sudo().get_ocr_account()
        if account and account.account_token and account.account_token != "dummy_token":
            return account.account_token.strip()
 
        # Fallback to config param
        token = self._get_config_param("ocr_api_token")
        if token and token.strip() != "dummy_token":
            return token.strip()
 
        _logger.warning("OCR: No IAP token found") # Alerte sur l'absence de token d'authentification OCR
        return None

    def _get_dbuuid(self) -> str:
        """Get database UUID."""
        return self._get_config_param("database.uuid", "")

    def _detect_mimetype(self, data: bytes) -> str:
        """Detect MIME type from file header."""
        if data.startswith(b"%PDF"):
            return "application/pdf"
        elif data.startswith(b"\xff\xd8\xff"):
            return "image/jpeg"
        elif data.startswith(b"\x89PNG\r\n\x1a\n"):
            return "image/png"
        elif data.startswith(b"II") or data.startswith(b"MM"):
            return "image/tiff"
        return "application/pdf"

    def _validate_document(self, doc_base64: str) -> bytes:
        """Validate document size and format."""
        try:
            data = base64.b64decode(doc_base64)
        except Exception as e:
            raise OCRProcessingError(_("Invalid base64 document encoding: %s") % str(e))

        size_mb = len(data) / (1024 * 1024)
        if size_mb > MAX_FILE_SIZE_MB:
            raise OCRDocumentTooLargeError(
                _("Document size (%.1f MB) exceeds limit of %d MB")
                % (size_mb, MAX_FILE_SIZE_MB)
            )

        mimetype = self._detect_mimetype(data)
        if mimetype not in SUPPORTED_MIME_TYPES:
            raise OCRUnsupportedFormatError(
                _("Unsupported document format: %s. Supported: PDF, PNG, JPG, TIFF")
                % mimetype
            )

        return data

    def _call_jsonrpc(
        self, method: str, params: Dict[str, Any], timeout: int = 300
    ) -> Dict[str, Any]:
        """
        Make a JSON-RPC 2.0 call to the OCR server.
 
        Args:
            method: The method to call (e.g., 'parse', 'get_result', 'validate')
            params: The parameters for the method call
            timeout: Request timeout in seconds (default: 300)
 
        Returns:
            The result from the server
 
        Raises:
            OCRConnectionError: If connection fails
            OCRTimeoutError: If request times out
            OCRAuthenticationError: If authentication fails
            OCRProcessingError: If server returns an error
        """
        url = f"{self.endpoint}/ocr/1/process"
 
        payload = {
            "jsonrpc": "2.0",
            "method": "call",
            "params": {
                "dbuuid": self._get_dbuuid(),
                "account_token": self.token,
                **params,
            },
            "id": str(uuid.uuid4()),
        }
 
        try:
            _logger.debug("OCR Request: %s %s", method, url) # Trace la requête envoyée au serveur OCR
            # Disable SSL verification for internal/Docker calls if using HTTP or .local
            verify = not (url.startswith("http://") or ".local" in url)
            response = requests.post(
                url,
                json=payload,
                timeout=timeout,
                headers={"Content-Type": "application/json"},
                verify=verify,
            )
            response.raise_for_status()
 
            result = response.json()
 
            if "error" in result:
                error = result["error"]
                error_code = error.get("code", 0)
                error_msg = error.get("message", "Unknown error")
 
                if error_code == -32001:
                    raise OCRAuthenticationError(
                        _("OCR authentication failed: %s") % error_msg
                    )
                elif error_code == -32002:
                    raise OCRQuotaExceededError(_("OCR quota exceeded: %s") % error_msg)
                else:
                    raise OCRProcessingError(_("OCR server error: %s") % error_msg)
 
            return result.get("result", {})
 
        except requests.Timeout:
            raise OCRTimeoutError(
                _("OCR server request timed out after %d seconds") % timeout
            )
        except requests.RequestException as e:
            raise OCRConnectionError(_("Failed to connect to OCR server: %s") % str(e))

    def parse(
        self,
        doc_base64: str,
        service_name: str = "invoice_ocr",
        user_infos: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """
        Send a document for OCR processing.
 
        Args:
            doc_base64: Base64-encoded document content
            service_name: Service type ('invoice_ocr' or 'receipt_ocr')
            user_infos: Optional user context information
 
        Returns:
            Dictionary with extraction results
        """
        if not self.token:
            raise OCRAuthenticationError(_("No OCR authentication token configured"))
 
        self._validate_document(doc_base64)
 
        doc_data = base64.b64decode(doc_base64)
        mimetype = self._detect_mimetype(doc_data)
 
        params = {
            "service_name": service_name,
            "document": doc_base64,
            "mime_type": mimetype,
            "version": OCR_VERSION,
        }
 
        if user_infos:
            params["user_infos"] = user_infos
 
        result = self._call_jsonrpc("parse", params)
 
        while isinstance(result, list):
            result = result[0] if result else {}
 
        if isinstance(result, dict) and result.get("status") == "success":
            result["document_token"] = str(uuid.uuid4())
 
            return result
 
        _logger.error("OCR parse failed. Raw result: %s", result) # Log l'échec du parsing OCR avec le résultat brut
        error_msg = result.get("message") if isinstance(result, dict) else str(result)
        raise OCRProcessingError(f"OCR processing failed: {error_msg}")

    def get_result(self, request_uuid: str) -> Dict[str, Any]:
        """
        Get OCR result for a previously submitted document.

        Args:
            request_uuid: The UUID of the OCR request

        Returns:
            Dictionary with extraction results
        """
        if not self.token:
            raise OCRAuthenticationError(_("No OCR authentication token configured"))

        params = {
            "uuid": request_uuid,
        }

        return self._call_jsonrpc("get_result", params)

    def validate(
        self, request_uuid: str, validated_values: Dict[str, Any]
    ) -> Dict[str, Any]:
        """
        Send validated values back to OCR server for ML feedback.

        Args:
            request_uuid: The UUID of the OCR request
            validated_values: The user-confirmed extracted values

        Returns:
            Dictionary with acknowledgment status
        """
        if not self.token:
            raise OCRAuthenticationError(_("No OCR authentication token configured"))

        params = {
            "uuid": request_uuid,
            "validated_values": validated_values,
        }

        return self._call_jsonrpc("validate", params)

    def _update_credits(self, credits: int):
        """Update IAP account credits balance."""
        account = (
            self.env["iap.account"]
            .sudo()
            .search(
                [
                    ("account_token", "!=", False),
                    (
                        "service_name",
                        "not in",
                        ["partner_autocomplete", "iap_partner_autocomplete"],
                    ),
                ],
                order="service_name desc",
                limit=1,
            )
        )
 
        if account:
            try:
                with self.env.cr.savepoint():
                    account.sudo().write({"ocr_credits_balance": int(credits)})
            except Exception as e:
                _logger.warning("Failed to update OCR credits: %s", str(e)) # Log l'erreur lors de la mise à jour du solde de crédits



class OCRClientFactory:
    """Factory for creating OCR client instances."""

    @staticmethod
    def create(env) -> OCRClient:
        """Create a new OCR client instance."""
        return OCRClient(env)
