package cmd

import (
	"crypto/sha256"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/spf13/cobra"

	agentteamexec "github.com/tidbcloud/AgentTeam/desktop/internal/api/exec"
)

const maxExecInputJSONBytes = int64(1 << 20)
const maxExecTokenFileBytes = maxRuntimeTokenFileBytes
const maxExecTokenFileOpenAttempts = maxRuntimeTokenFileOpenAttempts

func newExecCommand() *cobra.Command {
	return &cobra.Command{
		Use: "exec", SilenceUsage: true, SilenceErrors: true, DisableFlagParsing: true,
		RunE: func(cmd *cobra.Command, args []string) error { return runExec(cmd, args) },
	}
}

func execRuntimeEnvAvailable(getenv func(string) string) bool {
	baseURL := strings.TrimSpace(getenv(agentteamexec.EnvBaseURL))
	tokenFile := strings.TrimSpace(getenv(agentteamexec.EnvTokenFile))
	if tokenFile == "" || !filepath.IsAbs(tokenFile) {
		return false
	}
	_, err := agentteamexec.ActionEndpoint(baseURL, "workspace", "list")
	return err == nil
}

func runExec(cmd *cobra.Command, args []string) error {
	baseURL := strings.TrimSpace(os.Getenv(agentteamexec.EnvBaseURL))
	token, err := resolveExecRuntimeToken(os.Getenv)
	if baseURL == "" || err != nil {
		return renderExecFailure(cmd, agentteamexec.NewError(agentteamexec.CodePermissionDenied, "Exec runtime environment is unavailable"))
	}
	catalog, err := (agentteamexec.LocalClient{}).FetchRuntimeCatalog(
		cmd.Context(),
		baseURL,
		token,
	)
	if err != nil {
		return renderExecFailure(cmd, err)
	}
	if help, ok := renderExecHelp(catalog, args); ok {
		fmt.Fprint(cmd.OutOrStdout(), help)
		return nil
	}
	if len(args) < 2 || len(args) > 3 {
		return renderExecFailure(cmd, agentteamexec.NewError(agentteamexec.CodeValidation, "usage: agentteam exec <domain> <action> [<json|->]"))
	}
	domain, action := args[0], args[1]
	if _, ok := catalog.Lookup(domain, action); !ok {
		return renderExecFailure(cmd, agentteamexec.NewError(agentteamexec.CodeNotFound, "unknown action"))
	}
	if len(args) == 3 && strings.HasPrefix(strings.TrimSpace(args[2]), "--") {
		return renderExecFailure(cmd, agentteamexec.NewError(agentteamexec.CodeValidation, "unknown option"))
	}
	input := "{}"
	if len(args) == 3 {
		input = args[2]
	}
	raw, _, _, err := readExecInput(cmd.InOrStdin(), input)
	if err != nil {
		return renderExecFailure(cmd, agentteamexec.NewError(agentteamexec.CodeValidation, err.Error()))
	}
	if domain == "workflow" {
		if err := agentteamexec.ValidateWorkflowActionJSON(raw); err != nil {
			return renderExecFailure(cmd, agentteamexec.NewError(agentteamexec.CodeValidation, "invalid Workflow input JSON"))
		}
	}
	if err := catalog.ValidateInput(domain, action, raw); err != nil {
		return renderExecFailure(cmd, &agentteamexec.Error{Code: agentteamexec.CodeValidation, Problem: "invalid input", Cause: err})
	}
	result, err := (agentteamexec.LocalClient{}).Call(cmd.Context(), baseURL, token, domain, action, raw)
	if err != nil {
		return renderExecFailure(cmd, err)
	}
	fmt.Fprintln(cmd.OutOrStdout(), string(result))
	return nil
}

func resolveExecRuntimeToken(getenv func(string) string) (string, error) {
	tokenFile := strings.TrimSpace(getenv(agentteamexec.EnvTokenFile))
	if tokenFile == "" || !filepath.IsAbs(tokenFile) {
		return "", errors.New("exec token file path is invalid")
	}
	return readExecRuntimeTokenFile(tokenFile, os.Lstat, os.Open)
}

func readExecRuntimeTokenFile(
	tokenFile string,
	lstat func(string) (os.FileInfo, error),
	open func(string) (*os.File, error),
) (string, error) {
	return readRuntimeTokenFile("exec", tokenFile, lstat, open)
}

func renderExecHelp(r *agentteamexec.RuntimeCatalogView, args []string) (string, bool) {
	if len(args) == 0 || (len(args) == 1 && args[0] == "--help") {
		return r.RenderRootHelp(), true
	}
	if len(args) == 1 && args[0] == "--error-schema" {
		return r.RenderErrorSchemaJSON()
	}
	if len(args) == 2 && args[1] == "--help" {
		out, ok := r.RenderDomainHelp(args[0])
		return out, ok
	}
	if len(args) == 3 && args[2] == "--help" {
		out, ok := r.RenderActionHelp(args[0], args[1])
		return out, ok
	}
	if len(args) == 3 && args[2] == "--input-schema" {
		out, ok := r.RenderActionInputSchemaJSON(args[0], args[1])
		return out, ok
	}
	if len(args) == 3 && args[2] == "--output-schema" {
		out, ok := r.RenderActionOutputSchemaJSON(args[0], args[1])
		return out, ok
	}
	return "", false
}

func readExecInput(stdin io.Reader, value string) (json.RawMessage, string, int64, error) {
	raw := []byte(value)
	var stdinHash string
	var stdinBytes int64
	if value == "-" {
		var err error
		raw, err = io.ReadAll(io.LimitReader(stdin, maxExecInputJSONBytes+1))
		if err != nil {
			return nil, "", 0, err
		}
		if int64(len(raw)) > maxExecInputJSONBytes {
			return nil, "", 0, fmt.Errorf("input-json exceeds 1 MiB")
		}
		stdinBytes = int64(len(raw))
		stdinHash = fmt.Sprintf("sha256:%x", sha256.Sum256(raw))
	}
	if int64(len(raw)) > maxExecInputJSONBytes {
		return nil, "", 0, fmt.Errorf("input-json exceeds 1 MiB")
	}
	raw = []byte(strings.TrimSpace(string(raw)))
	if len(raw) == 0 || !json.Valid(raw) {
		return nil, "", 0, fmt.Errorf("input-json must be one JSON value")
	}
	return raw, stdinHash, stdinBytes, nil
}

func renderExecFailure(cmd *cobra.Command, err error) error {
	envelope := agentteamexec.ApplyErrorBudget(err)
	data, _ := json.Marshal(map[string]any{"error": envelope})
	fmt.Fprintln(cmd.ErrOrStderr(), string(data))
	return execRenderedError{envelope: envelope}
}

type execRenderedError struct {
	envelope agentteamexec.ErrorEnvelope
}

func (e execRenderedError) Error() string {
	return agentteamexec.RenderErrorText(e.envelope)
}

func (e execRenderedError) exitCode() int {
	return agentteamexec.ErrorExitCode(e.envelope.Code)
}

// ExecExitCode reports the classified process code only for rendered Exec
// failures. Ordinary Cobra and wrapped subprocess errors return ok=false.
func ExecExitCode(err error) (code int, ok bool) {
	var rendered interface{ exitCode() int }
	if !errors.As(err, &rendered) {
		return 0, false
	}
	return rendered.exitCode(), true
}

var execCmd = newExecCommand()
