from __future__ import annotations

import argparse
import sys
from typing import TYPE_CHECKING, Any, cast

from pdm import termui
from pdm.cli import actions
from pdm.cli.commands.base import BaseCommand
from pdm.cli.hooks import HookManager
from pdm.cli.options import skip_option
from pdm.cli.templates import ProjectTemplate
from pdm.exceptions import PdmUsageError, ProjectError
from pdm.models.backends import _BACKENDS, DEFAULT_BACKEND, BuildBackend, get_backend, get_backend_by_spec
from pdm.models.specifiers import get_specifier
from pdm.utils import (
    get_user_email_from_git,
    package_installed,
    sanitize_project_name,
    validate_project_name,
)

if TYPE_CHECKING:
    from pdm.project import Project


class Command(BaseCommand):
    """Initialize a pyproject.toml for PDM.

    Built-in templates:
    - default: `pdm init`, A simple template with a basic structure.
    - minimal: `pdm init minimal`, A minimal template with only `pyproject.toml`.
    """

    supports_other_generator = True

    def __init__(self) -> None:
        self.interactive = True

    def initialize_git(self, project: Project) -> None:
        """Initialize a git repository if git is available and .git doesn't exist."""
        import shutil
        import subprocess

        if (project.root / ".git").exists():
            project.core.ui.info("Git repository already exists, skipping initialization.")
            return

        git_command = shutil.which("git")
        if not git_command:
            project.core.ui.info("Git command not found, skipping initialization.")
            return

        try:
            subprocess.run(
                [git_command, "init"],
                cwd=project.root,
                check=True,
                capture_output=True,
                encoding="utf-8",
            )
            project.core.ui.info("Git repository initialized successfully.")
        except subprocess.CalledProcessError as e:
            project.core.ui.error(f"Failed to initialize Git repository: {e.stderr}")

    def do_init(self, project: Project, options: argparse.Namespace) -> None:
        """Bootstrap the project and create a pyproject.toml"""
        hooks = HookManager(project, options.skip)
        workspace_project = project.parent_workspace_project
        is_workspace_member = workspace_project is not None
        gitignore_file = project.root / ".gitignore"
        has_gitignore = gitignore_file.exists()
        match options.generator:
            case "copier":
                self._init_copier(project, options)
            case "cookiecutter":
                self._init_cookiecutter(project, options)
            case _:
                if not is_workspace_member:
                    self.set_python(project, options.python, hooks)
                self._init_builtin(project, options)
        if is_workspace_member and not has_gitignore:
            gitignore_file.unlink(missing_ok=True)

        if is_workspace_member:
            options.init_git = False
        elif options.init_git and self.interactive:
            options.init_git = termui.confirm("Do you want to initialize a git repository?", default=True)
        if options.init_git:
            self.initialize_git(project)

        hooks.try_emit("post_init")

    def _init_copier(self, project: Project, options: argparse.Namespace) -> None:
        if not package_installed("copier"):
            raise PdmUsageError(
                "--copier is passed but copier is not installed. Install it by `pdm self add copier`"
            ) from None

        from copier.cli import CopierApp

        if not options.template:
            raise PdmUsageError("template argument is required when --copier is passed")
        _, retval = CopierApp.run(
            ["copier", "copy", options.template, str(project.root), *options.generator_args], exit=False
        )
        if retval != 0:
            raise RuntimeError("Copier exited with non-zero status code")

    def _init_cookiecutter(self, project: Project, options: argparse.Namespace) -> None:
        if not package_installed("cookiecutter"):
            raise PdmUsageError(
                "--cookiecutter is passed but cookiecutter is not installed. Install it by `pdm self add cookiecutter`"
            ) from None

        from cookiecutter.cli import main as cookiecutter

        if not options.template:
            raise PdmUsageError("template argument is required when --cookiecutter is passed")
        try:
            cookiecutter.main(
                [options.template, "--output-dir", str(project.root), *options.generator_args], standalone_mode=False
            )
        except SystemExit as e:
            raise RuntimeError("Cookiecutter exited with an error") from e

    def _init_builtin(self, project: Project, options: argparse.Namespace) -> None:
        metadata = self.get_metadata_from_input(project, options)
        template = options.template
        if not template:
            template = "default" if options.dist else "minimal"
        with ProjectTemplate(template) as template:
            template.generate(project.root, metadata, options.overwrite)
        project.pyproject.reload()

    def set_interactive(self, value: bool) -> None:
        self.interactive = value

    def ask(self, question: str, default: str) -> str:
        if not self.interactive:
            return default
        return termui.ask(question, default=default)

    def ask_project(self, project: Project, default: str | None = None) -> str:
        default = default or sanitize_project_name(project.root.name)
        name = self.ask("Project name", default)
        if default == name or validate_project_name(name):
            return name
        project.core.ui.echo(
            "Project name is not valid, it should follow PEP 426",
            err=True,
            style="warning",
        )
        return self.ask_project(project, default)

    def get_metadata_from_input(self, project: Project, options: argparse.Namespace) -> dict[str, Any]:
        from pdm.formats.base import array_of_inline_tables, make_array, make_inline_table

        existing_data = project.pyproject.open_for_read()
        existing_project = existing_data.get("project", {})
        existing_settings = existing_data.get("tool", {}).get("pdm", {})

        if options.name:
            if not validate_project_name(options.name):
                raise ProjectError("Project name is not valid, it should follow PEP 426")
            name = options.name
        else:
            name = self.ask_project(project, existing_project.get("name"))
        version = self.ask("Project version", options.project_version or existing_project.get("version", "0.1.0"))
        is_dist = options.dist or bool(options.backend) or bool(existing_settings.get("distribution", False))
        if not (options.dist or options.backend) and self.interactive:
            is_dist = termui.confirm(
                "Do you want to build this project for distribution(such as wheel)?\n"
                "If yes, it will be installed by default when running `pdm install`.",
                default=is_dist,
            )
        options.dist = is_dist
        build_backend: type[BuildBackend] | None = None
        existing_build_system = existing_data.get("build-system", {})
        workspace_project = project.parent_workspace_project
        python = None if workspace_project is not None else project.python
        python_major, python_minor = (python.major, python.minor) if python is not None else sys.version_info[:2]
        if is_dist:
            description = self.ask("Project description", existing_project.get("description", ""))
            if options.backend:
                build_backend = get_backend(options.backend)
            elif self.interactive:
                all_backends = list(_BACKENDS)
                default_backend = get_backend_by_spec(existing_build_system)
                default_backend_index = next(
                    (i for i, backend in enumerate(all_backends) if get_backend(backend) is default_backend), 0
                )
                project.core.ui.echo("Which build backend to use?")
                for i, backend in enumerate(all_backends):
                    project.core.ui.echo(f"{i}. [success]{backend}[/]")
                selected_backend = termui.ask(
                    "Please select",
                    prompt_type=int,
                    choices=[str(i) for i in range(len(all_backends))],
                    show_choices=False,
                    default=default_backend_index,
                )
                build_backend = get_backend(all_backends[int(selected_backend)])
            else:
                build_backend = get_backend_by_spec(existing_build_system) if existing_build_system else DEFAULT_BACKEND
            default_python_requires = f">={python_major}.{python_minor}"
        else:
            description = ""
            default_python_requires = f"=={python_major}.{python_minor}.*"
        if workspace_project is not None:
            default_python_requires = workspace_project.pyproject.metadata.get(
                "requires-python", default_python_requires
            )

        existing_license = existing_project.get("license", {})
        if isinstance(existing_license, dict):
            default_license = existing_license.get("text") or existing_license.get("file") or "MIT"
        else:
            default_license = existing_license or "MIT"
        license = self.ask("License(SPDX name)", options.license or default_license)

        git_user, git_email = get_user_email_from_git()
        existing_authors = existing_project.get("authors", [])
        existing_author = existing_authors[0] if existing_authors else {}
        author = self.ask("Author name", existing_author.get("name", git_user))
        email = self.ask("Author email", existing_author.get("email", git_email))
        python_requires = self.ask(
            "Python requires('*' to allow any)", existing_project.get("requires-python", default_python_requires)
        )

        data = {
            "project": {
                "name": name,
                "version": version,
                "authors": array_of_inline_tables([{"name": author, "email": email}]),
                "license": make_inline_table({"text": license}),
                "dependencies": make_array([], True),
            },
            "tool": {"pdm": {"distribution": is_dist}},
        }

        if python_requires and python_requires != "*":
            get_specifier(python_requires)
            data["project"]["requires-python"] = python_requires  # type: ignore[index]
        if description:
            data["project"]["description"] = description  # type: ignore[index]
        if build_backend is not None:
            data["build-system"] = cast(dict, build_backend.build_system())

        return data

    def add_arguments(self, parser: argparse.ArgumentParser) -> None:
        skip_option.add_to_parser(parser)

        status = {
            False: termui.style("\\[not installed]", style="error"),
            True: termui.style("\\[installed]", style="success"),
        }
        if self.supports_other_generator:
            generator = parser.add_mutually_exclusive_group()
            generator.add_argument(
                "--copier",
                action="store_const",
                dest="generator",
                const="copier",
                help=f"Use Copier to generate project {status[package_installed('copier')]}",
            )
            generator.add_argument(
                "--cookiecutter",
                action="store_const",
                dest="generator",
                const="cookiecutter",
                help=f"Use Cookiecutter to generate project {status[package_installed('cookiecutter')]}",
            )
        group = parser.add_argument_group("builtin generator options")
        group.add_argument(
            "-n",
            "--non-interactive",
            action="store_true",
            help="Don't ask questions but use default values",
        )
        group.add_argument("--python", help="Specify the Python version/path to use")
        group.add_argument(
            "--dist", "--lib", dest="dist", action="store_true", help="Create a package for distribution"
        )
        group.add_argument("--backend", choices=list(_BACKENDS), help="Specify the build backend, which implies --dist")
        group.add_argument("--license", help="Specify the license (SPDX name)")
        group.add_argument("--name", help="Specify the project name")
        group.add_argument("--project-version", help="Specify the project's version")
        group.add_argument(
            "--no-git", dest="init_git", action="store_false", default=True, help="Do not initialize a git repository"
        )
        parser.add_argument(
            "template", nargs="?", help="Specify the project template, which can be a local path or a Git URL"
        )
        if self.supports_other_generator:
            parser.add_argument("generator_args", nargs=argparse.REMAINDER, help="Arguments passed to the generator")
        parser.add_argument("-r", "--overwrite", action="store_true", help="Overwrite existing files")
        parser.set_defaults(search_parent=False, generator="builtin")

    def set_python(self, project: Project, python: str | None, hooks: HookManager) -> None:
        from pdm.cli.commands.use import Command as UseCommand

        python_info = UseCommand().do_use(
            project,
            python or "",
            first=bool(python) or not self.interactive,
            ignore_remembered=True,
            ignore_requires_python=True,
            save=False,
            hooks=hooks,
        )

        if python_info.get_venv() is None:
            project.core.ui.info(
                "You are using the PEP 582 mode, no virtualenv is created.\n"
                "You can change configuration with `pdm config python.use_venv True`.\n"
                "For more info, please visit https://peps.python.org/pep-0582/"
            )
        project.python = python_info

    def handle(self, project: Project, options: argparse.Namespace) -> None:
        if project.pyproject.exists():
            project.core.ui.echo("pyproject.toml already exists, update it now.", style="primary")
        else:
            project.core.ui.echo("Creating a pyproject.toml for PDM...", style="primary")
        self.set_interactive(not options.non_interactive and termui.is_interactive())
        self.do_init(project, options=options)
        project.maybe_add_to_workspace()
        project.core.ui.echo("Project is initialized successfully", style="primary")
        if self.interactive:
            actions.ask_for_import(project)
