# Source code for noctua.sequencer

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-

    '''
    Sequencer module for Observation Blocks (OBs) in JSON format
    '''

    # System modules
    import argparse
    import importlib
    import importlib.resources
    import json
    import os
    import signal
    import sys
    from datetime import datetime
    from pathlib import Path  # New import

    # Other templates
    from .utils.logger import log  # Corrected relative import

    class Sequencer():
        '''
        Manage a sequence of JSON Observation Blocks (OBs)
        '''

        def __init__(self, embedded=True):
            '''Constructor'''
            self.ob_file = None
            self.ob = []  # Content of the ob_file
            self.error = []
            self.tpl = None  # The running template
            self.params = {}  # The parameters of the running template
            self.quitting = False
            self.embedded = embedded  # To manage sys.exit if called alone
            signal.signal(signal.SIGINT, self.interrupt)
            self.original_sigint = signal.getsignal(signal.SIGINT)

        def load_script(self, template_script_path, params={}):
            '''Load a python file where the template is implemented'''

            # template_script_path could be a full path or a module path like "noctua.templates.lampsoff"
            # For simplicity with argparse, we'll assume it's a file path for now,
            # or a simple name if it's a bundled template.

            script_path = Path(template_script_path)
            if not script_path.exists():
                # Try to resolve it as a module within noctua.templates
                try:
                    # Create noctua/templates/__init__.py if it doesn't exist
                    with importlib.resources.path(f"noctua.templates", f"{template_script_path}.py") as p:
                        script_path = p
                        log.info(
                            f"Loading script '{template_script_path}' from package resources.")
                except FileNotFoundError:
                    log.error(
                        f"Template script '{template_script_path}' not found as file or package resource.")
                    self.error.append(
                        f"Template script '{template_script_path}' not found.")
                    return

            # The stem of the script file is used as the template name
            # This matches how templates are named in JSON OBs
            module_stem_name = script_path.stem

            self.ob = {"template": module_stem_name, "params": params}
            self.ob_file = str(script_path)  # Store the path for logging/reference
            log.info(
                f"SEQUENCER: Loaded script {
                    self.ob_file} with params {params}")

        def load_file(self, ob_file_path_str):
            '''Load a JSON file with an OB'''
            ob_file_path = Path(ob_file_path_str)
            self.ob_file = str(ob_file_path)

            try:
                with open(ob_file_path, encoding="utf-8") as jsonfile:
                    log.info(f"SEQUENCER: Loading the ob from file {self.ob_file}")
                    self.error = []
                    self.ob = json.load(jsonfile)
            except json.decoder.JSONDecodeError as e:
                msg = f"SEQUENCER: Probably malformed json in {self.ob_file}"
                log.error(msg)
                log.error(e)
                self.error.append(msg)
                self.error.append(str(e))
            except UnicodeDecodeError as e:
                msg = f"SEQUENCER: Not a text file: {self.ob_file}"
                log.error(msg)
                log.error(e)
                self.error.append(msg)
                self.error.append(str(e))
            except FileNotFoundError:
                msg = f"SEQUENCER: OB file not found: {self.ob_file}"
                log.error(msg)
                self.error.append(msg)
            except Exception as e:
                msg = f"SEQUENCER: Not handled exception while loading {
                    self.ob_file}"
                log.error(msg)
                log.error(e)
                self.error.append(msg)
                self.error.append(str(e))
                if not self.embedded:
                    raise SystemExit(e) from e

        def execute(self):
            '''Execute the template(s) contained in the loaded OB'''
            if not self.ob:
                log.error("SEQUENCER: No OB loaded to execute.")
                self.error.append("No OB loaded.")
                return

            self.quitting = False
            ob_list = self.ob if isinstance(self.ob, list) else [self.ob]

            log.info(
                f"There are {
                    len(ob_list)} templates in the OB: {
                    self.ob_file}")
            log.debug(ob_list)

            now = datetime.utcnow()

            for template_item in ob_list:
                if self.quitting:
                    log.info("SEQUENCER: Execution quitting.")
                    break
                log.info("--------------------------------------------")
                try:
                    self.error = []  # Reset errors for this template execution

                    template_name = template_item.get("template")
                    if not template_name:
                        log.error(
                            "SEQUENCER: Template item missing 'template' key.")
                        self.error.append("Template item missing 'template' key.")
                        continue  # Skip to next template if malformed

                    # Importing the module given its name in the json.
                    full_module_name = f"noctua.templates.{template_name}"
                    try:
                        tplmodule = importlib.import_module(full_module_name)
                    except ModuleNotFoundError as e:
                        log.error(
                            f"SEQUENCER: Template module '{full_module_name}' not found: {e}")
                        self.error.append(
                            f"Template module '{full_module_name}' not found.")
                        continue

                    # Every template module has a Template() class.
                    self.tpl = tplmodule.Template()
                    self.params = template_item.get("params", {})

                    # Run the template given its parameters in the json.
                    self.tpl.run(self.params)

                except AttributeError as e:
                    msg = f"SEQUENCER: Attribute Error in template '{template_name}'"
                    log.error(msg)
                    log.error(e, exc_info=True)  # Log full traceback
                    self.error.append(msg)
                    self.error.append(str(e))

                except KeyError as e:
                    msg = f"SEQUENCER: Parameter '{e}' is not defined for template '{template_name}'"
                    log.error(msg)
                    self.error.append(msg)

                except Exception as e:  # Catch other unexpected errors during template execution
                    msg = f"SEQUENCER: Unexpected error during execution of template '{template_name}'"
                    log.error(msg)
                    log.error(e, exc_info=True)
                    self.error.append(msg)
                    self.error.append(str(e))

            log.info("--------------------------------------------")
            log.debug(
                f"Total elapsed time for OB {
                    self.ob_file}: {
                    (
                        datetime.utcnow() -
                        now).total_seconds():.3f}s")

        def interrupt(self, signum, frame):
            '''Intercept a CTRL+C instead of raising a KeyboardInterrupt exception
            in order to be able to operate on the template class, for
            example to modify a pause attribute or call a method.
            '''

            # signal.signal(signal.SIGINT, self.original_sigint)

            sys.stderr.write("\n--- INTERRUPT ---\n")
            sys.stderr.write("(P)ause, (R)esume, (N)ext template, (Q)uit:\n")
            sys.stderr.flush()

            raw_line = sys.stdin.readline()

            try:
                # answer = input(
                #     "(P)ause, (R)esume, (N)ext template, (Q)uit:\n")

                if not raw_line:  # THIS IS THE CHECK FOR EOF (Ctrl+D)
                    msg = "SEQUENCER: CTRL-D (EOF) detected with readline!"
                    log.critical(msg)
                    if not self.embedded:
                        log.info("SEQUENCER: EOF in non-embedded mode. Forcing exit.")
                        if hasattr(
                                self,
                                'tpl') and self.tpl and hasattr(
                                self.tpl,
                                'abort'):
                            try:
                                self.tpl.abort()
                            except Exception:
                                pass  # Best effort
                        os._exit(1)
                    else:
                        log.info("SEQUENCER: EOF in embedded mode. Signaling quit.")
                        self.quit()
                    return  # Exit the interrupt handler

                answer = raw_line.strip().lower()

                if answer.startswith('p'):
                    self.pause()
                elif answer.startswith('r'):
                    self.resume()
                elif answer.startswith('n'):  # Abort current template, move to next
                    self.abort_current_template()
                elif answer.startswith('q'):  # Quit entire OB
                    self.quit()
                else:
                    log.info("SEQUENCER: No valid action taken on interrupt.")

            # except EOFError:  # CTRL+D
            #     msg = "SEQUENCER: CTRL-D detected! Exiting for real!"
            #     log.critical(msg)
            #     if not self.embedded:
            #         os._exit(1)  # Force exit if not embedded
            #     else:
            #         self.quit()  # Attempt graceful quit if embedded

            except (KeyboardInterrupt, RuntimeError):  # If Ctrl+C is hit again during input
                log.warning(
                    "SEQUENCER: Interrupted during interrupt handling. Quitting.")
                self.quit()
            except Exception as e:  # Add a broad catch here for debugging
                sys.stderr.write(
                    f"ERROR IN INTERRUPT HANDLER: {
                        type(e).__name__}: {e}\n")
                log.error("Error in interrupt handler", exc_info=True)
                # Fallback to quitting if the handler itself fails
                self.quit()
            finally:
                signal.signal(signal.SIGINT, self.interrupt)  # Re-hook

        def pause(self):
            '''
            Put the pause attribute in the running template to True.
            It will be checked in a template method.
            '''
            if self.tpl and hasattr(self.tpl, 'paused'):
                msg = f"SEQUENCER: pausing template {
                    self.tpl.name} in ob {
                    self.ob_file}"
                log.warning(msg)
                self.tpl.paused = True
            else:
                log.warning(
                    "SEQUENCER: No active template to pause or template doesn't support pausing.")

        def resume(self):
            '''
            Restore the pause attribute in the template class to its
            original state. It will be checked in a template method.
            '''
            if self.tpl and hasattr(self.tpl, 'paused'):
                msg = f"SEQUENCER: resuming template {
                    self.tpl.name} in ob {
                    self.ob_file}"
                log.warning(msg)
                self.tpl.paused = False
            else:
                log.warning(
                    "SEQUENCER: No active template to resume or template doesn't support pausing.")

        def abort_current_template(self):
            '''
            Abort the currently running template. The sequencer will pass to the
            following one, if any.
            '''
            if self.tpl and hasattr(
                    self.tpl,
                    'aborted') and hasattr(
                    self.tpl,
                    'abort'):
                msg = f"SEQUENCER: Aborting current template {
                    self.tpl.name} in OB {
                    self.ob_file}."
                log.error(msg)
                self.error.append(msg)
                self.tpl.aborted = True  # Signal the template to stop its
                # current paragraph self.tpl.abort() # Call template's
                # specific abort logic (e.g., stop camera)
            else:
                log.warning(
                    "SEQUENCER: No active template to abort or template doesn't support aborting.")

        def quit(self):
            '''Abort the execution of the whole OB'''

            msg = f"SEQUENCER: Quit detected during OB {self.ob_file}."
            log.error(msg)
            self.error.append(msg)
            self.quitting = True  # Signal the main execution loop to stop

            if self.tpl and hasattr(
                    self.tpl,
                    'aborted') and hasattr(
                    self.tpl,
                    'abort'):
                log.warning(
                    "SEQUENCER: Aborting current template as part of quitting.")
                self.abort_current_template()

            if not self.embedded:
                log.info("SEQUENCER: Exiting CLI.")
                sys.exit(1)  # Exit if running standalone

    def cli():
        '''
        Command line interface for sequencer
        '''

        parser = argparse.ArgumentParser(
            description="Noctua Observatory Sequencer: Loads and executes Observation Blocks (OBs) or individual template scripts.",
            formatter_class=argparse.RawTextHelpFormatter)
        parser.add_argument(
            "target",
            nargs='?',  # Makes it optional
            help=(
                "Path to the JSON Observation Block (OB) file to execute.\n"
                "If not provided and --script is not used, help is shown.\n"
                "Example: defaults/bias.json\n"
                "If the path is not absolute and the file is not in the current directory,\n"
                "it will be searched for within the 'noctua.defaults' package resources."
            )
        )
        parser.add_argument(
            "-s",
            "--script",
            metavar="TEMPLATE_NAME_OR_PATH",
            help=(
                "Execute a single template script directly.\n"
                "Provide the name of the template (e.g., 'lampsoff') or a path to a .py file.\n"
                "If just a name, it's assumed to be in 'noctua.templates'.\n"
                "Requires --params to be specified.\n"
                "Example: -s lampsoff --params '{}'"))
        parser.add_argument(
            "-p", "--params",
            metavar="JSON_STRING",
            help=(
                "JSON string of parameters for the template when using --script.\n"
                "Example: --params '{\"repeat\": 5, \"filter\": \"R\"}'"
            ),
            default="{}"  # Default to empty JSON object
        )

        args = parser.parse_args()
        seq = Sequencer(embedded=False)  # For CLI, always run not embedded

        # If no arguments are given, or only 'target' is missing without --script
        if not args.target and not args.script:
            parser.print_help()
            sys.exit(0)

        if args.script:
            if not args.params:
                log.error("Error: --params must be provided when using --script.")
                parser.print_help()
                sys.exit(1)
            try:
                script_params = json.loads(args.params)
            except json.JSONDecodeError:
                log.error(
                    f"Error: Invalid JSON string for --params: {args.params}")
                parser.print_help()
                sys.exit(1)

            seq.load_script(args.script, script_params)
        elif args.target:
            # Logic to load from CWD or package resources
            ob_file_path = Path(args.target)
            if not ob_file_path.is_absolute() and not ob_file_path.exists():
                try:
                    # Create noctua/defaults/__init__.py (can be empty) if it
                    # doesn't exist
                    with importlib.resources.path("noctua.defaults", ob_file_path.name) as p:
                        log.info(
                            f"Loading OB '{
                                ob_file_path.name}' from package resources.")
                        seq.load_file(str(p))
                except FileNotFoundError:
                    log.error(
                        f"OB file '{
                            args.target}' not found in CWD or package resources (noctua.defaults).")
                    sys.exit(1)
            else:
                seq.load_file(args.target)
        else:
            # Should not happen due to the check above, but as a safeguard
            parser.print_help()
            sys.exit(0)

        if not seq.error:  # Only execute if loading was successful
            seq.execute()
        else:
            log.error(f"Failed to load OB/Script. Errors: {seq.error}")
            sys.exit(1)  # Exit with error if loading failed

    if __name__ == "__main__":
        # If called alone
        cli()