# -*- coding: utf-8 -*-
#
# Copyright (C) 2014-2023 Edgewall Software
# Copyright (C) 2010 Robert Corsaro
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://trac.edgewall.org/log/.

from trac.util.datefmt import datetime_now, utc, to_utimestamp

__all__ = ['Subscription', 'Watch']


class Subscription(object):

    __slots__ = ('env', 'values')

    fields = ('id', 'sid', 'authenticated', 'distributor', 'format',
              'priority', 'adverb', 'class')

    def __init__(self, env):
        self.env = env
        self.values = {}

    def __repr__(self):
        values = ' '.join('%s=%r' % (name, self.values.get(name))
                          for name in self.fields)
        return '<%s %s>' % (self.__class__.__name__, values)

    def __getitem__(self, name):
        if name not in self.fields:
            raise KeyError(name)
        return self.values.get(name)

    def __setitem__(self, name, value):
        if name not in self.fields:
            raise KeyError(name)
        self.values[name] = value

    def _from_database(self, id, sid, authenticated, distributor, format,
                       priority, adverb, class_):
        self['id'] = id
        self['sid'] = sid
        self['authenticated'] = int(authenticated)
        self['distributor'] = distributor
        self['format'] = format or None
        self['priority'] = int(priority)
        self['adverb'] = adverb
        self['class'] = class_

    @classmethod
    def add(cls, env, subscription):
        """id and priority overwritten."""
        with env.db_transaction as db:
            priority = len(cls.find_by_sid_and_distributor(
                env, subscription['sid'], subscription['authenticated'],
                subscription['distributor'])) + 1
            now = to_utimestamp(datetime_now(utc))
            cursor = db.cursor()
            cursor.execute("""
                INSERT INTO
                notify_subscription (time, changetime, sid, authenticated,
                                     distributor, format, priority, adverb,
                                     class)
                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)""",
            (now, now, subscription['sid'], int(subscription['authenticated']),
             subscription['distributor'], subscription['format'] or None,
             int(priority), subscription['adverb'],
             subscription['class']))
            return db.get_last_id(cursor, 'notify_subscription')

    @classmethod
    def delete(cls, env, rule_id, sid=None, authenticated=None):
        with env.db_transaction as db:
            kwargs = {'id': rule_id}
            if sid is not None or authenticated is not None:
                kwargs['sid'] = sid
                kwargs['authenticated'] = 1 if authenticated else 0
            for sub in cls._find(env, **kwargs):
                break
            else:
                return
            db("DELETE FROM notify_subscription WHERE id=%s", (sub['id'],))
            subs = cls.find_by_sid_and_distributor(
                env, sub['sid'], sub['authenticated'], sub['distributor'])
            now = to_utimestamp(datetime_now(utc))
            values = [(new_priority, now, sub['id'])
                      for new_priority, sub in enumerate(subs, 1)
                      if new_priority != sub['priority']]
            db.executemany("""
                UPDATE notify_subscription
                SET priority=%s, changetime=%s WHERE id=%s
                """, values)

    @classmethod
    def move(cls, env, rule_id, priority, sid=None, authenticated=None):
        with env.db_transaction as db:
            kwargs = {'id': rule_id}
            if sid is not None or authenticated is not None:
                kwargs['sid'] = sid
                kwargs['authenticated'] = 1 if authenticated else 0
            for sub in cls._find(env, **kwargs):
                break
            else:
                return
            subs = cls.find_by_sid_and_distributor(
                env, sub['sid'], sub['authenticated'], sub['distributor'])
            if not (1 <= priority <= len(subs)):
                return
            for idx, sub in enumerate(subs):
                if sub['id'] == rule_id:
                    break
            else:
                return
            subs.insert(priority - 1, subs.pop(idx))
            now = to_utimestamp(datetime_now(utc))
            values = [(new_priority, now, sub['id'])
                      for new_priority, sub in enumerate(subs, 1)
                      if new_priority != sub['priority']]
            db.executemany("""
                UPDATE notify_subscription
                SET priority=%s, changetime=%s WHERE id=%s
                """, values)

    @classmethod
    def replace_all(cls, env, sid, authenticated, subscriptions):
        authenticated = int(authenticated)
        with env.db_transaction as db:
            ids_map = {}
            for id_, distributor, class_ in db("""\
                    SELECT id, distributor, class FROM notify_subscription
                    WHERE sid=%s AND authenticated=%s""",
                    (sid, authenticated)):
                ids_map.setdefault((distributor, class_), []).append(id_)
            for ids in ids_map.values():
                ids.sort(reverse=True)

            priorities = {}
            now = to_utimestamp(datetime_now(utc))
            for sub in subscriptions:
                distributor = sub['distributor']
                priorities.setdefault(distributor, 0)
                priorities[distributor] += 1
                prio = priorities[distributor]
                key = (distributor, sub['class'])
                if ids_map.get(key):
                    id_ = ids_map[key].pop()
                    db("""\
                        UPDATE notify_subscription
                        SET changetime=%s,distributor=%s,format=%s,priority=%s,
                            adverb=%s,class=%s
                        WHERE id=%s""",
                        (now, sub['distributor'], sub['format'] or None, prio,
                         sub['adverb'], sub['class'], id_))
                else:
                    db("""\
                        INSERT INTO notify_subscription (
                            time,changetime,sid,authenticated,distributor,
                            format,priority,adverb,class)
                        VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
                        (now, now, sid, authenticated, sub['distributor'],
                         sub['format'] or None, prio, sub['adverb'],
                         sub['class']))

            delete_ids = []
            for ids in ids_map.values():
                delete_ids.extend(ids)
            if delete_ids:
                db("DELETE FROM notify_subscription WHERE id IN (%s)" %
                   ','.join(('%s',) * len(delete_ids)), delete_ids)


    @classmethod
    def update_format_by_distributor_and_sid(cls, env, distributor, sid,
                                             authenticated, format):
        with env.db_transaction as db:
            db("""
                UPDATE notify_subscription
                   SET format=%s
                 WHERE distributor=%s
                   AND sid=%s
                   AND authenticated=%s
            """, (format or None, distributor, sid, int(authenticated)))

    @classmethod
    def _find(cls, env, order=None, **kwargs):
        with env.db_query as db:
            conditions = []
            args = []
            for name, value in sorted(kwargs.items()):
                if name.endswith('_'):
                    name = name[:-1]
                if name == 'authenticated':
                    value = int(value)
                conditions.append(db.quote(name) + '=%s')
                args.append(value)
            query = 'SELECT id, sid, authenticated, distributor, format, ' \
                    'priority, adverb, class FROM notify_subscription'
            if conditions:
                query += ' WHERE ' + ' AND '.join(conditions)
            if order:
                if not isinstance(order, (tuple, list)):
                    order = (order,)
                query += ' ORDER BY ' + \
                         ', '.join(db.quote(name) for name in order)
            cursor = db.cursor()
            cursor.execute(query, args)
            for row in cursor:
                sub = Subscription(env)
                sub._from_database(*row)
                yield sub

    @classmethod
    def find_by_sid_and_distributor(cls, env, sid, authenticated, distributor):
        return list(cls._find(env, sid=sid, authenticated=authenticated,
                              distributor=distributor, order='priority'))

    @classmethod
    def find_by_sids_and_class(cls, env, uids, class_):
        """uids should be a collection to tuples (sid, auth)"""
        subs = []
        for sid, authenticated in uids:
            subs.extend(cls._find(env, class_=class_, sid=sid,
                                  authenticated=authenticated,
                                  order='priority'))
        return subs

    @classmethod
    def find_by_class(cls, env, class_):
        return list(cls._find(env, class_=class_))

    def subscription_tuple(self):
        return (
            self.values['class'],
            self.values['distributor'],
            self.values['sid'],
            self.values['authenticated'],
            None,
            self.values['format'] or None,
            int(self.values['priority']),
            self.values['adverb']
        )

    def _update_priority(self):
        with self.env.db_transaction as db:
            cursor = db.cursor()
            now = to_utimestamp(datetime_now(utc))
            cursor.execute("""
                UPDATE notify_subscription
                   SET changetime=%s, priority=%s
                 WHERE id=%s
            """, (now, int(self.values['priority']), self.values['id']))


class Watch(object):

    __slots__ = ('env', 'values')

    fields = ('id', 'sid', 'authenticated', 'class', 'realm', 'target')

    def __init__(self, env):
        self.env = env
        self.values = {}

    def __getitem__(self, name):
        if name not in self.fields:
            raise KeyError(name)
        return self.values.get(name)

    def __setitem__(self, name, value):
        if name not in self.fields:
            raise KeyError(name)
        self.values[name] = value

    def _from_database(self, id, sid, authenticated, class_, realm, target):
        self['id'] = id
        self['sid'] = sid
        self['authenticated'] = int(authenticated)
        self['class'] = class_
        self['realm'] = realm
        self['target'] = target

    @classmethod
    def add(cls, env, sid, authenticated, class_, realm, targets):
        with env.db_transaction as db:
            for target in targets:
                db("""
                    INSERT INTO notify_watch (sid, authenticated, class,
                                              realm, target)
                    VALUES (%s, %s, %s, %s, %s)
                """, (sid, int(authenticated), class_, realm, target))

    @classmethod
    def delete(cls, env, watch_id):
        with env.db_transaction as db:
            db("DELETE FROM notify_watch WHERE id = %s", (watch_id,))

    @classmethod
    def delete_by_sid_and_class(cls, env, sid, authenticated, class_):
        with env.db_transaction as db:
            db("""
                DELETE FROM notify_watch
                WHERE sid = %s AND authenticated = %s AND class = %s
            """, (sid, int(authenticated), class_))

    @classmethod
    def delete_by_class_realm_and_target(cls, env, class_, realm, target):
        with env.db_transaction as db:
            db("""
                DELETE FROM notify_watch
                WHERE class = %s AND realm = %s AND target = %s
            """, (realm, class_, target))

    @classmethod
    def _find(cls, env, order=None, **kwargs):
        with env.db_query as db:
            conditions = []
            args = []
            for name, value in sorted(kwargs.items()):
                if name.endswith('_'):
                    name = name[:-1]
                if name == 'authenticated':
                    value = int(value)
                conditions.append(db.quote(name) + '=%s')
                if name == 'authenticated':
                    value = int(value)
                args.append(value)
            query = 'SELECT id, sid, authenticated, class, realm, target ' \
                    'FROM notify_watch'
            if conditions:
                query += ' WHERE ' + ' AND '.join(conditions)
            if order:
                if not isinstance(order, (tuple, list)):
                    order = (order,)
                query += ' ORDER BY ' + \
                         ', '.join(db.quote(name) for name in order)
            cursor = db.cursor()
            cursor.execute(query, args)
            for row in cursor:
                watch = Watch(env)
                watch._from_database(*row)
                yield watch

    @classmethod
    def find_by_sid_and_class(cls, env, sid, authenticated, class_):
        return list(cls._find(env, sid=sid, authenticated=authenticated,
                              class_=class_, order='target'))

    @classmethod
    def find_by_sid_class_realm_and_target(cls, env, sid, authenticated,
                                           class_, realm, target):
        return list(cls._find(env, sid=sid, authenticated=authenticated,
                              class_=class_, realm=realm, order='target'))

    @classmethod
    def find_by_class_realm_and_target(cls, env, class_, realm, target):
        return list(cls._find(env, class_=class_, realm=realm, target=target))

    @classmethod
    def find_by_class_and_realm(cls, env, class_, realm):
        return list(cls._find(env, class_=class_, realm=realm))
