# SPDX-FileCopyrightText: 2021 - 2023  StorPool <support@storpool.com>
# SPDX-License-Identifier: BSD-2-Clause
"""Substitute variant data using Jinja2 templates."""

from __future__ import annotations

import dataclasses
import functools
import json
import pathlib
import typing

import cfg_diag
import click
import jinja2
import trivver

from sp_variant import defs
from sp_variant import variant


if typing.TYPE_CHECKING:
    from typing import Final


@dataclasses.dataclass(frozen=True)
class Config(cfg_diag.Config):
    """Runtime configuration for the variant substitution tool."""

    output: pathlib.Path
    output_mode: int
    template: pathlib.Path


def regex_un_x(value: str) -> str:
    """Un-re.X a regular expression... somewhat."""
    return (
        value.replace(" ", "")
        .replace("\t", "")
        .replace("\n", "")
        .replace("\\s", "[[:space:]]")
        .replace("(?:", "(")
    )


def rust_bool(value: bool) -> str:  # noqa: FBT001  # this is a conversion routine
    """Format a boolean value as a Rust bool one."""
    return "true" if value else "false"


def dictvsort(data: dict[str, variant.Variant]) -> list[tuple[str, variant.Variant]]:
    """Sort a dict of variants by name, preserving some numerical order."""

    def compare(left: tuple[str, variant.Variant], right: tuple[str, variant.Variant]) -> int:
        """Compare two variants by name."""
        res: Final = trivver.compare(left[0], right[0])
        assert res  # noqa: S101  # just to be absolutely sure
        return res

    return sorted(data.items(), key=functools.cmp_to_key(compare))


def vsort(names: list[str]) -> list[str]:
    """Sort a list of variant names, preserving some numerical order."""
    return sorted(names, key=trivver.key_compare)


def build_json(var: variant.Variant) -> str:
    """Represent the variant data as a JSON string."""
    return json.dumps(defs.jsonify(var), sort_keys=True, indent=2)


def substitute(cfg: Config) -> None:
    """Perform the substitutions."""
    cfg.diag_("Building the variants data")
    variants: Final = variant.get_all_variants_in_order()

    cfg.diag_("Preparing the Jinja substitution environment")
    jenv: Final = jinja2.Environment(
        autoescape=False,  # noqa: S701
        loader=jinja2.FileSystemLoader(cfg.template.parent),
        undefined=jinja2.StrictUndefined,
    )
    jenv.filters["dictvsort"] = dictvsort
    jenv.filters["regexunx"] = regex_un_x
    jenv.filters["rust_bool"] = rust_bool
    jenv.filters["vsort"] = vsort
    jvars: Final = {
        "format_version": defs.FORMAT_VERSION,
        "order": [var.name for var in variants],
        "repotypes": {repo.name: repo for repo in defs.REPO_TYPES},
        "variants": {var.name: var for var in variants},
        "variants_json": {var.name: build_json(var) for var in variants},
        "version": defs.VERSION,
    }

    cfg.diag_("Rendering the template")
    result: Final = jenv.get_template(cfg.template.name).render(**jvars)
    cfg.diag(lambda: f"Got {len(result)} characters")

    cfg.diag(lambda: f"Generating the {cfg.output} output file")
    cfg.output.write_text(result, encoding="UTF-8")
    cfg.output.chmod(cfg.output_mode)

    cfg.diag(lambda: f"Rendered {cfg.template} into {cfg.output}")


class OctalInteger(click.ParamType):
    """Convert a string value to an integer using base 8."""

    name = "octal"

    def convert(
        self, value: str | int, param: click.Parameter | None, ctx: click.Context | None
    ) -> int:
        """Convert a string to an integer using base 8."""
        if isinstance(value, int):
            return value

        try:
            return int(value, 8)
        except ValueError as err:
            self.fail(f"{value!r} is not a valid integer: {err}", param, ctx)


OCTAL_INTEGER: Final = OctalInteger()


@click.command(name="subst")
@click.option(
    "-m",
    "--mode",
    type=OCTAL_INTEGER,
    default=0o644,
    help="the octal permissions mode of the output file",
)
@click.option(
    "-o",
    "--output",
    type=click.Path(
        dir_okay=False, file_okay=True, writable=True, resolve_path=True, path_type=pathlib.Path
    ),
    required=True,
    help="the output file to generate",
)
@click.option(
    "-t",
    "--template",
    type=click.Path(
        exists=True,
        dir_okay=False,
        file_okay=True,
        readable=True,
        resolve_path=True,
        path_type=pathlib.Path,
    ),
    required=True,
    help="the template to render",
)
@click.option(
    "-v",
    "--verbose",
    type=bool,
    is_flag=True,
    help="verbose operation; display diagnostic messages",
)
def main(*, mode: int, output: pathlib.Path, template: pathlib.Path, verbose: bool) -> None:
    """Parse command-line arguments, substitute data."""
    cfg: Final = Config(
        output=output,
        output_mode=mode,
        template=template,
        verbose=verbose,
    )
    substitute(cfg)


if __name__ == "__main__":
    main()
