# -*- coding: utf-8 -*-
import datetime as dt
import calendar as cal
import collections
from decimal import Decimal
from dateutil.relativedelta import relativedelta

Payment=collections.namedtuple('Payment',['date','payment_amount','interest_amount','principal_amount','special_principal_amount','total_principal_amount','loan_balance_amount'])

Special_Payment=collections.namedtuple('Special_Payment',['payment_amount','first_payment_date','special_payment_term','annual_payments'])
Loan_Summary=collections.namedtuple('Loan_Summary',['loan_amount','total_payment_amount','total_principal_amount','total_interest_amount','residual_loan_balance','repayment_to_principal'])

class Loan(object):

    def __init__(self,loan_amount,interest_rate,loan_term,start_date,payment_amount=None,first_payment_date=None,payment_end_of_month=True,annual_payments=12,interest_only_period=0,compounding_method='30E/360',loan_type='annuity'):
        
        '''
        Input validtion for attribute loan_amount
        '''
        try:
            if type(loan_amount) == int or type(loan_amount) == float:
                if loan_amount < 0:
                    raise ValueError('Variable LOAN_AMMOUNT can only be non-negative.')
            else:
                raise TypeError('Variable LOAN_AMOUNT can only be of type integer or float, both non-negative.')

        # handle exceptions for loan_amount
        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)

        else:
            self.loan_amount=Decimal(str(loan_amount))

        '''
        Input validation for attribute interet_rate
        '''
        try:
            if type(interest_rate) == int or type(interest_rate) == float:
                if interest_rate < 0:
                    raise ValueError('Variable INTEREST_RATE can only be non-negative.')
            else:
                raise TypeError('Variable INTEREST_RATE can only be of type integer or float, both non-negative.')

        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)
        
        else:
            self.interest_rate=Decimal(str(interest_rate/100)).quantize(Decimal(str(0.0001)))
        
        '''
        Input validation for attribute loan_term
        '''
        try:
            if type(loan_term) == int:
                if loan_term < 1:
                    raise ValueError('Variable LOAN_TERM can only be integers greater or equal to 1.')
            else:
                raise TypeError('Variable LOAN_TERM can only be of type integer.')

        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)

        else:
            self.loan_term=loan_term
            
        '''
        Input validation for attribute payment_amount
        '''
        try:
            if payment_amount is None:
                pass
            elif payment_amount is not None and (type(payment_amount) == int or type(payment_amount) == float):
                if payment_amount < 0:
                    raise ValueError('Variable PAYMENT_AMOUNT can only be non-negative.')
            else:
                raise TypeError('Variable PAYMENT_AMOUNT can only be of type integer or float, both non-negative.')
            
        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)

        else:
            self.payment_amount=payment_amount
            
        '''
        Input validation for attribute start_date
        '''
        try:
            if start_date is None:
                raise TypeError('Varable START_DATE must by of type date with format YYYY-MM-DD')
            elif bool(dt.datetime.strptime(start_date,'%Y-%m-%d')) is False:
                raise ValueError 
        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)
        else:
            self.start_date=dt.datetime.strptime(start_date,'%Y-%m-%d')
            
        '''
        Input validation for attribute first_paymnt_date
        '''
        try:
            if first_payment_date is None:
                pass
            elif bool(dt.datetime.strptime(first_payment_date,'%Y-%m-%d')) is False:
                raise ValueError

        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)
        else:
            self.first_payment_date=dt.datetime.strptime(first_payment_date,'%Y-%m-%d') if first_payment_date is not None else None

            try:
                if self.first_payment_date is None:
                    pass
                elif self.start_date > self.first_payment_date:
                    raise ValueError('FIRST_PAYMENT_DATE cannot be before START_DATE')

            except ValueError as val_e:
                print(val_e)

        '''
        Input validation for attribute payment_end_of_month
        '''
        try:
            if type(payment_end_of_month) is not bool:
                raise TypeError('Variable PAYMENT_END_OF_MONTH can only be of type boolean (either True or False)')

        except TypeError as typ_e:
            print(typ_e)

        else:
            self.payment_end_of_month = payment_end_of_month
        
        '''
        Input validation for attribute annual_payments
        '''
        try:
            if type(annual_payments) == int:
                if annual_payments not in [12,4,2,1]:
                    raise ValueError('Attribute ANNUAL_PAYMENTS must be either set to 12, 4, 2 or 1.')
            else:
                raise TypeError('Attribute ANNUAL_PAYMENTS must be of type integer.')
        
        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)
        else:
            self.annual_payments=annual_payments

        '''
        Setting of no_of_payments and delta_dt if loan_term and annual_payments are set.
        '''
        try:
            if hasattr(self,'loan_term') is False or hasattr(self,'annual_payments') is False:
                print(self.loan_term)
                print(self.annual_payments)
                raise ValueError('Please make sure that LOAN_TERM and/or ANNUAL_PAYMENTS were correctly defined.11')

        except ValueError as val_e:
            print(val_e)
        
        else:
            self.no_of_payments=self.loan_term * self.annual_payments
            self.delta_dt=Decimal(str(12/self.annual_payments))


        '''
        Input validation for attribute interest_only_period
        '''
        try:
            if type(interest_only_period) == int:
                if interest_only_period < 0:
                    raise ValueError('Attribute INTEREST_ONLY_PERIOD must be greater or equal to 0.')
                elif hasattr(self,'no_of_payments') is False:
                    raise ValueError('Please make sure that LOAN_TERM and/or ANNUAL_PAYMENTS were correctly defined.')
                elif hasattr(self,'no_of_payments') is True and self.no_of_payments - interest_only_period < 0:
                    raise ValueError('Attribute INTEREST_ONLY_PERIOD is greater than product of LOAN_TERM and ANNUAL_PAYMENTS.')
            else:
                raise TypeError('Attribute INTEREST_ONLY_PERIOD must be of type integer.')

        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)
        else:
            self.interest_only_period=interest_only_period

        '''
        Input validation for attribute compounding_method
        '''
        try:
            if type(compounding_method) == str:
                if compounding_method not in ['30A/360','30U/360','30E/360','30E/360 ISDA','A/360','A/365F','A/A ISDA', 'A/A AFB']:
                    raise ValueError('Attribute COMPOUNDING_METHOD must be set to one of the following: 30A/360, 30U/360, 30E/360, 30E/360 ISDA, A/360, A/365F, A/A ISDA, A/A AFB.')
            else:
                raise TypeError('Attribute COMPOUNDING_METHOD must be of type string')

        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)
        else:
            self.compounding_method=compounding_method

        '''
        Input validation for attribute loan_type
        '''
        try:
            if type(loan_type) == str:
                if loan_type not in ['annuity','linear','interest-only']:
                    raise ValueError('Attribute LOAN_TYPE must be either set to annuity or linear or interest-only.')
            else:
                raise TypeError('Attribute LOAN_TYPE must be of type string')

        except ValueError as val_e:
            print(val_e)
        except TypeError as typ_e:
            print(typ_e)
        else:
            self.loan_type=loan_type

        # define non-input variables
        self.special_payments=[]
        self.special_payments_schedule=[]

    @staticmethod
    def _quantize(amount):
        return Decimal(str(amount)).quantize(Decimal(str(0.01)))

    @staticmethod
    def _get_day_count(dt1,dt2,method,eom=False):

        def get_julian_day_number(y,m,d):
            julian_day_count = (1461 * (y + 4800 + (m - 14)/12))/4 +(367 * (m - 2 - 12 * ((m - 14)/12)))/12 - (3 * ((y + 4900 + (m - 14)/12)/100))/4 + d - 32075
            return julian_day_count

        y1, m1, d1 = dt1.year, dt1.month, dt1.day
        y2, m2, d2 = dt2.year, dt2.month, dt2.day
        dt1_eom_day=cal.monthrange(y1,m1)[1]
        dt2_eom_day=cal.monthrange(y2,m2)[1]

        if method in {'30A/360','30U/360','30E/360','30E/360 ISDA'}:
            if method=='30A/360':
                d1 = min(d1,30)
                d2 = min(d2,30) if d1 == 30 else d2
            if method=='30U/360':
                if eom and m1 == 2 and d1==dt1_eom_day and m2==2 and d2==dt2_eom_day:
                    d2=30
                if eom and m1 == 2 and d1==dt1_eom_day:
                    d1=30
                if d2 == 31 and d1 >= 30:
                    d2=30
                if d1==31:
                    d1=30
            if method=='30E/360':
                if d1 == 31:
                    d1=30
                if d2 == 31:
                    d2=30
            if method=='30E/360 ISDA':
                if d1==dt1_eom_day:
                    d1=30
                if d2==dt2_eom_day and m2 != 2:
                    d2=30

            day_count = (360*(y2-y1)+30*(m2-m1)+(d2-d1))
            year_days = 360

        if method=='A/365F':
            day_count=(dt2-dt1).days
            year_days=365

        if method=='A/360':
            day_count=(dt2-dt1).days
            year_days=360

        if method in {'A/A ISDA','A/A AFB'}:
            djn_dt1= get_julian_day_number(y1,m1,d1)
            djn_dt2= get_julian_day_number(y2,m2,d2)
            if y1==y2:
                day_count=djn_dt2-djn_dt1
                if method=='A/A ISDA':
                    year_days=366 if cal.isleap(y2) else 365
                if method=='A/A AFB':
                    year_days=366 if cal.isleap(y1) and (m1<3) else 365
            if y1 < y2:
                djn_dt1_eoy= get_julian_day_number(y1,12,31)
                day_count_dt1=djn_dt1_eoy-djn_dt1
                if method=='A/A ISDA':
                    year_days_dt1=366 if cal.isleap(y1) else 365
                if method=='A/A AFB':
                    year_days_dt1=366 if cal.isleap(y1) and (m1<3) else 365

                djn_dt2_boy= get_julian_day_number(y2,1,1)
                day_count_dt2=djn_dt2-djn_dt2_boy
                if method=='A/A ISDA':
                    year_days_dt2=366 if cal.isleap(y2) else 365
                if method=='A/A AFB':
                    year_days_dt2=366 if cal.isleap(y2) and (m2>=3) else 365

                diff=y2-y1-1

                day_count=(day_count_dt1*year_days_dt2)+(day_count_dt2*year_days_dt1)+(diff*year_days_dt1*year_days_dt2)
                year_days=year_days_dt1*year_days_dt2

        factor = day_count / year_days
        return factor

    @staticmethod
    def _get_special_payment_schedule(self,special_payment):
        no_of_payments=special_payment.special_payment_term * special_payment.annual_payments
        annual_payments = special_payment.annual_payments
        dt0=dt.datetime.strptime(special_payment.first_payment_date,'%Y-%m-%d')
        special_payment_amount=self._quantize(special_payment.payment_amount)
        initial_special_payment=Payment(date=dt0,payment_amount=self._quantize(0),interest_amount=self._quantize(0),principal_amount=self._quantize(0),special_principal_amount=special_payment_amount,total_principal_amount=self._quantize(0),loan_balance_amount=self._quantize(0))
        special_payment_schedule=[initial_special_payment]

        for i in range(1,no_of_payments):
            date=dt0+relativedelta(months=i*12/annual_payments)
            special_payment=Payment(date=date,payment_amount=self._quantize(0),interest_amount=self._quantize(0),principal_amount=self._quantize(0),special_principal_amount=special_payment_amount,total_principal_amount=self._quantize(0),loan_balance_amount=self._quantize(0))
            special_payment_schedule.append(special_payment)

        return special_payment_schedule

    '''
    Define method that calculates payment schedule
    '''
    def get_payment_schedule(self):

        attributes = ['loan_amount','interest_rate','loan_term','payment_amount','start_date','first_payment_date','payment_end_of_month','annual_payments','no_of_payments','delta_dt','interest_only_period','compounding_method','special_payments','special_payments_schedule']
        raise_error_flag = 0
        for attribute in attributes:
            if hasattr(self,attribute) is False:
                raise_error_flag = raise_error_flag + 1
        
        try:
            if raise_error_flag != 0:
                raise ValueError('Necessary attributes are not well defined, please review your inputs')

        except ValueError as val_e:
            print(val_e)
        else:
            initial_payment=Payment(date=self.start_date,payment_amount=self._quantize(0),interest_amount=self._quantize(0),principal_amount=self._quantize(0),special_principal_amount=self._quantize(0),total_principal_amount=self._quantize(0),loan_balance_amount=self._quantize(self.loan_amount))
            payment_schedule=[initial_payment]
            interest_only_period = self.interest_only_period

            # take care of loan type
            if self.loan_type == 'annuity':
                if self.payment_amount is None:
                    regular_principal_payment_amount= self.loan_amount*((self.interest_rate/self.annual_payments)*(1+(self.interest_rate/self.annual_payments))**((self.no_of_payments-interest_only_period)))/((1+(self.interest_rate/self.annual_payments))**((self.no_of_payments-interest_only_period))-1)
                else:
                    regular_principal_payment_amount=self.payment_amount

            if self.loan_type == 'linear':
                if self.payment_amount is None:
                    regular_principal_payment_amount= self.loan_amount / (self.no_of_payments-self.interest_only_period)
                else:
                    regular_principal_payment_amount=self.payment_amount

            if self.loan_type == 'interest-only':
                    regular_principal_payment_amount=0
                    interest_only_period = self.no_of_payments


            if self.first_payment_date is None:
                if self.payment_end_of_month==True:
                    if self.start_date.day == cal.monthrange(self.start_date.year,self.start_date.month)[1]:
                        dt0 = self.start_date
                    else:
                        dt0 = dt.datetime(self.start_date.year,self.start_date.month,cal.monthrange(self.start_date.year,self.start_date.month)[1],0,0)+relativedelta(months=-12/self.annual_payments)
                else:
                    dt0 = self.start_date
            else:
                dt0=self.first_payment_date+relativedelta(months=-12/self.annual_payments)

            # take care of special payments
            special_payments_schedule_raw=[]
            special_payments_schedule=[]
            special_payments_dates=[]
            if len(self.special_payments_schedule)>0:
                for i in range(len(self.special_payments_schedule)):
                    for j in range(len(self.special_payments_schedule[i])):
                        special_payments_schedule_raw.append([self.special_payments_schedule[i][j].date,self.special_payments_schedule[i][j].special_principal_amount])
                        if self.special_payments_schedule[i][j].date not in special_payments_dates:
                            special_payments_dates.append(self.special_payments_schedule[i][j].date)

            for i in range(len(special_payments_dates)):
                amt=self._quantize(str(0))
                for j in range(len(special_payments_schedule_raw)):
                    if special_payments_schedule_raw[j][0]==special_payments_dates[i]:
                        amt+=special_payments_schedule_raw[j][1]
                special_payments_schedule.append([special_payments_dates[i],amt])

            # calculate payment schedule
            m=0
            for i in range(1,self.no_of_payments+1):

                date=dt0+relativedelta(months=i*12/self.annual_payments)
                if self.payment_end_of_month==True and self.first_payment_date is None:
                    eom_day=cal.monthrange(date.year,date.month)[1]
                    date=date.replace(day=eom_day)#dt.datetime(date.year,date.month,eom_day)

                special_principal_amount= self._quantize(0)
                bop_date = payment_schedule[(i+m)-1].date
                compounding_factor=Decimal(str(self._get_day_count(bop_date,date,self.compounding_method,eom=self.payment_end_of_month)))
                balance_bop=self._quantize(payment_schedule[(i+m)-1].loan_balance_amount)

                for j in range(len(special_payments_schedule)):
                    if date == special_payments_schedule[j][0]:
                        special_principal_amount = special_payments_schedule[j][1]
                    if (bop_date < special_payments_schedule[j][0] and special_payments_schedule[j][0] < date):
                        # handle special payment inserts
                        compounding_factor= Decimal(str(self._get_day_count(bop_date,special_payments_schedule[j][0],self.compounding_method,eom=self.payment_end_of_month)))
                        interest_amount = self._quantize(0) if balance_bop == Decimal(str(0)) else self._quantize(balance_bop*self.interest_rate*compounding_factor)
                        principal_amount= self._quantize(0)
                        special_principal_amount = self._quantize(0) if balance_bop == Decimal(str(0)) else min(special_payments_schedule[j][1]-interest_amount,balance_bop)
                        total_principal_amount=min(principal_amount+special_principal_amount,balance_bop)
                        total_payment_amount=total_principal_amount+interest_amount
                        balance_eop = max(balance_bop-total_principal_amount,self._quantize(0))
                        payment = Payment(date=special_payments_schedule[j][0], payment_amount=total_payment_amount,interest_amount=interest_amount,principal_amount=principal_amount,special_principal_amount=special_principal_amount,total_principal_amount=special_principal_amount,loan_balance_amount=balance_eop)
                        payment_schedule.append(payment)
                        m+=1
                        # handle regular payment inserts : update bop_date and bop_date, and special_principal_amount
                        bop_date=special_payments_schedule[j][0]
                        balance_bop=balance_eop
                        special_principal_amount=self._quantize(0)
                        compounding_factor=Decimal(str(self._get_day_count(bop_date,date,self.compounding_method,eom=self.payment_end_of_month)))

                interest_amount= self._quantize(0) if balance_bop == Decimal(str(0)) else self._quantize(balance_bop*self.interest_rate*compounding_factor)

                principal_amount = self._quantize(0) if balance_bop == Decimal(str(0)) or interest_only_period >= i else min(self._quantize(regular_principal_payment_amount)-(interest_amount if self.loan_type == 'annuity' else 0),balance_bop)
                special_principal_amount=min(balance_bop-principal_amount,special_principal_amount) if interest_only_period < i else self._quantize(0)
                total_principal_amount= min(principal_amount+special_principal_amount,balance_bop)
                total_payment_amount=total_principal_amount+interest_amount
                balance_eop = max(balance_bop-total_principal_amount,self._quantize(0))

                payment=Payment(date=date,payment_amount=total_payment_amount,interest_amount=interest_amount,principal_amount=principal_amount,special_principal_amount=special_principal_amount,total_principal_amount=total_principal_amount,loan_balance_amount=balance_eop)
                payment_schedule.append(payment)

            return payment_schedule

    def add_special_payment(self,payment_amount,first_payment_date,special_payment_term,annual_payments):
        special_payment=Special_Payment(payment_amount=payment_amount,first_payment_date=first_payment_date,special_payment_term=special_payment_term,annual_payments=annual_payments)
        self.special_payments.append(special_payment)
        self.special_payments_schedule.append(self._get_special_payment_schedule(self,special_payment))

    def get_loan_summary(self):
        payment_schedule=self.get_payment_schedule()
        total_payment_amount=0
        total_interest_amount=0
        total_principal_amount=0
        repayment_to_principal=0

        for payment in payment_schedule:
            total_payment_amount +=payment.payment_amount
            total_interest_amount +=payment.interest_amount
            total_principal_amount +=payment.total_principal_amount

        repayment_to_principal=self._quantize(total_payment_amount/total_principal_amount)
        loan_summary=Loan_Summary(loan_amount=self._quantize(self.loan_amount),total_payment_amount=total_payment_amount,total_principal_amount=total_principal_amount,total_interest_amount=total_interest_amount,residual_loan_balance=self._quantize(self.loan_amount-total_principal_amount),repayment_to_principal=repayment_to_principal)

        return loan_summary
