import json
import os
import shutil
import subprocess
import threading
import time
from datetime import datetime
from types import SimpleNamespace

from croniter import croniter
from loguru import logger
from typing_extensions import Any, Optional, Union

from syftbox.lib import (
    SyftPermission,
    get_file_hash,
    perm_file_path,
)


def find_and_run_script(task_path, extra_args):
    script_path = os.path.join(task_path, "run.sh")
    env = os.environ.copy()  # Copy the current environment

    # Check if the script exists
    if os.path.isfile(script_path):
        # Set execution bit (+x)
        os.chmod(script_path, os.stat(script_path).st_mode | 0o111)

        # Check if the script has a shebang
        with open(script_path, "r") as script_file:
            first_line = script_file.readline().strip()
            has_shebang = first_line.startswith("#!")

        # Prepare the command based on whether there's a shebang or not
        command = (
            [script_path] + extra_args
            if has_shebang
            else ["/bin/bash", script_path] + extra_args
        )

        try:
            result = subprocess.run(
                command,
                cwd=task_path,
                check=True,
                capture_output=True,
                text=True,
                env=env,
            )

            # logger.info("✅ Script run.sh executed successfully.")
            return result
        except Exception as e:
            logger.info("Error running shell script", e)
    else:
        raise FileNotFoundError(f"run.sh not found in {task_path}")


DEFAULT_SCHEDULE = 10000
DESCRIPTION = "Runs Apps"
RUNNING_APPS = {}
DEFAULT_APPS_PATH = os.path.abspath(
    os.path.join(os.path.dirname(__file__), "..", "..", "..", "default_apps")
)


def copy_default_apps(apps_path):
    if not os.path.exists(DEFAULT_APPS_PATH):
        logger.info(f"Default apps directory not found: {DEFAULT_APPS_PATH}")
        return

    for app in os.listdir(DEFAULT_APPS_PATH):
        src_app_path = os.path.join(DEFAULT_APPS_PATH, app)
        dst_app_path = os.path.join(apps_path, app)

        if os.path.isdir(src_app_path):
            if os.path.exists(dst_app_path):
                logger.info(f"App already installed at: {dst_app_path}")
                # shutil.rmtree(dst_app_path)
            else:
                shutil.copytree(src_app_path, dst_app_path)
            logger.info(f"Copied default app: {app}")


def dict_to_namespace(data) -> Union[SimpleNamespace, list, Any]:
    if isinstance(data, dict):
        return SimpleNamespace(
            **{key: dict_to_namespace(value) for key, value in data.items()}
        )
    elif isinstance(data, list):
        return [dict_to_namespace(item) for item in data]
    else:
        return data


def load_config(path: str) -> Optional[SimpleNamespace]:
    try:
        with open(path, "r") as f:
            data = json.load(f)
        return dict_to_namespace(data)
    except Exception:
        return None


def run_apps(client_config):
    # create the directory
    apps_path = client_config.sync_folder + "/" + "apps"
    os.makedirs(apps_path, exist_ok=True)

    # Copy default apps if they don't exist
    copy_default_apps(apps_path)

    # add the first perm file
    file_path = perm_file_path(apps_path)
    if os.path.exists(file_path):
        perm_file = SyftPermission.load(file_path)
    else:
        logger.info(f"> {client_config.email} Creating Apps Permfile")
        try:
            perm_file = SyftPermission.datasite_default(client_config.email)
            perm_file.save(file_path)
        except Exception as e:
            logger.error("Failed to create perm file")
            logger.exception(e)

    apps = os.listdir(apps_path)
    for app in apps:
        app_path = os.path.abspath(apps_path + "/" + app)
        if os.path.isdir(app_path):
            app_config = load_config(app_path + "/" + "config.json")
            if app_config is None:
                run_app(client_config, app_path)
            elif RUNNING_APPS.get(app, None) is None:
                logger.info("⏱  Scheduling a  new app run.")
                thread = threading.Thread(
                    target=run_custom_app_config,
                    args=(client_config, app_config, app_path),
                )
                thread.start()
                RUNNING_APPS[app] = thread


def output_published(app_output, published_output) -> bool:
    return (
        os.path.exists(app_output)
        and os.path.exists(published_output)
        and get_file_hash(app_output) == get_file_hash(published_output)
    )


def run_custom_app_config(client_config, app_config, path):
    env = os.environ.copy()
    app_name = os.path.basename(path)

    # Update environment with any custom variables in app_config
    app_envs = getattr(app_config.app, "env", {})
    if not isinstance(app_envs, dict):
        app_envs = vars(app_envs)
    env.update(app_envs)

    # Retrieve the cron-style schedule from app_config
    cron_iter = None
    interval = None
    cron_schedule = getattr(app_config.app.run, "schedule", None)
    if cron_schedule is not None:
        base_time = datetime.now()
        cron_iter = croniter(cron_schedule, base_time)
    elif getattr(app_config.app.run, "interval", None) is not None:
        interval = app_config.app.run.interval
    else:
        raise Exception(
            "There's no schedule configuration. Please add schedule or interval in your app config.json"
        )

    while True:
        current_time = datetime.now()
        logger.info(
            f"👟 Running {app_name} at scheduled time {current_time.strftime('%Y-%m-%d %H:%M:%S')}"
        )
        try:
            result = subprocess.run(
                app_config.app.run.command,
                cwd=path,
                check=True,
                capture_output=True,
                text=True,
                env=env,
            )
            logger.info(result.stdout)
            logger.error(result.stderr)
        except subprocess.CalledProcessError as e:
            logger.error(f"Error running {app_name}: {e}")
            logger.error(e.stderr)

        if cron_iter is not None:
            # Schedule the next exection
            next_execution = cron_iter.get_next(datetime)
            time_to_wait = int((next_execution - current_time).total_seconds())
            logger.info(
                f"⏲ Waiting for scheduled time. Current time: {current_time.strftime('%Y-%m-%d %H:%M:%S')}, Next execution: {next_execution.strftime('%Y-%m-%d %H:%M:%S')}"
            )
        else:
            time_to_wait = int(interval)
        time.sleep(time_to_wait)


def run_app(client_config, path):
    app_name = os.path.basename(path)

    extra_args = []
    try:
        logger.info(f"👟 Running {app_name} app", end="")
        result = find_and_run_script(path, extra_args)
        if hasattr(result, "returncode"):
            if "Already generated" not in str(result.stdout):
                logger.info("\n")
                logger.info(result.stdout)
            else:
                logger.info(" - no change")
            exit_code = result.returncode
            if exit_code != 0:
                logger.info(f"Error running: {app_name}", result.stdout, result.stderr)
    except Exception as e:
        logger.info(f"Failed to run. {e}")


def run(shared_state):
    # logger.info("> Running Apps")
    client_config = shared_state.client_config
    run_apps(client_config)
