# Copyright 2018 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import logging
import math
from datetime import datetime

from dateutil.relativedelta import relativedelta

from odoo import api, fields, models

_logger = logging.getLogger(__name__)
try:
    import numpy_financial
except (ImportError, IOError) as err:
    _logger.debug(err)


class AccountLoan(models.Model):
    _name = "account.loan"
    _description = "Loan"
    _inherit = ["mail.thread", "mail.activity.mixin"]

    def _default_company(self):
        return self.env.company

    name = fields.Char(
        copy=False,
        required=True,
        readonly=True,
        default="/",
        states={"draft": [("readonly", False)]},
    )
    partner_id = fields.Many2one(
        "res.partner",
        required=True,
        string="Lender",
        help="Company or individual that lends the money at an interest rate.",
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    company_id = fields.Many2one(
        "res.company",
        required=True,
        default=_default_company,
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    state = fields.Selection(
        [
            ("draft", "Draft"),
            ("posted", "Posted"),
            ("cancelled", "Cancelled"),
            ("closed", "Closed"),
        ],
        required=True,
        copy=False,
        default="draft",
    )
    line_ids = fields.One2many(
        "account.loan.line",
        readonly=True,
        inverse_name="loan_id",
        copy=False,
    )
    periods = fields.Integer(
        required=True,
        readonly=True,
        states={"draft": [("readonly", False)]},
        help="Number of periods that the loan will last",
    )
    method_period = fields.Integer(
        string="Period Length",
        default=1,
        help="State here the time between 2 depreciations, in months",
        required=True,
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    start_date = fields.Date(
        help="Start of the moves",
        readonly=True,
        states={"draft": [("readonly", False)]},
        copy=False,
    )
    rate = fields.Float(
        required=True,
        default=0.0,
        digits=(8, 6),
        help="Currently applied rate",
        tracking=True,
    )
    rate_period = fields.Float(
        compute="_compute_rate_period",
        digits=(8, 6),
        help="Real rate that will be applied on each period",
    )
    rate_type = fields.Selection(
        [("napr", "Nominal APR"), ("ear", "EAR"), ("real", "Real rate")],
        required=True,
        help="Method of computation of the applied rate",
        default="napr",
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    loan_type = fields.Selection(
        [
            ("fixed-annuity", "Fixed Annuity"),
            ("fixed-annuity-begin", "Fixed Annuity Begin"),
            ("fixed-principal", "Fixed Principal"),
            ("interest", "Only interest"),
        ],
        required=True,
        help="Method of computation of the period annuity",
        readonly=True,
        states={"draft": [("readonly", False)]},
        default="fixed-annuity",
    )
    fixed_amount = fields.Monetary(
        currency_field="currency_id",
        compute="_compute_fixed_amount",
    )
    fixed_loan_amount = fields.Monetary(
        currency_field="currency_id",
        readonly=True,
        copy=False,
        default=0,
    )
    fixed_periods = fields.Integer(
        readonly=True,
        copy=False,
        default=0,
    )
    loan_amount = fields.Monetary(
        currency_field="currency_id",
        required=True,
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    residual_amount = fields.Monetary(
        currency_field="currency_id",
        default=0.0,
        required=True,
        readonly=True,
        states={"draft": [("readonly", False)]},
        help="Residual amount of the lease that must be payed on the end in "
        "order to acquire the asset",
    )
    round_on_end = fields.Boolean(
        default=False,
        help="When checked, the differences will be applied on the last period"
        ", if it is unchecked, the annuity will be recalculated on each "
        "period.",
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    payment_on_first_period = fields.Boolean(
        default=False,
        readonly=True,
        states={"draft": [("readonly", False)]},
        help="When checked, the first payment will be on start date",
    )
    currency_id = fields.Many2one(
        "res.currency",
        compute="_compute_currency",
        readonly=True,
    )
    journal_type = fields.Char(compute="_compute_journal_type")
    journal_id = fields.Many2one(
        "account.journal",
        domain="[('company_id', '=', company_id),('type', '=', journal_type)]",
        required=True,
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    short_term_loan_account_id = fields.Many2one(
        "account.account",
        domain="[('company_id', '=', company_id)]",
        string="Short term account",
        help="Account that will contain the pending amount on short term",
        required=True,
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    long_term_loan_account_id = fields.Many2one(
        "account.account",
        string="Long term account",
        help="Account that will contain the pending amount on Long term",
        domain="[('company_id', '=', company_id)]",
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    interest_expenses_account_id = fields.Many2one(
        "account.account",
        domain="[('company_id', '=', company_id)]",
        string="Interests account",
        help="Account where the interests will be assigned to",
        required=True,
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    is_leasing = fields.Boolean(
        default=False,
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    leased_asset_account_id = fields.Many2one(
        "account.account",
        domain="[('company_id', '=', company_id)]",
        readonly=True,
        states={"draft": [("readonly", False)]},
    )
    product_id = fields.Many2one(
        "product.product",
        string="Loan product",
        help="Product where the amount of the loan will be assigned when the "
        "invoice is created",
    )
    interests_product_id = fields.Many2one(
        "product.product",
        string="Interest product",
        help="Product where the amount of interests will be assigned when the "
        "invoice is created",
    )
    move_ids = fields.One2many("account.move", copy=False, inverse_name="loan_id")
    pending_principal_amount = fields.Monetary(
        currency_field="currency_id",
        compute="_compute_total_amounts",
    )
    payment_amount = fields.Monetary(
        currency_field="currency_id",
        string="Total payed amount",
        compute="_compute_total_amounts",
    )
    interests_amount = fields.Monetary(
        currency_field="currency_id",
        string="Total interests payed",
        compute="_compute_total_amounts",
    )
    post_invoice = fields.Boolean(
        default=True, help="Invoices will be posted automatically"
    )

    _sql_constraints = [
        ("name_uniq", "unique(name, company_id)", "Loan name must be unique"),
    ]

    @api.depends("line_ids", "currency_id", "loan_amount")
    def _compute_total_amounts(self):
        for record in self:
            lines = record.line_ids.filtered(lambda r: r.move_ids)
            record.payment_amount = sum(lines.mapped("payment_amount")) or 0.0
            record.interests_amount = sum(lines.mapped("interests_amount")) or 0.0
            record.pending_principal_amount = (
                record.loan_amount - record.payment_amount + record.interests_amount
            )

    @api.depends("rate_period", "fixed_loan_amount", "fixed_periods", "currency_id")
    def _compute_fixed_amount(self):
        """
        Computes the fixed amount in order to be used if round_on_end is
        checked. On fix-annuity interests are included and on fixed-principal
        and interests it isn't.
        :return:
        """
        for record in self:
            if record.loan_type == "fixed-annuity":
                record.fixed_amount = -record.currency_id.round(
                    numpy_financial.pmt(
                        record._loan_rate() / 100,
                        record.fixed_periods,
                        record.fixed_loan_amount,
                        -record.residual_amount,
                    )
                )
            elif record.loan_type == "fixed-annuity-begin":
                record.fixed_amount = -record.currency_id.round(
                    numpy_financial.pmt(
                        record._loan_rate() / 100,
                        record.fixed_periods,
                        record.fixed_loan_amount,
                        -record.residual_amount,
                        when="begin",
                    )
                )
            elif record.loan_type == "fixed-principal":
                record.fixed_amount = record.currency_id.round(
                    (record.fixed_loan_amount - record.residual_amount)
                    / record.fixed_periods
                )
            else:
                record.fixed_amount = 0.0

    @api.model
    def _compute_rate(self, rate, rate_type, method_period):
        """
        Returns the real rate
        :param rate: Rate
        :param rate_type: Computation rate
        :param method_period: Number of months between payments
        :return:
        """
        if rate_type == "napr":
            return rate / 12 * method_period
        if rate_type == "ear":
            return math.pow(1 + rate, method_period / 12) - 1
        return rate

    @api.depends("rate", "method_period", "rate_type")
    def _compute_rate_period(self):
        for record in self:
            record.rate_period = record._loan_rate()

    def _loan_rate(self):
        return self._compute_rate(self.rate, self.rate_type, self.method_period)

    @api.depends("journal_id", "company_id")
    def _compute_currency(self):
        for rec in self:
            rec.currency_id = rec.journal_id.currency_id or rec.company_id.currency_id

    @api.depends("is_leasing")
    def _compute_journal_type(self):
        for record in self:
            if record.is_leasing:
                record.journal_type = "purchase"
            else:
                record.journal_type = "general"

    @api.onchange("is_leasing")
    def _onchange_is_leasing(self):
        self.journal_id = self.env["account.journal"].search(
            [
                ("company_id", "=", self.company_id.id),
                ("type", "=", "purchase" if self.is_leasing else "general"),
            ],
            limit=1,
        )
        self.residual_amount = 0.0

    @api.onchange("company_id")
    def _onchange_company(self):
        self._onchange_is_leasing()
        self.interest_expenses_account_id = (
            self.short_term_loan_account_id
        ) = self.long_term_loan_account_id = False

    def _get_default_name(self, vals):
        return self.env["ir.sequence"].next_by_code("account.loan") or "/"

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get("name", "/") == "/":
                vals["name"] = self._get_default_name(vals)
        return super().create(vals_list)

    def post(self):
        self.ensure_one()
        if not self.start_date:
            self.start_date = fields.Date.today()
        self._compute_draft_lines()
        self.write({"state": "posted"})

    def close(self):
        self.write({"state": "closed"})

    def compute_lines(self):
        self.ensure_one()
        if self.state == "draft":
            return self._compute_draft_lines()
        return self._compute_posted_lines()

    def _compute_posted_lines(self):
        """
        Recompute the amounts of not finished lines. Useful if rate is changed
        """
        amount = self.loan_amount
        for line in self.line_ids.sorted("sequence"):
            if line.move_ids:
                amount = line.final_pending_principal_amount
            else:
                line.rate = self.rate_period
                line.pending_principal_amount = amount
                line._check_amount()
                amount -= line.payment_amount - line.interests_amount
        if self.long_term_loan_account_id:
            self._check_long_term_principal_amount()

    def _check_long_term_principal_amount(self):
        """
        Recomputes the long term pending principal of unfinished lines.
        """
        lines = self.line_ids.filtered(lambda r: not r.move_ids)
        amount = 0
        if not lines:
            return
        final_sequence = min(lines.mapped("sequence"))
        for line in lines.sorted("sequence", reverse=True):
            date = line.date + relativedelta(months=12)
            if self.state == "draft" or line.sequence != final_sequence:
                line.long_term_pending_principal_amount = sum(
                    self.line_ids.filtered(lambda r: r.date >= date).mapped(
                        "principal_amount"
                    )
                )
            line.long_term_principal_amount = (
                line.long_term_pending_principal_amount - amount
            )
            amount = line.long_term_pending_principal_amount

    def _new_line_vals(self, sequence, date, amount):
        return {
            "loan_id": self.id,
            "sequence": sequence,
            "date": date,
            "pending_principal_amount": amount,
            "rate": self.rate_period,
        }

    def _compute_draft_lines(self):
        self.ensure_one()
        self.fixed_periods = self.periods
        self.fixed_loan_amount = self.loan_amount
        self.line_ids.unlink()
        amount = self.loan_amount
        if self.start_date:
            date = self.start_date
        else:
            date = datetime.today().date()
        delta = relativedelta(months=self.method_period)
        if not self.payment_on_first_period:
            date += delta
        for i in range(1, self.periods + 1):
            line = self.env["account.loan.line"].create(
                self._new_line_vals(i, date, amount)
            )
            line._check_amount()
            date += delta
            amount -= line.payment_amount - line.interests_amount
        if self.long_term_loan_account_id:
            self._check_long_term_principal_amount()

    def view_account_moves(self):
        self.ensure_one()
        result = self.env["ir.actions.act_window"]._for_xml_id(
            "account.action_move_line_form"
        )
        result["domain"] = [("loan_id", "=", self.id)]
        return result

    def view_account_invoices(self):
        self.ensure_one()
        result = self.env["ir.actions.act_window"]._for_xml_id(
            "account.action_move_out_invoice_type"
        )
        result["domain"] = [("loan_id", "=", self.id), ("move_type", "=", "in_invoice")]
        return result

    @api.model
    def _generate_loan_entries(self, date):
        """
        Generate the moves of unfinished loans before date
        :param date:
        :return:
        """
        res = []
        for record in self.search(
            [("state", "=", "posted"), ("is_leasing", "=", False)]
        ):
            lines = record.line_ids.filtered(
                lambda r: r.date <= date and not r.move_ids
            )
            res += lines._generate_move()
        return res

    @api.model
    def _generate_leasing_entries(self, date):
        res = []
        for record in self.search(
            [("state", "=", "posted"), ("is_leasing", "=", True)]
        ):
            res += record.line_ids.filtered(
                lambda r: r.date <= date and not r.move_ids
            )._generate_invoice()
        return res
