
import os
import sys
import re
import json
import stat
import pprint
import uuid
import signal
import subprocess

from socketserver import TCPServer, StreamRequestHandler

try:
   from socketserver import UnixStreamServer
except:
   pass


from log import log_init, log, log_error, trace
from notebook import notebook_execute, RestartKernel

import asyncio
if sys.platform == 'win32':
   from asyncio.windows_events import *
   asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())


class ExecuteHandler(StreamRequestHandler):

   def handle(self):

      try:
         trace('handling server request')

         # read input
         input = str(self.rfile.readline().strip(), 'utf-8')
         input = json.loads(input)

         # validate secret
         if not self.server.validate_secret(input["secret"]):
            trace('invalid secret (exiting server)')
            self.server.request_exit()
            return

         # if this is an abort command then request exit
         command = input["command"]
         if command == "abort":
            trace('abort command received (exiting server)')
            self.server.request_exit()
            return

         # options
         options = input["options"]

         # stream status back to client
         def status(msg):
            self.message("status", msg)
      
         # execute the notebook
         trace('executing notebook')
         persist = notebook_execute(options, status)
         if not persist:
            trace('notebook not persistable (exiting server)')
            self.server.request_exit()
      except RestartKernel:
         trace('notebook restart request recived (exiting server)')
         self.message("restart")
         self.server.request_exit()
      except Exception as e:
         self.message("error", "\n\n" + str(e))
         self.server.request_exit()

   # write a message back to the client      
   def message(self, type, data = ""):
      message = {
         "type": type,
         "data": data 
      }
      self.wfile.write(bytearray(json.dumps(message) + "\n", 'utf-8'))
      self.wfile.flush()
  
def execute_server(options):

   # determine server type
   is_tcp = options["type"] == "tcp"
   if is_tcp:
      base = TCPServer
   else:
      base = UnixStreamServer

   class ExecuteServer(base):

      allow_reuse_address = True
      exit_pending = False
      
      def __init__(self, options):

         trace('creating notebook server (' + 
               options["type"] + ': ' + options["transport"] 
               + ')')

         # set secret for tcp
         if is_tcp:
            self.secret = str(uuid.uuid4())
         else:
            self.secret = ""

         # server params
         self.transport = options["transport"]
         self.timeout = options["timeout"]

         # initialize with address (based on server type) and handler
         if is_tcp:
            server_address = ("localhost",0)
         else:
            server_address = self.transport
         super().__init__(server_address, ExecuteHandler)

         # if we are a tcp server then get the port number and write it 
         # to the transport file. change file permissions to user r/w
         # for both tcp and unix domain sockets
         if is_tcp:
            port = self.socket.getsockname()[1]
            trace('notebook server bound to port ' + str(port))
            with open(self.transport,"w") as file:
               file.write("")
            os.chmod(self.transport, stat.S_IRUSR | stat.S_IWUSR)
            with open(self.transport,"w") as file:
               file.write(json.dumps(dict({
                  "port": port,
                  "secret": self.secret
               })))
         else:
            os.chmod(self.transport, stat.S_IRUSR | stat.S_IWUSR)


      def handle_request(self):
         if self.exit_pending:
            self.exit()
         super().handle_request()

      def handle_timeout(self):
         trace('request timeout (exiting server)')
         self.exit()

      def validate_secret(self, secret):
         return self.secret == secret

      def request_exit(self):
         self.exit_pending = True

      def exit(self):
         try:
            trace('cleaning up server resources')
            self.server_close()
            self.remove_transport()
          
         finally:
            trace('exiting server')
            sys.exit(0)

      def remove_transport(self):
         try:
            if os.path.exists(self.transport):
               os.remove(self.transport)
         except:
            pass

   return ExecuteServer(options)

  
def run_server(options): 
   try:
      with execute_server(options) as server:  
         while True:
            server.handle_request() 
   except Exception as e:
      log_error("Unable to run server", exc_info = e)

# run a server as a detached subprocess
def run_server_subprocess(options, status):

   # python executable
   python_exe = sys.executable

   # detached process flags for windows
   flags = 0
   if sys.platform == 'win32':
      python_exe = re.sub('python\\.exe$', 'pythonw.exe', python_exe)
      flags |= 0x00000008  # DETACHED_PROCESS
      flags |= 0x00000200  # CREATE_NEW_PROCESS_GROUP
      flags |= 0x08000000  # CREATE_NO_WINDOW
      flags |= 0x01000000  # CREATE_BREAKAWAY_FROM_JOB
   else:
      signal.signal(signal.SIGCHLD, signal.SIG_IGN)

   # forward options via env vars
   os.environ["QUARTO_JUPYTER_OPTIONS"] = json.dumps(options)

   # create subprocess
   subprocess.Popen([python_exe] + sys.argv + ["serve"],
      stdin = subprocess.DEVNULL,
      stdout = subprocess.DEVNULL,
      stderr = subprocess.DEVNULL,
      creationflags = flags,
      close_fds = True,
      start_new_session = True
   )


# run a notebook directly (not a server)
def run_notebook(options, status):

   # run notebook w/ some special exception handling. note that we don't 
   # log exceptions here b/c they are considered normal course of execution
   # for errors that occur in notebook cells
   try:   
      notebook_execute(options, status)
   except Exception as e:
      # CellExecutionError for execution at the terminal includes a bunch
      # of extra stack frames internal to this script. remove them
      msg = str(e)
      kCellExecutionError = "nbclient.exceptions.CellExecutionError: "
      loc = msg.find(kCellExecutionError)
      if loc != -1:
         msg = msg[loc + len(kCellExecutionError)]
      sys.stderr.write("\n\n" + msg + "\n")
      sys.stderr.flush()
      sys.exit(1)


if __name__ == "__main__":

   # stream status to stderr
   def status(msg):
      sys.stderr.write(msg)
      sys.stderr.flush()

   try:
      # read command from cmd line if it's there (in that case 
      # options are passed via environment variable)
      if len(sys.argv) > 1:
         command = sys.argv[1]
         options = json.loads(os.getenv("QUARTO_JUPYTER_OPTIONS"))
         del os.environ["QUARTO_JUPYTER_OPTIONS"]
      # otherwise read from stdin
      else:
         input = json.load(sys.stdin)
         command = input["command"]
         options = input["options"]

      # initialize log
      log_init(options["log"], options["debug"])

      # start the server (creates a new detached process, we implement this here 
      # only b/c Deno doesn't currently support detaching spawned processes)
      if command == "start":
         trace('starting notebook server subprocess')
         run_server_subprocess(options, status)

      # serve a notebook (invoked by run_server_subprocess)
      elif command == "serve":
         trace('running notebook server subprocess')
         run_server(options)
      
      # execute a notebook and then quit
      elif command == "execute":
         trace('running notebook without keepalive')
         run_notebook(options, status)
        
   except Exception as e:
      log_error("Unable to run notebook", exc_info = e)
      sys.exit(1)

