// Copyright 2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package toolchain

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os/exec"
	"runtime"

	"github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)

type typeChecker int

const (
	// TypeCheckerNone is the default typeChecker
	TypeCheckerNone typeChecker = iota
	// TypeCheckerMypy is the mypy typeChecker
	TypeCheckerMypy
	// TypeCheckerPyright is the pyright typeChecker
	TypeCheckerPyright
)

type toolchain int

const (
	Pip toolchain = iota
	Poetry
)

type PythonOptions struct {
	// The root directory of the project.
	Root string
	// The program directory of the project.
	ProgramDir string
	// Virtual environment to use, relative to `Root`.
	Virtualenv string
	// Use a typechecker to type check.
	Typechecker typeChecker
	// The package manager to use for managing dependencies.
	Toolchain toolchain
}

type PythonPackage struct {
	Name     string `json:"name"`
	Version  string `json:"version"`
	Location string `json:"location"`
}

type Info struct {
	Executable string
	Version    string
}

type Toolchain interface {
	// InstallDependencies installs the dependencies of the project found in `cwd`.
	InstallDependencies(ctx context.Context, cwd string, useLanguageVersionTools,
		showOutput bool, infoWriter, errorWriter io.Writer) error
	// EnsureVenv validates virtual environment of the toolchain and creates it if it doesn't exist.
	EnsureVenv(ctx context.Context, cwd string, useLanguageVersionTools,
		showOutput bool, infoWriter, errorWriter io.Writer) error
	// ValidateVenv checks if the virtual environment of the toolchain is valid.
	ValidateVenv(ctx context.Context) error
	// ListPackages returns a list of Python packages installed in the toolchain.
	ListPackages(ctx context.Context, transitive bool) ([]PythonPackage, error)
	// Command returns an *exec.Cmd for running `python` using the configured toolchain.
	Command(ctx context.Context, args ...string) (*exec.Cmd, error)
	// ModuleCommand returns an *exec.Cmd for running an installed python module using the configured toolchain.
	// https://docs.python.org/3/using/cmdline.html#cmdoption-m
	ModuleCommand(ctx context.Context, module string, args ...string) (*exec.Cmd, error)
	// About returns information about the python executable of the toolchain.
	About(ctx context.Context) (Info, error)
}

func Name(tc toolchain) string {
	switch tc {
	case Pip:
		return "Pip"
	case Poetry:
		return "Poetry"
	default:
		return "Unknown"
	}
}

func ResolveToolchain(options PythonOptions) (Toolchain, error) {
	if options.Toolchain == Poetry {
		dir := options.ProgramDir
		if dir == "" {
			dir = options.Root
		}
		return newPoetry(dir)
	}
	return newPip(options.Root, options.Virtualenv)
}

func virtualEnvBinDirName() string {
	if runtime.GOOS == windows {
		return "Scripts"
	}
	return "bin"
}

// Determines if we should use pyenv. To use pyenv we need:
//   - pyenv installed
//   - .python-version file in the current directory or any of its parents
func usePyenv(cwd string) (bool, string, string, error) {
	versionFile, err := fsutil.Searchup(cwd, ".python-version")
	if err != nil {
		if !errors.Is(err, fsutil.ErrNotFound) {
			return false, "", "", fmt.Errorf("error while looking for .python-version %s", err)
		}
		// No .python-version file found
		return false, "", "", nil
	}
	logging.V(9).Infof("Python toolchain: found .python-version %s", versionFile)
	pyenvPath, err := exec.LookPath("pyenv")
	if err != nil {
		if !errors.Is(err, exec.ErrNotFound) {
			return false, "", "", fmt.Errorf("error while looking for pyenv %+v", err)
		}
		// No pyenv installed
		logging.V(9).Infof("Python toolchain: found .python-version file at %s, but could not find pyenv executable",
			versionFile)
		return false, "", "", nil
	}
	return true, pyenvPath, versionFile, nil
}

func installPython(ctx context.Context, cwd string, showOutput bool, infoWriter, errorWriter io.Writer) error {
	use, pyenv, versionFile, err := usePyenv(cwd)
	if err != nil {
		return err
	}
	if !use {
		return nil
	}

	if showOutput {
		_, err := infoWriter.Write([]byte(fmt.Sprintf("Installing python version from .python-version file at %s\n",
			versionFile)))
		if err != nil {
			return fmt.Errorf("error while writing to infoWriter %s", err)
		}
	}
	cmd := exec.CommandContext(ctx, pyenv, "install", "--skip-existing")
	cmd.Dir = cwd
	if showOutput {
		cmd.Stdout = infoWriter
		cmd.Stderr = errorWriter
	}
	err = cmd.Run()
	if err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			return fmt.Errorf("error while running pyenv install: %s", string(exitErr.Stderr))
		}
		return fmt.Errorf("error while running pyenv install: %s", err)
	}
	return nil
}