# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

from trytond.model import ModelSQL, ModelView, fields, tree
from trytond.model import sequence_ordered
from trytond.pool import Pool
from trytond.pyson import Eval, Bool
from trytond.exceptions import UserError

__all__ = [
    'FacilityStatus',
    'Facility',
    'FacilityCurrency',
    'FacilityCovenant',
    'FacilityLimit',
    'FacilityLimitHaircut',
    'FacilityLimitCurrency',
    'FacilityLimitCost',
    'FacilityLimitCostVariation',
    'FacilityLimitOpStatus',
    'FacilityLimitBankAccount',
    'FacilityCap',
    'FacilityCapHaircut',
    'FacilityConstraint',
]

# ---------------------------------------------------------------------------
# Shared Selection lists
# ---------------------------------------------------------------------------

ATTRIBUTE_TYPES = [
    ('commodity', 'Commodity'),
    ('article', 'Article'),
    ('origin', 'Origin'),
    ('destination', 'Destination'),
    ('loading_place', 'Loading Place'),
    ('supplier', 'Supplier'),
    ('customer', 'Customer'),
    ('payment_condition_purchase', 'Payment Condition (Purchase)'),
    ('payment_condition_sale', 'Payment Condition (Sale)'),
    ('purchase_currency', 'Purchase Currency'),
    ('warehouse', 'Warehouse'),
    ('incoterm', 'Incoterm'),
    ('counterparty_rating', 'Counterparty Rating'),
    ('receivable_category', 'Receivable Category'),
]


# ---------------------------------------------------------------------------
# Facility Status (configurable reference)
# ---------------------------------------------------------------------------

class FacilityStatus(ModelSQL, ModelView):
    'Facility Status'
    __name__ = 'trade_finance.facility_status'
    _rec_name = 'name'

    code = fields.Char('Code', required=True)
    name = fields.Char('Name', required=True)
    active = fields.Boolean('Active')

    @staticmethod
    def default_active():
        return True


# ---------------------------------------------------------------------------
# Facility Header
# ---------------------------------------------------------------------------

class Facility(ModelSQL, ModelView):
    'TF Facility'
    __name__ = 'trade_finance.facility'
    _rec_name = 'name'

    name = fields.Char('Name', required=True)
    tfe = fields.Many2One('bank', 'Trade Finance Entity', required=True,
        ondelete='RESTRICT',
        help='Bank or fund providing this facility')
    description = fields.Text('Description')

    status = fields.Many2One('trade_finance.facility_status', 'Status',
        ondelete='RESTRICT')
    commitment_status = fields.Selection([
        ('uncommitted', 'Uncommitted'),
        ('committed', 'Committed'),
    ], 'Commitment Status', required=True)

    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')

    currency = fields.Many2One('currency.currency', 'Facility Currency',
        required=True, ondelete='RESTRICT')
    is_tpa = fields.Boolean('TPA',
        help='Third Party Account — broker managed structure')
    broker = fields.Many2One('party.party', 'Broker',
        states={'invisible': ~Bool(Eval('is_tpa'))},
        depends=['is_tpa'], ondelete='RESTRICT')
    broker_account = fields.Char('Broker Account',
        states={'invisible': ~Bool(Eval('is_tpa'))},
        depends=['is_tpa'])

    limits = fields.One2Many('trade_finance.facility_limit', 'facility',
        'Limits', domain=[('parent', '=', None)],
        filter=[('parent', '=', None)])
    currencies = fields.One2Many('trade_finance.facility_currency', 'facility',
        'Accepted Currencies')
    caps = fields.One2Many('trade_finance.facility_cap', 'facility', 'Caps')
    covenants = fields.One2Many('trade_finance.facility_covenant', 'facility',
        'Covenants')
    constraints = fields.One2Many('trade_finance.facility_constraint',
        'facility', 'Facility Constraints',
        domain=[('limit', '=', None)])

    @classmethod
    def default_status(cls):
        pool = Pool()
        FacilityStatus = pool.get('trade_finance.facility_status')
        statuses = FacilityStatus.search([('code', '=', 'DRAFT')], limit=1)
        if statuses:
            return statuses[0].id
        return None

    @staticmethod
    def default_commitment_status():
        return 'uncommitted'

    @staticmethod
    def default_is_tpa():
        return False


# ---------------------------------------------------------------------------
# Facility Accepted Currency
# ---------------------------------------------------------------------------

class FacilityCurrency(ModelSQL, ModelView):
    'Facility Currency'
    __name__ = 'trade_finance.facility_currency'

    facility = fields.Many2One('trade_finance.facility', 'Facility',
        required=True, ondelete='CASCADE')
    currency = fields.Many2One('currency.currency', 'Currency',
        required=True, ondelete='RESTRICT')
    fx_haircut_formula = fields.Many2One('trade_finance.haircut_formula',
        'FX Haircut Formula', ondelete='RESTRICT')
    fx_feeder = fields.Many2One('trade_finance.fx_feeder', 'FX Rate Feeder',
        ondelete='RESTRICT')
    valuation_method = fields.Many2One('trade_finance.valuation_method',
        'Valuation Method', ondelete='RESTRICT')
    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')


# ---------------------------------------------------------------------------
# Covenant
# ---------------------------------------------------------------------------

class FacilityCovenant(ModelSQL, ModelView):
    'Facility Covenant'
    __name__ = 'trade_finance.facility_covenant'
    _rec_name = 'name'

    facility = fields.Many2One('trade_finance.facility', 'Facility',
        required=True, ondelete='CASCADE')
    name = fields.Char('Covenant', required=True)
    ratio_type = fields.Selection([
        ('leverage_ratio', 'Leverage Ratio'),
        ('dso', 'DSO'),
        ('interest_coverage', 'Interest Coverage Ratio'),
        ('current_ratio', 'Current Ratio'),
        ('other', 'Other'),
    ], 'Ratio Type', required=True)
    threshold = fields.Numeric('Threshold', digits=(16, 4))
    currency = fields.Many2One('currency.currency', 'Currency',
        ondelete='RESTRICT')
    notes = fields.Text('Notes')


# ---------------------------------------------------------------------------
# Facility Limit (Global Limit + Sub-Limits in one model)
# ---------------------------------------------------------------------------

class FacilityLimit(tree(), ModelSQL, ModelView):
    'Facility Limit'
    __name__ = 'trade_finance.facility_limit'
    _rec_name = 'name'

    facility = fields.Many2One('trade_finance.facility', 'Facility',
        required=True, ondelete='CASCADE',
        states={'readonly': Bool(Eval('parent'))},
        depends=['parent'])
    parent = fields.Many2One('trade_finance.facility_limit', 'Parent Limit',
        left='left', right='right',
        ondelete='RESTRICT',
        domain=[('facility', '=', Eval('facility'))],
        depends=['facility'],
        help='Leave empty for Global Limit (root node)')
    children = fields.One2Many('trade_finance.facility_limit', 'parent',
        'Sub-Limits')
    left = fields.Integer('Left', required=True)
    right = fields.Integer('Right', required=True)

    name = fields.Char('Name', required=True)
    alternative_name = fields.Char('Limit Alternative Name')
    financing_type = fields.Many2One('trade_finance.financing_type',
        'Financing Type', ondelete='RESTRICT')
    currency = fields.Many2One('currency.currency', 'Currency',
        ondelete='RESTRICT',
        states={'readonly': Bool(Eval('parent'))},
        depends=['parent'],
        help='Currency of the global limit. Sub-limits inherit it from '
             'their parent.')
    amount = fields.Numeric('Amount', digits=(16, 2), required=True)
    tenor = fields.Integer('Tenor (days)',
        help='Maximum duration of financing from drawdown to repayment')
    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')
    sequence = fields.Integer('Sequence')
    notes = fields.Text('Notes')

    _order = [('sequence', 'ASC'), ('id', 'ASC')]

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        cls._rebuild_tree('parent', None, 0)

    haircuts = fields.One2Many('trade_finance.facility_limit_haircut', 'limit',
        'Haircuts')
    currencies = fields.One2Many('trade_finance.facility_limit_currency',
        'limit', 'Accepted Currencies')
    costs = fields.One2Many('trade_finance.facility_limit_cost', 'limit',
        'Costs')
    op_statuses = fields.One2Many('trade_finance.facility_limit_op_status',
        'limit', 'Expected Operational Statuses')
    bank_accounts = fields.One2Many('trade_finance.facility_limit_bank_account',
        'limit', 'Bank Accounts')
    constraints = fields.One2Many('trade_finance.facility_constraint', 'limit',
        'Limit Constraints',
        domain=[('facility', '=', None)])

    @staticmethod
    def default_sequence():
        return 10

    @staticmethod
    def default_left():
        return 0

    @staticmethod
    def default_right():
        return 0

    @fields.depends('facility', 'currency', 'date_from', 'date_to', 'parent',
        '_parent_parent.facility', '_parent_parent.currency',
        '_parent_parent.date_from', '_parent_parent.date_to')
    def on_change_parent(self):
        if self.parent:
            self.facility = self.parent.facility
            self.currency = self.parent.currency
            if not self.date_from:
                self.date_from = self.parent.date_from
            if not self.date_to:
                self.date_to = self.parent.date_to

    @fields.depends('facility', 'currency', 'date_from', 'date_to', 'parent',
        '_parent_facility.currency', '_parent_facility.date_from',
        '_parent_facility.date_to')
    def on_change_facility(self):
        if self.facility and not self.parent:
            if not self.currency:
                self.currency = self.facility.currency
            if not self.date_from:
                self.date_from = self.facility.date_from
            if not self.date_to:
                self.date_to = self.facility.date_to

    @classmethod
    def create(cls, vlist):
        vlist = [v.copy() for v in vlist]
        for values in vlist:
            if values.get('parent') and not values.get('facility'):
                parent = cls(values['parent'])
                values['facility'] = parent.facility.id
            if values.get('parent'):
                parent = cls(values['parent'])
                values['currency'] = (
                    parent.currency.id if parent.currency else None)
                values.setdefault('date_from', parent.date_from)
                values.setdefault('date_to', parent.date_to)
            elif values.get('facility'):
                facility = Pool().get('trade_finance.facility')(
                    values['facility'])
                values.setdefault('currency',
                    facility.currency.id if facility.currency else None)
                values.setdefault('date_from', facility.date_from)
                values.setdefault('date_to', facility.date_to)
        return super().create(vlist)

    @classmethod
    def write(cls, *args):
        args = list(args)
        actions = iter(args)
        new_args = []
        check_descendants = []
        sync_currency = []
        for limits, values in zip(actions, actions):
            values = values.copy()
            if values.get('parent'):
                parent = cls(values['parent'])
                values['facility'] = parent.facility.id
                values['currency'] = (
                    parent.currency.id if parent.currency else None)
            if {'parent', 'currency'} & set(values):
                sync_currency.extend(limits)
            if {'parent', 'currency', 'date_from', 'date_to'} & set(values):
                check_descendants.extend(limits)
            new_args.extend((limits, values))
        result = super().write(*new_args)
        cls._sync_descendant_currencies(sync_currency)
        cls._check_descendants(check_descendants)
        return result

    @classmethod
    def _sync_descendant_currencies(cls, limits):
        for limit in limits:
            limit = cls(limit.id)
            if not limit.currency:
                continue
            descendants = cls.search([('parent', 'child_of', [limit.id])])
            if descendants:
                super(FacilityLimit, cls).write(
                    descendants, {'currency': limit.currency.id})

    @classmethod
    def _check_descendants(cls, limits):
        descendants = []
        for limit in limits:
            descendants.extend(
                cls.search([('parent', 'child_of', [limit.id])]))
        if descendants:
            cls.validate(descendants)

    @classmethod
    def validate(cls, limits):
        super().validate(limits)
        for limit in limits:
            limit.check_single_root()
            limit.check_required_values()
            limit.check_parent_consistency()
            limit.check_dates_within_parent()
            limit.check_amount_vs_parent()
            limit.check_children_amounts()

    def check_single_root(self):
        if self.parent is None:
            roots = self.__class__.search([
                ('facility', '=', self.facility.id),
                ('parent', '=', None),
                ('id', '!=', self.id),
            ])
            if roots:
                raise UserError(
                    f"Facility '{self.facility.name}' already has a Global "
                    f"Limit ('{roots[0].name}'). Only one root limit is "
                    f"allowed per facility.")

    def check_required_values(self):
        if not self.currency:
            raise UserError(
                f"Limit '{self.name}' must define or inherit a currency.")
        if not self.date_from or not self.date_to:
            raise UserError(
                f"Limit '{self.name}' must define valid from and valid to "
                f"dates.")
        if self.date_from > self.date_to:
            raise UserError(
                f"Limit '{self.name}' valid from date cannot be after "
                f"its valid to date.")

    def check_parent_consistency(self):
        if not self.parent:
            return
        if self.facility != self.parent.facility:
            raise UserError(
                f"Sub-limit '{self.name}' must belong to the same facility "
                f"as parent limit '{self.parent.name}'.")
        if self.currency != self.parent.currency:
            raise UserError(
                f"Sub-limit '{self.name}' must use the same currency as "
                f"parent limit '{self.parent.name}'.")

    def check_dates_within_parent(self):
        if self.date_from and self.date_to and self.date_from > self.date_to:
            raise UserError(
                f"Limit '{self.name}' valid from date cannot be after its "
                f"valid to date.")
        if not self.parent:
            return
        if (self.date_from and self.parent.date_from
                and self.date_from < self.parent.date_from):
            raise UserError(
                f"Sub-limit '{self.name}' valid from date cannot be before "
                f"parent limit '{self.parent.name}' valid from date.")
        if (self.date_to and self.parent.date_to
                and self.date_to > self.parent.date_to):
            raise UserError(
                f"Sub-limit '{self.name}' valid to date cannot be after "
                f"parent limit '{self.parent.name}' valid to date.")

    def check_amount_vs_parent(self):
        if self.parent and self.amount > self.parent.amount:
            raise UserError(
                f"Sub-limit '{self.name}' amount ({self.amount}) "
                f"cannot exceed parent limit '{self.parent.name}' "
                f"amount ({self.parent.amount}).")

    def check_children_amounts(self):
        for child in self.children:
            if child.amount > self.amount:
                raise UserError(
                    f"Sub-limit '{child.name}' amount ({child.amount}) "
                    f"cannot exceed parent limit '{self.name}' "
                    f"amount ({self.amount}).")


# ---------------------------------------------------------------------------
# Limit Haircut
# ---------------------------------------------------------------------------

class FacilityLimitHaircut(ModelSQL, ModelView):
    'Facility Limit Haircut'
    __name__ = 'trade_finance.facility_limit_haircut'

    limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
        required=True, ondelete='CASCADE')
    attribute = fields.Selection(ATTRIBUTE_TYPES, 'Attribute Type',
        required=True)
    value = fields.Char('Value',
        help='The specific attribute value this haircut applies to '
             '(e.g. "Brazil", "Coffee", "USD")')
    haircut_pct = fields.Numeric('Haircut (%)', digits=(16, 2), required=True)
    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')


# ---------------------------------------------------------------------------
# Limit Accepted Currency
# ---------------------------------------------------------------------------

class FacilityLimitCurrency(ModelSQL, ModelView):
    'Facility Limit Currency'
    __name__ = 'trade_finance.facility_limit_currency'

    limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
        required=True, ondelete='CASCADE')
    currency = fields.Many2One('currency.currency', 'Currency',
        required=True, ondelete='RESTRICT')
    haircut_pct = fields.Numeric('FX Haircut (%)', digits=(16, 2))
    fx_feeder = fields.Many2One('trade_finance.fx_feeder', 'FX Rate Feeder',
        ondelete='RESTRICT')
    valuation_method = fields.Many2One('trade_finance.valuation_method',
        'Valuation Method', ondelete='RESTRICT')
    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')


# ---------------------------------------------------------------------------
# Limit Cost
# ---------------------------------------------------------------------------

class FacilityLimitCost(ModelSQL, ModelView):
    'Facility Limit Cost'
    __name__ = 'trade_finance.facility_limit_cost'

    limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
        required=True, ondelete='CASCADE')
    cost_type = fields.Selection([
        ('interest', 'Interest-Based'),
        ('flat', 'Flat / Minimal Amount'),
    ], 'Cost Type', required=True)

    # Interest-based fields
    spread = fields.Numeric('Spread (%)', digits=(16, 4),
        states={'invisible': Eval('cost_type') != 'interest'},
        depends=['cost_type'])
    index = fields.Many2One('trade_finance.market_index', 'Market Index',
        ondelete='RESTRICT',
        states={'invisible': Eval('cost_type') != 'interest'},
        depends=['cost_type'])
    index_term = fields.Many2One('trade_finance.index_term', 'Index Term',
        ondelete='RESTRICT',
        states={'invisible': Eval('cost_type') != 'interest'},
        depends=['cost_type'])
    interest_formula = fields.Many2One('trade_finance.interest_formula',
        'Interest Formula', ondelete='RESTRICT',
        states={'invisible': Eval('cost_type') != 'interest'},
        depends=['cost_type'])

    # Flat fields
    flat_amount = fields.Numeric('Flat Amount', digits=(16, 2),
        states={'invisible': Eval('cost_type') != 'flat'},
        depends=['cost_type'])
    flat_currency = fields.Many2One('currency.currency', 'Currency',
        ondelete='RESTRICT',
        states={'invisible': Eval('cost_type') != 'flat'},
        depends=['cost_type'])

    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')

    variations = fields.One2Many('trade_finance.facility_limit_cost_variation',
        'cost', 'Cost Variations')


# ---------------------------------------------------------------------------
# Limit Cost Variation
# ---------------------------------------------------------------------------

class FacilityLimitCostVariation(ModelSQL, ModelView):
    'Facility Limit Cost Variation'
    __name__ = 'trade_finance.facility_limit_cost_variation'

    cost = fields.Many2One('trade_finance.facility_limit_cost', 'Cost',
        required=True, ondelete='CASCADE')
    attribute = fields.Selection(ATTRIBUTE_TYPES, 'Attribute Type',
        required=True)
    value = fields.Char('Value')
    variation_type = fields.Selection([
        ('pct', 'Percentage (+/-)'),
        ('flat', 'Flat Amount (+/-)'),
    ], 'Variation Type', required=True)
    variation_value = fields.Numeric('Variation Value', digits=(16, 4),
        required=True)
    variation_currency = fields.Many2One('currency.currency', 'Currency',
        ondelete='RESTRICT',
        states={'invisible': Eval('variation_type') != 'flat'},
        depends=['variation_type'])


# ---------------------------------------------------------------------------
# Limit Expected Operational Status
# ---------------------------------------------------------------------------

class FacilityLimitOpStatus(ModelSQL, ModelView):
    'Facility Limit Operational Status'
    __name__ = 'trade_finance.facility_limit_op_status'

    limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
        required=True, ondelete='CASCADE')
    operational_status = fields.Many2One('trade_finance.operational_status',
        'Operational Status', required=True, ondelete='RESTRICT')
    evidence_type = fields.Many2One('trade_finance.evidence_type',
        'Required Evidence', ondelete='RESTRICT',
        help='Evidence required when this operational status is reached')


# ---------------------------------------------------------------------------
# Limit Bank Account
# ---------------------------------------------------------------------------

class FacilityLimitBankAccount(ModelSQL, ModelView):
    'Facility Limit Bank Account'
    __name__ = 'trade_finance.facility_limit_bank_account'

    limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
        required=True, ondelete='CASCADE')
    bank_account = fields.Many2One('bank.account', 'Bank Account',
        required=True, ondelete='RESTRICT')
    currency = fields.Many2One('currency.currency', 'Currency',
        required=True, ondelete='RESTRICT')
    is_default = fields.Boolean('Default')
    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')

    @staticmethod
    def default_is_default():
        return False


# ---------------------------------------------------------------------------
# Cap
# ---------------------------------------------------------------------------

class FacilityCap(ModelSQL, ModelView):
    'Facility Cap'
    __name__ = 'trade_finance.facility_cap'
    _rec_name = 'name'

    facility = fields.Many2One('trade_finance.facility', 'Facility',
        required=True, ondelete='CASCADE')
    name = fields.Char('Name', required=True)
    amount = fields.Numeric('Cap Amount', digits=(16, 2), required=True)
    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')

    haircuts = fields.One2Many('trade_finance.facility_cap_haircut', 'cap',
        'Haircuts')
    constraints = fields.One2Many('trade_finance.facility_constraint', 'cap',
        'Cap Constraints')


# ---------------------------------------------------------------------------
# Cap Haircut
# ---------------------------------------------------------------------------

class FacilityCapHaircut(ModelSQL, ModelView):
    'Facility Cap Haircut'
    __name__ = 'trade_finance.facility_cap_haircut'

    cap = fields.Many2One('trade_finance.facility_cap', 'Cap',
        required=True, ondelete='CASCADE')
    attribute = fields.Selection(ATTRIBUTE_TYPES, 'Attribute Type',
        required=True)
    value = fields.Char('Value')
    haircut_pct = fields.Numeric('Haircut (%)', digits=(16, 2), required=True)
    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')


# ---------------------------------------------------------------------------
# Constraint (shared by Facility, Limit, Cap)
# ---------------------------------------------------------------------------

class FacilityConstraint(ModelSQL, ModelView):
    'Facility Constraint'
    __name__ = 'trade_finance.facility_constraint'

    facility = fields.Many2One('trade_finance.facility', 'Facility',
        ondelete='CASCADE')
    limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
        ondelete='CASCADE')
    cap = fields.Many2One('trade_finance.facility_cap', 'Cap',
        ondelete='CASCADE')

    constraint_type = fields.Many2One('trade_finance.constraint_type',
        'Constraint Type', required=True, ondelete='RESTRICT')
    is_exclusion = fields.Boolean('Exclusion',
        help='Checked = Exclusion constraint, unchecked = Inclusion constraint')
    date_from = fields.Date('Valid From')
    date_to = fields.Date('Valid To')

    @staticmethod
    def default_is_exclusion():
        return False
