from __future__ import annotations

import asyncio
import json
import os
import shutil
import tarfile
import threading
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

import yaml

from config import _expand_env_vars
from models import AppPackage, ParameterDef, TaskDefinition


class AppManagerError(Exception):
    """App 管理异常基类"""

    def __init__(self, message: str, status_code: int = 400):
        super().__init__(message)
        self.status_code = status_code


class AppManager:
    """App 包（.lldtp）管理器：安装、卸载、列出已安装应用。"""

    def __init__(self, apps_dir: Path):
        self._apps_dir = Path(apps_dir)
        self._apps_dir.mkdir(parents=True, exist_ok=True)
        self._apps: Dict[str, AppPackage] = {}  # key = "app_id@app_version"
        self._lock = asyncio.Lock()
        self._json_path = self._apps_dir / "apps.json"
        self._file_lock = threading.Lock()

    # ---------- 公共接口 ----------

    def list_apps(self) -> List[AppPackage]:
        return list(self._apps.values())

    def get_app(self, app_id: str, app_version: str) -> Optional[AppPackage]:
        return self._apps.get(f"{app_id}@{app_version}")

    def get_app_by_full_id(self, full_id: str) -> Optional[AppPackage]:
        return self._apps.get(full_id)

    async def install(
        self,
        lldtp_path: Path,
        builtin_definition_ids: Optional[set] = None,
    ) -> AppPackage:
        """安装一个 .lldtp 包。

        :param lldtp_path: 上传的 .lldtp 文件路径
        :param builtin_definition_ids: 内置任务定义 ID 集合，用于冲突检测
        :return: 安装后的 AppPackage
        :raises AppManagerError: 各种校验失败
        """
        builtin_definition_ids = builtin_definition_ids or set()
        lldtp_path = Path(lldtp_path)

        # 1. 扩展名校验
        if not lldtp_path.name.lower().endswith(".lldtp"):
            raise AppManagerError("Only .lldtp files are allowed", 400)

        async with self._lock:
            # 2. 创建临时目录解压
            tmp_dir = self._apps_dir / f".tmp_{uuid.uuid4().hex}"
            try:
                self._extract_lldtp(lldtp_path, tmp_dir)

                # 3. 读取并验证 manifest
                manifest_path = tmp_dir / "manifest.yaml"
                if not manifest_path.exists():
                    raise AppManagerError("manifest.yaml not found in package", 400)

                raw = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
                if not raw:
                    raise AppManagerError("manifest.yaml is empty", 400)
                raw = _expand_env_vars(raw)

                manifest = self._validate_manifest(raw)
                app_id = manifest["app_id"]
                app_version = manifest["app_version"]
                full_id = f"{app_id}@{app_version}"

                # 4. 检查是否与已安装版本冲突
                if full_id in self._apps:
                    raise AppManagerError(
                        f"App '{full_id}' already installed", 409
                    )

                # 5. 检查是否与内置任务冲突（保护内置任务）
                if full_id in builtin_definition_ids:
                    raise AppManagerError(
                        f"App ID '{full_id}' conflicts with built-in task", 409
                    )

                # 6. 移动到目标目录
                target_dir = self._apps_dir / full_id
                if target_dir.exists():
                    shutil.rmtree(target_dir)
                shutil.move(str(tmp_dir), str(target_dir))

                # 7. 设置可执行权限
                bin_dir = target_dir / "bin"
                if bin_dir.exists():
                    for f in bin_dir.iterdir():
                        if f.is_file():
                            os.chmod(f, os.stat(f).st_mode | 0o111)

                # 8. 构造 AppPackage 和 TaskDefinition
                app_package = self._build_app_package(manifest, str(target_dir))

                # 9. 注册
                self._apps[full_id] = app_package
                self._persist()

                return app_package

            except AppManagerError:
                raise
            except Exception as e:
                raise AppManagerError(f"Installation failed: {e}", 500)
            finally:
                # 清理临时目录
                if tmp_dir.exists():
                    shutil.rmtree(tmp_dir, ignore_errors=True)

    async def uninstall(self, app_id: str, app_version: str) -> None:
        """卸载指定版本的 app。"""
        full_id = f"{app_id}@{app_version}"

        async with self._lock:
            app = self._apps.get(full_id)
            if not app:
                raise AppManagerError(f"App '{full_id}' not found", 404)

            target_dir = Path(app.app_dir)
            if target_dir.exists():
                shutil.rmtree(target_dir, ignore_errors=True)

            self._apps.pop(full_id, None)
            self._persist()

    def load_installed(self, builtin_definition_ids: Optional[set] = None) -> Dict[str, AppPackage]:
        """启动时从 apps_dir 加载所有已安装的 app。

        遍历 apps_dir/*@*/manifest.yaml，重建 AppPackage。
        不依赖 apps.json（apps.json 仅作辅助记录），以磁盘实际内容为准。
        """
        builtin_definition_ids = builtin_definition_ids or set()
        loaded: Dict[str, AppPackage] = {}

        if not self._apps_dir.exists():
            return loaded

        for entry in self._apps_dir.iterdir():
            if not entry.is_dir():
                continue
            if "@" not in entry.name:
                continue

            manifest_path = entry / "manifest.yaml"
            if not manifest_path.exists():
                continue

            try:
                raw = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
                if not raw:
                    continue
                raw = _expand_env_vars(raw)
                manifest = self._validate_manifest(raw)

                full_id = f"{manifest['app_id']}@{manifest['app_version']}"
                # 跳过与内置冲突的（异常情况，如内置任务后添加）
                if full_id in builtin_definition_ids:
                    print(f"[WARN] App '{full_id}' conflicts with built-in task, skipping")
                    continue

                app_package = self._build_app_package(manifest, str(entry))
                loaded[full_id] = app_package
            except Exception as e:
                print(f"[WARN] Failed to load app from {entry}: {e}")
                continue

        self._apps = loaded
        self._persist()
        return loaded

    # ---------- 内部方法 ----------

    @staticmethod
    def _extract_lldtp(lldtp_path: Path, dest_dir: Path) -> None:
        """解压 .lldtp（tar.gz）到目标目录，带 path traversal 防护。"""
        dest_dir.mkdir(parents=True, exist_ok=True)

        with tarfile.open(lldtp_path, "r:gz") as tar:
            for member in tar.getmembers():
                # Path traversal 防护
                member_path = member.name
                if ".." in member_path or member_path.startswith("/"):
                    raise AppManagerError(
                        f"Illegal path in archive: {member_path}", 400
                    )

                # 解压到 dest_dir / member_path
                member_path_obj = dest_dir / member_path
                try:
                    member_path_obj.resolve().relative_to(dest_dir.resolve())
                except ValueError:
                    raise AppManagerError(
                        f"Path traversal detected: {member_path}", 400
                    )

            tar.extractall(path=str(dest_dir))

    @staticmethod
    def _validate_manifest(raw: Dict[str, Any]) -> Dict[str, Any]:
        """验证 manifest 内容，返回标准化后的字典。"""
        required = ["app_id", "app_version", "name", "command_template"]
        for key in required:
            if key not in raw or raw[key] is None:
                raise AppManagerError(
                    f"Missing required field in manifest: {key}", 400
                )

        app_id = str(raw["app_id"]).strip()
        app_version = str(raw["app_version"]).strip()
        name = str(raw["name"]).strip()

        if not app_id:
            raise AppManagerError("app_id cannot be empty", 400)
        if not app_version:
            raise AppManagerError("app_version cannot be empty", 400)
        if not name:
            raise AppManagerError("name cannot be empty", 400)

        # 检查非法字符
        for ch in ["/", "\\", "@", " "]:
            if ch in app_id:
                raise AppManagerError(
                    f"app_id contains illegal character: '{ch}'", 400
                )

        manifest = dict(raw)
        manifest["app_id"] = app_id
        manifest["app_version"] = app_version
        manifest["name"] = name
        return manifest

    @staticmethod
    def _build_app_package(manifest: Dict[str, Any], app_dir: str) -> AppPackage:
        """根据 manifest 构造 AppPackage 和 TaskDefinition。"""
        app_id = manifest["app_id"]
        app_version = manifest["app_version"]
        full_id = f"{app_id}@{app_version}"

        # 构造 TaskDefinition
        params_raw = manifest.get("parameters", [])
        parameters = []
        for p in params_raw:
            parameters.append(ParameterDef.model_validate(p))

        # 收集多语言字段
        name_i18n = {}
        description_i18n = {}
        for lang in ["en", "ru"]:
            if f"name_{lang}" in manifest:
                name_i18n[lang] = manifest[f"name_{lang}"]
            if f"description_{lang}" in manifest:
                description_i18n[lang] = manifest[f"description_{lang}"]

        definition = TaskDefinition(
            version=str(manifest.get("version", "1")),
            id=full_id,
            name=manifest["name"],
            description=manifest.get("description", ""),
            command_template=manifest["command_template"],
            parameters=parameters,
            env=manifest.get("env", {}),
            working_dir=manifest.get("working_dir"),
            auto_restart=manifest.get("auto_restart", False),
            progress_protocol=manifest.get("progress_protocol", "stdout"),
            setup_script=manifest.get("setup_script"),
            name_i18n=name_i18n,
            description_i18n=description_i18n,
        )

        return AppPackage(
            app_id=app_id,
            app_version=app_version,
            name=manifest["name"],
            description=manifest.get("description", ""),
            app_dir=app_dir,
            installed_at=datetime.utcnow(),
            definition=definition,
            hidden=manifest.get("hidden", False),
        )

    def _persist(self) -> None:
        """原子写入 apps.json"""
        data: Dict[str, Any] = {}
        for full_id, app in self._apps.items():
            data[full_id] = {
                "app_id": app.app_id,
                "app_version": app.app_version,
                "name": app.name,
                "app_dir": app.app_dir,
                "installed_at": app.installed_at.isoformat(),
                "hidden": app.hidden,
            }

        tmp = self._json_path.with_suffix(".tmp")
        with self._file_lock:
            with open(tmp, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=2, default=str)
            tmp.replace(self._json_path)
            os.sync()
