from trytond.model import fields
from trytond.report import Report
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard
from itertools import chain, groupby
from operator import itemgetter
import datetime
import logging
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError

logger = logging.getLogger(__name__)

VALTYPE = [
    ('priced', 'Price'),
    ('pur. priced', 'Pur. price'),
    ('pur. efp', 'Pur. efp'),
    ('sale priced', 'Sale price'),
    ('sale efp', 'Sale efp'),
    ('line fee', 'Line fee'),
    ('pur. fee', 'Pur. fee'),
    ('sale fee', 'Sale fee'),
    ('shipment fee', 'Shipment fee'),
    ('market', 'Market'),
    ('derivative', 'Derivative'),
]

class ValuationBase(ModelSQL):
    purchase = fields.Many2One('purchase.purchase',"Purchase")
    line = fields.Many2One('purchase.line',"Purch. Line")
    date = fields.Date("Date")
    type = fields.Selection(VALTYPE, "Type")
    reference = fields.Char("Reference")
    counterparty = fields.Many2One('party.party',"Counterparty")
    product = fields.Many2One('product.product',"Product")
    state = fields.Char("State")
    price = fields.Numeric("Price",digits=(16,4))
    currency = fields.Many2One('currency.currency',"Cur")
    quantity = fields.Numeric("Quantity",digits=(16,5))
    unit = fields.Many2One('product.uom',"Unit")
    amount = fields.Numeric("Amount",digits=(16,2))
    mtm = fields.Numeric("Mtm",digits=(16,2))
    strategy = fields.Many2One('mtm.strategy',"Strategy")
    lot = fields.Many2One('lot.lot',"Lot")
    base_amount = fields.Numeric("Base Amount",digits=(16,2))
    rate = fields.Numeric("Rate", digits=(16,6))

    @classmethod
    def _base_pnl(cls, *, line, lot, pnl_type, sale=None):
        Date = Pool().get('ir.date')

        values = {
            'purchase': line.purchase.id,
            'line': line.id,
            'type': pnl_type,
            'date': Date.today(),
            'lot': lot.id,
        }

        if sale:
            values['sale'] = sale.id

        return values
    
    @classmethod
    def _build_basis_pnl(cls, *, line, lot, sale_line, pc, sign):
        Currency = Pool().get('currency.currency')
        Date = Pool().get('ir.date')
        values = cls._base_pnl(
            line=line,
            lot=lot,
            sale=sale_line.sale if sale_line else None,
            pnl_type='sale priced' if sale_line else 'pur. priced'
        )

        qty = lot.get_current_quantity_converted()

        values.update({
            'reference': f"{pc.get_name()} / {pc.ratio}%",
            'price': round(pc.price, 4),
            'counterparty': sale_line.sale.party.id if sale_line else line.purchase.party.id,
            'product': sale_line.product.id if sale_line else line.product.id,
        })

        # State
        if pc.unfixed_qt == 0:
            values['state'] = 'fixed'
        elif pc.fixed_qt == 0:
            values['state'] = 'unfixed'
        else:
            base = sale_line.quantity_theorical if sale_line else line.quantity_theorical
            values['state'] = f"part. fixed {round(pc.fixed_qt / Decimal(base) * 100, 0)}%"

        if pc.price and pc.ratio:
            amount = round(pc.price * qty * Decimal(sign) * pc.ratio / 100, 2)
            base_amount = amount
            currency = sale_line.sale.currency.id if sale_line else line.purchase.currency.id
            rate = Decimal(1)
            if line.purchase.company.currency != currency:
                with Transaction().set_context(date=Date.today()):
                    base_amount = Currency.compute(currency,amount, line.purchase.company.currency)
                    rate = round(amount / base_amount,6)
            last_price = pc.get_last_price()
            # mtm = round(Decimal(last_price) * qty * Decimal(sign), 2) if last_price else Decimal(0)
            values.update({
                'quantity': round(qty, 5),
                'amount': amount,
                'base_amount': base_amount,
                'rate': rate,
                'mtm': None, #round(amount - (mtm * pc.ratio / 100), 2),
                'unit': sale_line.unit.id if sale_line else line.unit.id,
                'currency': currency,
            })

            return values

    @classmethod
    def _build_simple_pnl(cls, *, line, lot, sale_line, price, state, sign, pnl_type):
        Currency = Pool().get('currency.currency')
        Date = Pool().get('ir.date')
        values = cls._base_pnl(
            line=line,
            lot=lot,
            sale=sale_line.sale if sale_line else None,
            pnl_type=pnl_type
        )

        qty = lot.get_current_quantity_converted()
        amount = round(price * qty * Decimal(sign), 2)
        base_amount = amount
        currency = sale_line.sale.currency.id if sale_line else line.purchase.currency.id
        company_currency = sale_line.sale.company.currency if sale_line else line.purchase.company.currency
        rate = Decimal(1)
        if line.purchase.company.currency != currency:
            with Transaction().set_context(date=Date.today()):
                base_amount = Currency.compute(currency,amount, company_currency)
                if base_amount and amount:
                    rate = round(amount / base_amount,6)

        values.update({
            'price': round(price, 4),
            'quantity': round(qty, 5),
            'amount': amount,
            'base_amount': base_amount,
            'rate': rate,
            'mtm': Decimal(0),
            'state': state,
            'unit': sale_line.unit.id if sale_line else line.unit.id,
            'currency': currency,
            'counterparty': sale_line.sale.party.id if sale_line else line.purchase.party.id,
            'product': sale_line.product.id if sale_line else line.product.id,
            'reference': (
                'Sale/Physic' if lot.lot_type == 'physic'
                else 'Sale/Open' if sale_line
                else 'Purchase/Physic'
            ),
        })

        return values

    @classmethod
    def create_pnl_price_from_line(cls, line):
        price_lines = []
        LotQt = Pool().get('lot.qt')

        for lot in line.lots:

            if line.price_type == 'basis':
                for pc in line.price_summary or []:
                    values = cls._build_basis_pnl(line=line, lot=lot, sale_line=None, pc=pc, sign=-1)
                    if line.mtm:
                        for strat in line.mtm:
                            values['mtm'] = strat.get_mtm(values['quantity'])
                            values['strategy'] = strat
                
                            if values:
                                price_lines.append(values)
                    else:
                        if values:
                            price_lines.append(values)

            elif line.price_type in ('priced', 'efp') and lot.lot_price:
                values = cls._build_simple_pnl(
                        line=line,
                        lot=lot,
                        sale_line=None,
                        price=lot.lot_price,
                        state='fixed' if line.price_type == 'priced' else 'not fixed',
                        sign=-1,
                        pnl_type=f'pur. {line.price_type}'
                    )
                if line.mtm:
                    for strat in line.mtm:
                        values['mtm'] = strat.get_mtm(values['quantity'])
                        values['strategy'] = strat
            
                        if values:
                            price_lines.append(values)
                else:
                    if values:
                        price_lines.append(values)

            sale_lots = [lot] if lot.sale_line else [
                lqt.lot_s for lqt in LotQt.search([
                    ('lot_p', '=', lot.id),
                    ('lot_s', '>', 0),
                    ('lot_quantity', '>', 0),
                ])
            ]

            for sl in sale_lots:
                sl_line = sl.sale_line
                if not sl_line:
                    continue

                if sl_line.price_type == 'basis':
                    for pc in sl_line.price_summary or []:
                        values = cls._build_basis_pnl(line=line, lot=sl, sale_line=sl_line, pc=pc, sign=+1)
                        if sl_line.mtm:
                            for strat in line.mtm:
                                values['mtm'] = strat.get_mtm(values['quantity'])
                                values['strategy'] = strat
                    
                                if values:
                                    price_lines.append(values)
                        else:
                            if values:
                                price_lines.append(values)

                elif sl_line.price_type in ('priced', 'efp'):
                    values = cls._build_simple_pnl(
                            line=line,
                            lot=sl,
                            sale_line=sl_line,
                            price=sl.lot_price_sale,
                            state='fixed' if sl_line.price_type == 'priced' else 'not fixed',
                            sign=+1,
                            pnl_type=f'sale {sl_line.price_type}'
                        )
                    if sl_line.mtm:
                        for strat in sl_line.mtm:
                            values['mtm'] = strat.get_mtm(values['quantity'])
                            values['strategy'] = strat
                
                            if values:
                                price_lines.append(values)
                    else:
                        if values:
                            price_lines.append(values)

        return price_lines


    @classmethod
    def group_fees_by_type_supplier(cls,line,fees):
        grouped = defaultdict(list)

        # Regrouper par (type, supplier)
        for fee in fees:
            key = (fee.product, fee.supplier)
            grouped[key].append(fee)
        result = []
        for key, fee_list in grouped.items():
            ordered_fees = [f for f in fee_list if f.type == 'ordered']
            if ordered_fees:
                result.extend(ordered_fees)
            else:
                budgeted_fees = [f for f in fee_list if f.type == 'budgeted']
                result.extend(budgeted_fees)
        return result
    
    @classmethod
    def create_pnl_fee_from_line(cls, line):
        fee_lines = []
        Date = Pool().get('ir.date')
        Currency = Pool().get('currency.currency')
        FeeLots = Pool().get('fee.lots')

        for lot in line.lots or []:
            fl = FeeLots.search([('lot', '=', lot.id)])
            if not fl:
                continue

            fees = [e.fee for e in fl]
            for sf in cls.group_fees_by_type_supplier(line, fees):
                sign = -1 if sf.p_r == 'pay' else 1
                if sf.mode == 'ppack':
                    price = sf.price
                    amount = sf.amount
                else:
                    price = Decimal(sf.get_price_per_qt())
                    amount = round(price * lot.get_current_quantity_converted() * sign, 2)
                if sf.currency != line.purchase.currency:
                    with Transaction().set_context(date=Date.today()):
                        price = Currency.compute(sf.currency, price, line.purchase.currency)
                if line.mtm:
                    for strat in line.mtm:
                        qty = round(lot.get_current_quantity_converted(), 5)
                        fee_lines.append({
                            'lot': lot.id,
                            'sale': lot.sale_line.sale.id if lot.sale_line else None,
                            'purchase': line.purchase.id,
                            'line': line.id,
                            'type': (
                                'shipment fee' if sf.shipment_in
                                else 'sale fee' if sf.sale_line
                                else 'pur. fee'
                            ),
                            'date': Date.today(),
                            'price': price,
                            'counterparty': sf.supplier.id,
                            'reference': f"{sf.product.name}/{'Physic' if lot.lot_type == 'physic' else 'Open'}",
                            'product': sf.product.id,
                            'state': sf.type,
                            'quantity': qty,
                            'amount': amount,
                            'mtm': strat.get_mtm(qty),
                            'strategy': strat,
                            'unit': sf.unit.id if sf.unit else line.unit.id,
                            'currency': sf.currency.id,
                        })
                else:
                    fee_lines.append({
                        'lot': lot.id,
                        'sale': lot.sale_line.sale.id if lot.sale_line else None,
                        'purchase': line.purchase.id,
                        'line': line.id,
                        'type': (
                            'shipment fee' if sf.shipment_in
                            else 'sale fee' if sf.sale_line
                            else 'pur. fee'
                        ),
                        'date': Date.today(),
                        'price': price,
                        'counterparty': sf.supplier.id,
                        'reference': f"{sf.product.name}/{'Physic' if lot.lot_type == 'physic' else 'Open'}",
                        'product': sf.product.id,
                        'state': sf.type,
                        'quantity': qty,
                        'amount': amount,
                        'mtm': Decimal(0),
                        'strategy': None,
                        'unit': sf.unit.id if sf.unit else line.unit.id,
                        'currency': sf.currency.id,
                    })

        return fee_lines

    @classmethod
    def create_pnl_der_from_line(cls, line):
        Date = Pool().get('ir.date')
        der_lines = []

        for d in line.derivatives or []:
            price = Decimal(d.price_index.get_price_per_qt(
                d.price, line.unit, line.purchase.currency
            ))

            mtm = Decimal(d.price_index.get_price(
                Date.today(), line.unit, line.purchase.currency, True
            ))

            der_lines.append({
                'purchase': line.purchase.id,
                'line': line.id,
                'type': 'derivative',
                'date': Date.today(),
                'reference': d.price_index.price_index,
                'price': round(price, 4),
                'counterparty': d.party.id,
                'product': d.product.id,
                'state': 'fixed',
                'quantity': round(d.quantity, 5),
                'amount': round(price * d.quantity * Decimal(-1), 2),
                'mtm': round((price * d.quantity * Decimal(-1)) - (mtm * d.quantity * Decimal(-1)), 2),
                'unit': line.unit.id,
                'currency': line.purchase.currency.id,
            })

        return der_lines
    
    @classmethod
    def generate(cls, line):
        Date = Pool().get('ir.date')
        Valuation = Pool().get('valuation.valuation')
        ValuationLine = Pool().get('valuation.valuation.line')
        Valuation.delete(Valuation.search([
            ('line', '=', line.id),
            ('date', '=', Date.today()),
        ]))

        ValuationLine.delete(ValuationLine.search([
            ('line', '=', line.id),
        ]))

        values = []
        values.extend(cls.create_pnl_fee_from_line(line))
        values.extend(cls.create_pnl_price_from_line(line))
        values.extend(cls.create_pnl_der_from_line(line))

        Valuation.create(values)
        ValuationLine.create(values)

class Valuation(ValuationBase, ModelView):
    "Valuation"
    __name__ = 'valuation.valuation'

    @classmethod
    def get_totals(cls):
        cursor = Transaction().connection.cursor()
        table = cls.__table__()

        sql = f"""
            WITH ranked AS (
                SELECT
                    CASE
                        WHEN line IS NOT NULL THEN 'P:' || line::text
                        WHEN sale_line IS NOT NULL THEN 'S:' || sale_line::text
                    END AS block_key,
                    date,
                    amount,
                    ROW_NUMBER() OVER (
                        PARTITION BY
                            CASE
                                WHEN line IS NOT NULL THEN 'P:' || line::text
                                WHEN sale_line IS NOT NULL THEN 'S:' || sale_line::text
                            END
                        ORDER BY date DESC
                    ) AS rn
                FROM {table}
                WHERE line IS NOT NULL
                   OR sale_line IS NOT NULL
            ),
            current_prev AS (
                SELECT
                    block_key,
                    MAX(CASE WHEN rn = 1 THEN amount END)  AS amount_t,
                    MAX(CASE WHEN rn = 2 THEN amount END)  AS amount_t1
                FROM ranked
                WHERE rn <= 2
                GROUP BY block_key
            )
            SELECT
                COALESCE(SUM(amount_t), 0)  AS total_t,
                COALESCE(SUM(amount_t1), 0) AS total_t1,
                COALESCE(SUM(amount_t), 0)
              - COALESCE(SUM(amount_t1), 0) AS variation
            FROM current_prev
        """

        cursor.execute(sql)
        total_t, total_t1, variation = cursor.fetchone()

        return total_t, total_t1, variation

class ValuationLine(ValuationBase, ModelView):
    "Last Valuation"
    __name__ = 'valuation.valuation.line'

class ValuationDyn(ModelSQL,ModelView):
    "Valuation"
    __name__ = 'valuation.valuation.dyn'

    r_purchase = fields.Many2One('purchase.purchase',"Purchase")
    r_line = fields.Many2One('purchase.line',"Line")
    r_date = fields.Date("Date")
    r_type = fields.Selection(VALTYPE, "Type")
    r_reference = fields.Char("Reference")
    r_counterparty = fields.Many2One('party.party',"Counterparty")
    r_product = fields.Many2One('product.product',"Product")
    r_state = fields.Char("State")
    r_price = fields.Numeric("Price",digits='r_unit')
    r_currency = fields.Many2One('currency.currency',"Cur")
    r_quantity = fields.Numeric("Quantity",digits='r_unit')
    r_unit = fields.Many2One('product.uom',"Unit")
    r_amount = fields.Numeric("Amount",digits='r_unit')
    r_base_amount = fields.Numeric("Base Amount",digits='r_unit')
    r_rate = fields.Numeric("Rate",digits=(16,6))
    r_mtm = fields.Numeric("Mtm",digits='r_unit')
    r_strategy = fields.Many2One('mtm.strategy',"Strategy")
    r_lot = fields.Many2One('lot.lot',"Lot")

    @classmethod
    def table_query(cls):
        Valuation = Pool().get('valuation.valuation.line')
        val = Valuation.__table__()
        context = Transaction().context
        group_pnl = context.get('group_pnl')
        wh = (val.id > 0)
        
        query = val.select(
        Literal(0).as_('create_uid'),
        CurrentTimestamp().as_('create_date'),
        Literal(None).as_('write_uid'),
        Literal(None).as_('write_date'),
        Max(val.id).as_('id'),
        Max(val.purchase).as_('r_purchase'),
        Max(val.line).as_('r_line'),
        Max(val.date).as_('r_date'),
        val.type.as_('r_type'),
        Max(val.reference).as_('r_reference'),
        val.counterparty.as_('r_counterparty'),
        Max(val.product).as_('r_product'),
        val.state.as_('r_state'),
        Avg(val.price).as_('r_price'),
        Max(val.currency).as_('r_currency'),
        Sum(val.quantity).as_('r_quantity'),
        Max(val.unit).as_('r_unit'),
        Sum(val.amount).as_('r_amount'),
        Sum(val.base_amount).as_('r_base_amount'),
        Sum(val.rate).as_('r_rate'),
        Sum(val.mtm).as_('r_mtm'),
        Max(val.strategy).as_('r_strategy'),
        Max(val.lot).as_('r_lot'),
        where=wh,
        group_by=[val.type,val.counterparty,val.state])
            
        return query    

class ValuationReport(ValuationBase, ModelView):
    "Valuation Report"
    __name__ = 'valuation.report'

    @classmethod
    def table_query(cls):
        Valuation = Pool().get('valuation.valuation')
        val = Valuation.__table__()
        context = Transaction().context
        valuation_date = context.get('valuation_date')
        strategy = context.get('strategy')
        wh = (val.date == valuation_date)
        if strategy:
            wh &= (val.strategy == strategy)

        query = val.select(
        Literal(0).as_('create_uid'),
        CurrentTimestamp().as_('create_date'),
        Literal(None).as_('write_uid'),
        Literal(None).as_('write_date'),
        val.id.as_('id'),
        val.purchase.as_('purchase'),
        val.sale.as_('sale'),
        val.sale_line.as_('sale_line'),
        val.line.as_('line'),
        val.date.as_('date'),
        val.type.as_('type'),
        val.reference.as_('reference'),
        val.counterparty.as_('counterparty'),
        val.product.as_('product'),
        val.state.as_('state'),
        val.price.as_('price'),
        val.currency.as_('currency'),
        val.quantity.as_('quantity'),
        val.unit.as_('unit'),
        val.amount.as_('amount'),
        val.base_amount.as_('base_amount'),
        val.rate.as_('rate'),
        val.mtm.as_('mtm'),
        val.strategy.as_('strategy'),
        val.lot.as_('lot'),
        where=wh)
            
        return query
    
class ValuationReportContext(ModelView):
    "Valuation Report Context"
    __name__ = 'valuation.report.context'

    valuation_date = fields.Date("Valuation date")
    supplier = fields.Many2One('party.party',"Supplier")
    client = fields.Many2One('party.party',"Client")
    product = fields.Many2One('product.product',"Product")
    purchase =  fields.Many2One('purchase.purchase', "Purchase")
    sale = fields.Many2One('sale.sale',"Sale")
    state = fields.Selection([
        ('all', 'All'),
        ('open', 'Open'),
        ('fixed', 'Fixed'),
        ('hedged', 'Hedged')
    ], 'State')
    strategy = fields.Many2One('mtm.strategy')
    
    @classmethod
    def default_valuation_date(cls):
        pool = Pool()
        Date = pool.get('ir.date')
        return Date.today()

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