#  """
#    Copyright (c) 2016- 2024, Wiliot Ltd. All rights reserved.
#
#    Redistribution and use of the Software in source and binary forms, with or without modification,
#     are permitted provided that the following conditions are met:
#
#       1. Redistributions of source code must retain the above copyright notice,
#       this list of conditions and the following disclaimer.
#
#       2. Redistributions in binary form, except as used in conjunction with
#       Wiliot's Pixel in a product or a Software update for such product, must reproduce
#       the above copyright notice, this list of conditions and the following disclaimer in
#       the documentation and/or other materials provided with the distribution.
#
#       3. Neither the name nor logo of Wiliot, nor the names of the Software's contributors,
#       may be used to endorse or promote products or services derived from this Software,
#       without specific prior written permission.
#
#       4. This Software, with or without modification, must only be used in conjunction
#       with Wiliot's Pixel or with Wiliot's cloud service.
#
#       5. If any Software is provided in binary form under this license, you must not
#       do any of the following:
#       (a) modify, adapt, translate, or create a derivative work of the Software; or
#       (b) reverse engineer, decompile, disassemble, decrypt, or otherwise attempt to
#       discover the source code or non-literal aspects (such as the underlying structure,
#       sequence, organization, ideas, or algorithms) of the Software.
#
#       6. If you create a derivative work and/or improvement of any Software, you hereby
#       irrevocably grant each of Wiliot and its corporate affiliates a worldwide, non-exclusive,
#       royalty-free, fully paid-up, perpetual, irrevocable, assignable, sublicensable
#       right and license to reproduce, use, make, have made, import, distribute, sell,
#       offer for sale, create derivative works of, modify, translate, publicly perform
#       and display, and otherwise commercially exploit such derivative works and improvements
#       (as applicable) in conjunction with Wiliot's products and services.
#
#       7. You represent and warrant that you are not a resident of (and will not use the
#       Software in) a country that the U.S. government has embargoed for use of the Software,
#       nor are you named on the U.S. Treasury Department’s list of Specially Designated
#       Nationals or any other applicable trade sanctioning regulations of any jurisdiction.
#       You must not transfer, export, re-export, import, re-import or divert the Software
#       in violation of any export or re-export control laws and regulations (such as the
#       United States' ITAR, EAR, and OFAC regulations), as well as any applicable import
#       and use restrictions, all as then in effect
#
#     THIS SOFTWARE IS PROVIDED BY WILIOT "AS IS" AND "AS AVAILABLE", AND ANY EXPRESS
#     OR IMPLIED WARRANTIES OR CONDITIONS, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED
#     WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, NONINFRINGEMENT,
#     QUIET POSSESSION, FITNESS FOR A PARTICULAR PURPOSE, AND TITLE, ARE DISCLAIMED.
#     IN NO EVENT SHALL WILIOT, ANY OF ITS CORPORATE AFFILIATES OR LICENSORS, AND/OR
#     ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
#     OR CONSEQUENTIAL DAMAGES, FOR THE COST OF PROCURING SUBSTITUTE GOODS OR SERVICES,
#     FOR ANY LOSS OF USE OR DATA OR BUSINESS INTERRUPTION, AND/OR FOR ANY ECONOMIC LOSS
#     (SUCH AS LOST PROFITS, REVENUE, ANTICIPATED SAVINGS). THE FOREGOING SHALL APPLY:
#     (A) HOWEVER CAUSED AND REGARDLESS OF THE THEORY OR BASIS LIABILITY, WHETHER IN
#     CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE);
#     (B) EVEN IF ANYONE IS ADVISED OF THE POSSIBILITY OF ANY DAMAGES, LOSSES, OR COSTS; AND
#     (C) EVEN IF ANY REMEDY FAILS OF ITS ESSENTIAL PURPOSE.
#  """
import datetime
import time
from multiprocessing import Queue
import requests
import json

from wiliot_core import GetApiKey, set_logger
from wiliot_api import PlatformClient, TagRole, WiliotCloudError

TIME_BTWN_REQUEST = 20  # seconds
URL_PATH = 'https://us-central1-wiliot-firefly.cloudfunctions.net/ingest-assocation-data-printer-test'
OWNER_ID = '185369804174'
GDC_TYPE = 'hvdc'  # options: outbound-pallet, hvdc
CATEGORY_ID = '1e55d376-a12f-4f75-9583-ff8bad995d35'
IS_GCP = False


class CloudAssociation(object):
    def __init__(self, associate_q, stop_event, owner_id=OWNER_ID, is_gcp=IS_GCP, category_id=CATEGORY_ID,
                 time_btwn_request=TIME_BTWN_REQUEST):
        """
        @type associate_q Queue
        @param associate_q each element of the queue is a dict {'wiliot_code': [], 'asset_code': [], 'timestamp': 0}
        """
        self.logger_path, self.logger = set_logger(app_name='CloudAssociation',
                                                   dir_name='cloud_association', file_name='cloud_association_log')
        self.associate_q = associate_q
        self.is_stop_event = stop_event
        self.time_btwn_request = time_btwn_request
        self.associate_batch = {}
        try:
            g = GetApiKey(gui_type='ttk', env='prod', owner_id=owner_id, client_type='asset')
            api_key = g.get_api_key()
            if is_gcp:
                self.client = PlatformClient(api_key=api_key, owner_id=owner_id,  env='prod', logger_=self.logger.name,
                                             cloud='GCP', region='us-central1')
            else:
                self.client = PlatformClient(api_key=api_key, owner_id=owner_id,  env='prod', logger_=self.logger.name)
        except Exception as e:
            self.logger.warning(f' Could not create a client due to {e}')
            raise Exception(f' Could not create a client due to {e}')

        try:
            self.base_payload = {"scan_source": "gdc",
                                 "pixel_id": [],
                                 "asset_id": None,
                                 "gdc_type": GDC_TYPE,
                                 "timestamp": datetime.datetime.now().timestamp(),
                                 "printer": True}
            self.category_id = category_id
        except Exception as e:
            self.logger.warning(f'Could not create the base payload due to {e}')
            raise Exception(f'Could not create the base payload due to {e}')

        if associate_q:
            self.run()

    def run(self):
        last_cloud_post = time.time()
        while not self.is_stop_event.is_set():
            if time.time() - last_cloud_post > self.time_btwn_request:
                last_cloud_post = time.time()
                if self.associate_batch:
                    self.send_batch_to_cloud()
                    self.associate_batch = {}

            if self.associate_q.empty():
                time.sleep(1)
                continue
            n = self.associate_q.qsize()
            for _ in range(n):
                data = self.associate_q.get(timeout=1)
                self.add_data_to_batch(data)

        self.stop_app()

    def stop_app(self):
        self.logger.info('Checking there are no new request to send to the cloud...')
        time.sleep(1)
        while not self.associate_q.empty():
            data = self.associate_q.get(timeout=1)
            self.add_data_to_batch(data)

        if self.associate_batch:
            self.logger.info('sending data to cloud before exiting CloudAssociation')
            bad_association = self.send_batch_to_cloud()
            if len(bad_association):
                self.logger.warning(f'association was done with the following bad association: {bad_association}')
        self.logger.info('CloudAssociation is Done')

    def add_data_to_batch(self, data):
        wiliot_code = data['wiliot_code']  # list
        asset_id = data['asset_code']
        timestamp = data['timestamp']

        if len(asset_id) > 1:
            self.logger.warning(f"Too many Asset codes were scanned: {asset_id}")
            return

        asset_id = asset_id[0] if asset_id else None
        if asset_id in self.associate_batch.keys():
            self.associate_batch[asset_id]['pixel_id'] += wiliot_code
            self.associate_batch[asset_id]['timestamp'] += timestamp
        else:
            self.associate_batch[asset_id] = {'pixel_id': wiliot_code}
        self.associate_batch[asset_id]['timestamp'] = timestamp

    def send_batch_to_cloud(self):
        bad_association = []
        payload = self.base_payload.copy()
        for asset_id, pixel_dict in self.associate_batch.items():
            payload["asset_id"] = asset_id
            payload["pixel_id"] = list(set(pixel_dict['pixel_id']))
            payload["timestamp"] = int(pixel_dict['timestamp'] * 1000)
            try:
                message = self.send_to_cloud_debug(payload)  # for debug
                if IS_GCP:
                    message = self.send_to_cloud(payload)
                else:  # AWS
                    message = self.create_asset_and_label(asset_id=payload["asset_id"], category_id=self.category_id,
                                                          pixels=payload["pixel_id"])
            except Exception as e:
                message = {'status_code': 999,
                           'data': f'Exception during send_to_cloud {e}'}

            bad_association = self.handle_results(message, asset_id, pixel_dict, bad_association)

        for b_ass in bad_association:  # try again bad association
            self.add_data_to_batch(b_ass)

        return bad_association

    def handle_results(self, message, asset_id, pixel_dict, bad_association):
        if message['status_code'] == 201:
            self.logger.info(f'Successful Association for {asset_id}: {message}')
        else:
            bad_association.append({'asset_code': asset_id,
                                    'wiliot_code': pixel_dict['pixel_id'],
                                    'timestamp': pixel_dict['timestamp']})
            if message['status_code'] in [404, 409, 504]:
                pass  # already handled
            elif message['status_code'] == 403:
                self.logger.warning(f'Failed Association for {asset_id} due '
                                    f'to Authentication and authorization issues: {message}')
            else:
                self.logger.warning(f'Failed Association for {asset_id} due '
                                    f'to Unexpected error: {message}')
        return bad_association

    def send_to_cloud(self, payload):
        self.client._renew_token()
        headers = {'Content-Type': 'Application/json',
                   'Authorization': f'Bearer {self.client.headers["Authorization"]}'}
        response = requests.post(url=URL_PATH, headers=headers,
                                 data=json.dumps({"data": payload}), params=None)
        try:
            message = response.json()
        except Exception as e:
            message = response.text

        if isinstance(message, str):
            message = {"data": message}
        message.update({'status_code': response.status_code})

        return message

    def send_to_cloud_debug(self, payload):
        self.logger.info(f'post: url={URL_PATH}, headers={self.client.headers}, data={json.dumps({"data": payload})}')
        return {'data': 'debug', 'status_code': 201}

    def create_asset_and_label(self, asset_id: str, category_id: str, pixels: list):
        """
        Creates an asset with the given category and pixel.
        :param asset_id: Required -
        :param category_id: Required
        :param pixels: Required - list of pixel IDs to associate with the asset
        """
        res = {"data": f"no asset id or pixels: asset: {asset_id}, pixels: {pixels}",
               "status_code": 700}

        platform_client = self.client
        self.logger.info(f"Trying to create an asset in owner {platform_client.owner_id} ")
        if not asset_id or not pixels:
            self.logger.info(res['data'])
            return res
        try_again = 'first'
        while try_again:
            if try_again == 'associate':
                bad_res_list = []
                for pixel in pixels:
                    try:
                        success = platform_client.associate_pixel_to_asset(asset_id=asset_id, pixel_id=pixel)
                        """
                        url: 'https://api.us-east-2.prod.wiliot.cloud//v1/traceability/owner/<OWNER_ID>/asset/<ASSET_ID>/tag/<PIXEL_ID>'
                        the above pixel id after decoding, i.e. (01)00850027865010(21)0bm6T0000 is %2801%2900850027865010%2821%290bm6T0000
                        data: {}
                        """

                        if success:
                            self.logger.info(f"Associated pixel {pixel} to asset {asset_id}")
                        else:
                            raise Exception("Association failed with no cloud exception")
                    except Exception as e:
                        if e.args[0]['status_code'] == 400 and \
                                'tagId is already associated with this asset' in e.args[0]['error']:
                            self.logger.info(f"pixel {pixel} is already associate to asset {asset_id}")
                            continue
                        res = {"data": f"Association for {asset_id} and {pixel} failed due to {e}",
                               "status_code": 710}
                        bad_res_list.append(res)
                if bad_res_list:
                    res = {"data": '\n'.join([b_res["data"] for b_res in bad_res_list]),
                           "status_code": 710}
                else:
                    res = {"data": f"Associated pixels {pixels} to asset {asset_id}",
                           "status_code": 201}
                try_again = ''
            else:
                try_again = ''
                try:
                    res = platform_client.create_asset(None, asset_id=asset_id,
                                                       category_id=category_id,
                                                       pixels=[{'tagId': p, 'role': TagRole.DEFAULT}
                                                               for p in pixels])
                    """
                    url: 'https://api.us-east-2.prod.wiliot.cloud//v2/traceability/owner/<OWNER_ID>/asset'
                    headers: {'accept': 'application/json', 
                              'Content-Type': 'application/json', 
                              'Authorization': <TOKEN>}
                    data: {"id": <ASSET_ID>, 
                           "name": null, 
                           "categoryId": <CATEGORY_ID>, 
                            "tags": [{"tagId": <PIXEL_ID1>, "role": "DEFAULT"}, 
                                     {"tagId": <PIXEL_ID2>, "role": "DEFAULT"}], 
                            "status": null}'
                    """
                    if res:
                        created_asset_id = res['id']
                        self.logger.info(f"Asset {created_asset_id} creation request was successful")
                        self.logger.info(res)
                        res['status_code'] = 201

                except WiliotCloudError as wce:
                    self.logger.error(f"Error when creating asset {asset_id}: {wce.args}")
                    res = {"data": wce.args[0].get('error'),
                           "status_code": wce.args[0].get('status_code', '')}
                    # What we do depends on the status code
                    if wce.args[0].get('status_code', '') == 404:
                        # Either the pixel or the category do not exist in this account
                        self.logger.warning(f"One or more of the pixels do not exist in the account")
                        self.logger.warning(wce.args[0].get('error'))
                    elif wce.args[0].get('status_code', '') == 504:
                        # If the upstream server timed out - return from here - this will be retried
                        self.logger.error("Upstream server timeout")
                    elif wce.args[0].get('status_code', '') == 409:
                        # There are two situations we can get a 409 error:
                        # 1. The tag is already associated with another asset - can happen when an asset ID and pixel
                        # are scanned more than once. In this case - we log and exit because there is nothing
                        # else to do
                        if wce.args[0].get("error", "").lower().find("already associated") != -1:
                            self.logger.error(f"Tag {pixels} already associated with an asset")
                        # 2. Due to a previous failure - the asset has already been created - log and continue
                        # to add labels to this asset
                        else:
                            # If the asset already exists - log this but keep going - we might need to add labels
                            self.logger.error(f"Asset {asset_id} already exists")
                            try_again = 'associate'
                    else:
                        self.logger.error(f"Unexpected Cloud error during asset creation: {wce}")
                except Exception as e:
                    self.logger.error(f"Unexpected error during asset creation: {e}")
                    res = {"data": f"Unexpected error during asset creation: {e}",
                           "status_code": 900}

        return res


if __name__ == '__main__':
    c = CloudAssociation(None, None)
    # base_str = '(01)00850027865010(21)0bm6T'
    # for i in range(5000):
    #     new_tag = base_str + str(i).zfill(4)
    #     c.create_asset_and_label(asset_id=new_tag, pixels=[new_tag], category_id=CATEGORY_ID)

    # c.create_asset_and_label(asset_id='01234', pixels=['(01)00850027865010(21)0bm6T0000',
    #                                                    '(01)00850027865010(21)0bm6T0001'], category_id=CATEGORY_ID)
    # c.create_asset_and_label(asset_id='01235', pixels=['(01)00850027865010(21)0bm7T0000'], category_id=CATEGORY_ID)
    # c.create_asset_and_label(asset_id='01231', pixels=['(01)00850027865010(21)0bm6T0003',
    #                                                    '(01)00850027865010(21)0bm6T0004'], category_id=CATEGORY_ID)
    # c.create_asset_and_label(asset_id='01236', pixels=['(01)00850027865010(21)0bm6T0005',
    #                                                    '(01)00850027865010(21)0bm6T0005'], category_id=CATEGORY_ID)
    print('done')
