import datetime
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.transaction import Transaction
from sql import Literal
from sql.functions import CurrentTimestamp
from trytond.wizard import Button, StateTransition, StateView, Wizard
from trytond.pyson import Bool, Eval, Id, If

class DerivativeMatch(ModelSQL, ModelView):
    "Derivative Match"
    __name__ = 'derivative.match'

    long_position = fields.Many2One('derivative.derivative', 'Long Position', required=True)
    short_position = fields.Many2One('derivative.derivative', 'Short Position', required=True)
    quantity = fields.Numeric('Matched Quantity', digits='unit', required=True)
    match_date = fields.Date('Match Date', required=True)

    method = fields.Selection([
    ('fifo', 'FIFO'),
    ('lifo', 'LIFO'),
    ('manual', 'Manual'),
    ], 'Method', required=True)

    pnl = fields.Function(fields.Numeric('PnL', digits='currency'), 'get_pnl')

    def get_pnl(self, name):
        return (self.short_position.price - self.long_position.price) * self.quantity
    
class Derivative(ModelSQL,ModelView):
    "Derivative"
    __name__ = 'derivative.derivative'

    purchase = fields.Many2One('purchase.purchase',"Purchase")
    line = fields.Many2One('purchase.line',"Purch. Line")
    efp = fields.Boolean("EFP")
    price_index = fields.Many2One('price.price',"Curve")
    nb_ct = fields.Integer("Nb ct")
    price = fields.Numeric("Entry price",digits='currency')
    exit_price = fields.Numeric("Exit price",digits='currency')
    product = fields.Many2One('product.product',"Product")
    party = fields.Many2One('party.party','Supplier')
    currency = fields.Function(fields.Many2One('currency.currency',"Cur"),'get_cur')
    quantity = fields.Function(fields.Numeric("Quantity",digits='unit'),'get_qt')
    alloc_qty = fields.Function(fields.Numeric("Alloc. Qty",digits='unit'),'get_alloc_qt')
    unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
    amount = fields.Function(fields.Numeric("Amount",digits='currency'),'get_amount')
    currency2 = fields.Function(fields.Many2One('currency.currency',"Cur"),'get_cur2')

    trade_date = fields.Date('Trade Date')
    maturity_date = fields.Date('Maturity')
    open_qty = fields.Numeric('Open Quantity', digits='unit')

    matches_long = fields.One2Many('derivative.match', 'long_position', 'Matches (Long)', states={'invisible': (Eval('direction') == 'long')})
    matches_short = fields.One2Many('derivative.match', 'short_position', 'Matches (Short)', states={'invisible': (Eval('direction') == 'short')})

    direction = fields.Selection([
    ('long', 'Long'),
    ('short', 'Short'),
    ], 'Direction', required=True)

    state = fields.Selection([
    ('open', 'Open'),
    ('closed', 'Closed'),
    ], 'State', required=True)

    @classmethod
    def default_state(cls):
        return 'open'

    def get_amount(self,name):
        if self.price_index and self.line and self.price and self.nb_ct:
            #pi = Pool().get('price.price')(self.price_index)
            return self.price_index.get_amount_nb_ct(self.price,self.nb_ct,self.line.unit,self.line.purchase.currency if self.line.purchase else self.sale_line.sale.currency)

    def get_cur(self,name):
        if self.price_index:
            #pi = Pool().get('price.price')(self.price_index)
            return self.price_index.price_currency

    def get_cur2(self,name):
        if self.purchase:
            return self.purchase.currency

    def get_unit(self,name):
        if self.line:
            return self.line.unit

    def get_qt(self,name):
        if self.line:
            line = self.line
        else:
            if self.purchase and self.purchase.lines:
                line = self.purchase.lines[0]
            else:
                line = None
        if self.price_index and self.nb_ct and line:
            return self.price_index.get_qt(self.nb_ct,line.unit)
    
    def get_alloc_qt(self,name):
        if self.line:
            line = self.line
        else:
            if self.purchase and self.purchase.lines:
                line = self.purchase.lines[0]
            else:
                line = None
        if self.price_index and self.nb_ct and line:
            if line.price_summary:
                return line.price_summary[0].fixed_qt

class MatchWizardStart(ModelView):
    "Match Selected Derivatives"
    __name__ = 'derivative.match.start'

    method = fields.Selection([
        ('fifo', 'FIFO'),
        ('lifo', 'LIFO'),
        ('manual', 'Manual'),
    ], 'Method', required=True)

    quantity = fields.Numeric(
        'Quantity to Match',
        digits='unit',
        required=True
    )

class DerivativeMatchWizard(Wizard):
    "Derivative Match Wizard"
    __name__ = 'derivative.match.wizard'

    start = StateView(
        'derivative.match.start',
        'purchase_trade.derivative_match_start_form_view',
        [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Match', 'match', 'tryton-ok', default=True),
        ]
    )

    match = StateTransition()

    def transition_match(self):
        pool = Pool()
        Derivative = pool.get('derivative.derivative')
        Date = pool.get('ir.date')
        Match = pool.get('derivative.match')

        active_ids = Transaction().context.get('active_ids', [])
        if not active_ids:
            return 'end'

        positions = Derivative.browse(active_ids)

        longs = [
            p for p in positions
            if p.direction == 'long'
            and p.state == 'open'
            and p.open_qty > 0
        ]

        shorts = [
            p for p in positions
            if p.direction == 'short'
            and p.state == 'open'
            and p.open_qty > 0
        ]

        if not longs or not shorts:
            return 'end'

        # FIFO / LIFO ordering
        reverse = self.start.method == 'lifo'
        longs.sort(key=lambda p: p.trade_date or datetime.date.min, reverse=reverse)
        shorts.sort(key=lambda p: p.trade_date or datetime.date.min, reverse=reverse)

        remaining_qty = self.start.quantity

        for long_pos in longs:
            if remaining_qty <= 0:
                break

            for short_pos in shorts:
                if remaining_qty <= 0:
                    break

                match_qty = min(
                    long_pos.open_qty,
                    short_pos.open_qty,
                    remaining_qty
                )

                if match_qty <= 0:
                    continue

                Match.create([{
                    'long_position': long_pos.id,
                    'short_position': short_pos.id,
                    'quantity': match_qty,
                    'method': self.start.method,
                    'match_date': Date.today(),
                }])

                long_pos.open_qty -= match_qty
                short_pos.open_qty -= match_qty
                remaining_qty -= match_qty

                if long_pos.open_qty == 0:
                    long_pos.state = 'closed'
                if short_pos.open_qty == 0:
                    short_pos.state = 'closed'

                long_pos.save()
                short_pos.save()

        return 'end'

class DerivativeReport(ModelSQL, ModelView):
    "Derivative Position Report"
    __name__ = 'derivative.report'

    r_derivative = fields.Many2One(
        'derivative.derivative',
        "Derivative"
    )

    r_trade_date = fields.Date("Trade Date")
    r_maturity_date = fields.Date("Maturity")

    r_product = fields.Many2One(
        'product.product',
        "Product"
    )

    r_party = fields.Many2One(
        'party.party',
        "Counterparty"
    )

    r_direction = fields.Selection([
        (None, ''),
        ('long', 'Long'),
        ('short', 'Short'),
    ], 'Direction')

    r_state = fields.Selection([
        ('open', 'Open'),
        ('closed', 'Closed'),
    ], 'State')

    r_quantity = fields.Function(fields.Numeric("Initial Qty",digits='unit'),'get_qt')

    r_open_qty = fields.Numeric(
        "Open Qty",
        digits='unit'
    )

    r_price = fields.Numeric(
        "Entry Price",
        digits='currency'
    )

    r_currency = fields.Function(fields.Many2One('currency.currency',"Currency"),'get_cur')

    def get_cur(self,name):
        if self.r_derivative:
            if self.r_derivative.price_index:
                return self.r_derivative.price_index.price_currency
        
    def get_qt(self,name):
        if self.r_derivative:
            if self.r_derivative.line:
                line = self.r_derivative.line
            else:
                if self.r_derivative.purchase and self.r_derivative.purchase.lines:
                    line = self.r_derivative.purchase.lines[0]
                else:
                    line = None
            if self.r_derivative.price_index and self.r_derivative.nb_ct and line:
                return self.r_derivative.price_index.get_qt(self.r_derivative.nb_ct,line.unit)
        
    # ------------------------------------------------------------
    # TABLE QUERY
    # ------------------------------------------------------------
    @classmethod
    def table_query(cls):
        pool = Pool()
        Derivative = pool.get('derivative.derivative')
        d = Derivative.__table__()

        context = Transaction().context

        product = context.get('product')
        party = context.get('party')
        direction = context.get('direction')
        state = context.get('state')
        trade_from = context.get('trade_from')
        trade_to = context.get('trade_to')
        maturity_from = context.get('maturity_from')
        maturity_to = context.get('maturity_to')
        open_only = context.get('open_only')

        wh = Literal(True)

        if product:
            wh &= (d.product == product)
        if party:
            wh &= (d.party == party)
        if direction:
            wh &= (d.direction == direction)
        if state:
            wh &= (d.state == state)
        if open_only:
            wh &= (d.open_qty > 0)

        if trade_from:
            wh &= (d.trade_date >= trade_from)
        if trade_to:
            wh &= (d.trade_date <= trade_to)

        if maturity_from:
            wh &= (d.maturity_date >= maturity_from)
        if maturity_to:
            wh &= (d.maturity_date <= maturity_to)

        query = d.select(
            # mandatory technical fields
            Literal(0).as_('create_uid'),
            CurrentTimestamp().as_('create_date'),
            Literal(None).as_('write_uid'),
            Literal(None).as_('write_date'),

            d.id.as_('id'),
            d.id.as_('r_derivative'),
            d.trade_date.as_('r_trade_date'),
            d.maturity_date.as_('r_maturity_date'),
            d.product.as_('r_product'),
            d.party.as_('r_party'),
            d.direction.as_('r_direction'),
            d.state.as_('r_state'),
            #d.quantity.as_('r_quantity'),
            d.open_qty.as_('r_open_qty'),
            d.price.as_('r_price'),
            where=wh
        )

        return query

    # ------------------------------------------------------------
    # SEARCH NAME
    # ------------------------------------------------------------
    @classmethod
    def search_rec_name(cls, name, clause):
        _, operator, operand, *extra = clause
        return [
            'OR',
            ('r_product', operator, operand, *extra),
            ('r_party', operator, operand, *extra),
        ]

class DerivativeReportContext(ModelView):
    "Derivative Report Context"
    __name__ = 'derivative.report.context'

    trade_from = fields.Date("Trade Date From")
    trade_to = fields.Date("Trade Date To")

    maturity_from = fields.Date("Maturity From")
    maturity_to = fields.Date("Maturity To")

    product = fields.Many2One(
        'product.product',
        "Product"
    )

    party = fields.Many2One(
        'party.party',
        "Counterparty"
    )

    direction = fields.Selection([
        ('long', 'Long'),
        ('short', 'Short'),
    ], 'Direction')

    state = fields.Selection([
        ('open', 'Open'),
        ('closed', 'Closed'),
    ], 'State')

    open_only = fields.Boolean("Open Positions Only")

    @classmethod
    def default_trade_from(cls):
        return datetime.date(1999, 1, 1)

    @classmethod
    def default_trade_to(cls):
        pool = Pool()
        Date = pool.get('ir.date')
        return Date.today()
