#!/usr/bin/env python3
"""Create the source tar.gz consumed by CreateDeployHistoryV2.

This is the standalone subset of coze-coding's devbox package.py. It keeps the
same archive layout and deployment exclusions without importing the devbox
service runtime (FastAPI, S3FS, project managers, and background tasks).
"""

import argparse
import os
import subprocess
import sys
import tarfile
from pathlib import Path

try:
    import tomllib
except ModuleNotFoundError as error:  # pragma: no cover - depends on host Python
    raise RuntimeError("Python 3.11 or newer is required to read .coze") from error


EXCLUDED_PARTS = {
    ".codegraph",
    ".next",
    ".venv",
    "__pycache__",
    "node_modules",
    "site-packages",
}
UPLOAD_SESSION_FILENAME = ".coze-deploy-upload.json"
UPLOAD_SESSION_TEMP_PREFIX = ".coze-deploy-upload-"
HOST_LINK_FILENAME = ".host.json"
PROJECT_METADATA_FILE_MODE = 0o644


def build_command_env():
    env = os.environ.copy()
    cli_bin_dir = Path(__file__).resolve().parent.parent / "node_modules" / ".bin"
    path = env.get("PATH", "")
    env["PATH"] = f"{cli_bin_dir}{os.pathsep}{path}" if path else str(cli_bin_dir)
    return env


def read_coze_config(source_dir: Path):
    coze_path = source_dir / ".coze"
    if not coze_path.is_file():
        return {}
    with coze_path.open("rb") as file:
        return tomllib.load(file)


def read_command(config, section: str, key: str):
    command = (config.get(section) or {}).get(key)
    if command is None:
        return None
    if not isinstance(command, list) or not all(
        isinstance(part, str) and part for part in command
    ):
        raise ValueError(f".coze {section}.{key} must be a string array")
    return command


def read_platform_command(config, section: str, key: str, platform=sys.platform):
    default_command = read_command(config, section, key)
    windows_command = (
        read_command(config, section, f"{key}_win")
        if platform.startswith("win")
        else None
    )
    if windows_command:
        return f"{key}_win", windows_command
    return (key, default_command) if default_command else None


def run_pack_command(source_dir: Path):
    configured_command = read_platform_command(
        read_coze_config(source_dir), "dev", "pack"
    )
    if configured_command:
        key, command = configured_command
        print(f"[package] Running dev.{key}: {' '.join(command)}", file=sys.stderr)
        subprocess.run(command, cwd=source_dir, env=build_command_env(), check=True)


def has_index_html(directory: Path):
    return (directory / "index.html").is_file()


def prepare_pages_output(source_dir: Path):
    if has_index_html(source_dir):
        print(f"[package] Pages static source: {source_dir}", file=sys.stderr)
        return source_dir

    coze_path = source_dir / ".coze"
    if not coze_path.is_file():
        raise ValueError(
            "Pages source contains neither index.html nor a .coze file. "
            "Add index.html or configure a Next.js build in .coze."
        )

    config = read_coze_config(source_dir)
    template = (config.get("project") or {}).get("template")
    if not isinstance(template, str) or template.strip().lower() != "nextjs":
        raise ValueError(
            'Pages source contains no index.html; .coze project.template must be "nextjs"'
        )
    if not read_command(config, "deploy", "build"):
        raise ValueError(
            "Pages source contains no index.html; .coze deploy.build is required"
        )
    print(f"[package] Pages Next.js source: {source_dir}", file=sys.stderr)
    return source_dir


def should_exclude(tar_info: tarfile.TarInfo, exclude_coze_file=False):
    parts = Path(tar_info.name).parts

    # Local deployment metadata is not application source. Including it can change
    # the package digest on the next CLI invocation and make the resumable PreDeploy
    # session look like a different artifact.
    if any(
        part in {UPLOAD_SESSION_FILENAME, HOST_LINK_FILENAME}
        or (part.startswith(UPLOAD_SESSION_TEMP_PREFIX) and part.endswith(".tmp"))
        for part in parts
    ):
        return None
    if ".env" in parts:
        return None
    if exclude_coze_file and ".coze" in parts:
        return None
    if any(part.startswith(".git") for part in parts):
        return None
    if any(part in EXCLUDED_PARTS for part in parts):
        return None
    if parts == (".coze",) and tar_info.isfile():
        tar_info.mode = PROJECT_METADATA_FILE_MODE
    return tar_info


def create_tar_gz_without_root(source_dir: Path, output_path: Path, exclude_coze_file=False):
    if not source_dir.is_dir():
        raise ValueError(f"source directory does not exist: {source_dir}")
    output_path.parent.mkdir(parents=True, exist_ok=True)
    print(f"[package] Creating archive from: {source_dir}", file=sys.stderr)
    with tarfile.open(output_path, "w:gz") as archive:
        for entry in sorted(os.listdir(source_dir)):
            if entry == ".codegraph":
                continue
            if (source_dir / entry).resolve() == output_path.resolve():
                continue
            archive.add(
                source_dir / entry,
                arcname=entry,
                filter=lambda info: should_exclude(info, exclude_coze_file),
            )
    if not output_path.is_file() or output_path.stat().st_size <= 0:
        raise RuntimeError("source archive is empty")
    print(
        f"[package] Archive ready: {output_path.stat().st_size} bytes",
        file=sys.stderr,
    )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("source_dir")
    parser.add_argument("output_path")
    parser.add_argument("--pages", action="store_true")
    args = parser.parse_args()

    source_dir = Path(args.source_dir).resolve()
    output_path = Path(args.output_path).resolve()
    if args.pages:
        package_dir = prepare_pages_output(source_dir)
    else:
        run_pack_command(source_dir)
        package_dir = source_dir
    create_tar_gz_without_root(package_dir, output_path, args.pages)
    print(output_path.stat().st_size)


if __name__ == "__main__":
    main()
