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

from odoo import api, Command, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import date_utils, float_is_zero, float_round


class FusionBudget(models.Model):
    """Represents a financial budget linked to accounting reports.

    A budget groups together individual budget line items, each targeting
    a specific account and month. Budgets are company-specific and appear
    as additional columns in accounting reports.
    """

    _name = 'account.report.budget'
    _description = "Fusion Report Budget"
    _order = 'sequence, id'

    name = fields.Char(
        string="Budget Name",
        required=True,
    )
    sequence = fields.Integer(
        string="Display Order",
        default=10,
    )
    company_id = fields.Many2one(
        comodel_name='res.company',
        string="Company",
        required=True,
        default=lambda self: self.env.company,
    )
    item_ids = fields.One2many(
        comodel_name='account.report.budget.item',
        inverse_name='budget_id',
        string="Budget Lines",
    )

    # --------------------------------------------------
    # CRUD
    # --------------------------------------------------

    @api.model_create_multi
    def create(self, vals_list):
        """Override create to sanitize the budget name by stripping whitespace."""
        for record_vals in vals_list:
            raw_name = record_vals.get('name')
            if raw_name:
                record_vals['name'] = raw_name.strip()
        return super().create(vals_list)

    # --------------------------------------------------
    # Constraints
    # --------------------------------------------------

    @api.constrains('name')
    def _check_budget_name_not_empty(self):
        """Ensure every budget record has a non-empty name."""
        for record in self:
            if not record.name or not record.name.strip():
                raise ValidationError(
                    _("A budget must have a non-empty name.")
                )

    # --------------------------------------------------
    # Duplication helpers
    # --------------------------------------------------

    def copy_data(self, default=None):
        """Append '(copy)' suffix to duplicated budget names."""
        data_list = super().copy_data(default=default)
        result = []
        for budget, vals in zip(self, data_list):
            vals['name'] = _("%s (copy)", budget.name)
            result.append(vals)
        return result

    def copy(self, default=None):
        """Duplicate budgets together with their line items."""
        duplicated_budgets = super().copy(default)
        for source_budget, target_budget in zip(self, duplicated_budgets):
            for line in source_budget.item_ids:
                line.copy({
                    'budget_id': target_budget.id,
                    'account_id': line.account_id.id,
                    'amount': line.amount,
                    'date': line.date,
                })
        return duplicated_budgets

    # --------------------------------------------------
    # Budget item management (called from report engine)
    # --------------------------------------------------

    def _create_or_update_budget_items(
        self, value_to_set, account_id, rounding, date_from, date_to
    ):
        """Distribute a target amount across monthly budget items.

        When the user edits a budget cell in the report view, this method
        calculates the difference between the desired total and the existing
        total for the given account/date range, then distributes that delta
        evenly across the months in the range.

        Existing items within the range are updated in place; new items are
        created for months that don't have one yet.

        Args:
            value_to_set: The desired total amount for the date range.
            account_id: The ``account.account`` record id.
            rounding: Number of decimal digits for monetary precision.
            date_from: Start date (inclusive) of the budget period.
            date_to: End date (inclusive) of the budget period.
        """
        self.ensure_one()

        period_start = fields.Date.to_date(date_from)
        period_end = fields.Date.to_date(date_to)

        BudgetItem = self.env['account.report.budget.item']

        # Fetch all items that already cover (part of) the requested range
        matching_items = BudgetItem.search_fetch(
            [
                ('budget_id', '=', self.id),
                ('account_id', '=', account_id),
                ('date', '>=', period_start),
                ('date', '<=', period_end),
            ],
            ['id', 'amount'],
        )
        current_total = sum(matching_items.mapped('amount'))

        # Calculate the remaining amount to distribute
        remaining_delta = value_to_set - current_total
        if float_is_zero(remaining_delta, precision_digits=rounding):
            return

        # Build a list of first-of-month dates spanning the period
        month_starts = [
            date_utils.start_of(d, 'month')
            for d in date_utils.date_range(period_start, period_end)
        ]
        month_count = len(month_starts)

        # Spread the delta equally across months (rounding down),
        # then assign any leftover cents to the final month.
        per_month = float_round(
            remaining_delta / month_count,
            precision_digits=rounding,
            rounding_method='DOWN',
        )
        monthly_portions = [per_month] * month_count
        distributed_sum = float_round(sum(monthly_portions), precision_digits=rounding)
        monthly_portions[-1] += float_round(
            remaining_delta - distributed_sum,
            precision_digits=rounding,
        )

        # Pair existing items with months and amounts; create or update as needed
        write_commands = []
        idx = 0
        for month_date, portion in zip(month_starts, monthly_portions):
            if idx < len(matching_items):
                # Update an existing item
                existing = matching_items[idx]
                write_commands.append(
                    Command.update(existing.id, {
                        'amount': existing.amount + portion,
                    })
                )
            else:
                # No existing item for this slot – create a new one
                write_commands.append(
                    Command.create({
                        'account_id': account_id,
                        'amount': portion,
                        'date': month_date,
                    })
                )
            idx += 1

        if write_commands:
            self.item_ids = write_commands
            # Ensure the ORM flushes new records to the database so
            # subsequent queries within the same request see them.
            BudgetItem.flush_model()


class FusionBudgetItem(models.Model):
    """A single monthly budget entry for one account within a budget.

    Each item records a monetary amount allocated to a specific
    ``account.account`` for a particular month. The ``date`` field
    stores the first day of the relevant month.
    """

    _name = 'account.report.budget.item'
    _description = "Fusion Report Budget Line"

    budget_id = fields.Many2one(
        comodel_name='account.report.budget',
        string="Parent Budget",
        required=True,
        ondelete='cascade',
    )
    account_id = fields.Many2one(
        comodel_name='account.account',
        string="Account",
        required=True,
    )
    date = fields.Date(
        string="Month",
        required=True,
    )
    amount = fields.Float(
        string="Budgeted Amount",
        default=0.0,
    )
