#
# Copyright (c) Daniel Sheffield 2021 - 2023
#
# All rights reserved
#
# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
from itertools import chain
from typing import (
    Union,
    Tuple,
    List,
    Iterable,
)
from dateutil.parser import parse as parse_time
from dateutil.parser._parser import ParserError

from urwid import (
    connect_signal,
    AttrMap,
    Button,
    Columns,
    Divider,
    Edit,
    Filler,
    LineBox,
    Pile,
    Text,
)

from ..data.decimal_util import decimal_or_none
from ..data.dataframe_util import get_caption, get_time_range, stats
from ..data.QueryManager import QueryManager
from .grouped_widget_util import (
    to_numbered_field,
    to_unnumbered_field,
    to_named_value,
)
from ..widgets import (
    AutoCompleteEdit,
    AutoCompleteFloatEdit,
    FocusWidget,
    AutoCompletePopUp,
    NoTabCheckBox,
    FlowBarGraphWithVScale,
)
from . import ActivityManager
from .Rating import Rating
from .NewProduct import NewProduct
from .Banner import banner

def unzip(_iter: List[Tuple[AutoCompleteEdit, Edit]]) -> Tuple[
    List[AutoCompleteEdit], List[Edit]
]:
    return zip(*_iter)


def extract_values(x: Union[List[AutoCompleteEdit], List[Edit]]) -> Iterable[str]:
    if isinstance(x, (list, tuple)):
        if len(x) == 0:
            return []
        return ( v.get_edit_text() for v in x )
    raise Exception(f"Unsupported type: {type(x)}")


def blank_tags_row(idx: int) -> Tuple[AutoCompleteEdit, Edit]:
    return (
        AutoCompleteEdit(('bg',f'tags#{idx}')), Edit(('bg', '')),
    )


class TransactionEditor(FocusWidget):

    def keypress(self, size, key):
        if isinstance(key, tuple):
            return None

        if getattr(self._w.original_widget, 'original_widget', None) is None:
            return super().keypress(size, key)

        if key == 'tab':
            self.advance_focus()
        elif key == 'shift tab':
            self.advance_focus(reverse=True)
        elif key == 'ctrl delete':
            self.clear()
            self.focus_on(self.edit_fields['product'])
        elif key == 'insert':
            self.save_and_clear_callback()
        elif key == 'ctrl p':
            self.focus_on(self.edit_fields['product'])
        else:
            return super().keypress(size, key)
        return None

    def apply_choice(self, name, value):
        self.apply_changes(name, value)
        data = {
            (field, idx): v for field, idx, v in map(
                to_numbered_field, self.data.items()
            )
        }
        for k,v in data.items():
            if f'{k[0]}#{k[1]}' == name or v:
                continue

            _data = dict(filter(
                lambda x: x[0] not in ('tags','description'),
                map(lambda x: (x[0][0], x[1]), data.items())
            ))
            options = self.query_manager.unique_suggestions(k[0], **_data)

            if len(options) == 1 and k != 'ts':
                self.apply_changes(f'{k[0]}#{k[1]}', list(options)[0])

    def apply_changes(self, name, value):
        self.data = {
            name: value if name != 'organic' else {
                'yes': True, 'no': False,
                True: True, False: False,
                'mixed': '',
            }[value],
        }

    @property
    def data(self):
        zipped = zip(
            ['tags',],
            #['tags', 'descriptions'],
            map(extract_values, unzip(self._tags)),
        )
        ret = dict(chain(
            *[ map(to_named_value(n), enumerate(l)) for n,l in zipped ],
            [(k, v.get_edit_text()) for k,v in self.edit_fields.items()],
            [(k, v.state) for k,v in self.checkboxes.items()]
        ))
        return ret

    @data.setter
    def data(self, _data: dict):
        for k,v in _data.items():
            _name = to_unnumbered_field(to_numbered_field((k,None)))
            if _name[0] not in [
                'tags', 'descriptions'
            ]:
                k = _name[0]
            if k in self.edit_fields and v != self.edit_fields[k].get_edit_text():
                self.edit_fields[k].set_edit_text(v)
            elif k in self.checkboxes and v != self.checkboxes[k].state:
                self.checkboxes[k].set_state(v)
            elif len(k.split('#')) > 1:
                name, idx = k.split('#', 1)
                w = self._tags[int(idx)][ next(( pos for pos, n in zip(
                    [0, 1],
                    ['tags', 'descriptions']
                ) if n == name ))]
                w.set_edit_text(v)


    def init_tags(self):
        _tags = Pile([AttrMap(
            AutoCompletePopUp(
                tag[0],
                self.apply_choice,
                lambda: self.activity_manager.show(self.update())
            ), 'streak') for tag in self._tags])
        gutter = Pile([
            *[ Divider() for _ in self._tags[:-1] ],
            Divider(),
            Divider(),
            self.buttons['add'],
        ])
        return _tags, gutter


    def add_tag(self):
        self._tags.append(
            blank_tags_row(len(self._tags))
        )
        _tags, gutter = self.init_tags()
        self.components['tags'].contents = list(_tags.contents)
        self.components['gutter'][1].contents = list(gutter.contents)
        for widget in self._tags:
            connect_signal(widget[0], 'postchange', lambda *_: self.update())
            connect_signal(widget[0], 'apply', lambda w, name: self.autocomplete_callback(
                w, name, self.autocomplete_options(name, dict(map(
                    to_unnumbered_field,
                    map(to_numbered_field, self.data.items()
                ))))
            ))


    def clear(self):
        self._tags: List[Tuple[AutoCompleteEdit, Edit]] = []
        self.add_tag()
        for (k, ef) in self.edit_fields.items():
            if k in ('ts', 'store',):
                continue
            ef.set_edit_text('')
        for (_, cb) in self.checkboxes.items():
            cb.set_state('mixed')
        for (k, tf) in self.text_fields.items():
            if k != 'dbview':
                tf.set_text('')
        self.graph.set_data([],0)
        return self.update()

    def update(self):
        data = self.data
        date, store = data['ts'], data['store']
        try:
            parse_time(date)
        except ParserError:
            date = None
        self.text_fields['dbview'].set_text(
            self.query_manager.get_session_transactions(date, store) if None not in (
                date or None, store or None
            ) else ''
        )
        self.update_historic_prices(data)
        return self

    def update_graph(self, df):
        # after truncating, need to recalculate avg(median), min, max
        df = df.sort_values(
            'ts_raw', ascending=True, ignore_index=True
        ).truncate(
            before=max(0, len(df.index)-self.graph._canvas_width)
        )
        data = df[['price', '$/unit','quantity']].apply(
            lambda x: (float(x['price']), float(x['$/unit']), float(x['quantity'])),
            axis=1, result_type='broadcast'
        )
        norm = [ (x,) for x in data['$/unit'] ] #.to_records(index=False)
        scale = stats(data, 'price', 'quantity', '$/unit')
        self.graph.set_data(norm, scale[-1], vscale=scale)
        time_range = get_time_range(df, 'ts_raw')
        caption = get_caption(time_range, self.graph.canvas_width)
        self.graph.set_caption(caption)

    def update_historic_prices(self, data):
        organic = None if data['organic'] == 'mixed' else data['organic']
        #sort = '$/unit' if self.buttons['sort_price'].state else 'ts'
        product, unit = data['product'] or None, data['unit'] or None
        price = decimal_or_none(data['price'])
        quantity = decimal_or_none(data['quantity'])

        if None in (product, unit):
            self.rating.update_rating(None, None, None, unit)
            return

        df = self.query_manager.get_historic_prices_data(unit, product=product, organic=organic, sort='ts').dropna()
        if df.empty:
            self.rating.update_rating(None, None, None, unit)
            return

        assert len(df['avg'].unique()) == 1, f"There should be only one average price: {df['avg'].unique()}"

        # all time (or all data) avg(mean), min, max
        _avg, _min, _max = [
            float(x) for x in df[['avg','min','max']].iloc[0]
        ]
        self.rating.update_rating(_avg, _min, _max, unit, price=price, quantity=quantity)
        self.update_graph(df)

    def autocomplete_callback(self, widget, name, options):
        if len(options):
            widget._emit('open', options)
            return

        new_product_activity = self.activity_manager.get('new_product')
        current_activity = self.activity_manager.current()
        if current_activity is not new_product_activity and name in (
            'product', 'category', 'group'
        ):
            self.new_product_callback()
        return

    def new_product_callback(self):
        new_product = self.activity_manager.create(
            NewProduct, 'new_product',
            self.activity_manager, self.query_manager,
            self.activity_manager.current()
        )
        self.activity_manager.show(new_product)

    def save_and_clear_callback(self):
        txn = self.activity_manager.get('transaction')
        self.activity_manager.app.save(txn.data)
        txn.clear()
        txn.focus_on(txn.edit_fields['product'])
        self.activity_manager.show(txn.update())

    def __init__(self,
        activity_manager: ActivityManager,
        query_manager: QueryManager,
    ):
        self.autocomplete_options = lambda name, data: self.query_manager.unique_suggestions(name.split('#', 1)[0], **data)
        self.activity_manager: ActivityManager = activity_manager
        self.query_manager = query_manager
        self.buttons = {
            'done': Button(('streak', u'Done')),
            'clear': Button(('streak', u'Clear')),
            'add': Button(('streak', 'Add')),
        }
        self._tags = []
        _tags, gutter = self.init_tags()
        self.components = {
            'tags': _tags,
            'gutter': (8,gutter),
        }
        self.edit_fields = {
            'ts': AutoCompleteEdit(('bg', 'ts')),
            'store': AutoCompleteEdit(('bg', 'store')),
            'product': AutoCompleteEdit(('bg', 'product')),
            'category': AutoCompleteEdit(('bg', 'category')),
            'group': AutoCompleteEdit(('bg', 'group')),
            'description': AutoCompleteEdit(('bg', 'description')),
            'unit': AutoCompleteEdit(('bg', 'unit')),
            'quantity': AutoCompleteFloatEdit(('bg', 'quantity')),
            'price': AutoCompleteFloatEdit(('bg', 'price')),
        }
        self.checkboxes = {
            'organic': NoTabCheckBox(('bg', "Organic"), state='mixed', has_mixed=True),
        }
        self.text_fields = {
            'dbview': Text(''),
            'rating': Text(''),
            'spread': Text(''),
            'marker': Text(''),
        }
        self.graph = FlowBarGraphWithVScale(
            50, 14,
            ['bg','popup_focus', 'badge_neutral' ],
            hatt=['dark red', 'dark red', 'dark red']
        )
        self.rating = Rating(dict(filter(
            lambda x: x[0] in ('spread','rating','marker'),
            self.text_fields.items()
        )))
        self.organic_checkbox = self.checkboxes['organic']
        connect_signal(self.organic_checkbox, 'postchange', lambda _,v: self.update())

        _widgets = dict(chain(*list(map(lambda x: x.items(), [
                self.edit_fields, self.text_fields, self.checkboxes
            ])
        )))
        _widgets.update({
            'dbview': LineBox(
                AttrMap(self.text_fields['dbview'], 'streak'),
                title="Session Data",
                title_align='left',
            )
        })
        for (k, ef) in self.edit_fields.items():
            connect_signal(ef, 'postchange', lambda *_: self.update())
            connect_signal(ef, 'apply', lambda w, name: self.autocomplete_callback(
                w, name, self.autocomplete_options(name, dict(map(
                    to_unnumbered_field,
                        map(to_numbered_field, self.data.items()
                ))))
            ))

        _widgets.update({
            k: LineBox(
                AttrMap(AutoCompletePopUp(
                    self.edit_fields[k],
                    self.apply_choice,
                    lambda: activity_manager.show(self.update())
                ), 'streak'),
                title=k.title(), title_align='left'
            ) for k in self.edit_fields if k != 'product'
        })

        self.components.update({
            'bottom_button_bar': Columns(
                [(8, self.buttons['done']), Divider(), (9, self.buttons['clear'])]
            ),
            'badge': Pile([
                _widgets[x] for x in ['rating', 'spread', 'marker']
            ]),
        })
        self.components.update({
            'bottom_pane': Columns([
                Pile([
                    _widgets[x] for x in ['description', 'dbview']
                ]),
                (self.graph.total_width+2, Pile([
                    LineBox(
                        AttrMap(self.components['badge'], 'badge'),
                        title="Current Price", title_align='left',
                    ),
                    LineBox(
                        self.graph,
                        title="Historic Price", title_align='left'
                    ),
                ])),
            ])
        })
        connect_signal(self.buttons['done'], 'click', lambda _: self.save_and_clear_callback())
        connect_signal(self.buttons['clear'], 'click', lambda _: self.clear())
        connect_signal(self.buttons['add'], 'click', lambda _: self.add_tag())

        _widgets.update({
            'product': LineBox(Columns([
                AttrMap(AutoCompletePopUp(
                    self.edit_fields['product'],
                    self.apply_choice,
                    lambda: activity_manager.show(self.update())
                ), 'streak'),
                self.organic_checkbox
            ], dividechars=2), title='Product', title_align='left')
        })

        self.components['side_pane'] = (12, Pile([
            _widgets[r] for r in ['unit', 'quantity', 'price']
        ]))

        self.components['main_pane'] = Pile([
            Columns([
                _widgets[c] for c in ['ts', 'store']
            ]),
            _widgets['product'],
            Columns([
                _widgets[c] for c in ['category', 'group']
            ])
        ])

        self.add_tag()

        widget = Pile([
            banner(u'Fill Transaction'),
            Divider(),
            Columns([
                self.components['main_pane'],
                self.components['side_pane'],
                (18,LineBox(
                    self.components['tags'],
                    title='Tags',
                    title_align='left'
                )),
                self.components['gutter'],
            ], dividechars=2),
            self.components['bottom_pane'],
            Divider(),
            self.components['bottom_button_bar']
        ])
        widget = Filler(widget, 'top')
        widget = AttrMap(widget, 'bg')
        super().__init__(widget, [
                'product', 'organic',
                'unit', 'quantity', 'price',
                'description', 'tags', 'add',
                'done', 'clear',
                'category', 'group',
                'ts', 'store'
            ], lambda: chain(
                (('tags', w[0]) for w in self._tags),
                self.buttons.items(),
                self.edit_fields.items(),
                self.checkboxes.items()
            )
        )
        self.update()
