"""
$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/lib/inquiry.py $
$Id: inquiry.py 31290 2008-11-17 14:59:38Z dbinger $
"""
from dulcinea.attachable import Attachable
from dulcinea.base import DulcineaPersistent
from dulcinea.contact import Contact
from dulcinea.interaction import Interactable
from dulcinea.partition import PartKeyed, Partition
from dulcinea.timestamped import Timestamped, reverse_timestamp_sorted
from dulcinea.user import get_matching_user
from dulcinea.util import html2txt, StringIO
from qp.lib.spec import add_getters_and_setters, specify, string, match
from qp.mail.send import sendmail
from qp.mail.rfc822_mailbox import parseaddr
from qp.pub.common import get_publisher
import email
import re
import sys


inquiry_re = re.compile(r"(i|inq|inquiry)(\s*#?0*|-)(?P<key>\d+)", re.I)

def get_inquiry_db():
    return get_publisher().get_connection().get_root().get('inquiry_db')


class Inquiry(DulcineaPersistent, PartKeyed, Timestamped, Interactable):

    msg_is = string
    text_is = string
    title_is = string
    from_field_is = string
    message_id_is = string
    user_is = (None, Contact)
    contact_is = (None, Contact)
    previous_inquiry_is = None # forward declaration

    def __init__(self, raw_msg):
        assert raw_msg, "Cannot instantiate an Inquiry with an empty string"
        Interactable.__init__(self)
        Timestamped.__init__(self)
        PartKeyed.__init__(self)
        self.set_msg(raw_msg)
        parsed_msg = self.parse()
        content_type = parsed_msg.get_content_type()
        if parsed_msg.is_multipart():
            text = '\n'.join(
                [sub_msg.get_payload() for sub_msg in parsed_msg.walk()
                 if sub_msg.get_content_type() == 'text/plain'])
            if not text:
               text = html2txt('\n'.join(
                   [sub_msg.get_payload() for sub_msg in parsed_msg.walk()
                    if sub_msg.get_content_type() == 'text/html']))
            if not text:
                text = ('Multipart message with no "text/plain" '
                        'or "text/html" parts')
        elif content_type == 'text/html':
            text = html2txt(parsed_msg.get_payload())
        elif content_type == 'text/plain':
            text = parsed_msg.get_payload()
        elif content_type and content_type.startswith('multipart/'):
            text = 'Badly formed multipart message'
        else:
            text = ('Bad Content type "%s", want ' % content_type +
                    '"multipart/alternative", "text/plain" or "text/html"')
        self.set_text(text)
        self.set_from_field(parsed_msg.get('from', ''))
        subject = parsed_msg.get('subject')
        self.set_title(subject or 'No Subject')
        self.set_user(get_matching_user(self.get_from_address()))
        self.set_contact(None)
        self.set_message_id(parsed_msg.get('message-id', ''))
        self.previous_inquiry = None

    def __eq__(self, other):
        if not type(other) is Inquiry:
            return False
        return (self.get_text() == other.get_text() and
                self.get_title() == other.get_title() and
                self.get_from_field() == other.get_from_field())

    def __hash__(self):
        return id(self)

    def __str__(self):
        return ("inquiry-%s: <%s> %s" % (self.get_key(),
                                         self.get_from_address(),
                                         self.get_title()))[:70]

    def set_msg(self, msg):
        """(msg : unicode_string | byte_string)
        """
        if not match(msg, self.msg_is):
            try:
                msg = msg.decode(self._get_charset(self._parse(msg)))
            except (LookupError, UnicodeDecodeError):
                try:
                    msg = msg.decode('utf8')
                except UnicodeDecodeError:
                    msg = msg.decode('latin1')
        specify(self, msg=msg)

    def set_previous_inquiry(self, previous_inquiry):
        seen = set([self, previous_inquiry])
        if previous_inquiry is self:
            raise ValueError('Previous Inquiry cannot be the same Inquiry')
        inquiry = previous_inquiry
        while 1:
            next = inquiry.get_previous_inquiry()
            if next is None:
                break
            if next in seen:
                raise ValueError('Previous Inquiry cycle detected')
            seen.add(next)
            inquiry = next
        self.previous_inquiry = previous_inquiry

    def get_from_address(self):
        return parseaddr(self.get_from_field())[1]

    def get_address_fields(self):
        parsed = self.parse()
        addresses = [parsed.get(field) for field in ['From', 'To']]
        if parsed.is_multipart(): # try to dig addresses out of forwarded msgs
            for msg in parsed.get_payload():
                if msg.get_content_type() == 'text/plain':
                    possible_forwarded_msg = msg.get_payload()
                    lines = possible_forwarded_msg.split('\n> ')
                    for line in lines:
                        if line.startswith('From:'):
                            addresses.append(line[6:])
        return sorted(set(addresses))

    def _get_key_from_email_header(self, email_header):
        """(email_header : string) -> int | None
        """
        real_name, email_address = parseaddr(email_header)
        if '@' in email_address:
            local_part, domain = email_address.split('@')
        else:
            local_part = email_address
        if '-' in local_part:
            try:
                return int(local_part.split('-')[1])
            except ValueError:
                pass
        return None

    def get_key_from_title(self):
        m = inquiry_re.search(self.get_title() or '')
        if m:
            try:
                return int(m.group('key'))
            except ValueError:
                pass
        return None

    def get_original_inquiry(self):
        """() -> Inquiry | None
        """
        inquiry_db = get_inquiry_db()

        # Search for relations specified in the Subject line
        key = self.get_key_from_title()
        if key is not None:
            inquiry = inquiry_db.get(key)
            if inquiry:
                return inquiry

        # Search for relations of the form "inquiry-#@domain"
        for header in ['to', 'cc']:
            parsed_header = self.get(header)
            if parsed_header:
                for address_field in parsed_header.split(','):
                    key = self._get_key_from_email_header(address_field)
                    if key is not None:
                        inquiry = inquiry_db.get(key)
                        if inquiry:
                            return inquiry

        # Search for inquiry with the new inquiry's In-Reply-To message ID
        in_reply_to = self.get('in-reply-to')
        if in_reply_to:
            inquiry = inquiry_db.get_inquiry_with_message_id(in_reply_to)
            if inquiry:
                return inquiry

        # If this is a forwarded message, search for inquiry with
        # message ID matching that of the enclosed forwarded message
        message_ids = set()
        parsed_msg = self.parse()
        if parsed_msg.is_multipart():
            for sub_msg in parsed_msg.walk():
                message_id = sub_msg.get('message-id')
                if message_id:
                    message_ids.add(message_id)
        for message_id in message_ids:
            if message_id != self.get_message_id():
                inquiry = inquiry_db.get_inquiry_with_message_id(message_id)
                if inquiry:
                    return inquiry

        return None

    def record_interaction(self, user, email, text):
        if not self.get_contact():
            self.set_contact(user)
        return Interactable.record_interaction(self, user, email, text)

    def get(self, header, default=None):
        return self.parse().get(header, default)

    def _get_charset(self, msg):
        """(msg : email.Message)
        """
        if msg.get_charset():
            return msg.get_charset()
        if msg.get_charsets():
            return msg.get_charsets()[0] or ''
        return ''

    def get_charset(self):
        return self._get_charset(self.parse())

    def get_title(self):
        return self.title or self.get_key()

    def has_interactions(self):
        return bool(len(self.get_interaction_history()))

    def is_closed(self):
        return self.get_part_key() == 'closed'

    def is_open(self):
        return self.get_part_key() == 'open'

    def _parse(self, raw_msg):
        """(raw_msg : string) -> email.Message
        """
        return email.message_from_file(StringIO(raw_msg))

    def parse(self):
        """() -> email.Message
        """
        return self._parse(self.get_msg())

Inquiry.previous_inquiry_is = (None, Inquiry)
add_getters_and_setters(Inquiry)


class InquiryDatabase (Partition, Attachable):

    def __init__(self):
        Partition.__init__(self, value_spec=Inquiry,
                           part_key_spec=('open', 'closed'))
        Attachable.__init__(self)

    def get_inquiries(self, sort=True):
        """() -> [Inquiry]
        """
        inquiries = self.itervalues()
        if sort:
            inquiries = reverse_timestamp_sorted(inquiries)
        return list(inquiries)

    def get_inquiry_with_message_id(self, message_id):
        """() -> Inquiry | None

        """
        if message_id:
            for inquiry in self.get_inquiries():
                if inquiry.get_message_id() == message_id:
                    return inquiry
        return None

    def add_inquiry_from_string(self, raw_msg):
        """() -> int

        Add a new Inquiry to InquiryDatabase or add an interaction to an
        existing inquiry if the inquiry is a followup to a previous inquiry.
        """
        new_inquiry = Inquiry(raw_msg)
        unixfrom = new_inquiry.parse().get_unixfrom()
        webmaster_address = get_publisher().get_webmaster_address()
        if not unixfrom:
            sendmail("No unixfrom in inquiry: " + new_inquiry.get_title(),
                     new_inquiry.get_msg(),
                     [webmaster_address],
                     from_addr=webmaster_address)
            return -1
        if get_publisher().get_feedback_domain() not in unixfrom:
            sendmail("Non-local inquiry: " + new_inquiry.get_title(),
                     new_inquiry.get_msg(),
                     [webmaster_address],
                     from_addr=webmaster_address)
            return -1
        if (self.get_part('open') and
            new_inquiry == self.get_part('open').get_max_item()[1]):
            return -1
        spam_status = new_inquiry.get('X-Spam-Status')
        spam_tags = new_inquiry.get('X-Spam-Tags')
        if spam_tags or spam_status and 'SPAM' in spam_status:
            sendmail("Spam inquiry: " + new_inquiry.get_title(),
                     new_inquiry.get_msg(),
                     [webmaster_address],
                     from_addr=webmaster_address)
            return -1
        original_inquiry = new_inquiry.get_original_inquiry()
        if original_inquiry:
            text = ''
            user = get_matching_user(new_inquiry.get_from_address())
            if not user:
                text = 'From: ' + new_inquiry.get_from_field() + '\n\n'
            text += new_inquiry.get_text()
            original_inquiry.record_interaction(user, Inquiry.EMAIL, text)
            return original_inquiry.get_key()
        else:
            self.add(new_inquiry, 'open')
            return new_inquiry.get_key()

