# events.py
#
# Copyright 2020-2025 Fabio Comuni, et al.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
from gettext import gettext as _
from typing import Any, Optional
from dataclasses import dataclass, field

from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import Adw

from .details import PageDetails, TAG_FMT
from .. import models
from ..config import FMT_DAY  # type:ignore # config is generated at compile time
from ..utils import clean_markup
from ..utils import InterruptibleTimeout
from ..widgets import EventActionRow, SearchBarOverlay


@dataclass
class ListBuilderCtx:
    lastobj: Optional[models.Event] = None
    listbox: Optional[Gtk.ListBox] = None
    data: list[GObject.GObject] = field(default_factory=list)

    def __repr__(self):
        return f"<ListBuilderCtx lastobj={self.lastobj}, listbox={self.listbox}>"


@Gtk.Template(resource_path="/net/kirgroup/confy/pages/events.ui")
class PageEvents(Adw.NavigationPage):
    """Talks list page (called "Events" from pentabarf xml)"""
    __gtype_name__ = "ConfyPageEvents"

    stack: Gtk.Stack = Gtk.Template.Child()  # type: ignore

    pagebox: Gtk.Box = Gtk.Template.Child()  # type: ignore

    # search_button: Gtk.ToggleButton = Gtk.Template.Child()  # type: ignore
    searchbar: SearchBarOverlay = Gtk.Template.Child()  # type: ignore
    scroll: Gtk.ScrolledWindow = Gtk.Template.Child()  # type: ignore

    filters: Optional[dict[str, Any]] = None
    _group_by: Optional[str] = None
    _order_by: str = "start, room"

    _update_handler_id: Optional[int] = None

    timeout = InterruptibleTimeout()

    _subtitle = ""
    _model = ""

    @GObject.Property(type=str, default="")
    def subtitle(self):
        return self._subtitle

    @subtitle.setter
    def subtitle_setter(self, v):
        self._subtitle = v

    @GObject.Property(type=str, default="")
    def model(self):
        return self._model

    @model.setter
    def model_setter(self, v):
        if v not in ["days", "tracks", "rooms", "starred", "search"]:
            raise ValueError(f"Model {v} is not valid for PageList.")
        self._model = v

    @GObject.Property(type=str, default=None)
    def group_by(self):
        return self._group_by

    @group_by.setter
    def group_by_setter(self, value: Optional[str]):
        if value == "NONE":  # I don't want to fight with Variant
            value = None
        self._group_py = value
        match value:
            case "track":
                self.order_by = "track, start, room"
            case "date":
                self.order_by = "start, room"
            case "room":
                self.order_by = "room, start"
            case _:
                self.order_by = "start, room"
        self._group_by_action.set_state(GLib.Variant.new_string(value if value else "NONE"))

    def set_model(self, v):
        self.props.model = v

    def set_group_by(self, value: Optional[str] = None):
        self.props.group_by = value  # type: ignore

    def __init__(self, value, **kwargs):
        super().__init__(**kwargs)
        self._setup_actions()
        self.set_value(value)

    def _setup_actions(self):
        group = Gio.SimpleActionGroup.new()
        # group by
        parameter_type = GLib.VariantType.new("s")
        state = GLib.Variant("s", "NONE")
        self._group_by_action = action = Gio.SimpleAction.new_stateful("group-by", parameter_type, state)
        action.connect("activate", self._on_group_by)
        group.insert(action)
        self.insert_action_group("list", group)

    def ready(self):
        pass

    def _on_group_by(self, action: Gio.SimpleAction, param: GLib.Variant):
        self.set_group_by(param.get_string())
        action.set_state(param)
        self.update()
        with models.Meta() as m:
            m.set(f"{self._model}_group_by", param.get_string())

    def set_value(self, obj):
        # print(f"PageEvents: set_value(obj={obj}) model={self._model}")
        if self._update_handler_id:
            models.dbUpdateWatcher.disconnect(self._update_handler_id)
        self._update_handler_id = None

        self.scroll.get_vadjustment().set_value(0)
        # self.search_button.props.visible = True
        # self.search_button.props.active = False
        self.searchbar.props.search_mode_enabled = False
        self.searchbar.props.show_close_button = True

        if self._model == "days":
            self.props.title = _("Talks on {}").format(obj.date.strftime(FMT_DAY))
            self.props.subtitle = ""
            self.filters = {'day': obj}
            self.set_group_by(None)
            self.order_by = "start, room"

        elif self._model == "tracks":
            subtitle = []
            if len(obj.room) == 1 and obj.room[0].name != "":
                subtitle.append(obj.room[0].name)
            if len(obj.date) == 1:
                subtitle.append(obj.date[0].date.strftime(FMT_DAY))

            self.props.title = _("{} Track").format(obj)
            self.props.subtitle = ", ".join(subtitle)
            self.filters = {'track': obj}
            self.set_group_by("date")

        elif self._model == "rooms":
            if obj.name is None:
                title = _("No Room")
            else:
                title = _("Room {}").format(clean_markup(obj.name))

            self.props.title = title
            self.props.subtitle = ""
            self.filters = {'room': obj}
            self.set_group_by("date")

        elif self._model == "starred":
            self.props.title = _("Starred")
            self.props.subtitle = ""
            self.filters = {'starred': True}
            self.set_group_by("date")  # TODO: or None?
            self._update_handler_id = models.dbUpdateWatcher.connect("update", self.update)

        elif self.model == "search":
            self.props.title = _("Search Talks")
            self.props.subtitle = ""
            # self.search_button.props.active = True
            # self.search_button.props.visible = False
            self.searchbar.props.search_mode_enabled = True
            self.searchbar.props.show_close_button = False
            self.connect("shown", lambda *_: self.searchbar.grab_focus())
            self.filters = None
            self.set_group_by(None)

        else:
            print(f"PageEvents: set_value: no valid model set '{self._model}'")
            return

        _meta = models.Meta()
        _key = f"{self._model}_group_by"
        if _key in _meta:
            self.set_group_by(_meta.get(_key))

        self.update()

    def update(self, *args):
        count, data = self.get_objects()

        self.timeout.stop()
        # empty box
        w = self.pagebox.get_last_child()
        while w:
            self.pagebox.remove(w)
            w = self.pagebox.get_last_child()

        if count == 0:
            self.show_placeholder()
        else:
            self.stack.set_visible_child_name('list')

        ctx = ListBuilderCtx()
        ctx.data = data
        self.timeout = InterruptibleTimeout(500, self.build_list, ctx)

    def build_list(self, timeout, ctx):
        for idx, obj in enumerate(ctx.data):
            if timeout.must_stop():
                return False

            header = self.build_header(obj, ctx.lastobj)
            if header:
                ctx.listbox = None
                self.pagebox.append(header)

            row = self.build_row(obj)
            if ctx.listbox is None:
                ctx.listbox = self.build_listbox()
                self.pagebox.append(ctx.listbox)
            ctx.listbox.append(row)
            ctx.lastobj = obj
            if idx > 10:
                return True

        return False

    def build_listbox(self):
        lb = Gtk.ListBox(margin_bottom=20)
        lb.add_css_class("boxed-list")
        lb.connect("row-activated", self.on_litstbox_activated)
        return lb

    def build_header(self, current, before):
        """
        `current` is current data to be added, `before` is data
        added before.
        return a widget for a new header, or None.
        """
        _title = None
        match self._group_py:
            case "date":
                if before is None or current.date.strftime(FMT_DAY) != before.date.strftime(FMT_DAY):
                    _title = current.date.strftime(FMT_DAY)
            case "track":
                if before is None or current.track != before.track:
                    _title = current.track
                if _title == "":
                    _title = "<i>no track</i>"
            case "room":
                if before is None or current.room != before.room:
                    _title = current.room

        if _title:
            label = Gtk.Label(label=_title, halign=Gtk.Align.START, use_markup=True)
            label.add_css_class("heading")
            return label

        return None

    @Gtk.Template.Callback()
    def on_searchbar_entry_changed(self, e):
        self.update()

    @Gtk.Template.Callback()
    def on_search_mode_changed(self, *_):
        self.update()

    def get_objects(self):
        if self.model == "search":
            term = self.searchbar.entry.get_text()
            is_starred = self.searchbar.is_starred.get_active()
            _filters = {}
            if is_starred:
                _filters['starred'] = True

            g = models.Event.search(term, **_filters)
            return next(g), g
        if self.filters is None:
            return 0, []
        if self.searchbar.is_searching():
            term = self.searchbar.entry.get_text()
            is_starred = self.searchbar.is_starred.get_active()
            _filters = self.filters.copy()
            if is_starred:
                _filters['starred'] = True
            g = models.Event.search(term, order_by=self._group_py, **_filters)
            return next(g), g
        g = models.Event.filter(**self.filters, order_by=self.order_by)
        return next(g), g

    def build_row(self, obj):
        row = EventActionRow(obj)
        return row

    def show_placeholder(self):
        if self._model == "search" or self.searchbar.is_searching():
            self.stack.set_visible_child_name('no_match')
        elif self._model == "starred":
            self.stack.set_visible_child_name('empty_star')
        else:
            self.stack.set_visible_child_name('list')

    def on_litstbox_activated(self, listbox, row):
        tagfmt = "search-{0.id}" if self._model == "search" else TAG_FMT
        page = PageDetails(obj=row.obj, tagfmt=tagfmt)
        self.get_parent().push(page)
