#!/usr/bin/env python3

import argparse
import json
import re
import shutil
import zipfile
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple


def find_project_root(start_dir: Path) -> Path:
    current = start_dir.resolve()
    while True:
        if (current / ".dbx").exists() or (current / ".git").exists():
            return current
        if current.parent == current:
            return start_dir.resolve()
        current = current.parent


def config_path(project_root: Path) -> Path:
    return project_root / ".dbx" / "config.json"


def workspace_dir(project_root: Path) -> Path:
    return project_root / ".dbx" / "workspace"


def manifest_path(project_root: Path) -> Path:
    return project_root / "manifest.yaml"


def manifest_template(app_key: Optional[str]) -> str:
    normalized_app_key = app_key.strip() if app_key and app_key.strip() else "db_app_mock_app"
    app_key_literal = json.dumps(normalized_app_key, ensure_ascii=False)
    return f"""manifest_version: 2
app_key: {app_key_literal}
name: "请填写智能服务名称"

mcp_server:
  # 本地调试默认地址；上传前请替换为公网可访问的 MCP 服务地址。
  end_point: "http://127.0.0.1:8000/mcp"
  description: "请填写 MCP 服务用途"
  mcp_config:
    protocol: Streamable

# 根据 MCP Server tools/list 返回的真实工具配置；未实现工具前保持为空。
tools: {{}}

# 需要返回业务实体或展示卡片时，再按实际数据结构配置。
entities: {{}}

# 需要登录、定位、相机等能力时，再声明相应权限。
permissions: []
"""


def ensure_manifest_template(path: Path, app_key: Optional[str] = None) -> bool:
    path.parent.mkdir(parents=True, exist_ok=True)
    if path.exists() and path.stat().st_size > 0:
        return False
    path.write_text(manifest_template(app_key), encoding="utf-8")
    return True


def skill_dir(project_root: Path) -> Path:
    return project_root / "skill"


def skill_md_path(project_root: Path) -> Path:
    return skill_dir(project_root) / "SKILL.md"


def is_root_layout(project_root: Path) -> bool:
    if not (
        (project_root / "manifest.yaml").is_file()
        or (project_root / "skill" / "SKILL.md").is_file()
    ):
        return False

    data = read_config(config_path(project_root))
    frontend = data.get("frontend")
    if isinstance(frontend, dict) and isinstance(frontend.get("directory"), str):
        directory = frontend["directory"].strip()
        if directory:
            return directory == "."
    return not (project_root / "miniapp").is_dir()


def archive_dir(project_root: Path) -> Path:
    return project_root / ".dbx" / "archive"


def zip_directory(src_dir: Path, dst_zip: Path) -> None:
    if not src_dir.exists() or not src_dir.is_dir():
        raise FileNotFoundError(f"source directory not found: {src_dir}")
    dst_zip.parent.mkdir(parents=True, exist_ok=True)
    if dst_zip.exists():
        dst_zip.unlink()
    dst_resolved = dst_zip.resolve()
    with zipfile.ZipFile(dst_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
        for file_path in sorted(p for p in src_dir.rglob("*") if p.is_file()):
            if file_path.resolve() == dst_resolved:
                continue
            zf.write(file_path, file_path.relative_to(src_dir).as_posix())


def workspace_required_files(project_root: Path) -> Dict[str, Tuple[Path, str]]:
    frontend_dir = frontend_directory_from_config(project_root, read_config(config_path(project_root)))
    app_config = frontend_app_config_path(frontend_dir)
    runtime_config = frontend_runtime_config_path(frontend_dir)
    manifest_label = "manifest.yaml"
    skill_label = "skill/SKILL.md"
    return {
        manifest_label: (manifest_path(project_root), "file"),
        skill_label: (skill_md_path(project_root), "file"),
        "frontend_dir": (frontend_dir, "dir"),
        "frontend_app_config": (app_config, "file"),
        "frontend_runtime_config": (runtime_config, "file"),
    }


def check_workspace_artifacts(project_root: Path) -> Dict[str, Any]:
    files = workspace_required_files(project_root)
    present = {
        name: path.exists() and (path.is_dir() if kind == "dir" else path.is_file())
        for name, (path, kind) in files.items()
    }
    missing = [name for name, exists in present.items() if not exists]
    return {
        "workspace_dir": str(workspace_dir(project_root)),
        "required": {name: str(path) for name, (path, _kind) in files.items()},
        "present": present,
        "missing": missing,
        "ready": not missing,
    }


def read_config(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    with path.open("r", encoding="utf-8") as f:
        data = json.load(f)
    return data if isinstance(data, dict) else {}


def app_key_from_manifest(path: Path) -> Optional[str]:
    if not path.exists() or not path.is_file():
        return None
    match = re.search(r"^\s*app_key\s*:\s*(.*?)\s*(?:#.*)?$", path.read_text(encoding="utf-8"), re.MULTILINE)
    if not match:
        return None
    value = match.group(1).strip()
    if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
        value = value[1:-1]
    return value or None


def frontend_directory_from_config(project_root: Path, data: Dict[str, Any]) -> Path:
    if is_root_layout(project_root):
        return project_root
    frontend = data.get("frontend")
    if isinstance(frontend, dict) and isinstance(frontend.get("directory"), str) and frontend["directory"].strip():
        directory = Path(frontend["directory"].strip())
    else:
        directory = Path("doubao-agentic-service")
    if directory.is_absolute():
        return directory
    return project_root / directory


def frontend_app_config_candidates(frontend_dir: Path) -> List[Path]:
    src_dir = frontend_dir / "src"
    return [
        src_dir / "app.config",
        src_dir / "app.config.ts",
        src_dir / "app.config.js",
        src_dir / "app.config.json",
    ]


def frontend_app_config_path(frontend_dir: Path) -> Path:
    candidates = frontend_app_config_candidates(frontend_dir)
    for candidate in candidates:
        if candidate.exists() and candidate.is_file():
            return candidate
    return candidates[0]


def frontend_runtime_config_candidates(frontend_dir: Path) -> List[Path]:
    config_dir = frontend_dir / "src" / "config"
    return [
        config_dir / "runtime.ts",
        config_dir / "runtime.js",
        config_dir / "runtime.json",
    ]


def frontend_runtime_config_path(frontend_dir: Path) -> Path:
    candidates = frontend_runtime_config_candidates(frontend_dir)
    for candidate in candidates:
        if candidate.exists() and candidate.is_file():
            return candidate
    return candidates[0]


def set_app_key_in_manifest(path: Path, app_key: str) -> str:
    if not app_key.strip():
        raise ValueError("AppID must not be empty")
    normalized = app_key.strip()
    if not path.exists():
        ensure_manifest_template(path, normalized)
        return normalized
    if not path.is_file():
        raise ValueError(f"manifest path is not a file: {path}")
    source = path.read_text(encoding="utf-8")
    replacement = f"app_key: {json.dumps(normalized, ensure_ascii=False)}"
    if re.search(r"^\s*app_key\s*:", source, re.MULTILINE):
        updated = re.sub(r"^\s*app_key\s*:.*$", replacement, source, count=1, flags=re.MULTILINE)
    else:
        updated = replacement + "\n" + source
    path.write_text(updated, encoding="utf-8")
    return normalized


def rel(project_root: Path, path: Path) -> str:
    try:
        return path.resolve().relative_to(project_root.resolve()).as_posix()
    except ValueError:
        return str(path.resolve())


def emit(payload: Dict[str, Any], as_json: bool) -> None:
    if as_json:
        print(json.dumps(payload, ensure_ascii=False, indent=2))
    else:
        print(json.dumps(payload, ensure_ascii=False, indent=2))


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Manage local files for Doubao app lifecycle work.")
    parser.add_argument("--project", default=".", help="Project path. Defaults to current directory.")
    parser.add_argument("--json", action="store_true", help="Print structured JSON.")

    subparsers = parser.add_subparsers(dest="command", required=True)
    output_parent = argparse.ArgumentParser(add_help=False)
    output_parent.add_argument("--json", action="store_true", help="Print structured JSON.")

    subparsers.add_parser("config-status", parents=[output_parent])
    app_id_parser = subparsers.add_parser("app-id", parents=[output_parent])
    app_id_parser.add_argument("app_id_command", nargs="?", choices=["get", "set"], default="get")
    app_id_parser.add_argument("--value", help="AppID to write into the resolved Manifest when using `set`.")

    workspace_parser = subparsers.add_parser("workspace")
    workspace_sub = workspace_parser.add_subparsers(dest="workspace_command", required=True)
    workspace_sub.add_parser("path", parents=[output_parent])
    workspace_sub.add_parser("ensure", parents=[output_parent])

    frontend_parser = subparsers.add_parser("frontend")
    frontend_sub = frontend_parser.add_subparsers(dest="frontend_command", required=True)
    frontend_sub.add_parser("path", parents=[output_parent])

    manifest_parser = subparsers.add_parser("manifest")
    manifest_sub = manifest_parser.add_subparsers(dest="manifest_command", required=True)
    manifest_path = manifest_sub.add_parser("path", parents=[output_parent])
    manifest_path.add_argument("--create", action="store_true")

    skill_parser = subparsers.add_parser("skill")
    skill_sub = skill_parser.add_subparsers(dest="skill_command", required=True)
    skill_path = skill_sub.add_parser("path", parents=[output_parent])
    skill_path.add_argument("--create", action="store_true")

    file_parser = subparsers.add_parser("file")
    file_sub = file_parser.add_subparsers(dest="file_command", required=True)
    file_path = file_sub.add_parser("path", parents=[output_parent])
    file_path.add_argument("--name", required=True)
    file_path.add_argument("--create", action="store_true")

    archive_parser = subparsers.add_parser("archive", parents=[output_parent])
    archive_parser.add_argument("--version-tag", required=True)

    artifacts_parser = subparsers.add_parser("artifacts")
    artifacts_sub = artifacts_parser.add_subparsers(dest="artifacts_command", required=True)
    pack_skill = artifacts_sub.add_parser("pack-skill", parents=[output_parent])
    pack_skill.add_argument("--source-dir", required=True)
    artifacts_sub.add_parser("check", parents=[output_parent])
    return parser


def main() -> None:
    args = build_parser().parse_args()
    project_root = find_project_root(Path(args.project))
    cfg_path = config_path(project_root)
    ws_dir = workspace_dir(project_root)

    if args.command == "config-status":
        emit(
            {
                "project_root": str(project_root),
                "config_path": str(cfg_path),
                "config_exists": cfg_path.exists(),
            },
            args.json,
        )
        return

    if args.command == "app-id":
        app_manifest_path = manifest_path(project_root)
        if args.app_id_command == "set":
            if not args.value:
                raise ValueError("app-id set requires --value")
            app_key = set_app_key_in_manifest(app_manifest_path, args.value)
            emit(
                {
                    "project_root": str(project_root),
                    "manifest_path": str(app_manifest_path),
                    "app_id": app_key,
                    "app_selected": True,
                    "updated": True,
                },
                args.json,
            )
            return
        app_key = app_key_from_manifest(app_manifest_path)
        emit(
            {
                "project_root": str(project_root),
                "manifest_path": str(app_manifest_path),
                "app_id": app_key,
                "app_selected": bool(app_key),
            },
            args.json,
        )
        return

    if args.command == "workspace":
        if args.workspace_command == "ensure":
            ws_dir.mkdir(parents=True, exist_ok=True)
        emit(
            {
                "project_root": str(project_root),
                "workspace_dir": str(ws_dir),
                "workspace_rel": rel(project_root, ws_dir),
                "exists": ws_dir.exists(),
            },
            args.json,
        )
        return

    if args.command == "frontend":
        frontend_dir = frontend_directory_from_config(project_root, read_config(cfg_path))
        if args.frontend_command == "path":
            app_config = frontend_app_config_path(frontend_dir)
            candidates = frontend_app_config_candidates(frontend_dir)
            runtime_config = frontend_runtime_config_path(frontend_dir)
            runtime_config_candidates = frontend_runtime_config_candidates(frontend_dir)
            emit(
                {
                    "project_root": str(project_root),
                    "frontend_dir": str(frontend_dir),
                    "frontend_rel": rel(project_root, frontend_dir),
                    "app_config": str(app_config),
                    "app_config_rel": rel(project_root, app_config),
                    "app_config_exists": app_config.exists() and app_config.is_file(),
                    "app_config_candidates": [str(path) for path in candidates],
                    "app_config_candidate_rels": [rel(project_root, path) for path in candidates],
                    "runtime_config": str(runtime_config),
                    "runtime_config_rel": rel(project_root, runtime_config),
                    "runtime_config_exists": runtime_config.exists() and runtime_config.is_file(),
                    "runtime_config_candidates": [str(path) for path in runtime_config_candidates],
                    "runtime_config_candidate_rels": [rel(project_root, path) for path in runtime_config_candidates],
                    "exists": frontend_dir.exists() and frontend_dir.is_dir(),
                },
                args.json,
            )
            return

    if args.command == "manifest":
        target = manifest_path(project_root)
        template_written = False
        if args.create:
            template_written = ensure_manifest_template(target)
        emit(
            {
                "project_root": str(project_root),
                "path": str(target),
                "rel": rel(project_root, target),
                "exists": target.exists(),
                "template_written": template_written,
            },
            args.json,
        )
        return

    if args.command == "skill":
        target = skill_dir(project_root)
        skill_md = skill_md_path(project_root)
        if args.create:
            target.mkdir(parents=True, exist_ok=True)
        emit(
            {
                "project_root": str(project_root),
                "path": str(target),
                "rel": rel(project_root, target),
                "exists": target.exists() and target.is_dir(),
                "skill_md": str(skill_md),
                "skill_md_rel": rel(project_root, skill_md),
                "skill_md_exists": skill_md.exists() and skill_md.is_file(),
            },
            args.json,
        )
        return

    if args.command == "file":
        safe_name = Path(args.name)
        if safe_name.is_absolute() or ".." in safe_name.parts:
            raise ValueError("--name must be a relative path inside .dbx/workspace")
        target = ws_dir / safe_name
        if args.create:
            target.parent.mkdir(parents=True, exist_ok=True)
            target.touch(exist_ok=True)
        emit(
            {
                "project_root": str(project_root),
                "path": str(target),
                "rel": rel(project_root, target),
                "exists": target.exists(),
            },
            args.json,
        )
        return

    if args.command == "archive":
        if not ws_dir.exists():
            emit({"project_root": str(project_root), "archived": [], "workspace_exists": False}, args.json)
            return
        target_dir = archive_dir(project_root) / args.version_tag
        target_dir.mkdir(parents=True, exist_ok=True)
        moved = []
        for item in sorted(ws_dir.iterdir()):
            dest = target_dir / item.name
            if dest.exists():
                raise FileExistsError(f"archive target already exists: {dest}")
            shutil.move(str(item), str(dest))
            moved.append(rel(project_root, dest))
        emit(
            {
                "project_root": str(project_root),
                "archive_dir": str(target_dir),
                "archive_rel": rel(project_root, target_dir),
                "archived": moved,
            },
            args.json,
        )
        return

    if args.command == "artifacts":
        if args.artifacts_command == "pack-skill":
            source_dir = Path(args.source_dir)
            if not source_dir.is_absolute():
                source_dir = (Path.cwd() / source_dir).resolve()
            if not (source_dir / "SKILL.md").exists():
                raise FileNotFoundError(f"SKILL.md not found in skill source directory: {source_dir}")
            target = ws_dir / "skill.zip"
            zip_directory(source_dir, target)
            emit(
                {
                    "project_root": str(project_root),
                    "source_dir": str(source_dir),
                    "path": str(target),
                    "rel": rel(project_root, target),
                    "exists": target.exists(),
                },
                args.json,
            )
            return

        if args.artifacts_command == "check":
            payload = check_workspace_artifacts(project_root)
            payload["project_root"] = str(project_root)
            emit(payload, args.json)
            return


if __name__ == "__main__":
    main()
