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

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import format_amount


class PaymentCaptureWizard(models.TransientModel):
    _name = 'payment.capture.wizard'
    _description = "Payment Capture Wizard"

    transaction_ids = fields.Many2many(  # All the source txs related to the capture request
        comodel_name='payment.transaction',
        default=lambda self: self.env.context.get('active_ids'),
        readonly=True,
    )
    authorized_amount = fields.Monetary(
        string="Authorized Amount", compute='_compute_authorized_amount'
    )
    captured_amount = fields.Monetary(string="Already Captured", compute='_compute_captured_amount')
    voided_amount = fields.Monetary(string="Already Voided", compute='_compute_voided_amount')
    available_amount = fields.Monetary(
        string="Maximum Capture Allowed", compute='_compute_available_amount'
    )
    amount_to_capture = fields.Monetary(
        compute='_compute_amount_to_capture', store=True, readonly=False
    )
    is_amount_to_capture_valid = fields.Boolean(compute='_compute_is_amount_to_capture_valid')
    void_remaining_amount = fields.Boolean()
    currency_id = fields.Many2one(related='transaction_ids.currency_id')
    support_partial_capture = fields.Boolean(
        help="Whether each of the transactions' provider supports the partial capture.",
        compute='_compute_support_partial_capture',
        compute_sudo=True,
    )
    has_draft_children = fields.Boolean(compute='_compute_has_draft_children')
    has_remaining_amount = fields.Boolean(compute='_compute_has_remaining_amount')

    #=== COMPUTE METHODS ===#

    @api.depends('transaction_ids')
    def _compute_authorized_amount(self):
        for wizard in self:
            wizard.authorized_amount = sum(wizard.transaction_ids.mapped('amount'))

    @api.depends('transaction_ids')
    def _compute_captured_amount(self):
        for wizard in self:
            full_capture_txs = wizard.transaction_ids.filtered(
                lambda tx: tx.state == 'done' and not tx.child_transaction_ids
            )  # Transactions that have been fully captured in a single capture operation.
            partial_capture_child_txs = wizard.transaction_ids.child_transaction_ids.filtered(
                lambda tx: tx.state == 'done'
            )  # Transactions that represent a partial capture of their source transaction.
            wizard.captured_amount = sum(
                (full_capture_txs | partial_capture_child_txs).mapped('amount')
            )

    @api.depends('transaction_ids')
    def _compute_voided_amount(self):
        for wizard in self:
            void_child_txs = wizard.transaction_ids.child_transaction_ids.filtered(
                lambda tx: tx.state == 'cancel'
            )
            wizard.voided_amount = sum(void_child_txs.mapped('amount'))

    @api.depends('authorized_amount', 'captured_amount', 'voided_amount')
    def _compute_available_amount(self):
        for wizard in self:
            wizard.available_amount = wizard.authorized_amount \
                                      - wizard.captured_amount \
                                      - wizard.voided_amount

    @api.depends('available_amount')
    def _compute_amount_to_capture(self):
        """ Set the default amount to capture to the amount available for capture. """
        for wizard in self:
            wizard.amount_to_capture = wizard.available_amount

    @api.depends('amount_to_capture', 'available_amount')
    def _compute_is_amount_to_capture_valid(self):
        for wizard in self:
            is_valid = 0 < wizard.amount_to_capture <= wizard.available_amount
            wizard.is_amount_to_capture_valid = is_valid

    @api.depends('transaction_ids')
    def _compute_support_partial_capture(self):
        for wizard in self:
            wizard.support_partial_capture = all(
                tx.provider_id.support_manual_capture == 'partial' for tx in wizard.transaction_ids
            )

    @api.depends('transaction_ids')
    def _compute_has_draft_children(self):
        for wizard in self:
            wizard.has_draft_children = bool(wizard.transaction_ids.child_transaction_ids.filtered(
                lambda tx: tx.state == 'draft'
            ))

    @api.depends('available_amount', 'amount_to_capture')
    def _compute_has_remaining_amount(self):
        for wizard in self:
            wizard.has_remaining_amount = wizard.amount_to_capture < wizard.available_amount
            if not wizard.has_remaining_amount:
                wizard.void_remaining_amount = False

    #=== CONSTRAINT METHODS ===#

    @api.constrains('amount_to_capture')
    def _check_amount_to_capture_within_boundaries(self):
        for wizard in self:
            if not wizard.is_amount_to_capture_valid:
                formatted_amount = format_amount(
                    self.env, wizard.available_amount, wizard.currency_id
                )
                raise ValidationError(_(
                    "The amount to capture must be positive and cannot be superior to %s.",
                    formatted_amount
                ))
            if not wizard.support_partial_capture \
               and wizard.amount_to_capture != wizard.available_amount:
                raise ValidationError(_(
                    "Some of the transactions you intend to capture can only be captured in full. "
                    "Handle the transactions individually to capture a partial amount."
                ))

    #=== ACTION METHODS ===#

    def action_capture(self):
        for wizard in self:
            remaining_amount_to_capture = wizard.amount_to_capture
            for source_tx in wizard.transaction_ids.filtered(lambda tx: tx.state == 'authorized'):
                partial_capture_child_txs = wizard.transaction_ids.child_transaction_ids.filtered(
                    lambda tx: tx.source_transaction_id == source_tx and tx.state == 'done'
                )  # We can void all the remaining amount only at once => don't check cancel state.
                source_tx_remaining_amount = source_tx.currency_id.round(
                    source_tx.amount - sum(partial_capture_child_txs.mapped('amount'))
                )
                if remaining_amount_to_capture:
                    amount_to_capture = min(source_tx_remaining_amount, remaining_amount_to_capture)
                    # In sudo mode because we need to be able to read on provider fields.
                    source_tx.sudo()._send_capture_request(amount_to_capture=amount_to_capture)
                    remaining_amount_to_capture -= amount_to_capture
                    source_tx_remaining_amount -= amount_to_capture

                if source_tx_remaining_amount and wizard.void_remaining_amount:
                    # The source tx isn't fully captured and the user wants to void the remaining.
                    # In sudo mode because we need to be able to read on provider fields.
                    source_tx.sudo()._send_void_request(amount_to_void=source_tx_remaining_amount)
                elif not remaining_amount_to_capture and not wizard.void_remaining_amount:
                    # The amount to capture has been completely captured.
                    break  # Skip the remaining transactions.
