"""Build command: generates package specifics from H5P vendor sources.

Provides the ``build`` CLI command that reads vendor ``library.json``
and ``semantics.json``, parses them into IR, and renders Jinja2
templates to produce code, tests, and documentation.
"""

from __future__ import annotations

import argparse
import logging
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Optional

from jinja2 import Environment, PackageLoader

from byteb4rb1e.utils.argparse import CLICommand

from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.index import (
    dir_name_to_import_name,
    dir_name_to_src_subpath,
    repo_name_to_dir_name,
)
from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.semantics import parse
from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.semantics.classify import (
    machine_name_to_directive,
)

from .context import (
    assets_context,
    conftest_context,
    directive_context,
    docs_context,
    params_context,
    test_directive_context,
    test_params_context,
)
from .readers import read_h5p_config, read_library_json, read_semantics_json

logger = logging.getLogger(__name__)

_VENDOR_EXCLUDE = {
    '.git',
    '.github',
    '.gitignore',
    '.gitmodules',
    '.h5pignore',
    '.travis.yml',
    'crowdin.yml',
    'library.json',
    'semantics.json',
    'upgrades.js',
    'presave.js',
    'package.json',
    'package-lock.json',
    'LICENSE',
    'LICENSE.md',
    'README.md',
    'CHANGES.md',
    'CHANGELOG.md',
    'node_modules',
    'language',
}


def _ignore_excluded(directory: str, contents: list) -> set:
    """Return names to exclude when copying the vendor tree."""
    return {name for name in contents if name in _VENDOR_EXCLUDE}


def _copy_vendor_assets(
    vendor_dir: Path,
    static_dir: Path,
) -> None:
    """Copy vendor assets to _static/, excluding metadata files."""
    if not vendor_dir.is_dir():
        logger.warning("Vendor directory not found: %s", vendor_dir)
        return
    if static_dir.exists():
        shutil.rmtree(str(static_dir))
    shutil.copytree(
        str(vendor_dir), str(static_dir),
        ignore=_ignore_excluded,
    )
    logger.info("Copied vendor assets to %s", static_dir)


_jinja_env = None  # type: Optional[Environment]


def _get_jinja_env() -> Environment:
    """Lazily initialise and return the Jinja2 template environment."""
    global _jinja_env
    if _jinja_env is None:
        _jinja_env = Environment(
            loader=PackageLoader(
                "byteb4rb1e.sphinxcontrib.h5p.utils.pkg.build",
                "templates",
            ),
            keep_trailing_newline=True,
            lstrip_blocks=True,
            trim_blocks=True,
        )
    return _jinja_env


def _write_template(
    env: Environment,
    template_name: str,
    output_path: Path,
    context: Dict[str, Any],
) -> None:
    """Render a template and write the result to a file."""
    tmpl = env.get_template(template_name)
    content = tmpl.render(**context)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(content, encoding="utf-8")
    logger.info("Wrote %s", output_path)


def _derive_dir_name(machine_name: str) -> str:
    """Derive the monorepo directory name from an H5P machine name."""
    directive_name = machine_name_to_directive(machine_name)
    return repo_name_to_dir_name(directive_name)


def build(pkg_dir: Path = Path(".")) -> int:
    """Run the build pipeline for an H5P package.

    Parameters
    ----------
    pkg_dir:
        Root directory of the bootstrapped H5P package.
        Defaults to the current working directory.
    """
    config = read_h5p_config(pkg_dir / "pyproject.toml")
    env = _get_jinja_env()

    for vendor_name, vendor_rel_path in config.vendor_map.items():
        vendor_dir = pkg_dir / vendor_rel_path

        lib_info = read_library_json(vendor_dir)
        if lib_info is None:
            logger.warning("No library.json in %s", vendor_dir)
            continue

        dir_name = _derive_dir_name(config.machine_name)
        import_name = dir_name_to_import_name(dir_name)
        src_dir = pkg_dir / "src" / dir_name_to_src_subpath(dir_name)

        # Asset registration (all packages)
        actx = assets_context(vendor_name, lib_info, import_name)
        _write_template(
            env, "_generated/__init__.py.j2",
            src_dir / "_generated" / "__init__.py", actx,
        )

        # Copy vendor assets to _static/
        _copy_vendor_assets(
            vendor_dir, src_dir / "_static" / vendor_name,
        )

        # Runnable packages: directive, params, tests, docs
        if lib_info.runnable:
            semantics = read_semantics_json(vendor_dir)
            library_str = "%s %d.%d" % (
                lib_info.machine_name,
                lib_info.major_version,
                lib_info.minor_version,
            )
            ir = parse(
                lib_info.machine_name, library_str, semantics,
            )

            # Directive and node classes
            dctx = directive_context(ir, lib_info)
            _write_template(
                env, "_generated/directive.py.j2",
                src_dir / "_generated" / "directive.py", dctx,
            )

            # Param reconstruction
            pctx = params_context(ir, semantics)
            _write_template(
                env, "_generated/params.py.j2",
                src_dir / "_generated" / "params.py", pctx,
            )

            # Test files
            tpctx = test_params_context(ir, semantics)
            tpctx["import_name"] = import_name
            _write_template(
                env, "tests/_generated/test_params.py.j2",
                pkg_dir / "tests" / "_generated" / "test_params.py",
                tpctx,
            )

            tdctx = test_directive_context(ir)
            tdctx["import_name"] = import_name
            _write_template(
                env, "tests/_generated/test_directive.py.j2",
                pkg_dir / "tests" / "_generated" / "test_directive.py",
                tdctx,
            )

            cctx = conftest_context(ir, import_name)
            _write_template(
                env, "tests/_generated/conftest.py.j2",
                pkg_dir / "tests" / "_generated" / "conftest.py",
                cctx,
            )

            # Documentation
            docsctx = docs_context(ir)
            _write_template(
                env, "docs/_generated/directive.rst.j2",
                pkg_dir / "docs" / "_generated" / "directive.rst",
                docsctx,
            )

    return 0


@dataclass
class BuildCommand(CLICommand):
    """CLI command for generating package specifics."""

    name: str = "build"
    help: str = "generate package specifics"

    def add_arguments(self, parser: argparse.ArgumentParser) -> None:
        parser.add_argument(
            "--pkg-dir",
            type=Path,
            default=".",
            help="Root directory of the H5P package (default: cwd)",
        )

    def execute(self, args: Any) -> int:
        return build(pkg_dir=Path(args.pkg_dir))


if __name__ == "__main__":
    from byteb4rb1e.utils.argparse import CLI

    cli = CLI(prog="build", description=build.__doc__)
    cmd = BuildCommand()
    cmd.add_arguments(cli.parser)
    cmd.execute(cli.parser.parse_args())
