"""
System Commands Module.
~~~~~~~~~~~~~~~~~~~~~~

This module registers system commands for the ChitraGupt CLI framework. These commands
provide various file system operations, terminal manipulations, plugin control, and other
utilities reserved for internal use.

Functions
---------
load(framework: client.ChitraGuptClientCLI) -> None
    Load all registered system commands into the CLI framework.

Registered Commands
-------------------
clear      - Clears the terminal.
use        - Switches to a specified plugin.
touch      - Creates or updates a file.
rm         - Deletes a file.
mkdir      - Creates a directory.
rmdir      - Deletes a directory.
stat       - Displays file details.
find       - Searches for files matching a substring.
debug      - Switches the logger debug mode.
exec       - Executes a single-line code snippet.
read       - Reads and logs the content of a file.
ls         - Lists directory entries.
cd         - Changes the current working directory.
chitrapad  - Launches the ChitraPad editor.
du         - Calculates disk usage for a directory.
wc         - Counts lines, words, and characters in a file.
help       - Displays a list of available system commands.
phelp      - Displays a list of available plugin commands.
exit       - Exits the application.
exif       - Extracts and displays EXIF data from an image.
mv         - Moves (renames) a file.
cp         - Copies a file.
"""

import datetime
import os
import pathlib
import shutil
import typing

import colorama
import PIL.ExifTags
import PIL.Image

import chitragupt.abc.generic as abc_generic
import chitragupt.client as client
import chitragupt.core.logger as logger
import chitragupt.core.plugin as plugin
import chitragupt.modules.misc.cli_board as cli_board
import chitragupt.modules.misc.universal_board as universal_board

# Initialize the system-level plugin for internal commands.
system_commands = plugin.ChitraguptPlugin(
    name="root",
    description="Commands reserved by the internal system.",
)


@system_commands.register(command_name="clear")
def clear_command(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
) -> None:
    """
    Clear the console.

    Erases the terminal content and logs the action.
    """
    try:
        framework.logger.clear_console()
        os.system("cls" if os.name == "nt" else "clear")
        framework.logger.log(
            message="Cleared the terminal.",
            log_type=logger.LogType.DEBUG,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error clearing the terminal: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="use",
    plugin=abc_generic.DataType.STRING,
)
def use_plugin(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    plugin: str,
) -> None:
    """
    Switch to a specified plugin.

    Parameters
    ----------
    plugin : str
        The name of the plugin to switch to.
    """
    framework.use_plugin(plugin=plugin)


@system_commands.register(
    command_name="touch",
    filename=abc_generic.DataType.STRING,
)
def create_file(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    filename: str,
) -> None:
    """
    Create or update a file.

    Parameters
    ----------
    filename : str
        The file path to create or update.
    """
    try:
        path = pathlib.Path(filename)
        path.touch(exist_ok=True)
        framework.logger.log(
            message=f"Created or updated file: {filename}",
            log_type=logger.LogType.INFO,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error creating file '{filename}': {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="rm",
    filename=abc_generic.DataType.STRING,
)
def delete_file(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    filename: str,
) -> None:
    """
    Delete a file.

    Parameters
    ----------
    filename : str
        The file path to delete.
    """
    try:
        path = pathlib.Path(filename)
        if path.is_file():
            path.unlink()
            framework.logger.log(
                message=f"Deleted file: {filename}",
                log_type=logger.LogType.INFO,
            )
        else:
            framework.logger.log(
                message=f"File not found: {filename}",
                log_type=logger.LogType.ERROR,
            )
    except Exception as e:
        framework.logger.log(
            message=f"Error deleting file '{filename}': {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="mkdir",
    directory=abc_generic.DataType.STRING,
)
def create_directory(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    directory: str,
) -> None:
    """
    Create a directory.

    Parameters
    ----------
    directory : str
        The directory path to create.
    """
    try:
        path = pathlib.Path(directory)
        path.mkdir(
            parents=True,
            exist_ok=True,
        )
        framework.logger.log(
            message=f"Created directory: {directory}",
            log_type=logger.LogType.INFO,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error creating directory '{directory}': {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="rmdir",
    directory=abc_generic.DataType.STRING,
)
def delete_directory(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    directory: str,
) -> None:
    """
    Delete a directory.

    Parameters
    ----------
    directory : str
        The directory path to delete.
    """
    try:
        path = pathlib.Path(directory)
        if path.is_dir():
            path.rmdir()
            framework.logger.log(
                message=f"Deleted directory: {directory}",
                log_type=logger.LogType.INFO,
            )
        else:
            framework.logger.log(
                message=f"Directory not found: {directory}",
                log_type=logger.LogType.ERROR,
            )
    except Exception as e:
        framework.logger.log(
            message=f"Error deleting directory '{directory}': {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="stat",
    filename=abc_generic.DataType.STRING,
)
def file_details(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    filename: str,
) -> None:
    """
    Display file details.

    Parameters
    ----------
    filename : str
        The file path to inspect.
    """
    try:
        path = pathlib.Path(filename)
        if path.exists() and path.is_file():
            details = (
                f"\nFile: {path.name}\n"
                f"Size: {path.stat().st_size} bytes\n"
                f"Modified: {datetime.datetime.fromtimestamp(path.stat().st_mtime)}\n"
            )
            framework.logger.log(
                message=details,
                log_type=logger.LogType.INFO,
            )
        else:
            framework.logger.log(
                message=f"File not found: {filename}",
                log_type=logger.LogType.ERROR,
            )
    except Exception as e:
        framework.logger.log(
            message=f"Error retrieving file details: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="find",
    name=abc_generic.DataType.STRING,
)
def search_file(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    name: str,
) -> None:
    """
    Search for files whose names contain a given substring.

    Parameters
    ----------
    name : str
        The substring to search for.
    """
    try:
        matches: typing.List[str] = []
        for (
            root,
            dirs,
            files,
        ) in os.walk(os.getcwd()):
            for file in files:
                if name.lower() in file.lower():
                    matches.append(
                        os.path.join(
                            root,
                            file,
                        )
                    )
        if matches:
            framework.logger.log(
                message="Found files:\n" + "\n".join(matches),
                log_type=logger.LogType.INFO,
            )
        else:
            framework.logger.log(
                message=f"No files found matching: {name}",
                log_type=logger.LogType.INFO,
            )
    except Exception as e:
        framework.logger.log(
            message=f"Error searching for file '{name}': {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="debug-mode",
    switch=abc_generic.DataType.BOOL,
)
def debug_switch(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    switch: bool,
) -> None:
    """
    Switch the debug mode on the logger.

    Parameters
    ----------
    switch : bool
        True to enable debug mode; False to disable.
    """
    try:
        if switch:
            framework.logger.log(
                message="Setting debug mode to 'on'",
                log_type=logger.LogType.INFO,
            )
            framework.logger.config.level = logger.LogLevel.DEBUG
        else:
            framework.logger.log(
                message="Setting debug mode to 'off'.",
                log_type=logger.LogType.DEBUG,
            )
            framework.logger.config.level = logger.LogLevel.BASIC
    except Exception as e:
        framework.logger.log(
            message=f"Error setting debug mode: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="exec",
    code=abc_generic.DataType.STRING,
)
def exec_command(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    code: str,
) -> None:
    """
    Execute a single-line code snippet.

    Parameters
    ----------
    code : str
        The code to execute.
    """
    try:
        exec(code)
        framework.logger.log(
            message=f"Executed code: {code}",
            log_type=logger.LogType.DEBUG,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error executing code: {code}. Exception: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="read",
    path=abc_generic.DataType.STRING,
)
def read_file(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    path: str,
) -> None:
    """
    Read and log the content of a specified file.

    Parameters
    ----------
    path : str
        The file path to read.
    """
    try:
        if not os.path.exists(path):
            framework.logger.log(
                message=f"File not found: {path}",
                log_type=logger.LogType.ERROR,
            )
            return
        if not os.path.isfile(path):
            framework.logger.log(
                message=f"Specified path is not a file: {path}",
                log_type=logger.LogType.ERROR,
            )
            return
        with open(
            path,
            "r",
            encoding="utf-8",
        ) as f:
            framework.logger.log(
                message="\n" + f.read(),
                log_type=logger.LogType.INFO,
            )
    except PermissionError:
        framework.logger.log(
            message=f"Permission denied for file: {path}",
            log_type=logger.LogType.ERROR,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error reading file: {path}. Exception: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(command_name="ls")
def list_entries(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    path: typing.Optional[str] = None,
) -> None:
    """
    List entries in a directory in a formatted table.

    Parameters
    ----------
    path : Optional[str]
        The directory to list (defaults to the current working directory).
    """
    directory = path or os.getcwd()
    try:
        if not os.path.exists(directory):
            framework.logger.log(
                message=f"Directory does not exist: {directory}",
                log_type=logger.LogType.ERROR,
            )
            return
        entries = os.listdir(directory)
        if not entries:
            framework.logger.log(
                message=f"No entries found in '{directory}'.",
                log_type=logger.LogType.INFO,
            )
            return
        header = (
            f"{'Name':<20} | {'Type':<6} | {'Extension':<9} | {'Created':<19}"
        )
        divider = "-" * len(header)
        formatted_entries: typing.List[str] = [
            header,
            divider,
        ]
        for entry in entries:
            full_path = os.path.join(
                directory,
                entry,
            )
            if os.path.isdir(full_path):
                entry_type = "Folder"
                ext = "N/A"
            else:
                entry_type = "File"
                ext = os.path.splitext(entry)[-1] or "N/A"
            creation_time = datetime.datetime.fromtimestamp(
                os.path.getctime(full_path)
            ).strftime("%Y-%m-%d %H:%M:%S")
            formatted_entries.append(
                f"{entry:<20} | {entry_type:<6} | {ext:<9} | {creation_time:<19}"
            )
        framework.logger.log(
            message=f"\nEntries in '{directory}':\n"
            + "\n".join(formatted_entries),
            log_type=logger.LogType.INFO,
        )
    except PermissionError:
        framework.logger.log(
            message=f"Permission denied while accessing '{directory}'.",
            log_type=logger.LogType.ERROR,
        )
    except Exception as e:
        framework.logger.log(
            message=f"An unexpected error occurred while listing entries: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="cd",
    folder=abc_generic.DataType.OSTRING,
    back=abc_generic.DataType.OBOOL,
)
def change_directory(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    folder: typing.Optional[str] = None,
    back: typing.Optional[bool] = False,
) -> None:
    """
    Change the current working directory.

    Parameters
    ----------
    folder : Optional[str]
        The directory to change to. If not provided, logs the current directory.
    back : Optional[bool]
        If True, moves to the parent directory.
    """
    try:
        if folder:
            if not os.path.exists(folder):
                framework.logger.log(
                    message=f"Directory does not exist: {folder}",
                    log_type=logger.LogType.ERROR,
                )
                return
            if not os.path.isdir(folder):
                framework.logger.log(
                    message=f"Specified path is not a directory: {folder}",
                    log_type=logger.LogType.ERROR,
                )
                return
            os.chdir(folder)
            framework._current_dir = os.getcwd()  # type: ignore
            framework.logger.log(
                message=f"Changed the current directory to {os.getcwd()}",
                log_type=logger.LogType.INFO,
            )
        elif back:
            os.chdir(
                os.path.abspath(
                    os.path.join(
                        os.getcwd(),
                        "..",
                    )
                )
            )
            framework.logger.log(
                message=f"Moved back to parent directory: {os.getcwd()}",
                log_type=logger.LogType.INFO,
            )
        else:
            framework.logger.log(
                message=f"Current directory is {os.getcwd()}",
                log_type=logger.LogType.INFO,
            )
    except PermissionError:
        framework.logger.log(
            message=f"Permission denied for directory: {folder}",
            log_type=logger.LogType.ERROR,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error changing directory: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="chitrapad",
    custom_path=abc_generic.DataType.OSTRING,
)
def chitrapad_start(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    custom_path: typing.Optional[str] = None,
) -> None:
    """
    Start the ChitraPad editor.

    Parameters
    ----------
    custom_path : Optional[str]
        Custom path for launching the editor. If valid, launches with that path; otherwise, uses defaults.
    """
    framework.logger.log(
        message="Trying to start Chitrapad",
        log_type=logger.LogType.DEBUG,
    )
    try:
        if custom_path and pathlib.Path(custom_path).is_dir():
            framework.logger.log(
                message=f"Starting Chitrapad with custom path: {custom_path}",
                log_type=logger.LogType.DEBUG,
            )
            try:
                editor = cli_board.ChitraEditor()
                editor.run(custom_path)
                framework.logger.log(
                    message="Started Chitrapad with custom path",
                    log_type=logger.LogType.DEBUG,
                )
            except Exception as exe:
                framework.logger.log(
                    message=f"Failed to launch Chitrapad with custom path. Error: {exe.args}",
                    log_type=logger.LogType.ERROR,
                )
                notepad_universal = universal_board.ChitraPadUniversal()
                notepad_universal.run()
        else:
            try:
                editor = cli_board.ChitraEditor()
                editor.run()
                framework.logger.log(
                    message="Started Chitrapad with default path",
                    log_type=logger.LogType.DEBUG,
                )
            except Exception as exe:
                framework.logger.log(
                    message=f"Failed to launch Chitrapad. Error: {exe.args}",
                    log_type=logger.LogType.ERROR,
                )
                notepad_universal = universal_board.ChitraPadUniversal()
                notepad_universal.run()
    except Exception as exe:
        framework.logger.log(
            message=f"Failed to launch Chitrapad. Error: {exe.args}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="du",
    directory=abc_generic.DataType.OSTRING,
)
def disk_usage(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    directory: typing.Optional[str] = None,
) -> None:
    """
    Calculate and log the total disk usage in a directory.

    Parameters
    ----------
    directory : Optional[str]
        The directory to calculate disk usage for. Defaults to the current working directory.
    """
    try:
        path = pathlib.Path(directory or os.getcwd())
        if path.is_dir():
            total_size = sum(
                f.stat().st_size for f in path.glob("**/*") if f.is_file()
            )
            framework.logger.log(
                message=f"Total disk usage in '{path}': {total_size} bytes",
                log_type=logger.LogType.INFO,
            )
        else:
            framework.logger.log(
                message=f"Directory not found: {directory}",
                log_type=logger.LogType.ERROR,
            )
    except Exception as e:
        framework.logger.log(
            message=f"Error calculating disk usage: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="wc",
    filename=abc_generic.DataType.STRING,
)
def word_count(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    filename: str,
) -> None:
    """
    Count lines, words, and characters in a file.

    Parameters
    ----------
    filename : str
        The file path to analyze.
    """
    try:
        path = pathlib.Path(filename)
        if path.exists() and path.is_file():
            with path.open(
                "r",
                encoding="utf-8",
            ) as file:
                content = file.read()
                lines = content.count("\n")
                words = len(content.split())
                chars = len(content)
                framework.logger.log(
                    message=f"Lines: {lines}, Words: {words}, Characters: {chars}",
                    log_type=logger.LogType.INFO,
                )
        else:
            framework.logger.log(
                message=f"File not found: {filename}",
                log_type=logger.LogType.ERROR,
            )
    except Exception as e:
        framework.logger.log(
            message=f"Error counting words: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(command_name="help")
def show_help(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
) -> None:
    """Display a list of available system commands and their descriptions."""
    try:
        header = "\nAvailable Commands:\n" + "=" * 20
        command_list = "\n\n".join(
            [
                f"  {index + 1}. {colorama.Style.BRIGHT}{cmd.name}{colorama.Style.RESET_ALL}"
                f"{colorama.Fore.GREEN}\n     {cmd.function.__doc__.strip() if cmd.function.__doc__ else 'No description available.'}"
                for index, cmd in enumerate(system_commands.commands)
            ]
        )
        footer = "=" * 20
        full_help_message = f"{header}\n{command_list}\n{footer}"
        framework.logger.log(
            message=full_help_message,
            log_type=logger.LogType.INFO,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error displaying help: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(command_name="phelp")
def show_plugin_help(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
) -> None:
    """Display a list of available plugin commands and their descriptions."""
    try:
        if framework.current_plugin:  # type: ignore
            header = "\nAvailable Plugin Commands:\n" + "=" * 20
            command_list = "\n\n".join(
                [
                    f"  {index + 1}. {colorama.Style.BRIGHT}{cmd.name}{colorama.Style.RESET_ALL}"
                    f"{colorama.Fore.GREEN}\n     {cmd.function.__doc__.strip() if cmd.function.__doc__ else 'No description available.'}"
                    for index, cmd in enumerate(
                        framework.current_plugin.commands
                    )  # type: ignore
                ]
            )
            footer = "=" * 20
            full_help_message = f"{header}\n{command_list}\n{footer}"
            framework.logger.log(
                message=full_help_message,
                log_type=logger.LogType.INFO,
            )
        else:
            framework.logger.log(
                message="There is no plugin enabled.",
                log_type=logger.LogType.WARNING,
            )
    except Exception as e:
        framework.logger.log(
            message=f"Error displaying plugin help: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(command_name="exit")
def exit_command(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
) -> None:
    """
    Exit the application.

    If a plugin lock is enabled, exits the plugin context.
    """
    framework.handle_exit()


@system_commands.register(
    command_name="exif",
    image_path=abc_generic.DataType.STRING,
)
def exif_command(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    image_path: str,
) -> None:
    """
    Extract and display EXIF information from an image.

    Parameters
    ----------
    image_path : str
        Path to the image file.
    """
    try:
        image = PIL.Image.open(image_path)
        exif_data = image.getexif()
        if not exif_data:
            framework.logger.log(
                message="No EXIF data found in the image.",
                log_type=logger.LogType.INFO,
            )
            return
        exif_info = "\nEXIF Information:\n" + "-" * 30
        for (
            tag,
            value,
        ) in exif_data.items():
            tag_name = PIL.ExifTags.TAGS.get(
                tag,
                tag,
            )
            if isinstance(
                value,
                bytes,
            ):
                try:
                    value = value.decode(
                        "utf-8",
                        errors="ignore",
                    )
                except UnicodeDecodeError:
                    value = str(value)
            elif (
                isinstance(
                    value,
                    int,
                )
                and tag_name == "DateTime"
            ):
                try:
                    value = datetime.datetime.strptime(
                        str(value),
                        "%Y:%m:%d %H:%M:%S",
                    ).strftime("%B %d, %Y %H:%M:%S")
                except ValueError:
                    value = str(value)
            exif_info += f"\n{tag_name:<25}: {value}"
        exif_info += "\n" + "-" * 30
        framework.logger.log(
            message=exif_info,
            log_type=logger.LogType.INFO,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error extracting EXIF data: {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="mv",
    source=abc_generic.DataType.STRING,
    destination=abc_generic.DataType.STRING,
)
def move_file(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    source: str,
    destination: str,
) -> None:
    """
    Move (rename) a file from source to destination.

    Parameters
    ----------
    source : str
        The current file path.
    destination : str
        The new file path.
    """
    try:
        src = pathlib.Path(source)
        dest = pathlib.Path(destination)
        if not src.exists() or not src.is_file():
            framework.logger.log(
                message=f"Source file not found: {source}",
                log_type=logger.LogType.ERROR,
            )
            return
        src.rename(dest)
        framework.logger.log(
            message=f"Moved file from {source} to {destination}",
            log_type=logger.LogType.INFO,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error moving file from '{source}' to '{destination}': {e}",
            log_type=logger.LogType.ERROR,
        )


@system_commands.register(
    command_name="cp",
    source=abc_generic.DataType.STRING,
    destination=abc_generic.DataType.STRING,
)
def copy_file(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
    source: str,
    destination: str,
) -> None:
    """
    Copy a file from source to destination.

    Parameters
    ----------
    source : str
        The file path to copy.
    destination : str
        The destination file path.
    """
    try:
        src = pathlib.Path(source)
        dest = pathlib.Path(destination)
        if not src.exists() or not src.is_file():
            framework.logger.log(
                message=f"Source file not found: {source}",
                log_type=logger.LogType.ERROR,
            )
            return
        dest.parent.mkdir(
            parents=True,
            exist_ok=True,
        )
        shutil.copy(
            src,
            dest,
        )
        framework.logger.log(
            message=f"Copied file from {source} to {destination}",
            log_type=logger.LogType.INFO,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error copying file from '{source}' to '{destination}': {e}",
            log_type=logger.LogType.ERROR,
        )


def load(
    framework: client.ChitraGuptClientCLI,
    cmd_str: str,
) -> None:
    """
    Load all registered system commands into the CLI framework.

    Parameters
    ----------
    framework : client.ChitraGuptClientCLI
        The CLI framework instance into which commands are loaded.
    """
    try:
        for command in system_commands.commands:
            framework.logger.log(
                message=f"Loaded {system_commands.name} command: '{command.name}'",
                log_type=logger.LogType.DEBUG,
            )
            framework.system_commands.append(command)  # type: ignore
        framework.logger.log(
            message="System and root commands have been loaded and are working in context.",
            log_type=logger.LogType.DEBUG,
        )
    except Exception as e:
        framework.logger.log(
            message=f"Error loading system commands: {e}",
            log_type=logger.LogType.ERROR,
        )
