# -*- coding: utf-8 -*-
"""
Author  : CraftPilot
"""

import os
import json
import logging
import re
import subprocess
import yaml

from inspect import signature, Signature
from functools import wraps
from typing import *
from .const import *
from .dson import DsonMetaInfo, DsonManager, craftmethod
from .dson_field import Field, FieldCategory
from .env_detector import EnvType, get_env_config, validate_env_config


def export_env(history_only=False, include_builds=False):
    """ Capture `conda env export` output """
    cmd = ['conda', 'env', 'export']
    if history_only:
        cmd.append('--from-history')
        if include_builds:
            raise ValueError('Cannot include build versions with "from history" mode')
    if not include_builds:
        cmd.append('--no-builds')
    output = subprocess.check_output(cmd)
    return yaml.safe_load(output)


def _is_history_dep(d, history_deps):
    if not isinstance(d, str):
        return False
    d_prefix = re.sub(r'=.*', '', d)
    return d_prefix in history_deps


def _get_pip_deps(full_deps):
    for dep in full_deps:
        if isinstance(dep, dict) and 'pip' in dep:
            return dep


def _combine_env_data(env_data_full, env_data_hist):
    deps_full = env_data_full['dependencies']
    deps_hist = env_data_hist['dependencies']
    deps = [dep for dep in deps_full if _is_history_dep(dep, deps_hist)]

    pip_deps = _get_pip_deps(deps_full)

    env_data = {}
    env_data['channels'] = env_data_full['channels']
    env_data['dependencies'] = deps
    env_data['dependencies'].append(pip_deps)

    return env_data


def generate_env_file(base_path):
    env_data_full = export_env()
    env_data_hist = export_env(history_only=True)
    env_data = _combine_env_data(env_data_full, env_data_hist)

    with open(os.path.join(base_path, 'environment.yaml'), 'w', encoding='utf-8') as file:
        yaml.dump(env_data, file)


def create_dson_from_pda(func: Callable, tag, dson_meta_info: DsonMetaInfo) -> Dict:
    cda_json = {}
    try:
        func_sig = signature(func)
        func_name = func.__name__
        inputs, params = get_inputs(func_sig)
        outputs = get_outputs(func_sig)
        doc = get_doc(func)
        cda_json_cda = {dson_inputs_tag: inputs, dson_params_tag: params,
                        dson_outputs_tag: outputs, dson_doc_tag: doc,
                       dson_meta_field_tag: dson_meta_info.to_json()}
        cda_json = {func_name + "." + tag: cda_json_cda}
    except Exception as e:
        logging.exception(e)
    finally:
        return cda_json


def _import_dson_modules(dson_modules):
    """导入 dson_config 中配置的所有模块"""
    for module in dson_modules:
        if isinstance(module, str):
            # only module name
            try:
                __import__(module, globals(), locals(), [], 0)
            except Exception as e:
                print(e)
                exit(1)
        else:
            for module_name in module:
                # if has class name
                try:
                    __import__(module_name, globals(), locals(), module[module_name], 0)
                except Exception as e:
                    print(e)
                    exit(1)


def generate_dson_files(base_path, dson_config):
    index = 0
    dson_path = os.path.join(base_path, "dson")
    dson_plugin_name = dson_config['name']
    dson_modules = dson_config['modules']

    # get all dson class
    _import_dson_modules(dson_modules)

    # path prepare
    if not os.path.exists(dson_path):
        os.makedirs(dson_path, exist_ok=True)

    # generate dson file
    for cls_name, cls in DsonManager().dsonMap.items():
        for name, value in vars(cls).items():
            if isinstance(value, craftmethod):
                func = value.__func__
                dson_meta_info = DsonMetaInfo(cls_name, cls.if_in_server, dson_plugin_name)
                json_data = create_dson_from_pda(func, cls.label, dson_meta_info)
                file_name = name + '.dson'
                file_path = os.path.join(dson_path, file_name).replace('\\', '/')
                fp = open(file_path, "w")
                json.dump(json_data, fp)
                index += 1


def generate_dson_init_script(base_path, dson_config):
    """
    生成虚拟环境启动脚本
    
    支持的虚拟环境类型：
    - conda: Anaconda/Miniconda 环境
    - venv: Python 内置虚拟环境
    - virtualenv: 第三方虚拟环境工具
    - poetry: Poetry 包管理工具
    - pipenv: Pipfile 风格的包管理工具
    """
    # 获取环境配置
    env_config = get_env_config(dson_config, base_path)
    env_type = env_config['env_type']
    
    # 验证环境配置
    is_valid, error_msg = validate_env_config(env_config, base_path)
    if not is_valid:
        logging.warning(f"虚拟环境配置验证警告: {error_msg}")
    
    # 根据环境类型生成脚本
    if os.name != 'posix':
        script_name = 'dson_init.bat'
        script_string = _generate_windows_script(env_type, env_config)
    else:
        script_name = 'dson_init.sh'
        script_string = _generate_linux_script(env_type, env_config)
    
    # 写入脚本文件
    craftpilot_path = os.path.join(base_path, '.craftpilot')
    if not os.path.exists(craftpilot_path):
        os.makedirs(craftpilot_path, exist_ok=True)
    
    script_path = os.path.join(craftpilot_path, script_name)
    with open(script_path, 'w', encoding='utf-8') as f:
        f.write(script_string)
    
    # Linux 下设置可执行权限
    if os.name == 'posix':
        os.chmod(script_path, 0o755)


def _generate_windows_script(env_type: EnvType, env_config: dict) -> str:
    """
    生成 Windows 启动脚本 (.bat)
    """
    if env_type == EnvType.CONDA:
        conda_env = env_config.get('conda_env', 'base')
        return f"""@echo off
cd %~dp0
cd ..
call conda activate {conda_env}
if errorlevel 1 (
    echo 错误: 无法激活 Conda 环境 '{conda_env}'
    echo 请确认环境已创建: conda create -n {conda_env} python
    exit /b 1
)
python dson_main.py
"""
    
    elif env_type in (EnvType.VENV, EnvType.VIRTUALENV):
        env_path = env_config.get('env_path', '.\\venv')
        # 处理相对路径
        if env_path.startswith('./'):
            env_path = '.' + env_path[1:].replace('/', '\\')
        return f"""@echo off
cd %~dp0
cd ..
if exist "{env_path}\\Scripts\\activate.bat" (
    call "{env_path}\\Scripts\\activate.bat"
) else (
    echo 错误: 找不到虚拟环境 '{env_path}'
    echo 请先创建虚拟环境: python -m venv {env_path}
    exit /b 1
)
python dson_main.py
"""
    
    elif env_type == EnvType.POETRY:
        return """@echo off
cd %~dp0
cd ..
where poetry >nul 2>&1
if errorlevel 1 (
    echo 错误: 未找到 Poetry，请先安装: pip install poetry
    exit /b 1
)
poetry run python dson_main.py
"""
    
    elif env_type == EnvType.PIPENV:
        return """@echo off
cd %~dp0
cd ..
where pipenv >nul 2>&1
if errorlevel 1 (
    echo 错误: 未找到 pipenv，请先安装: pip install pipenv
    exit /b 1
)
pipenv run python dson_main.py
"""
    
    else:
        # 默认回退到 Conda
        conda_env = env_config.get('conda_env', 'base')
        return f"""@echo off
cd %~dp0
cd ..
call conda activate {conda_env}
python dson_main.py
"""


def _generate_linux_script(env_type: EnvType, env_config: dict) -> str:
    """
    生成 Linux/macOS 启动脚本 (.sh)
    """
    if env_type == EnvType.CONDA:
        conda_env = env_config.get('conda_env', 'base')
        return f"""#!/usr/bin/env bash
set -e

# 获取脚本所在目录
BASEDIR=$(dirname $(readlink -f "$0"))
cd "$BASEDIR"
cd ..

# 初始化 Conda
if [ -f ~/.bashrc ]; then
    source ~/.bashrc
fi
if [ -f ~/.bash_profile ]; then
    source ~/.bash_profile
fi

# 激活 Conda 环境
eval "$(conda shell.bash hook)" 2>/dev/null || true
conda activate {conda_env}
if [ $? -ne 0 ]; then
    echo "错误: 无法激活 Conda 环境 '{conda_env}'"
    echo "请确认环境已创建: conda create -n {conda_env} python"
    exit 1
fi

python dson_main.py
"""
    
    elif env_type in (EnvType.VENV, EnvType.VIRTUALENV):
        env_path = env_config.get('env_path', './venv')
        return f"""#!/usr/bin/env bash
set -e

# 获取脚本所在目录
BASEDIR=$(dirname $(readlink -f "$0"))
cd "$BASEDIR"
cd ..

# 激活虚拟环境
if [ -f "{env_path}/bin/activate" ]; then
    source "{env_path}/bin/activate"
else
    echo "错误: 找不到虚拟环境 '{env_path}'"
    echo "请先创建虚拟环境: python -m venv {env_path}"
    exit 1
fi

python dson_main.py
"""
    
    elif env_type == EnvType.POETRY:
        return """#!/usr/bin/env bash
set -e

# 获取脚本所在目录
BASEDIR=$(dirname $(readlink -f "$0"))
cd "$BASEDIR"
cd ..

# 检查 Poetry 是否安装
if ! command -v poetry &> /dev/null; then
    echo "错误: 未找到 Poetry，请先安装: pip install poetry"
    exit 1
fi

poetry run python dson_main.py
"""
    
    elif env_type == EnvType.PIPENV:
        return """#!/usr/bin/env bash
set -e

# 获取脚本所在目录
BASEDIR=$(dirname $(readlink -f "$0"))
cd "$BASEDIR"
cd ..

# 检查 pipenv 是否安装
if ! command -v pipenv &> /dev/null; then
    echo "错误: 未找到 pipenv，请先安装: pip install pipenv"
    exit 1
fi

pipenv run python dson_main.py
"""
    
    else:
        # 默认回退到 Conda
        conda_env = env_config.get('conda_env', 'base')
        return f"""#!/usr/bin/env bash
BASEDIR=$(dirname $(readlink -f "$0"))
cd "$BASEDIR"
cd ..
source ~/.bash_profile 2>/dev/null || true
eval "$(conda shell.bash hook)" 2>/dev/null || true
conda activate {conda_env}
python dson_main.py
"""


def generate_dson_main(base_path, dson_config):
    # data prepare
    dson_plugin_name = dson_config['name']
    # import info to dson_main_head
    dson_main_head = f"""
import sys
import os
import json
import logging

# 兼容两种 import 方式：新版 craftpilot 包（pip install craftpilot）和旧版直接 import
try:
    import craftpilot.common.craftpilot as craftpilot
    from craftpilot.pypapi.dispatch import Dispatcher
    from craftpilot.pypapi.plugin_protocol import module_send_done, setup_logger
except ImportError:
    import common.craftpilot as craftpilot
    from pypapi.dispatch import Dispatcher
    from pypapi.plugin_protocol import module_send_done, setup_logger

"""
    # load necessary modules or classes to dson_main_import
    dson_main_import = ''
    try:
        for module in dson_config['modules']:
            if isinstance(module, str):
                # only module name
                dson_main_import += 'import ' + module + '\n'
            else:
                # has class name
                for module_name in module:
                    dson_main_import += 'from ' + module_name + ' import  '
                    for class_name in module[module_name]:
                        dson_main_import += class_name + ','
                    dson_main_import = dson_main_import[:-1] + '\n'
    except Exception as e:
        print(e)
        exit(1)
    # main function to dson_main_function
    dson_main_function = f"""

if __name__ == '__main__':
    dson_plugin_name = '{dson_plugin_name}'
    logger = logging.getLogger('pypapi_'+dson_plugin_name)
    
    current_path = os.path.split(os.path.realpath(__file__))[0] # path to dosn_path.py
    dot_craftpilot_path = os.path.join(current_path, '.craftpilot')

    setup_logger(logger, dot_craftpilot_path)
    
    module_send_done()
    while(True):
        json_file_name, work_path = input().split()
        json_error_data = {{}}
        try:
            json_data_error_file_name = os.path.join(work_path, 'error.json').replace("\\\\\\\\",'/') 
            with open(json_file_name, 'r') as f:
                json_data = json.load(f)
            if not json_data:
                print("didn't get dson data")
                module_send_done()
            Dispatcher().run_func(json_data, logger, work_path, craftpilot.pson_system_tag, json_error_data)
            # write back json_data
            with open(json_file_name, 'w') as f:
                json.dump(json_data, f)
        except Exception as e:
            json_error_data['Error_Tag'] = 'plugin {{dson_plugin_name}} error in main loop'
            json_error_data['Error_Info'] = str(e)
            logger.exception(e)
        finally:
            with open(json_data_error_file_name, 'w') as f:
                json_error_data = json.dump(json_error_data, f)
        module_send_done()
"""
    # merge
    dson_main_string = dson_main_head + dson_main_import + dson_main_function
    with open(os.path.join(base_path, 'dson_main.py'), 'w', encoding='utf-8') as f:
        f.write(dson_main_string)


def generate_test_files(base_path, dson_config):
    """
    为每个 @craftmethod 生成对应的 Python 测试文件
    测试文件放在 base_path/tests/ 目录下，与 dson/ 目录平级
    
    测试生成器依赖 tools.testing 模块（内部工具，不随 pip 包发布）。
    查找顺序：
    1. 直接 import（适用于已将 PythonPackage 加入 sys.path 的场景）
    2. 基于本文件位置回溯到 PythonPackage/tools/（内部开发环境）
    3. 都找不到则跳过测试生成
    """
    try:
        from tools.testing.test_generator import generate_test_file_for_method
    except ImportError:
        try:
            # 内部开发环境：本文件在 PythonPackage/craftpilot/pypapi/dson_create.py
            # 回溯到 PythonPackage/ 目录，将其加入 sys.path 以便导入 tools.testing
            import sys
            _this_dir = os.path.dirname(os.path.abspath(__file__))          # craftpilot/pypapi/
            _package_root = os.path.dirname(os.path.dirname(_this_dir))     # PythonPackage/
            if _package_root not in sys.path:
                sys.path.insert(0, _package_root)
            # 清除可能被缓存的 tools 模块（避免与插件项目的 tools/ 包冲突）
            for mod_name in list(sys.modules.keys()):
                if mod_name == 'tools' or mod_name.startswith('tools.'):
                    del sys.modules[mod_name]
            from tools.testing.test_generator import generate_test_file_for_method
        except ImportError:
            print("提示: 未找到测试生成模块 (tools.testing)，跳过测试文件生成")
            return 0

    tests_path = os.path.join(base_path, "tests")
    if not os.path.exists(tests_path):
        os.makedirs(tests_path, exist_ok=True)

    test_count = 0
    for cls_name, cls in DsonManager().dsonMap.items():
        for name, value in vars(cls).items():
            if isinstance(value, craftmethod):
                func = value.__func__
                test_file_path = generate_test_file_for_method(
                    cls_name=cls_name,
                    method_name=name,
                    func=func,
                    output_dir=tests_path,
                    cls=cls
                )
                if test_file_path:
                    test_count += 1
                    print(f"  生成测试: {test_file_path}")

    return test_count


def generate(base_path, dson_config):
    generate_dson_files(base_path, dson_config)
    generate_dson_init_script(base_path, dson_config)
    generate_dson_main(base_path, dson_config)
    generate_test_files(base_path, dson_config)



def get_inputs(sig: Signature):
    inputs = {}
    params = {}
    input_index = 0
    for param in sig.parameters.values():
        if param.default is param.empty:
            # no default value, we put it in inputs
            param_type = param.annotation
            assert issubclass(param_type, Field)
            field = param_type.create_json_field(FieldCategory.Input, param.name)
            input_index += 1
            inputs[dson_input_tag + str(input_index)] = field
        else:
            # have default value, we put it in params
            param_type = param.annotation
            field = param_type.create_json_field(FieldCategory.Param, param.name)
            if not param_type.support_param:
                raise Exception(str(param_type) + " can't be param")
            # param_value = param_type(param.default)
            # param_value.get_output(field, "Default Value")
            param_type.get_param_default(field, param.default)
            params[param.name] = field
    return inputs, params


def get_outputs(sig: Signature):
    outputs = {}
    return_annotation = sig.return_annotation
    output_index = 0
    if return_annotation is None:
        return outputs
    if hasattr(return_annotation, '__origin__') and (return_annotation.__origin__ == tuple or return_annotation.__origin__ == Tuple):
        for ele_type in return_annotation.__args__:
            output_index += 1
            outputs["output" + str(output_index)] = ele_type.create_json_field(FieldCategory.Output)
    else:
        output_index += 1
        outputs['output' + str(output_index)] = return_annotation.create_json_field(FieldCategory.Output)
    return outputs


def get_doc(sig: Callable):
    return sig.__doc__


# for test
if __name__ == "__main__":
    from dson_field import Int, Int2, String, ListField

    def foo(a: Int, b: ListField[Int2] = [1, 2], c: Int2 = (1, 2)) -> String:
        pass


    jj = create_dson_from_pda(foo)
    file_path = "test.json"
    with open(file_path, "w") as fp:
        json.dump(jj, fp)
