#!/usr/bin/env python3
"""Projects/files."""

from pathlib import Path

from libcli.basecli import BaseCLI


class ProjectsFilesCLI(BaseCLI):
    """CLI for listing Python projects and expanding file paths across them."""

    config = {
        # name of config file.
        "config-file": Path("~/.pf.toml"),
        # toml [section-name].
        "config-name": "pf",
    }

    def init_config(self) -> None:
        """Load default project groups from the configuration file."""

        super().init_config()

        # projects to display when unspecified
        self._default_projects = []

        if not (default_group_names := self.config.get("default_groups")):
            return
        if not (groups := self.config.get("groups")):
            return

        composite_groups = self.config.get("composite_groups", {})

        for group_name in default_group_names:
            if group := groups.get(group_name):
                self._default_projects.append(group)
            elif group_name in composite_groups:
                flat = [path for gn in composite_groups[group_name] for path in groups[gn]]
                self._default_projects.append(flat)
            else:
                raise KeyError(f"Unknown group `{group_name}` in `{self.config['config-file']}`")

    def init_parser(self) -> None:
        """Initialize the argument parser with program description."""

        self.parser = self.ArgumentParser(
            description=self.dedent("""
        List python projects and files. Expands to project/file, in build order,
        from least to most dependent.
                """),
        )

    def add_arguments(self) -> None:
        """Add command-line arguments for project selection and file expansion."""

        self.parser.add_argument(
            "-s",
            "--short",
            action="store_true",
            help=self.dedent("""
        When no `FILESPEC` is given, `%(prog)s` prints the paths to the selected projects.
        With `--short`, only the project names are printed.
                """),
        )

        self._add_section_config_file()
        self._add_section_project_groups()
        self._add_section_default_groups()
        self._add_section_excludes()
        self._add_section_additional_projects()
        self._add_section_filenames()

    def _add_section_config_file(self) -> None:
        self.parser.add_argument_group(
            "Configuration file",
            self.dedent("""
        The configuration file (see `--config FILE` below) defines
        projects, project groups, and the order in which they are
        to be built.
                """),
        )

    def _add_section_project_groups(self) -> None:
        title = "Project Groups"
        if not (groups := self.config.get("groups")):
            self.parser.add_argument_group(title, "No projects or groups are configured.")
            return

        parser = self.parser.add_argument_group(
            title, "Select one or more from the current configuration:"
        )

        for name, paths in groups.items():
            parser.add_argument(
                f"--{name}",
                dest="projects",
                action="append_const",
                const=paths,
                help="[" + ", ".join(f"`{Path(path).name}`" for path in paths) + "]",
            )

        if composite_groups := self.config.get("composite_groups"):
            for name, group_names in composite_groups.items():
                flat = [path for gn in group_names for path in groups[gn]]
                parser.add_argument(
                    f"--{name}",
                    dest="projects",
                    action="append_const",
                    const=flat,
                    help="[" + " + ".join(f"`{gn}`" for gn in group_names) + "]",
                )

    def _add_section_default_groups(self) -> None:
        description = (
            self.dedent(
                """
        When no projects groups are specified, the `default_groups` list from
        the configuration file is used:
            """,
            )
            + "\n\n"
        )

        if not (default_groups := self.config.get("default_groups")):
            description += "The `default_groups` list is not configured."
        else:
            description += "[" + ", ".join(f"`{x}`" for x in default_groups) + "]."
            description += "\n\nThis may be a subset of the `Project Groups` above."

        self.parser.add_argument_group("Default Groups", description)

    def _add_section_excludes(self) -> None:
        parser = self.parser.add_argument_group(
            "Exclude Projects", "Exclude one or more projects:"
        )

        parser.add_argument(
            "--exclude",
            dest="excludes",
            metavar="PROJECT",
            nargs="+",
            type=Path,
            help="list of projects to exclude",
        )

    def _add_section_additional_projects(self) -> None:
        group = self.parser.add_argument_group(
            "Additional projects",
            "Projects that are not defined in the configuration file may also be given:",
        )

        group.add_argument(
            "--prepend",
            dest="prepend_projects",
            metavar="PROJECT",
            nargs="+",
            type=Path,
            help="list of projects to prepend",
        )

        group.add_argument(
            "--project",
            dest="projects",
            metavar="PROJECT",
            nargs="+",
            type=Path,
            help="list of projects to use instead of project groups",
        )

        group.add_argument(
            "--append",
            dest="append_projects",
            metavar="PROJECT",
            nargs="+",
            type=Path,
            help="list of projects to append",
        )

    def _add_section_filenames(self) -> None:
        group = self.parser.add_argument_group("Filenames")

        glob = group.add_mutually_exclusive_group()
        arg = glob.add_argument(
            "--glob",
            dest="glob",
            action="store_true",
            default=True,
            help="expand wildcards in `FILESPEC`",
        )
        self.add_default_to_help(arg)

        arg = glob.add_argument(
            "--no-glob",
            dest="glob",
            action="store_false",
            help="don't expand wildcards in `FILESPEC`",
        )
        self.add_default_to_help(arg)

        group.add_argument(
            "filespec",
            metavar="FILESPEC",
            nargs="?",
            help=self.dedent("""
    Join `FILESPEC` to projects. Any `%%` within `FILESPEC` is replaced
    with the name of the project.
                """),
        )

    # Too many branches; main dispatches over many mutually exclusive output options.
    def main(self) -> None:  # noqa: PLR0912
        """Print project paths or expanded file paths to stdout."""

        # pylint: disable=too-many-branches

        if self.options.excludes and (groups := self.config.get("groups")):
            projects = [Path(x).expanduser() for y in groups.values() for x in y]
            if errors := [str(x) for x in self.options.excludes if x not in projects]:
                self.parser.error(f"Invalid excludes {errors}")

        list_of_projects = []

        if self.options.prepend_projects:
            list_of_projects.append(self.options.prepend_projects)

        if self.options.projects:
            list_of_projects.extend(self.options.projects)
        else:
            list_of_projects.extend(self._default_projects)

        if self.options.append_projects:
            list_of_projects.append(self.options.append_projects)

        for projects in list_of_projects:
            for project in projects:
                path = Path(project).expanduser()
                if self.options.excludes and path in self.options.excludes:
                    continue
                if not self.options.filespec:
                    print(path.name if self.options.short else path)
                    continue

                filespec = self.options.filespec
                if "%" in filespec:
                    filespec = filespec.replace("%", Path(project).name)

                if not self.options.glob:
                    print(path.joinpath(filespec))
                else:
                    for file in path.glob(filespec):
                        print(file)


def main() -> None:
    """CLI entry point."""
    ProjectsFilesCLI().main()


if __name__ == "__main__":
    main()
