#!/usr/bin/python3
#
# Copyright 2012-2014 "Korora Project" <dev@kororaproject.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the temms 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/>.
#

# FIXME: Hack for gobject don't blow up'
import os
os.environ['XDG_RUNTIME_DIR'] = '/root'

import argparse
import dbus
import dbus.service
import dbus.mainloop.glib
import dnfdaemon.server
import json
import logging

from dnfdaemon.server import Logger
from Pharlap.modprobe import ModProbe

DAEMON_ORG = 'org.kororaproject.Pharlap'
DAEMON_INT = DAEMON_ORG
PHARLAP_VERSION = 1

logger = logging.getLogger('pharlap.daemon')


class AccessDeniedError(dbus.DBusException):
  _dbus_error_name = DAEMON_ORG + '.AccessDeniedError'

class LockedError(dbus.DBusException):
    _dbus_error_name = DAEMON_ORG + '.LockedError'

class NotImplementedError(dbus.DBusException):
  _dbus_error_name = DAEMON_ORG + '.NotImplementedError'



class PharlapDaemon(dnfdaemon.server.DnfDaemonBase):

  def __init__(self):
    dnfdaemon.server.DnfDaemonBase.__init__(self)
    bus_name = dbus.service.BusName(DAEMON_ORG, bus=dbus.SystemBus())
    dbus.service.Object.__init__(self, bus_name, '/')
    self._gpg_confirm = {}


  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='', out_signature='i')
  def GetVersion(self):
    """
    Get the daemon version
    """
    return PHARLAP_VERSION


  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='', out_signature='b', sender_keyword='sender')
  def Lock(self, sender=None):
    """
    Get the yum lock
    :param sender:
    """
    self.check_permission_read(sender)
    if not self._lock:
      self._lock = sender
      logger.info('LOCK: Locked by : %s' % sender)
      return True

    return False

  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='', out_signature='b', sender_keyword='sender')
  def Unlock(self, sender=None):
    """ release the lock"""
    self.check_permission_read(sender)
    if self.check_lock(sender):
      self._reset_base()
      logger.info('UNLOCK: Lock Release by %s' % self._lock)
      self._lock = None
      return True

  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='', out_signature='s')
  def GetHardware(self):
    """
    Get system hardware listing
    """

    hwinfo = '{}'

    try:
      p = os.popen('/usr/sbin/lshw -quiet -json')
      ret = p.read()
      p.close()
      if len(ret) > 0:
        hwinfo = json.dumps(json.loads(ret))
    except:
      pass

    return hwinfo


  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='sb', out_signature='b', sender_keyword='sender')
  def BlacklistModule(self, name, state, sender=None):
    """
    Get the value a list of repo ids
    :param name: name of module to blacklist
    :param sender:
    """
    return True


  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='ss', out_signature='b', sender_keyword='sender')
  def SetModuleQuirks(self, name, quirks, sender=None):
    """
    Get the value a list of repo ids
    :param filter: filter to limit the listed repositories
    :param sender:
    """
    return True

  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='s', out_signature='s', sender_keyword='sender')
  def Install(self, cmds, sender=None):
    """
    Install packages based on command patterns separated by spaces
    simulate what 'dnf install <arguments>' does
    :param cmds: command patterns separated by spaces
    :param sender:
    """
    self.working_start(sender)
    value = self.install(cmds)
    return self.working_ended(value)

  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='s', out_signature='s', sender_keyword='sender')
  def Remove(self, cmds, sender=None):
    """
    Remove packages based on command patterns separated by spaces
    simulate what 'dnf remove <arguments>' does
    :param cmds: command patterns separated by spaces
    :param sender:
    """
    self.working_start(sender)
    value = self.remove(cmds)
    return self.working_ended(value)

  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='ss', out_signature='s', sender_keyword='sender')
  def AddTransaction(self, pkg_id, action, sender=None):
    """
    Add an package to the current transaction

    :param pkg_id: package pkg_id for the package to add
    :param action: the action to perform ( install, update, remove,
                   obsolete, reinstall, downgrade, localinstall )
    """
    self.working_start(sender, write=False)
    value = self.add_transaction(pkg_id, action)
    return self.working_ended(value)

  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='', out_signature='', sender_keyword='sender')
  def ClearTransaction(self, sender):
    """
    Clear the transactopm
    """
    self.working_start(sender, write=False)
    self.clear_transaction()
    return self.working_ended()

  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='', out_signature='s', sender_keyword='sender')
  def GetTransaction(self, sender=None):
    """
    Return the members of the current transaction
    """
    self.working_start(sender, write=False)
    value = self.get_transaction()
    return self.working_ended(value)

  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='', out_signature='s', sender_keyword='sender')
  def BuildTransaction(self, sender):
    """
    Resolve dependencies of current transaction
    """
    self.working_start(sender, write=False)
    value = self.build_transaction()
    return self.working_ended(value)

  @Logger
  @dbus.service.method(DAEMON_INT, in_signature='i', out_signature='s', sender_keyword='sender')
  def RunTransaction(self, max_err, sender=None):
    """
    Run the current yum transaction
    :param max_err: maximum download errors before bail out
    """
    self.working_start(sender)
    self.check_permission_write(sender)
    self.check_lock(sender)
    result = self.run_transaction(max_err)
    return self.working_ended(result)

#
# DBUS SIGNALS
#

  @dbus.service.signal(DAEMON_INT)
  def ErrorMessage(self, error_msg):
    """ Send an error message """
    pass

  @dbus.service.signal(DAEMON_INT)
  def DownloadStart(self, num_files, num_bytes):
    """ Starting a new parallel download batch """
    pass

  @dbus.service.signal(DAEMON_INT)
  def DownloadProgress(self, name, frac, total_frac, total_files):
    """ Progress for a single instance in the batch """
    pass

  @dbus.service.signal(DAEMON_INT)
  def DownloadEnd(self, name, status, msg):
    """ Download of af single instace ended """
    pass

  @dbus.service.signal(DAEMON_INT)
  def RepoMetaDataProgress(self, name, frac):
    """ Repository Metadata Download progress """
    pass

  @dbus.service.signal(DAEMON_INT)
  def TransactionEvent(self, event, data):
    """
    DBus signal with Transaction event information, telling the current
    step in the processing of the current transaction.

    Steps are : start-run, download, pkg-to-download, signature-check,
                run-test-transaction,
    run-transaction, verify, fail, end-run

    :param event: current step
    """
    # print "event: %s" % event
    pass

  @dbus.service.signal(DAEMON_INT)
  def RPMProgress(self, package, action, te_current, te_total, ts_current, ts_total):
    """
    RPM Progress DBus signal
    :param package: A yum package object or simple string of a package name
    :param action: A yum.constant transaction set state or in the obscure
                   rpm repackage case it could be the string 'repackaging'
    :param te_current: Current number of bytes processed in the transaction
                       element being processed
    :param te_total: Total number of bytes in the transaction element being
                     processed
    :param ts_current: number of processes completed in whole transaction
    :param ts_total: total number of processes in the transaction.
    """
    pass

  @dbus.service.signal(DAEMON_INT)
  def GPGImport(self, pkg_id, userid, hexkeyid, keyurl, timestamp):
    """
    GPG Key Import DBus signal

    :param pkg_id: pkg_id for the package needing the GPG Key
                   to be verified
    :param userid: GPG key name
    :param hexkeyid: GPG key hex id
    :param keyurl: Url to the GPG Key
    :param timestamp:
    """
    pass

#
# HELPERS
#

  def working_start(self, sender, write=True):
    if write:
      self.check_permission_write(sender)

    else:
      self.check_permission_read(sender)

    self.check_lock(sender)
    self._is_working = True
    self._watchdog_count = 0

  def working_ended(self, value=None):
    self._is_working = False
    return value

  def check_lock(self, sender):
    """
    Check that the current sender is owning the pharlap lock
    :param sender:
    """
    if self._lock == sender:
      return True

    else:
      raise LockedError('pharlapd is locked by another application')

  def check_permission_write(self, sender):
    """ Check for senders permission to update system packages"""
    if sender in self.authorized_sender_write:
      return

    else:
      self._check_permission(sender, 'org.kororaproject.Pharlap.write')
      self.authorized_sender_write.add(sender)

  def check_permission_read(self, sender):
    """ Check for senders permission to read system packages"""
    if sender in self.authorized_sender_read:
      return

    else:
      self._check_permission(sender, 'org.kororaproject.Pharlap.read')
      self.authorized_sender_read.add(sender)

  def _check_permission(self, sender, action):
    """ Check senders permissions using PolicyKit1
    """
    if not sender:
      raise ValueError('sender == None')

    obj = dbus.SystemBus().get_object(
      'org.freedesktop.PolicyKit1',
      '/org/freedesktop/PolicyKit1/Authority'
    )

    obj = dbus.Interface(obj, 'org.freedesktop.PolicyKit1.Authority')

    (granted, _, details) = obj.CheckAuthorization(
      ('system-bus-name', {'name': sender}), action, {},
      dbus.UInt32(1), '', timeout=600)

    if not granted:
      raise AccessDeniedError('Session is not authorized')



def main():
  parser = argparse.ArgumentParser(description='Pharlap D-Bus Daemon')
  parser.add_argument('-v', '--verbose', action='store_true')
  parser.add_argument('-d', '--debug', action='store_true')
  parser.add_argument('--notimeout', action='store_true')
  args = parser.parse_args()
  if args.verbose:
    if args.debug:
      dnfdaemon.server.doTextLoggerSetup(logroot='pharlap', loglvl=logging.DEBUG)
    else:
      dnfdaemon.server.doTextLoggerSetup(logroot='pharlap')

  # setup the DBus mainloop
  dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

  pd = PharlapDaemon()
  pd.mainloop_run()


if __name__ == '__main__':
  main()
