package server

import (
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"

	"github.com/pingcap/agent-git-service/internal/tenant"
)

type tenantDomainBinding struct {
	Info    tenant.Info
	Aliases []string
}

type tenantResolver struct {
	trustForwardedHost bool
	tenantByHost       map[string]tenant.Info
}

type tenantResolution struct {
	Tenant tenant.Info
	Host   string
	Source string
	Reason string
	OK     bool
}

func newTenantResolver(bindings []tenantDomainBinding, trustForwardedHost bool) (tenantResolver, error) {
	if len(bindings) == 0 {
		return tenantResolver{}, fmt.Errorf("at least one tenant domain binding is required")
	}
	resolver := tenantResolver{
		trustForwardedHost: trustForwardedHost,
		tenantByHost:       make(map[string]tenant.Info),
	}
	for _, binding := range bindings {
		if err := binding.Info.Validate(); err != nil {
			return tenantResolver{}, err
		}
		origins := append([]string{binding.Info.BaseURL()}, binding.Aliases...)
		for _, origin := range origins {
			keys, err := tenantOriginHostKeys(origin)
			if err != nil {
				return tenantResolver{}, fmt.Errorf("tenant %q domain %q: %w", binding.Info.ID, origin, err)
			}
			for _, host := range keys {
				if existing, ok := resolver.tenantByHost[host]; ok && existing.ID != binding.Info.ID {
					return tenantResolver{}, fmt.Errorf("tenant host %q is bound to both %q and %q", host, existing.ID, binding.Info.ID)
				}
				resolver.tenantByHost[host] = binding.Info.Clone()
			}
		}
	}
	return resolver, nil
}

func (r tenantResolver) resolve(req *http.Request) tenantResolution {
	rawHost, source, ok := requestTenantHost(req, r.trustForwardedHost)
	if !ok {
		return tenantResolution{Source: source, Reason: "invalid_host"}
	}
	for _, host := range tenantHostKeys(rawHost, "") {
		if info, exists := r.tenantByHost[host]; exists {
			return tenantResolution{Tenant: info.Clone(), Host: host, Source: source, OK: true}
		}
	}
	return tenantResolution{Source: source, Reason: "unknown_host"}
}

func tenantOriginHostKeys(origin string) ([]string, error) {
	parsed, err := url.Parse(strings.TrimSpace(origin))
	if err != nil || parsed.Scheme == "" || parsed.Host == "" || parsed.User != nil {
		return nil, fmt.Errorf("must be an absolute HTTP(S) origin")
	}
	if !strings.EqualFold(parsed.Scheme, "http") && !strings.EqualFold(parsed.Scheme, "https") {
		return nil, fmt.Errorf("must use HTTP or HTTPS")
	}
	if (parsed.Path != "" && parsed.Path != "/") || parsed.RawQuery != "" || parsed.Fragment != "" {
		return nil, fmt.Errorf("must not include a path, query, or fragment")
	}
	keys := tenantHostKeys(parsed.Host, parsed.Scheme)
	if len(keys) == 0 {
		return nil, fmt.Errorf("contains an invalid host")
	}
	return keys, nil
}

func requestTenantHost(req *http.Request, trustForwardedHost bool) (host, source string, ok bool) {
	if trustForwardedHost {
		values := req.Header.Values("X-Forwarded-Host")
		if len(values) != 1 {
			return "", "x_forwarded_host", false
		}
		raw := strings.TrimSpace(values[0])
		if raw == "" || strings.Contains(raw, ",") {
			return "", "x_forwarded_host", false
		}
		return raw, "x_forwarded_host", true
	}
	host = strings.TrimSpace(req.Host)
	return host, "host", host != ""
}

func tenantHostKeys(rawHost, scheme string) []string {
	rawHost = strings.ToLower(strings.TrimSpace(rawHost))
	if rawHost == "" || strings.HasSuffix(rawHost, ":") || strings.ContainsAny(rawHost, "/\\@?#\t\r\n ") {
		return nil
	}

	host := rawHost
	port := ""
	if parsedHost, parsedPort, err := net.SplitHostPort(rawHost); err == nil {
		host = strings.ToLower(strings.TrimSpace(parsedHost))
		port = strings.TrimSpace(parsedPort)
	} else if strings.Count(rawHost, ":") > 1 {
		if !strings.HasPrefix(rawHost, "[") || !strings.HasSuffix(rawHost, "]") {
			return nil
		}
		host = strings.Trim(rawHost, "[]")
	}
	if host == "" {
		return nil
	}

	withPort := host
	if port != "" {
		withPort = net.JoinHostPort(host, port)
	} else if strings.Contains(host, ":") {
		withPort = "[" + host + "]"
	}
	keys := []string{withPort}
	if tenantDefaultPort(scheme, port) {
		keys = append(keys, host)
	}
	return keys
}

func tenantDefaultPort(scheme, port string) bool {
	switch strings.ToLower(strings.TrimSpace(scheme)) {
	case "http":
		return port == "80"
	case "https":
		return port == "443"
	case "":
		return port == "80" || port == "443"
	default:
		return false
	}
}
