# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import calendar
import logging
from datetime import date

from dateutil.relativedelta import relativedelta

from odoo import fields
from odoo import models

_logger = logging.getLogger(__name__)


def _month_bounds(day):
    """Return ``(first_day, last_day)`` of the calendar month containing ``day``."""
    first_day = day.replace(day=1)
    last_day = day.replace(day=calendar.monthrange(day.year, day.month)[1])
    return first_day, last_day


class ResPartner(models.Model):
    _inherit = "res.partner"

    def _cron_create_monthly_membership_invoices(self):
        """Monthly cron: ensure every member has a membership line for each month
        up to the current one, with no missing month in between.

        For each active member it backfills from the month following their last
        covered period up to the current calendar month, creating one draft
        membership invoice per missing month and copying the fee from their last
        membership line. Each line's period is forced to the calendar month it
        covers (day 1 to last day), independent of the previous subscription
        period or the product's fixed dates.

        The membership module is invoice-driven: creating the invoice (via
        ``create_membership_invoice``) is what auto-creates the
        ``membership.membership_line`` through the ``account.move.line`` override.
        """
        enabled = (
            self.env["ir.config_parameter"]
            .sudo()
            .get_param("membership_monthly_invoicing.enabled", "True")
        )
        if enabled in ("False", "0", "", None):
            _logger.info("membership_monthly_invoicing: disabled, skipping run.")
            return

        today = fields.Date.today()
        current_month_start = today.replace(day=1)

        membership_line = self.env["membership.membership_line"]

        # Candidates: members with history, not cancelled. free_member would raise
        # in create_membership_invoice, and associate_member inherit their host's
        # state, so both are excluded here.
        partners = self.search(
            [
                ("membership_state", "in", ("paid", "invoiced", "waiting", "old")),
                ("free_member", "=", False),
                ("associate_member", "=", False),
                ("member_lines", "!=", False),
            ]
        )

        created = 0
        skipped = 0
        for partner in partners:
            lines = partner.member_lines
            # Template line for product/fee: the most recent period.
            last_line = lines.sorted(
                key=lambda line: (line.date_from or date.min, line.id)
            )[-1]
            product = last_line.membership_id
            if not product:
                skipped += 1
                continue
            amount = last_line.member_price or product.list_price

            # Coverage end: latest period end across all existing lines. Backfill
            # starts the month after it, but never later than the current month.
            coverage_end = max(
                (
                    line.date_to or line.date_from
                    for line in lines
                    if (line.date_to or line.date_from)
                ),
                default=None,
            )
            if coverage_end:
                month_cursor = coverage_end.replace(day=1) + relativedelta(months=1)
            else:
                month_cursor = current_month_start
            if month_cursor > current_month_start:
                # Already covered up to (or beyond) the current month.
                skipped += 1
                continue

            while month_cursor <= current_month_start:
                first_day, last_day = _month_bounds(month_cursor)
                month_cursor += relativedelta(months=1)

                # Idempotency / gap-safety: skip months already covered by a line.
                if membership_line.search_count(
                    [
                        ("partner", "=", partner.id),
                        ("date_from", "<=", last_day),
                        ("date_to", ">=", first_day),
                    ]
                ):
                    continue

                try:
                    move = partner.create_membership_invoice(product, amount)
                except Exception:
                    _logger.exception(
                        "membership_monthly_invoicing: failed to invoice partner "
                        "%d (%s) for %s",
                        partner.id,
                        partner.name,
                        first_day,
                    )
                    continue

                # Date the draft invoice in the month it covers, and force the
                # membership line period to that calendar month (overriding the
                # product's fixed dates set by the membership override).
                move.invoice_date = first_day
                membership_line.search(
                    [("account_invoice_line", "in", move.invoice_line_ids.ids)]
                ).write({"date_from": first_day, "date_to": last_day})
                created += 1

        _logger.info(
            "membership_monthly_invoicing: %d draft invoice(s) created, "
            "%d member(s) already up to date (through %s).",
            created,
            skipped,
            current_month_start,
        )
