#!/usr/bin/python3
# @file
# @ingroup ifw-iptool_compiler
# @copyright
#   SPDX-FileCopyrightText: 2026 European Southern Observatory (ESO)
#   SPDX-License-Identifier: LGPL-3.0-only
#
# @brief CLI entry point for the IpTool compiler.

"""
Compiles a set of `.ipt.yaml` files into an OTTO Instrument Package (IP)
plus Sequencer Script stubs under a target output directory.

Example:
    ifwIpToolCompiler \\
        -f example/tstins/ipt/resource/ipt/tstins/tstins.meta.ipt.yaml \\
           example/tstins/ipt/resource/ipt/tstins/*.tpl.ipt.yaml \\
        -o out

Produces:
    out/resource/ip/tstins/ip/{format,templates,constraints,checklist,laser}.json
    out/resource/ip/tstins/ip/templates/*.json
    out/ipt/tstins/obd/*.obd.json                       (demo OBDs)
    out/ipt/tstins/seq/tstins/stubs/*.py                (Sequencer stubs)
"""
from __future__ import annotations

import argparse
import glob
import logging
import os
import sys
from pathlib import Path
from typing import List

from ifw.iptool.compiler import compile_files
from ifw.iptool.errors import IpToolError


log = logging.getLogger("ifwIpToolCompiler")


def parse_args(argv=None) -> argparse.Namespace:
    p = argparse.ArgumentParser(
        description=(
            "Compile IpTool YAML (*.ipt.yaml) sources into an OTTO Instrument "
            "Package (templates, OBDs, IP-wide metadata, Sequencer stubs). "
            "Requires exactly one ip_metadata instance across the inputs."
        ),
        epilog="IFW IpTool - YAML front-end for OTTO Instrument Packages.",
    )
    p.add_argument("-f", "--files", nargs="+", required=True,
                   help="Input .ipt.yaml files (globs supported; "
                        "includes the *.meta.ipt.yaml + *.tpl.ipt.yaml of the instrument).")
    p.add_argument("-o", "--output", required=True,
                   help="Output base directory (the compiler creates 'resource/' "
                        "and 'seq/' trees underneath). Must NOT end in 'resource'.")
    p.add_argument("--no-seq-stubs", action="store_true",
                   help="Skip Sequencer Script stub generation.")
    p.add_argument("--no-manual", action="store_true",
                   help="Skip Template Reference Manual (LaTeX) generation.")
    p.add_argument("--no-zip", action="store_true",
                   help="Skip emitting the IP root as archives. "
                        "By default the compiler also writes "
                        "<output>/resource/ip/<instr>/ip.zip and "
                        "ip.tar.gz alongside the directory tree. "
                        "The flag name is historical; it covers all "
                        "archive formats.")
    p.add_argument("--keep-manual-prose", action="store_true",
                   help="Preserve a pre-existing template-manual main "
                        "file (the hand-authored prose skeleton) instead "
                        "of regenerating it. The auto-generated data "
                        "file is always regenerated. Use this once the "
                        "instrument team has started filling in prose.")
    p.add_argument("--omit-pdf", action="store_true",
                   help="Skip building the manual PDF. By default the "
                        "compiler runs pdflatex/latexmk to produce "
                        "<instr>_template_manual.pdf alongside the .tex "
                        "files. PDF build failures are non-fatal "
                        "regardless of this flag.")
    p.add_argument("--jobs", "-j", type=int, default=0, metavar="N",
                   help="Number of worker processes for the CII Config "
                        "Service load phase. 0 (default) picks "
                        "min(8, cpu_count()), capped to the input count. "
                        "1 forces serial loading (useful for debugging). "
                        "Tiny projects (<= 4 inputs) always load serially "
                        "regardless - worker-pool overhead exceeds the gain.")
    p.add_argument("-l", "--log-level", default="INFO",
                   help="Log level (DEBUG, INFO, WARN, ERROR).")
    return p.parse_args(argv)


def _resolve_patterns(patterns: List[str]) -> List[Path]:
    """Expand globs locally and via CFGPATH; preserve order, dedupe."""
    cfgpath = [p for p in os.environ.get("CFGPATH", "").split(os.pathsep) if p]
    seen = set()
    out: List[Path] = []
    for pattern in patterns:
        matches = list(glob.glob(pattern))
        if not matches:
            for root in cfgpath:
                matches = list(glob.glob(str(Path(root) / pattern)))
                if matches:
                    break
        if not matches:
            log.warning("no files matched pattern %s (local or CFGPATH)", pattern)
            continue
        for m in matches:
            resolved = Path(m).resolve()
            if resolved not in seen:
                seen.add(resolved)
                out.append(resolved)
    return out


def main(argv=None) -> int:
    args = parse_args(argv)
    logging.basicConfig(
        level=getattr(logging, args.log_level.upper(), logging.INFO),
        format="%(levelname)s:%(name)s: %(message)s",
    )

    inputs = _resolve_patterns(args.files)
    if not inputs:
        log.error("no input files to process")
        return 2

    # Require at least one template (.tpl.ipt.yaml) among the resolved
    # inputs. With only a meta file the compiler would emit an empty
    # templates/ tree and exit 0; the reviewer (correctly) called that
    # silent success out as a CLI bug.
    tpl_inputs = [p for p in inputs if p.name.endswith(".tpl.ipt.yaml")]
    if not tpl_inputs:
        log.error(
            "no template files matched (*.tpl.ipt.yaml); check the -f patterns "
            "and that the directory exists"
        )
        return 2

    out_base = Path(args.output).resolve()
    if out_base.name == "resource":
        log.error("output path should be the parent directory; do not include 'resource'. Given: %s",
                  out_base)
        return 2
    try:
        out_base.mkdir(parents=True, exist_ok=True)
    except OSError as err:
        log.error("cannot create output directory %s: %s", out_base, err)
        return 2

    try:
        result = compile_files(
            inputs, out_base,
            emit_seq_stubs=not args.no_seq_stubs,
            emit_manual=not args.no_manual,
            emit_zip=not args.no_zip,
            keep_manual_prose=args.keep_manual_prose,
            build_pdf=not args.omit_pdf,
            jobs=args.jobs,
        )
    except IpToolError as err:
        log.error("%s", err)
        return 1
    except Exception as err:  # pragma: no cover - defensive
        log.exception("unexpected compiler error: %s", err)
        return 3

    # Always print a short summary.
    print(f"Instrument : {result.instrument}")
    print(f"IP root    : {result.ip_root}")
    print(f"Templates  : {len(result.template_signatures)}")
    print(f"OBDs       : {len(result.obds)}")
    if result.seq_stubs:
        print(f"Seq stubs  : {len(result.seq_stubs)} under {result.seq_stubs[0].parent}")
    if result.constraints_json:
        print(f"Constraints: {result.constraints_json}")
    if result.checklist_json:
        print(f"Checklist  : {result.checklist_json}")
    if result.laser_json:
        print(f"Laser      : {result.laser_json}")
    if result.library_cfg:
        print(f"Library    : {result.library_cfg}")
    if result.ip_zip:
        print(f"IP zip     : {result.ip_zip}")
    if result.ip_targz:
        print(f"IP tar.gz  : {result.ip_targz}")
    if result.manual_main_tex:
        print(f"Manual TeX : {result.manual_main_tex}")
    if result.manual_pdf:
        print(f"Manual PDF : {result.manual_pdf}")
    if result.manual_errors:
        print("Manual errors (non-fatal):")
        for err in result.manual_errors:
            print(f"  - {err}")

    archives = [p for p in (result.ip_zip, result.ip_targz) if p is not None]
    if archives:
        print()
        print(f"Instrument Package available here:")
        print()
        for p in archives:
            print(f"  {p}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
