# Fusion Accounting - Multicurrency Revaluation Report Handler
# Computes unrealised FX gains/losses and provides an adjustment wizard

from itertools import chain

from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools import float_is_zero, SQL


class FusionMulticurrencyRevaluationHandler(models.AbstractModel):
    """Manages unrealised gains and losses arising from fluctuating
    exchange rates. Presents balances at both historical and current
    rates and offers an adjustment-entry wizard."""

    _name = 'account.multicurrency.revaluation.report.handler'
    _inherit = 'account.report.custom.handler'
    _description = 'Multicurrency Revaluation Report Custom Handler'

    # ---- Display Configuration ----
    def _get_custom_display_config(self):
        return {
            'components': {
                'AccountReportFilters': 'fusion_accounting.MulticurrencyRevaluationReportFilters',
            },
            'templates': {
                'AccountReportLineName': 'fusion_accounting.MulticurrencyRevaluationReportLineName',
            },
        }

    # ---- Options ----
    def _custom_options_initializer(self, report, options, previous_options):
        super()._custom_options_initializer(report, options, previous_options=previous_options)

        active_currencies = self.env['res.currency'].search([('active', '=', True)])
        if len(active_currencies) < 2:
            raise UserError(_("At least two active currencies are required for this report."))

        fx_rates = active_currencies._get_rates(
            self.env.company, options.get('date', {}).get('date_to'),
        )
        base_rate = fx_rates[self.env.company.currency_id.id]
        for cid in fx_rates:
            fx_rates[cid] /= base_rate

        options['currency_rates'] = {
            str(cur.id): {
                'currency_id': cur.id,
                'currency_name': cur.name,
                'currency_main': self.env.company.currency_id.name,
                'rate': (
                    fx_rates[cur.id]
                    if not previous_options.get('currency_rates', {}).get(str(cur.id), {}).get('rate')
                    else float(previous_options['currency_rates'][str(cur.id)]['rate'])
                ),
            }
            for cur in active_currencies
        }

        for cr in options['currency_rates'].values():
            if cr['rate'] == 0:
                raise UserError(_("Currency rate cannot be zero."))

        options['company_currency'] = options['currency_rates'].pop(
            str(self.env.company.currency_id.id),
        )
        options['custom_rate'] = any(
            not float_is_zero(cr['rate'] - fx_rates[cr['currency_id']], 20)
            for cr in options['currency_rates'].values()
        )
        options['multi_currency'] = True
        options['buttons'].append({
            'name': _('Adjustment Entry'),
            'sequence': 30,
            'action': 'action_multi_currency_revaluation_open_revaluation_wizard',
            'always_show': True,
        })

    # ---- Warnings ----
    def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
        if len(self.env.companies) > 1:
            warnings['fusion_accounting.multi_currency_revaluation_report_warning_multicompany'] = {
                'alert_type': 'warning',
            }
        if options['custom_rate']:
            warnings['fusion_accounting.multi_currency_revaluation_report_warning_custom_rate'] = {
                'alert_type': 'warning',
            }

    # ---- Post-Processing ----
    def _custom_line_postprocessor(self, report, options, lines):
        adj_line_id = self.env.ref('fusion_accounting.multicurrency_revaluation_to_adjust').id
        excl_line_id = self.env.ref('fusion_accounting.multicurrency_revaluation_excluded').id

        processed = []
        for idx, ln in enumerate(lines):
            model_name, model_id = report._get_model_info_from_id(ln['id'])

            if model_name == 'account.report.line' and (
                (model_id == adj_line_id
                 and report._get_model_info_from_id(lines[idx + 1]['id']) == ('account.report.line', excl_line_id))
                or (model_id == excl_line_id and idx == len(lines) - 1)
            ):
                continue

            elif model_name == 'res.currency':
                rate_val = float(options['currency_rates'][str(model_id)]['rate'])
                ln['name'] = '{fc} (1 {mc} = {r:.6} {fc})'.format(
                    fc=ln['name'],
                    mc=self.env.company.currency_id.display_name,
                    r=rate_val,
                )

            elif model_name == 'account.account':
                ln['is_included_line'] = (
                    report._get_res_id_from_line_id(ln['id'], 'account.account') == adj_line_id
                )

            ln['cur_revaluation_line_model'] = model_name
            processed.append(ln)

        return processed

    def _custom_groupby_line_completer(self, report, options, line_dict):
        info = report._get_model_info_from_id(line_dict['id'])
        if info[0] == 'res.currency':
            line_dict['unfolded'] = True
            line_dict['unfoldable'] = False

    # ---- Actions ----
    def action_multi_currency_revaluation_open_revaluation_wizard(self, options):
        wiz_view = self.env.ref(
            'fusion_accounting.view_account_multicurrency_revaluation_wizard', False,
        )
        return {
            'name': _("Make Adjustment Entry"),
            'type': 'ir.actions.act_window',
            'res_model': 'account.multicurrency.revaluation.wizard',
            'view_mode': 'form',
            'view_id': wiz_view.id,
            'views': [(wiz_view.id, 'form')],
            'multi': 'True',
            'target': 'new',
            'context': {
                **self.env.context,
                'multicurrency_revaluation_report_options': options,
            },
        }

    def action_multi_currency_revaluation_open_general_ledger(self, options, params):
        report = self.env['account.report'].browse(options['report_id'])
        acct_id = report._get_res_id_from_line_id(params['line_id'], 'account.account')
        acct_line_id = report._get_generic_line_id('account.account', acct_id)
        gl_options = self.env.ref('fusion_accounting.general_ledger_report').get_options(options)
        gl_options['unfolded_lines'] = [acct_line_id]

        gl_action = self.env['ir.actions.actions']._for_xml_id(
            'fusion_accounting.action_account_report_general_ledger',
        )
        gl_action['params'] = {
            'options': gl_options,
            'ignore_session': True,
        }
        return gl_action

    def action_multi_currency_revaluation_toggle_provision(self, options, params):
        """Toggle inclusion/exclusion of an account from the provision."""
        id_map = self.env['account.report']._get_res_ids_from_line_id(
            params['line_id'], ['res.currency', 'account.account'],
        )
        acct = self.env['account.account'].browse(id_map['account.account'])
        cur = self.env['res.currency'].browse(id_map['res.currency'])
        if cur in acct.exclude_provision_currency_ids:
            acct.exclude_provision_currency_ids -= cur
        else:
            acct.exclude_provision_currency_ids += cur
        return {'type': 'ir.actions.client', 'tag': 'reload'}

    def action_multi_currency_revaluation_open_currency_rates(self, options, params=None):
        cur_id = self.env['account.report']._get_res_id_from_line_id(
            params['line_id'], 'res.currency',
        )
        return {
            'type': 'ir.actions.act_window',
            'name': _('Currency Rates (%s)', self.env['res.currency'].browse(cur_id).display_name),
            'views': [(False, 'list')],
            'res_model': 'res.currency.rate',
            'context': {**self.env.context, 'default_currency_id': cur_id, 'active_id': cur_id},
            'domain': [('currency_id', '=', cur_id)],
        }

    # ---- Custom Engines ----
    def _report_custom_engine_multi_currency_revaluation_to_adjust(
        self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None,
    ):
        return self._revaluation_custom_lines(
            options, 'to_adjust', current_groupby, next_groupby, offset=offset, limit=limit,
        )

    def _report_custom_engine_multi_currency_revaluation_excluded(
        self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None,
    ):
        return self._revaluation_custom_lines(
            options, 'excluded', current_groupby, next_groupby, offset=offset, limit=limit,
        )

    def _revaluation_custom_lines(self, options, line_code, current_groupby, next_groupby, offset=0, limit=None):
        def _build_result(report_obj, qr):
            return {
                'balance_currency': qr['balance_currency'] if len(qr['currency_id']) == 1 else None,
                'currency_id': qr['currency_id'][0] if len(qr['currency_id']) == 1 else None,
                'balance_operation': qr['balance_operation'],
                'balance_current': qr['balance_current'],
                'adjustment': qr['adjustment'],
                'has_sublines': qr['aml_count'] > 0,
            }

        report = self.env['account.report'].browse(options['report_id'])
        report._check_groupby_fields(
            (next_groupby.split(',') if next_groupby else [])
            + ([current_groupby] if current_groupby else []),
        )

        if not current_groupby:
            return {
                'balance_currency': None, 'currency_id': None,
                'balance_operation': None, 'balance_current': None,
                'adjustment': None, 'has_sublines': False,
            }

        rate_values_sql = "(VALUES {})".format(
            ', '.join("(%s, %s)" for _ in options['currency_rates']),
        )
        rate_params = list(chain.from_iterable(
            (cr['currency_id'], cr['rate']) for cr in options['currency_rates'].values()
        ))
        custom_rate_table = SQL(rate_values_sql, *rate_params)
        report_date = options['date']['date_to']

        no_exchange_clause = SQL(
            """
            NOT EXISTS (
                SELECT 1
                  FROM account_partial_reconcile pr
                 WHERE pr.exchange_move_id = account_move_line.move_id
                   AND pr.max_date <= %s
            )
            """,
            report_date,
        )

        qry = report._get_report_query(options, 'strict_range')
        tail = report._get_engine_query_tail(offset, limit)

        provision_test = 'NOT EXISTS' if line_code == 'to_adjust' else 'EXISTS'

        groupby_col = f"account_move_line.{current_groupby}" if current_groupby else ''
        groupby_select = f"{groupby_col} AS grouping_key," if current_groupby else ''

        full_sql = SQL(
            """
            WITH custom_currency_table(currency_id, rate) AS (%(rate_table)s)
            SELECT
                   subquery.grouping_key,
                   ARRAY_AGG(DISTINCT(subquery.currency_id)) AS currency_id,
                   SUM(subquery.balance_currency) AS balance_currency,
                   SUM(subquery.balance_operation) AS balance_operation,
                   SUM(subquery.balance_current) AS balance_current,
                   SUM(subquery.adjustment) AS adjustment,
                   COUNT(subquery.aml_id) AS aml_count
              FROM (
                SELECT
                       """ + groupby_select + """
                       ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places) AS balance_operation,
                       ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) AS balance_currency,
                       ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) / custom_currency_table.rate AS balance_current,
                       (
                          ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) / custom_currency_table.rate
                          - ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places)
                       ) AS adjustment,
                       account_move_line.currency_id AS currency_id,
                       account_move_line.id AS aml_id
                  FROM %(from_refs)s,
                       account_account AS account,
                       res_currency AS aml_currency,
                       res_currency AS aml_comp_currency,
                       custom_currency_table,
                       LATERAL (
                               SELECT COALESCE(SUM(part.amount), 0.0) AS amount_debit,
                                      ROUND(SUM(part.debit_amount_currency), curr.decimal_places) AS amount_debit_currency,
                                      0.0 AS amount_credit, 0.0 AS amount_credit_currency,
                                      account_move_line.currency_id AS currency_id,
                                      account_move_line.id AS aml_id
                                 FROM account_partial_reconcile part
                                 JOIN res_currency curr ON curr.id = part.debit_currency_id
                                WHERE account_move_line.id = part.debit_move_id AND part.max_date <= %(dt)s
                             GROUP BY aml_id, curr.decimal_places
                           UNION
                               SELECT 0.0 AS amount_debit, 0.0 AS amount_debit_currency,
                                      COALESCE(SUM(part.amount), 0.0) AS amount_credit,
                                      ROUND(SUM(part.credit_amount_currency), curr.decimal_places) AS amount_credit_currency,
                                      account_move_line.currency_id AS currency_id,
                                      account_move_line.id AS aml_id
                                 FROM account_partial_reconcile part
                                 JOIN res_currency curr ON curr.id = part.credit_currency_id
                                WHERE account_move_line.id = part.credit_move_id AND part.max_date <= %(dt)s
                             GROUP BY aml_id, curr.decimal_places
                            ) AS ara
                 WHERE %(where)s
                   AND account_move_line.account_id = account.id
                   AND account_move_line.currency_id = aml_currency.id
                   AND account_move_line.company_currency_id = aml_comp_currency.id
                   AND account_move_line.currency_id = custom_currency_table.currency_id
                   AND account.account_type NOT IN ('income', 'income_other', 'expense', 'expense_depreciation', 'expense_direct_cost', 'off_balance')
                   AND (
                        account.currency_id != account_move_line.company_currency_id
                        OR (account.account_type IN ('asset_receivable', 'liability_payable')
                            AND account_move_line.currency_id != account_move_line.company_currency_id)
                   )
                   AND """ + provision_test + """ (
                        SELECT 1 FROM account_account_exclude_res_currency_provision
                         WHERE account_account_id = account_move_line.account_id
                           AND res_currency_id = account_move_line.currency_id
                   )
                   AND (%(no_exch)s)
              GROUP BY account_move_line.id, aml_comp_currency.decimal_places, aml_currency.decimal_places, custom_currency_table.rate
                HAVING ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places) != 0
                    OR ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) != 0.0

                UNION

                SELECT
                       """ + groupby_select + """
                       account_move_line.balance AS balance_operation,
                       account_move_line.amount_currency AS balance_currency,
                       account_move_line.amount_currency / custom_currency_table.rate AS balance_current,
                       account_move_line.amount_currency / custom_currency_table.rate - account_move_line.balance AS adjustment,
                       account_move_line.currency_id AS currency_id,
                       account_move_line.id AS aml_id
                  FROM %(from_refs)s
                  JOIN account_account account ON account_move_line.account_id = account.id
                  JOIN custom_currency_table ON custom_currency_table.currency_id = account_move_line.currency_id
                 WHERE %(where)s
                   AND account.account_type NOT IN ('income', 'income_other', 'expense', 'expense_depreciation', 'expense_direct_cost', 'off_balance')
                   AND (
                        account.currency_id != account_move_line.company_currency_id
                        OR (account.account_type IN ('asset_receivable', 'liability_payable')
                            AND account_move_line.currency_id != account_move_line.company_currency_id)
                   )
                   AND """ + provision_test + """ (
                        SELECT 1 FROM account_account_exclude_res_currency_provision
                         WHERE account_account_id = account_id
                           AND res_currency_id = account_move_line.currency_id
                   )
                   AND (%(no_exch)s)
                   AND NOT EXISTS (
                        SELECT 1 FROM account_partial_reconcile part
                        WHERE (part.debit_move_id = account_move_line.id OR part.credit_move_id = account_move_line.id)
                          AND part.max_date <= %(dt)s
                   )
                   AND (account_move_line.balance != 0.0 OR account_move_line.amount_currency != 0.0)
            ) subquery
            GROUP BY grouping_key
            ORDER BY grouping_key
            %(tail)s
            """,
            rate_table=custom_rate_table,
            from_refs=qry.from_clause,
            dt=report_date,
            where=qry.where_clause,
            no_exch=no_exchange_clause,
            tail=tail,
        )
        self.env.cr.execute(full_sql)
        rows = self.env.cr.dictfetchall()

        if not current_groupby:
            return _build_result(report, rows[0] if rows else {})
        return [(r['grouping_key'], _build_result(report, r)) for r in rows]
