#!/usr/bin/env python3
# $Id: ppbuild_instructions 200 2026-10-12 12:00:59Z peter $
#
# ppbuild_instructions --- the CLI entry point for
# build_instructions.py's own library functions. ADDED 2026-08-26,
# per Péter's own direct preference (no extension on executable
# files, a project-specific "pp" prefix) -- see ppnetlist's own
# header comment for the full real account of why this is a real,
# separate file rather than a direct rename of build_instructions.py
# itself. Corrected from an EARLIER, mistaken categorization: this
# module was first assumed to be pure CLI (never imported elsewhere),
# but a direct dependency sweep confirmed test_build_instructions.py
# genuinely imports several of its own real functions directly
# (generate_steps, describe_step, all four formatters, etc.) -- the
# SAME real tension as the other five dual-purpose tools, just
# missed on the first pass and caught before executing anything
# based on the wrong premise.

import os
import sys
import argparse
from netlist import load_from_schematic
from auto_layout import TooManyComponentsError, compute_optimal_config, diagram_fallback, verify_connectivity
from breadboard_lib import NoFreeColumnError
from render import render
from breadboard_config import resolve_config_path, load_config
from build_instructions import generate_steps, ORDER_STRATEGIES, FORMATTERS, board_note


def _version():
    """This project's own current Dewey-decimal version -- CLI-
    specific, so it lives here, not in build_instructions.py's own
    library code. See ppnetlist's own identical helper for the
    fuller reasoning."""
    here = os.path.dirname(os.path.abspath(__file__))
    try:
        with open(os.path.join(here, "VERSION")) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#"):
                    return line
    except OSError:
        pass
    return "unknown"


def main(argv=None):
    """CLI entry point -- brought up to DevelopmentStandard.org's own
    "Options and Arguments" bar from the start (see
    ppbuild_instructions.1, this program's own manual page, renamed
    alongside this file itself).

    MOVED 2026-08-26 from build_instructions.py's own file into this
    new, separate, extensionless CLI entry point -- unchanged
    otherwise."""
    parser = argparse.ArgumentParser(
        prog="ppbuild_instructions",
        description="Convert a real KiCad schematic's own auto-layout "
                     "breadboard placement into a textual, step-by-step "
                     "build instruction list a real person can follow to "
                     "physically assemble the circuit by hand.")
    parser.add_argument("schematic", metavar="SCHEMATIC",
                         help="a real .kicad_sch file")
    parser.add_argument("-o", "--output", metavar="FILE", default=None,
                         help="output path (default: print to stdout)")
    parser.add_argument("--kicad-cli", metavar="CMD",
                         default=os.environ.get("KICAD_CLI", "kicad-cli"),
                         help="kicad-cli command (default: $KICAD_CLI if "
                              "set, else 'kicad-cli' on PATH)")
    parser.add_argument("--max-components", type=int, metavar="N",
                         help="override this project's own real, default "
                              "component-count limit (breadboard_config."
                              "DEFAULT_MAX_COMPONENTS) -- see that "
                              "constant's own docstring for the real, "
                              "direct rationale (2026-09-15)")
    parser.add_argument("--config", metavar="FILE", default=None,
                         help="a real ppbreadboard_config config file "
                              "(same resolution as pprender -- see "
                              "ppbreadboard_config(1))")
    parser.add_argument("--order", choices=sorted(ORDER_STRATEGIES),
                         default="position",
                         help="step ordering strategy (default: "
                              "%(default)s -- confirmed to minimize real "
                              "physical backtracking; see "
                              "build_instructions.py's own docstring)")
    parser.add_argument("--format", choices=sorted(FORMATTERS),
                         default="plain",
                         help="output format (default: %(default)s)")
    parser.add_argument("--child-level", action="store_true",
                         help="use simpler language, aimed at a child "
                              "around age 7, instead of the real, "
                              "default adult-level phrasing")
    parser.add_argument("--search-datasheets", action="store_true",
                         help="attempt a real web search for a more "
                              "direct datasheet link per specific part "
                              "(best-effort; falls back to an offline "
                              "search-engine query link on any failure "
                              "-- see find_datasheet_url()'s own "
                              "docstring)")
    parser.add_argument("--version", action="version",
                         version=f"%(prog)s {_version()}")
    parser.add_argument("--debug", action="store_true",
                         help="also print each step's own board index")
    parser.add_argument("--quiet", action="store_true",
                         help="suppress informational messages")
    args = parser.parse_args(argv)

    nl = load_from_schematic(args.schematic, kicad_cli=args.kicad_cli)
    config_path = resolve_config_path(explicit_path=args.config,
                                       schematic_path=args.schematic)
    board_config = load_config(config_path) if config_path else None
    # compute_optimal_config(), ADDED 2026-09-29, replacing a direct
    # layout() call -- Péter's own, direct instruction: most real
    # breadboards he has genuinely encountered have 5 rows per half,
    # so a real auto-retry (escalating sectors, THEN rows_per_half,
    # see that function's own docstring for the full, real account)
    # is worth having here by default, the SAME kind kicad_layout.
    # py's own generate_schematic_auto() already gives the KiCad-side
    # CLI. Always used, not conditional on whether --config was
    # given: board_config (None, or whatever --config resolved to)
    # is passed as base_config, the real, already-established
    # starting tier this same function's own docstring already
    # guarantees is tried first and never narrowed -- an explicit
    # --config is still fully honored, just no longer a dead end on
    # its own if it genuinely doesn't fit.
    try:
        p, anchors, passives, rails, board_config = compute_optimal_config(
            nl, base_config=board_config, max_components=args.max_components,
            verbose=args.debug)
    except TooManyComponentsError as e:
        print(f"ppbuild_instructions: {e}", file=sys.stderr)
        raise SystemExit(1)
    except RuntimeError as e:
        print(f"ppbuild_instructions: {e}", file=sys.stderr)
        raise SystemExit(1)

    # render() must run first -- see far_leg_position()'s own
    # docstring for why the far-leg column can't be resolved any
    # other way. Rendered to a real temp file, discarded -- this
    # command's own real job is the text, not the picture (pprender
    # already exists for that). render()'s own diagnostic prints
    # (hole-collision/label-overlap checks) go to stdout unconditionally
    # -- it has no real "quiet" parameter of its own -- so they're
    # suppressed HERE, locally, rather than modifying that central,
    # already-verified function just for this one, internal call.
    import tempfile
    import contextlib
    import io
    with tempfile.NamedTemporaryFile(suffix=".svg", delete=False) as tf:
        tmp_svg = tf.name
    try:
        with contextlib.redirect_stdout(io.StringIO()):
            render(nl, p, anchors, passives, rails, tmp_svg)
        # the far legs are drawn now: a lead left in another net's strip is a real short, and the physical layout of a dense circuit is the usual cause (0.72.65)
        _ok, _failures = verify_connectivity(nl, p, rails)
        _shorts = [f for f in _failures if f.startswith("accidental short")]
        if _shorts:
            diagram_fallback("ppbuild_instructions", _shorts[0], p)
    except NoFreeColumnError as e:
        diagram_fallback("ppbuild_instructions", e, p)      # does not return when it can fall back; otherwise the error is the user's to see
        raise
    finally:
        try:
            os.unlink(tmp_svg)
        except OSError:
            pass

    steps = generate_steps(nl, p, anchors, passives, rails,
                            board_config=board_config, order=args.order,
                            search_web=args.search_datasheets)
    if not args.quiet:
        print(f"generated {len(steps)} steps ({args.order} order, "
              f"{args.format} format)", file=sys.stderr)
        if p.unplaced:
            print(f"note: {len(p.unplaced)} component(s) could not be placed "
                  f"at all by the layout algorithm, and are NOT included in "
                  f"these instructions: {sorted(p.unplaced)}", file=sys.stderr)
    if args.debug:
        for n, step in enumerate(steps, 1):
            print(f"  [{n}] kind={step.kind} ref={step.ref} "
                  f"board={step.board_index}", file=sys.stderr)

    formatter = FORMATTERS[args.format]
    title = f"Build instructions: {os.path.basename(args.schematic)}"
    text = formatter(steps, child_level=args.child_level, title=title,
                     board_note=board_note(board_config))

    if args.output:
        with open(args.output, "w") as f:
            f.write(text)
        if not args.quiet:
            print(f"wrote {args.output}", file=sys.stderr)
    else:
        print(text, end="")


if __name__ == "__main__":
    main()
