"""Write the CLI reference for hsql into `docs/generated/hsql-reference.md`.

The command already describes itself twice -- `--help` for a person, `--spec`
for a machine -- and neither is a page. This renders the third view from the
same source: every option with its type, the values it takes, its default, its
environment variable and its help, plus the arguments, the output formats and
the exit codes.

Usage:
    uv run python scripts/write_cli_reference.py

Built from `bare_command()`, with no adapter loaded, exactly as
`write_config_schema.py` is: the artifact describes the command rather than the
machine it was generated on, so it is publishable and reproducible.

`tests/unit_tests/test_cli_reference.py` is what fails when the file and the
generator disagree, which is any change to hsql's options, to what they take,
or to the formats and exit codes below.
"""

from __future__ import annotations

import inspect
from pathlib import Path
from typing import Any, Sequence

import click

from harlequin.hsql import output
from harlequin.hsql.cli import bare_command
from harlequin.hsql.diagnostics import ExitCode
from harlequin.hsql.modes.spec import command_document

REFERENCE_PATH = (
    Path(__file__).resolve().parents[1] / "docs" / "generated" / "hsql-reference.md"
)

BANNER = "<!-- Generated by scripts/write_cli_reference.py. Do not edit by hand. -->"

PREAMBLE = """\
Generated from the bare `hsql` command, with no adapters loaded. An adapter
contributes its own options on top of these: `hsql --help -a NAME` shows one
adapter's, and `hsql --spec` reports every installed adapter's as JSON.\
"""

OPTIONS_NOTE = "Alphabetical by name. Flags are off by default."

ADAPTER_NOTE = """\
`-a`/`--adapter` names an installed adapter plug-in. `hsql --info` reports
adapters installed in your environment.\
"""

FORMATS_NOTE = """\
`--format` takes one of these names, which affect the layout of data in stdout.
Modify output further with options above (e.g., `-t`, `-A`, `--display-rows`).
`none` runs the SQL but generates no output. The suffix is what `-o DIRECTORY`
names a file with.\
"""

INSTALLATION_DEPENDENT = {"adapter"}
"""Options whose choices are the plug-ins one machine happens to have. The
document describes the command, so it names the option and not the answer --
the same call the packaged base config schema makes about `adapter`."""

EXIT_CODES = {
    ExitCode.OK: "Success.",
    ExitCode.QUERY: "The database rejected the SQL.",
    ExitCode.USAGE: "A bad flag, a bad profile, or a config file hsql could not read.",
    ExitCode.CONNECTION: "hsql could not connect.",
    ExitCode.TIMEOUT: "`--timeout` ran out, and hsql stopped the run.",
    ExitCode.INTERRUPT: "Interrupted.",
}
"""What each code means, in a sentence. A copy of what the enum's own docstrings
say, because a docstring on an enum member is not there at run time; the test
is what fails when a new code arrives without one."""


def build_reference() -> str:
    """The whole page, as the bytes the file holds."""
    command = bare_command()
    document = command_document(command)
    blocks = [
        BANNER,
        f"# {command.name}",
        inspect.cleandoc(command.help or ""),
        PREAMBLE,
        "## Usage",
        _usage(command),
        "## Arguments",
        _arguments_table(document["arguments"]),
        "## Options",
        OPTIONS_NOTE,
        _options_table(document["options"]),
        ADAPTER_NOTE,
        "## Formats",
        FORMATS_NOTE,
        _formats_table(),
        "## Exit codes",
        _exit_codes_table(),
    ]
    return "\n\n".join(blocks) + "\n"


def _usage(command: click.Command) -> str:
    context = click.Context(command, info_name=command.name)
    pieces = command.collect_usage_pieces(context)
    return _fence(" ".join([str(command.name), *pieces]))


def _arguments_table(arguments: Sequence[dict[str, Any]]) -> str:
    return _table(
        ["Argument", "Type", "Accepts"],
        [
            [
                _code(str(argument["metavar"])),
                str(argument["type"]),
                _accepts(int(argument["nargs"]), bool(argument["required"])),
            ]
            for argument in arguments
        ],
    )


def _options_table(options: Sequence[dict[str, Any]]) -> str:
    return _table(
        ["Option", "Type", "Values", "Default", "Env var", "Description"],
        [
            [
                ", ".join(_code(decl) for decl in option["decls"]),
                str(option["type"]),
                _values(option),
                _default(option),
                _envvar(option),
                _cell(option["help"] or ""),
            ]
            for option in options
        ],
    )


def _values(option: dict[str, Any]) -> str:
    """What may be written after the option: its choices, or its placeholder."""
    choices = None if option["name"] in INSTALLATION_DEPENDENT else option["choices"]
    if choices:
        return ", ".join(_code(str(choice)) for choice in choices)
    return _code(str(option["metavar"])) if option["metavar"] else ""


def _default(option: dict[str, Any]) -> str:
    """What the command uses when the option is absent.

    Empty for a flag, which the note above the table covers once rather than
    20 times.
    """
    default = option["default"]
    if option["is_flag"] or default is None:
        return ""
    return _code(str(default))


def _envvar(option: dict[str, Any]) -> str:
    envvar = option["envvar"]
    if envvar is None:
        return ""
    names = [envvar] if isinstance(envvar, str) else envvar
    return ", ".join(_code(str(name)) for name in names)


def _accepts(nargs: int, required: bool) -> str:
    if nargs == -1:
        return "one or more" if required else "zero or more"
    return "one" if nargs == 1 else str(nargs)


def _formats_table() -> str:
    return _table(
        ["Format", "Kind", "Suffix", "Holds several result sets"],
        [
            [
                _code(name),
                _kind(name),
                _code(output.suffix(name)) if output.suffix(name) else "",
                "yes" if output.holds_many(name) else "no",
            ]
            for name in output.format_names()
        ],
    )


def _kind(format_name: str) -> str:
    if format_name == output.NONE:
        return "writes nothing"
    return "text layout" if output.is_layout(format_name) else "file"


def _exit_codes_table() -> str:
    missing = [code for code in ExitCode if code not in EXIT_CODES]
    if missing:
        raise ValueError(
            f"EXIT_CODES does not describe: {', '.join(code.name for code in missing)}"
        )
    return _table(
        ["Code", "Meaning"],
        [[_code(str(code.value)), EXIT_CODES[code]] for code in ExitCode],
    )


def _table(headers: Sequence[str], rows: Sequence[Sequence[str]]) -> str:
    lines = [
        "| " + " | ".join(headers) + " |",
        "| " + " | ".join("---" for _ in headers) + " |",
        *("| " + " | ".join(row) + " |" for row in rows),
    ]
    return "\n".join(lines)


def _fence(text: str) -> str:
    return f"```\n{text}\n```"


def _code(text: str) -> str:
    """A code span in a table cell, so the pipe in `all|last|N` is escaped."""
    escaped = text.replace("|", r"\|")
    return f"`{escaped}`"


def _cell(text: str) -> str:
    """One paragraph of help, as a table cell: no newlines, no stray pipes."""
    return " ".join(text.split()).replace("|", r"\|")


def main() -> None:
    REFERENCE_PATH.parent.mkdir(parents=True, exist_ok=True)
    REFERENCE_PATH.write_text(build_reference(), encoding="utf-8", newline="\n")
    print(f"wrote {REFERENCE_PATH}")


if __name__ == "__main__":
    main()
