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

import requests

import trytond.config as config
from trytond.cache import Cache
from trytond.i18n import gettext
from trytond.model import (
    MatchMixin, ModelSQL, ModelView, fields, sequence_ordered)
from trytond.pool import Pool, PoolMeta
from trytond.protocols.wrappers import HTTPStatus
from trytond.pyson import Eval, If

from .exceptions import SendcloudCredentialWarning, SendcloudError

SENDCLOUD_API_URL = 'https://panel.sendcloud.sc/api/v2/'
HEADERS = {
    'Sendcloud-Partner-Id': '03c1facb-63da-4bb1-889c-192fc91ec4e6',
    }


def sendcloud_api(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        nb_tries, error_message = 0, ''
        try:
            while nb_tries < 5:
                try:
                    return func(*args, **kwargs)
                except requests.HTTPError as e:
                    if e.response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
                        error_message = e.args[0]
                        nb_tries += 1
                        time.sleep(1)
                    else:
                        raise
        except requests.HTTPError as e:
            error_message = e.args[0]
        raise SendcloudError(
            gettext('stock_package_shipping_sendcloud'
                '.msg_sendcloud_webserver_error',
                message=error_message))
    return wrapper


class CredentialSendcloud(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
    __name__ = 'carrier.credential.sendcloud'

    company = fields.Many2One('company.company', "Company")
    public_key = fields.Char("Public Key", required=True, strip=False)
    secret_key = fields.Char("Secret Key", required=True, strip=False)

    addresses = fields.One2Many(
        'carrier.sendcloud.address', 'sendcloud', "Addresses",
        states={
            'readonly': ~Eval('id') | (Eval('id', -1) < 0),
            })
    shipping_methods = fields.One2Many(
        'carrier.sendcloud.shipping_method', 'sendcloud', "Methods",
        states={
            'readonly': ~Eval('id') | (Eval('id', -1) < 0),
            })

    _addresses_sender_cache = Cache(
        'carrier.credential.sendcloud.addresses_sender',
        duration=config.getint(
            'stock_package_shipping_sendcloud', 'addresses_cache',
            default=15 * 60),
        context=False)
    _shiping_methods_cache = Cache(
        'carrier.credential.sendcloud.shipping_methods',
        duration=config.getint(
            'stock_package_shipping_sendcloud', 'shipping_methods_cache',
            default=60 * 60))

    @property
    def auth(self):
        return self.public_key, self.secret_key

    @property
    @sendcloud_api
    def addresses_sender(self):
        addresses = self._addresses_sender_cache.get(self.id)
        if addresses is not None:
            return addresses
        timeout = config.getfloat(
            'stock_package_shipping_sendcloud', 'requests_timeout',
            default=300)
        response = requests.get(
            SENDCLOUD_API_URL + 'user/addresses/sender',
            auth=self.auth, timeout=timeout, headers=HEADERS)
        response.raise_for_status()
        addresses = response.json()['sender_addresses']
        return self._addresses_sender_cache.set(self.id, addresses)

    def get_sender_address(self, shipment_or_warehouse, pattern=None):
        pattern = pattern.copy() if pattern is not None else {}
        if shipment_or_warehouse.__name__ == 'stock.location':
            warehouse = shipment_or_warehouse
            pattern['warehouse'] = warehouse.id
        else:
            shipment = shipment_or_warehouse
            pattern['warehouse'] = shipment.shipping_warehouse.id
        for address in self.addresses:
            if address.match(pattern):
                return int(address.address) if address.address else None

    @sendcloud_api
    def get_shipping_methods(
            self, sender_address=None, service_point=None, is_return=False):
        key = (self.id, sender_address, service_point, is_return)
        methods = self._shiping_methods_cache.get(key)
        if methods is not None:
            return methods
        params = {}
        if sender_address:
            params['sender_address'] = sender_address
        if service_point:
            params['service_point'] = service_point
        if is_return:
            params['is_return'] = is_return
        timeout = config.getfloat(
            'stock_package_shipping_sendcloud', 'requests_timeout',
            default=300)
        response = requests.get(
            SENDCLOUD_API_URL + 'shipping_methods', params=params,
            auth=self.auth, timeout=timeout, headers=HEADERS)
        response.raise_for_status()
        methods = response.json()['shipping_methods']
        return self._shiping_methods_cache.set(key, methods)

    def get_shipping_method(self, shipment, package=None):
        pattern = self._get_shipping_method_pattern(shipment, package=package)
        for method in self.shipping_methods:
            if method.match(pattern):
                if method.shipping_method:
                    return int(method.shipping_method)
                else:
                    return None

    @classmethod
    def _get_shipping_method_pattern(cls, shipment, package=None):
        pool = Pool()
        UoM = pool.get('product.uom')
        ModelData = pool.get('ir.model.data')
        kg = UoM(ModelData.get_id('product', 'uom_kilogram'))

        if package:
            weight = UoM.compute_qty(
                package.weight_uom, package.total_weight, kg, round=False)
        else:
            weight = UoM.compute_qty(
                shipment.weight_uom, shipment.weight, kg, round=False)
        return {
            'carrier': shipment.carrier.id if shipment.carrier else None,
            'weight': weight,
            }

    @sendcloud_api
    def get_parcel(self, id):
        timeout = config.getfloat(
            'stock_package_shipping_sendcloud', 'requests_timeout',
            default=300)
        response = requests.get(
            SENDCLOUD_API_URL + 'parcels/%s' % id,
            auth=self.auth, timeout=timeout, headers=HEADERS)
        response.raise_for_status()
        return response.json()['parcel']

    @sendcloud_api
    def create_parcels(self, parcels):
        timeout = config.getfloat(
            'stock_package_shipping_sendcloud', 'requests_timeout',
            default=300)
        response = requests.post(
            SENDCLOUD_API_URL + 'parcels', json={'parcels': parcels},
            auth=self.auth, timeout=timeout, headers=HEADERS)
        if response.status_code == HTTPStatus.BAD_REQUEST:
            msg = response.json()['error']['message']
            raise requests.HTTPError(msg, response=response)
        response.raise_for_status()
        return response.json()['parcels']

    @sendcloud_api
    def get_label(self, url):
        timeout = config.getfloat(
            'stock_package_shipping_sendcloud', 'requests_timeout',
            default=300)
        response = requests.get(
            url, auth=self.auth, timeout=timeout, headers=HEADERS)
        response.raise_for_status()
        return response.content

    @classmethod
    def check_modification(
            cls, mode, credentials, values=None, external=False):
        pool = Pool()
        Warning = pool.get('res.user.warning')
        super().check_modification(
            mode, credentials, values=values, external=external)
        if (mode == 'write'
                and external
                and values.keys() & {'public_key', 'secret_key'}):
            warning_name = Warning.format(
                'sendcloud_credential', credentials)
            if Warning.check(warning_name):
                raise SendcloudCredentialWarning(
                    warning_name,
                    gettext('stock_package_shipping_sendcloud'
                        '.msg_sendcloud_credential_modified'))


class SendcloudAddress(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
    __name__ = 'carrier.sendcloud.address'

    sendcloud = fields.Many2One(
        'carrier.credential.sendcloud', "Sendcloud", required=True)
    warehouse = fields.Many2One(
        'stock.location', "Warehouse",
        domain=[
            ('type', '=', 'warehouse'),
            ])
    address = fields.Selection(
        'get_addresses', "Address",
        help="Leave empty for the Sendcloud default.")

    @fields.depends('sendcloud', '_parent_sendcloud.id')
    def get_addresses(self):
        addresses = [('', "")]
        if (self.sendcloud
                and self.sendcloud.id is not None
                and self.sendcloud.id >= 0):
            for address in self.sendcloud.addresses_sender:
                addresses.append(
                    (str(address['id']), self._format_address(address)))
        return addresses

    @classmethod
    def _format_address(cls, address):
        return ', '.join(
            filter(None, [
                    address.get('company_name'),
                    address.get('street'),
                    address.get('house_number'),
                    address.get('postal_code'),
                    address.get('city'),
                    address.get('country')]))


class SendcloudShippingMethod(
        sequence_ordered(), ModelSQL, ModelView, MatchMixin):
    __name__ = 'carrier.sendcloud.shipping_method'

    sendcloud = fields.Many2One(
        'carrier.credential.sendcloud', "Sendcloud", required=True)
    carrier = fields.Many2One(
        'carrier', "Carrier",
        domain=[
            ('shipping_service', '=', 'sendcloud'),
            ])
    warehouse = fields.Many2One(
        'stock.location', "Warehouse",
        domain=[
            ('type', '=', 'warehouse'),
            ])
    min_weight = fields.Float(
        "Minimal Weight",
        domain=[
            ['OR',
                ('min_weight', '=', None),
                ('min_weight', '>', 0),
                ],
            If(Eval('max_weight', 0),
                ('min_weight', '<=', Eval('max_weight', 0)),
                ()),
            ],
        help="Minimal weight included in kg.")
    max_weight = fields.Float(
        "Maximal Weight",
        domain=[
            ['OR',
                ('max_weight', '=', None),
                ('max_weight', '>', 0),
                ],
            If(Eval('min_weight', 0),
                ('max_weight', '>=', Eval('min_weight', 0)),
                ()),
            ],
        help="Maximal weight included in kg.")
    shipping_method = fields.Selection(
        'get_shipping_methods', "Shipping Method")

    @fields.depends('sendcloud', 'warehouse', '_parent_sendcloud.id')
    def get_shipping_methods(self, pattern=None):
        methods = [(None, '')]
        if (self.sendcloud
                and self.sendcloud.id is not None
                and self.sendcloud.id >= 0):
            if self.warehouse:
                sender_address = self.sendcloud.get_sender_address(
                    self.warehouse, pattern=pattern)
            else:
                sender_address = None
            methods += [
                (str(m['id']), m['name'])
                for m in self.sendcloud.get_shipping_methods(
                    sender_address=sender_address)]
        return methods

    def match(self, pattern, match_none=False):
        pattern = pattern.copy()
        if (weight := pattern.pop('weight')) is not None:
            min_weight = self.min_weight or 0
            max_weight = self.max_weight or weight
            if not (min_weight <= weight <= max_weight):
                return False
        return super().match(pattern, match_none=match_none)


class Carrier(metaclass=PoolMeta):
    __name__ = 'carrier'

    sendcloud_format = fields.Selection([
            ('normal 0', "A4 - Top left"),
            ('normal 1', "A4 - Top right"),
            ('normal 2', "A4 - Bottom left"),
            ('normal 3', "A4 - Bottom right"),
            ('label', "A6 - Full page"),
            ], "Format",
        states={
            'invisible': Eval('shipping_service') != 'sendcloud',
            'required': Eval('shipping_service') == 'sendcloud',
            })

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.shipping_service.selection.append(('sendcloud', "Sendcloud"))

    @classmethod
    def default_sendcloud_format(cls):
        return 'label'

    @classmethod
    def view_attributes(cls):
        return super().view_attributes() + [
            ("/form/separator[@id='sendcloud']", 'states', {
                    'invisible': Eval('shipping_service') != 'sendcloud',
                    }),
            ]

    @property
    def shipping_label_mimetype(self):
        mimetype = super().shipping_label_mimetype
        if self.shipping_service == 'sendcloud':
            mimetype = 'application/pdf'
        return mimetype
