//go:build integration
// +build integration

package testlib

import (
	"crypto/md5"
	"crypto/rand"
	"encoding/base32"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/jpillora/backoff"
	"github.com/pelletier/go-toml/v2"
	"github.com/superfly/flyctl/iostreams"
	"github.com/superfly/flyctl/terminal"
)

const defaultRegion = "cdg"

type selectedRegions struct {
	primary string
	other   []string
}

var (
	regionSelectionOnce sync.Once
	regionSelection     selectedRegions
)

type platformRegion struct {
	Code             string `json:"code"`
	Name             string `json:"name"`
	GatewayAvailable bool   `json:"gateway_available"`
	RequiresPaidPlan bool   `json:"requires_paid_plan"`
	Deprecated       bool   `json:"deprecated"`
	Capacity         int    `json:"capacity"`
}

// getBestRegions fetches platform regions and returns the top N regions
// with the most available capacity, filtering for usable regions
func getBestRegions(count int) ([]string, error) {
	flyctlBin := currentRepoFlyctl()

	cmd := exec.Command(flyctlBin, "platform", "regions", "--json")
	cmd.Env = os.Environ() // Inherit environment for auth tokens

	// If using preflight test token, map it to FLY_API_TOKEN for the command
	if preflightToken := os.Getenv("FLY_PREFLIGHT_TEST_ACCESS_TOKEN"); preflightToken != "" {
		// Create new env with FLY_API_TOKEN set
		env := os.Environ()
		env = append(env, "FLY_API_TOKEN="+preflightToken)
		cmd.Env = env
	}

	output, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("failed to get platform regions (exit code %v): %s", err, string(output))
	}

	var regions []platformRegion
	if err := json.Unmarshal(output, &regions); err != nil {
		return nil, fmt.Errorf("failed to parse regions JSON: %w (output: %s)", err, string(output))
	}

	// Filter for usable regions (not deprecated, not paid-only, has gateway)
	var usable []platformRegion
	for _, r := range regions {
		if !r.Deprecated && !r.RequiresPaidPlan && r.GatewayAvailable {
			usable = append(usable, r)
		}
	}

	// Sort by capacity (highest first)
	sort.Slice(usable, func(i, j int) bool {
		return usable[i].Capacity > usable[j].Capacity
	})

	// Take top N regions
	if len(usable) < count {
		count = len(usable)
	}

	result := make([]string, count)
	for i := 0; i < count; i++ {
		result[i] = usable[i].Code
	}

	return result, nil
}

func primaryRegionFromEnv() string {
	regions := selectRegionsFromEnv()
	return regions.primary
}

func otherRegionsFromEnv() []string {
	regions := selectRegionsFromEnv()
	return append([]string(nil), regions.other...)
}

func selectRegionsFromEnv() selectedRegions {
	regionSelectionOnce.Do(func() {
		regions := strings.Fields(os.Getenv("FLY_PREFLIGHT_TEST_FLY_REGIONS"))
		if len(regions) > 0 {
			regionSelection.primary = regions[0]
			if len(regions) > 1 {
				regionSelection.other = append([]string(nil), regions[1:]...)
			}
			return
		}

		best, err := getBestRegions(2)
		if err == nil && len(best) > 0 {
			regionSelection.primary = best[0]
			if len(best) > 1 {
				regionSelection.other = append([]string(nil), best[1:]...)
			}
			terminal.Debugf("no region set with FLY_PREFLIGHT_TEST_FLY_REGIONS, auto-selected region with best capacity: %s", best[0])
			return
		}

		regionSelection.primary = defaultRegion
		terminal.Warnf("no region set with FLY_PREFLIGHT_TEST_FLY_REGIONS, failed to auto-select (%v), using fallback: %s", err, defaultRegion)
	})

	return selectedRegions{
		primary: regionSelection.primary,
		other:   append([]string(nil), regionSelection.other...),
	}
}

func RepositoryRoot() string {
	_, filename, _, _ := runtime.Caller(0)
	return filepath.Join(path.Dir(filename), "../../..")
}

func currentRepoFlyctl() string {
	_, filename, _, _ := runtime.Caller(0)
	flyctlBin := path.Join(path.Dir(filename), "../../..", "bin", "flyctl")
	return flyctlBin
}

func randomName(t testingTWrapper, prefix string) string {
	b := make([]byte, 4)
	_, err := rand.Read(b)
	if err != nil {
		t.Fatalf("failed to read from random: %v", err)
	}
	if !strings.HasPrefix(prefix, "preflight") {
		prefix = fmt.Sprintf("preflight-%s", prefix)
	}
	randStr := base32.StdEncoding.EncodeToString(b)
	randStr = strings.Replace(randStr, "=", "z", -1)
	dateStr := time.Now().Format("2006-01")
	return fmt.Sprintf("%s-%s-%s", prefix, dateStr, strings.ToLower(randStr))
}

// fun times with sockets
// https://github.com/golang/go/issues/6895#issuecomment-66088946
func socketSafeTempDir(t testing.TB) string {
	tempDir := t.TempDir()
	maxLen := 103 - len(filepath.Join(".fly", "fly-agent.sock"))
	if len(tempDir) < maxLen {
		return tempDir
	}
	hasher := md5.New()
	hasher.Write([]byte(tempDir))
	sum := base32.StdEncoding.EncodeToString(hasher.Sum(nil))
	sum = strings.Replace(sum, "=", "", -1)
	shorterTempDir := filepath.Join(os.TempDir(), sum)
	err := os.Symlink(tempDir, shorterTempDir)
	if err != nil {
		t.Fatalf("default temp dir is too long (len %d), but we failed to create symlink to %s (len %d) because: %v", len(tempDir), shorterTempDir, len(shorterTempDir), err)
	}
	t.Cleanup(func() {
		os.Remove(shorterTempDir)
	})
	return shorterTempDir
}

func tryToStopAgentsInOriginalHomeDir(flyctlBin string) {
	testIostreams, _, _, _ := iostreams.Test()
	cmd := exec.Command(flyctlBin, "agent", "stop")
	cmd.Stdin = testIostreams.In
	cmd.Stdout = testIostreams.Out
	cmd.Stderr = testIostreams.ErrOut
	err := cmd.Start()
	if err != nil {
		return
	}
	_ = cmd.Wait()
}

func tryToStopAgentsFromPastPreflightTests(t testing.TB, flyctlBin string) {
	// FIXME: make something like ps au | grep flyctl | grep $TMPDIR | grep agent, then kill those procs?
}

// RunHealthcheck verifies if an app was deployed successfully.
// It runs the checks 10 times with some backoff.
func RunHealthCheck(url string) (string, error) {
	var (
		resp *http.Response
		err  error
	)

	lastStatusCode, attempts := -1, 10
	b := &backoff.Backoff{
		Factor: 2,
		Jitter: true,
		Min:    100 * time.Millisecond,
		Max:    5 * time.Second,
	}

	for i := 0; i < attempts; i++ {
		resp, err = http.Get(url)
		if err == nil {
			lastStatusCode = resp.StatusCode
		}
		if lastStatusCode == http.StatusOK {
			break
		}

		time.Sleep(b.Duration())
	}

	if err != nil {
		return "", err
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	return string(body), nil
}

func castEnv(raw any) (map[string]string, error) {
	env := map[string]string{}

	switch cast := raw.(type) {
	case map[string]string:
		env = cast
	case []map[string]any:
		for _, raw2 := range cast {
			env2, err := castEnv(raw2)
			if err != nil {
				return nil, err
			}
			for k, v := range env2 {
				env[k] = v
			}
		}
	case []any:
		for _, raw2 := range cast {
			env2, err := castEnv(raw2)
			if err != nil {
				return nil, err
			}
			for k, v := range env2 {
				env[k] = v
			}
		}
	case map[string]any:
		for k, v := range cast {
			if stringVal, ok := v.(string); ok {
				env[k] = stringVal
			} else {
				env[k] = fmt.Sprintf("%v", v)
			}
		}
	default:
		fmt.Println(cast)
		return nil, errors.New("failed to cast 'env'")
	}

	return env, nil
}

// LoadConfig loads the app config at the given path.
func readToml(path string) (map[string]any, error) {
	buf, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	def := map[string]any{}
	err = toml.Unmarshal(buf, &def)
	if err != nil {
		return nil, err
	}

	return def, err
}

func writeToml(path string, data map[string]any) error {
	file, err := os.Create(path)
	if err != nil {
		return err
	}
	defer file.Close()

	err = toml.NewEncoder(file).Encode(data)
	if err != nil {
		return err
	}

	return nil
}

// OverwriteConfig overwrites app config fields with the given data.
func OverwriteConfig(path string, data map[string]any) error {
	cfg, err := readToml(path)
	if err != nil {
		return err
	}

	cfgEnv, err := castEnv(cfg["env"])
	if err != nil {
		return err
	}

	dataEnv, err := castEnv(data["env"])
	if err != nil {
		return err
	}

	// merge the 2 "envs"
	for k, v := range dataEnv {
		cfgEnv[k] = v
	}

	cfg["app"] = data["app"]
	cfg["env"] = cfgEnv
	cfg["primary_region"] = data["region"]

	err = writeToml(path, cfg)
	if err != nil {
		return err
	}

	return nil
}
