# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from itertools import chain

try:
    import phonenumbers
    from phonenumbers import NumberParseException, PhoneNumberFormat
except ImportError:
    phonenumbers = None

from trytond.i18n import gettext
from trytond.model import (
    DeactivableMixin, Index, ModelSQL, ModelView, MultiValueMixin, ValueMixin,
    fields, sequence_ordered)
from trytond.model.exceptions import AccessError
from trytond.pyson import Eval
from trytond.tools.email_ import (
    EmailNotValidError, normalize_email, validate_email)
from trytond.transaction import Transaction

from .exceptions import InvalidEMail, InvalidPhoneNumber

_TYPES = [
    ('phone', 'Phone'),
    ('mobile', 'Mobile'),
    ('fax', 'Fax'),
    ('email', 'Email'),
    ('website', 'Website'),
    ('skype', 'Skype'),
    ('sip', 'SIP'),
    ('irc', 'IRC'),
    ('jabber', 'Jabber'),
    ('other', 'Other'),
]

_PHONE_TYPES = {
    'phone',
    'mobile',
    'fax',
    }


class _ContactMechanismMixin:
    __slots__ = ()

    def contact_mechanism_get(self, types=None, usage=None):
        """
        Try to find a contact mechanism for the given types and usage, if no
        usage matches the first mechanism of the given types is returned.
        """
        default_mechanism = None
        if types:
            if isinstance(types, str):
                types = {types}
            mechanisms = [m for m in self.contact_mechanisms
                if m.type in types]
        else:
            mechanisms = self.contact_mechanisms
        if mechanisms:
            default_mechanism = mechanisms[0]
            if usage:
                for mechanism in mechanisms:
                    if getattr(mechanism, usage):
                        return mechanism
        return default_mechanism


class ContactMechanism(
        DeactivableMixin, sequence_ordered(), ModelSQL, ModelView,
        MultiValueMixin):
    __name__ = 'party.contact_mechanism'
    _rec_name = 'value'

    type = fields.Selection(_TYPES, "Type", required=True, sort=False)
    type_string = type.translated('type')
    value = fields.Char(
        "Value", required=True,
        # Add all function fields to ensure to always fill them via on_change
        depends={
            'email', 'website', 'skype', 'sip', 'other_value',
            'value_compact'})
    value_compact = fields.Char('Value Compact', readonly=True)
    name = fields.Char("Name")
    comment = fields.Text("Comment")
    party = fields.Many2One(
        'party.party', "Party", required=True, ondelete='CASCADE')
    address = fields.Many2One(
        'party.address', "Address", ondelete='CASCADE',
        domain=[
            ('party', '=', Eval('party', -1)),
            ])
    language = fields.MultiValue(
        fields.Many2One('ir.lang', "Language",
            help="Used to translate communication made "
            "using the contact mechanism.\n"
            "Leave empty for the party language."))
    languages = fields.One2Many(
        'party.contact_mechanism.language', 'contact_mechanism', "Languages")
    email = fields.Function(fields.Char('Email', states={
        'invisible': Eval('type') != 'email',
        'required': Eval('type') == 'email',
        }, depends={'value'}),
        'get_value', setter='set_value')
    website = fields.Function(fields.Char('Website', states={
        'invisible': Eval('type') != 'website',
        'required': Eval('type') == 'website',
        }, depends={'value'}),
        'get_value', setter='set_value')
    skype = fields.Function(fields.Char('Skype', states={
        'invisible': Eval('type') != 'skype',
        'required': Eval('type') == 'skype',
        }, depends={'value'}),
        'get_value', setter='set_value')
    sip = fields.Function(fields.Char('SIP', states={
        'invisible': Eval('type') != 'sip',
        'required': Eval('type') == 'sip',
        }, depends={'value'}),
        'get_value', setter='set_value')
    other_value = fields.Function(fields.Char('Value', states={
        'invisible': Eval('type').in_(['email', 'website', 'skype', 'sip']),
        'required': ~Eval('type').in_(['email', 'website']),
        }, depends={'value'}),
        'get_value', setter='set_value')
    url = fields.Function(fields.Char('URL', states={
                'invisible': ~Eval('url'),
                }),
        'on_change_with_url')

    @classmethod
    def __setup__(cls):
        cls.value.search_unaccented = False
        cls.value_compact.search_unaccented = False
        super().__setup__()
        cls.__access__.add('party')
        t = cls.__table__()
        cls._sql_indexes.add(
            Index(t, (Index.Unaccent(t.value_compact), Index.Similarity())))
        cls._order.insert(0, ('party.distance', 'ASC NULLS LAST'))
        cls._order.insert(1, ('party', 'ASC'))

    @staticmethod
    def default_type():
        return 'phone'

    @classmethod
    def default_party(cls):
        return Transaction().context.get('related_party')

    @fields.depends('address', '_parent_address.party')
    def on_change_address(self):
        if self.address:
            self.party = self.address.party

    @classmethod
    def get_value(cls, mechanisms, names):
        return dict((name, dict((m.id, m.value) for m in mechanisms))
            for name in names)

    @fields.depends('type', 'value')
    def on_change_with_url(self, name=None, value=None):
        if value is None:
            value = self.value
        if self.type == 'email':
            return 'mailto:%s' % value
        elif self.type == 'website':
            return value
        elif self.type == 'skype':
            return 'callto:%s' % value
        elif self.type == 'sip':
            return 'sip:%s' % value
        elif self.type in {'phone', 'mobile'}:
            return 'tel:%s' % value
        elif self.type == 'fax':
            return 'fax:%s' % value
        return None

    @fields.depends('party', '_parent_party.addresses')
    def _phone_country_codes(self):
        if self.party:
            for address in self.party.addresses:
                if address.country:
                    yield address.country.code

    @fields.depends(methods=['_phone_country_codes'])
    def _parse_phonenumber(self, value):
        for country_code in chain(self._phone_country_codes(), [None]):
            try:
                # Country code is ignored if value has an international prefix
                phonenumber = phonenumbers.parse(value, country_code)
            except NumberParseException:
                pass
            else:
                if phonenumbers.is_valid_number(phonenumber):
                    return phonenumber
        return None

    @fields.depends(methods=['_parse_phonenumber'])
    def format_value(self, value=None, type_=None):
        if phonenumbers and type_ in _PHONE_TYPES:
            phonenumber = self._parse_phonenumber(value)
            if phonenumber:
                value = phonenumbers.format_number(
                    phonenumber, PhoneNumberFormat.INTERNATIONAL)
        if type_ == 'email' and value:
            value = normalize_email(value)
        return value

    @fields.depends(methods=['_parse_phonenumber'])
    def format_value_compact(self, value=None, type_=None):
        if phonenumbers and type_ in _PHONE_TYPES:
            phonenumber = self._parse_phonenumber(value)
            if phonenumber:
                value = phonenumbers.format_number(
                    phonenumber, PhoneNumberFormat.E164)
        return value

    @classmethod
    def set_value(cls, mechanisms, name, value):
        #  Setting value is done by on_changes
        pass

    @fields.depends(
        methods=['on_change_with_url', 'format_value', 'format_value_compact'])
    def _change_value(self, value, type_):
        self.value = self.format_value(value=value, type_=type_)
        self.value_compact = self.format_value_compact(
            value=value, type_=type_)
        self.website = value
        self.email = value
        self.skype = value
        self.sip = value
        self.other_value = value
        self.url = self.on_change_with_url(value=value)

    @fields.depends('value', 'type', methods=['_change_value'])
    def on_change_value(self):
        return self._change_value(self.value, self.type)

    @fields.depends('website', 'type', methods=['_change_value'])
    def on_change_website(self):
        return self._change_value(self.website, self.type)

    @fields.depends('email', 'type', methods=['_change_value'])
    def on_change_email(self):
        return self._change_value(self.email, self.type)

    @fields.depends('skype', 'type', methods=['_change_value'])
    def on_change_skype(self):
        return self._change_value(self.skype, self.type)

    @fields.depends('sip', 'type', methods=['_change_value'])
    def on_change_sip(self):
        return self._change_value(self.sip, self.type)

    @fields.depends('other_value', 'type', methods=['_change_value'])
    def on_change_other_value(self):
        return self._change_value(self.other_value, self.type)

    def get_rec_name(self, name):
        name = self.name or self.party.rec_name
        return '%s <%s>' % (name, self.value_compact or self.value)

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
            ('value',) + tuple(clause[1:]),
            ('value_compact',) + tuple(clause[1:]),
            ]

    def compute_fields(self, field_names=None):
        values = super().compute_fields(field_names=field_names)
        if field_names is None or {'value', 'type'} & field_names:
            if getattr(self, 'value', None) and getattr(self, 'type', None):
                value = self.format_value(value=self.value, type_=self.type)
                if self.value != value:
                    values['value'] = value
                value_compact = self.format_value_compact(
                    value=self.value, type_=self.type)
                if getattr(self, 'value_compact', None) != value_compact:
                    values['value_compact'] = value_compact
        return values

    @classmethod
    def check_modification(cls, mode, mechanisms, values=None, external=False):
        super().check_modification(
            mode, mechanisms, values=values, external=external)
        if mode == 'write' and 'party' in values:
            for mechanism in mechanisms:
                if mechanism.party.id != values['party']:
                    raise AccessError(gettext(
                            'party.msg_contact_mechanism_change_party',
                            contact=mechanism.rec_name))

    @classmethod
    def validate_fields(cls, mechanisms, field_names):
        super().validate_fields(mechanisms, field_names)
        cls.check_valid_phonenumber(mechanisms, field_names)
        cls.check_valid_email(mechanisms, field_names)

    @classmethod
    def check_valid_phonenumber(cls, mechanisms, field_names=None):
        if field_names and not (field_names & {'type', 'value'}):
            return
        if not phonenumbers:
            return
        for mechanism in mechanisms:
            if mechanism.type not in _PHONE_TYPES:
                continue
            if not mechanism._parse_phonenumber(mechanism.value):
                raise InvalidPhoneNumber(
                    gettext('party.msg_invalid_phone_number',
                        phone=mechanism.value, party=mechanism.party.rec_name))

    @classmethod
    def check_valid_email(cls, mechanisms, field_names=None):
        if field_names and not (field_names & {'type', 'value'}):
            return
        for mechanism in mechanisms:
            if mechanism.type == 'email' and mechanism.value:
                try:
                    validate_email(mechanism.value)
                except EmailNotValidError as e:
                    raise InvalidEMail(gettext(
                            'party.msg_email_invalid',
                            email=mechanism.value,
                            party=mechanism.party.rec_name),
                        str(e)) from e

    @classmethod
    def usages(cls, _fields=None):
        "Returns the selection list of usage"
        usages = [(None, "")]
        if _fields:
            for name, desc in cls.fields_get(_fields).items():
                usages.append((name, desc['string']))
        return usages

    @fields.depends(methods=['_notify_duplicate'])
    def on_change_notify(self):
        notifications = super().on_change_notify()
        notifications.extend(self._notify_duplicate())
        return notifications

    @fields.depends('value', 'type', methods=['format_value_compact'])
    def _notify_duplicate(self):
        cls = self.__class__
        if self.value and self.type:
            value_compact = self.format_value_compact(self.value, self.type)
            others = cls.search([
                    ('id', '!=', self.id),
                    ('type', '=', self.type),
                    ('value_compact', '=', value_compact),
                    ], limit=1)
            if others:
                other, = others
                yield ('warning', gettext(
                        'party.msg_party_contact_mechanism_duplicate',
                        party=other.party.rec_name,
                        type=other.type_string,
                        value=other.value))


class ContactMechanismLanguage(ModelSQL, ValueMixin):
    __name__ = 'party.contact_mechanism.language'
    contact_mechanism = fields.Many2One(
        'party.contact_mechanism', "Contact Mechanism",
        ondelete='CASCADE')
    language = fields.Many2One('ir.lang', "Language")
