# Part of Odoo. See LICENSE file for full copyright and licensing details.

import json
import logging

import requests

from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError

from odoo.addons.fusion_clover import const
from odoo.addons.fusion_clover import utils as clover_utils

_logger = logging.getLogger(__name__)


class PaymentProvider(models.Model):
    _inherit = 'payment.provider'

    code = fields.Selection(
        selection_add=[('clover', "Clover")],
        ondelete={'clover': 'set default'},
    )
    clover_api_key = fields.Char(
        string="Ecommerce Private Token",
        help="The private token from Clover's Ecommerce API Tokens page. "
             "Used for online charges and refunds (scl.clover.com).",
        required_if_provider='clover',
        copy=False,
        groups='base.group_system',
    )
    clover_merchant_id = fields.Char(
        string="Merchant ID",
        help="The Clover merchant ID for this business.",
        required_if_provider='clover',
        copy=False,
    )
    clover_rest_api_token = fields.Char(
        string="REST API Token",
        help="The merchant's REST API token from the Clover dashboard "
             "(Setup > API Tokens). Used for Platform API (devices, orders) "
             "and terminal payments (Cloud Pay Display). This is different "
             "from the Ecommerce API token.",
        copy=False,
        groups='base.group_system',
    )
    clover_app_id = fields.Char(
        string="App ID (Client ID)",
        help="The Clover App ID (client_id) from the developer dashboard. "
             "Used for OAuth2 merchant authorization flow.",
        copy=False,
    )
    clover_app_secret = fields.Char(
        string="App Secret",
        help="The Clover App Secret (client_secret) from the developer dashboard.",
        copy=False,
        groups='base.group_system',
    )
    clover_public_key = fields.Char(
        string="Public API Key (PAKMS)",
        help="The public token from Clover's Ecommerce API Tokens page. "
             "Used for client-side tokenization. Safe to expose in the browser.",
        copy=False,
    )
    clover_default_terminal_id = fields.Many2one(
        'clover.terminal',
        string="Default Terminal",
        help="The default Clover terminal used for in-store payment collection. "
             "Staff can override this per transaction.",
        domain="[('provider_id', '=', id), ('active', '=', True)]",
    )

    # === COMPUTE METHODS === #

    def _compute_feature_support_fields(self):
        """Override of `payment` to enable additional features."""
        super()._compute_feature_support_fields()
        self.filtered(lambda p: p.code == 'clover').update({
            'support_manual_capture': 'full_only',
            'support_refund': 'partial',
            'support_tokenization': True,
        })

    # === CRUD METHODS === #

    def _get_default_payment_method_codes(self):
        """Override of `payment` to return the default payment method codes."""
        self.ensure_one()
        if self.code != 'clover':
            return super()._get_default_payment_method_codes()
        return const.DEFAULT_PAYMENT_METHOD_CODES

    # === BUSINESS METHODS - API REQUESTS === #

    def _clover_make_ecom_request(self, method, endpoint, payload=None, params=None):
        """Make an authenticated API request to the Clover Ecommerce API.

        :param str method: HTTP method (GET, POST, PUT, DELETE).
        :param str endpoint: The API endpoint path (e.g., 'v1/charges').
        :param dict payload: The JSON request body (optional).
        :param dict params: The query parameters (optional).
        :return: The parsed JSON response.
        :rtype: dict
        :raises ValidationError: If the API request fails.
        """
        self.ensure_one()

        is_test = self.state == 'test'
        url = clover_utils.build_ecom_url(endpoint, is_test=is_test)

        idempotency_key = clover_utils.generate_idempotency_key()
        headers = clover_utils.build_ecom_headers(
            self.clover_api_key, idempotency_key=idempotency_key,
        )

        _logger.info(
            "Clover Ecom API %s request to %s (idempotency=%s)",
            method, url, idempotency_key,
        )

        try:
            response = requests.request(
                method,
                url,
                json=payload,
                params=params,
                headers=headers,
                timeout=60,
            )
        except requests.exceptions.RequestException as e:
            _logger.error("Clover Ecom API request failed: %s", e)
            raise ValidationError(_("Communication with Clover failed: %s", e))

        if response.status_code in (202, 204):
            return {}

        try:
            result = response.json()
        except ValueError:
            if response.status_code < 400:
                return {}
            _logger.error("Clover returned non-JSON response: %s", response.text[:500])
            raise ValidationError(_("Clover returned an invalid response."))

        if response.status_code >= 400:
            error = result.get('error', {})
            error_msg = error.get('message', '') if isinstance(error, dict) else str(error)
            error_code = error.get('code', '') if isinstance(error, dict) else ''
            _logger.error(
                "Clover Ecom API error %s: %s (code=%s)\n"
                "  URL: %s %s\n  Payload: %s\n  Response: %s",
                response.status_code, error_msg, error_code,
                method, url,
                json.dumps(payload)[:2000] if payload else 'None',
                response.text[:2000],
            )
            raise ValidationError(
                _("Clover API error (%(code)s): %(msg)s",
                  code=response.status_code, msg=error_msg or 'Unknown error')
            )

        return result

    def _clover_make_platform_request(self, method, endpoint, payload=None, params=None):
        """Make an authenticated request to the Clover Platform API.

        :param str method: HTTP method.
        :param str endpoint: The API endpoint path.
        :param dict payload: The JSON request body (optional).
        :param dict params: The query parameters (optional).
        :return: The parsed JSON response.
        :rtype: dict
        :raises ValidationError: If the API request fails.
        """
        self.ensure_one()

        is_test = self.state == 'test'
        url = clover_utils.build_platform_url(
            endpoint, merchant_id=self.clover_merchant_id, is_test=is_test,
        )

        # Platform API uses the REST API token, falling back to ecom key
        api_token = self.clover_rest_api_token or self.clover_api_key
        headers = clover_utils.build_ecom_headers(api_token)

        _logger.info("Clover Platform API %s request to %s", method, url)

        try:
            response = requests.request(
                method,
                url,
                json=payload,
                params=params,
                headers=headers,
                timeout=60,
            )
        except requests.exceptions.RequestException as e:
            _logger.error("Clover Platform API request failed: %s", e)
            raise ValidationError(_("Communication with Clover failed: %s", e))

        if response.status_code in (202, 204):
            return {}

        try:
            result = response.json()
        except ValueError:
            if response.status_code < 400:
                return {}
            raise ValidationError(_("Clover returned an invalid response."))

        if response.status_code >= 400:
            error_msg = result.get('message', result.get('error', 'Unknown error'))
            raise ValidationError(
                _("Clover API error (%(code)s): %(msg)s",
                  code=response.status_code, msg=error_msg)
            )

        return result

    # === BUSINESS METHODS - CHARGE / TOKENIZE === #

    def _clover_create_charge(self, source_token, amount, currency,
                              capture=True, description='', ecomind='ecom',
                              external_reference_id='', receipt_email='',
                              metadata=None):
        """Create a charge via the Clover Ecommerce API.

        :param str source_token: The Clover card token.
        :param float amount: The charge amount in major currency units.
        :param recordset currency: The currency record.
        :param bool capture: Whether to capture immediately.
        :param str description: Optional charge description.
        :param str ecomind: 'ecom' or 'moto'.
        :param str external_reference_id: External reference.
        :param str receipt_email: Email for receipt.
        :param dict metadata: Optional metadata.
        :return: The charge response dict.
        :rtype: dict
        """
        self.ensure_one()
        payload = clover_utils.build_charge_payload(
            amount=amount,
            currency=currency,
            source_token=source_token,
            capture=capture,
            description=description,
            ecomind=ecomind,
            external_reference_id=external_reference_id,
            receipt_email=receipt_email,
            metadata=metadata,
        )
        return self._clover_make_ecom_request('POST', 'v1/charges', payload=payload)

    def _clover_capture_charge(self, charge_id, amount=None, currency=None):
        """Capture a previously authorized charge.

        :param str charge_id: The Clover charge ID.
        :param float amount: Optional capture amount (for partial captures).
        :param recordset currency: Optional currency record.
        :return: The capture response dict.
        :rtype: dict
        """
        self.ensure_one()
        payload = {}
        if amount is not None and currency:
            payload['amount'] = clover_utils.format_clover_amount(amount, currency)
        return self._clover_make_ecom_request(
            'POST', f'v1/charges/{charge_id}/capture', payload=payload,
        )

    def _clover_create_refund(self, charge_id, amount=None, currency=None, reason=''):
        """Create a refund via the Clover Ecommerce API.

        :param str charge_id: The Clover charge ID to refund.
        :param float amount: Optional partial refund amount.
        :param recordset currency: Optional currency record.
        :param str reason: Optional reason.
        :return: The refund response dict.
        :rtype: dict
        """
        self.ensure_one()
        payload = clover_utils.build_refund_payload(
            charge_id=charge_id,
            amount=amount,
            currency=currency,
            reason=reason,
        )
        return self._clover_make_ecom_request('POST', 'v1/refunds', payload=payload)

    # === BUSINESS METHODS - NON-REFERENCED CREDIT === #

    def _clover_create_credit(self, amount, currency, description=''):
        """Issue a non-referenced credit (manual refund) via Clover Ecommerce API.

        This creates a credit without referencing an original charge.  Useful
        when the original transaction is too old for a referenced refund.

        Note: merchants must have manual refunds enabled by Clover support.

        :param float amount: The credit amount in major currency units.
        :param recordset currency: The currency record.
        :param str description: Optional description.
        :return: The credit response dict.
        :rtype: dict
        """
        self.ensure_one()
        minor_amount = clover_utils.format_clover_amount(amount, currency)
        payload = {
            'amount': minor_amount,
            'currency': currency.name.lower(),
        }
        if description:
            payload['description'] = description
        return self._clover_make_ecom_request('POST', 'v1/credits', payload=payload)

    # === BUSINESS METHODS - VERIFICATION === #

    def _clover_get_charge(self, charge_id):
        """Fetch a charge from the Clover Ecommerce API.

        :param str charge_id: The Clover charge ID.
        :return: The charge data dict.
        :rtype: dict
        """
        self.ensure_one()
        return self._clover_make_ecom_request('GET', f'v1/charges/{charge_id}')

    def _clover_verify_charge_not_reversed(self, charge_id):
        """Check that a charge has not already been fully refunded or voided.

        :param str charge_id: The Clover charge ID.
        :return: The charge data dict.
        :rtype: dict
        :raises UserError: If the charge is already refunded.
        """
        self.ensure_one()
        charge_data = self._clover_get_charge(charge_id)
        status = charge_data.get('status', '')
        if status == 'refunded':
            raise UserError(_(
                "This charge (%(charge_id)s) has already been fully refunded "
                "on Clover. A duplicate refund cannot be issued.",
                charge_id=charge_id,
            ))
        return charge_data

    # === BUSINESS METHODS - INLINE FORM === #

    def _clover_get_inline_form_values(self, amount, currency, partner_id, is_validation,
                                       payment_method_sudo=None, **kwargs):
        """Return serialized JSON of values needed for the inline payment form.

        :param float amount: The payment amount.
        :param recordset currency: The currency of the transaction.
        :param int partner_id: The partner ID.
        :param bool is_validation: Whether this is a validation operation.
        :param recordset payment_method_sudo: The sudoed payment method record.
        :return: The JSON-serialized inline form values.
        :rtype: str
        """
        self.ensure_one()

        partner = self.env['res.partner'].browse(partner_id).exists()
        minor_amount = clover_utils.format_clover_amount(amount, currency) if amount else 0

        inline_form_values = {
            'provider_id': self.id,
            'merchant_id': self.clover_merchant_id,
            'public_key': self.clover_public_key or '',
            'currency_name': currency.name if currency else 'USD',
            'minor_amount': minor_amount,
            'capture_method': 'manual' if self.capture_manually else 'automatic',
            'is_test': self.state == 'test',
            'billing_details': {
                'name': partner.name or '',
                'email': partner.email or '',
                'phone': partner.phone or '',
                'address': {
                    'line1': partner.street or '',
                    'line2': partner.street2 or '',
                    'city': partner.city or '',
                    'state': partner.state_id.code or '',
                    'country': partner.country_id.code or '',
                    'postal_code': partner.zip or '',
                },
            },
            'is_tokenization_required': (
                self.allow_tokenization
                and self._is_tokenization_required(**kwargs)
                and payment_method_sudo
                and payment_method_sudo.support_tokenization
            ),
        }

        ICP = self.env['ir.config_parameter'].sudo()
        surcharge_enabled = ICP.get_param(
            'fusion_clover.surcharge_enabled', 'False',
        ) == 'True'
        if surcharge_enabled:
            inline_form_values['surcharge'] = {
                'enabled': True,
                'visa': float(ICP.get_param('fusion_clover.surcharge_visa_rate', '0') or 0),
                'mastercard': float(ICP.get_param('fusion_clover.surcharge_mastercard_rate', '0') or 0),
                'amex': float(ICP.get_param('fusion_clover.surcharge_amex_rate', '0') or 0),
                'debit': float(ICP.get_param('fusion_clover.surcharge_debit_rate', '0') or 0),
                'other': float(ICP.get_param('fusion_clover.surcharge_other_rate', '0') or 0),
            }

        return json.dumps(inline_form_values)

    # === BUSINESS METHODS - TERMINAL (REST Pay Display Cloud API) === #

    def _clover_terminal_request(self, method, endpoint, serial_number=None,
                                  payload=None, params=None):
        """Make a request to the Clover REST Pay Display Cloud API.

        Sends commands to Clover terminals through Clover's cloud (Cloud Pay Display).

        :param str method: HTTP method (GET, POST).
        :param str endpoint: The API endpoint path (e.g., 'payments', 'device/ping').
        :param str serial_number: The device serial number (X-Clover-Device-Id).
        :param dict payload: The JSON request body (optional).
        :param dict params: The query parameters (optional).
        :return: The parsed JSON response.
        :rtype: dict
        :raises ValidationError: If the API request fails.
        """
        self.ensure_one()

        is_test = self.state == 'test'
        base_url = const.CONNECT_BASE_URL_TEST if is_test else const.CONNECT_BASE_URL
        url = f"{base_url}/{endpoint}"

        headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': f'Bearer {self.clover_rest_api_token or self.clover_api_key}',
            'X-POS-ID': 'FusionCloverOdoo',
        }
        if serial_number:
            headers['X-Clover-Device-Id'] = serial_number

        idempotency_key = clover_utils.generate_idempotency_key()
        headers['Idempotency-Key'] = idempotency_key

        _logger.info(
            "Clover Terminal API %s request to %s (device=%s)",
            method, url, serial_number or 'none',
        )

        try:
            response = requests.request(
                method,
                url,
                json=payload,
                params=params,
                headers=headers,
                timeout=120,
            )
        except requests.exceptions.RequestException as e:
            _logger.error("Clover Terminal API request failed: %s", e)
            raise ValidationError(_("Communication with Clover terminal failed: %s", e))

        if response.status_code in (202, 204):
            return {}

        try:
            result = response.json()
        except ValueError:
            if response.status_code < 400:
                return {}
            _logger.error("Clover Terminal returned non-JSON: %s", response.text[:500])
            raise ValidationError(_("Clover terminal returned an invalid response."))

        if response.status_code >= 400:
            error_msg = result.get('message', result.get('error', 'Unknown error'))
            _logger.error(
                "Clover Terminal API error %s: %s\n  URL: %s %s",
                response.status_code, error_msg, method, url,
            )
            raise ValidationError(
                _("Clover terminal error (%(code)s): %(msg)s",
                  code=response.status_code, msg=error_msg)
            )

        return result

    def _clover_get_merchant_devices(self):
        """Fetch all devices provisioned to the merchant from the Platform API.

        :return: List of device dicts with id, serial, name, model.
        :rtype: list[dict]
        """
        self.ensure_one()
        result = self._clover_make_platform_request('GET', 'devices')
        elements = result.get('elements', [])
        return [
            {
                'id': d.get('id', ''),
                'serial': d.get('serial', ''),
                'name': d.get('name', d.get('productName', 'Clover Device')),
                'model': d.get('model', d.get('productName', '')),
            }
            for d in elements
            if d.get('serial')
        ]

    def action_sync_terminals(self):
        """Sync terminals from the Clover Platform API."""
        self.ensure_one()
        if self.code != 'clover':
            return

        try:
            devices = self._clover_get_merchant_devices()
        except (ValidationError, UserError) as e:
            return self._clover_notification(
                _("Failed to fetch devices: %(error)s", error=str(e)),
                'danger',
            )

        if not devices:
            return self._clover_notification(
                _("No devices found for this merchant."),
                'warning',
            )

        Terminal = self.env['clover.terminal'].sudo()
        created = 0
        updated = 0

        for device in devices:
            serial = device['serial']
            existing = Terminal.search([
                ('serial_number', '=', serial),
                ('provider_id', '=', self.id),
            ], limit=1)

            if existing:
                # Only update metadata; don't overwrite user-set name
                vals = {
                    'device_id': device['id'],
                    'model_name': device['model'],
                    'clover_device_name': device['name'],
                }
                existing.write(vals)
                updated += 1
            else:
                Terminal.create({
                    'name': device['name'],
                    'clover_device_name': device['name'],
                    'serial_number': serial,
                    'device_id': device['id'],
                    'model_name': device['model'],
                    'provider_id': self.id,
                })
                created += 1

        return self._clover_notification(
            _("Sync complete: %(created)s created, %(updated)s updated.",
              created=created, updated=updated),
            'success',
        )

    # === ACTION METHODS === #

    def action_clover_test_connection(self):
        """Test the connection to Clover by fetching merchant info.

        :return: A notification action with the result.
        :rtype: dict
        """
        self.ensure_one()

        try:
            result = self._clover_make_platform_request('GET', '')
            merchant_name = result.get('name', 'Unknown')
            message = _(
                "Connection successful. Merchant: %(name)s (ID: %(mid)s)",
                name=merchant_name,
                mid=self.clover_merchant_id,
            )
            notification_type = 'success'
        except (ValidationError, UserError) as e:
            message = _("Connection failed: %(error)s", error=str(e))
            notification_type = 'danger'

        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'message': message,
                'sticky': False,
                'type': notification_type,
            },
        }

    def _clover_notification(self, message, notification_type='info'):
        """Return a display_notification action."""
        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'message': message,
                'sticky': False,
                'type': notification_type,
            },
        }
