package autogencase

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/icholy/digest"
	"github.com/tidwall/gjson"
	"github.com/tidwall/sjson"
	"go.uber.org/ratelimit"

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

var bodyPlaceholderPattern = regexp.MustCompile(`\{([^{}]+)\}`)

type scenarioPlaceholder map[string]string // this is for scenario e2e

func (lookup scenarioPlaceholder) buildHTTPClient(auth *model.HTTPRequestAuth) *http.Client {
	if auth.AuthType != "digest" {
		return &http.Client{} // Create a new client instead of using the global one(with digest Transport)
	}
	var username, password string
	switch {
	case auth.AuthRole != "":
		username, password, _ = strings.Cut(lookup[auth.AuthRole], ":")
	case auth.Content.PublicKey == Placehodler && auth.Content.PrivateKey == Placehodler:
		username, password, _ = strings.Cut(lookup["open_api_key"], ":")
	default:
		username, password = auth.Content.PublicKey, auth.Content.PrivateKey
	}
	return &http.Client{Transport: &digest.Transport{Username: username, Password: password}}
}

func (lookup *scenarioPlaceholder) buildHTTPRequest(
	spec *model.HTTPRequestSpec,
	openAPIHost string,
	auth *model.HTTPRequestAuth,
	token string,
	base_path string,
) (*http.Request, error) {
	p := spec.Path
	method := strings.ToUpper(spec.Method)
	var body io.Reader = http.NoBody
	var bodyBytes []byte
	query := make(url.Values)
	// fill path params with the path_params in the request info
	p = lookup.buildPathParams(p, spec.PathParams)
	// fill query params with query_params in the request info
	queryparams := gjson.ParseBytes(spec.QueryParams)
	if err := lookup.fillQueryParams(&query, &queryparams); err != nil {
		return nil, fmt.Errorf("faile to parse query params %w", err)
	}
	// fill body with body in request info
	if len(spec.Body) > 0 {
		out := make(map[string]any)
		bodydata := gjson.ParseBytes(spec.Body)
		// add timesteps for case displayName
		displayName := bodydata.Get("displayName").String()
		var finalBodydata gjson.Result
		if displayName == "" {
			finalBodydata = bodydata
		} else {
			ts := time.Now().Unix() % 1000000
			newDisplayName := fmt.Sprintf("%s-%d", displayName, ts)
			updatedBodydata, err := sjson.SetBytes(spec.Body, "displayName", newDisplayName)
			if err != nil {
				panic(err)
			}
			finalBodydata = gjson.ParseBytes(updatedBodydata)
		}
		lookup.fillBody(out, &finalBodydata, spec.BodyParams)
		bs, err := json.Marshal(out)
		if err != nil {
			return nil, fmt.Errorf("faile to parse body %w", err)
		}
		bodyBytes = bs
		body = ReaderWrapper{bytes.NewReader(bs)}
	}
	u := p
	if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
		u = fmt.Sprintf("https://%s%s%s", openAPIHost, base_path, p)
	}
	if encoded := query.Encode(); encoded != "" {
		u = fmt.Sprintf("%s?%s", u, encoded)
	}
	// build request
	req, err := http.NewRequest(method, u, body)
	if err != nil {
		return nil, err
	}
	if len(bodyBytes) > 0 {
		req.ContentLength = int64(len(bodyBytes))
		req.GetBody = func() (io.ReadCloser, error) {
			return ReaderWrapper{bytes.NewReader(bodyBytes)}, nil
		}
	}
	for k, v := range spec.Headers {
		req.Header.Add(k, v)
	}
	switch auth.AuthType {
	case "bearer_token":
		req.Header.Add("Authorization", "Bearer "+token)
	case "customized":
		// Parse public_key and private_key for basic auth
		publicKey, privateKey, _ := strings.Cut((*lookup)["open_api_key"], ":")

		// // Set custom headers for basic auth (only if not already present)
		// if req.Header.Get("X-Auth-Method") == "" {
		// 	req.Header.Add("X-Auth-Method", "basic")
		// }

		if req.Header.Get("X-Auth-Content") == "" {
			// Build auth content JSON - only include non-empty fields
			var authParts []string
			if publicKey != "" {
				authParts = append(authParts, fmt.Sprintf(`"public_key": %q`, publicKey))
			}
			if privateKey != "" {
				authParts = append(authParts, fmt.Sprintf(`"private_key": %q`, privateKey))
			}

			authContent := "{" + strings.Join(authParts, ", ") + "}"
			req.Header.Add("X-Auth-Content", authContent)
		}
	}
	return req, nil
}

func (lookup scenarioPlaceholder) buildPathParams(oldPath string, params map[string]string) string {
	p := oldPath
	for k, v := range params {
		placeholderKey := v
		if v == Placehodler {
			placeholderKey = k
		}
		realworldVal := lookup[placeholderKey]
		p = strings.Replace(p, fmt.Sprintf("{%s}", k), realworldVal, 1)
	}
	return p
}

func (lookup scenarioPlaceholder) fillQueryParams(query *url.Values, params *gjson.Result) error {
	var err error
	params.ForEach(func(key, value gjson.Result) bool {
		rk := key.String()
		switch value.Type {
		case gjson.Null:
			// or may be "" as null
			query.Add(key.String(), "null")
		case gjson.False, gjson.True, gjson.Number:
			query.Add(key.String(), value.String())
		case gjson.String:
			// TODO: replace placeholder
			rv := value.String()
			placeholderKey := rv
			if placeholderKey == Placehodler {
				placeholderKey = rk
			}
			if v, ok := lookup[placeholderKey]; ok {
				rv = v
			}
			query.Add(rk, rv)
		case gjson.JSON:
			if value.IsObject() {
				if value.String() != "{}" {
					err = fmt.Errorf("found unspported vaule for key %v in query_params", key.String())
					return false
				}
				query.Add(key.String(), value.String())
			}
			if value.IsArray() {
				array := value.Array()
				for _, item := range array {
					query.Add(key.String(), item.String())
				}
			}
		}
		return true // keep iterating
	})
	return err
}

func (lookup scenarioPlaceholder) fillBody(out map[string]any, old *gjson.Result, params map[string]string) {
	old.ForEach(func(key, value gjson.Result) bool {
		k := key.String()
		switch value.Type {
		case gjson.Null:
			out[k] = json.RawMessage(value.Raw)
		case gjson.False, gjson.True:
			out[k] = value.Bool()
		case gjson.Number:
			out[k] = value.Int()
		case gjson.String:
			out[k] = lookup.resolveBodyStringValue(k, value.String(), params)
		case gjson.JSON:
			if value.IsArray() {
				array := make([]any, 0)
				for _, item := range value.Array() {
					if !item.IsObject() {
						m := make(map[string]any)
						lookup.fillBody(m, &item, params)
						array = append(array, m[""])
					} else {
						m := make(map[string]any)
						lookup.fillBody(m, &item, params)
						array = append(array, m)
					}
				}
				out[k] = array
			} else if value.IsObject() {
				m := make(map[string]any)
				lookup.fillBody(m, &value, params)
				out[k] = m
			}
		}
		return true // keep iterating
	})
}

func (lookup scenarioPlaceholder) resolveBodyStringValue(fieldKey, value string, params map[string]string) any {
	if value == Placehodler {
		if lookupKey, ok := params[fieldKey]; ok {
			return lookup[lookupKey]
		}
		return value
	}

	matches := bodyPlaceholderPattern.FindAllStringSubmatchIndex(value, -1)
	if len(matches) == 0 {
		return value
	}

	if len(matches) == 1 && matches[0][0] == 0 && matches[0][1] == len(value) {
		placeholderDef := value[matches[0][2]:matches[0][3]]
		return lookup.resolveTypedBodyPlaceholder(placeholderDef, value, params)
	}

	return bodyPlaceholderPattern.ReplaceAllStringFunc(value, func(match string) string {
		placeholderDef := strings.TrimSuffix(strings.TrimPrefix(match, "{"), "}")
		replaced, ok := lookup.resolveBodyPlaceholderValue(placeholderDef, params)
		if !ok {
			return match
		}
		return replaced
	})
}

func (lookup scenarioPlaceholder) resolveTypedBodyPlaceholder(placeholderDef, fallback string, params map[string]string) any {
	placeholderKey, typeDef, _ := strings.Cut(placeholderDef, ":")
	valStr, ok := lookup.resolveBodyPlaceholderValue(placeholderKey, params)
	if !ok {
		valStr = fallback
	}

	switch typeDef {
	case "integer":
		if integer, err := strconv.ParseInt(valStr, 10, 64); err == nil {
			return integer
		}
		return valStr
	case "number":
		if number, err := strconv.ParseFloat(valStr, 64); err == nil {
			return number
		}
		if integer, err := strconv.ParseInt(valStr, 10, 64); err == nil {
			return integer
		}
		return valStr
	default:
		return valStr
	}
}

func (lookup scenarioPlaceholder) resolveBodyPlaceholderValue(placeholderDef string, params map[string]string) (string, bool) {
	placeholderKey, _, _ := strings.Cut(placeholderDef, ":")
	lookupKey, ok := params[placeholderKey]
	if !ok {
		return "", false
	}
	return lookup[lookupKey], true
}

// gcp command may has args `/project/a/location/b/resource/tidb{clusterId}` and need to replace placeholder
func (lookup scenarioPlaceholder) buildCommandArgs(args []string, params map[string]string) []string {
	replacement := make(map[string]string)
	for rk, rv := range params {
		lookupKey := rv
		if rv == Placehodler {
			lookupKey = rk
		}
		replacement[fmt.Sprintf("{%s}", rk)] = lookup[lookupKey]
	}
	realArgs := make([]string, 0, len(args))
	for _, arg := range args {
		if strings.HasPrefix(arg, "--") {
			realArgs = append(realArgs, arg)
			continue
		}
		realArg := arg
		for rk, rv := range replacement {
			if strings.Contains(realArg, rk) {
				realArg = strings.ReplaceAll(realArg, rk, rv)
			}
		}
		realArgs = append(realArgs, realArg)
	}
	return realArgs
}

func (lookup scenarioPlaceholder) patchCELExpr(old string) string {
	if len(old) < 3 {
		return old
	}
	patched := old
	for k, v := range lookup {
		patched = strings.ReplaceAll(patched, fmt.Sprintf("{%s}", k), v)
	}
	return patched
}

type ActionBuilder struct {
	scenarioPlaceholder

	host        string
	env         string
	ratelimiter ratelimit.Limiter
	token       string
}

func NewActionBuilder(env, host, token string) *ActionBuilder {
	return &ActionBuilder{
		scenarioPlaceholder: make(scenarioPlaceholder),
		host:                host,
		env:                 env,
		token:               token,
	}
}

func (builder *ActionBuilder) SetLimiter(limiter ratelimit.Limiter) {
	builder.ratelimiter = limiter
}

func (builder *ActionBuilder) resolveScenarioHost(host string) string {
	if host == "" || !strings.Contains(host, ".env.") {
		return host
	}

	switch builder.env {
	case "dev":
		// - .env.shared. => .nightly.shared.
		// - .env. => .dev.
		if strings.Contains(host, ".env.shared.") {
			return strings.Replace(host, ".env.shared.", ".nightly.shared.", 1)
		}

		return strings.Replace(host, ".env.", "."+builder.env+".", 1)
	case "staging":
		// - .env.shared. => .staging.shared.
		// - .env. => .staging.
		return strings.Replace(host, ".env.", "."+builder.env+".", 1)
	case "prod":
		// - .env.shared. => .prod.
		// - .env. => .
		if strings.Contains(host, ".env.shared.") {
			return strings.Replace(host, ".env.shared.", ".prod.", 1)
		}

		return strings.Replace(host, ".env.", ".", 1)
	}

	return host
}

func (builder *ActionBuilder) BuildAction(step *model.GenerationStep, scc *model.ScenarioContext) (Action, error) {
	switch step.RequestType {
	case model.RequestTypeHTTP:
		action := NewHTTPRequestAction()
		spec, err := step.ParseHTTPRequestSpec()
		if err != nil {
			return nil, fmt.Errorf("ParseHTTPRequestSpec failed, err:%w", err)
		}
		expect, err := step.ParseHTTPResponseExpect()
		if err != nil {
			return nil, fmt.Errorf("ParseHTTPResponseExpect failed, err:%w", err)
		}
		save, err := step.ParseSaveSpec()
		if err != nil {
			return nil, fmt.Errorf("ParseSaveSpec failed, err:%w", err)
		}
		action.placeholderExpr = save.Placeholder
		lookup := builder.scenarioPlaceholder
		expect.CELExpr = lookup.patchCELExpr(expect.CELExpr)
		action.client = lookup.buildHTTPClient(spec.Auth)
		host := builder.host
		if scc.Host != "" {
			host = builder.resolveScenarioHost(scc.Host)
		}
		req, err := lookup.buildHTTPRequest(spec, host, spec.Auth, builder.token, scc.BasePath)
		if err != nil {
			return nil, fmt.Errorf("buildHTTPRequest failed, err:%w", err)
		}
		action.req = req
		action.maxRetries = step.MaxRetries
		action.delayAfter = step.DelayAfter
		action.expect = expect
		if builder.ratelimiter != nil {
			action.ratelimiter = builder.ratelimiter
		}
		return action, nil
	case model.RequestTypeCMDAWS, model.RequestTypeCMDGCP, model.RequestTypeCMDAzure, model.RequestTypeCMDMysqlsh, model.RequestTypeCMDALIYUN:
		action := NewCommandAction()
		spec, err := step.ParseCommandSpec()
		if err != nil {
			return nil, fmt.Errorf("ParseCommandSpec failed, err:%w", err)
		}
		expect, err := step.ParseCommandExpect()
		if err != nil {
			return nil, fmt.Errorf("ParseCommandExpect failed, err:%w", err)
		}
		save, err := step.ParseSaveSpec()
		if err != nil {
			return nil, fmt.Errorf("ParseSaveSpec failed, err:%w", err)
		}
		action.placeholderExpr = save.Placeholder
		lookup := builder.scenarioPlaceholder
		expect.CELExpr = lookup.patchCELExpr(expect.CELExpr)
		action.name = spec.Name
		action.args = lookup.buildCommandArgs(spec.Args, spec.ArgsParams)
		action.maxRetries = step.MaxRetries
		action.delayAfter = step.DelayAfter
		action.expect = expect
		return action, nil
	}
	return nil, fmt.Errorf("RequestType %s not supported", step.RequestType)
}
