"""
Copyright 2023, 2024 Jobin Rezai

This file is part of LVNAuth.

LVNAuth is free software: you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.

LVNAuth is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.

You should have received a copy of the GNU General Public License along with
LVNAuth. If not, see <https://www.gnu.org/licenses/>. 
"""


# ReplyPost
# Used for sending binary or string data to the server where we're expecting 
# a simple text reply back from the server.

# Usage examples: sending a captcha answer to the server, sending an image 
# to the server.
# In both example cases, the server would reply with a simple response to 
# indicate whether the submission was successful or not.


# Client

import niquests as requests
import json
from os import path
from threading import Thread
from typing import Dict
from queue import Queue
from response_code import ServerResponseReceipt, ServerResponseCode



class ReplyPost(Thread):
    """
    This class is used for submitting binary and json data to the server.
    It expects a simple response back from the server.
    
    This class is not used for downloading images or complex data types.
    Its main purpose is for submitting images and/or json strings to the
    server and getting back a simple "ok" type response.
    
    Usage example: for submitting a new registration (json string),
    for uploading an image (file + json (the json will have the image
    info/purpose)), etc.
    
    In both examples, we expect the server to send us a simple "ok"
    or "error" to indicate whether the submission was successful or not.
    """
    
    the_queue = Queue()
    
    def __init__(self, request_uri, to_be_json_data: Dict, callback_method):
        Thread.__init__(self, daemon=True)
        
        self.request_uri = request_uri
        self.callback_method = callback_method
        
        # If the request takes a long time, an outside thread might want to 
        # cancel this thread, so we use this flag to indicate that another 
        # thread wants this thread cancelled.
        # So even if this thread ends up completing successfully, it won't run 
        # the callback method.
        self.cancelled = False

        self.files = []

        # Convert Dict to json
        json_data = json.dumps(to_be_json_data)
        
        self.files.append( ("json", ("json", json_data, "application/json" ))  )
        
    def add_file_bytes(self, file_name, file_bytes):
        """
        Add one file to the list of data to be sent to the server.
        This method is almost the same as add_file(), except this doesn't
        take a path, it takes the file as bytes.
        
        Multiple files can be added by calling this method again with
        the next file.
        
        Once a file has been added, it cannot be removed.
        
        Arguments:
        
        - file_name: the file name (not full path) of file to be sent to Flask
        - file_bytes: the bytes data of the file.
        """

        if not file_name:
            raise ValueError("No file name was provided.")
        
        elif not file_bytes:
            raise ValueError("No file byte data was provided.")
        
        elif not isinstance(file_bytes, bytes):
            raise TypeError(f"Expected bytes type for the image, but got {type(file_bytes)} instead.")
        

        # Add file to list
        self.files.append( ("file", (file_name, file_bytes, "application/octet")) )
        
    def add_file(self, file_path):
        """
        Add one file to the list of data to be sent to the server.
        
        Multiple files can be added by calling this method again
        with the next file.
        
        Once a file has been added, it cannot be removed.
        
        Argument:
        - file_path: the full string path to the file to be sent.
        """
        
        # Extract the file name
        file_name = path.basename(file_path)
        
        # Add file to list
        self.files.append( ("file", (file_name, open(file_path, "rb"), "application/octet")) )
        
    def cancel(self):
        """
        Set the cancel boolean flag in in this object so that even if
        we get a response from the server, we won't do anything with the
        response, but this thread has been cancelled from another thread.
        
        The purpose of cancelling this thread is if the server is taking a
        long time. At that point, an outside thread will automatically run
        this cancel() method to indicate that it's no longer interested in
        waiting for this thread to complete.
        """
        self.cancelled = True

    def run(self):
        """
        Send the request to the web server.
        """
        
        try:
            
            #if self.files:
                # Sending files?
            r: requests.models.Response = requests.post(self.request_uri,
                                                        files=self.files)
            #else:
                ## Sending just json data?
                #r:requests.models.Response = requests.post(self.request_uri, json=self.json_data)

            # Has an outside thread cancelled this thread?
            # Don't bother doing anything with the results that we just got 
            # from the server, because an outside thread has 
            # cancelled this thread.
            if self.cancelled:
                return
            

            response_code = None
            response_text = None


            # Status code 200 means: HTTP_200_OK (successful http request)
            # The plain text response will be in r.text
            if r.status_code == 200 and r.ok and r.text:
                # r.text will have the server response code from Flask 
                # and possibly more information, such as an error message.
                
                # The two will be separated by a semicolon, 
                # if there is any extra info.
                
                print("The value we got from post was",
                      r.text, "and is of type:", type(r.text))

                try:
                    # Convert json to a Dict
                    json_data = json.loads(r.text)
                
                    response_code = json_data.get("ServerResponseCode")
                    response_text = json_data.get("ServerResponseText")
                
                except ValueError:
                    # We couldn't parse the json to a Dict
                    response_code = ServerResponseCode.UNKNOWN
                    response_text = "invalid-json-data"
                
            else:
                response_code = ServerResponseCode.UNKNOWN
                response_text = "error-replypost"
                
            # Send the response to the main GUI thread.
            receipt = ServerResponseReceipt(self.callback_method,
                                            response_code,
                                            response_text)
            ReplyPost.the_queue.put(receipt)
                
        except requests.ConnectionError:
                
            receipt = ServerResponseReceipt(self.callback_method,
                                            ServerResponseCode.CONNECTION_ERROR)
            ReplyPost.the_queue.put(receipt)
