# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

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


class ResCurrency(models.Model):
    _inherit = 'res.currency'

    def _get_fiscal_country_codes(self):
        return ','.join(self.env.companies.mapped('account_fiscal_country_id.code'))

    display_rounding_warning = fields.Boolean(string="Display Rounding Warning", compute='_compute_display_rounding_warning',
        help="The warning informs a rounding factor change might be dangerous on res.currency's form view.")
    fiscal_country_codes = fields.Char(store=False, default=_get_fiscal_country_codes)

    @api.depends('rounding')
    def _compute_display_rounding_warning(self):
        for record in self:
            record.display_rounding_warning = (
                record._origin.id and record._origin.rounding != record.rounding
            )

    def write(self, vals):
        if 'rounding' in vals:
            rounding_val = vals['rounding']
            for record in self:
                if (rounding_val > record.rounding or rounding_val == 0) and record._has_accounting_entries():
                    raise UserError(_("You cannot reduce the number of decimal places of a currency which has already been used to make accounting entries."))

        return super(ResCurrency, self).write(vals)

    def _has_accounting_entries(self):
        """ Returns True iff this currency has been used to generate (hence, round)
        some move lines (either as their foreign currency, or as the main currency).
        """
        self.ensure_one()
        return bool(self.env['account.move.line'].sudo().search_count(['|', ('currency_id', '=', self.id), ('company_currency_id', '=', self.id)]))

    def _get_simple_currency_table(self, companies) -> SQL:
        """ Helper creating the currency table and returning its definition for basic cases of Odoo reports needing to convert amounts using only the
        current rates, in a single period.
        """
        if self._check_currency_table_monocurrency(companies):
            return self._get_monocurrency_currency_table_sql(companies)

        self._create_currency_table(companies, [('period', None, fields.Date.today())])
        return SQL('account_currency_table')

    def _check_currency_table_monocurrency(self, companies):
        """ Returns whether displaying the data of the provided companies can be done with a monocurrency currency table.
        If it can, calling _get_monocurrency_currency_table_sql is enough to join the currency table (which actually consists of a bunch of VALUES
        directly injected in the join).
        Else, a full-flegdge temporary table will be needed, that will have to be generated by a call to _create_currency_table.
        """
        return len(companies.currency_id) == 1

    def _get_monocurrency_currency_table_sql(self, companies, use_cta_rates=False):
        """ Returns a simplified currency table, faster to generate, for cases were all the data to convert are expressed in the same currency,
        to be use in a JOIN. It actually just consists of a few VALUES ; no temporary table is created in this case.

        All the rates in this currency table are equal to 1 (since everything is in the same currency). This is useful so that the queries can
        be written exactly in the same way, joining the currency table returned by some function, for both mono and multi currency cases.
        """
        unit_rates = [
            SQL("(%(company_id)s, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), %(rate_type)s, 1)", company_id=company.id, rate_type=rate_type)
            for company in companies
            for rate_type in (('historical', 'current', 'average') if use_cta_rates else ('current',))
        ]
        return SQL('(VALUES %s) AS account_currency_table(company_id, period_key, date_from, date_next, rate_type, rate)', SQL(',').join(unit_rates))

    def _create_currency_table(self, companies, date_periods, use_cta_rates=False):
        """ Creates a temporary table containing the currency rates to be used in order to aggregate amounts belonging to companies
        with different main currencies in a reporting query.
        These rates are computed from the res.currrency.rate objects defined for self.env.company.

        The currency table consists of the following columns:
            - company_id: The id of the company whose amounts can be converted with this rate.
            - period_key: The key corresponding to the period this rate is valid for. (see params list)
            - date_from: Only set for rate_type 'historical'. The starting date for this rate.
            - date_next: Only set for rate_type 'historical'. The date of the next rate. So, the rate applies until one day before date_next.
            - rate_type: 'historical', 'current' or 'average'
                            - 'historical' means the rate is to be used to convert operations at the date they were made; they each
                               directly correspond to the res.currency.rate objects of the active company
                            - 'current' means this rate is the most recent rate within the period. This rate is unique per (company_id, period_key).
                            - 'average' means this rate is the average rate for the period. This rate is unique per (company_id, period_key).
            - rate: The rate to apply, as a decimal factor to apply directly to the value to convert, provided it is expressed in the
                    main currency of the company referred to by company_id.


        :param companies: The res.company objects to generate rates for.
        :param date_periods: List of tuples in the form (period_key, date_from, date_to), containing each of the periods to generate rates for, where:
                             - period_key is a unique string identifier used to differentiate the periods
                             - date_from is the date the period starts at ; it can be None if the period want to consider everything from the beginning
                             - date_to is the date the periods ends at
        :param use_cta_rates: Boolean parameter, enabling the computation of CTA rates. If True, 'current', 'average' and 'historical' rates will be
                        computed for all companies, for all periods. Else, only 'current' will be computed.
        """
        main_company = self.env.company
        domestic_currency_companies = companies.filtered(lambda x: x.currency_id == main_company.currency_id)
        other_companies = companies - domestic_currency_companies

        table_builders = []
        if domestic_currency_companies:
            table_builders += [self._get_table_builder_domestic_currency(domestic_currency_companies, use_cta_rates)]

        last_date_to = None
        for period_key, date_from, date_to in date_periods:
            main_company_unit_factor, _ = main_company.currency_id._get_rates(main_company, date_to)[main_company.currency_id.id]

            table_builders.append(self._get_table_builder_current(period_key, main_company, other_companies, date_to, main_company_unit_factor))

            if use_cta_rates:
                table_builders += [
                    self._get_table_builder_historical(main_company, other_companies, date_to, main_company_unit_factor, last_date_to),
                    self._get_table_builder_average(period_key, main_company, other_companies, date_from, date_to, main_company_unit_factor),
                ]

            last_date_to = date_to

        self.env.cr.execute(SQL(
            """
                -- Tests may call this function multiple times within the same transaction; we then need to delete an regenerate the currency table
                DROP TABLE IF EXISTS account_currency_table;

                -- Create a temporary table
                CREATE TEMPORARY TABLE
                account_currency_table (company_id, period_key, date_from, date_next, rate_type, rate)
                ON COMMIT DROP
                AS (%(currency_table_build_query)s);

                -- Create a supporting index to avoid seq.scans
                CREATE INDEX account_currency_table_index ON account_currency_table (company_id, rate_type, date_from, date_next);
                -- Update statistics for correct planning
                ANALYZE account_currency_table;
            """,
            currency_table_build_query=SQL(" UNION ALL ").join(SQL('(%s)', builder) for builder in table_builders),
        ))

    def _get_table_builder_domestic_currency(self, companies, use_cta_rates) -> SQL:
        """ Returns a query building one rate of each appropriate type equal to 1 for each of the provided companies. Those companies should be
        the ones sharing the same currency as self.env.company.
        """
        rate_values = []
        for company in companies:
            rate_values.append(SQL("(%s, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), 'current', 1)", company.id))

            if use_cta_rates:
                rate_values += [
                    SQL("(%s, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), 'average', 1)", company.id),
                    SQL("(%s, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), 'historical', 1)", company.id),
                ]

        return SQL(
            """
                SELECT *
                FROM ( VALUES
                    %(rate_values)s
                ) values
            """,
            rate_values=SQL(", ").join(rate_values)
        )

    def _get_table_builder_current(self, period_key, main_company, other_companies, date_to, main_company_unit_factor) -> SQL:
        return SQL(
            """
                SELECT DISTINCT ON (other_company.id)
                    other_company.id,
                    %(period_key)s,
                    CAST(NULL AS DATE),
                    CAST(NULL AS DATE),
                    'current',
                    CASE WHEN rate.id IS NOT NULL THEN %(main_company_unit_factor)s / rate.rate ELSE 1 END
                FROM res_company other_company
                LEFT JOIN res_currency_rate rate
                    ON rate.currency_id = other_company.currency_id
                    AND rate.name <= %(date_to)s
                    AND (rate.company_id = %(root_company_id)s OR rate.company_id IS NULL)
                WHERE
                    other_company.id IN %(other_company_ids)s
                ORDER BY other_company.id, rate.name DESC
            """,
            period_key=period_key,
            root_company_id=main_company.root_id.id,
            other_company_ids=tuple(other_companies.ids),
            date_to=date_to,
            main_company_unit_factor=main_company_unit_factor,
        )

    def _get_table_builder_historical(self, main_company, other_companies, date_to, main_company_unit_factor, date_exclude) -> SQL:
        # main_company_unit_factor is kept for API stability but superseded: the domestic
        # rate is now looked up per-date via a lateral join so that fluctuations in the
        # domestic currency's own rate are correctly reflected in historical entries.
        return SQL(
            """
                SELECT
                    other_company.id,
                    CAST(NULL AS VARCHAR),
                    rate.name,
                    LAG(rate.name, 1) OVER (PARTITION BY other_company.id, rate.currency_id ORDER BY rate.name DESC),
                    'historical',
                    COALESCE(domestic_rate.rate, 1) / rate.rate
                FROM res_company other_company
                JOIN res_currency_rate rate
                    ON rate.currency_id = other_company.currency_id
                LEFT JOIN LATERAL (
                    SELECT dr.rate
                    FROM res_currency_rate dr
                    WHERE dr.currency_id = %(domestic_currency_id)s
                      AND dr.name <= rate.name
                      AND (dr.company_id = %(root_company_id)s OR dr.company_id IS NULL)
                    ORDER BY dr.name DESC
                    LIMIT 1
                ) domestic_rate ON true
                WHERE
                    other_company.id IN %(other_company_ids)s
                    AND (rate.company_id = %(root_company_id)s OR rate.company_id IS NULL)
                    AND rate.name <= %(date_to)s
                    %(exclusion_condition)s
            """,
            domestic_currency_id=main_company.currency_id.id,
            root_company_id=main_company.root_id.id,
            other_company_ids=tuple(other_companies.ids),
            date_to=date_to,
            exclusion_condition=SQL("AND rate.name > %(date_exclude)s", date_exclude=date_exclude) if date_exclude else SQL(),
        )

    def _get_table_builder_average(self, period_key, main_company, other_companies, date_from, date_to, main_company_unit_factor) -> SQL:
        if not date_from:
            # When there is no start date, we want to compute the average rate on the current year only
            date_from = date_utils.start_of(fields.Date.from_string(date_to), 'year')

        # main_company_unit_factor is kept for API stability but superseded: the domestic
        # rate is now looked up per-segment via lateral joins so that fluctuations in the
        # domestic currency's own rate are correctly weighted in the average.
        #
        # The period is split into segments on every rate change of either the foreign
        # currency or the domestic currency. Within each segment both rates are constant,
        # so the conversion factor is (domestic_rate / foreign_rate) for that segment.
        return SQL(
            """
                SELECT
                    rate_with_days.other_company_id,
                    %(period_key)s,
                    CAST(NULL AS DATE),
                    CAST(NULL AS DATE),
                    'average',
                    SUM(rate_with_days.domestic_rate / rate_with_days.foreign_rate * rate_with_days.number_of_days)
                        / SUM(rate_with_days.number_of_days)
                FROM (
                    SELECT
                        seg.other_company_id,
                        EXTRACT(
                            'Day' FROM COALESCE(seg.next_date, %(date_to)s::TIMESTAMP + INTERVAL '1' DAY)
                                     - seg.seg_date::TIMESTAMP
                        ) AS number_of_days,
                        COALESCE(foreign_rate.rate, 1.0) AS foreign_rate,
                        COALESCE(domestic_rate.rate, 1.0) AS domestic_rate
                    FROM (
                        -- One row per (company, breakpoint). Breakpoints are the start of the
                        -- period plus every rate change — for the foreign OR domestic currency —
                        -- that falls strictly inside the period.
                        SELECT
                            other_company.id AS other_company_id,
                            other_company.currency_id AS foreign_currency_id,
                            breakpoint.date AS seg_date,
                            LEAD(breakpoint.date) OVER (PARTITION BY other_company.id ORDER BY breakpoint.date) AS next_date
                        FROM res_company other_company
                        JOIN LATERAL (
                            SELECT %(date_from)s AS date
                            UNION
                            SELECT rate.name
                            FROM res_currency_rate rate
                            WHERE rate.currency_id = other_company.currency_id
                              AND rate.name > %(date_from)s
                              AND rate.name <= %(date_to)s
                              AND (rate.company_id = %(root_company_id)s OR rate.company_id IS NULL)
                            UNION
                            SELECT dr.name
                            FROM res_currency_rate dr
                            WHERE dr.currency_id = %(domestic_currency_id)s
                              AND dr.name > %(date_from)s
                              AND dr.name <= %(date_to)s
                              AND (dr.company_id = %(root_company_id)s OR dr.company_id IS NULL)
                        ) breakpoint ON true
                        WHERE other_company.id IN %(other_company_ids)s
                    ) seg
                    -- Foreign rate in effect at the start of this segment
                    LEFT JOIN LATERAL (
                        SELECT cr.rate
                        FROM res_currency_rate cr
                        WHERE cr.currency_id = seg.foreign_currency_id
                          AND cr.name <= seg.seg_date
                          AND (cr.company_id = %(root_company_id)s OR cr.company_id IS NULL)
                        ORDER BY cr.name DESC
                        LIMIT 1
                    ) foreign_rate ON true
                    -- Domestic rate in effect at the start of this segment
                    LEFT JOIN LATERAL (
                        SELECT cr.rate
                        FROM res_currency_rate cr
                        WHERE cr.currency_id = %(domestic_currency_id)s
                          AND cr.name <= seg.seg_date
                          AND (cr.company_id = %(root_company_id)s OR cr.company_id IS NULL)
                        ORDER BY cr.name DESC
                        LIMIT 1
                    ) domestic_rate ON true
                ) rate_with_days
                WHERE rate_with_days.number_of_days > 0
                GROUP BY rate_with_days.other_company_id
            """,
            period_key=period_key,
            root_company_id=main_company.root_id.id,
            other_company_ids=tuple(other_companies.ids),
            date_from=date_from,
            date_to=date_to,
            domestic_currency_id=main_company.currency_id.id,
        )
