# Fusion Accounting - Asset Modification Wizard
# Enables users to modify, dispose, sell, pause, or resume
# fixed assets with full depreciation board recalculation.

from dateutil.relativedelta import relativedelta

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


class AssetModify(models.TransientModel):
    """Wizard for modifying running assets, including value changes,
    depreciation parameter adjustments, disposal, and sales."""

    _name = 'asset.modify'
    _description = 'Modify Asset'

    # --- Core Fields ---
    name = fields.Text(string='Note')
    asset_id = fields.Many2one(
        string="Asset",
        comodel_name='account.asset',
        required=True,
        help="The target asset to modify.",
        ondelete="cascade",
    )
    currency_id = fields.Many2one(
        related='asset_id.currency_id',
    )
    company_id = fields.Many2one(
        comodel_name='res.company',
        related='asset_id.company_id',
    )
    date = fields.Date(
        default=lambda self: fields.Date.today(),
        string='Date',
    )

    # --- Depreciation Parameters ---
    method_number = fields.Integer(
        string='Duration',
        required=True,
    )
    method_period = fields.Selection(
        selection=[('1', 'Months'), ('12', 'Years')],
        string='Number of Months in a Period',
        help="Interval between successive depreciation entries.",
    )
    value_residual = fields.Monetary(
        string="Depreciable Amount",
        help="Updated depreciable amount for the asset.",
        compute="_compute_value_residual",
        store=True,
        readonly=False,
    )
    salvage_value = fields.Monetary(
        string="Not Depreciable Amount",
        help="Updated non-depreciable (salvage) amount.",
    )

    # --- Action Selection ---
    modify_action = fields.Selection(
        selection="_get_selection_modify_options",
        string="Action",
    )

    # --- Gross Increase Accounts ---
    account_asset_id = fields.Many2one(
        comodel_name='account.account',
        string="Gross Increase Account",
        check_company=True,
        domain="[]",
    )
    account_asset_counterpart_id = fields.Many2one(
        comodel_name='account.account',
        check_company=True,
        domain="[]",
        string="Asset Counterpart Account",
    )
    account_depreciation_id = fields.Many2one(
        comodel_name='account.account',
        check_company=True,
        domain="[]",
        string="Depreciation Account",
    )
    account_depreciation_expense_id = fields.Many2one(
        comodel_name='account.account',
        check_company=True,
        domain="[]",
        string="Expense Account",
    )

    # --- Sale / Disposal Fields ---
    invoice_ids = fields.Many2many(
        comodel_name='account.move',
        string="Customer Invoice",
        check_company=True,
        domain="[('move_type', '=', 'out_invoice'), ('state', '=', 'posted')]",
        help="Invoice(s) linked to this disposal or sale.",
    )
    invoice_line_ids = fields.Many2many(
        comodel_name='account.move.line',
        check_company=True,
        domain="[('move_id', '=', invoice_id), ('display_type', '=', 'product')]",
        help="Specific invoice line(s) related to this asset.",
    )
    select_invoice_line_id = fields.Boolean(
        compute="_compute_select_invoice_line_id",
    )
    gain_value = fields.Boolean(
        compute="_compute_gain_value",
    )

    # --- Gain/Loss Accounts ---
    gain_account_id = fields.Many2one(
        comodel_name='account.account',
        check_company=True,
        domain="[]",
        compute="_compute_accounts",
        inverse="_inverse_gain_account",
        readonly=False,
        compute_sudo=True,
        help="Account for recording gains on asset disposal.",
    )
    loss_account_id = fields.Many2one(
        comodel_name='account.account',
        check_company=True,
        domain="[]",
        compute="_compute_accounts",
        inverse="_inverse_loss_account",
        readonly=False,
        compute_sudo=True,
        help="Account for recording losses on asset disposal.",
    )

    # --- Informational ---
    informational_text = fields.Html(
        compute='_compute_informational_text',
    )
    gain_or_loss = fields.Selection(
        selection=[('gain', 'Gain'), ('loss', 'Loss'), ('no', 'No')],
        compute='_compute_gain_or_loss',
    )

    # -------------------------------------------------------------------------
    # Selection Helpers
    # -------------------------------------------------------------------------

    def _compute_modify_action(self):
        """Determine the default action based on context."""
        if self.env.context.get('resume_after_pause'):
            return 'resume'
        return 'dispose'

    @api.depends('asset_id')
    def _get_selection_modify_options(self):
        """Return available modification actions."""
        if self.env.context.get('resume_after_pause'):
            return [('resume', _('Resume'))]
        return [
            ('dispose', _("Dispose")),
            ('sell', _("Sell")),
            ('modify', _("Re-evaluate")),
            ('pause', _("Pause")),
        ]

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

    @api.depends('company_id')
    def _compute_accounts(self):
        for rec in self:
            rec.gain_account_id = rec.company_id.gain_account_id
            rec.loss_account_id = rec.company_id.loss_account_id

    @api.depends('date')
    def _compute_value_residual(self):
        for rec in self:
            rec.value_residual = rec.asset_id._get_residual_value_at_date(rec.date)

    def _inverse_gain_account(self):
        for rec in self:
            rec.company_id.sudo().gain_account_id = rec.gain_account_id

    def _inverse_loss_account(self):
        for rec in self:
            rec.company_id.sudo().loss_account_id = rec.loss_account_id

    @api.depends('asset_id', 'invoice_ids', 'invoice_line_ids', 'modify_action', 'date')
    def _compute_gain_or_loss(self):
        """Determine whether disposing/selling results in a gain or loss."""
        for rec in self:
            invoice_total = abs(sum(
                ln.balance for ln in rec.invoice_line_ids
            ))
            book_val = rec.asset_id._get_own_book_value(rec.date)
            cmp_result = rec.company_id.currency_id.compare_amounts(
                book_val, invoice_total,
            )
            if rec.modify_action in ('sell', 'dispose') and cmp_result < 0:
                rec.gain_or_loss = 'gain'
            elif rec.modify_action in ('sell', 'dispose') and cmp_result > 0:
                rec.gain_or_loss = 'loss'
            else:
                rec.gain_or_loss = 'no'

    @api.depends('asset_id', 'value_residual', 'salvage_value')
    def _compute_gain_value(self):
        """Check whether the modification increases the asset's book value."""
        for rec in self:
            rec.gain_value = rec.currency_id.compare_amounts(
                rec._get_own_book_value(),
                rec.asset_id._get_own_book_value(rec.date),
            ) > 0

    @api.depends('loss_account_id', 'gain_account_id', 'gain_or_loss',
                 'modify_action', 'date', 'value_residual', 'salvage_value')
    def _compute_informational_text(self):
        """Generate user-facing description of what will happen."""
        for wiz in self:
            formatted_date = format_date(self.env, wiz.date)

            if wiz.modify_action == 'dispose':
                acct_name, result_label = '', _('gain/loss')
                if wiz.gain_or_loss == 'gain':
                    acct_name = wiz.gain_account_id.display_name or ''
                    result_label = _('gain')
                elif wiz.gain_or_loss == 'loss':
                    acct_name = wiz.loss_account_id.display_name or ''
                    result_label = _('loss')
                wiz.informational_text = _(
                    "Depreciation will be posted through %(date)s."
                    "<br/> A disposal entry will go to the %(account_type)s "
                    "account <b>%(account)s</b>.",
                    date=formatted_date,
                    account_type=result_label,
                    account=acct_name,
                )

            elif wiz.modify_action == 'sell':
                acct_name = ''
                if wiz.gain_or_loss == 'gain':
                    acct_name = wiz.gain_account_id.display_name or ''
                elif wiz.gain_or_loss == 'loss':
                    acct_name = wiz.loss_account_id.display_name or ''
                wiz.informational_text = _(
                    "Depreciation will be posted through %(date)s."
                    "<br/> A secondary entry will neutralize the original "
                    "revenue and post the sale outcome to account "
                    "<b>%(account)s</b>.",
                    date=formatted_date,
                    account=acct_name,
                )

            elif wiz.modify_action == 'pause':
                wiz.informational_text = _(
                    "Depreciation will be posted through %s.",
                    formatted_date,
                )

            elif wiz.modify_action == 'modify':
                increase_note = (
                    _("A child asset will be created for the value increase. <br/>")
                    if wiz.gain_value else ""
                )
                wiz.informational_text = _(
                    "Depreciation will be posted through %(date)s. <br/> "
                    "%(extra_text)s Future entries will be recalculated to "
                    "reflect the updated parameters.",
                    date=formatted_date,
                    extra_text=increase_note,
                )

            else:
                # Resume or other
                increase_note = (
                    _("A child asset will be created for the value increase. <br/>")
                    if wiz.gain_value else ""
                )
                wiz.informational_text = _(
                    "%s Future entries will be recalculated to reflect "
                    "the updated parameters.",
                    increase_note,
                )

    @api.depends('invoice_ids', 'modify_action')
    def _compute_select_invoice_line_id(self):
        for rec in self:
            rec.select_invoice_line_id = (
                rec.modify_action == 'sell'
                and len(rec.invoice_ids.invoice_line_ids) > 1
            )

    # -------------------------------------------------------------------------
    # Onchange
    # -------------------------------------------------------------------------

    @api.onchange('modify_action')
    def _onchange_action(self):
        if (
            self.modify_action == 'sell'
            and self.asset_id.children_ids.filtered(
                lambda child: child.state in ('draft', 'open')
                or child.value_residual > 0
            )
        ):
            raise UserError(_(
                "Cannot automate the sale journal entry for an asset "
                "with active gross increases. Please dispose of the "
                "increase(s) first."
            ))
        if self.modify_action not in ('modify', 'resume'):
            self.write({
                'value_residual': self.asset_id._get_residual_value_at_date(self.date),
                'salvage_value': self.asset_id.salvage_value,
            })

    @api.onchange('invoice_ids')
    def _onchange_invoice_ids(self):
        """Keep invoice_line_ids in sync when invoices change."""
        valid_line_ids = self.invoice_ids.invoice_line_ids.filtered(
            lambda ln: ln._origin.id in self.invoice_line_ids.ids,
        )
        self.invoice_line_ids = valid_line_ids
        # Auto-select lines for single-line invoices
        for inv in self.invoice_ids.filtered(lambda i: len(i.invoice_line_ids) == 1):
            self.invoice_line_ids += inv.invoice_line_ids

    # -------------------------------------------------------------------------
    # CRUD Override
    # -------------------------------------------------------------------------

    @api.model_create_multi
    def create(self, vals_list):
        """Populate defaults from the linked asset when not provided."""
        AssetModel = self.env['account.asset']
        for vals in vals_list:
            if 'asset_id' not in vals:
                continue
            asset = AssetModel.browse(vals['asset_id'])
            # Block if future posted depreciation exists
            future_posted = asset.depreciation_move_ids.filtered(
                lambda mv: (
                    mv.state == 'posted'
                    and not mv.reversal_move_ids
                    and mv.date > fields.Date.today()
                )
            )
            if future_posted:
                raise UserError(_(
                    'Please reverse any future-dated depreciation entries '
                    'before modifying this asset.'
                ))
            # Fill in missing defaults from the asset
            field_defaults = {
                'method_number': asset.method_number,
                'method_period': asset.method_period,
                'salvage_value': asset.salvage_value,
                'account_asset_id': asset.account_asset_id.id,
                'account_depreciation_id': asset.account_depreciation_id.id,
                'account_depreciation_expense_id': asset.account_depreciation_expense_id.id,
            }
            for fld, default_val in field_defaults.items():
                if fld not in vals:
                    vals[fld] = default_val
        return super().create(vals_list)

    # -------------------------------------------------------------------------
    # Main Business Actions
    # -------------------------------------------------------------------------

    def modify(self):
        """Re-evaluate the asset: update depreciation parameters,
        handle value increases/decreases, and recompute the schedule."""
        lock_threshold = self.asset_id.company_id._get_user_fiscal_lock_date(
            self.asset_id.journal_id,
        )
        if self.date <= lock_threshold:
            raise UserError(_(
                "The selected date is on or before the fiscal lock date. "
                "Re-evaluation is not permitted."
            ))

        # Snapshot current values for change tracking
        prior_vals = {
            'method_number': self.asset_id.method_number,
            'method_period': self.asset_id.method_period,
            'value_residual': self.asset_id.value_residual,
            'salvage_value': self.asset_id.salvage_value,
        }

        updated_vals = {
            'method_number': self.method_number,
            'method_period': self.method_period,
            'salvage_value': self.salvage_value,
            'account_asset_id': self.account_asset_id,
            'account_depreciation_id': self.account_depreciation_id,
            'account_depreciation_expense_id': self.account_depreciation_expense_id,
        }

        # Handle resume-after-pause scenario
        is_resuming = self.env.context.get('resume_after_pause')
        if is_resuming:
            latest_depr = self.asset_id.depreciation_move_ids
            last_depr_date = (
                max(latest_depr, key=lambda m: m.date).date
                if latest_depr else self.asset_id.acquisition_date
            )
            gap_days = self.asset_id._get_delta_days(last_depr_date, self.date) - 1
            if self.currency_id.compare_amounts(gap_days, 0) < 0:
                raise UserError(_(
                    "Resume date must be after the pause date."
                ))
            updated_vals['asset_paused_days'] = (
                self.asset_id.asset_paused_days + gap_days
            )
            updated_vals['state'] = 'open'
            self.asset_id.message_post(
                body=_("Asset resumed. %s", self.name),
            )

        # Compute value changes
        current_book = self.asset_id._get_own_book_value(self.date)
        target_book = self._get_own_book_value()
        book_increase = target_book - current_book

        new_residual, new_salvage = self._get_new_asset_values(current_book)
        residual_diff = max(0, self.value_residual - new_residual)
        salvage_diff = max(0, self.salvage_value - new_salvage)

        # Create depreciation up to modification date (unless resuming)
        if not is_resuming:
            draft_before = self.env['account.move'].search_count([
                ('asset_id', '=', self.asset_id.id),
                ('state', '=', 'draft'),
                ('date', '<=', self.date),
            ], limit=1)
            if draft_before:
                raise UserError(_(
                    'Unposted depreciation entries exist before the '
                    'selected date. Please process them first.'
                ))
            self.asset_id._create_move_before_date(self.date)

        updated_vals['salvage_value'] = new_salvage

        # Detect if child assets need recomputation
        schedule_changed = (
            updated_vals['method_number'] != self.asset_id.method_number
            or updated_vals['method_period'] != self.asset_id.method_period
            or (
                updated_vals.get('asset_paused_days')
                and not float_is_zero(
                    updated_vals['asset_paused_days']
                    - self.asset_id.asset_paused_days, 8,
                )
            )
        )
        self.asset_id.write(updated_vals)

        # Create gross increase asset if value went up
        total_increase = residual_diff + salvage_diff
        if self.currency_id.compare_amounts(total_increase, 0) > 0:
            increase_date = self.date + relativedelta(days=1)
            increase_entry = self.env['account.move'].create({
                'journal_id': self.asset_id.journal_id.id,
                'date': increase_date,
                'move_type': 'entry',
                'asset_move_type': 'positive_revaluation',
                'line_ids': [
                    Command.create({
                        'account_id': self.account_asset_id.id,
                        'debit': total_increase,
                        'credit': 0,
                        'name': _(
                            'Value increase for: %(asset)s',
                            asset=self.asset_id.name,
                        ),
                    }),
                    Command.create({
                        'account_id': self.account_asset_counterpart_id.id,
                        'debit': 0,
                        'credit': total_increase,
                        'name': _(
                            'Value increase for: %(asset)s',
                            asset=self.asset_id.name,
                        ),
                    }),
                ],
            })
            increase_entry._post()

            child_asset = self.env['account.asset'].create({
                'name': (
                    self.asset_id.name + ': ' + self.name
                    if self.name else ""
                ),
                'currency_id': self.asset_id.currency_id.id,
                'company_id': self.asset_id.company_id.id,
                'method': self.asset_id.method,
                'method_number': self.method_number,
                'method_period': self.method_period,
                'method_progress_factor': self.asset_id.method_progress_factor,
                'acquisition_date': increase_date,
                'value_residual': residual_diff,
                'salvage_value': salvage_diff,
                'prorata_date': increase_date,
                'prorata_computation_type': (
                    'daily_computation'
                    if self.asset_id.prorata_computation_type == 'daily_computation'
                    else 'constant_periods'
                ),
                'original_value': self._get_increase_original_value(
                    residual_diff, salvage_diff,
                ),
                'account_asset_id': self.account_asset_id.id,
                'account_depreciation_id': self.account_depreciation_id.id,
                'account_depreciation_expense_id': self.account_depreciation_expense_id.id,
                'journal_id': self.asset_id.journal_id.id,
                'parent_id': self.asset_id.id,
                'original_move_line_ids': [
                    (6, 0, increase_entry.line_ids.filtered(
                        lambda ln: ln.account_id == self.account_asset_id,
                    ).ids),
                ],
            })
            child_asset.validate()

            link_html = child_asset._get_html_link()
            self.asset_id.message_post(
                body=_('Gross increase created: %(link)s', link=link_html),
            )

        # Create negative revaluation entry if value went down
        if self.currency_id.compare_amounts(book_increase, 0) < 0:
            depr_vals = self.env['account.move']._prepare_move_for_asset_depreciation({
                'amount': -book_increase,
                'asset_id': self.asset_id,
                'move_ref': _(
                    'Value decrease for: %(asset)s', asset=self.asset_id.name,
                ),
                'depreciation_beginning_date': self.date,
                'depreciation_end_date': self.date,
                'date': self.date,
                'asset_number_days': 0,
                'asset_value_change': True,
                'asset_move_type': 'negative_revaluation',
            })
            self.env['account.move'].create(depr_vals)._post()

        # Recompute depreciation schedule
        board_start = self.date if is_resuming else self.date + relativedelta(days=1)
        if self.asset_id.depreciation_move_ids:
            self.asset_id.compute_depreciation_board(board_start)
        else:
            self.asset_id.compute_depreciation_board()

        # Propagate changes to child assets if schedule params changed
        if schedule_changed:
            child_assets = self.asset_id.children_ids
            child_assets.write({
                'method_number': updated_vals['method_number'],
                'method_period': updated_vals['method_period'],
                'asset_paused_days': self.asset_id.asset_paused_days,
            })
            for child in child_assets:
                if not is_resuming:
                    child._create_move_before_date(self.date)
                if child.depreciation_move_ids:
                    child.compute_depreciation_board(board_start)
                else:
                    child.compute_depreciation_board()
                child._check_depreciations()
                child.depreciation_move_ids.filtered(
                    lambda mv: mv.state != 'posted',
                )._post()

        # Log tracked changes in the chatter
        tracked_fields = self.env['account.asset'].fields_get(prior_vals.keys())
        changes, tracking_vals = self.asset_id._mail_track(
            tracked_fields, prior_vals,
        )
        if changes:
            self.asset_id.message_post(
                body=_('Depreciation board modified %s', self.name),
                tracking_value_ids=tracking_vals,
            )

        self.asset_id._check_depreciations()
        self.asset_id.depreciation_move_ids.filtered(
            lambda mv: mv.state != 'posted',
        )._post()

        return {'type': 'ir.actions.act_window_close'}

    def pause(self):
        """Pause depreciation for the asset."""
        for rec in self:
            rec.asset_id.pause(pause_date=rec.date, message=self.name)

    def sell_dispose(self):
        """Dispose of or sell the asset, generating closing entries."""
        self.ensure_one()
        if (
            self.gain_account_id == self.asset_id.account_depreciation_id
            or self.loss_account_id == self.asset_id.account_depreciation_id
        ):
            raise UserError(_(
                "The gain/loss account cannot be the same as the "
                "Depreciation Account."
            ))
        disposal_lines = (
            self.env['account.move.line']
            if self.modify_action == 'dispose'
            else self.invoice_line_ids
        )
        return self.asset_id.set_to_close(
            invoice_line_ids=disposal_lines,
            date=self.date,
            message=self.name,
        )

    # -------------------------------------------------------------------------
    # Utility Methods
    # -------------------------------------------------------------------------

    def _get_own_book_value(self):
        """Return the wizard's configured book value (residual + salvage)."""
        return self.value_residual + self.salvage_value

    def _get_increase_original_value(self, residual_increase, salvage_increase):
        """Compute the original value for a gross increase child asset."""
        return residual_increase + salvage_increase

    def _get_new_asset_values(self, current_asset_book):
        """Calculate capped residual and salvage values to ensure
        they don't exceed the current book value."""
        self.ensure_one()
        capped_residual = min(
            current_asset_book - min(self.salvage_value, self.asset_id.salvage_value),
            self.value_residual,
        )
        capped_salvage = min(
            current_asset_book - capped_residual,
            self.salvage_value,
        )
        return capped_residual, capped_salvage
