"""CLI entry point for the MLD SDK.

Provides the ``mld build`` command to package plugins into ``.mld`` bundles.
"""

import argparse
import json
import shutil
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path

try:
    import tomllib
except ModuleNotFoundError:  # Python < 3.11
    import tomli as tomllib  # type: ignore[no-redef]


def _read_pyproject(project_dir: Path) -> dict:
    """Read and return the [project] table from pyproject.toml."""
    path = project_dir / "pyproject.toml"
    if not path.exists():
        print(f"Error: {path} not found.", file=sys.stderr)
        sys.exit(1)
    with open(path, "rb") as f:
        data = tomllib.load(f)
    if "project" not in data:
        print("Error: pyproject.toml has no [project] table.", file=sys.stderr)
        sys.exit(1)
    return data["project"]


def _detect_package_manager(frontend_dir: Path) -> tuple[str, str]:
    """Detect the JavaScript package manager for a frontend directory.

    Returns a (name, command) tuple, e.g. ("bun", "bun") or ("npm", "npm").
    Exits if the required tool is not on PATH.
    """
    has_bun_lockfile = any(
        (frontend_dir / f).exists() for f in ("bun.lockb", "bun.lock")
    )
    if has_bun_lockfile:
        if not shutil.which("bun"):
            print("Error: bun lockfile found but 'bun' is not on PATH.", file=sys.stderr)
            sys.exit(1)
        return ("bun", "bun")

    if (frontend_dir / "package-lock.json").exists():
        if not shutil.which("npm"):
            print(
                "Error: package-lock.json found but 'npm' is not on PATH.",
                file=sys.stderr,
            )
            sys.exit(1)
        return ("npm", "npm")

    # No lockfile — prefer bun, fallback to npm
    if shutil.which("bun"):
        return ("bun", "bun")
    if shutil.which("npm"):
        return ("npm", "npm")

    print(
        "Error: No JavaScript package manager found. Install bun or npm.",
        file=sys.stderr,
    )
    sys.exit(1)


def _build_frontend(project_dir: Path) -> bool:
    """Build the plugin frontend if present. Returns True if a frontend was built."""
    frontend_dir = project_dir / "frontend"
    if not (frontend_dir / "package.json").exists():
        return False

    pm_name, pm_cmd = _detect_package_manager(frontend_dir)
    print(f"Building frontend with {pm_name}...")

    for step, cmd in [("install", [pm_cmd, "install"]), ("run build", [pm_cmd, "run", "build"])]:
        result = subprocess.run(cmd, cwd=frontend_dir, capture_output=True, text=True)
        if result.returncode != 0:
            print(f"Error: {pm_name} {step} failed:\n{result.stderr}", file=sys.stderr)
            sys.exit(1)

    # Verify dist exists and is non-empty
    dist_dir = frontend_dir / "dist"
    if not dist_dir.exists() or not any(dist_dir.iterdir()):
        print(
            "Error: frontend build produced no output in frontend/dist/.",
            file=sys.stderr,
        )
        sys.exit(1)

    print("Frontend build complete.")
    return True


def _check_force_include(project_dir: Path) -> None:
    """Warn if pyproject.toml lacks a force-include entry for frontend/dist."""
    path = project_dir / "pyproject.toml"
    if not path.exists():
        return
    with open(path, "rb") as f:
        data = tomllib.load(f)
    wheel_config = data.get("tool", {}).get("hatch", {}).get("build", {}).get("targets", {}).get("wheel", {})
    force_include = wheel_config.get("force-include", {})
    has_frontend_entry = any("frontend/dist" in k for k in force_include)
    if not has_frontend_entry:
        print(
            "Warning: pyproject.toml has no [tool.hatch.build.targets.wheel.force-include] "
            "entry for frontend/dist. The frontend build output may not be included in the wheel.",
            file=sys.stderr,
        )


def _build_wheel(project_dir: Path, output_dir: Path) -> Path:
    """Build a wheel using ``uv build`` and return its path.

    If a wheel already exists in the output directory, it is reused.
    """
    result = subprocess.run(
        ["uv", "build", "--wheel", "--out-dir", str(output_dir)],
        cwd=project_dir,
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        print(f"Error: uv build failed:\n{result.stderr}", file=sys.stderr)
        sys.exit(1)

    whls = list(output_dir.glob("*.whl"))
    if not whls:
        print("Error: uv build produced no .whl file.", file=sys.stderr)
        sys.exit(1)
    return whls[0]


def _download_deps(project_dir: Path, dest: Path) -> list[Path]:
    """Download binary dependency wheels into *dest*."""
    result = subprocess.run(
        [
            "uv",
            "pip",
            "download",
            "--only-binary",
            ":all:",
            "--dest",
            str(dest),
            "--requirement",
            str(project_dir / "pyproject.toml"),
        ],
        cwd=project_dir,
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        print(
            f"Warning: dependency download failed (non-fatal):\n{result.stderr}",
            file=sys.stderr,
        )
        return []
    return list(dest.glob("*.whl"))


def _build_manifest(
    project: dict,
    main_whl_name: str,
    dep_whl_names: list[str],
    *,
    has_frontend: bool = False,
) -> dict:
    """Build the manifest.json content."""
    return {
        "format_version": 1,
        "plugin": {
            "name": project["name"],
            "version": project["version"],
            "description": project.get("description", ""),
            "requires_mld": project.get("requires_mld", ">=0.1.0"),
            "has_frontend": has_frontend,
        },
        "wheels": {
            "main": main_whl_name,
            "dependencies": dep_whl_names,
        },
    }


def _version_from_wheel(whl_path: Path, name: str) -> str:
    """Extract version from a wheel filename (name-version-...)."""
    # Wheel filenames use underscores: my_pkg-1.2.3-py3-none-any.whl
    normalized = name.replace("-", "_")
    stem = whl_path.stem  # strip .whl
    prefix = f"{normalized}-"
    if stem.startswith(prefix):
        # Everything between name- and the next - after version
        rest = stem[len(prefix):]
        # Version ends at the first segment that looks like a tag (py3, cp312, etc.)
        parts = rest.split("-")
        return parts[0]
    # Fallback: split on second hyphen
    parts = stem.split("-")
    return parts[1] if len(parts) >= 2 else "0.0.0"


def cmd_build(args: argparse.Namespace) -> None:
    """Execute the ``mld build`` command."""
    project_dir = Path(args.path).resolve()
    output_dir = Path(args.output_dir).resolve()
    output_dir.mkdir(parents=True, exist_ok=True)

    project = _read_pyproject(project_dir)
    name = project["name"]

    # Build frontend (before wheel so Hatch force-include picks up dist/)
    has_frontend = not args.no_frontend and _build_frontend(project_dir)
    if has_frontend:
        _check_force_include(project_dir)

    # Build the wheel
    with tempfile.TemporaryDirectory() as build_tmp:
        build_path = Path(build_tmp)
        main_whl = _build_wheel(project_dir, build_path)

        # Resolve version: static from pyproject.toml or dynamic from wheel
        version = project.get("version") or _version_from_wheel(main_whl, name)
        project["version"] = version

        # Optionally vendor dependencies
        dep_whls: list[Path] = []
        if args.vendor_deps:
            dep_dir = build_path / "deps"
            dep_dir.mkdir()
            dep_whls = _download_deps(project_dir, dep_dir)
            # Exclude the main wheel if it ended up in deps
            dep_whls = [w for w in dep_whls if w.name != main_whl.name]

        # Build manifest
        manifest = _build_manifest(
            project,
            main_whl.name,
            [w.name for w in dep_whls],
            has_frontend=has_frontend,
        )

        # Create .mld archive
        mld_filename = f"{name}-{version}.mld"
        mld_path = output_dir / mld_filename

        with zipfile.ZipFile(mld_path, "w", zipfile.ZIP_DEFLATED) as zf:
            zf.writestr("manifest.json", json.dumps(manifest, indent=2))
            zf.write(main_whl, main_whl.name)
            for dep in dep_whls:
                zf.write(dep, dep.name)

    print(f"Built {mld_path}")


def main(argv: list[str] | None = None) -> None:
    """Main CLI entry point."""
    parser = argparse.ArgumentParser(
        prog="mld",
        description="MLD SDK command-line tools",
    )
    subparsers = parser.add_subparsers(dest="command")

    build_parser = subparsers.add_parser(
        "build",
        help="Package a plugin into a .mld bundle",
    )
    build_parser.add_argument(
        "path",
        nargs="?",
        default=".",
        help="Plugin project directory (default: current dir)",
    )
    build_parser.add_argument(
        "--vendor-deps",
        action="store_true",
        help="Include dependency wheels in the .mld bundle",
    )
    build_parser.add_argument(
        "--no-frontend",
        action="store_true",
        help="Skip frontend build even if frontend/package.json exists",
    )
    build_parser.add_argument(
        "--output-dir",
        default="dist",
        help="Output directory (default: dist/)",
    )

    args = parser.parse_args(argv)

    if args.command == "build":
        cmd_build(args)
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()
