# models.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 typing import Optional, Any, Union
import datetime

from gi.repository import GObject, Gtk
from dateutil.tz import UTC

from . import local
from .settings import Settings


class ObjectNotFound(Exception):
    pass


DayOrDateOrStr = Optional[Union["Day", str, datetime.date]]
RoomOrName = Optional[Union["Room", str]]
TrackOrName = Optional[Union["Track", str]]
EventOrId = Optional[Union["Track", str]]


# models
def _build_filter(
        table: str = "",
        *,
        day: DayOrDateOrStr = None,
        room: RoomOrName = None,
        track: TrackOrName = None,
        event: EventOrId = None,
        **kwargs):
    filterdata = []
    wherestm = []

    if table != "":
        table = f"{table}."

    def __f(col, value):
        if value is None:
            wherestm.append(f"{table}{col} is null")
        else:
            wherestm.append(f"{table}{col} = ?")
            filterdata.append(value)

    match day:
        case str():
            __f("date", day)
        case datetime.date():
            __f("date", day)
        case Day():
            __f("date", day.date)

    match room:
        case str():
            __f("room", room)
        case Room():
            __f("room", room.name)

    match track:
        case str():
            __f("track", track)
        case Track():
            __f("track", track.name)

    match event:
        case str():
            __f("id", event)
        case Event():
            __f("id", event.id)

    for k, v in kwargs.items():
        __f(k, v)

    return (filterdata, wherestm)


class Conference:
    """A conference in menu files (ggmenu.json and usermenu.json)"""

    # was added by user (true) or come from giggity (false)?
    user: bool = False

    id: str = ""
    version: str = ""
    url: str = ""
    title: str = ""
    timezone: str = "UTC"   # TODO: should default to current tz?
    start: datetime.date = datetime.date.today()
    end: datetime.date = datetime.date.today()

    metadata: dict[str, Any] = {}

    def _init(self, obj):
        self.metadata = {}
        self.user = False
        for k, v in obj.items():
            setattr(self, k, v)

        self.start = datetime.date(*[int(s) for s in self.start.split("-")])
        self.end = datetime.date(*[int(s) for s in self.end.split("-")])

        return self

    def get_map_links(self):
        maps = [link for link in self.metadata.get('links', []) if link['title'] == "Map"]
        return maps

    def get_map_files(self, cbk):
        r = []
        for link in self.get_map_links():
            r.append(local.get_image_async(link['url'], cbk))
        return r

    def get_logo_file(self, cbk):
        logourl = self.metadata.get('icon', None)
        return local.get_image_async(logourl, cbk)

    def __repr__(self):
        return f"<{self.__class__.__name__} {self.__dict__}>"

    @classmethod
    def all(cls):
        # from giggity menu
        return [cls()._init(s) for s in local.get_menu()]

    @classmethod
    def by_url(cls, url):
        ss = [s for s in local.get_menu() if url in s['url']]
        if len(ss) == 0:
            return None
        return cls()._init(ss[0])

    def has_cache(self):
        """Check if there is local db for this conference"""
        return local.existsdb(self.url)

    def get_meta(self):
        """Return metadata from db. This will close any opened db connection."""
        local.close()
        local.opendb(self.url, False)
        return Meta()

    def to_json(self):
        """Export conference meta as json for user menu"""
        d = self.__dict__.copy()
        for k in d:
            if isinstance(d[k], (datetime.date, datetime.datetime)):
                d[k] = d[k].strftime("%Y-%m-%d")
        return d


class Meta:
    """Conference metadata from "meta" table in db"""
    def __init__(self):
        _c = local.getDb().execute("SELECT key, value FROM meta")
        _d = [(row['key'], self._fmtd_value(row['key'], row['value'])) for row in _c]
        self._data = {'last_update': 0}
        self._data.update(dict(_d))

    def _fmtd_value(self, k, v):
        if k == "start" or k == "end":
            if v is not None and v != "":
                # drop any time info and keep only date
                v = v.split(" ")[0]
                v = v.split("T")[0]
                v = datetime.date(*[int(s) for s in v.split("-")])
            else:
                v = None
        if k == "last_update":
            v = float(v)
        return v

    def __getattr__(self, name):
        if name.startswith("_"):
            return super().__getattr__(name)
        return self._data.get(name)

    def __setattr__(self, name, value):
        if name.startswith("_"):
            return super().__setattr__(name, value)
        self._data[name] = value

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.save()

    def __contains__(self, name):
        return name in self._data

    def __repr__(self):
        return f"<{self.__class__.__name__} {self.__dict__}>"

    def get(self, name, default=None):
        #print("Meta.get", name, "->", self._data.get(name, default))
        return self._data.get(name, default)

    def set(self, name, value):
        #print("Meta.set", name, value)
        self._data[name] = value

    def save(self):
        db = local.getDb()
        if db is None:
            local.opendb(self.url, False)
            db = local.getDb()
        print(self._data.items())
        db.executemany("INSERT OR REPLACE INTO meta (key, value) VALUES (?,?)", self._data.items())
        db.commit()

    def to_json(self):
        """Export conference meta as json for user menu"""
        return {
            "url": self.url,
            "title": self.title if self.title else "",
            "start": self.start.strftime("%Y-%m-%d"),
            "end": self.end.strftime("%Y-%m-%d"),
            "user": True
        }


class Day:
    def _init(self, data):
        self.date = data['date']
        return self

    def __repr__(self):
        return "<{} {!r}>".format(self.__class__.__name__, self.date)

    def __str__(self):
        return self.date.strftime("%x")

    @classmethod
    def all(cls):
        for row in local.getDb().execute("SELECT date FROM events GROUP BY date"):
            yield cls()._init(row)

    @classmethod
    def count(cls):
        for row in local.getDb().execute("SELECT count(DISTINCT date) FROM events"):
            return row[0]

    @classmethod
    def filter(cls, room=None, track=None, event=None):
        filterdata, wherestm = _build_filter(room=room, track=track, event=event)

        query = """SELECT date FROM events
                    WHERE {}
                    GROUP BY date""".format(" AND ".join(wherestm))

        for row in local.getDb().execute(query, filterdata):
            yield cls()._init(row)


class Room:
    def _init(self, data):
        self.name = data['room']
        return self

    def __repr__(self):
        return "<{} {!r}>".format(self.__class__.__name__, self.name)

    def __str__(self):
        return self.name or "- no room -"

    @classmethod
    def all(cls):
        for row in local.getDb().execute("SELECT room FROM events GROUP BY room"):
            yield cls()._init(row)

    @classmethod
    def count(cls):
        for row in local.getDb().execute("SELECT count(DISTINCT room) FROM events"):
            return row[0]

    @classmethod
    def filter(cls, day=None, track=None, event=None):
        filterdata, wherestm = _build_filter(day=day, track=track, event=event)

        query = """SELECT room FROM events
                    WHERE {}
                    GROUP BY room""".format(" AND ".join(wherestm))

        for row in local.getDb().execute(query, filterdata):
            yield cls()._init(row)


class Track:
    def _init(self, data):
        self.name = data['track']
        self.room = list(Room.filter(track=self))
        self.date = list(Day.filter(track=self))
        return self

    def __repr__(self):
        return "<{} {!r} in {!r} @ {}>".format(
            self.__class__.__name__, self.name, self.room, self.date)

    def __str__(self):
        return self.name or "- no track -"

    @classmethod
    def all(cls):
        for row in local.getDb().execute(
                "SELECT track, room, date FROM events WHERE track != '' GROUP BY track ORDER BY date, track"):
            yield cls()._init(row)

    @classmethod
    def count(cls):
        for row in local.getDb().execute("SELECT count(DISTINCT track) FROM events"):
            return row[0]


class Person:
    def _init(self, data):
        self.id = data['id']
        self.name = data['name']
        self.organization = data['organization']
        self.thumbnail = data['thumbnail']
        self.bio = data['bio']
        self.url = data['url']
        return self

    def events(self):
        for row in local.getDb().execute(
                """SELECT e.* FROM events as e
                    LEFT JOIN event_person AS ep ON e.id = ep.event_id
                    WHERE ep.person_id = ?""", (self.id,)):
            yield Event()._init(row)

    def __repr__(self):
        return "<{} {!r}>".format(self.__class__.__name__, self.name)

    def __str__(self):
        return self.name

    @classmethod
    def all(cls):
        for row in local.getDb().execute(
                "SELECT * FROM persons"):
            yield cls()._init(row)

    @classmethod
    def by_event(cls, event_id):
        for row in local.getDb().execute(
                """SELECT * FROM persons as p
                    LEFT JOIN event_person AS ep ON p.id = ep.person_id
                    WHERE ep.event_id = ?""", (event_id,)):
            yield cls()._init(row)


class Link:
    def _init(self, event, data):
        self.event = event
        self.href = data['href']
        self.name = data['name']
        return self

    def __repr__(self):
        return "<{} for {!r} {!r} {!r}>".format(
            self.__class__.__name__, self.event, self.name, self.href)

    def __str__(self):
        return '<a href="{}">{}</a>'.format(self.href, self.name)


class DbUpdateWatcherObj(GObject.GObject):
    @GObject.Signal
    def update(self):
        ...


dbUpdateWatcher = DbUpdateWatcherObj()


class Event(GObject.GObject):
    @GObject.Signal
    def update(self):
        ...

    def _init(self, data):
        self.id = data['id']
        self.date = data['date']
        self.start = data['start']
        self.end = data['end']
        self.room = data['room']
        self.slug = data['slug']
        self.title = data['title']
        self.subtitle = data['subtitle'] or ""
        self.track = data['track']
        self.evtype = data['type']
        self.abstract = data['abstract'] or ""
        self.description = data['description'] or ""
        self.starred = data['starred'] == 1
        self.notified = data['notified'] == 1
        return self

    def set_star(self, starred):
        local.getDb().execute("UPDATE events SET starred=? WHERE id=?", (starred, self.id,))
        local.getDb().commit()
        self.starred = starred
        self.emit('update')
        dbUpdateWatcher.emit('update')

    def set_notified(self, notified):
        local.getDb().execute("UPDATE events SET notified=? WHERE id=?", (notified, self.id,))
        local.getDb().commit()
        self.notified = notified
        self.emit('update')
        dbUpdateWatcher.emit('update')

    def start_in_tz(self):
        return self._dt_in_tz(self.start)

    def end_in_tz(self):
        return self._dt_in_tz(self.end)

    def _dt_in_tz(self, dt):
        dt = dt.replace(tzinfo=UTC)
        dt = dt.astimezone(Settings.instance().get_timezone())
        return dt

    def persons(self):
        # print("event", self.id, "persons()")
        return Person.by_event(self.id)

    def links(self):
        for row in local.getDb().execute(
            """SELECT href, name FROM links
                WHERE event_id=?""", (self.id,)):
            yield Link()._init(self, row)

    def __repr__(self):
        return "<{} {!r} in {!r} @ {!r}>".format(self.__class__.__name__, self.title, self.room, self.start)

    def __str__(self):
        return self.title

    def get_conflicts(self):
        query = """SELECT * FROM events
                    WHERE starred = 1 AND id != ? AND date = ?
                    AND end > ? AND start < ?
                """
        for row in local.getDb().execute(query, (self.id, self.date, self.start, self.end)):
            yield Event()._init(row)

    @classmethod
    def all(cls):
        row = local.getDb().execute("SELECT count(id) as c FROM events").fetchone()
        yield 0 if row is None else row['c']

        for row in local.getDb().execute("SELECT * FROM events"):
            yield cls()._init(row)

    @classmethod
    def by_id(cls, id):
        row = local.getDb().execute("SELECT * FROM events WHERE id = ?", (id,)).fetchone()
        if row is None:
            raise ObjectNotFound("<{} #{}>".format(cls.__name__, id))
        return cls()._init(row)

    @classmethod
    def filter(cls,
               day: Optional[Union[Day, str]] = None,
               room: Optional[Room] = None,
               track: Optional[Track] = None,
               order_by: str = "start, room",
               **kwargs):
        filterdata, wherestm = _build_filter("events", day=day, room=room, track=track, **kwargs)

        query = """SELECT * FROM events
                    WHERE {}
                    ORDER BY {}""".format(" AND ".join(wherestm), order_by)

        resultcountquery = query.replace("SELECT *", "SELECT count(events.id) as c")
        row = local.getDb().execute(resultcountquery, filterdata).fetchone()
        yield 0 if row is None else row['c']

        for row in local.getDb().execute(query, filterdata):
            yield cls()._init(row)

    @classmethod
    def nextup(cls, minutes):
        """get events starting in next `minutes`"""
        minutes = min(int(minutes), 30)  # some sanity check
        now = datetime.datetime.now(UTC).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S.000")
        # now = "now" # is "now" somehow broken?
        query = """SELECT * FROM events
                    WHERE starred=1 AND notified=0
                        AND datetime(start) >= datetime('{1}')
                        AND datetime(start) <= datetime('{1}', '+{0} minutes');"""
        for row in local.getDb().execute(query.format(minutes, now)):
            yield cls()._init(row)

    @classmethod
    def search(cls, term, starred=False, order_by=None, **filters):
        """Search events for 'term'"""
        if starred:
            filters["starred"] = 1
        filterdata, wherestm = _build_filter("events", **filters)

        if term == "" and len(wherestm) == 0:
            yield 0
            return

        if term != "":
            order = ['rank']
            if order_by:
                order = [order_by, 'rank']

            wherestm.append("fts_event = ?")
            filterdata.append(term)

            query = """SELECT events.* FROM fts_event
                        JOIN events ON events.id = fts_event.event_id
                        WHERE {}
                        GROUP BY events.id
                        ORDER BY {}"""
        else:
            order = ['start', 'room']
            if order_by:
                order = [order_by]
            query = """SELECT events.* FROM events
                        WHERE {}
                        ORDER BY {}"""

        query = query.format(
            " AND ".join(wherestm),
            ", ".join(order)
        )

        resultcountquery = query.replace("SELECT events.*", "SELECT count(events.id) as c")
        row = local.getDb().execute(resultcountquery, filterdata).fetchone()
        yield 0 if row is None else row['c']

        results = local.getDb().execute(query, filterdata)
        for row in results:
            yield cls()._init(row)


class RecentConf(GObject.Object):
    __gtype_name__ = "ConfyRecentConf"

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

    @GObject.Property(type=str, default="<unknown>")
    def title(self):
        return self._title

    def __init__(self, recentinfo: Gtk.RecentInfo):
        super().__init__()
        self.data = recentinfo.get_description()
        self._uri = recentinfo.get_uri()
        self._title = recentinfo.get_display_name()

    def __str__(self):
        return f"{self._title} : {self._uri}"
