package autogencase

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"maps"
	"math"
	"net/http"
	"os/exec"
	"strconv"
	"strings"
	"time"

	"github.com/avast/retry-go"
	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/checker/decls"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/common/types/ref"
	"go.uber.org/ratelimit"

	"github.com/tidbcloud/tidb-management-service/functional-test/cases/openapidedicated/autogencase/model"
	"github.com/tidbcloud/tidb-management-service/functional-test/cases/openapidedicated/autogencase/utils"
)

type Action interface {
	Do() error
	BuildResult(_ *model.GenerationStep) (*model.ScenarioResultRecord, error)
	FillPlaceholder(placeholder scenarioPlaceholder)
}

type HTTPRequestAction struct {
	ratelimiter     ratelimit.Limiter
	maxRetries      int
	delayAfter      int
	client          *http.Client
	req             *http.Request
	placeholderExpr []model.SaveExpression
	expect          *model.HTTPResponseExpect
	attempted       bool
	// out
	resp             *http.Response
	dur              time.Duration
	savedPlaceholder map[string]string
}

func NewHTTPRequestAction() *HTTPRequestAction {
	return &HTTPRequestAction{
		savedPlaceholder: make(map[string]string),
	}
}

func (action *HTTPRequestAction) Do() error {
	action.savedPlaceholder["curr_action_do_start_unix"] = strconv.FormatInt(time.Now().Unix(), 10)
	// current time +10 mins
	t := time.Now().UTC().Add(10 * time.Minute)
	formatted := t.Format("2006-01-02T15:04:05.000Z")
	action.savedPlaceholder["curr_action_delay_10min_time"] = formatted
	action.savedPlaceholder["curr_action_delay_10min_hhmm"] = t.Format("15:04")

	if action.maxRetries == 0 {
		if err := action.doRequest(); err != nil {
			return err
		}
		return action.doSavePlaceholder()
	}
	if err := retry.Do(action.doRequest,
		retry.Attempts(uint(action.maxRetries+1)),
		retry.Delay(time.Duration(action.delayAfter)*time.Second),
		retry.MaxDelay(time.Duration(action.delayAfter)*time.Second),
	); err != nil {
		return err
	}
	return action.doSavePlaceholder()
}

func (action *HTTPRequestAction) doRequest() error {
	if action.ratelimiter != nil {
		action.ratelimiter.Take()
	}
	if action.attempted && action.req != nil && action.req.GetBody != nil {
		if action.req.Body != nil {
			_ = action.req.Body.Close()
		}
		body, err := action.req.GetBody()
		if err != nil {
			return err
		}
		action.req.Body = body
	}
	action.attempted = true
	start := time.Now()
	resp, err := action.client.Do(action.req)
	if err != nil {
		return err
	}
	action.dur = time.Since(start)
	bs, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	_ = resp.Body.Close()
	resp.Body = ReaderWrapper{bytes.NewReader(bs)}
	action.resp = resp
	action.printHTTPRequestResponse()

	if action.expect != nil {
		isExpected, err := action.expect.IsExpected(action.resp)
		if err != nil {
			return fmt.Errorf("evalute expect failed, err: %w", err)
		}
		if !isExpected {
			return fmt.Errorf("response is unexpected, expression:%s", action.expect)
		}
	}
	return nil
}

func (action *HTTPRequestAction) doSavePlaceholder() error {
	if len(action.placeholderExpr) == 0 {
		return nil
	}
	// if action.resp.StatusCode != http.StatusOK {
	// 	return nil
	// }
	// only save placeholder on success
	if rseeker, ok := action.resp.Body.(io.ReadSeeker); ok {
		_, _ = rseeker.Seek(0, 0)
	}
	bs, err := io.ReadAll(action.resp.Body)
	if err != nil {
		return err
	}
	celVars, bodyEnvOpt, err := utils.ParseHTTPBodyToCElVars(bs)
	if err != nil {
		return err
	}
	celVars["saved_placeholder"] = action.savedPlaceholder
	savedEnvOpt := cel.Declarations(decls.NewVar("saved_placeholder", decls.NewMapType(decls.String, decls.Dyn)))
	env, err := cel.NewEnv(bodyEnvOpt, savedEnvOpt)
	if err != nil {
		return err
	}
	for _, expr := range action.placeholderExpr {
		key := expr.Key
		eval := expr.Eval
		ast, issues := env.Compile(eval)
		if issues != nil && issues.Err() != nil {
			return issues.Err()
		}
		prg, err := env.Program(ast)
		if err != nil {
			return err
		}
		out, _, err := prg.Eval(celVars)
		if err != nil {
			return err
		}
		str, err := convertToGoString(out)
		if err != nil {
			return err
		}
		action.savedPlaceholder[key] = str
	}
	// log.Printf("save placeholder resp:%v, placeholder:%v", string(bs), action.savedPlaceholder)
	return nil
}

func (action *HTTPRequestAction) BuildResult(step *model.GenerationStep) (*model.ScenarioResultRecord, error) {
	if action.expect == nil {
		return step.BuildHTTPRequestResult(action.req, action.resp, action.dur)
	}

	patchedStep := *step
	patchedExpect := strings.TrimSpace(action.expect.String())
	patchedStep.Expect = &patchedExpect
	return patchedStep.BuildHTTPRequestResult(action.req, action.resp, action.dur)
}

func (action *HTTPRequestAction) FillPlaceholder(placeholder scenarioPlaceholder) {
	maps.Copy(placeholder, action.savedPlaceholder)
}

func (action *HTTPRequestAction) printHTTPRequestResponse() {
	reqBody := ""
	respBody := ""
	if action.req != nil {
		if rseeker, ok := action.req.Body.(io.ReadSeeker); ok {
			_, _ = rseeker.Seek(0, 0)
		}
		bs, _ := io.ReadAll(action.req.Body)
		reqBody = string(bs)
	}
	if action.resp != nil {
		if rseeker, ok := action.resp.Body.(io.ReadSeeker); ok {
			_, _ = rseeker.Seek(0, 0)
		}
		bs, _ := io.ReadAll(action.resp.Body)
		respBody = string(bs)
	}
	log.Printf("DEBUG: http request, url:%v, method:%v, request_body:%v, resp_header:%v, resp_body:%v, status_code:%v",
		action.req.URL.String(), action.req.Method, reqBody, action.resp.Header, respBody, action.resp.StatusCode)
}

type CommandAction struct {
	maxRetries      int
	delayAfter      int
	name            string
	args            []string
	placeholderExpr []model.SaveExpression
	expect          *model.CommandExpect
	// output
	output           []byte
	exitCode         int
	dur              time.Duration
	savedPlaceholder map[string]string
}

func NewCommandAction() *CommandAction {
	return &CommandAction{
		savedPlaceholder: make(map[string]string),
	}
}

func (action *CommandAction) Do() error {
	action.savedPlaceholder["curr_action_do_start_unix"] = strconv.FormatInt(time.Now().Unix(), 10)
	// current time +10 mins
	t := time.Now().UTC().Add(10 * time.Minute)
	formatted := t.Format("2006-01-02T15:04:05.000Z")
	action.savedPlaceholder["curr_action_delay_10min_time"] = formatted
	action.savedPlaceholder["curr_action_delay_10min_hhmm"] = t.Format("15:04")

	if action.maxRetries == 0 {
		if err := action.doCommand(); err != nil {
			return err
		}
		return action.doSavePlaceholder()
	}
	if err := retry.Do(action.doCommand,
		retry.Attempts(uint(action.maxRetries+1)),
		retry.Delay(time.Duration(action.delayAfter)*time.Second),
		retry.MaxDelay(time.Duration(action.delayAfter)*time.Second),
	); err != nil {
		return err
	}
	return action.doSavePlaceholder()
}

func (action *CommandAction) doCommand() error {
	start := time.Now()
	cmd := exec.Command(action.name, action.args...) //nolint:gosec // name is check in actionBuilder
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Printf("ERROR: exec fialed, command:%v, output:%v, err:%v", cmd.String(), string(out), err)
		return err
	}
	action.exitCode = cmd.ProcessState.ExitCode()
	action.dur = time.Since(start)
	action.output = out
	action.printCommandOutput()
	if action.expect != nil {
		isExpected, err := action.expect.IsExpected(action.exitCode, action.output)
		if err != nil {
			return fmt.Errorf("evalute expect failed, err: %w", err)
		}
		if !isExpected {
			return fmt.Errorf("output is unexpected, expression:%s", action.expect)
		}
	}
	return nil
}

func (action *CommandAction) doSavePlaceholder() error {
	if len(action.placeholderExpr) == 0 {
		return nil
	}
	// if action.resp.StatusCode != http.StatusOK {
	// 	return nil
	// }
	// only save placeholder on success
	action.printCommandOutput()
	celVars, outputEnvOpt, err := utils.ParseCMDOutputToCElVars(action.output)
	if err != nil {
		return err
	}
	celVars["saved_palceholder"] = action.savedPlaceholder
	savedEnvOpt := cel.Declarations(decls.NewVar("saved_palceholder", decls.NewMapType(decls.String, decls.Dyn)))
	env, err := cel.NewEnv(outputEnvOpt, savedEnvOpt)
	if err != nil {
		return err
	}
	for _, expr := range action.placeholderExpr {
		key := expr.Key
		eval := expr.Eval
		ast, issues := env.Compile(eval)
		if issues != nil && issues.Err() != nil {
			return issues.Err()
		}
		prg, err := env.Program(ast)
		if err != nil {
			return err
		}
		// the evaluated data is
		// {
		//		"output": object or array
		// }
		out, _, err := prg.Eval(celVars)
		if err != nil {
			return err
		}
		str, err := convertToGoString(out)
		if err != nil {
			return err
		}
		action.savedPlaceholder[key] = str
	}
	// log.Printf("save placeholder output:%v, placeholder:%v", string(action.output), action.savedPlaceholder)
	return nil
}

func (action *CommandAction) printCommandOutput() {
	log.Printf("DEBUG: commad, name:%v, args:%v, output:%s", action.name, action.args, string(action.output))
}

// FIXME: move BuildResult logic into action
func (action *CommandAction) BuildResult(step *model.GenerationStep) (*model.ScenarioResultRecord, error) {
	return step.BuildCommandResult(action.exitCode, action.output, action.dur)
}

func (action *CommandAction) FillPlaceholder(placeholder scenarioPlaceholder) {
	maps.Copy(placeholder, action.savedPlaceholder)
}

func convertToGoString(val ref.Val) (string, error) {
	switch v := val.(type) {
	case types.String:
		return string(v), nil
	case types.Int, types.Bool:
		return fmt.Sprintf("%v", v), nil
	case types.Double:
		f := float64(v)
		if !math.IsNaN(f) && !math.IsInf(f, 0) && math.Trunc(f) == f && f >= math.MinInt64 && f <= math.MaxInt64 {
			return strconv.FormatInt(int64(f), 10), nil
		}
		return strconv.FormatFloat(f, 'f', -1, 64), nil
	default:
		return "", fmt.Errorf("unsupported type for conversion to string: %T", v)
	}
}
