"""
Module: macro_recorder.py
Description: Records and replays action sequences for automation purposes.
"""

import logging
import time
import json
from typing import List, Dict, Any, Callable

# Configure logging
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(levelname)s - %(message)s')

class MacroRecorder:
    """
    Records and replays macro sequences.
    """

    def __init__(self) -> None:
        """
        Initializes the MacroRecorder with an empty recording.
        """
        self.recording: List[Dict[str, Any]] = []
        self.is_recording: bool = False

    def start_recording(self) -> None:
        """
        Starts the macro recording.
        """
        self.recording = []
        self.is_recording = True
        logging.info("Macro recording started.")

    def stop_recording(self) -> None:
        """
        Stops the macro recording.
        """
        self.is_recording = False
        logging.info("Macro recording stopped.")

    def record_action(self, action_name: str, action_args: Dict[str, Any]) -> None:
        """
        Records a single action with its arguments.

        Args:
            action_name (str): The name of the action to record.
            action_args (Dict[str, Any]): A dictionary of arguments for the action.
        """
        if self.is_recording:
            timestamp = time.time()
            action_data = {
                "timestamp": timestamp,
                "action": action_name,
                "args": action_args
            }
            self.recording.append(action_data)
            logging.debug(f"Recorded action: {action_name} with args: {action_args}")
        else:
            logging.warning("Attempted to record action while not recording.")

    def play_recording(self, action_handlers: Dict[str, Callable[..., None]]) -> None:
        """
        Plays back the recorded macro.

        Args:
            action_handlers (Dict[str, Callable[..., None]]): A dictionary mapping action names to their handler functions.
        """
        if not self.recording:
            logging.warning("No recording to play.")
            return

        logging.info("Playing macro recording...")
        start_time = self.recording[0]["timestamp"] if self.recording else 0

        for action_data in self.recording:
            action_name = action_data["action"]
            action_args = action_data["args"]
            timestamp = action_data["timestamp"]

            # Calculate delay based on the start time
            delay = timestamp - start_time

            time.sleep(delay)

            if action_name in action_handlers:
                try:
                    action_handlers[action_name](**action_args)
                    logging.debug(f"Executed action: {action_name} with args: {action_args}")
                except Exception as e:
                    logging.error(f"Error executing action {action_name}: {e}")
            else:
                logging.warning(f"No handler found for action: {action_name}")

            start_time = timestamp # ensure next delay is correct

        logging.info("Macro recording playback finished.")

    def save_recording(self, filepath: str) -> None:
        """
        Saves the recording to a JSON file.

        Args:
            filepath (str): The path to save the recording to.
        """
        try:
            with open(filepath, "w") as f:
                json.dump(self.recording, f, indent=4)
            logging.info(f"Macro recording saved to: {filepath}")
        except IOError as e:
            logging.error(f"Error saving recording to {filepath}: {e}")

    def load_recording(self, filepath: str) -> None:
        """
        Loads a recording from a JSON file.

        Args:
            filepath (str): The path to load the recording from.
        """
        try:
            with open(filepath, "r") as f:
                self.recording = json.load(f)
            logging.info(f"Macro recording loaded from: {filepath}")
        except FileNotFoundError:
            logging.error(f"File not found: {filepath}")
        except json.JSONDecodeError as e:
            logging.error(f"Error decoding JSON from {filepath}: {e}")
        except IOError as e:
            logging.error(f"Error loading recording from {filepath}: {e}")

if __name__ == '__main__':
    # Example Usage:

    def example_action(arg1: str, arg2: int) -> None:
        """
        An example action handler.
        """
        logging.info(f"Executing example_action with arg1: {arg1}, arg2: {arg2}")

    # Create MacroRecorder instance
    recorder = MacroRecorder()

    # Start recording
    recorder.start_recording()

    # Record some actions
    time.sleep(1)
    recorder.record_action("example_action", {"arg1": "hello", "arg2": 123})
    time.sleep(0.5)
    recorder.record_action("example_action", {"arg1": "world", "arg2": 456})
    time.sleep(2)

    # Stop recording
    recorder.stop_recording()

    # Save recording
    recorder.save_recording("macro_recording.json")

    # Load recording
    loaded_recorder = MacroRecorder()
    loaded_recorder.load_recording("macro_recording.json")

    # Define action handlers
    action_handlers = {
        "example_action": example_action
    }

    # Play recording
    loaded_recorder.play_recording(action_handlers)