#!/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()
