package python

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strings"
)

type Python struct {
	Requirements string `json:"requirements"`
	Script       string `json:"script"`

	env []string
}

func (a *Python) SetEnv(env []string) {
	a.env = env
}

func (a *Python) Run(ctx context.Context, payload json.RawMessage) ([]byte, error) {
	tempDir, err := os.MkdirTemp("", "catalyst_action")
	if err != nil {
		return nil, err
	}

	defer os.RemoveAll(tempDir)

	b, err := pythonSetup(ctx, tempDir)
	if err != nil {
		var ee *exec.ExitError
		if errors.As(err, &ee) {
			b = append(b, ee.Stderr...)
		}

		return nil, fmt.Errorf("failed to setup python, %w: %s", err, string(b))
	}

	b, err = a.pythonInstallRequirements(ctx, tempDir)
	if err != nil {
		var ee *exec.ExitError
		if errors.As(err, &ee) {
			b = append(b, ee.Stderr...)
		}

		return nil, fmt.Errorf("failed to run install requirements, %w: %s", err, string(b))
	}

	b, err = a.pythonRunScript(ctx, tempDir, string(payload))
	if err != nil {
		var ee *exec.ExitError
		if errors.As(err, &ee) {
			b = append(b, ee.Stderr...)
		}

		return nil, fmt.Errorf("failed to run script, %w: %s", err, string(b))
	}

	return b, nil
}

func pythonSetup(ctx context.Context, tempDir string) ([]byte, error) {
	pythonPath, err := findExec("python3", "python")
	if err != nil {
		return nil, fmt.Errorf("python or python3 binary not found, %w", err)
	}

	// setup virtual environment
	return exec.CommandContext(ctx, pythonPath, "-m", "venv", tempDir+"/venv").Output()
}

func (a *Python) pythonInstallRequirements(ctx context.Context, tempDir string) ([]byte, error) {
	hasRequirements := len(strings.TrimSpace(a.Requirements)) > 0

	if !hasRequirements {
		return nil, nil
	}

	requirementsPath := tempDir + "/requirements.txt"

	if err := os.WriteFile(requirementsPath, []byte(a.Requirements), 0o600); err != nil {
		return nil, err
	}

	// install dependencies
	pipPath := tempDir + "/venv/bin/pip"

	return exec.CommandContext(ctx, pipPath, "install", "-r", requirementsPath).Output()
}

func (a *Python) pythonRunScript(ctx context.Context, tempDir, payload string) ([]byte, error) {
	scriptPath := tempDir + "/script.py"

	if err := os.WriteFile(scriptPath, []byte(a.Script), 0o600); err != nil {
		return nil, err
	}

	pythonPath := tempDir + "/venv/bin/python"

	cmd := exec.CommandContext(ctx, pythonPath, scriptPath, payload)

	cmd.Env = a.env

	return cmd.Output()
}

func findExec(name ...string) (string, error) {
	for _, n := range name {
		if p, err := exec.LookPath(n); err == nil {
			return p, nil
		}
	}

	return "", errors.New("no executable found")
}
