# Fusion Accounting - Multicurrency Revaluation Wizard
# Creates journal entries to adjust for exchange rate differences
# on foreign-currency denominated balances, with automatic reversal.

import json
from dateutil.relativedelta import relativedelta

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


class MulticurrencyRevaluationWizard(models.TransientModel):
    """Generates revaluation journal entries that capture unrealized
    gains or losses from exchange rate fluctuations, then creates
    an auto-reversal entry for the following period."""

    _name = 'account.multicurrency.revaluation.wizard'
    _description = 'Multicurrency Revaluation Wizard'

    # --- Organization ---
    company_id = fields.Many2one(
        comodel_name='res.company',
        default=lambda self: self.env.company,
    )

    # --- Accounting Configuration ---
    journal_id = fields.Many2one(
        comodel_name='account.journal',
        compute='_compute_accounting_values',
        inverse='_inverse_revaluation_journal',
        compute_sudo=True,
        domain=[('type', '=', 'general')],
        required=True,
        readonly=False,
    )
    date = fields.Date(
        default=lambda self: self.env.context[
            'multicurrency_revaluation_report_options'
        ]['date']['date_to'],
        required=True,
    )
    reversal_date = fields.Date(required=True)
    expense_provision_account_id = fields.Many2one(
        comodel_name='account.account',
        compute='_compute_accounting_values',
        inverse='_inverse_expense_provision_account',
        compute_sudo=True,
        string="Expense Account",
        required=True,
        readonly=False,
    )
    income_provision_account_id = fields.Many2one(
        comodel_name='account.account',
        compute='_compute_accounting_values',
        inverse='_inverse_income_provision_account',
        compute_sudo=True,
        string="Income Account",
        required=True,
        readonly=False,
    )

    # --- Preview & Warnings ---
    preview_data = fields.Text(
        compute='_compute_preview_data',
    )
    show_warning_move_id = fields.Many2one(
        comodel_name='account.move',
        compute='_compute_show_warning',
    )

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

    @api.model
    def default_get(self, default_fields):
        """Initialize reversal date and validate that adjustments are needed."""
        result = super().default_get(default_fields)
        if 'reversal_date' in default_fields:
            report_opts = self.env.context['multicurrency_revaluation_report_options']
            period_end = fields.Date.to_date(report_opts['date']['date_to'])
            result['reversal_date'] = period_end + relativedelta(days=1)

        # Verify there is actually something to adjust
        if (
            not self.env.context.get('revaluation_no_loop')
            and not self.with_context(
                revaluation_no_loop=True,
            )._get_move_vals()['line_ids']
        ):
            raise UserError(_("No currency adjustment is needed."))
        return result

    # -------------------------------------------------------------------------
    # Compute Methods
    # -------------------------------------------------------------------------

    @api.depends('expense_provision_account_id', 'income_provision_account_id',
                 'reversal_date')
    def _compute_show_warning(self):
        """Warn if there's an unreversed entry on the provision accounts."""
        AML = self.env['account.move.line']
        for wiz in self:
            provision_accts = (
                wiz.expense_provision_account_id
                + wiz.income_provision_account_id
            )
            recent_entry = AML.search([
                ('account_id', 'in', provision_accts.ids),
                ('date', '<', wiz.reversal_date),
            ], order='date desc', limit=1).move_id
            wiz.show_warning_move_id = (
                False if recent_entry.reversed_entry_id else recent_entry
            )

    @api.depends('expense_provision_account_id', 'income_provision_account_id',
                 'date', 'journal_id')
    def _compute_preview_data(self):
        """Build JSON data for the move preview widget."""
        col_definitions = [
            {'field': 'account_id', 'label': _("Account")},
            {'field': 'name', 'label': _("Label")},
            {'field': 'debit', 'label': _("Debit"), 'class': 'text-end text-nowrap'},
            {'field': 'credit', 'label': _("Credit"), 'class': 'text-end text-nowrap'},
        ]
        for wiz in self:
            move_vals = self._get_move_vals()
            preview_groups = [
                self.env['account.move']._move_dict_to_preview_vals(
                    move_vals, wiz.company_id.currency_id,
                ),
            ]
            wiz.preview_data = json.dumps({
                'groups_vals': preview_groups,
                'options': {'columns': col_definitions},
            })

    @api.depends('company_id')
    def _compute_accounting_values(self):
        """Load revaluation settings from the company."""
        for wiz in self:
            wiz.journal_id = wiz.company_id.account_revaluation_journal_id
            wiz.expense_provision_account_id = (
                wiz.company_id.account_revaluation_expense_provision_account_id
            )
            wiz.income_provision_account_id = (
                wiz.company_id.account_revaluation_income_provision_account_id
            )

    # -------------------------------------------------------------------------
    # Inverse Methods (persist settings back to company)
    # -------------------------------------------------------------------------

    def _inverse_revaluation_journal(self):
        for wiz in self:
            wiz.company_id.sudo().account_revaluation_journal_id = wiz.journal_id

    def _inverse_expense_provision_account(self):
        for wiz in self:
            wiz.company_id.sudo().account_revaluation_expense_provision_account_id = (
                wiz.expense_provision_account_id
            )

    def _inverse_income_provision_account(self):
        for wiz in self:
            wiz.company_id.sudo().account_revaluation_income_provision_account_id = (
                wiz.income_provision_account_id
            )

    # -------------------------------------------------------------------------
    # Move Value Construction
    # -------------------------------------------------------------------------

    @api.model
    def _get_move_vals(self):
        """Build the journal entry values from the multicurrency
        revaluation report data, creating a pair of lines per
        account/currency that needs adjustment."""

        def _extract_model_id(parsed_segments, target_model):
            """Extract the record ID for a given model from parsed line segments."""
            for _dummy, seg_model, seg_id in parsed_segments:
                if seg_model == target_model:
                    return seg_id
            return None

        def _extract_adjustment(report_line):
            """Pull the adjustment amount from the report line columns."""
            for col in report_line.get('columns', []):
                if col.get('expression_label') == 'adjustment':
                    return col.get('no_format')
            return None

        report = self.env.ref('fusion_accounting.multicurrency_revaluation_report')
        included_section = report.line_ids.filtered(
            lambda ln: ln.code == 'multicurrency_included',
        )
        included_line_id = report._get_generic_line_id(
            'account.report.line', included_section.id,
        )

        report_options = {
            **self.env.context['multicurrency_revaluation_report_options'],
            'unfold_all': False,
        }
        all_report_lines = report._get_lines(report_options)
        entry_lines = []

        for rpt_line in report._get_unfolded_lines(
            all_report_lines, included_line_id,
        ):
            parsed = report._parse_line_id(rpt_line.get('id'))
            adj_balance = _extract_adjustment(rpt_line)

            # Only process account-level lines with non-zero adjustments
            if parsed[-1][-2] != 'account.account':
                continue
            if self.env.company.currency_id.is_zero(adj_balance):
                continue

            target_account = _extract_model_id(parsed, 'account.account')
            target_currency = _extract_model_id(parsed, 'res.currency')
            currency_name = self.env['res.currency'].browse(
                target_currency,
            ).display_name
            company_cur_name = self.env.company.currency_id.display_name
            current_rate = report_options['currency_rates'][
                str(target_currency)
            ]['rate']

            # Account adjustment line
            entry_lines.append(Command.create({
                'name': _(
                    "Provision for %(for_cur)s "
                    "(1 %(comp_cur)s = %(rate)s %(for_cur)s)",
                    for_cur=currency_name,
                    comp_cur=company_cur_name,
                    rate=current_rate,
                ),
                'debit': adj_balance if adj_balance > 0 else 0,
                'credit': -adj_balance if adj_balance < 0 else 0,
                'amount_currency': 0,
                'currency_id': target_currency,
                'account_id': target_account,
            }))

            # Provision counterpart line
            if adj_balance < 0:
                provision_label = _(
                    "Expense Provision for %s", currency_name,
                )
                provision_account = self.expense_provision_account_id.id
            else:
                provision_label = _(
                    "Income Provision for %s", currency_name,
                )
                provision_account = self.income_provision_account_id.id

            entry_lines.append(Command.create({
                'name': provision_label,
                'debit': -adj_balance if adj_balance < 0 else 0,
                'credit': adj_balance if adj_balance > 0 else 0,
                'amount_currency': 0,
                'currency_id': target_currency,
                'account_id': provision_account,
            }))

        return {
            'ref': _(
                "Foreign currency adjustment as of %s",
                format_date(self.env, self.date),
            ),
            'journal_id': self.journal_id.id,
            'date': self.date,
            'line_ids': entry_lines,
        }

    # -------------------------------------------------------------------------
    # Main Action
    # -------------------------------------------------------------------------

    def create_entries(self):
        """Create the revaluation entry and its automatic reversal."""
        self.ensure_one()
        move_data = self._get_move_vals()

        if not move_data['line_ids']:
            raise UserError(_("No provision adjustment is required."))

        # Create and post the revaluation entry
        reval_move = self.env['account.move'].create(move_data)
        reval_move.action_post()

        # Create and post the reversal
        reversal = reval_move._reverse_moves(default_values_list=[{
            'ref': _("Reversal of: %s", reval_move.ref),
        }])
        reversal.date = self.reversal_date
        reversal.action_post()

        # Open the revaluation entry in form view
        form_view = self.env.ref('account.view_move_form', False)
        clean_ctx = {
            k: v for k, v in self.env.context.items()
            if k != 'id'
        }
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'account.move',
            'res_id': reval_move.id,
            'view_mode': 'form',
            'view_id': form_view.id,
            'views': [(form_view.id, 'form')],
            'context': clean_ctx,
        }
