import asyncio
import collections
import concurrent.futures
import json
import logging
import threading
import time
import pprint

import aiohttp
import aiohttp.client


class HTTPConnection:
    #TODO: test with moonraker and replace in moonraker_sender.py

    SERVICE_NAME = "HTTPConnection"
    DEFAULT_HTTP_PORT = 7125
    DEFAULT_HTTPS_PORT = 7130
    DEFAULT_HOST_IP = "127.0.0.1"
    HOST_MASK = "http://%s:%d"
    #WS_HOST_MASK = "ws://%s:%d/socket"

    UPLOAD_FILENAME = "3dprinteros.gcode" # don't use dot in name and be very cautious with length and special characters

    APIKEY_HEADER_NAME = "X-Api-Key"

    #SERVER_INFO_PATH = "/server/info"
    PRINTER_STATUS_PATH = "/printer/objects/query?extruder=target,temperature&heater_bed=target,temperature&toolhead=position&print_stats&virtual_sdcard"
    PRINTER_INFO_PATH = "/printer/info"
    EXEC_GCODE_PATH = "/printer/gcode/script?script=" #POST #NOTE user url encode for spaces
    LOGIN_PATH = "/access/login" #POST
    EMERGENCY_STOP_PATH = "/printer/emergency_stop" # POST
    FILES_PATH = "/server/files"
    UPLOAD_PATH = FILES_PATH + "/upload" #POST
    UPLOAD_ROOT = 'gcodes'
    METADATA_PATH = '/server/files/metadata?filename=' + UPLOAD_FILENAME
    MAX_MESSAGE_SIZE = 0x2000
    MAX_MESSAGE_INDEX = 0xfffffff
    TIMEOUT = 2
    UPLOAD_TIMEOUT = 60
    COMMAND_TIMEOUT = 15
    LOOP_TIME = 0.5
    REQ_RETRY = 6

    def __init__(self, host=None, port=None, timeout=TIMEOUT, parent=None, logger=None):
        self.stop_flag = False
        if logger:
            self.logger = logger
        else:
            self.logger = logging.getLogger()
        if not host:
            host = self.DEFAULT_HOST_IP
        if not port:
            port = self.DEFAULT_HTTP_PORT
        self.host = self.HOST_MASK % (host, port)
        if port == self.DEFAULT_HTTPS_PORT:
            self.host = self.host.replace('http:', 'https:')
        if not timeout:
            timeout = self.TIMEOUT
        self.timeout = timeout
        self.parent = parent
        self.loop = None
        self.send_now_buffer = collections.deque()
        self.command_future = None
        self.print_future = None
        self.apikey = None
        self.operational_flag = False
        self.printing_flag = False
        self.paused_flag = False
        self.session = None
        self.message_index = 0
        self.temps = [0.0, 0.0]
        self.ttemps = [0.0, 0.0]
        self.total_lines = 0
        self.current_line = 0
        self.percent = 0.0
        self.est_print_time = 0.0
        self.print_time_left = 0.0
        self.commands_to_process = collections.deque()
        if parent:
            self.upload_root = self.parent.settings.get('upload_root', self.UPLOAD_ROOT)
            self.moonraker_token = parent.settings.get('token')
            self.moonraker_username = parent.settings.get('login')
            self.moonraker_password = parent.settings.get('password')
        else:
            self.upload_root = self.UPLOAD_ROOT
            self.moonraker_token = None
            self.moonraker_username = None
            self.moonraker_password = None
        self.auth_headers = None
        if logger:
            self.logger = logger.getChild('connection')
        else:
            self.logger = logging.getLogger(self.__class__.__name__)
        self.status_thread = threading.Thread(target=self.init_loop)
        self.status_thread.start()

    def init_loop(self):
        try:
            self.loop = asyncio.get_event_loop()
        except RuntimeError:
            self.logger.info("Staring event loop...")
            self.loop = asyncio.new_event_loop()
            asyncio.set_event_loop(self.loop)
        while not self.stop_flag:
            self.loop.run_until_complete(self.connect())
            time.sleep(self.TIMEOUT)

    def upload(self, f):
        self.logger.info("Creating upload gcodes task...")
        self.print_future = asyncio.run_coroutine_threadsafe(self.send_file_to_print(f), self.loop)
        self.logger.info("Waiting for upload gcodes to finish...")
        try:
            upload_error = self.print_future.result(self.UPLOAD_TIMEOUT)
        except (asyncio.TimeoutError, concurrent.futures.TimeoutError):
            self.print_future.cancel()
            upload_error = None
        except asyncio.CancelledError:
            upload_error = 'Cancelled'
        if upload_error:
            self.logger.info("Upload error:" + str(upload_error))
        self.print_future = None
        return not bool(upload_error)

    async def send_file_to_print(self, f):
        form = aiohttp.FormData()
        form.add_field('file', f, filename=self.UPLOAD_FILENAME)
        form.add_field('root', self.upload_root)
        form.add_field('print', 'true')
        self.logger.info('Uploading file to ' + self.SERVICE_NAME)
        if not self.session:
            self.logger.error('Unable to send file due to lack of connection to ' + self.SERVICE_NAME)
        else:
            try:
                await self.session.delete(self.host + self.FILES_PATH + "/" + self.upload_root + "/" + self.UPLOAD_FILENAME, headers=self.auth_headers, timeout=aiohttp.ClientTimeout(total=self.COMMAND_TIMEOUT))
                async with self.session.post(self.host + self.UPLOAD_PATH, headers=self.auth_headers, data=form, timeout=aiohttp.ClientTimeout(total=self.UPLOAD_TIMEOUT)) as resp:
                    if resp.ok:
                        self.percent = 0.0
                        self.est_print_time = 0.0
                        self.logger.info('Successful upload to %s',  self.host)
                        resp_dict = await resp.json()
                        if resp_dict.get("print_started"):
                            await self.get_and_parse_metadata()
                        else:
                            return f"Print start error:" + pprint.pformat(resp_dict)
                    else:
                        return f"Upload error: {resp.status}:\n" + pprint.pformat(await resp.text())
            except asyncio.TimeoutError:
                 return "Upload timeout"
            except asyncio.CancelledError:
                 return "Upload cancelled"
            except Exception as e:
                self.logger.exception('Exception on upload:')
                return "Exception on upload: " + str(e)

    async def get_and_parse_metadata(self):
        async with self.session.get(self.host + self.METADATA_PATH, headers=self.auth_headers, timeout=aiohttp.ClientTimeout(total=self.UPLOAD_TIMEOUT)) as resp:
            if resp.ok:
                metata_dict = await resp.json()
                metadata_result = metata_dict.get('result', {})
                try:
                    est_print_time = float(metadata_result.get('estimated_time'))
                except:
                    est_print_time = None
                if est_print_time:
                    self.est_print_time = est_print_time

    #TODO implement this in future versions
    async def authorize(self):
        return True

    async def connect(self):
        self.logger.info(f"Connecting to {self.SERVICE_NAME} server at {self.host}")
        try:
            self.session = aiohttp.ClientSession(loop=self.loop, timeout=aiohttp.ClientTimeout(total=self.timeout))
            # if self.moonraker_username:
            #     if not await self.authorize():
            #         self.register_error(1000, 'Unable to connect to ' + str(self.host))
            async with self.session.get(self.host + self.PRINTER_INFO_PATH) as resp:
                if resp.ok:
                    resp_dict = await resp.json()
                    if resp_dict.get('result', {}).get('state') == 'ready':
                        self.operational_flag = True
                    await self.status_requesting()
        except:
            #self.logger.exception("Unable to connect to " + str(self.host))
            self.logger.warning("Unable to connect to " + str(self.host))
        finally:
            self.logger.info(f"Closing connection" + str(self.host))
            if self.session:
                await self.session.close()
            self.session = None
            self.operational_flag = False

    async def status_requesting(self):
        self.logger.info('Entering status loop...')
        retries_left = self.REQ_RETRY
        while not self.stop_flag:
            loop_start_time = time.monotonic()
            if not self.session:
                break
            #if not self.moonraker_token:
                # resp = await self.session.post({'username': self.moonraker_username, "password": self.moonraker_password})
                # if resp.ok:
                #     resp_dict = await resp.json()
                #     self.moonraker_token = resp_dict.get('token')
                #     self.moonraker_refresh_token = resp_dict.get('token')
            if not self.command_future and not self.print_future:
                try:
                    resp = await self.session.get(self.host + self.PRINTER_STATUS_PATH, headers=self.auth_headers)
                    if resp.ok:
                        retries_left = self.REQ_RETRY
                        self.operational_flag = True
                        try:
                            self.parse_printer_status(await resp.json())
                        except:
                            self.logger.exception(f"Exception on processing {self.SERVICE_NAME}'s response: ")
                except:
                    retries_left -= 1
                    if not retries_left:
                        self.operational_flag = False
                        self.register_error(2000, self.SERVICE_NAME + ' connection lost', job_fail=True)
            delta = time.monotonic() + self.LOOP_TIME - loop_start_time
            if delta > 0:
                await asyncio.sleep(delta)
        self.logger.info('Status loop exit')

    def parse_printer_status(self, resp_dict):
        self.logger.debug('Resp: %s', json.dumps(resp_dict))
        status = resp_dict.get('result', {}).get('status', {})
        self.temps[0] = status.get('heater_bed', {}).get('temperature', 0.0)
        self.temps[1] = status.get('extruder', {}).get('temperature', 0.0)
        self.ttemps[0] = status.get('heater_bed', {}).get('target', 0.0)
        self.ttemps[1] = status.get('extruder', {}).get('target', 0.0)
        position = status.get('toolhead', {}).get('position')
        if isinstance(position, list) or isinstance(position, tuple) and len(position) == 4:
            self.position = list(position)
        print_stats = status.get('print_stats', {})
        self.printing_flag = print_stats.get('state') == 'printing'
        self.paused_flag = print_stats.get('state') == 'paused'
        if self.printing_flag or self.paused_flag:
            print_duration = print_stats.get('print_duration')
            if print_duration and self.est_print_time:
                self.print_time_left = max(self.est_print_time - print_duration, 0.0)
        progress = status.get('virtual_sdcard', {}).get('progress', 0.0)
        if isinstance(progress, float):
            self.percent = round(progress*100, 2)
        else:
            self.percent = round(max(100*float(print_duration)/self.est_print_time, 100.0), 2)

    # def get_message_index(self):
    #     if self.message_index > self.MAX_MESSAGE_INDEX:
    #         self.message_index = 0
    #     self.message_index += 1
    #     return self.message_index

    def parse_command_response(self, data):
        if isinstance(data, dict):
            return data.get("ok", False)
        return False

    def run_async_method(self, method, *args, **kwargs):
        self.logger.info("Creating command future...")
        while self.command_future:
            if self.stop_flag:
                return False
            time.sleep(0.1)
        self.command_future = asyncio.run_coroutine_threadsafe(
            method(*args, **kwargs),
            self.loop
        )
        self.logger.info("Waiting for command future to finish...")
        success = False
        try:
            result = self.command_future.result(self.TIMEOUT)
            success = self.parse_command_response(result)
        except (asyncio.TimeoutError, concurrent.futures.TimeoutError):
            self.logger.error('Command future timeout')
            # self.command_future.cancel()  # Optionally cancel the future on timeout
        except asyncio.CancelledError:
            self.logger.error('Command future cancelled')
        finally:
            self.command_future = None
            self.logger.info(f'Command execution result: {success}')
            return success

    def run_async_command(self, path, method, data=None):
         return self.run_async_method(method, self.host + path, data=data,
                                      timeout=aiohttp.ClientTimeout(total=self.COMMAND_TIMEOUT))

    def pause(self):
        return self.run_async_command('/printer/print/pause', self.session.post)

    def resume(self):
        return self.run_async_command('/printer/print/resume', self.session.post)

    def cancel(self):
        return self.run_async_command('/printer/print/cancel', self.session.post)

    def execute_gcode(self, gcode):
        return self.run_async_command(self.EXEC_GCODE_PATH + gcode, self.session.post)

    def register_error(self, code, message, job_fail=False, info=False):
        if self.parent:
            self.parent.register_error(code, message, job_fail, info=info)
        else:
            self.logger.error(f"Register error: {code} {message} blocking={job_fail} info={info}")

    def close(self):
        self.logger.info("Closing " + __class__.__name__)
        self.stop_flag = True
        if self.print_future:
            self.print_future.cancel()
        if self.command_future:
            self.command_future.cancel()
        self.status_thread.join(self.TIMEOUT + 0.1)
        if self.loop:
            try:
                self.loop.close()
            except RuntimeError:
                pass
        time.sleep(0.1)
