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

from dateutil.relativedelta import relativedelta

from odoo import api, fields, models, _
from odoo.exceptions import UserError


class FusionFollowupLevel(models.Model):
    """Defines escalation levels for payment follow-up reminders.

    Each level represents a stage in the collection process, configured
    with a delay (days past the invoice due date) and communication
    channels (email, SMS, letter). Levels are ordered by sequence so
    the system can automatically escalate from gentle reminders to
    more urgent notices.
    """

    _name = 'fusion.followup.level'
    _description = "Fusion Payment Follow-up Level"
    _order = 'sequence, id'

    # ---- Core Fields ----
    name = fields.Char(
        string="Follow-up Action",
        required=True,
        translate=True,
        help="Short label for this follow-up step (e.g. 'First Reminder').",
    )
    description = fields.Html(
        string="Message Body",
        translate=True,
        help="Default message included in the follow-up communication.",
    )
    sequence = fields.Integer(
        string="Sequence",
        default=10,
        help="Determines the escalation order. Lower values run first.",
    )
    delay = fields.Integer(
        string="Due Days",
        required=True,
        default=15,
        help="Number of days after the invoice due date before this level triggers.",
    )
    company_id = fields.Many2one(
        comodel_name='res.company',
        string="Company",
        required=True,
        default=lambda self: self.env.company,
    )
    active = fields.Boolean(
        string="Active",
        default=True,
    )

    # ---- Communication Channels ----
    send_email = fields.Boolean(
        string="Send Email",
        default=True,
        help="Automatically send an email when this follow-up level is executed.",
    )
    send_sms = fields.Boolean(
        string="Send SMS",
        default=False,
        help="Send an SMS notification when this follow-up level is executed.",
    )
    send_letter = fields.Boolean(
        string="Print Letter",
        default=False,
        help="Generate a printable letter when this follow-up level is executed.",
    )

    # ---- Templates ----
    email_template_id = fields.Many2one(
        comodel_name='mail.template',
        string="Email Template",
        domain="[('model', '=', 'res.partner')]",
        help="Email template to use. Leave empty to use the default follow-up template.",
    )
    sms_template_id = fields.Many2one(
        comodel_name='sms.template',
        string="SMS Template",
        domain="[('model', '=', 'res.partner')]",
        help="SMS template to use when the SMS channel is enabled.",
    )

    # ---- Options ----
    join_invoices = fields.Boolean(
        string="Attach Open Invoices",
        default=False,
        help="When enabled, PDF copies of open invoices are attached to the email.",
    )

    # --------------------------------------------------
    # Helpers
    # --------------------------------------------------

    def _get_next_level(self):
        """Return the follow-up level that comes after this one.

        :returns: A ``fusion.followup.level`` recordset (single or empty).
        """
        self.ensure_one()
        return self.search([
            ('company_id', '=', self.company_id.id),
            ('sequence', '>', self.sequence),
        ], order='sequence, id', limit=1)


class FusionFollowupLine(models.Model):
    """Tracks the follow-up state for a specific partner.

    Each record links a partner to their current follow-up level and
    stores the date of the last action. Computed fields determine
    the next action date, overdue amounts, and whether action is needed.
    """

    _name = 'fusion.followup.line'
    _description = "Fusion Partner Follow-up Tracking"
    _order = 'next_followup_date asc, id'
    _rec_name = 'partner_id'

    # ---- Relational Fields ----
    partner_id = fields.Many2one(
        comodel_name='res.partner',
        string="Partner",
        required=True,
        ondelete='cascade',
        index=True,
    )
    company_id = fields.Many2one(
        comodel_name='res.company',
        string="Company",
        required=True,
        default=lambda self: self.env.company,
    )
    followup_level_id = fields.Many2one(
        comodel_name='fusion.followup.level',
        string="Current Level",
        domain="[('company_id', '=', company_id)]",
        help="The most recent follow-up level applied to this partner.",
    )

    # ---- Date Fields ----
    date = fields.Date(
        string="Last Follow-up Date",
        help="Date of the most recent follow-up action.",
    )
    next_followup_date = fields.Date(
        string="Next Action Date",
        compute='_compute_next_followup_date',
        store=True,
        help="Calculated date for the next follow-up step.",
    )

    # ---- Computed Amounts ----
    overdue_amount = fields.Monetary(
        string="Total Overdue",
        compute='_compute_overdue_values',
        currency_field='currency_id',
        store=True,
        help="Sum of all overdue receivable amounts for this partner.",
    )
    overdue_count = fields.Integer(
        string="Overdue Invoices",
        compute='_compute_overdue_values',
        store=True,
        help="Number of overdue invoices for this partner.",
    )
    currency_id = fields.Many2one(
        comodel_name='res.currency',
        string="Currency",
        related='company_id.currency_id',
        store=True,
        readonly=True,
    )

    # ---- Status ----
    followup_status = fields.Selection(
        selection=[
            ('in_need', 'In Need of Action'),
            ('with_overdue', 'With Overdue Invoices'),
            ('no_action_needed', 'No Action Needed'),
        ],
        string="Follow-up Status",
        compute='_compute_followup_status',
        store=True,
        help="Indicates whether this partner requires a follow-up action.",
    )

    # ---- SQL Constraint ----
    _sql_constraints = [
        (
            'partner_company_unique',
            'UNIQUE(partner_id, company_id)',
            'A partner can only have one follow-up tracking record per company.',
        ),
    ]

    # --------------------------------------------------
    # Computed Fields
    # --------------------------------------------------

    @api.depends('date', 'followup_level_id', 'followup_level_id.delay')
    def _compute_next_followup_date(self):
        """Calculate the next follow-up date based on the current level delay.

        If no level is assigned the next date equals the last follow-up
        date.  When no date exists at all the field stays empty.
        """
        for line in self:
            if line.date and line.followup_level_id:
                next_level = line.followup_level_id._get_next_level()
                if next_level:
                    line.next_followup_date = line.date + relativedelta(
                        days=next_level.delay - line.followup_level_id.delay,
                    )
                else:
                    # Already at the highest level; re-trigger after same delay
                    line.next_followup_date = line.date + relativedelta(
                        days=line.followup_level_id.delay,
                    )
            elif line.date:
                line.next_followup_date = line.date
            else:
                line.next_followup_date = False

    @api.depends('partner_id', 'company_id')
    def _compute_overdue_values(self):
        """Compute overdue totals from the partner's unpaid receivable move lines."""
        today = fields.Date.context_today(self)
        for line in self:
            overdue_lines = self.env['account.move.line'].search([
                ('partner_id', '=', line.partner_id.commercial_partner_id.id),
                ('company_id', '=', line.company_id.id),
                ('account_id.account_type', '=', 'asset_receivable'),
                ('parent_state', '=', 'posted'),
                ('reconciled', '=', False),
                ('amount_residual', '>', 0),
                ('date_maturity', '<', today),
            ])
            line.overdue_amount = sum(overdue_lines.mapped('amount_residual'))
            line.overdue_count = len(overdue_lines.mapped('move_id'))

    @api.depends('overdue_amount', 'next_followup_date')
    def _compute_followup_status(self):
        """Determine the follow-up status for each tracking record.

        * **in_need** – there are overdue invoices and the next
          follow-up date has been reached.
        * **with_overdue** – there are overdue invoices but the next
          action date is still in the future.
        * **no_action_needed** – nothing is overdue.
        """
        today = fields.Date.context_today(self)
        for line in self:
            if line.overdue_amount <= 0:
                line.followup_status = 'no_action_needed'
            elif line.next_followup_date and line.next_followup_date > today:
                line.followup_status = 'with_overdue'
            else:
                line.followup_status = 'in_need'

    # --------------------------------------------------
    # Business Logic
    # --------------------------------------------------

    def compute_followup_status(self):
        """Manually recompute overdue values and status.

        Useful for the UI refresh button and scheduled actions.
        """
        self._compute_overdue_values()
        self._compute_followup_status()
        return True

    def execute_followup(self):
        """Execute the follow-up action for the current level.

        Sends emails and/or SMS messages based on the channel settings
        of the current follow-up level, then advances the partner to
        the next level.

        Skips sending if the partner no longer has any overdue balance.

        :raises UserError: If no follow-up level is set.
        """
        self.ensure_one()
        if not self.followup_level_id:
            raise UserError(_(
                "No follow-up level is set for partner '%s'. "
                "Please configure follow-up levels first.",
                self.partner_id.display_name,
            ))

        self._compute_overdue_values()
        if self.overdue_amount <= 0:
            self.followup_status = 'no_action_needed'
            return True

        level = self.followup_level_id
        partner = self.partner_id

        # ---- Send Email ----
        if level.send_email:
            template = level.email_template_id or self.env.ref(
                'fusion_accounting.email_template_fusion_followup_default',
                raise_if_not_found=False,
            )
            if template:
                attachment_ids = []
                if level.join_invoices:
                    attachment_ids = self._get_invoice_attachments(partner)
                template.send_mail(
                    partner.id,
                    force_send=True,
                    email_values={'attachment_ids': attachment_ids},
                )

        # ---- Send SMS ----
        if level.send_sms and level.sms_template_id:
            try:
                level.sms_template_id._send_sms(partner.id)
            except Exception:
                # SMS delivery failures should not block the follow-up process
                pass

        # ---- Advance to next level ----
        next_level = level._get_next_level()
        self.write({
            'date': fields.Date.context_today(self),
            'followup_level_id': next_level.id if next_level else level.id,
        })

        return True

    def _get_invoice_attachments(self, partner):
        """Generate PDF attachments for the partner's open invoices.

        :param partner: A ``res.partner`` recordset.
        :returns: List of ``ir.attachment`` IDs.
        """
        overdue_invoices = self.env['account.move'].search([
            ('partner_id', '=', partner.commercial_partner_id.id),
            ('company_id', '=', self.company_id.id),
            ('move_type', 'in', ('out_invoice', 'out_debit')),
            ('payment_state', 'in', ('not_paid', 'partial')),
            ('state', '=', 'posted'),
        ])
        if not overdue_invoices:
            return []

        pdf_report = self.env.ref('account.account_invoices', raise_if_not_found=False)
        if not pdf_report:
            return []

        attachment_ids = []
        for invoice in overdue_invoices:
            content, _content_type = self.env['ir.actions.report']._render(
                pdf_report.report_name, invoice.ids,
            )
            attachment = self.env['ir.attachment'].create({
                'name': f"{invoice.name}.pdf",
                'type': 'binary',
                'raw': content,
                'mimetype': 'application/pdf',
                'res_model': 'account.move',
                'res_id': invoice.id,
            })
            attachment_ids.append(attachment.id)

        return attachment_ids
