package testdiff

import (
	"encoding/json"
	"fmt"
	"path/filepath"
	"regexp"
	"runtime"
	"slices"
	"strings"

	"github.com/databricks/cli/internal/testutil"
	"github.com/databricks/cli/libs/iamutil"
	"github.com/databricks/databricks-sdk-go"
	"github.com/databricks/databricks-sdk-go/service/iam"
)

const (
	testerName = "$USERNAME"
)

var (
	uuidRegex        = regexp.MustCompile(`[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}`)
	numIdRegex       = regexp.MustCompile(`[0-9]{3,}`)
	privatePathRegex = regexp.MustCompile(`(/tmp|/private)(/.*)/([a-zA-Z0-9]+)`)
)

type Replacement struct {
	Old *regexp.Regexp
	New string
}

type ReplacementsContext struct {
	Repls []Replacement
}

func (r *ReplacementsContext) Clone() ReplacementsContext {
	return ReplacementsContext{Repls: slices.Clone(r.Repls)}
}

func (r *ReplacementsContext) Replace(s string) string {
	// QQQ Should probably only replace whole words
	for _, repl := range r.Repls {
		s = repl.Old.ReplaceAllString(s, repl.New)
	}
	return s
}

func (r *ReplacementsContext) append(pattern *regexp.Regexp, replacement string) {
	r.Repls = append(r.Repls, Replacement{
		Old: pattern,
		New: replacement,
	})
}

func (r *ReplacementsContext) appendLiteral(old, new string) {
	r.append(
		// Transform the input strings such that they can be used as literal strings in regular expressions.
		regexp.MustCompile(regexp.QuoteMeta(old)),
		// Transform the replacement string such that `$` is interpreted as a literal dollar sign.
		// For more information about how the replacement string is used, see [regexp.Regexp.Expand].
		strings.ReplaceAll(new, `$`, `$$`),
	)
}

func (r *ReplacementsContext) Set(old, new string) {
	if old == "" || new == "" {
		return
	}

	// Always include both verbatim and json version of replacement.
	// This helps when the string in question contains \ or other chars that need to be quoted.
	// In that case we cannot rely that json(old) == '"{old}"' and need to add it explicitly.

	encodedNew, err := json.Marshal(new)
	if err == nil {
		encodedOld, err := json.Marshal(old)
		if err == nil {
			r.appendLiteral(trimQuotes(string(encodedOld)), trimQuotes(string(encodedNew)))
		}
	}

	r.appendLiteral(old, new)
}

func trimQuotes(s string) string {
	if len(s) > 0 && s[0] == '"' {
		s = s[1:]
	}
	if len(s) > 0 && s[len(s)-1] == '"' {
		s = s[:len(s)-1]
	}
	return s
}

func (r *ReplacementsContext) SetPath(old, new string) {
	r.Set(old, new)

	if runtime.GOOS != "windows" {
		return
	}

	// Support both forward and backward slashes
	m1 := strings.ReplaceAll(old, "\\", "/")
	if m1 != old {
		r.Set(m1, new)
	}

	m2 := strings.ReplaceAll(old, "/", "\\")
	if m2 != old && m2 != m1 {
		r.Set(m2, new)
	}
}

func (r *ReplacementsContext) SetPathWithParents(old, new string) {
	r.SetPath(old, new)
	r.SetPath(filepath.Dir(old), new+"_PARENT")
	r.SetPath(filepath.Dir(filepath.Dir(old)), new+"_GPARENT")
}

func PrepareReplacementsWorkspaceClient(t testutil.TestingT, r *ReplacementsContext, w *databricks.WorkspaceClient) {
	t.Helper()
	// in some clouds (gcp) w.Config.Host includes "https://" prefix in others it's really just a host (azure)
	host := strings.TrimPrefix(strings.TrimPrefix(w.Config.Host, "http://"), "https://")
	r.Set("https://"+host, "$DATABRICKS_URL")
	r.Set("http://"+host, "$DATABRICKS_URL")
	r.Set(host, "$DATABRICKS_HOST")
	r.Set(w.Config.ClusterID, "$DATABRICKS_CLUSTER_ID")
	r.Set(w.Config.WarehouseID, "$DATABRICKS_WAREHOUSE_ID")
	r.Set(w.Config.ServerlessComputeID, "$DATABRICKS_SERVERLESS_COMPUTE_ID")
	r.Set(w.Config.MetadataServiceURL, "$DATABRICKS_METADATA_SERVICE_URL")
	r.Set(w.Config.AccountID, "$DATABRICKS_ACCOUNT_ID")
	r.Set(w.Config.Token, "$DATABRICKS_TOKEN")
	r.Set(w.Config.Username, "$DATABRICKS_USERNAME")
	r.Set(w.Config.Password, "$DATABRICKS_PASSWORD")
	r.Set(w.Config.Profile, "$DATABRICKS_CONFIG_PROFILE")
	r.Set(w.Config.ConfigFile, "$DATABRICKS_CONFIG_FILE")
	r.Set(w.Config.GoogleServiceAccount, "$DATABRICKS_GOOGLE_SERVICE_ACCOUNT")
	r.Set(w.Config.GoogleCredentials, "$GOOGLE_CREDENTIALS")
	r.Set(w.Config.AzureResourceID, "$DATABRICKS_AZURE_RESOURCE_ID")
	r.Set(w.Config.AzureClientSecret, "$ARM_CLIENT_SECRET")
	// r.Set(w.Config.AzureClientID, "$ARM_CLIENT_ID")
	r.Set(w.Config.AzureClientID, testerName)
	r.Set(w.Config.AzureTenantID, "$ARM_TENANT_ID")
	r.Set(w.Config.ActionsIDTokenRequestURL, "$ACTIONS_ID_TOKEN_REQUEST_URL")
	r.Set(w.Config.ActionsIDTokenRequestToken, "$ACTIONS_ID_TOKEN_REQUEST_TOKEN")
	r.Set(w.Config.AzureEnvironment, "$ARM_ENVIRONMENT")
	r.Set(w.Config.ClientID, "$DATABRICKS_CLIENT_ID")
	r.Set(w.Config.ClientSecret, "$DATABRICKS_CLIENT_SECRET")
	r.Set(w.Config.DatabricksCliPath, "$DATABRICKS_CLI_PATH")
	// This is set to words like "path" that happen too frequently
	// r.Set(w.Config.AuthType, "$DATABRICKS_AUTH_TYPE")
}

func PrepareReplacementsUser(t testutil.TestingT, r *ReplacementsContext, u iam.User) {
	t.Helper()
	// There could be exact matches or overlap between different name fields, so sort them by length
	// to ensure we match the largest one first and map them all to the same token

	r.Set(u.UserName, testerName)
	r.Set(u.DisplayName, testerName)
	if u.Name != nil {
		r.Set(u.Name.FamilyName, testerName)
		r.Set(u.Name.GivenName, testerName)
	}

	for _, val := range u.Emails {
		r.Set(val.Value, testerName)
	}

	r.Set(iamutil.GetShortUserName(&u), testerName)

	for ind, val := range u.Groups {
		r.Set(val.Value, fmt.Sprintf("$USER.Groups[%d]", ind))
	}

	r.Set(u.Id, "$USER.Id")

	for ind, val := range u.Roles {
		r.Set(val.Value, fmt.Sprintf("$USER.Roles[%d]", ind))
	}
}

func PrepareReplacementsUUID(t testutil.TestingT, r *ReplacementsContext) {
	t.Helper()
	r.append(uuidRegex, "<UUID>")
}

func PrepareReplacementsNumber(t testutil.TestingT, r *ReplacementsContext) {
	t.Helper()
	r.append(numIdRegex, "<NUMID>")
}

func PrepareReplacementsTemporaryDirectory(t testutil.TestingT, r *ReplacementsContext) {
	t.Helper()
	r.append(privatePathRegex, "/tmp/.../$3")
}
