# ruff: noqa: E501 Line too long
import os
import sys
import json
from argparse import ArgumentParser, _SubParsersAction
from pathlib import Path
from typing import Any, Generator
from . import __version__ as moulti_version
from .helpers import pint, send_to_moulti_and_handle_reply
from .protocol import send_to_moulti, PRINTABLE_MOULTI_SOCKET
from .protocol import moulti_connect, send_json_message, recv_json_message
from .widgets.cli import add_cli_arguments

def init(args: dict) -> None:
	"""Start a new Moulti instance."""
	from .app import main as init_moulti # pylint: disable=import-outside-toplevel
	init_moulti(**args)

def wait(verbose: bool = False, delay: int = 500, max_attempts: int = 0) -> None:
	"""Wait until the Moulti instance is available.
	Args:
		verbose: if True, output the reason why each connection attempt failed
		delay: number of milliseconds between two connection attempts
		max_attempts: maximum number of attempts before giving up; 0 means "never give up"
	"""
	import time # pylint: disable=import-outside-toplevel
	connected = False
	attempts = 0
	while not connected:
		try:
			attempts += 1
			send_to_moulti({'command': 'ping'})
			connected = True
			break
		except Exception as exc:
			if verbose:
				print(f'Connection #{attempts} to {PRINTABLE_MOULTI_SOCKET}: {exc}')
			if max_attempts > 0 and attempts == max_attempts:
				print('Giving up.')
				break
			time.sleep(delay / 1000.0)
	sys.exit(0 if connected else 1)

def saved_files(directory: Path) -> Generator:
	"""
	Iterate over a directory supposedly generated by Moulti's "Save" feature.
	Yield triplets:
	- step id (None for instance properties)
	- Moulti message to send
	- file descriptor (int) to the related log file (None for questions)
	"""
	json_ext = '.properties.json'
	for json_file in sorted(directory.glob('*' + json_ext)):
		if not json_file.is_file():
			continue
		with json_file.open(encoding='utf-8', errors='surrogateescape') as json_file_desc:
			data = json.load(json_file_desc)
			log_file = json_file.with_name(json_file.name[:-len(json_ext)] + '.contents.log')
			fileno = os.open(str(log_file), os.O_RDONLY) if log_file.is_file() else None
			yield data.get('id'), data, fileno

def load(args: dict, read_size: int = 1024**2) -> None:
	"""
	Load the contents of a directory supposedly generated by Moulti's "Save" feature and push it to a Moulti instance.
	Note: so far, this is the only function in Moulti that pipelines messages and replies. Therefore, it is also the
	only function that actually leverages message ids.
	"""
	errors = 0
	sent_messages: dict[str, bool] = {}
	def send_message(*args: Any) -> None:
		msg_id = send_json_message(*args)
		sent_messages[msg_id] = False
	filenos = []
	with moulti_connect() as moulti_socket:
		for step_id, data, fileno in saved_files(args['saved_directory']):
			# Send all messages using protocol pipelining:
			try:
				# Properties, typically a command that creates and configures a step:
				send_message(moulti_socket, data)
				# Contents:
				if fileno is not None:
					filenos.append(fileno)
					pass_msg = {'command': 'pass', 'id': step_id, 'read_size': read_size}
					send_message(moulti_socket, pass_msg, [fileno])
			except Exception as exc:
				errors += 1
				print(f'Error with {step_id}/{data}/{fileno}: {exc}')
		# Receive replies to all sent messages:
		while not all(sent_messages.values()):
			reply, _ = recv_json_message(moulti_socket, 0)
			if 'msgid' in reply and reply['msgid'] in sent_messages:
				sent_messages[reply['msgid']] = True
				if reply.get('done') is not True:
					errors += 1
					print(f'Error: {reply.get("error")}')
		for fileno in filenos:
			os.close(fileno)
	sys.exit(errors)

def add_main_commands(subparsers: _SubParsersAction) -> None:
	# moulti init
	init_parser = subparsers.add_parser('init', help='Start a new Moulti instance.')
	init_parser.set_defaults(func=init)

	# moulti run
	run_parser = subparsers.add_parser('run', help='Start a new Moulti instance and run a command')
	run_parser.set_defaults(func=init)
	run_parser.add_argument('command', type=str, nargs='+', help='command to run along with its arguments')

	# moulti wait
	wait_parser = subparsers.add_parser('wait', help='Wait until the Moulti instance is available.')
	wait_parser.set_defaults(func=wait)
	wait_parser.add_argument('--verbose', '-v', action='store_true', help='if True, output the reason why each connection attempt failed')
	wait_parser.add_argument('--delay', '-d', type=pint, default=500, help='number of milliseconds between two connection attempts')
	wait_parser.add_argument('--max-attempts', '-m', type=pint, default=0, help='maximum number of attempts before giving up; 0 means "never give up"')

	# moulti set
	set_parser = subparsers.add_parser('set', help='Set Moulti options')
	set_parser.set_defaults(func=send_to_moulti_and_handle_reply, command='set')
	set_parser.add_argument('--title', '-t', type=str, help='title displayed at the top of the screen')

	# moulti load
	load_parser = subparsers.add_parser('load', help='Load a saved directory into Moulti')
	load_parser.set_defaults(func=load, command='load')
	load_parser.add_argument('saved_directory', type=Path, help='path to a directory generated by Moulti\'s "Save" feature')

def build_arg_parser() -> ArgumentParser:
	arg_parser = ArgumentParser(prog='moulti', description='step-by-step logs')
	arg_parser.add_argument('--version', action='version', version=moulti_version)
	subparsers = arg_parser.add_subparsers(required=True)
	# moulti init, moulti wait:
	add_main_commands(subparsers)
	# moulti <widget>:
	add_cli_arguments(subparsers)
	return arg_parser

def main() -> None:
	try:
		arg_parser = build_arg_parser()
		args = vars(arg_parser.parse_args())
		func = args.pop('func')
		# Subtlety: func and args are not always used the same way:
		if func == wait: # pylint: disable=comparison-with-callable
			wait(**args)
		else:
			func(args)
	except KeyboardInterrupt:
		print('')
		sys.exit(1)

if __name__ == '__main__':
	main()
