"""
Module lifecycle commands (init, validate) and internal build helper.

The ``build`` Click command is NOT exposed as a top-level CLI entry point
but is used internally by ``dev``, ``serve``, and ``test`` for local
containerised runs.
"""

from __future__ import annotations

import base64
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, Optional

import boto3
import click
from botocore.exceptions import BotoCoreError, ClientError, NoCredentialsError, ProfileNotFound
from rich.prompt import Confirm

from ..config import Config
from ..validation import ModuleValidator
from .utils import _ensure_verbose, console, ensure_docker_available, verbose_option

DEFAULT_MODULE_RUNTIME_IMAGE = "python:3.11-slim-bookworm"
MODULE_RUNTIME_IMAGE_ENVVAR = "HLA_COMPASS_MODULE_RUNTIME_IMAGE"
PRIVATE_ECR_HOST_RE = re.compile(r"^(?P<account>\d{12})\.dkr\.ecr\.(?P<region>[a-z0-9-]+)\.amazonaws\.com$")


def load_sdk_config():
    try:
        config_path = Config.get_config_path()
        if config_path.exists():
            with open(config_path) as f:
                return json.load(f)
    except Exception:
        pass
    return None


# ---------------------------------------------------------------------------
# Build helpers (used by ``build`` and ``dev``/``serve``/``test``)
# ---------------------------------------------------------------------------

SUPPORTED_COMPUTE_TYPES = {"docker", "fargate", "batch", "lambda", "sagemaker"}


def _require_supported_compute(manifest: Dict[str, Any]) -> None:
    ctype = manifest.get("computeType") or "docker"
    if ctype not in SUPPORTED_COMPUTE_TYPES:
        console.print(f"[red]Compute type '{ctype}' is not supported by the CLI yet.[/red]")
        console.print(f"Supported: {', '.join(sorted(SUPPORTED_COMPUTE_TYPES))}")
        sys.exit(1)


def _extract_registry_host(image_ref: str) -> str:
    return str(image_ref or "").split("/", 1)[0].strip()


def _parse_private_ecr_host(host: str) -> tuple[str, str] | None:
    match = PRIVATE_ECR_HOST_RE.fullmatch(host)
    if not match:
        return None
    return match.group("account"), match.group("region")


def _ensure_registry_push_auth(image_ref: str) -> None:
    host = _extract_registry_host(image_ref)
    parsed = _parse_private_ecr_host(host)
    if parsed is None:
        return

    account_id, region = parsed
    console.print(f"[dim]Authenticating docker to ECR registry: {host}[/dim]")

    try:
        session = boto3.Session()
        client = session.client("ecr", region_name=region)
        response = client.get_authorization_token(registryIds=[account_id])
        auth_data = (response or {}).get("authorizationData") or []
        if not auth_data:
            raise click.ClickException(f"ECR did not return an authorization token for registry {host}")
        token = auth_data[0].get("authorizationToken")
        if not token:
            raise click.ClickException(f"ECR authorization token missing for registry {host}")
        username, password = base64.b64decode(token).decode("utf-8").split(":", 1)
    except (ProfileNotFound, NoCredentialsError) as exc:
        raise click.ClickException(
            f"Unable to authenticate Docker to ECR registry {host}: {exc}. Configure AWS credentials and retry."
        ) from exc
    except (BotoCoreError, ClientError, ValueError, UnicodeDecodeError) as exc:
        raise click.ClickException(f"Unable to authenticate Docker to ECR registry {host}: {exc}") from exc

    try:
        subprocess.run(
            ["docker", "login", "--username", username, "--password-stdin", host],
            input=password,
            text=True,
            capture_output=True,
            check=True,
        )
    except subprocess.CalledProcessError as exc:
        details = (exc.stderr or exc.stdout or str(exc)).strip()
        raise click.ClickException(f"Docker login to ECR registry {host} failed: {details}") from exc


def _resolve_module_runtime_image(explicit_image: Optional[str] = None) -> str:
    candidate = explicit_image or os.getenv(MODULE_RUNTIME_IMAGE_ENVVAR) or DEFAULT_MODULE_RUNTIME_IMAGE
    return str(candidate).strip()


def _requirements_file_has_hashes(path: Path) -> bool:
    try:
        return any("--hash=" in line for line in path.read_text(encoding="utf-8").splitlines())
    except FileNotFoundError:
        return False


def _build_oci_labels(manifest: Dict[str, Any]) -> Dict[str, str]:
    labels = {
        "org.opencontainers.image.title": str(manifest.get("name") or "hla-compass-module"),
        "org.opencontainers.image.version": str(manifest.get("version") or "0.0.0"),
        "org.opencontainers.image.description": str(manifest.get("description") or "HLA-Compass module image"),
    }
    repository = str(os.getenv("GITHUB_REPOSITORY") or "").strip()
    if repository:
        labels["org.opencontainers.image.source"] = f"https://github.com/{repository}"
    revision = str(os.getenv("GITHUB_SHA") or "").strip()
    if revision:
        labels["org.opencontainers.image.revision"] = revision
    return labels


def _write_container_serve_script(dist_dir: Path):
    script = r'''#!/usr/bin/env python3
import json
import importlib
import logging
import mimetypes
import os
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse

MODULE_ENTRY = os.getenv("HLA_COMPASS_MODULE", None)
MANIFEST_PATH = Path("/app/manifest.json")
INDEX_HTML = """<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>HLA-Compass Module - Local Development</title>
</head>
<body>
  <div id='root'></div>
  <!-- Standalone bundle includes React, ReactDOM, and antd -->
  <script src='/bundle.standalone.js'></script>
</body>
</html>"""

def _configure_logging():
    root_logger = logging.getLogger()
    if root_logger.handlers:
        return
    level_name = os.getenv("LOG_LEVEL") or os.getenv("HLA_COMPASS_LOG_LEVEL") or "INFO"
    level = getattr(logging, level_name.upper(), logging.INFO)
    logging.basicConfig(
        level=level,
        format="%(asctime)s %(levelname)s %(name)s %(message)s",
        stream=sys.stdout,
    )

def _resolve_module(entry: str):
    if ":" not in entry: return None
    mod, cls = entry.split(":", 1)
    m = importlib.import_module(mod)
    return getattr(m, cls)()

def _load_manifest():
    try: return json.loads(MANIFEST_PATH.read_text())
    except: return {}

def _locate_ui_dist():
    candidates = [Path("/app/ui/dist"), Path("/app/frontend/dist")]
    for p in candidates:
        if p.exists(): return p
    return None

class _Handler(BaseHTTPRequestHandler):
    server_version = "hla-compass-serve/1.0"

    def log_message(self, fmt, *args):
        logging.getLogger("container-serve").info("%s - %s", self.address_string(), fmt % args)

    def _send(self, body: bytes, status: int, content_type: str):
        self.send_response(status)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        if body:
            self.wfile.write(body)

    def _send_json(self, payload: dict, status: int = 200):
        body = json.dumps(payload).encode("utf-8")
        self._send(body, status, "application/json")

    def _send_text(self, text: str, status: int = 200, content_type: str = "text/html"):
        self._send(text.encode("utf-8"), status, content_type)

    def _serve_index(self, root: Path | None):
        if root:
            index_path = root / "index.html"
            if index_path.is_file():
                return self._serve_file(index_path)
        return self._send_text(INDEX_HTML)

    def _serve_file(self, path: Path):
        content_type, _ = mimetypes.guess_type(str(path))
        content_type = content_type or "application/octet-stream"
        self._send(path.read_bytes(), 200, content_type)

    def do_POST(self):
        if self.path != "/api/execute":
            return self._send_text("Not found", 404, "text/plain")
        length = int(self.headers.get("Content-Length", "0"))
        body = self.rfile.read(length) if length else b""
        try:
            payload = json.loads(body.decode("utf-8")) if body else {}
        except Exception:
            payload = {}
        input_data = payload.get("input", {})
        offline_env = str(os.getenv("HLA_COMPASS_OFFLINE", "")).lower() in {"1", "true", "yes"}
        has_credentials = bool(os.getenv("HLA_API_KEY") or os.getenv("HLA_ACCESS_TOKEN"))
        context = {
            "mode": "interactive",
            "run_id": "serve-dev",
            "offline": offline_env or not has_credentials,
        }
        try:
            module = self.server.module
            if module is None:
                return self._send_json({"status": "error", "message": "Module entrypoint not found"}, 500)
            result = module.run(input_data, context)
            return self._send_json(result)
        except Exception as exc:
            return self._send_json({"status": "error", "message": str(exc)}, 500)

    def do_GET(self):
        path = urlparse(self.path).path or "/"
        if path.startswith("/api/"):
            return self._send_text("Not found", 404, "text/plain")
        root = self.server.ui_root
        if path == "/":
            return self._serve_index(root)
        if not root:
            return self._send_text("No UI")
        file_path = (root / path.lstrip("/")).resolve()
        try:
            file_path.relative_to(root)
        except Exception:
            return self._send_text("Not found", 404, "text/plain")
        if file_path.is_file():
            return self._serve_file(file_path)
        return self._serve_index(root)

class _Server(ThreadingHTTPServer):
    allow_reuse_address = True

def main():
    _configure_logging()
    manifest = _load_manifest()
    entry = MODULE_ENTRY or manifest.get("execution", {}).get("entrypoint") or "backend.main:Module"
    module = _resolve_module(entry)
    ui_root = _locate_ui_dist()
    port = int(os.getenv("PORT", 8080))
    server = _Server(("0.0.0.0", port), _Handler)
    server.module = module
    server.ui_root = ui_root
    server.serve_forever()

if __name__ == "__main__":
    main()
'''
    (dist_dir / "container-serve.py").write_text(script, encoding="utf-8")


def _write_container_runner_script(dist_dir: Path):
    script = r"""#!/usr/bin/env python3
import logging
import os
import sys

def _configure_logging():
    root_logger = logging.getLogger()
    if root_logger.handlers:
        return
    level_name = os.getenv("LOG_LEVEL") or os.getenv("HLA_COMPASS_LOG_LEVEL") or "INFO"
    level = getattr(logging, level_name.upper(), logging.INFO)
    logging.basicConfig(
        level=level,
        format="%(asctime)s %(levelname)s %(name)s %(message)s",
        stream=sys.stdout,
    )

def main():
    _configure_logging()
    from hla_compass.runtime.runner import main as runner_main
    runner_main()

if __name__ == "__main__":
    main()
"""
    (dist_dir / "container-runner.py").write_text(script, encoding="utf-8")


def _prepare_sdk_source(sdk_path: Optional[Path], dist_dir: Path) -> Optional[str]:
    if not sdk_path:
        existing = dist_dir / "sdk-src"
        if existing.exists():
            shutil.rmtree(existing)
        return None

    resolved = sdk_path.expanduser().resolve()
    if not resolved.exists():
        console.print(f"[red]SDK path not found: {resolved}[/red]")
        sys.exit(1)
    if not resolved.is_dir():
        console.print(f"[red]SDK path must be a directory: {resolved}[/red]")
        sys.exit(1)

    target = dist_dir / "sdk-src"
    if target.exists():
        shutil.rmtree(target)

    def _ignore_sdk(_path: str, names: list[str]) -> set[str]:
        ignore = {
            ".git",
            ".venv",
            "venv",
            "__pycache__",
            ".pytest_cache",
            ".mypy_cache",
            ".ruff_cache",
            ".tox",
            "dist",
            "build",
            ".idea",
            ".vscode",
        }
        return {name for name in names if name in ignore}

    shutil.copytree(resolved, target, symlinks=True, ignore=_ignore_sdk)
    return target.as_posix()


def _is_hla_compass_requirement(line: str) -> bool:
    stripped = line.strip()
    if not stripped or stripped.startswith("#"):
        return False
    lowered = stripped.lower()
    if lowered.startswith(("hla-compass", "hla_compass")):
        return True
    if "hla-compass" in lowered or "hla_compass" in lowered:
        if lowered.startswith(("-e", "--editable")):
            return True
    return False


def _prepare_backend_requirements(dist_dir: Path, sdk_source: Optional[str]) -> None:
    override_lock = dist_dir / "requirements.sdk.lock.txt"
    override_plain = dist_dir / "requirements.sdk.txt"
    for candidate in (override_lock, override_plain):
        if candidate.exists():
            candidate.unlink()

    if not sdk_source:
        return

    lock_path = Path("backend/requirements.lock.txt")
    req_path = Path("backend/requirements.txt")
    source_path = lock_path if lock_path.exists() else req_path
    if not source_path.exists():
        return

    lines = source_path.read_text(encoding="utf-8").splitlines()
    filtered = [line for line in lines if not _is_hla_compass_requirement(line)]
    target = override_lock if lock_path.exists() else override_plain
    target.write_text("\n".join(filtered) + "\n", encoding="utf-8")


def _generate_dockerfile_content(
    manifest,
    sdk_source: Optional[str] = None,
    *,
    base_image: Optional[str] = None,
):
    entry = manifest.get("execution", {}).get("entrypoint") or "backend.main:Module"
    resolved_base_image = _resolve_module_runtime_image(base_image)

    lines = [
        "# syntax=docker/dockerfile:1",
        f"ARG HLA_COMPASS_BASE_IMAGE={resolved_base_image}",
    ]

    has_frontend = Path("frontend/package.json").exists()
    if has_frontend:
        lines.extend(
            [
                "FROM node:20.19.6-alpine AS ui",
                "WORKDIR /ui",
                "COPY frontend/package*.json ./",
                "RUN npm install --legacy-peer-deps",
                "COPY frontend/ ./",
                "# Build both platform bundle (with externals) and standalone bundle (for local serve)",
                "RUN npm run build:all",
            ]
        )

    lines.extend(
        [
            "FROM ${HLA_COMPASS_BASE_IMAGE}",
            "RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*",
            "RUN pip install --no-cache-dir --upgrade pip setuptools wheel",
            "WORKDIR /app",
        ]
    )

    if sdk_source:
        lines.append(f"COPY {sdk_source} /tmp/hla-compass-sdk")
        lines.append("RUN pip install --no-cache-dir /tmp/hla-compass-sdk")
    else:
        from .._version import __version__ as sdk_version

        lines.append(f"RUN pip install --no-cache-dir hla-compass=={sdk_version}")

    lines.append("COPY manifest.json /app/manifest.json")

    override_lock = Path("dist/requirements.sdk.lock.txt")
    override_plain = Path("dist/requirements.sdk.txt")
    if override_lock.exists():
        lines.append("COPY dist/requirements.sdk.lock.txt /tmp/reqs.lock.txt")
        if _requirements_file_has_hashes(override_lock):
            lines.append("RUN pip install --require-hashes -r /tmp/reqs.lock.txt")
        else:
            lines.append("RUN pip install -r /tmp/reqs.lock.txt")
    elif override_plain.exists():
        lines.append("COPY dist/requirements.sdk.txt /tmp/reqs.txt")
        lines.append("RUN pip install -r /tmp/reqs.txt")
    elif Path("backend/requirements.txt").exists():
        lock_path = Path("backend/requirements.lock.txt")
        if lock_path.exists():
            lines.append("COPY backend/requirements.lock.txt /tmp/reqs.lock.txt")
            if _requirements_file_has_hashes(lock_path):
                lines.append("RUN pip install --require-hashes -r /tmp/reqs.lock.txt")
            else:
                lines.append("RUN pip install -r /tmp/reqs.lock.txt")
        else:
            lines.append("COPY backend/requirements.txt /tmp/reqs.txt")
            lines.append("RUN pip install -r /tmp/reqs.txt")

    lines.append("COPY backend/ /app/backend/")

    if has_frontend:
        lines.extend(["RUN mkdir -p /app/ui", "COPY --from=ui /ui/dist /app/ui/dist"])

    lines.extend(
        [
            "ENV PYTHONPATH=/app",
            f"ENV HLA_COMPASS_MODULE={entry}",
            "COPY dist/container-serve.py /app/container-serve.py",
            "COPY dist/container-runner.py /app/container-runner.py",
            "EXPOSE 8080",
            'ENTRYPOINT ["python", "/app/container-runner.py"]',
        ]
    )

    return "\n".join(lines)


# ---------------------------------------------------------------------------
# build command — internal only, used by dev/serve/test via ctx.invoke()
# NOT registered as a top-level CLI entry point.
# ---------------------------------------------------------------------------


@click.command()
@verbose_option
@click.option("--tag", help="Docker image tag")
@click.option("--registry", help="Registry prefix")
@click.option(
    "--platform",
    default="linux/amd64",
    show_default=True,
    help="Docker build platform(s), e.g. linux/amd64 or linux/amd64,linux/arm64",
)
@click.option(
    "--sdk-path",
    type=click.Path(path_type=Path),
    envvar="HLA_COMPASS_SDK_PATH",
    help="Install the SDK from a local path inside the image (for development)",
)
@click.option(
    "--base-image",
    envvar=MODULE_RUNTIME_IMAGE_ENVVAR,
    help="Platform runtime base image (defaults to the managed module runtime image)",
)
@click.option("--push", is_flag=True, help="Push image")
@click.pass_context
def build(ctx, tag, registry, platform, sdk_path, base_image, push):
    """Build module container (internal — used by dev/serve/test)."""
    _ensure_verbose(ctx)
    ensure_docker_available()

    manifest_path = Path("manifest.json")
    if not manifest_path.exists():
        console.print("[red]manifest.json not found[/red]")
        sys.exit(1)

    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    _require_supported_compute(manifest)
    module_name = manifest.get("name", "module")
    version = manifest.get("version", "0.0.0")

    # Default tag logic
    def _sanitize(v):
        return "".join(c if c.isalnum() or c in "-_." else "-" for c in v.lower()).strip("-.")

    default_tag = f"{_sanitize(module_name)}:{_sanitize(version)}"
    image_tag = tag or default_tag

    local_tag = image_tag
    registry_tag = None
    if registry:
        registry = registry.rstrip("/")
        if "/" not in image_tag.split(":")[0]:
            safe_tag = image_tag.replace("/", "-")
            tag_suffix = safe_tag.replace(":", "-")
            registry_tag = f"{registry}:{tag_suffix}"
        else:
            registry_tag = image_tag
            local_tag = image_tag

    dist_dir = Path("dist")
    dist_dir.mkdir(parents=True, exist_ok=True)

    sdk_source = _prepare_sdk_source(sdk_path, dist_dir)
    _prepare_backend_requirements(dist_dir, sdk_source)

    # Helper to write container scripts
    _write_container_serve_script(dist_dir)
    _write_container_runner_script(dist_dir)

    resolved_base_image = _resolve_module_runtime_image(base_image)
    dockerfile_path = dist_dir / "Dockerfile.hla"
    dockerfile_path.write_text(
        _generate_dockerfile_content(manifest, sdk_source, base_image=resolved_base_image),
        encoding="utf-8",
    )

    published_tag = registry_tag or local_tag
    oci_labels = _build_oci_labels(manifest)

    multi_platform = bool(platform and "," in platform)
    if multi_platform and not push:
        console.print("[red]Multi-arch builds require --push (or specify a single --platform).[/red]")
        sys.exit(1)

    if push and multi_platform:
        _ensure_registry_push_auth(published_tag)

    console.print(f"[cyan]Building {local_tag}...[/cyan]")
    if multi_platform:
        build_cmd = [
            "docker",
            "buildx",
            "build",
            "--platform",
            platform,
            "-f",
            str(dockerfile_path),
            "-t",
            published_tag,
            ".",
            "--push",
        ]
        for key, value in oci_labels.items():
            build_cmd.extend(["--label", f"{key}={value}"])
        subprocess.run(build_cmd, check=True)
    else:
        build_cmd = ["docker", "build"]
        if platform:
            build_cmd.extend(["--platform", platform])
        for key, value in oci_labels.items():
            build_cmd.extend(["--label", f"{key}={value}"])
        build_cmd.extend(["-f", str(dockerfile_path), "-t", local_tag, "."])
        subprocess.run(build_cmd, check=True)

    if registry_tag and not multi_platform:
        subprocess.run(["docker", "tag", local_tag, registry_tag], check=True)

    if push and not multi_platform:
        _ensure_registry_push_auth(published_tag)
        console.print(f"[cyan]Pushing {published_tag}...[/cyan]")
        try:
            subprocess.run(["docker", "push", published_tag], check=True)
        except subprocess.CalledProcessError as e:
            console.print(f"[red]Failed to push image: {e}[/red]")
            if "denied" in str(e) or "unauthorized" in str(e) or "forbidden" in str(e).lower():
                console.print("[yellow]Tip: Check your permissions. You might need to run:[/yellow]")
                console.print(f"  docker login {published_tag.split('/')[0]}")
            raise e

    # Report
    report = {
        "image_tag": published_tag if multi_platform else local_tag,
        "published_tag": published_tag,
        "pushed": push,
        "platform": platform,
        "base_image": resolved_base_image,
        "oci_labels": oci_labels,
        "sdk_source": sdk_source,
    }
    (dist_dir / "build.json").write_text(json.dumps(report, indent=2), encoding="utf-8")

    return report


# ---------------------------------------------------------------------------
# CLI commands: init, validate
# ---------------------------------------------------------------------------


@click.command()
@verbose_option
@click.argument("name", required=False)
@click.option(
    "--template",
    type=click.Choice(["ui", "no-ui", "mcp"]),
    default="no-ui",
    help="Module template (no-ui, ui, or mcp for AI-consumable modules)",
)
@click.option("--yes", is_flag=True, help="Non-interactive mode")
@click.pass_context
def init(ctx, name, template, yes):
    """Create a new HLA-Compass module"""
    _ensure_verbose(ctx)
    if not name:
        console.print("[red]Module name required[/red]")
        sys.exit(1)

    # Validate name
    name_pattern = r"^[a-z0-9]([a-z0-9-]{1,48}[a-z0-9])?$"
    if not re.match(name_pattern, name):
        console.print(f"[red]Invalid module name '{name}'[/red]")
        console.print(f"Name must match regex: {name_pattern}")
        console.print("(Lowercase alphanumeric, hyphens, 2-50 chars, start/end with alphanumeric)")
        sys.exit(1)

    module_type = "with-ui" if template == "ui" else "no-ui"
    template_dir_name = f"{template}-template" if template != "mcp" else "mcp-template"

    # Locate template relative to hla_compass package (parent of this cli package)
    base_path = Path(__file__).parent.parent
    pkg_templates_dir = base_path / "templates" / template_dir_name

    if not pkg_templates_dir.exists():
        console.print(f"[red]Template not found at {pkg_templates_dir}[/red]")
        sys.exit(1)

    module_dir = Path(name)
    if module_dir.exists() and not yes:
        if not Confirm.ask(f"Directory '{name}' exists. Continue?"):
            return

    shutil.copytree(
        pkg_templates_dir,
        module_dir,
        dirs_exist_ok=True,
        ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store"),
    )

    # Update manifest
    manifest_path = module_dir / "manifest.json"
    with open(manifest_path) as f:
        manifest = json.load(f)

    manifest["name"] = name
    manifest["type"] = module_type
    sdk_config = load_sdk_config()
    author_info = sdk_config.get("author", {}) if sdk_config else {}

    # Try to get author info from git if not in config
    author_name = author_info.get("name")
    author_email = author_info.get("email")

    if not author_name:
        try:
            author_name = subprocess.check_output(["git", "config", "user.name"], text=True).strip()
        except Exception:
            author_name = os.getenv("USER", "Unknown")

    if not author_email:
        try:
            author_email = subprocess.check_output(["git", "config", "user.email"], text=True).strip()
        except Exception:
            author_email = "developer@example.com"

    manifest["author"]["name"] = author_name
    manifest["author"]["email"] = author_email

    with open(manifest_path, "w") as f:
        json.dump(manifest, f, indent=2)

    if module_type == "no-ui":
        shutil.rmtree(module_dir / "frontend", ignore_errors=True)

    # Pin the runtime SDK dependency inside the generated module to match the CLI SDK version.
    # This keeps `hla-compass init` outputs compatible with the CLI used to create them.
    requirements_path = module_dir / "backend" / "requirements.txt"
    requirements_lock_path = module_dir / "backend" / "requirements.lock.txt"
    if requirements_path.exists():
        try:
            from .._version import __version__ as sdk_version

            lines = requirements_path.read_text(encoding="utf-8").splitlines()
            rewritten: list[str] = []
            replaced = False
            for line in lines:
                stripped = line.strip()
                if (
                    stripped in {"hla-compass", "hla_compass"}
                    or stripped.startswith("hla-compass==")
                    or stripped.startswith("hla-compass>=")
                ):
                    rewritten.append(f"hla-compass=={sdk_version}")
                    replaced = True
                else:
                    rewritten.append(line)

            if not replaced:
                rewritten.insert(0, f"hla-compass=={sdk_version}")

            requirements_path.write_text("\n".join(rewritten) + "\n", encoding="utf-8")
            if not requirements_lock_path.exists():
                requirements_lock_path.write_text("\n".join(rewritten) + "\n", encoding="utf-8")
        except Exception as e:
            console.print(f"[yellow]Warning: failed to pin hla-compass dependency in requirements.txt: {e}[/yellow]")

    console.print(f"[green]✓ Module '{name}' created![/green]")


@click.command()
@verbose_option
@click.option("--manifest", default="manifest.json")
@click.option(
    "--format",
    type=click.Choice(["text", "json"]),
    default="text",
    help="Output format",
)
@click.option("--strict", is_flag=True, help="Fail on warnings")
def validate(manifest, format, strict):
    """Validate manifest structure and contents."""
    validator = ModuleValidator(manifest_path=manifest)
    res = validator.run(strict=strict)
    is_valid = res.valid and (not strict or not res.issues)

    if format == "json":
        output = {
            "valid": is_valid,
            "issues": [i.to_dict() for i in res.issues],
        }
        click.echo(json.dumps(output, indent=2))
    else:
        if is_valid:
            console.print("[green]✓ Module is valid![/green]")
        else:
            console.print("[red]Module validation failed[/red]")

        for issue in res.issues:
            color = "red" if issue.severity == "error" else "yellow"
            console.print(f"[{color}]{issue.severity.upper()}: {issue.message}[/{color}]")
            if issue.pointer:
                console.print(f"  Location: {issue.pointer}")
            if issue.suggestion:
                console.print(f"  [bold]Suggestion:[/bold] {issue.suggestion}")
            console.print()

    if not is_valid:
        sys.exit(1)
