# Fusion Accounting - Automatic Reconciliation Wizard
# Enables batch reconciliation of journal items using configurable
# matching strategies (perfect match or zero-balance clearing).

from datetime import date

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


class AccountAutoReconcileWizard(models.TransientModel):
    """Wizard for automated matching and reconciliation of journal items.
    Accessible via Accounting > Actions > Auto-reconcile."""

    _name = 'account.auto.reconcile.wizard'
    _description = 'Account automatic reconciliation wizard'
    _check_company_auto = True

    # --- Organizational Fields ---
    company_id = fields.Many2one(
        comodel_name='res.company',
        required=True,
        readonly=True,
        default=lambda self: self.env.company,
    )
    line_ids = fields.Many2many(
        comodel_name='account.move.line',
        help="Pre-selected journal items used to seed wizard defaults.",
    )

    # --- Filter Parameters ---
    from_date = fields.Date(string='From')
    to_date = fields.Date(
        string='To',
        default=fields.Date.context_today,
        required=True,
    )
    account_ids = fields.Many2many(
        comodel_name='account.account',
        string='Accounts',
        check_company=True,
        domain="[('reconcile', '=', True), "
               "('account_type', '!=', 'off_balance')]",
    )
    partner_ids = fields.Many2many(
        comodel_name='res.partner',
        string='Partners',
        check_company=True,
        domain="[('company_id', 'in', (False, company_id)), "
               "'|', ('parent_id', '=', False), ('is_company', '=', True)]",
    )
    search_mode = fields.Selection(
        selection=[
            ('one_to_one', "Perfect Match"),
            ('zero_balance', "Clear Account"),
        ],
        string='Reconcile',
        required=True,
        default='one_to_one',
        help="Choose to match items pairwise by opposite balance, "
             "or clear accounts where the total balance is zero.",
    )

    # -------------------------------------------------------------------------
    # Defaults
    # -------------------------------------------------------------------------

    @api.model
    def default_get(self, field_names):
        """Pre-configure the wizard from context domain if available."""
        defaults = super().default_get(field_names)
        ctx_domain = self.env.context.get('domain')
        if 'line_ids' in field_names and 'line_ids' not in defaults and ctx_domain:
            matching_lines = self.env['account.move.line'].search(ctx_domain)
            if matching_lines:
                defaults.update(self._derive_defaults_from_lines(matching_lines))
                defaults['line_ids'] = [Command.set(matching_lines.ids)]
        return defaults

    @api.model
    def _derive_defaults_from_lines(self, move_lines):
        """Infer wizard preset values from a set of journal items.
        When all items share a common account or partner, pre-fill those
        filters automatically."""
        unique_accounts = move_lines.mapped('account_id')
        unique_partners = move_lines.mapped('partner_id')
        total_balance = sum(move_lines.mapped('balance'))
        all_dates = move_lines.mapped('date')

        preset = {
            'account_ids': (
                [Command.set(unique_accounts.ids)]
                if len(unique_accounts) == 1 else []
            ),
            'partner_ids': (
                [Command.set(unique_partners.ids)]
                if len(unique_partners) == 1 else []
            ),
            'search_mode': (
                'zero_balance'
                if move_lines.company_currency_id.is_zero(total_balance)
                else 'one_to_one'
            ),
            'from_date': min(all_dates),
            'to_date': max(all_dates),
        }
        return preset

    def _snapshot_wizard_config(self):
        """Capture current wizard state as a comparable dict."""
        self.ensure_one()
        return {
            'account_ids': (
                [Command.set(self.account_ids.ids)] if self.account_ids else []
            ),
            'partner_ids': (
                [Command.set(self.partner_ids.ids)] if self.partner_ids else []
            ),
            'search_mode': self.search_mode,
            'from_date': self.from_date,
            'to_date': self.to_date,
        }

    # Keep backward-compatible alias
    _get_wizard_values = _snapshot_wizard_config
    _get_default_wizard_values = _derive_defaults_from_lines

    # -------------------------------------------------------------------------
    # Domain Construction
    # -------------------------------------------------------------------------

    def _get_amls_domain(self):
        """Build a search domain for journal items eligible for
        automatic reconciliation."""
        self.ensure_one()
        # If the config hasn't changed from the seed defaults, use IDs directly
        if (
            self.line_ids
            and self._snapshot_wizard_config() == self._derive_defaults_from_lines(self.line_ids)
        ):
            return [('id', 'in', self.line_ids.ids)]

        base_domain = [
            ('company_id', '=', self.company_id.id),
            ('parent_state', '=', 'posted'),
            ('display_type', 'not in', ('line_section', 'line_note')),
            ('date', '>=', self.from_date or date.min),
            ('date', '<=', self.to_date),
            ('reconciled', '=', False),
            ('account_id.reconcile', '=', True),
            ('amount_residual_currency', '!=', 0.0),
            ('amount_residual', '!=', 0.0),
        ]
        if self.account_ids:
            base_domain.append(('account_id', 'in', self.account_ids.ids))
            if self.partner_ids:
                base_domain.append(('partner_id', 'in', self.partner_ids.ids))
        return base_domain

    # -------------------------------------------------------------------------
    # Reconciliation Strategies
    # -------------------------------------------------------------------------

    def _auto_reconcile_one_to_one(self):
        """Pair items with matching opposite amounts and reconcile them.
        Groups by account, partner, currency, and absolute residual amount,
        then pairs positive with negative items chronologically."""
        AML = self.env['account.move.line']
        grouped_data = AML._read_group(
            self._get_amls_domain(),
            ['account_id', 'partner_id', 'currency_id',
             'amount_residual_currency:abs_rounded'],
            ['id:recordset'],
        )

        matched_lines = AML
        paired_groups = []

        for *_grouping, item_set in grouped_data:
            pos_items = item_set.filtered(
                lambda ln: ln.amount_residual_currency >= 0
            ).sorted('date')
            neg_items = (item_set - pos_items).sorted('date')
            pair_count = min(len(pos_items), len(neg_items))

            trimmed_pos = pos_items[:pair_count]
            trimmed_neg = neg_items[:pair_count]
            matched_lines += trimmed_pos + trimmed_neg

            for p_item, n_item in zip(trimmed_pos, trimmed_neg):
                paired_groups.append(p_item + n_item)

        AML._reconcile_plan(paired_groups)
        return matched_lines

    def _auto_reconcile_zero_balance(self):
        """Reconcile all items within groups (account/partner/currency)
        where the total residual sums to zero."""
        AML = self.env['account.move.line']
        grouped_data = AML._read_group(
            self._get_amls_domain(),
            groupby=['account_id', 'partner_id', 'currency_id'],
            aggregates=['id:recordset'],
            having=[('amount_residual_currency:sum_rounded', '=', 0)],
        )

        matched_lines = AML
        reconcile_groups = []

        for group_row in grouped_data:
            group_amls = group_row[-1]
            matched_lines += group_amls
            reconcile_groups.append(group_amls)

        AML._reconcile_plan(reconcile_groups)
        return matched_lines

    # -------------------------------------------------------------------------
    # Main Entry Point
    # -------------------------------------------------------------------------

    def auto_reconcile(self):
        """Execute the selected reconciliation strategy and display results."""
        self.ensure_one()
        if self.search_mode == 'zero_balance':
            reconciled = self._auto_reconcile_zero_balance()
        else:
            reconciled = self._auto_reconcile_one_to_one()

        # Gather all related lines (including exchange diff entries)
        related_lines = self.env['account.move.line'].search([
            ('full_reconcile_id', 'in', reconciled.full_reconcile_id.ids),
        ])

        if not related_lines:
            raise UserError(_("No matching entries found for reconciliation."))

        return {
            'name': _("Automatically Reconciled Entries"),
            'type': 'ir.actions.act_window',
            'res_model': 'account.move.line',
            'context': "{'search_default_group_by_matching': True}",
            'view_mode': 'list',
            'domain': [('id', 'in', related_lines.ids)],
        }
