# -*- coding: utf-8 -*-
"""
SoloEngine : 打包引擎模块

@file packager.py
@description 打包引擎 - AgenticFlow打包和导出模块
@author Sh4rlock
@date 2026-04-09

功能描述：
本模块提供以下核心功能：
    - AgenticFlow打包为可执行文件
    - 生成Dockerfile和docker-compose配置
    - 支持ZIP格式导出
    - 生成主程序入口
    - Skills依赖打包

依赖:
    - os: 操作系统接口
    - json: JSON处理
    - uuid: UUID生成
    - zipfile: ZIP文件处理
    - shutil: 文件操作
    - typing: 类型注解支持
    - dataclasses: 数据类支持
    - datetime: 日期时间处理
    - pathlib: 路径处理

使用示例:
    - from app.core.packager import Packager, PackageConfig
    - packager = Packager()
    - config = PackageConfig(name="my_flow", version="1.0.0")
    - result = await packager.create_package("project_name", config)

使用场景：
    - 导出AgenticFlow为独立可执行包
    - 部署到生产环境
    - 分享工作流
"""

import os
import json
import uuid
import zipfile
import shutil
import logging
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, field
from datetime import datetime
from zoneinfo import ZoneInfo
from pathlib import Path

from app.core.config import settings

logger = logging.getLogger(__name__)


@dataclass
class PackageConfig:
    """打包配置。"""
    name: str
    version: str = "1.0.0"
    description: str = ""
    author: str = ""
    entry_point: str = "main"
    runtime: str = "python"
    dependencies: List[str] = field(default_factory=list)
    environment_vars: Dict[str, str] = field(default_factory=dict)
    include_files: List[str] = field(default_factory=list)
    exclude_files: List[str] = field(default_factory=list)


@dataclass
class PackageResult:
    """打包结果。"""
    package_id: str
    name: str
    version: str
    output_path: str
    size_bytes: int
    created_at: str
    files_count: int


class Packager:
    """打包引擎。"""

    def __init__(self, output_dir: Optional[str] = None):
        if output_dir:
            self.output_dir = Path(output_dir)
        else:
            from app.core.data_paths import DataPaths
            self.output_dir = Path(DataPaths.get_data_root()) / "packages"
        self.output_dir.mkdir(parents=True, exist_ok=True)

    def _get_flows_dir(self) -> Path:
        """获取流程目录。"""
        return Path(__file__).parent.parent.parent / "flows"

    def _get_skills_dir(self) -> Path:
        """获取 Skills 目录。"""
        return Path(__file__).parent.parent.parent / "skills"

    def _generate_dockerfile(self, config: PackageConfig) -> str:
        """生成 Dockerfile。"""
        deps = ' '.join(config.dependencies) if config.dependencies else 'fastapi uvicorn'
        env_vars = ''.join(f"ENV {k}={v}\n" for k, v in config.environment_vars.items())
        return f'''# {config.name} - Generated by SoloEngine
FROM python:3.11-slim

WORKDIR /app

# Install dependencies
RUN pip install --no-cache-dir {deps}

# Copy application files
COPY . .

# Set environment variables
{env_vars}
# Expose port
EXPOSE 8000

# Run application
CMD ["python", "{config.entry_point}.py"]
'''

    def _generate_main_py(
        self,
        flow_data: Dict[str, Any],
        config: PackageConfig,
        project_id: str = None,
    ) -> str:
        """生成主程序文件。"""
        return f'''#!/usr/bin/env python3
"""Auto-generated by SoloEngine."""

import json
import asyncio
import logging
import os
from typing import Dict, Any, List, Optional
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Flow configuration
FLOW_CONFIG = {json.dumps(flow_data, indent=2)}

# Project context for sandboxed file system
PROJECT_ID = "{project_id}" if "{project_id}" else None
SANDBOX_BASE_PATH = None
CURRENT_PROJECT = None


def set_project_context(project_id: str, base_path: str):
    """设置项目上下文（沙箱）。"""
    global PROJECT_ID, SANDBOX_BASE_PATH, CURRENT_PROJECT
    PROJECT_ID = project_id
    SANDBOX_BASE_PATH = base_path
    CURRENT_PROJECT = {{"id": project_id, "path": base_path}}
    logger.info(f"Project context set: {{project_id}}, base_path={{base_path}}")


def get_sandboxed_path(relative_path: str) -> str:
    """获取沙箱内的安全路径。"""
    if not SANDBOX_BASE_PATH:
        raise ValueError("No project selected. Please set project context first.")
    
    relative_path = relative_path.lstrip("/\\\\")
    absolute_path = os.path.abspath(os.path.join(SANDBOX_BASE_PATH, relative_path))
    
    if not absolute_path.startswith(SANDBOX_BASE_PATH):
        raise PermissionError(f"Access denied: path '{{relative_path}}' is outside the sandbox")
    
    return absolute_path


@dataclass
class NodeResult:
    """节点执行结果。"""
    node_id: str
    status: str
    output: Optional[Dict[str, Any]] = None
    error: Optional[str] = None
    started_at: Optional[str] = None
    completed_at: Optional[str] = None


class AgentRunner:
    """Agent runner for packaged flow."""

    def __init__(self):
        self.nodes: Dict[str, Dict[str, Any]] = {{}}
        self.edges: List[Dict[str, Any]] = []
        self.execution_context: Dict[str, Any] = {{}}
        self.results: List[NodeResult] = []

    async def initialize(self):
        """Initialize the flow."""
        self.nodes = {{n["id"]: n for n in FLOW_CONFIG.get("nodes", [])}}
        self.edges = FLOW_CONFIG.get("edges", [])
        logger.info(f"Initialized flow with {{len(self.nodes)}} nodes and {{len(self.edges)}} edges")

    def _get_entry_nodes(self) -> List[str]:
        """获取入口节点（没有入边的节点）。"""
        target_nodes = {{e["target"] for e in self.edges}}
        return [nid for nid in self.nodes.keys() if nid not in target_nodes]

    def _get_next_nodes(self, node_id: str) -> List[str]:
        """获取下一个要执行的节点。"""
        return [e["target"] for e in self.edges if e["source"] == node_id]

    async def execute_node(self, node_id: str, input_data: Dict[str, Any]) -> NodeResult:
        """执行单个节点。"""
        node = self.nodes.get(node_id)
        if not node:
            return NodeResult(
                node_id=node_id,
                status="failed",
                error=f"Node {{node_id}} not found"
            )

        started_at = datetime.now(ZoneInfo(settings.DEFAULT_TIMEZONE)).isoformat()
        logger.info(f"Executing node: {{node_id}} ({{node.get('data', {{}}).get('agentType', 'unknown')}})")

        try:
            node_data = node.get("data", {{}})
            agent_type = node_data.get("agentType", "executor")
            
            # 根据节点类型执行不同的逻辑
            if agent_type == "orchestrator":
                output = await self._execute_orchestrator(node_id, node_data, input_data)
            elif agent_type == "planner":
                output = await self._execute_planner(node_id, node_data, input_data)
            else:
                output = await self._execute_executor(node_id, node_data, input_data)

            return NodeResult(
                node_id=node_id,
                status="completed",
                output=output,
                started_at=started_at,
                completed_at=datetime.now(ZoneInfo(settings.DEFAULT_TIMEZONE)).isoformat()
            )
        except Exception as e:
            logger.error(f"Node {{node_id}} failed: {{e}}")
            return NodeResult(
                node_id=node_id,
                status="failed",
                error=str(e),
                started_at=started_at,
                completed_at=datetime.now(ZoneInfo(settings.DEFAULT_TIMEZONE)).isoformat()
            )

    async def _execute_orchestrator(self, node_id: str, node_data: Dict, input_data: Dict) -> Dict[str, Any]:
        """执行协调者节点。"""
        # 协调者节点负责全局调度和决策
        return {{
            "node_id": node_id,
            "type": "orchestrator",
            "input": input_data,
            "decision": "continue",
            "context_update": {{}}
        }}

    async def _execute_planner(self, node_id: str, node_data: Dict, input_data: Dict) -> Dict[str, Any]:
        """执行规划者节点。"""
        # 规划者节点负责任务分解和规划
        return {{
            "node_id": node_id,
            "type": "planner",
            "input": input_data,
            "plan": [],
            "context_update": {{}}
        }}

    async def _execute_executor(self, node_id: str, node_data: Dict, input_data: Dict) -> Dict[str, Any]:
        """执行执行者节点。"""
        # 执行者节点负责具体任务执行
        return {{
            "node_id": node_id,
            "type": "executor",
            "input": input_data,
            "result": "executed",
            "context_update": {{}}
        }}

    async def run(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
        """Run the flow."""
        entry_nodes = self._get_entry_nodes()
        
        if not entry_nodes:
            return {{
                "status": "failed",
                "error": "No entry nodes found",
                "results": []
            }}

        # 从入口节点开始执行
        for entry_node_id in entry_nodes:
            result = await self.execute_node(entry_node_id, input_data)
            self.results.append(result)
            
            if result.status == "completed":
                # 继续执行后续节点
                await self._execute_downstream(entry_node_id, result.output or {{}})

        return {{
            "status": "success" if all(r.status == "completed" for r in self.results) else "partial",
            "input": input_data,
            "nodes_count": len(self.nodes),
            "executed_count": len(self.results),
            "results": [{{"node_id": r.node_id, "status": r.status}} for r in self.results]
        }}

    async def _execute_downstream(self, node_id: str, context: Dict[str, Any]):
        """执行下游节点。"""
        next_nodes = self._get_next_nodes(node_id)
        
        for next_node_id in next_nodes:
            result = await self.execute_node(next_node_id, context)
            self.results.append(result)
            
            if result.status == "completed":
                await self._execute_downstream(next_node_id, {{**context, **(result.output or {{}})}})


async def main():
    """Main entry point."""
    import argparse
    
    parser = argparse.ArgumentParser(description="Packaged SoloEngine Agent")
    parser.add_argument("--input", "-i", type=str, default="{{}}", help="Input JSON data")
    parser.add_argument("--file", "-f", type=str, help="Input JSON file path")
    args = parser.parse_args()

    # 获取输入数据
    if args.file:
        with open(args.file, "r", encoding="utf-8") as f:
            input_data = json.load(f)
    else:
        try:
            input_data = json.loads(args.input)
        except json.JSONDecodeError:
            input_data = {{"input": args.input}}

    runner = AgentRunner()
    await runner.initialize()
    result = await runner.run(input_data)
    print(json.dumps(result, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    asyncio.run(main())
'''

    def _generate_readme(self, config: PackageConfig) -> str:
        """生成 README 文件。"""
        return f'''# {config.name}

{config.description}

## Version
{config.version}

## Author
{config.author}

## Usage

### Docker
```bash
docker build -t {config.name}:{config.version} .
docker run -p 8000:8000 {config.name}:{config.version}
```

### Python
```bash
python main.py
```

## Dependencies
{chr(10).join(f"- {dep}" for dep in config.dependencies) if config.dependencies else "No external dependencies"}

## Generated by
SoloEngine - {datetime.now(ZoneInfo(settings.DEFAULT_TIMEZONE)).isoformat()}
'''

    def _generate_requirements(self, dependencies: List[str]) -> str:
        """生成 requirements.txt。"""
        return "\n".join(dependencies) if dependencies else "# No dependencies"

    def _generate_compose(self, config: PackageConfig) -> str:
        """生成 docker-compose.yml。"""
        return f'''version: '3.8'

services:
  {config.name.replace("-", "_")}:
    build: .
    ports:
      - "8000:8000"
    environment:
      {''.join(f"- {k}={v}" for k, v in config.environment_vars.items()) if config.environment_vars else "# No custom environment variables"}
    restart: unless-stopped
'''

    async def create_package(
        self,
        project_name: str,
        config: Optional[PackageConfig] = None,
    ) -> PackageResult:
        """创建包。"""
        # 加载流程数据
        flows_dir = self._get_flows_dir()
        flow_file = flows_dir / f"{project_name}.json"

        if not flow_file.exists():
            raise FileNotFoundError(f"Project '{project_name}' not found")

        with open(flow_file, "r", encoding="utf-8") as f:
            flow_data = json.load(f)

        # 使用默认配置或提供的配置
        if config is None:
            config = PackageConfig(
                name=project_name,
                description=f"Packaged from {project_name}",
            )

        # 创建包 ID
        package_id = str(uuid.uuid4())

        # 创建临时目录
        temp_dir = self.output_dir / f"temp_{package_id}"
        temp_dir.mkdir(parents=True, exist_ok=True)

        try:
            # 复制流程数据
            with open(temp_dir / "flow.json", "w", encoding="utf-8") as f:
                json.dump(flow_data, f, indent=2, ensure_ascii=False)

            # 生成主程序
            main_py = self._generate_main_py(flow_data, config)
            with open(temp_dir / f"{config.entry_point}.py", "w", encoding="utf-8") as f:
                f.write(main_py)

            # 生成 Dockerfile
            dockerfile = self._generate_dockerfile(config)
            with open(temp_dir / "Dockerfile", "w", encoding="utf-8") as f:
                f.write(dockerfile)

            # 生成 README
            readme = self._generate_readme(config)
            with open(temp_dir / "README.md", "w", encoding="utf-8") as f:
                f.write(readme)

            # 生成 requirements.txt
            requirements = self._generate_requirements(config.dependencies)
            with open(temp_dir / "requirements.txt", "w", encoding="utf-8") as f:
                f.write(requirements)

            # 生成 docker-compose.yml
            compose = self._generate_compose(config)
            with open(temp_dir / "docker-compose.yml", "w", encoding="utf-8") as f:
                f.write(compose)

            # 复制 Skills
            skills_dir = self._get_skills_dir()
            if skills_dir.exists():
                dest_skills = temp_dir / "skills"
                if skills_dir.exists():
                    shutil.copytree(skills_dir, dest_skills, dirs_exist_ok=True)

            # 创建 ZIP 包
            output_file = self.output_dir / f"{config.name}-{config.version}.zip"
            files_count = 0

            with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf:
                for root, dirs, files in os.walk(temp_dir):
                    for file in files:
                        file_path = Path(root) / file
                        arc_name = file_path.relative_to(temp_dir)
                        zf.write(file_path, arc_name)
                        files_count += 1

            # 获取包大小
            size_bytes = output_file.stat().st_size

            # 清理临时目录
            shutil.rmtree(temp_dir)

            logger.info(f"Created package: {output_file}")

            return PackageResult(
                package_id=package_id,
                name=config.name,
                version=config.version,
                output_path=str(output_file),
                size_bytes=size_bytes,
                created_at=datetime.now(ZoneInfo(settings.DEFAULT_TIMEZONE)).isoformat(),
                files_count=files_count,
            )

        except Exception as e:
            # 清理临时目录
            if temp_dir.exists():
                shutil.rmtree(temp_dir)
            raise

    async def list_packages(self) -> List[Dict[str, Any]]:
        """列出所有包。"""
        packages = []

        for zip_file in self.output_dir.glob("*.zip"):
            try:
                stat = zip_file.stat()
                packages.append({
                    "name": zip_file.stem,
                    "path": str(zip_file),
                    "size_bytes": stat.st_size,
                    "created_at": datetime.fromtimestamp(stat.st_ctime, tz=ZoneInfo(settings.DEFAULT_TIMEZONE)).isoformat(),
                })
            except Exception as e:
                logger.error(f"Failed to read package {zip_file}: {e}")

        return sorted(packages, key=lambda x: x["created_at"], reverse=True)

    async def delete_package(self, package_name: str) -> bool:
        """删除包。"""
        package_file = self.output_dir / f"{package_name}.zip"

        if not package_file.exists():
            # 尝试匹配版本号
            matches = list(self.output_dir.glob(f"{package_name}*.zip"))
            if matches:
                package_file = matches[0]
            else:
                return False

        try:
            package_file.unlink()
            logger.info(f"Deleted package: {package_file}")
            return True
        except Exception as e:
            logger.error(f"Failed to delete package: {e}")
            return False

    async def get_package_info(self, package_name: str) -> Optional[Dict[str, Any]]:
        """获取包信息。"""
        package_file = self.output_dir / f"{package_name}.zip"

        if not package_file.exists():
            matches = list(self.output_dir.glob(f"{package_name}*.zip"))
            if matches:
                package_file = matches[0]
            else:
                return None

        try:
            stat = package_file.stat()

            with zipfile.ZipFile(package_file, "r") as zf:
                files = zf.namelist()

            return {
                "name": package_file.stem,
                "path": str(package_file),
                "size_bytes": stat.st_size,
                "created_at": datetime.fromtimestamp(stat.st_ctime, tz=ZoneInfo(settings.DEFAULT_TIMEZONE)).isoformat(),
                "files": files,
                "files_count": len(files),
            }
        except Exception as e:
            logger.error(f"Failed to read package info: {e}")
            return None


packager = Packager()
