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

import base64
import json
import logging

from werkzeug.urls import url_encode

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.urls import urljoin as url_join

from odoo.addons.fusion_clover import const
from odoo.addons.fusion_clover import utils as clover_utils
from odoo.addons.fusion_clover.controllers.main import CloverController

_logger = logging.getLogger(__name__)


class PaymentTransaction(models.Model):
    _inherit = 'payment.transaction'

    clover_charge_id = fields.Char(
        string="Clover Charge ID",
        readonly=True,
        copy=False,
    )
    clover_refund_id = fields.Char(
        string="Clover Refund ID",
        readonly=True,
        copy=False,
    )
    clover_receipt_data = fields.Text(
        string="Clover Receipt Data",
        readonly=True,
        copy=False,
        help="JSON blob with receipt-relevant fields captured at payment time.",
    )
    clover_order_id = fields.Char(
        string="Clover Order ID",
        readonly=True,
        copy=False,
    )
    clover_voided = fields.Boolean(
        string="Voided",
        default=False,
        copy=False,
    )
    clover_void_date = fields.Datetime(
        string="Void Date",
        readonly=True,
        copy=False,
    )

    def _get_provider_sudo(self):
        return self.provider_id.sudo()

    # === BUSINESS METHODS - PAYMENT FLOW === #

    def _get_specific_processing_values(self, processing_values):
        """Override of payment to return Clover-specific processing values."""
        if self.provider_code != 'clover':
            return super()._get_specific_processing_values(processing_values)

        if self.operation == 'online_token':
            return {}

        provider = self._get_provider_sudo()
        base_url = provider.get_base_url()
        return_url = url_join(
            base_url,
            f'{CloverController._return_url}?{url_encode({"reference": self.reference})}',
        )

        return {
            'return_url': return_url,
            'merchant_id': provider.clover_merchant_id,
            'is_test': provider.state == 'test',
        }

    def _send_payment_request(self):
        """Override of `payment` to send a payment request to Clover."""
        if self.provider_code != 'clover':
            return super()._send_payment_request()

        if self.operation in ('online_token', 'offline'):
            return self._clover_process_token_payment()

    @staticmethod
    def _detect_card_brand_from_details(payment_details):
        """Detect card brand from the payment_details string on a token."""
        details = (payment_details or '').upper()
        if 'AMEX' in details or 'AMERICAN_EXPRESS' in details:
            return 'amex'
        if 'VISA' in details:
            return 'visa'
        if 'MASTER' in details:
            return 'mastercard'
        return 'other'

    def _apply_token_surcharge(self):
        """Apply surcharge to the linked invoice for token-based payments."""
        ICP = self.env['ir.config_parameter'].sudo()
        if ICP.get_param('fusion_clover.surcharge_enabled', 'False') != 'True':
            return

        if not self.token_id or not self.invoice_ids:
            return

        for inv in self.invoice_ids:
            sale_orders = inv.mapped('line_ids.sale_line_ids.order_id')
            for so in sale_orders:
                if getattr(so, 'is_rental_order', False):
                    if not getattr(so, 'rental_apply_cc_fee', True):
                        return

        card_type = self._detect_card_brand_from_details(
            self.token_id.payment_details,
        )
        rate_key = {
            'visa': 'fusion_clover.surcharge_visa_rate',
            'mastercard': 'fusion_clover.surcharge_mastercard_rate',
            'amex': 'fusion_clover.surcharge_amex_rate',
            'debit': 'fusion_clover.surcharge_debit_rate',
        }.get(card_type, 'fusion_clover.surcharge_other_rate')
        rate = float(ICP.get_param(rate_key, '0') or 0)
        if rate <= 0:
            return

        product_id = int(ICP.get_param('fusion_clover.surcharge_product_id', '0') or 0)
        product = self.env['product.product'].sudo().browse(product_id).exists()
        if not product:
            product = self.env.ref(
                'fusion_clover.product_cc_processing_fee', raise_if_not_found=False,
            )
        if not product:
            _logger.warning("Surcharge product not configured; skipping token surcharge")
            return

        total_fee = 0.0
        for invoice in self.invoice_ids.sudo():
            already_has = invoice.invoice_line_ids.filtered(
                lambda l: l.product_id.id == product.id
            )
            if already_has:
                continue

            fee_amount = round(invoice.amount_residual * rate / 100.0, 2)
            if fee_amount <= 0:
                continue

            was_posted = invoice.state == 'posted'
            if was_posted:
                invoice.button_draft()

            description = "Credit Card Processing Fee (%.2f%% surcharge)" % rate
            invoice.write({
                'invoice_line_ids': [(0, 0, {
                    'product_id': product.id,
                    'name': description,
                    'quantity': 1,
                    'price_unit': fee_amount,
                    'tax_ids': [(5, 0, 0)],
                })],
            })

            if was_posted:
                invoice.action_post()

            total_fee += fee_amount

        if total_fee > 0:
            self.amount += total_fee

    def _clover_process_token_payment(self):
        """Process a payment using a stored token (card on file)."""
        try:
            self._apply_token_surcharge()

            provider = self._get_provider_sudo()
            capture = not provider.capture_manually
            clover_token = self.token_id.clover_source_token

            if not clover_token:
                self._set_error(_("No Clover token found for this saved card."))
                return

            result = provider._clover_create_charge(
                source_token=clover_token,
                amount=self.amount,
                currency=self.currency_id,
                capture=capture,
                description=self.reference,
                ecomind='moto',
                metadata={'odoo_reference': self.reference},
            )

            charge_id = result.get('id', '')
            status = result.get('status', '')

            self.clover_charge_id = charge_id
            self.provider_reference = charge_id

            payment_data = {
                'reference': self.reference,
                'clover_charge_id': charge_id,
                'clover_status': status,
                'source': result.get('source', {}),
            }

            if status == 'failed':
                outcome = result.get('outcome', {})
                decline_msg = outcome.get('type', status)
                self._set_error(
                    _("Payment %(status)s: %(reason)s",
                      status=status, reason=decline_msg)
                )
                return

            self._process('clover', payment_data)
        except ValidationError as e:
            self._set_error(str(e))

    def _send_refund_request(self):
        """Override of `payment` to send a refund request to Clover."""
        if self.provider_code != 'clover':
            return super()._send_refund_request()

        source_tx = self.source_transaction_id
        charge_id = source_tx.clover_charge_id or source_tx.provider_reference
        refund_amount = abs(self.amount)

        try:
            result = self._get_provider_sudo()._clover_create_refund(
                charge_id=charge_id,
                amount=refund_amount,
                currency=self.currency_id,
                reason=f'Refund for {source_tx.reference}',
            )

            refund_id = result.get('id', '')
            self.provider_reference = refund_id
            self.clover_refund_id = refund_id

            payment_data = {
                'reference': self.reference,
                'clover_charge_id': charge_id,
                'clover_refund_id': refund_id,
                'clover_status': result.get('status', 'succeeded'),
            }
            self._process('clover', payment_data)
        except ValidationError as e:
            self._set_error(str(e))

    def _send_capture_request(self):
        """Override of `payment` to send a capture request to Clover."""
        if self.provider_code != 'clover':
            return super()._send_capture_request()

        source_tx = self.source_transaction_id
        charge_id = source_tx.clover_charge_id or source_tx.provider_reference

        try:
            result = self._get_provider_sudo()._clover_capture_charge(
                charge_id=charge_id,
                amount=self.amount,
                currency=self.currency_id,
            )

            payment_data = {
                'reference': self.reference,
                'clover_charge_id': result.get('id', charge_id),
                'clover_status': result.get('status', 'succeeded'),
            }
            self._process('clover', payment_data)
        except ValidationError as e:
            self._set_error(str(e))

    def _send_void_request(self):
        """Override of `payment` to send a void (refund full) request to Clover.

        Clover doesn't have a dedicated void endpoint -- a full refund before
        settlement acts as a void.
        """
        if self.provider_code != 'clover':
            return super()._send_void_request()

        source_tx = self.source_transaction_id
        charge_id = source_tx.clover_charge_id or source_tx.provider_reference

        try:
            result = self._get_provider_sudo()._clover_create_refund(
                charge_id=charge_id,
                reason=f'Void for {source_tx.reference}',
            )

            payment_data = {
                'reference': self.reference,
                'clover_charge_id': charge_id,
                'clover_refund_id': result.get('id', ''),
                'clover_status': result.get('status', 'succeeded'),
            }
            self._process('clover', payment_data)
        except ValidationError as e:
            self._set_error(str(e))

    # === ACTION METHODS - VOID === #

    def action_clover_void(self):
        """Void a confirmed Clover transaction (same-day, before settlement).

        Clover's Ecommerce API treats a full refund on an unsettled charge as a
        void.  We issue ``POST /v1/refunds`` for the full amount; if the charge
        has already settled, the processor will decline the void (the user
        should create a credit note and use the refund wizard instead).
        """
        self.ensure_one()
        if self.provider_code != 'clover':
            raise ValidationError(_("This action is only available for Clover transactions."))
        if self.state != 'done':
            raise ValidationError(_("Only confirmed transactions can be voided."))

        charge_id = self.clover_charge_id or self.provider_reference
        if not charge_id:
            raise ValidationError(_("No Clover charge ID found."))

        # Guard against double reversal
        existing_refund = self.env['payment.transaction'].sudo().search([
            ('source_transaction_id', '=', self.id),
            ('operation', '=', 'refund'),
            ('state', '=', 'done'),
        ], limit=1)
        if existing_refund:
            raise ValidationError(_(
                "This transaction has already been refunded "
                "(%(ref)s). Voiding would result in a double reversal.",
                ref=existing_refund.reference,
            ))

        provider = self._get_provider_sudo()

        # Verify on Clover the charge hasn't already been refunded
        try:
            charge_data = provider._clover_make_ecom_request(
                'GET', f'v1/charges/{charge_id}',
            )
            charge_status = charge_data.get('status', '')
            if charge_status == 'refunded':
                raise ValidationError(_(
                    "This charge has already been refunded on Clover. "
                    "It cannot be voided again."
                ))
        except ValidationError:
            raise
        except Exception:
            _logger.debug("Could not verify charge %s before void", charge_id)

        # Issue full refund (acts as void before settlement)
        try:
            result = provider._clover_create_refund(
                charge_id=charge_id,
                reason=f'Void for {self.reference}',
            )
        except ValidationError as e:
            error_msg = str(e)
            if '400' in error_msg or 'declined' in error_msg.lower():
                raise ValidationError(_(
                    "Void declined by the payment processor. This usually "
                    "means the batch has already settled. Settled transactions "
                    "cannot be voided.\n\n"
                    "To reverse this payment, create a Credit Note on the "
                    "invoice and process a refund through the Clover refund "
                    "wizard."
                ))
            raise

        _logger.info(
            "Clover void response: id=%s, status=%s",
            result.get('id', ''), result.get('status', ''),
        )

        # Cancel the Odoo payment
        if self.payment_id:
            self.payment_id.sudo().action_cancel()

        self.sudo().write({
            'state': 'cancel',
            'clover_voided': True,
            'clover_void_date': fields.Datetime.now(),
        })

        invoice = self.invoice_ids[:1]
        if invoice:
            invoice.sudo().message_post(
                body=_(
                    "Payment voided: transaction %(ref)s was voided on Clover "
                    "(Clover Refund ID: %(refund_id)s).",
                    ref=self.reference,
                    refund_id=result.get('id', ''),
                ),
            )

        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'type': 'success',
                'message': _("Transaction voided successfully on Clover."),
                'next': {'type': 'ir.actions.client', 'tag': 'soft_reload'},
            },
        }

    # === BUSINESS METHODS - NOTIFICATION PROCESSING === #

    @api.model
    def _search_by_reference(self, provider_code, payment_data):
        """Override of payment to find the transaction based on Clover data."""
        if provider_code != 'clover':
            return super()._search_by_reference(provider_code, payment_data)

        reference = payment_data.get('reference')
        if reference:
            tx = self.search([
                ('reference', '=', reference),
                ('provider_code', '=', 'clover'),
            ])
        else:
            charge_id = payment_data.get('clover_charge_id')
            if charge_id:
                tx = self.search([
                    ('clover_charge_id', '=', charge_id),
                    ('provider_code', '=', 'clover'),
                ])
            else:
                _logger.warning("Received Clover data with no reference or charge ID")
                tx = self

        if not tx:
            _logger.warning(
                "No transaction found matching Clover reference %s", reference,
            )

        return tx

    def _apply_updates(self, payment_data):
        """Override of `payment` to update the transaction based on Clover data."""
        if self.provider_code != 'clover':
            return super()._apply_updates(payment_data)

        charge_id = payment_data.get('clover_charge_id')
        if charge_id:
            self.provider_reference = charge_id
            self.clover_charge_id = charge_id

        refund_id = payment_data.get('clover_refund_id')
        if refund_id:
            self.clover_refund_id = refund_id

        source = payment_data.get('source', {})
        if source:
            card_details = clover_utils.extract_card_details(source)
            if card_details.get('brand'):
                payment_method = self.env['payment.method']._get_from_code(
                    card_details['brand'],
                    mapping=const.CARD_BRAND_MAPPING,
                )
                if payment_method:
                    self.payment_method_id = payment_method

        status = payment_data.get('clover_status', '')
        if not status:
            self._set_error(_("Received data with missing transaction status."))
            return

        odoo_state = clover_utils.get_clover_status(status)

        if odoo_state == 'authorized':
            self._set_authorized()
        elif odoo_state == 'done':
            self._set_done()
            self._post_process()
            self._clover_generate_receipt(payment_data)
        elif odoo_state == 'cancel':
            self._set_canceled()
        elif odoo_state == 'refund':
            self._set_done()
            self._post_process()
            self._clover_generate_receipt(payment_data)
        elif odoo_state == 'error':
            error_msg = payment_data.get('error_message', _("Payment was declined by Clover."))
            self._set_error(error_msg)
        else:
            _logger.warning(
                "Received unknown Clover status (%s) for transaction %s.",
                status, self.reference,
            )
            self._set_error(
                _("Received data with unrecognized status: %s.", status)
            )

    def _create_payment(self, **extra_create_values):
        """Override to route Clover payments directly to the bank account."""
        if self.provider_code != 'clover':
            return super()._create_payment(**extra_create_values)

        self.ensure_one()
        provider = self._get_provider_sudo()
        reference = f'{self.reference} - {self.provider_reference or ""}'
        payment_method_line = provider.journal_id.inbound_payment_method_line_ids\
            .filtered(lambda l: l.payment_provider_id == provider)
        payment_values = {
            'amount': abs(self.amount),
            'payment_type': 'inbound' if self.amount > 0 else 'outbound',
            'currency_id': self.currency_id.id,
            'partner_id': self.partner_id.commercial_partner_id.id,
            'partner_type': 'customer',
            'journal_id': provider.journal_id.id,
            'company_id': provider.company_id.id,
            'payment_method_line_id': payment_method_line.id,
            'payment_token_id': self.token_id.id,
            'payment_transaction_id': self.id,
            'memo': reference,
            'write_off_line_vals': [],
            'invoice_ids': self.invoice_ids,
            **extra_create_values,
        }

        payment_term_lines = self.invoice_ids.line_ids.filtered(
            lambda line: line.display_type == 'payment_term'
        )
        if payment_term_lines:
            payment_values['destination_account_id'] = payment_term_lines[0].account_id.id

        payment = self.env['account.payment'].create(payment_values)

        bank_account = provider.journal_id.default_account_id
        if bank_account and bank_account.account_type == 'asset_cash':
            payment.outstanding_account_id = bank_account

        payment.action_post()
        self.payment_id = payment

        if self.operation == self.source_transaction_id.operation:
            invoices = self.source_transaction_id.invoice_ids
        else:
            invoices = self.invoice_ids
        invoices = invoices.filtered(lambda inv: inv.state != 'cancel')
        if invoices:
            invoices.filtered(lambda inv: inv.state == 'draft').action_post()
            (payment.move_id.line_ids + invoices.line_ids).filtered(
                lambda line: line.account_id == payment.destination_account_id
                and not line.reconciled
            ).reconcile()

        return payment

    def _extract_token_values(self, payment_data):
        """Override of `payment` to return token data based on Clover data."""
        if self.provider_code != 'clover':
            return super()._extract_token_values(payment_data)

        source = payment_data.get('source', {})
        card_details = clover_utils.extract_card_details(source)

        if not card_details:
            _logger.warning(
                "Tokenization requested but no card data in payment response."
            )
            return {}

        return {
            'payment_details': card_details.get('last4', ''),
            'clover_source_token': source.get('id', ''),
        }

    # === RECEIPT GENERATION === #

    def _clover_generate_receipt(self, payment_data=None):
        """Store receipt data and generate a PDF receipt."""
        self.ensure_one()
        if self.provider_code != 'clover' or not self.clover_charge_id:
            return

        try:
            self._clover_store_receipt_data(payment_data)
            self._clover_attach_receipt_pdf()
        except Exception:
            _logger.exception(
                "Receipt generation failed for transaction %s", self.reference,
            )

    def _clover_store_receipt_data(self, payment_data=None):
        """Persist receipt-relevant fields as a JSON blob."""
        source = payment_data.get('source', {}) if payment_data else {}

        receipt = {
            'charge_id': self.clover_charge_id or '',
            'reference': self.reference,
            'status': payment_data.get('clover_status', '') if payment_data else '',
            'card_brand': source.get('brand', ''),
            'card_last4': str(source.get('last4', '')),
            'card_first6': str(source.get('first6', '')),
            'exp_month': source.get('exp_month', ''),
            'exp_year': source.get('exp_year', ''),
            'transaction_amount': float(self.amount),
            'currency': self.currency_id.name,
        }

        self.clover_receipt_data = json.dumps(receipt)

    def _clover_attach_receipt_pdf(self):
        """Render the QWeb receipt report and attach the PDF to the invoice."""
        invoice = self.invoice_ids[:1]
        if not invoice:
            return

        try:
            report = self.env.ref('fusion_clover.action_report_clover_receipt')
            pdf_content, _report_type = report._render_qweb_pdf(report.report_name, [self.id])
        except Exception:
            _logger.debug("Could not render Clover receipt PDF for %s", self.reference)
            return

        filename = f"Payment_Receipt_{self.reference}.pdf"
        attachment = self.env['ir.attachment'].sudo().create({
            'name': filename,
            'type': 'binary',
            'datas': base64.b64encode(pdf_content),
            'res_model': 'account.move',
            'res_id': invoice.id,
            'mimetype': 'application/pdf',
        })

        invoice.sudo().message_post(
            body=_(
                "Payment receipt generated for transaction %(ref)s.",
                ref=self.reference,
            ),
            attachment_ids=[attachment.id],
        )

    def _get_clover_receipt_values(self):
        """Parse the stored receipt JSON for use in QWeb templates."""
        self.ensure_one()
        data = self.clover_receipt_data
        if not data and self.source_transaction_id:
            data = self.source_transaction_id.clover_receipt_data
        if not data:
            return {}
        try:
            return json.loads(data)
        except (json.JSONDecodeError, TypeError):
            return {}

    def _get_source_receipt_values(self):
        """Return receipt values from the original sale transaction."""
        self.ensure_one()
        if self.source_transaction_id and self.source_transaction_id.clover_receipt_data:
            try:
                return json.loads(self.source_transaction_id.clover_receipt_data)
            except (json.JSONDecodeError, TypeError):
                pass
        return {}
