# Copyright 2018 ACSONE SA/NV.
# Copyright 2020 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from dateutil.relativedelta import relativedelta

from odoo import api, fields, models


class ContractRecurrencyBasicMixin(models.AbstractModel):
    _name = "contract.recurrency.basic.mixin"
    _description = "Basic recurrency mixin for abstract contract models"

    recurring_rule_type = fields.Selection(
        [
            ("daily", "Day(s)"),
            ("weekly", "Week(s)"),
            ("monthly", "Month(s)"),
            ("monthlylastday", "Month(s) last day"),
            ("quarterly", "Quarter(s)"),
            ("semesterly", "Semester(s)"),
            ("yearly", "Year(s)"),
        ],
        default="monthly",
        string="Recurrence",
        help="Specify Interval for automatic invoice generation.",
    )
    recurring_invoicing_type = fields.Selection(
        [("pre-paid", "Pre-paid"), ("post-paid", "Post-paid")],
        default="pre-paid",
        string="Invoicing type",
        help=(
            "Specify if the invoice must be generated at the beginning "
            "(pre-paid) or end (post-paid) of the period."
        ),
    )
    recurring_invoicing_offset = fields.Integer(
        compute="_compute_recurring_invoicing_offset",
        string="Invoicing offset",
        help=(
            "Number of days to offset the invoice from the period end "
            "date (in post-paid mode) or start date (in pre-paid mode)."
        ),
    )
    recurring_interval = fields.Integer(
        default=1,
        string="Invoice Every",
        help="Invoice every (Days/Week/Month/Year)",
    )
    date_start = fields.Date()
    recurring_next_date = fields.Date(string="Date of Next Invoice")

    @api.depends("recurring_invoicing_type", "recurring_rule_type")
    def _compute_recurring_invoicing_offset(self):
        for rec in self:
            method = self._get_default_recurring_invoicing_offset
            rec.recurring_invoicing_offset = method(
                rec.recurring_invoicing_type, rec.recurring_rule_type
            )

    @api.model
    def _get_default_recurring_invoicing_offset(
        self, recurring_invoicing_type, recurring_rule_type
    ):
        if (
            recurring_invoicing_type == "pre-paid"
            or recurring_rule_type == "monthlylastday"
        ):
            return 0
        else:
            return 1


class ContractRecurrencyMixin(models.AbstractModel):
    _inherit = "contract.recurrency.basic.mixin"
    _name = "contract.recurrency.mixin"
    _description = "Recurrency mixin for contract models"

    date_start = fields.Date(default=lambda self: fields.Date.context_today(self))
    recurring_next_date = fields.Date(
        compute="_compute_recurring_next_date", store=True, readonly=False, copy=True
    )
    date_end = fields.Date(index=True)
    next_period_date_start = fields.Date(
        string="Next Period Start",
        compute="_compute_next_period_date_start",
    )
    next_period_date_end = fields.Date(
        string="Next Period End",
        compute="_compute_next_period_date_end",
    )
    last_date_invoiced = fields.Date(readonly=True, copy=False)

    @api.depends("next_period_date_start")
    def _compute_recurring_next_date(self):
        for rec in self:
            rec.recurring_next_date = self.get_next_invoice_date(
                rec.next_period_date_start,
                rec.recurring_invoicing_type,
                rec.recurring_invoicing_offset,
                rec.recurring_rule_type,
                rec.recurring_interval,
                max_date_end=rec.date_end,
            )

    @api.depends("last_date_invoiced", "date_start", "date_end")
    def _compute_next_period_date_start(self):
        for rec in self:
            if rec.last_date_invoiced:
                next_period_date_start = rec.last_date_invoiced + relativedelta(days=1)
            else:
                next_period_date_start = rec.date_start
            if (
                rec.date_end
                and next_period_date_start
                and next_period_date_start > rec.date_end
            ):
                next_period_date_start = False
            rec.next_period_date_start = next_period_date_start

    @api.depends(
        "next_period_date_start",
        "recurring_invoicing_type",
        "recurring_invoicing_offset",
        "recurring_rule_type",
        "recurring_interval",
        "date_end",
        "recurring_next_date",
    )
    def _compute_next_period_date_end(self):
        for rec in self:
            rec.next_period_date_end = self.get_next_period_date_end(
                rec.next_period_date_start,
                rec.recurring_rule_type,
                rec.recurring_interval,
                max_date_end=rec.date_end,
                next_invoice_date=rec.recurring_next_date,
                recurring_invoicing_type=rec.recurring_invoicing_type,
                recurring_invoicing_offset=rec.recurring_invoicing_offset,
            )

    @api.model
    def get_relative_delta(self, recurring_rule_type, interval):
        """Return a relativedelta for one period.

        When added to the first day of the period,
        it gives the first day of the next period.
        """
        if recurring_rule_type == "daily":
            return relativedelta(days=interval)
        elif recurring_rule_type == "weekly":
            return relativedelta(weeks=interval)
        elif recurring_rule_type == "monthly":
            return relativedelta(months=interval)
        elif recurring_rule_type == "monthlylastday":
            return relativedelta(months=interval, day=1)
        elif recurring_rule_type == "quarterly":
            return relativedelta(months=3 * interval)
        elif recurring_rule_type == "semesterly":
            return relativedelta(months=6 * interval)
        else:
            return relativedelta(years=interval)

    @api.model
    def get_next_period_date_end(
        self,
        next_period_date_start,
        recurring_rule_type,
        recurring_interval,
        max_date_end,
        next_invoice_date=False,
        recurring_invoicing_type=False,
        recurring_invoicing_offset=False,
    ):
        """Compute the end date for the next period.

        The next period normally depends on recurrence options only.
        It is however possible to provide it a next invoice date, in
        which case this method can adjust the next period based on that
        too. In that scenario it required the invoicing type and offset
        arguments.
        """
        if not next_period_date_start:
            return False
        if max_date_end and next_period_date_start > max_date_end:
            # start is past max date end: there is no next period
            return False
        if not next_invoice_date:
            # regular algorithm
            next_period_date_end = (
                next_period_date_start
                + self.get_relative_delta(recurring_rule_type, recurring_interval)
                - relativedelta(days=1)
            )
        else:
            # special algorithm when the next invoice date is forced
            if recurring_invoicing_type == "pre-paid":
                next_period_date_end = (
                    next_invoice_date
                    - relativedelta(days=recurring_invoicing_offset)
                    + self.get_relative_delta(recurring_rule_type, recurring_interval)
                    - relativedelta(days=1)
                )
            else:  # post-paid
                next_period_date_end = next_invoice_date - relativedelta(
                    days=recurring_invoicing_offset
                )
        if max_date_end and next_period_date_end > max_date_end:
            # end date is past max_date_end: trim it
            next_period_date_end = max_date_end
        return next_period_date_end

    @api.model
    def get_next_invoice_date(
        self,
        next_period_date_start,
        recurring_invoicing_type,
        recurring_invoicing_offset,
        recurring_rule_type,
        recurring_interval,
        max_date_end,
    ):
        next_period_date_end = self.get_next_period_date_end(
            next_period_date_start,
            recurring_rule_type,
            recurring_interval,
            max_date_end=max_date_end,
        )
        if not next_period_date_end:
            return False
        if recurring_invoicing_type == "pre-paid":
            recurring_next_date = next_period_date_start + relativedelta(
                days=recurring_invoicing_offset
            )
        else:  # post-paid
            recurring_next_date = next_period_date_end + relativedelta(
                days=recurring_invoicing_offset
            )
        return recurring_next_date
