# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from functools import wraps
from itertools import groupby

from sql import Null
from sql.conditionals import Case
from sql.functions import CharLength

from trytond.i18n import gettext
from trytond.model import (
    ChatMixin, Index, ModelSQL, ModelView, Workflow, fields)
from trytond.modules.company import CompanyReport
from trytond.modules.currency.fields import Monetary
from trytond.modules.product import price_digits
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
from trytond.tools import sortable_values
from trytond.transaction import Transaction
from trytond.wizard import (
    Button, StateAction, StateTransition, StateView, Wizard)

from .exceptions import PreviousQuotation


def process_request(func):
    @wraps(func)
    def wrapper(cls, quotations):
        pool = Pool()
        Request = pool.get('purchase.request')
        result = func(cls, quotations)
        requests = [l.request for q in quotations for l in q.lines]
        Request.update_state(requests)
        return result
    return wrapper


class Configuration(metaclass=PoolMeta):
    __name__ = 'purchase.configuration'
    purchase_request_quotation_sequence = fields.MultiValue(fields.Many2One(
            'ir.sequence', 'Purchase Request Quotation Sequence',
            required=True,
            domain=[
                ('company', 'in',
                    [Eval('context', {}).get('company', -1), None]),
                ('sequence_type', '=',
                    Id('purchase_request_quotation',
                    'sequence_type_purchase_request_quotation')),
                ]))

    @classmethod
    def multivalue_model(cls, field):
        pool = Pool()
        if field == 'purchase_request_quotation_sequence':
            return pool.get('purchase.configuration.sequence')
        return super().multivalue_model(field)

    @classmethod
    def default_purchase_request_quotation_sequence(cls, **pattern):
        return cls.multivalue_model('purchase_request_quotation_sequence'
            ).default_purchase_request_quotation_sequence()


class ConfigurationSequence(metaclass=PoolMeta):
    __name__ = 'purchase.configuration.sequence'
    purchase_request_quotation_sequence = fields.Many2One(
        'ir.sequence', 'Purchase Request Quotation Sequence',
        required=True,
        domain=[
            ('company', 'in',
                [Eval('context', {}).get('company', -1), None]),
            ('sequence_type', '=',
                Id('purchase_request_quotation',
                    'sequence_type_purchase_request_quotation')),
            ])

    @classmethod
    def default_purchase_request_quotation_sequence(cls):
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        try:
            return ModelData.get_id(
                'purchase_request_quotation',
                'sequence_purchase_request_quotation')
        except KeyError:
            return None


class Quotation(Workflow, ModelSQL, ModelView, ChatMixin):
    __name__ = 'purchase.request.quotation'
    _rec_name = 'number'

    number = fields.Char('Number', readonly=True,
        states={
            'required': ~Eval('state').in_(['draft', 'cancelled'])
            },
        help="The unique identifier of the quotation.")
    revision = fields.Integer('Revision', readonly=True,
        help="Number incremented each time the quotation is sent.")
    reference = fields.Char(
        "Reference",
        help="The reference used by the supplier.")
    company = fields.Many2One(
        'company.company', "Company", required=True,
        states={
            'readonly': Eval('state') != 'draft',
            })
    warehouse = fields.Many2One('stock.location', 'Warehouse',
        domain=[('type', '=', 'warehouse')])
    supplier = fields.Many2One(
        'party.party', "Supplier", required=True,
        states={
            'readonly': Eval('lines', [0]) & Eval('supplier'),
            },
        context={
            'company': Eval('company', -1),
            },
        depends={'company'})
    supplier_address = fields.Many2One('party.address', 'Supplier Address',
        domain=[
            ('party', '=', Eval('supplier', -1)),
            ])
    lines = fields.One2Many('purchase.request.quotation.line', 'quotation',
        'Lines', states={
            'readonly': Eval('state') != 'draft',
            })
    state = fields.Selection([
        ('draft', 'Draft'),
        ('sent', 'Sent'),
        ('received', 'Received'),
        ('rejected', 'Rejected'),
        ('cancelled', 'Cancelled'),
        ], "State", readonly=True, required=True, sort=False)

    @classmethod
    def __setup__(cls):
        cls.number.search_unaccented = False
        cls.reference.search_unaccented = False
        super().__setup__()
        t = cls.__table__()
        cls._sql_indexes.update({
                Index(t, (t.reference, Index.Similarity())),
                Index(
                    t, (t.state, Index.Equality(cardinality='low')),
                    where=t.state.in_(['draft', 'sent'])),
                })
        cls._transitions |= set((
                ('draft', 'cancelled'),
                ('cancelled', 'draft'),
                ('draft', 'sent'),
                ('sent', 'rejected'),
                ('sent', 'received'),
                ('sent', 'draft'),
                ('received', 'rejected'),
                ('rejected', 'received'),
                ))
        cls._buttons.update({
                'cancel': {
                    'invisible': Eval('state') != 'draft',
                    },
                'draft': {
                    'invisible': ~Eval('state').in_(['cancelled', 'sent']),
                    'icon': If(Eval('state') == 'cancelled',
                        'tryton-undo',
                        'tryton-back'),
                    },
                'send': {
                    'invisible': ((Eval('state') != 'draft')
                        | ~Eval('lines', [])),
                    'readonly': ~Eval('lines', []),
                    },
                'receive': {
                    'invisible': ~Eval('state').in_(['sent', 'rejected']),
                    },
                'reject': {
                    'invisible': ~Eval('state').in_(['sent', 'received']),
                    },
                })

    @classmethod
    def order_number(cls, tables):
        table, _ = tables[None]
        return [
            ~((table.state == 'cancelled') & (table.number == Null)),
            CharLength(table.number), table.number]

    @classmethod
    def default_company(cls):
        return Transaction().context.get('company')

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

    @classmethod
    def default_revision(cls):
        return 1

    @classmethod
    def default_warehouse(cls):
        Location = Pool().get('stock.location')
        return Location.get_default_warehouse()

    @classmethod
    def set_number(cls, quotations):
        pool = Pool()
        Config = pool.get('purchase.configuration')

        config = Config(1)
        for company, c_quotations in groupby(
                quotations, key=lambda q: q.company):
            missing_number = []
            for quotation in c_quotations:
                if quotation.number:
                    quotation.revision += 1
                else:
                    missing_number.append(quotation)
            if missing_number:
                sequence = config.get_multivalue(
                    'purchase_request_quotation_sequence', company=company.id)
                for quotation, number in zip(
                        missing_number,
                        sequence.get_many(len(missing_number))):
                    quotation.number = number
        cls.save(quotations)

    @fields.depends('supplier')
    def on_change_supplier(self):
        self.supplier_address = None
        if self.supplier:
            self.supplier_address = self.supplier.address_get()

    def chat_language(self, audience='internal'):
        language = super().chat_language(audience=audience)
        if audience == 'public':
            language = self.supplier.lang.code if self.supplier.lang else None
        return language

    @classmethod
    def copy(cls, groups, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('number', None)
        default.setdefault('reference')
        default.setdefault('revision', cls.default_revision())
        return super().copy(groups, default=default)

    @property
    def delivery_full_address(self):
        if self.warehouse and self.warehouse.address:
            return self.warehouse.address.full_address
        return ''

    @classmethod
    @ModelView.button
    @Workflow.transition('cancelled')
    def cancel(cls, quotations):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, quotations):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('sent')
    def send(cls, quotations):
        cls.set_number(quotations)

    @classmethod
    @ModelView.button
    @process_request
    @Workflow.transition('received')
    def receive(cls, quotations):
        pass

    @classmethod
    @ModelView.button
    @process_request
    @Workflow.transition('rejected')
    def reject(cls, quotations):
        pass


class QuotationLine(ModelSQL, ModelView):
    __name__ = 'purchase.request.quotation.line'

    supplier = fields.Function(fields.Many2One('party.party', 'Supplier'),
        'get_supplier')
    supply_date = fields.Date('Supply Date',
        help="When it should be delivered.")
    product = fields.Function(fields.Many2One('product.product', 'Product'),
        'get_product', searcher='search_product')
    description = fields.Text('Description',
        states={
            'required': ~Eval('product')
            })
    quantity = fields.Float("Quantity", digits='unit', required=True)
    unit = fields.Many2One(
        'product.uom', 'Unit', ondelete='RESTRICT',
        states={
            'required': Bool(Eval('product')),
            },
        domain=[
            If(Bool(Eval('product_uom_category')),
                ('category', '=', Eval('product_uom_category')),
                ('category', '!=', -1)),
            ])
    product_uom_category = fields.Function(
        fields.Many2One(
            'product.uom.category', "Product UoM Category",
            help="The category of Unit of Measure for the product."),
        'on_change_with_product_uom_category')
    unit_price = Monetary(
        "Unit Price", currency='currency', digits=price_digits)
    currency = fields.Many2One('currency.currency', 'Currency',
        states={
            'required': Bool(Eval('unit_price')),
            })
    request = fields.Many2One(
        'purchase.request', "Request", ondelete='CASCADE', required=True,
        domain=[
            If(Eval('quotation_state') == 'draft',
                ('state', 'in', ['draft', 'quotation', 'received']), (), ),
        ],
        states={
            'readonly': Eval('quotation_state') != 'draft'
            },
        help="The request which this line belongs to.")
    quotation = fields.Many2One('purchase.request.quotation', 'Quotation',
        ondelete='CASCADE', required=True,
        domain=[
            ('supplier', '=', Eval('supplier', -1)),
            ])
    quotation_state = fields.Function(fields.Selection(
            'get_quotation_state', 'Quotation State'),
        'on_change_with_quotation_state', searcher='search_quotation_state')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('quotation')

    @staticmethod
    def order_quotation_state(tables):
        pool = Pool()
        Quotation = pool.get('purchase.request.quotation')
        table, _ = tables[None]
        if 'quotation' not in tables:
            quotation = Quotation.__table__()
            tables['quotation'] = {
                None: (quotation, table.quotation == quotation.id),
                }
        else:
            quotation, _ = tables['quotation'][None]
        return [Case((quotation.state == 'received', 0), else_=1),
            quotation.state]

    def get_supplier(self, name):
        if self.quotation and self.quotation.supplier:
            return self.quotation.supplier.id

    @fields.depends('request',
        '_parent_request.product', '_parent_request.description',
        '_parent_request.quantity', '_parent_request.unit',
        '_parent_request.company', '_parent_request.supply_date')
    def on_change_request(self):
        if self.request:
            self.product = self.request.product
            self.description = self.request.description
            self.quantity = self.request.quantity
            self.unit = self.request.unit
            if self.request.company:
                self.currency = self.request.company.currency
            self.supply_date = self.request.supply_date or datetime.date.max

    @fields.depends('product')
    def on_change_with_product_uom_category(self, name=None):
        return self.product.default_uom_category if self.product else None

    @classmethod
    def get_quotation_state(cls):
        pool = Pool()
        Quotation = pool.get('purchase.request.quotation')
        return (Quotation.fields_get(
            ['state'])['state']['selection'])

    @fields.depends('quotation', '_parent_quotation.state')
    def on_change_with_quotation_state(self, name=None):
        pool = Pool()
        Quotation = pool.get('purchase.request.quotation')
        if self.quotation:
            return self.quotation.state
        return Quotation.default_state()

    @classmethod
    def search_quotation_state(cls, name, clause):
        return [('quotation.state',) + tuple(clause[1:])]

    def get_rec_name(self, name):
        return '%s - %s' % (self.quotation.rec_name, self.supplier.rec_name)

    @classmethod
    def search_rec_name(cls, name, clause):
        domain = []
        _, operator, value = clause
        if value is not None:
            names = clause[2].split(' - ', 1)
            domain.append(('quotation', operator, names[0]))
            if len(names) != 1 and names[1]:
                domain.append(('supplier', operator, names[1]))
            if operator.startswith('!') or operator.startswith('not'):
                domain.insert(0, 'OR')
        elif not operator.startswith('!') and not operator.startswith('not'):
            domain.append(('id', '<', 0))
        return domain

    @classmethod
    def on_modification(cls, mode, records, field_names=None):
        pool = Pool()
        Request = pool.get('purchase.request')
        super().on_modification(mode, records, field_names=field_names)
        if mode in {'create', 'write'}:
            if field_names is None or 'request' in field_names:
                requests = {r.request for r in records if r.request}
                if requests:
                    Request.update_state(requests)

    @classmethod
    def on_write(cls, lines, values):
        pool = Pool()
        Request = pool.get('purchase.request')

        callback = super().on_write(lines, values)

        if 'request' in values:
            if requests := {l.request for l in lines if l.request}:
                callback.append(lambda: Request.update_state(requests))
        return callback

    @classmethod
    def on_delete(cls, lines):
        pool = Pool()
        Request = pool.get('purchase.request')
        callback = super().on_delete(lines)
        requests = {l.request for l in lines}
        if requests:
            requests = Request.browse(requests)
            callback.append(lambda: Request.update_state(requests))
        return callback

    def get_product(self, name):
        if self.request and self.request.product:
            return self.request.product.id

    @classmethod
    def search_product(cls, name, clause):
        return [('request.' + clause[0],) + tuple(clause[1:])]


class PurchaseRequestQuotationReport(CompanyReport):
    __name__ = 'purchase.request.quotation'

    @classmethod
    def execute(cls, ids, data):
        with Transaction().set_context(address_with_party=True):
            return super(
                PurchaseRequestQuotationReport, cls).execute(ids, data)

    @classmethod
    def get_context(cls, records, header, data):
        pool = Pool()
        Date = pool.get('ir.date')
        context = super().get_context(records, header, data)
        company = header.get('company')
        with Transaction().set_context(
                company=company.id if company else None):
            context['today'] = Date.today()
        return context


class CreatePurchaseRequestQuotationAskSuppliers(ModelView):
    __name__ = 'purchase.request.quotation.create.ask_suppliers'

    suppliers = fields.Many2Many('party.party', None, None, 'Suppliers',
                    required=True)


class CreatePurchaseRequestQuotationSucceed(ModelView):
    __name__ = 'purchase.request.quotation.create.succeed'

    number_quotations = fields.Integer('Number of Created Quotations',
        readonly=True)


class CreatePurchaseRequestQuotation(Wizard):
    __name__ = 'purchase.request.quotation.create'

    start = StateTransition()
    ask_suppliers = StateView(
        'purchase.request.quotation.create.ask_suppliers',
        'purchase_request_quotation.'
        'purchase_request_quotation_create_ask_suppliers', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Process', 'create_quotations', 'tryton-ok', default=True),
            ])
    create_quotations = StateAction(
        'purchase_request_quotation.act_purchase_request_quotation_form')

    def transition_start(self):
        pool = Pool()
        Warning = pool.get('res.user.warning')

        reqs = [
            r for r in self.records
            if r.state in {'draft', 'quotation', 'received'}]
        if reqs:
            reqs = [r for r in reqs if r.state in {'quotation', 'received'}]
            if reqs:
                warning_key = Warning.format(
                    'purchase_request_quotation_create', reqs)
                if Warning.check(warning_key):
                    names = ', '.join(r.rec_name for r in reqs[:5])
                    if len(reqs) > 5:
                        names += '...'
                    raise PreviousQuotation(
                        warning_key,
                        gettext('purchase_request_quotation'
                            '.msg_previous_quotation',
                            requests=names))
            return 'ask_suppliers'
        return 'end'

    def default_ask_suppliers(self, fields):
        reqs = [
            r for r in self.records
            if r.party and r.state in {'draft', 'quotation', 'received'}]
        return {
            'suppliers': [r.party.id for r in reqs],
            }

    def default_succeed(self, fields):
        return {
            'number_quotations': self.succeed.number_quotations,
            }

    def filter_request(self, request, supplier):
        return request

    def _group_request_key(self, request):
        return (('company', request.company),)

    def do_create_quotations(self, action):
        pool = Pool()
        Quotation = pool.get('purchase.request.quotation')
        QuotationLine = pool.get('purchase.request.quotation.line')
        quotations = []
        lines = []

        requests = [
            r for r in self.records
            if r.state in {'draft', 'quotation', 'received'}]
        for supplier in self.ask_suppliers.suppliers:
            sub_requests = [
                r for r in requests if self.filter_request(r, supplier)]
            sub_requests = sorted(
                sub_requests, key=sortable_values(self._group_request_key))
            for key, grouped_requests in groupby(
                    sub_requests, key=self._group_request_key):
                quotation = self.get_quotation(supplier, key)
                for request in grouped_requests:
                    line = self.get_quotation_line(request, quotation)
                    line.quotation = quotation
                    lines.append(line)
                quotations.append(quotation)
        Quotation.save(quotations)
        QuotationLine.save(lines)

        self.model.update_state(requests)
        action['domain'] = []
        if len(quotations) == 1:
            action['views'].reverse()
        return action, {
            'res_id': list(map(int, quotations))
            }

    def get_quotation(self, supplier, key):
        pool = Pool()
        Quotation = pool.get('purchase.request.quotation')
        quotation = Quotation()
        quotation.supplier = supplier
        quotation.supplier_address = supplier.address_get()
        for f, v in key:
            setattr(quotation, f, v)
        return quotation

    def get_quotation_line(self, request, quotation):
        pool = Pool()
        QuotationLine = pool.get('purchase.request.quotation.line')
        quotation_line = QuotationLine()
        quotation_line.request = request
        quotation_line.description = request.description
        quotation_line.quantity = request.quantity
        quotation_line.unit = request.unit
        quotation_line.currency = request.currency
        quotation_line.supply_date = request.supply_date or datetime.date.max
        return quotation_line


class PurchaseRequest(metaclass=PoolMeta):
    __name__ = 'purchase.request'

    quotation_lines = fields.One2Many(
        'purchase.request.quotation.line', 'request', 'Quotation Lines',
        )
    quotation_lines_active = fields.One2Many(
        'purchase.request.quotation.line', 'request', 'Active Quotation Lines',
        filter=[('quotation.state', 'in', ['draft', 'sent', 'received'])],
        order=[('quotation_state', 'ASC'), ('unit_price', 'ASC')])
    best_quotation_line = fields.Function(fields.Many2One(
        'purchase.request.quotation.line', 'Best Quotation Line'),
        'get_best_quotation')
    preferred_quotation_line = fields.Many2One(
        'purchase.request.quotation.line', 'Preferred Quotation Line',
        domain=[
            ('quotation_state', '=', 'received'),
            ('request', '=', Eval('id', -1))
        ],
        help="The quotation that will be chosen to create the purchase\n"
        "otherwise first ordered received quotation line will be selected.")

    @property
    def currency(self):
        currency = super().currency
        if self.best_quotation_line:
            return self.best_quotation_line.currency
        return currency

    def get_best_quotation(self, name):
        if self.preferred_quotation_line:
            return self.preferred_quotation_line
        else:
            for line in self.quotation_lines_active:
                if line.quotation_state == 'received':
                    return line
            return None

    @classmethod
    def __setup__(cls):
        super().__setup__()
        selection = [('quotation', 'Quotation'), ('received', 'Received')]
        for s in selection:
            if s not in cls.state.selection:
                cls.state.selection.append(s)
        cls._buttons.update({
                'create_quotation': {
                    'invisible': ~Eval('state').in_(
                        ['draft', 'quotation', 'received']),
                    'depends': ['state'],
                    },
                })

    def get_state(self):
        state = super().get_state()
        if state == 'draft' and self.quotation_lines:
            state = 'quotation'
            if any(l.quotation_state == 'received'
                    for l in self.quotation_lines):
                state = 'received'
        return state

    @classmethod
    @ModelView.button_action(
        'purchase_request_quotation.wizard_create_quotation')
    def create_quotation(cls, requests):
        pass


class CreatePurchase(Wizard):
    __name__ = 'purchase.request.create_purchase'

    init = StateTransition()

    @classmethod
    def __setup__(cls):
        super().__setup__()

    def transition_start(self):
        to_save = []
        reqs = [r for r in self.records
            if not r.purchase_line and r.quotation_lines]
        to_save = []
        for req in reqs:
            if req.best_quotation_line:
                to_save.append(self.apply_quotation(req))
        if to_save:
            self.model.save(to_save)
        state = super().transition_start()
        return state

    def apply_quotation(self, request):
        request.party = request.best_quotation_line.supplier.id
        request.description = request.best_quotation_line.description
        request.quantity = request.best_quotation_line.quantity
        if not request.preferred_quotation_line:
            request.preferred_quotation_line = request.best_quotation_line
        return request

    @classmethod
    def compute_purchase_line(cls, key, requests, purchase):
        line = super().compute_purchase_line(key,
                    requests, purchase)
        try:
            line.unit_price = min(req.best_quotation_line.unit_price
                for req in requests if req.best_quotation_line)
        except ValueError:
            pass
        return line
