""" ResourceStatusDB:
    This module provides definition of the DB tables, and methods to access them.

    Written using sqlalchemy declarative_base


    For extending the ResourceStatusDB tables:

    1) In the extended module, call:

    from DIRAC.ResourceStatusSystem.DB.ResourceStatusDB import rmsBase, TABLESLIST, TABLESLISTWITHID
    TABLESLIST = TABLESLIST + [list of new table names]
    TABLESLISTWITHID = TABLESLISTWITHID + [list of new table names]

    2) provide a declarative_base definition of the tables (new or extended) in the extension module

"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

__RCSID__ = "$Id$"


import six
import datetime
from sqlalchemy.orm import class_mapper
from sqlalchemy.orm.query import Query
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, String, DateTime, exc, BigInteger

from DIRAC import S_OK, S_ERROR, gConfig
from DIRAC.Core.Base.SQLAlchemyDB import SQLAlchemyDB
from DIRAC.ResourceStatusSystem.Utilities import Utils


TABLESLIST = ['SiteStatus',
              'ResourceStatus',
              'NodeStatus']

TABLESLISTWITHID = ['ResourceStatusCache',
                    'SiteLog',
                    'SiteHistory',
                    'ResourceLog',
                    'ResourceHistory',
                    'NodeLog',
                    'NodeHistory']


# Defining the tables

rssBase = declarative_base()


class ResourceStatusCache(rssBase):
  """
  Table for EmailAction
  """
  __tablename__ = 'ResourceStatusCache'
  __table_args__ = {'mysql_engine': 'InnoDB',
                    'mysql_charset': 'utf8'}

  id = Column('ID', BigInteger, nullable=False, autoincrement=True, primary_key=True)
  sitename = Column('SiteName', String(64), nullable=False)
  name = Column('ResourceName', String(64), nullable=False)
  status = Column('Status', String(8), nullable=False, server_default='')
  previousstatus = Column('PreviousStatus', String(8), nullable=False, server_default='')
  statustype = Column('StatusType', String(128), nullable=False, server_default='all')
  time = Column('Time', DateTime, nullable=False, server_default='9999-12-31 23:59:59')

  def fromDict(self, dictionary):
    """
    Fill the fields of the AccountingCache object from a dictionary

    :param dictionary: Dictionary to fill a single line
    :type arguments: dict
    """

    self.id = dictionary.get('ID', self.id)
    self.name = dictionary.get('ResourceName', self.name)
    self.sitename = dictionary.get('SiteName', self.sitename)
    self.status = dictionary.get('Status', self.status)
    self.previousstatus = dictionary.get('PreviousStatus', self.previousstatus)
    self.statustype = dictionary.get('StatusType', self.statustype)
    self.time = dictionary.get('Time', datetime.datetime.utcnow())

  def toList(self):
    """
    Simply returns a list of column values
    """

    return [self.id, self.sitename, self.name, self.status, self.previousstatus, self.statustype, self.time]


class ElementStatusBase(object):
  """
  Prototype for tables.
  """

  __table_args__ = {'mysql_engine': 'InnoDB',
                    'mysql_charset': 'utf8'}

  name = Column('Name', String(64), nullable=False, primary_key=True)
  statustype = Column('StatusType', String(128), nullable=False, server_default='all', primary_key=True)
  vo = Column('VO', String(64), nullable=False, primary_key=True, server_default='all')
  status = Column('Status', String(8), nullable=False, server_default='')
  reason = Column('Reason', String(512), nullable=False, server_default='Unspecified')
  dateeffective = Column('DateEffective', DateTime, nullable=False)
  tokenexpiration = Column('TokenExpiration', DateTime, nullable=False, server_default='9999-12-31 23:59:59')
  elementtype = Column('ElementType', String(32), nullable=False, server_default='')
  lastchecktime = Column('LastCheckTime', DateTime, nullable=False, server_default='1000-01-01 00:00:00')
  tokenowner = Column('TokenOwner', String(16), nullable=False, server_default='rs_svc')

  columnsOrder = ['Name', 'StatusType', 'Status', 'Reason',
                  'DateEffective', 'TokenExpiration', 'ElementType',
                  'LastCheckTime', 'TokenOwner', 'VO']

  def fromDict(self, dictionary):
    """
    Fill the fields of the AccountingCache object from a dictionary

    :param dictionary: Dictionary to fill a single line
    :type arguments: dict
    """

    utcnow = self.lastchecktime.replace(microsecond=0) if self.lastchecktime\
        else datetime.datetime.utcnow().replace(microsecond=0)

    self.name = dictionary.get('Name', self.name)
    self.statustype = dictionary.get('StatusType', self.statustype)
    self.vo = dictionary.get('VO', self.vo)
    self.status = dictionary.get('Status', self.status)
    self.reason = dictionary.get('Reason', self.reason)
    self.dateeffective = dictionary.get('DateEffective', self.dateeffective)
    self.tokenexpiration = dictionary.get('TokenExpiration', self.tokenexpiration)
    self.elementtype = dictionary.get('ElementType', self.elementtype)
    self.lastchecktime = dictionary.get('LastCheckTime', utcnow)
    self.tokenowner = dictionary.get('TokenOwner', self.tokenowner)

    if self.dateeffective:
      self.dateeffective = self.dateeffective.replace(microsecond=0)
    if self.tokenexpiration:
      self.tokenexpiration = self.tokenexpiration.replace(microsecond=0)

  def toList(self):
    """ Simply returns a list of column values
    """
    return [self.name, self.statustype, self.status, self.reason,
            self.dateeffective, self.tokenexpiration, self.elementtype,
            self.lastchecktime, self.tokenowner, self.vo]


class ElementStatusBaseWithID(ElementStatusBase):
  """ Prototype for tables

      This is almost the same as ElementStatusBase, with the following differences:
      - there's an autoincrement ID column which is also the primary key
      - the name and statusType components are not part of the primary key
  """

  id = Column('ID', BigInteger, nullable=False, autoincrement=True, primary_key=True)
  name = Column('Name', String(64), nullable=False)
  statustype = Column('StatusType', String(128), nullable=False, server_default='all')
  vo = Column('VO', String(64), nullable=False, server_default='all')
  status = Column('Status', String(8), nullable=False, server_default='')
  reason = Column('Reason', String(512), nullable=False, server_default='Unspecified')
  dateeffective = Column('DateEffective', DateTime, nullable=False)
  tokenexpiration = Column('TokenExpiration', DateTime, nullable=False, server_default='9999-12-31 23:59:59')
  elementtype = Column('ElementType', String(32), nullable=False, server_default='')
  lastchecktime = Column('LastCheckTime', DateTime, nullable=False, server_default='1000-01-01 00:00:00')
  tokenowner = Column('TokenOwner', String(16), nullable=False, server_default='rs_svc')

  columnsOrder = ['ID', 'Name', 'StatusType', 'Status', 'Reason',
                  'DateEffective', 'TokenExpiration', 'ElementType',
                  'LastCheckTime', 'TokenOwner', 'VO']

  def fromDict(self, dictionary):
    """
    Fill the fields of the AccountingCache object from a dictionary

    :param dictionary: Dictionary to fill a single line
    :type arguments: dict
    """

    self.id = dictionary.get('ID', self.id)
    super(ElementStatusBaseWithID, self).fromDict(dictionary)

  def toList(self):
    """ Simply returns a list of column values
    """
    return [self.id, self.name, self.statustype, self.status, self.reason,
            self.dateeffective, self.tokenexpiration, self.elementtype,
            self.lastchecktime, self.tokenowner, self.vo]


# tables with schema defined in ElementStatusBase

class SiteStatus(ElementStatusBase, rssBase):
  """ SiteStatus table
  """

  __tablename__ = 'SiteStatus'


class ResourceStatus(ElementStatusBase, rssBase):
  """ ResourceStatusDB table
  """

  __tablename__ = 'ResourceStatus'


class NodeStatus(ElementStatusBase, rssBase):
  """ NodeStatus table
  """

  __tablename__ = 'NodeStatus'


# tables with schema defined in ElementStatusBaseWithID

class SiteLog(ElementStatusBaseWithID, rssBase):
  """ SiteLog table
  """

  __tablename__ = 'SiteLog'


class SiteHistory(ElementStatusBaseWithID, rssBase):
  """ SiteHistory table
  """
  __tablename__ = 'SiteHistory'


class ResourceLog(ElementStatusBaseWithID, rssBase):
  """ ResourceLog table
  """

  __tablename__ = 'ResourceLog'


class ResourceHistory(ElementStatusBaseWithID, rssBase):
  """ ResourceHistory table
  """
  __tablename__ = 'ResourceHistory'


class NodeLog(ElementStatusBaseWithID, rssBase):
  """ NodeLog table
  """

  __tablename__ = 'NodeLog'


class NodeHistory(ElementStatusBaseWithID, rssBase):
  """ NodeHistory table
  """
  __tablename__ = 'NodeHistory'


# Interaction with the DB

class ResourceStatusDB(SQLAlchemyDB):
  """
    Class that defines the interactions with the tables of the ResourceStatusDB.
  """

  def __init__(self):
    """c'tor

    :param self: self reference
    """

    super(ResourceStatusDB, self).__init__()

    # These are the list of tables that will be created.
    # They can be extended in an extension module
    self.tablesList = getattr(Utils.voimport('DIRAC.ResourceStatusSystem.DB.ResourceStatusDB'),
                              'TABLESLIST')
    self.tablesListWithID = getattr(Utils.voimport('DIRAC.ResourceStatusSystem.DB.ResourceStatusDB'),
                                    'TABLESLISTWITHID')

    self.extensions = gConfig.getValue('DIRAC/Extensions', [])
    self._initializeConnection('ResourceStatus/ResourceStatusDB')

    # Create required tables
    self._createTablesIfNotThere(self.tablesList)
    self._createTablesIfNotThere(self.tablesListWithID)

  def insert(self, table, params):
    """
    Inserts params in the DB.

    :param table: table where to insert
    :type table: str
    :param params: Dictionary to fill a single line
    :type params: dict

    :return: S_OK() || S_ERROR()
    """

    if not params.get('DateEffective'):
      params['DateEffective'] = datetime.datetime.utcnow().replace(microsecond=0)

    return super(ResourceStatusDB, self).insert(table, params)

  def addOrModify(self, table, params):
    """
    Using the PrimaryKeys of the table, it looks for the record in the database.
    If it is there, it is updated, if not, it is inserted as a new entry.

    :param table: table where to add or modify
    :type table: str
    :param params: dictionary of what to add or modify
    :type params: dict

    :return: S_OK() || S_ERROR()
    """

    session = self.sessionMaker_o()
    found = False
    for ext in self.extensions:
      try:
        table_c = getattr(__import__(ext + __name__, globals(), locals(), [table]), table)
        found = True
        break
      except (ImportError, AttributeError):
        continue
    # If not found in extensions, import it from DIRAC base (this same module).
    if not found:
      table_c = getattr(__import__(__name__, globals(), locals(), [table]), table)
    primaryKeys = [key.name for key in class_mapper(table_c).primary_key]

    try:
      select = Query(table_c, session=session)
      for columnName, columnValue in params.items():
        if not columnValue or columnName not in primaryKeys:
          continue
        column_a = getattr(table_c, columnName.lower())
        if isinstance(columnValue, (list, tuple)):
          select = select.filter(column_a.in_(list(columnValue)))
        elif isinstance(columnValue, six.string_types):
          select = select.filter(column_a == columnValue)
        else:
          self.log.error("type(columnValue) == %s" % type(columnValue))

      res = select.first()  # the selection is done via primaryKeys only
      if not res:  # if not there, let's insert it (and exit)
        return self.insert(table, params)

      # From now on, we assume we need to modify

      # Treating case of time value updates
      if not params.get('LastCheckTime'):
        params['LastCheckTime'] = None
      if not params.get('DateEffective'):
        params['DateEffective'] = None

      # Should we change DateEffective?
      changeDE = False
      if params.get('Status'):
        if params.get('Status') != res.status:  # we update dateEffective iff we change the status
          changeDE = True

      for columnName, columnValue in params.items():
        if columnName == 'LastCheckTime' and not columnValue:  # we always update lastCheckTime
          columnValue = datetime.datetime.utcnow().replace(microsecond=0)
        if changeDE and columnName == 'DateEffective' and not columnValue:
          columnValue = datetime.datetime.utcnow().replace(microsecond=0)
        if columnValue:
          if isinstance(columnValue, datetime.datetime):
            columnValue = columnValue.replace(microsecond=0)
          setattr(res, columnName.lower(), columnValue)
      session.commit()

      # and since we modified, we now insert a new line in the log table
      return self.insert(table.replace('Status', '') + 'Log', params)
      # The line inserted will maybe become a History line thanks to the SummarizeLogsAgent

    except exc.SQLAlchemyError as e:
      session.rollback()
      self.log.exception("addOrModify: unexpected exception", lException=e)
      return S_ERROR("addOrModify: unexpected exception %s" % e)
    finally:
      session.close()

  def addIfNotThere(self, table, params):
    """
    Using the PrimaryKeys of the table, it looks for the record in the database.
    If it is not there, it is inserted as a new entry.

    :param table: table where to add or modify
    :type table: str
    :param params: dictionary of what to add or modify
    :type params: dict

    :return: S_OK() || S_ERROR()
    """

    session = self.sessionMaker_o()
    table_c = getattr(__import__(__name__, globals(), locals(), [table]), table)
    primaryKeys = [key.name for key in class_mapper(table_c).primary_key]

    try:
      select = Query(table_c, session=session)
      for columnName, columnValue in params.items():
        if not columnValue or columnName not in primaryKeys:
          continue
        column_a = getattr(table_c, columnName.lower())
        if isinstance(columnValue, (list, tuple)):
          select = select.filter(column_a.in_(list(columnValue)))
        elif isinstance(columnValue, six.string_types):
          select = select.filter(column_a == columnValue)
        else:
          self.log.error("type(columnValue) == %s" % type(columnValue))

      res = select.first()  # the selection is done via primaryKeys only
      if not res:  # if not there, let's insert it
        return self.insert(table, params)

      session.commit()
      return S_OK()

    except exc.SQLAlchemyError as e:
      session.rollback()
      self.log.exception("addIfNotThere: unexpected exception", lException=e)
      return S_ERROR("addIfNotThere: unexpected exception %s" % e)
    finally:
      session.close()
