# Fusion Accounting - Tax & Tax Unit Extensions
# Deferred date propagation in tax computations and tax unit management

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


class FusionAccountTax(models.Model):
    """Extends the tax engine to carry deferred revenue/expense dates
    through the base-line and tax-line computation pipeline."""

    _inherit = "account.tax"

    def _prepare_base_line_for_taxes_computation(self, record, **kwargs):
        """Inject deferred period dates into the base-line dictionary."""
        vals = super()._prepare_base_line_for_taxes_computation(record, **kwargs)
        vals['deferred_start_date'] = self._get_base_line_field_value_from_record(
            record, 'deferred_start_date', kwargs, False,
        )
        vals['deferred_end_date'] = self._get_base_line_field_value_from_record(
            record, 'deferred_end_date', kwargs, False,
        )
        return vals

    def _prepare_tax_line_for_taxes_computation(self, record, **kwargs):
        """Inject deferred period dates into the tax-line dictionary."""
        vals = super()._prepare_tax_line_for_taxes_computation(record, **kwargs)
        vals['deferred_start_date'] = self._get_base_line_field_value_from_record(
            record, 'deferred_start_date', kwargs, False,
        )
        vals['deferred_end_date'] = self._get_base_line_field_value_from_record(
            record, 'deferred_end_date', kwargs, False,
        )
        return vals

    def _prepare_base_line_grouping_key(self, base_line):
        """Include deferred dates in the grouping key so lines with
        different deferral periods are not merged."""
        grp_key = super()._prepare_base_line_grouping_key(base_line)
        grp_key['deferred_start_date'] = base_line['deferred_start_date']
        grp_key['deferred_end_date'] = base_line['deferred_end_date']
        return grp_key

    def _prepare_base_line_tax_repartition_grouping_key(
        self, base_line, base_line_grouping_key, tax_data, tax_rep_data,
    ):
        """Propagate deferred dates into the repartition grouping key
        only when the account is deferral-compatible and the tax
        repartition line does not participate in tax closing."""
        grp_key = super()._prepare_base_line_tax_repartition_grouping_key(
            base_line, base_line_grouping_key, tax_data, tax_rep_data,
        )
        source_record = base_line['record']
        is_deferral_eligible = (
            isinstance(source_record, models.Model)
            and source_record._name == 'account.move.line'
            and source_record._has_deferred_compatible_account()
            and base_line['deferred_start_date']
            and base_line['deferred_end_date']
            and not tax_rep_data['tax_rep'].use_in_tax_closing
        )
        if is_deferral_eligible:
            grp_key['deferred_start_date'] = base_line['deferred_start_date']
            grp_key['deferred_end_date'] = base_line['deferred_end_date']
        else:
            grp_key['deferred_start_date'] = False
            grp_key['deferred_end_date'] = False
        return grp_key

    def _prepare_tax_line_repartition_grouping_key(self, tax_line):
        """Mirror deferred dates from the tax line into its repartition
        grouping key."""
        grp_key = super()._prepare_tax_line_repartition_grouping_key(tax_line)
        grp_key['deferred_start_date'] = tax_line['deferred_start_date']
        grp_key['deferred_end_date'] = tax_line['deferred_end_date']
        return grp_key


class FusionTaxUnit(models.Model):
    """A tax unit groups multiple companies for consolidated tax
    reporting. Manages fiscal position synchronisation and
    horizontal-group linkage to generic tax reports."""

    _name = "account.tax.unit"
    _description = "Tax Unit"

    name = fields.Char(string="Name", required=True)
    country_id = fields.Many2one(
        comodel_name='res.country',
        string="Country",
        required=True,
        help="Jurisdiction under which this unit's consolidated tax returns are filed.",
    )
    vat = fields.Char(
        string="Tax ID",
        required=True,
        help="VAT identification number used when submitting the unit's declaration.",
    )
    company_ids = fields.Many2many(
        comodel_name='res.company',
        string="Companies",
        required=True,
        help="Member companies grouped under this unit.",
    )
    main_company_id = fields.Many2one(
        comodel_name='res.company',
        string="Main Company",
        required=True,
        help="The reporting entity responsible for filing and payment.",
    )
    fpos_synced = fields.Boolean(
        string="Fiscal Positions Synchronised",
        compute='_compute_fiscal_position_completion',
        help="Indicates whether fiscal positions exist for every member company.",
    )

    # ---- CRUD Overrides ----
    def create(self, vals_list):
        """After creation, set up horizontal groups on the generic tax
        reports so this unit appears in multi-company views."""
        records = super().create(vals_list)

        h_groups = self.env['account.report.horizontal.group'].create([
            {
                'name': unit.name,
                'rule_ids': [
                    Command.create({
                        'field_name': 'company_id',
                        'domain': f"[('account_tax_unit_ids', 'in', {unit.id})]",
                    }),
                ],
            }
            for unit in records
        ])

        # Link horizontal groups to all relevant tax reports
        report_refs = [
            'account.generic_tax_report',
            'account.generic_tax_report_account_tax',
            'account.generic_tax_report_tax_account',
            'fusion_accounting.generic_ec_sales_report',
        ]
        for ref_str in report_refs:
            tax_rpt = self.env.ref(ref_str)
            tax_rpt.horizontal_group_ids |= h_groups

        # Also attach to country-specific variants
        base_generic = self.env.ref('account.generic_tax_report')
        for unit in records:
            matching_variants = base_generic.variant_report_ids.filtered(
                lambda v: v.country_id == unit.country_id
            )
            matching_variants.write({
                'horizontal_group_ids': [Command.link(hg.id) for hg in h_groups],
            })

        return records

    def unlink(self):
        """Clean up fiscal positions before deletion."""
        self._get_tax_unit_fiscal_positions(
            companies=self.env['res.company'].search([]),
        ).unlink()
        return super().unlink()

    # ---- Computed Fields ----
    @api.depends('company_ids')
    def _compute_fiscal_position_completion(self):
        """Check whether every member company has a synchronised fiscal
        position mapping all other members' taxes to no-tax."""
        for unit in self:
            is_synced = True
            for company in unit.company_ids:
                origin = company._origin if isinstance(company.id, models.NewId) else company
                fp = unit._get_tax_unit_fiscal_positions(companies=origin)
                partners_with_fp = (
                    self.env['res.company']
                    .search([])
                    .with_company(origin)
                    .partner_id
                    .filtered(lambda p: p.property_account_position_id == fp)
                    if fp
                    else self.env['res.partner']
                )
                is_synced = partners_with_fp == (unit.company_ids - origin).partner_id
                if not is_synced:
                    break
            unit.fpos_synced = is_synced

    # ---- Fiscal Position Management ----
    def _get_tax_unit_fiscal_positions(self, companies, create_or_refresh=False):
        """Retrieve (or create) fiscal positions for each company in the
        unit. When *create_or_refresh* is True, positions are upserted
        with mappings that zero-out all company taxes.

        :param companies:          Companies to process.
        :param create_or_refresh:  If True, create/update the fiscal positions.
        :returns:                  Recordset of fiscal positions.
        """
        fp_set = self.env['account.fiscal.position'].with_context(
            allowed_company_ids=self.env.user.company_ids.ids,
        )
        for unit in self:
            for comp in companies:
                xml_ref = f'account.tax_unit_{unit.id}_fp_{comp.id}'
                existing = self.env.ref(xml_ref, raise_if_not_found=False)
                if create_or_refresh:
                    company_taxes = self.env['account.tax'].with_context(
                        allowed_company_ids=self.env.user.company_ids.ids,
                    ).search(self.env['account.tax']._check_company_domain(comp))
                    fp_data = {
                        'xml_id': xml_ref,
                        'values': {
                            'name': unit.name,
                            'company_id': comp.id,
                            'tax_ids': [Command.clear()] + [
                                Command.create({'tax_src_id': t.id}) for t in company_taxes
                            ],
                        },
                    }
                    existing = fp_set._load_records([fp_data])
                if existing:
                    fp_set += existing
        return fp_set

    def action_sync_unit_fiscal_positions(self):
        """Remove existing unit fiscal positions and recreate them
        with up-to-date tax mappings for all member companies."""
        self._get_tax_unit_fiscal_positions(
            companies=self.env['res.company'].search([]),
        ).unlink()
        for unit in self:
            for comp in unit.company_ids:
                fp = unit._get_tax_unit_fiscal_positions(
                    companies=comp, create_or_refresh=True,
                )
                (unit.company_ids - comp).with_company(comp).partner_id.property_account_position_id = fp

    # ---- Constraints ----
    @api.constrains('country_id', 'company_ids')
    def _validate_companies_country(self):
        """All member companies must share the same currency and each
        company may only belong to one unit per country."""
        for unit in self:
            currencies_seen = set()
            for comp in unit.company_ids:
                currencies_seen.add(comp.currency_id)
                other_units_same_country = any(
                    u != unit and u.country_id == unit.country_id
                    for u in comp.account_tax_unit_ids
                )
                if other_units_same_country:
                    raise ValidationError(_(
                        "Company %(company)s already belongs to a tax unit in "
                        "%(country)s. Each company can only participate in one "
                        "tax unit per country.",
                        company=comp.name,
                        country=unit.country_id.name,
                    ))
            if len(currencies_seen) > 1:
                raise ValidationError(
                    _("All companies within a tax unit must share the same primary currency.")
                )

    @api.constrains('company_ids', 'main_company_id')
    def _validate_main_company(self):
        """The designated main company must be among the unit's members."""
        for unit in self:
            if unit.main_company_id not in unit.company_ids:
                raise ValidationError(
                    _("The main company must be a member of the tax unit.")
                )

    @api.constrains('company_ids')
    def _validate_companies(self):
        """A tax unit requires at least two member companies."""
        for unit in self:
            if len(unit.company_ids) < 2:
                raise ValidationError(
                    _("A tax unit requires a minimum of two companies. "
                      "Consider deleting the unit instead.")
                )

    @api.constrains('country_id', 'vat')
    def _validate_vat(self):
        """Validate the VAT number against the unit's country."""
        for unit in self:
            if not unit.vat:
                continue
            detected_code = self.env['res.partner']._run_vat_test(
                unit.vat, unit.country_id,
            )
            if detected_code and detected_code != unit.country_id.code.lower():
                raise ValidationError(
                    _("The country derived from the VAT number does not match "
                      "the country configured on this tax unit.")
                )
            if not detected_code:
                unit_label = _("tax unit [%s]", unit.name)
                err_msg = self.env['res.partner']._build_vat_error_message(
                    unit.country_id.code.lower(), unit.vat, unit_label,
                )
                raise ValidationError(err_msg)

    # ---- Onchange ----
    @api.onchange('company_ids')
    def _onchange_company_ids(self):
        """Auto-select the first company as main when the current main
        is removed from the member list."""
        if self.main_company_id not in self.company_ids and self.company_ids:
            self.main_company_id = self.company_ids[0]._origin
        elif not self.company_ids:
            self.main_company_id = False
