package jenkins

import (
	"context"
	"fmt"
	"net"
	"net/url"
	"strconv"
	"strings"
	"sync"

	"github.com/tidbcloud/AgentTeam/internal/extension/egress"
	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
)

type targetClient struct {
	baseURL *url.URL
	rest    *runner.RESTClient
}

type clientCache struct {
	mu      sync.Mutex
	byURL   map[string]*targetClient
	factory restClientFactory
}

type restClientFactory func(baseURL *url.URL) (*runner.RESTClient, error)

func newClientCache(factory restClientFactory) *clientCache {
	if factory == nil {
		factory = productionRESTClient
	}
	return &clientCache{
		byURL:   make(map[string]*targetClient),
		factory: factory,
	}
}

func productionRESTClient(baseURL *url.URL) (*runner.RESTClient, error) {
	// One transport per canonical URL keeps connection pools isolated while
	// the guarded dialer resolves DNS again before every new connection.
	transport := egress.NewGuardedTransportForPrivateOrigin()
	return runner.NewRESTClient(runner.RESTOptions{
		BaseURL:   baseURL.String(),
		Auth:      AuthSpec(),
		Transport: transport,
	})
}

func (cache *clientCache) resolve(rawBaseURL string) (*targetClient, error) {
	if cache == nil || cache.factory == nil {
		return nil, fmt.Errorf("jenkins: client cache is unavailable")
	}
	baseURL, err := validateBaseURL(rawBaseURL)
	if err != nil {
		return nil, err
	}
	key := baseURL.String()

	cache.mu.Lock()
	defer cache.mu.Unlock()
	if existing := cache.byURL[key]; existing != nil {
		return existing, nil
	}
	rest, err := cache.factory(baseURL)
	if err != nil {
		return nil, fmt.Errorf("jenkins: build client: %w", err)
	}
	client := &targetClient{baseURL: cloneURL(baseURL), rest: rest}
	cache.byURL[key] = client
	return client, nil
}

func validateBaseURL(raw string) (*url.URL, error) {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	if err != nil || parsed == nil ||
		!strings.EqualFold(parsed.Scheme, "https") || parsed.Host == "" ||
		parsed.User != nil || parsed.RawQuery != "" || parsed.ForceQuery ||
		parsed.Fragment != "" || parsed.Opaque != "" {
		return nil, fmt.Errorf("jenkins: base URL is invalid")
	}
	if strings.ContainsAny(parsed.Path, "\x00\r\n\\") ||
		(parsed.Path != "/" && strings.Contains(parsed.Path, "//")) {
		return nil, fmt.Errorf("jenkins: base URL is invalid")
	}
	escapedPath := strings.ToLower(parsed.EscapedPath())
	if strings.Contains(escapedPath, "%2f") ||
		strings.Contains(escapedPath, "%5c") {
		return nil, fmt.Errorf("jenkins: base URL is invalid")
	}
	for _, segment := range strings.Split(parsed.Path, "/") {
		if segment == "." || segment == ".." {
			return nil, fmt.Errorf("jenkins: base URL is invalid")
		}
	}

	hostname := strings.TrimSuffix(strings.ToLower(parsed.Hostname()), ".")
	if hostname == "" || strings.Contains(hostname, "%") ||
		(net.ParseIP(hostname) == nil && !validDNSHostname(hostname)) {
		return nil, fmt.Errorf("jenkins: base URL hostname is invalid")
	}
	port := parsed.Port()
	if port == "443" {
		port = ""
	} else if port != "" {
		number, conversionErr := strconv.Atoi(port)
		if conversionErr != nil || number < 1 || number > 65535 {
			return nil, fmt.Errorf("jenkins: base URL port is invalid")
		}
	}
	if port == "" {
		if strings.Contains(hostname, ":") {
			parsed.Host = "[" + hostname + "]"
		} else {
			parsed.Host = hostname
		}
	} else {
		parsed.Host = net.JoinHostPort(hostname, port)
	}
	parsed.Scheme = "https"
	parsed.Path = strings.TrimSuffix(parsed.Path, "/")
	parsed.RawPath = strings.TrimSuffix(parsed.RawPath, "/")
	return cloneURL(parsed), nil
}

func validDNSHostname(hostname string) bool {
	if len(hostname) > 253 {
		return false
	}
	for _, label := range strings.Split(hostname, ".") {
		if len(label) == 0 || len(label) > 63 ||
			!asciiLetterOrDigit(label[0]) ||
			!asciiLetterOrDigit(label[len(label)-1]) {
			return false
		}
		for index := 1; index < len(label)-1; index++ {
			if !asciiLetterOrDigit(label[index]) && label[index] != '-' {
				return false
			}
		}
	}
	return true
}

func asciiLetterOrDigit(value byte) bool {
	return value >= 'a' && value <= 'z' ||
		value >= 'A' && value <= 'Z' ||
		value >= '0' && value <= '9'
}

func cloneURL(source *url.URL) *url.URL {
	if source == nil {
		return nil
	}
	cloned := *source
	if source.User != nil {
		user := *source.User
		cloned.User = &user
	}
	return &cloned
}

func callJSONObject(
	ctx context.Context,
	client *targetClient,
	credentials runner.Credentials,
	request runner.RESTRequest,
) ([]byte, int64, error) {
	response, err := client.rest.CallResponse(
		ctx, credentials, request)
	if err != nil {
		return nil, response.RawBytes, err
	}
	body := response.Body
	if len(body) < 2 || body[0] != '{' || body[len(body)-1] != '}' {
		return nil, response.RawBytes, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Jenkins response was not an object",
			nil,
		)
	}
	return body, response.RawBytes, nil
}
