# kernel OSA 1.0 by Java86
# basic OSAF implication
import json
import os
import pathlib
import platform
import shlex
import socket
import subprocess
import sys
from datetime import datetime

from libs.osaf.osaf_core import OSAF_Core, osa_sample_conf
from libs.pyfap.fap import FAP
from libs.spider_logger import logger
from libs.spider_logger.logger import SpiderLog


class AppEnv:
    def __init__(self, main_script_dir):
        self.debug = True
        self.silent = False
        self.argv = sys.argv
        self.shellenv = os.environ
        self.d_script = main_script_dir


class kOSA:
    def __init__(self):
        r = {
            "os_family": platform.system().lower(),
            "os_arch": platform.machine().lower(),
            "os_hostname": socket.gethostname(),
            "os_path_separator": '/'
        }
        self.os_conf = r

    def appenv_kick_t1(
            self,
            env,
            app_id="generic_app",
            app_displayname="Generic Application",
            app_binname="python main.py",
            nosavelog = False
    ):
        env.app_id = app_id
        env.app_displayname = app_displayname
        env.app_binname = app_binname


        env.argproc = FAP(env.app_id, env.app_displayname, env.app_binname)


        env.d_cur = os.path.abspath(os.curdir)
        env.app_root = env.d_script

        env.d_etc = os.path.join(env.app_root, "etc/")
        env.d_var = os.path.join(env.app_root, "var/")
        env.d_log = os.path.join(env.app_root, "var/logs")

        logconf = self.conf_read(f'{env.d_etc}spiderlog/conf.json', default=logger.DEFAULT_CONFIG)

        logconf['core']['no_save_log'] = nosavelog
        logconf['core']['log_dir'] = os.path.join(env.d_log, f"{env.app_id}-{self.t_now_file()}-{self.os_conf['os_hostname']}")
        env.log = logger.SpiderLog(logconf)

        env.kosa = self
        osa_conf = self.conf_read(os.path.join(env.d_etc, "osaf/conf.json"), osa_sample_conf)
        # env.d_osa_assets = env.app_root + "assets"
        env.osa : OSAF_Core = self.get_osa(env.log, env.app_root, osa_conf)
        env.log.d("Used 'kick t1' for init OSAF")


        return env

    def get_osa(self, logger: SpiderLog, app_dir, conf) -> OSAF_Core:
        """Получить объект OSAF для текущей ОС"""
        conf.update({
            'os_conf': self.os_conf,
        })

        logger.dd(f"OS: {self.os_conf} conf: {conf}")

        os_mapping = {
            'windows': OSAF_Core,
            'win32': OSAF_Core,
            'nt': OSAF_Core,
            'linux': OSAF_Core,
            'linux2': OSAF_Core
        }

        os_class = os_mapping.get(self.os_conf['os_family'])
        if os_class:
            return os_class(logger, app_dir, conf)

        logger.e(f"Unsupported OS: {self.os_conf['os_family']}")
        raise OSError(f"Unsupported operating system: {self.os_conf['os_family']}")


    def isWin(self):
        return self.os_conf['os_family'] in ['windows', 'win32', 'nt']

    def isLinux(self):
        return self.os_conf['os_family'] in ['linux', 'linux2']

    def isMac(self):
        return self.os_conf['os_family'] in ['darwin', 'macos']

    # Return True if current os is Unix-Like
    def isUnix(self):
        return self.isLinux() or self.isMac()

    def p_exec(self, command, capture_output=False, shell=False):
        try:
            if shell and isinstance(command, list):
                # Для shell=True команда должна быть строкой
                command = " ".join(str(x) for x in command)
            elif not shell and isinstance(command, str):
                # Для shell=False команда должна быть списком
                command = shlex.split(command)

            result = subprocess.run(
                command,
                shell=shell,
                capture_output=capture_output,
                text=True,
                encoding="UTF-8",
                timeout=60
            )
            return result.returncode == 0, result.stdout if capture_output else ""
        except Exception as e:
            print(f"Command failed: {command}, error: {e}")
            return False, str(e)

    def conf_read(self, path, default=None):
        try:
            path = pathlib.Path(path)
            if not path.is_file():
                return default
            with open(path, 'r', encoding='UTF-8') as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError, OSError) as e:
            print(f"Config read error: {e}, using default")
            return default

    def conf_write(self, path, data, indent=4, ensure_ascii=False):
        """
        Записать конфигурацию в JSON файл

        Args:
            path (str): Путь к файлу
            data (dict): Данные для записи
            indent (int): Отступ для форматирования JSON
            ensure_ascii (bool): Экранирование не-ASCII символов

        Returns:
            bool: Успешность операции
        """
        try:
            path = pathlib.Path(path)

            # Создать директорию если не существует
            path.parent.mkdir(parents=True, exist_ok=True)

            with open(path, 'w', encoding='UTF-8') as f:
                json.dump(data, f, indent=indent, ensure_ascii=ensure_ascii)

            return True

        except (IOError, OSError, TypeError) as e:
            print(f"Config write error: {e}")
            return False
        except Exception as e:
            print(f"Unexpected error writing config: {e}")
            return False
    def conf_write_a(self, path, data, indent=4, ensure_ascii=False, backup_existing=True):
        """
        Записать конфигурацию в JSON файл с расширенными возможностями

        Args:
            path (str): Путь к файлу
            data (dict): Данные для записи
            indent (int): Отступ для форматирования JSON
            ensure_ascii (bool): Экранирование не-ASCII символов
            backup_existing (bool): Создать бэкап существующего файла

        Returns:
            bool: Успешность операции
        """
        try:
            path = pathlib.Path(path)

            # Валидация входных данных
            if not isinstance(data, (dict, list)):
                raise TypeError("Data must be dictionary or list")

            # Создать директорию если не существует
            path.parent.mkdir(parents=True, exist_ok=True)

            # Бэкап существующего файла
            if backup_existing and path.exists():
                backup_path = path.with_suffix(f'.backup_{self.t_now_file()}.json')
                try:
                    path.rename(backup_path)
                except OSError as e:
                    print(f"Backup failed: {e}, continuing without backup")

            # Временный файл для атомарной записи
            temp_path = path.with_suffix('.tmp')

            with open(temp_path, 'w', encoding='UTF-8') as f:
                json.dump(data, f, indent=indent, ensure_ascii=ensure_ascii, sort_keys=True)

            # Атомарная замена файла
            if path.exists():
                path.unlink()
            temp_path.rename(path)

            return True

        except (IOError, OSError, TypeError) as e:
            print(f"Config write error for {path}: {e}")
            # Удалить временный файл в случае ошибки
            try:
                temp_path.unlink()
            except:
                pass
            return False
        except Exception as e:
            print(f"Unexpected error writing config: {e}")
            return False

    def conf_merge(self, path, new_data, indent=4):
        """
        Объединить новую конфигурацию с существующей

        Args:
            path (str): Путь к файлу
            new_data (dict): Новые данные для объединения
            indent (int): Отступ для форматирования

        Returns:
            bool: Успешность операции
        """
        try:
            # Прочитать существующую конфигурацию
            existing_data = self.conf_read(path, {})

            # Рекурсивное объединение словарей
            def deep_merge(source, destination):
                for key, value in source.items():
                    if isinstance(value, dict) and key in destination and isinstance(destination[key], dict):
                        deep_merge(value, destination[key])
                    else:
                        destination[key] = value
                return destination

            merged_data = deep_merge(new_data, existing_data.copy())

            # Записать объединенную конфигурацию
            return self.conf_write(path, merged_data, indent=indent)

        except Exception as e:
            print(f"Config merge error: {e}")
            return False

    def conf_validate_schema(self, data, schema):
        """
        Простая валидация схемы конфигурации

        Args:
            data (dict): Данные конфигурации
            schema (dict): Ожидаемая схема {key: type}

        Returns:
            tuple: (bool, str) - успех и сообщение об ошибке
        """
        for key, expected_type in schema.items():
            if key not in data:
                return False, f"Missing required key: {key}"
            if not isinstance(data[key], expected_type):
                return False, f"Key '{key}' must be {expected_type}, got {type(data[key])}"
        return True, "Valid"


    def f_read_s(self, path):

        path = self.p(path)
        if not path:
            return None
        with open(path, 'r', encoding="UTF-8") as file:
            d = file.read()

        return d



    def f_write_s(self, path, d, append=False):
        try:
            path = pathlib.Path(path)
            path.parent.mkdir(parents=True, exist_ok=True)  # Более идиоматично

            mode = 'a' if append else 'w'
            with open(path, mode, encoding="UTF-8") as f:
                f.write(d)
            return True
        except IOError as e:
            print(f"Write error: {e}")
            return False


    # Check, file, directory or link exists
    def f_is_dir(self, path):
        return os.path.isdir(self.p(path))

    def f_is_file(self, path):
        return os.path.isfile(self.p(path))

    def f_is_link(self, path):
        return os.path.islink(self.p(path))

    def f_exists(self, path):
        return self.f_is_file(path) or self.f_is_dir(path) or self.f_is_link(path)

    # Make dir with subdirs
    def f_mkdir(self, path):
        os.makedirs(path)

    def t_now_file(self):
        now = datetime.now()
        return now.strftime('%Y-%m-%d_%H-%M-%S')

    def p(self, path):
        """Нормализовать путь"""
        return os.path.normpath(os.path.expanduser(path))






