# -*- coding: utf-8 -*-
# Stalker a Production Asset Management System
# Copyright (C) 2009-2013 Erkan Ozgur Yilmaz
#
# This file is part of Stalker.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

import uuid

from sqlalchemy.exc import UnboundExecutionError
from sqlalchemy.orm import synonym, relationship
from sqlalchemy.orm.mapper import validates
from sqlalchemy import Column, Integer, String
from sqlalchemy.schema import ForeignKey, Table
from sqlalchemy.types import Enum

from stalker.db.declarative import Base
from stalker.models.entity import Entity, SimpleEntity
from stalker.models.mixins import StatusMixin

from stalker import defaults

from stalker.log import logging_level
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging_level)

# RESOLUTIONS
FIXED = 'fixed'
INVALID = 'invalid'
WONTFIX = 'wontfix'
DUPLICATE = 'duplicate'
WORKSFORME = 'worksforme'
CANTFIX = 'cantfix'


class Ticket(Entity, StatusMixin):
    """Tickets are the way of reporting errors or asking for changes in Stalker.

    The Stalker Ticketing system is based on Trac Basic Workflow. For more
    information please visit `Trac Workflow`_

    _`Trac Workflow`:: http://trac.edgewall.org/wiki/TracWorkflow

    Stalker Ticket system is very flexible, to customize the workflow please
    update the :class:`~stalker.config.Config.ticket_workflow` dictionary.

    In the default setup, there are four actions available; ``accept``,
    ``resolve``, ``reopen``, ``reassign``, and five statuses available ``New``,
    ``Assigned``, ``Accepted``, ``Reopened``, ``Closed``.

    :param project: The Project that this Ticket is assigned to. A Ticket in
        Stalker must be assigned to a Project. ``project`` argument can not be
        skipped or can not be None.

    :type project: :class:`~stalker.models.project.Project`

    :param str summary: A string which contains the title or a short
        description of this Ticket.

    :param enum priority: The priority of the Ticket which is an enum value.
        Possible values are:

          +--------------+-------------------------------------------------+
          | 0 / TRIVIAL  | defect with little or no impact / cosmetic      |
          |              | enhancement                                     |
          +--------------+-------------------------------------------------+
          | 1 / MINOR    | defect with minor impact / small enhancement    |
          +--------------+-------------------------------------------------+  
          | 2 / MAJOR    | defect with major impact / big enhancement      |
          +--------------+-------------------------------------------------+ 
          | 3 / CRITICAL | severe loss of data due to the defect or highly |
          |              | needed enhancement                              |
          +--------------+-------------------------------------------------+
          | 4 / BLOCKER  | basic functionality is not available until this |
          |              | is fixed                                        |
          +--------------+-------------------------------------------------+

    :param reported_by: An instance of :class:`~stalker.models.user.User` who
        created this Ticket. It is basically a synonym for the
        :attr:`~stalker.models.entity.SimpleEntity.created_by` attribute.

    Changing the :class:`~stalker.models.ticket.Ticket`\ .\ 
    :attr`~stalker.models.ticket.Ticket.status` will create a new
    :class:`~stalker.models.Ticket.TicketLog` instance showing the previous
    operation.

    Even though Tickets needs statuses they don't need to be supplied a
    :class:`~stalker.models.status.StatusList` nor
    :class:`~stalker.models.status.Status` for the Tickets. It will be
    automatically filled accordingly. For newly created Tickets the status of
    the ticket is ``NEW`` and can be changed to other statuses as follows:

        Status -> Action -> New Status

        NEW      -> resolve  -> CLOSED
        NEW      -> accept   -> ACCEPTED
        NEW      -> reassign -> ASSIGNED

        ASSIGNED -> resolve  -> CLOSED
        ASSIGNED -> accept   -> ACCEPTED
        ASSIGNED -> reassign -> ASSIGNED

        ACCEPTED -> resolve  -> CLOSED
        ACCEPTED -> accept   -> ACCEPTED
        ACCEPTED -> reassign -> ASSIGNED

        REOPENED -> resolve  -> CLOSED
        REOPENED -> accept   -> ACCEPTED
        REOPENED -> reassign -> ASSIGNED

        CLOSED   -> reopen   -> REOPENED

        actions available:
        resolve
        reassign
        accept
        reopen

    The :attr:`~stalker.models.ticket.Ticket.name` is automatically generated
    by using the ``stalker.config.Config.ticket_label`` attribute and
    :attr:`~stalker.models.ticket.Ticket.ticket_number`\ . So if defaults are
    used the first ticket name will be "Ticket#1" and the second "Ticket#2" and
    so on. For every project the number will restart from 1.

    Use the :meth:`~stalker.models.ticket.Ticket.resolve`,
    :meth:`~stalker.models.ticket.Ticket.reassign`,
    :meth:`~stalker.models.ticket.Ticket.accept`,
    :meth:`~stalker.models.ticket.Ticket.reopen` methods to change the status
    of the current Ticket.

    Changing the status of the Ticket will create
    :class:`~stalker.models.ticket.TicketLog` entries reflecting the change
    made.
    """

    # logs attribute
    __auto_name__ = True
    __tablename__ = "Tickets"
    #__table_args__ = (
    #    UniqueConstraint("project_id", 'number'), {}
    #)
    __mapper_args__ = {"polymorphic_identity": "Ticket"}

    ticket_id = Column(
        "id", Integer, ForeignKey("Entities.id"), primary_key=True
    )

    project_id = Column('project_id', Integer, ForeignKey('Projects.id'),
                        nullable=False)

    _project = relationship(
        'Project',
        primaryjoin='Tickets.c.project_id==Projects.c.id',
        back_populates='tickets'
    )

    _number = Column(
        'number',
        Integer,
        autoincrement=True,
        default=1,
        nullable=False,
        unique=True,
    )

    related_tickets = relationship(
        'Ticket',
        secondary='Ticket_Related_Tickets',
        primaryjoin='Tickets.c.id==Ticket_Related_Tickets.c.ticket_id',
        secondaryjoin='Ticket_Related_Tickets.c.related_ticket_id==Tickets.c.id',
        doc="""A list of other Ticket instances which are related
        to this one. Can be used to related Tickets to point to a common
        problem. The Ticket itself can not be assigned to this list
        """
    )

    links = relationship(
        'SimpleEntity',
        secondary='Ticket_SimpleEntities'
    )

    comments = synonym('notes',
                       doc="""A list of :class:`~stalker.models.note.Note` instances showing
        the comments made for this Ticket instance. It is a synonym for the
        :attr:`~stalker.models.ticket.Ticket.notes` attribute.
        """
    )

    reported_by = synonym('created_by', doc="Shows who created this Ticket")

    owner_id = Column('owner_id', Integer, ForeignKey('Users.id'))
    owner = relationship(
        'User',
        primaryjoin='Tickets.c.owner_id==Users.c.id'
    )

    resolution = Column(String(128))

    priority = Column(
        Enum('TRIVIAL', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER',
             name='PriorityType'),
        default='TRIVIAL',
        doc="""The priority of the Ticket which is an enum value.
        Possible values are:

          +--------------+-------------------------------------------------+
          | 0 / TRIVIAL  | defect with little or no impact / cosmetic      |
          |              | enhancement                                     |
          +--------------+-------------------------------------------------+
          | 1 / MINOR    | defect with minor impact / small enhancement    |
          +--------------+-------------------------------------------------+  
          | 2 / MAJOR    | defect with major impact / big enhancement      |
          +--------------+-------------------------------------------------+ 
          | 3 / CRITICAL | severe loss of data due to the defect or highly |
          |              | needed enhancement                              |
          +--------------+-------------------------------------------------+
          | 4 / BLOCKER  | basic functionality is not available until this |
          |              | is fixed                                        |
          +--------------+-------------------------------------------------+
        """
    )

    def __init__(self, project=None, links=None, priority='TRIVIAL', **kwargs):
        # just force auto name generation
        self._number = self._generate_ticket_number()
        kwargs['name'] = defaults.ticket_label + ' #%i' % self.number

        super(Ticket, self).__init__(**kwargs)
        StatusMixin.__init__(self, **kwargs)

        self._project = project

        self.priority = priority
        if links is None:
            links = []
        self.links = links

    def _number_getter(self):
        """returns the number attribute
        """
        return self._number

    number = synonym(
        '_number',
        descriptor=property(_number_getter),
        doc="""The automatically generated number for the tickets.
        """
    )

    def _project_getter(self):
        """returns the project attribute
        """
        return self._project

    project = synonym(
        '_project',
        descriptor=property(_project_getter)
    )

    def _maximum_number(self):
        """returns the maximum available number from the database

        :return: integer
        """
        try:
            # do your query
            max_ticket = Ticket.query \
                .order_by(Ticket.number.desc()) \
                .first()
        except UnboundExecutionError:
            max_ticket = None

        return max_ticket.number if max_ticket is not None else 0

    def _generate_ticket_number(self):
        """auto generates a number for the ticket

        :return: integer
        """
        # TODO: try to make it atomic
        return self._maximum_number() + 1

    @validates('related_tickets')
    def _validate_related_tickets(self, key, related_ticket):
        """validates the given related_ticket attribute
        """
        if not isinstance(related_ticket, Ticket):
            raise TypeError('%s.related_ticket attribute should be a list '
                            'of other stalker.models.ticket.Ticket instances not %s' %
                            (self.__class__.__name__,
                             related_ticket.__class__.__name__))

        if related_ticket is self:
            raise ValueError('%s.related_ticket attribute can not have '
                             'itself in the list' % self.__class__.__name__)

        return related_ticket

    @validates('_project')
    def _validate_project(self, key, project):
        """validates the given project instance
        """
        from stalker import Project

        if project is None or not isinstance(project, Project):
            raise TypeError('%s.project should be an instance of '
                            'stalker.models.project.Project, not %s' %
                            (self.__class__.__name__,
                             project.__class__.__name__))

        return project

    #Ticket.resolve(User1, 'fixed')
    #Ticket.__action__('resolve', User1, 'fixed')

    def __action__(self, action, created_by, action_arg=None):
        """updates the ticket status and creates a ticket log according to the
        Ticket.__available_actions__ dictionary

        :param str action: The name of the action
        :param stalker.models.auth.User created_by: The User creating this
            action
        """
        statuses = defaults.ticket_workflow[action].keys()
        status = self.status.code.lower()
        if status in statuses:
            action_data = defaults.ticket_workflow[action][status]
            new_status_code = action_data['new_status']
            action_name = action_data['action']

            # there is an action defined for this status
            # get the to_status
            from_status = self.status
            to_status = self.status_list[new_status_code]
            self.status = to_status

            # call the action with action_arg
            func = getattr(self, action_name)
            func(action_arg)

            # create log entry
            self.logs.append(
                TicketLog(self, from_status, to_status, action,
                          created_by=created_by)
            )

    def resolve(self, created_by=None, resolution=''):
        """resolves the ticket
        """
        self.__action__('resolve', created_by, resolution)

    def accept(self, created_by=None):
        """accepts the ticket
        """
        self.__action__('accept', created_by, created_by)

    def reassign(self, created_by=None, assign_to=None):
        """reassigns the ticket
        """
        self.__action__('reassign', created_by, assign_to)

    def reopen(self, created_by=None):
        """reopens the ticket
        """
        self.__action__('reopen', created_by)

    # actions
    def set_owner(self, *args):
        """sets owner to the given owner
        """
        self.owner = args[0]

    def set_resolution(self, *args):
        """sets the timing_resolution
        """
        self.resolution = args[0]

    def del_resolution(self, *args):
        """deletes the timing_resolution
        """
        self.resolution = ''

    def __eq__(self, other):
        """the equality operator
        """
        return super(Ticket, self).__eq__(other) and \
               isinstance(other, Ticket) and \
               other.name == self.name and \
               other.number == self.number and \
               other.status == self.status and \
               other.logs == self.logs and \
               other.priority == self.priority and \
               other.ticket_for == self.ticket_for


class TicketLog(SimpleEntity):
    """A class to hold :class:`~stalker.models.ticket.Ticket`\ .\ :attr:`~stalker.models.ticket.Ticket.status` change operations.

    :param ticket: An instance of :class:`~stalker.models.ticket.Ticket` which
        the subject to the operation.

    :type ticket: :class:`~stalker.models.ticket.Ticket`

    :param from_status: Holds a reference to a
        :class:`~stalker.models.status.Status` instance which is the previous
        status of the :class:`~stalker.models.ticket.Ticket`\ .

    :param to_status: Holds a reference to a
        :class:`~stalker.models.status.Status` instance which is the new status
        of the :class;`~stalker.models.ticket.Ticket`\ .

    :param operation: An Enumerator holding the type of the operation. Possible
        values are: RESOLVE or REOPEN

        Operations follow the `Track Workflow`_\ , 

        .. image:: http://trac.edgewall.org/chrome/common/guide/original-workflow.png
            :width: 787 px
            :height: 509 px
            :align: left

    .. _Track Workflow: http://trac.edgewall.org/wiki/TracWorkflow 
    """

    # TODO: there are no tests for the TicketLog class

    __tablename__ = 'TicketLogs'
    __mapper_args__ = {'polymorphic_identity': 'TicketLog'}

    ticket_log_id = Column('id', ForeignKey('SimpleEntities.id'),
                           primary_key=True)

    from_status_id = Column(Integer, ForeignKey('Statuses.id'))
    to_status_id = Column(Integer, ForeignKey('Statuses.id'))

    from_status = relationship(
        'Status',
        primaryjoin='TicketLogs.c.from_status_id==Statuses.c.id'
    )

    to_status = relationship(
        'Status',
        primaryjoin='TicketLogs.c.to_status_id==Statuses.c.id'
    )

    action = Column(
        Enum(*defaults.ticket_workflow.keys(), name='TicketActions')
    )

    ticket_id = Column(Integer, ForeignKey('Tickets.id'))
    ticket = relationship(
        'Ticket',
        primaryjoin='TicketLogs.c.ticket_id==Tickets.c.id',
        backref='logs'
    )

    def __init__(self,
                 ticket=None,
                 from_status=None,
                 to_status=None,
                 action=None, **kwargs):
        kwargs['name'] = 'TicketLog_' + uuid.uuid4().hex
        super(TicketLog, self).__init__(**kwargs)
        self.ticket = ticket
        self.from_status = from_status
        self.to_status = to_status
        self.action = action


# A secondary Table for Ticket to Ticket relations
Ticket_Related_Tickets = Table(
    'Ticket_Related_Tickets', Base.metadata,
    Column('ticket_id', Integer, ForeignKey('Tickets.id'), primary_key=True),
    Column('related_ticket_id', Integer, ForeignKey('Tickets.id'),
           primary_key=True),
    extend_existing=True
)

# Ticket SimpleEntity Relation, link anything to a ticket
Ticket_SimpleEntities = Table(
    'Ticket_SimpleEntities', Base.metadata,
    Column('ticket_id', Integer, ForeignKey('Tickets.id'), primary_key=True),
    Column('simple_entity_id', Integer, ForeignKey('SimpleEntities.id'),
           primary_key=True)
)
