package surf

import (
	"context"
	"fmt"
	"time"

	"github.com/enetx/g"
	"github.com/enetx/http"
)

// browser represents the browser type being impersonated for fingerprinting.
type browser int

const (
	unknownBrowser browser = iota // No specific browser fingerprinting
	chromeBrowser                 // Chrome browser fingerprinting
	firefoxBrowser                // Firefox browser fingerprinting
)

// Builder provides a fluent interface for configuring HTTP clients with various advanced features
// including proxy settings, TLS fingerprinting, HTTP/2 and HTTP/3 support, retry logic,
// redirect handling, and browser impersonation capabilities.
type Builder struct {
	cli                      *Client                                    // The client being configured
	proxy                    any                                        // Proxy configuration (static string/slice or dynamic function)
	checkRedirect            func(*http.Request, []*http.Request) error // Custom redirect policy function
	http2settings            *HTTP2Settings                             // HTTP/2 specific settings
	http3settings            *HTTP3Settings                             // HTTP/3 specific settings
	retryCodes               g.Slice[int]                               // HTTP status codes that trigger retries
	cliMWs                   *middleware[*Client]                       // Priority-ordered client middlewares
	retryWait                time.Duration                              // Wait duration between retry attempts
	retryMax                 int                                        // Maximum number of retry attempts
	maxRedirects             int                                        // Maximum number of redirects to follow
	forceHTTP1               bool                                       // Force HTTP/1.1 protocol usage
	cacheBody                bool                                       // Enable response body caching
	followOnlyHostRedirects  bool                                       // Only follow redirects within same host
	forwardHeadersOnRedirect bool                                       // Preserve headers during redirects
	ja                       bool                                       // Enable JA3 TLS fingerprinting
	singleton                bool                                       // Use singleton pattern for connection reuse
	browser                  browser                                    // Browser type for fingerprinting
	http3                    bool                                       // Enable HTTP/3 with automatic browser detection
}

// Build sets the provided settings for the client and returns the updated client.
// It configures various settings like HTTP2, sessions, keep-alive, dial TLS, resolver,
// interface address, timeout, and redirect policy.
func (b *Builder) Build() *Client {
	// Apply HTTP/3 settings lazily if enabled
	if b.http3 {
		http3s := b.HTTP3Settings()

		switch b.browser {
		case chromeBrowser:
			http3s.Chrome().Set()
		case firefoxBrowser:
			http3s.Firefox().Set()
		default:
			// Default to Chrome if no browser was detected
			http3s.Chrome().Set()
		}
	}

	// apply each middleware to the Client
	b.cliMWs.run(b.cli)

	return b.cli
}

// With registers middleware into the client builder with optional priority.
//
// It accepts one of the following middleware function types:
//   - func(*surf.Client) error   — client middleware, modifies or initializes the client
//   - func(*surf.Request) error  — request middleware, intercepts or transforms outgoing requests
//   - func(*surf.Response) error — response middleware, intercepts or transforms incoming responses
//
// Parameters:
//   - middleware: A function matching one of the supported middleware types.
//   - priority (optional): Integer priority level. Lower values run earlier. Defaults to 0.
//
// Middleware with the same priority are executed in order of insertion (FIFO).
// If the middleware type is not recognized, With panics with an informative error.
//
// Example:
//
//	// Adding client middleware to modify client settings.
//	.With(func(client *surf.Client) error {
//	    // Custom logic to modify the client settings.
//	    return nil
//	})
//
//	// Adding request middleware to intercept outgoing requests.
//	.With(func(req *surf.Request) error {
//	    // Custom logic to modify outgoing requests.
//	    return nil
//	})
//
//	// Adding response middleware to intercept incoming responses.
//	.With(func(resp *surf.Response) error {
//	    // Custom logic to handle incoming responses.
//	    return nil
//	})
//
// Note: Ensure that middleware functions adhere to the specified function signatures to work correctly with the With method.
func (b *Builder) With(middleware any, priority ...int) *Builder {
	p := g.Int(g.Slice[int](priority).Get(0).UnwrapOrDefault())

	switch v := middleware.(type) {
	case func(*Client) error:
		b.addCliMW(v, p)
	case func(*Request) error:
		b.addReqMW(v, p)
	case func(*Response) error:
		b.addRespMW(v, p)
	default:
		panic(fmt.Sprintf("invalid middleware type: %T", v))
	}

	return b
}

// addCliMW adds a client middleware to the ClientBuilder.
func (b *Builder) addCliMW(m func(*Client) error, priority g.Int) *Builder {
	b.cliMWs.add(priority, m)
	return b
}

// addReqMW adds a request middleware to the ClientBuilder.
func (b *Builder) addReqMW(m func(*Request) error, priority g.Int) *Builder {
	b.cli.reqMWs.add(priority, m)
	return b
}

// addRespMW adds a response middleware to the ClientBuilder.
func (b *Builder) addRespMW(m func(*Response) error, priority g.Int) *Builder {
	b.cli.respMWs.add(priority, m)
	return b
}

func (b *Builder) Boundary(boundary func() g.String) *Builder {
	return b.addCliMW(func(client *Client) error { return boundaryMW(client, boundary) }, 999)
}

// Singleton configures the client to use a singleton instance, ensuring there's only one client instance.
// This is needed specifically for JA or Impersonate functionalities.
//
//	cli := surf.NewClient().
//		Builder().
//		Singleton(). // for reuse client
//		Impersonate().
//		FireFox().
//		Build()
//
//	defer cli.CloseIdleConnections()
func (b *Builder) Singleton() *Builder {
	b.singleton = true
	return b
}

// H2C configures the client to handle HTTP/2 Cleartext (h2c).
func (b *Builder) H2C() *Builder { return b.addCliMW(h2cMW, 999) }

// HTTP2Settings configures settings related to HTTP/2 and returns an http2s struct.
func (b *Builder) HTTP2Settings() *HTTP2Settings {
	h2 := &HTTP2Settings{builder: b}
	b.http2settings = h2

	return h2
}

// HTTP3Settings configures settings related to HTTP/3 and returns an http3s struct.
func (b *Builder) HTTP3Settings() *HTTP3Settings {
	h3 := &HTTP3Settings{builder: b}
	b.http3settings = h3
	b.http3 = false

	return h3
}

// HTTP3 enables HTTP/3 with automatic browser detection.
// Settings are applied lazily in Build() based on the impersonated browser.
// Usage: surf.NewClient().Builder().Impersonate().Chrome().HTTP3().Build()
func (b *Builder) HTTP3() *Builder {
	b.http3 = true
	return b
}

// Impersonate configures something related to impersonation and returns an impersonate struct.
func (b *Builder) Impersonate() *Impersonate { return &Impersonate{builder: b} }

// JA configures the client to use a specific TLS fingerprint.
func (b *Builder) JA() *JA {
	b.ja = true
	return &JA{builder: b}
}

// UnixSocket sets the path for a Unix domain socket.
// This allows the HTTP client to connect to the server using a Unix domain
// socket instead of a traditional TCP/IP connection.
func (b *Builder) UnixSocket(address g.String) *Builder {
	return b.addCliMW(func(client *Client) error { return unixSocketMW(client, address) }, 0)
}

// DNS sets the custom DNS resolver address.
func (b *Builder) DNS(dns g.String) *Builder {
	return b.addCliMW(func(client *Client) error { return dnsMW(client, dns) }, 0)
}

// DNSOverTLS configures the client to use DNS over TLS.
func (b *Builder) DNSOverTLS() *DNSOverTLS { return &DNSOverTLS{builder: b} }

// Timeout sets the timeout duration for the client.
func (b *Builder) Timeout(timeout time.Duration) *Builder {
	return b.addCliMW(func(client *Client) error { return timeoutMW(client, timeout) }, 0)
}

// InterfaceAddr sets the network interface address for the client.
func (b *Builder) InterfaceAddr(address g.String) *Builder {
	return b.addCliMW(func(client *Client) error { return interfaceAddrMW(client, address) }, 0)
}

// Proxy sets the proxy settings for the client.
// Supports both static proxy configurations and dynamic proxy provider functions.
//
// Static proxy examples:
//
//	.Proxy("socks5://127.0.0.1:9050")
//	.Proxy([]string{"socks5://proxy1", "http://proxy2"})
//
// Dynamic proxy example:
//
//	.Proxy(func() g.String {
//	  // Your proxy rotation logic here
//	  return "socks5://127.0.0.1:9050"
//	})
func (b *Builder) Proxy(proxy any) *Builder {
	b.proxy = proxy
	return b.addCliMW(func(client *Client) error { return proxyMW(client, proxy) }, 0)
}

// BasicAuth sets the basic authentication credentials for the client.
func (b *Builder) BasicAuth(authentication g.String) *Builder {
	return b.addReqMW(func(req *Request) error { return basicAuthMW(req, authentication) }, 900)
}

// BearerAuth sets the bearer token for the client.
func (b *Builder) BearerAuth(authentication g.String) *Builder {
	return b.addReqMW(func(req *Request) error { return bearerAuthMW(req, authentication) }, 901)
}

// UserAgent sets the user agent for the client.
func (b *Builder) UserAgent(userAgent any) *Builder {
	return b.addReqMW(func(req *Request) error { return userAgentMW(req, userAgent) }, 0)
}

// SetHeaders sets headers for the request, replacing existing ones with the same name.
func (b *Builder) SetHeaders(headers ...any) *Builder {
	return b.addReqMW(func(r *Request) error {
		r.SetHeaders(headers...)
		return nil
	}, 0)
}

// AddHeaders adds headers to the request, appending to any existing headers with the same name.
func (b *Builder) AddHeaders(headers ...any) *Builder {
	return b.addReqMW(func(r *Request) error {
		r.AddHeaders(headers...)
		return nil
	}, 0)
}

// AddCookies adds cookies to the request.
func (b *Builder) AddCookies(cookies ...*http.Cookie) *Builder {
	return b.addReqMW(func(r *Request) error {
		r.AddCookies(cookies...)
		return nil
	}, 0)
}

// WithContext associates the provided context with the request.
func (b *Builder) WithContext(ctx context.Context) *Builder {
	return b.addReqMW(func(r *Request) error {
		r.WithContext(ctx)
		return nil
	}, 0)
}

// ContentType sets the content type for the client.
func (b *Builder) ContentType(contentType g.String) *Builder {
	return b.addReqMW(func(req *Request) error { return contentTypeMW(req, contentType) }, 0)
}

// CacheBody configures whether the client should cache the body of the response.
func (b *Builder) CacheBody() *Builder {
	b.cacheBody = true
	return b
}

// GetRemoteAddress configures whether the client should get the remote address.
func (b *Builder) GetRemoteAddress() *Builder { return b.addReqMW(remoteAddrMW, 0) }

// DisableKeepAlive disable keep-alive connections.
func (b *Builder) DisableKeepAlive() *Builder { return b.addCliMW(disableKeepAliveMW, 0) }

// DisableCompression disables compression for the HTTP client.
func (b *Builder) DisableCompression() *Builder { return b.addCliMW(disableCompressionMW, 0) }

// Retry configures the retry behavior of the client.
//
// Parameters:
//
//	retryMax: Maximum number of retries to be attempted.
//	retryWait: Duration to wait between retries.
//	codes: Optional list of HTTP status codes that trigger retries.
//	       If no codes are provided, default codes will be used
//	       (500, 429, 503 - Internal Server Error, Too Many Requests, Service Unavailable).
func (b *Builder) Retry(retryMax int, retryWait time.Duration, codes ...int) *Builder {
	b.retryMax = retryMax
	b.retryWait = retryWait

	if len(codes) == 0 {
		b.retryCodes = g.SliceOf(
			http.StatusInternalServerError,
			http.StatusTooManyRequests,
			http.StatusServiceUnavailable,
		)
	} else {
		b.retryCodes = g.SliceOf(codes...)
	}

	return b
}

// ForceHTTP1MW configures the client to use HTTP/1.1 forcefully.
func (b *Builder) ForceHTTP1() *Builder {
	b.forceHTTP1 = true
	return b.addCliMW(forseHTTP1MW, 0)
}

// Session configures whether the client should maintain a session.
func (b *Builder) Session() *Builder { return b.addCliMW(sessionMW, 0) }

// MaxRedirects sets the maximum number of redirects the client should follow.
func (b *Builder) MaxRedirects(maxRedirects int) *Builder {
	b.maxRedirects = maxRedirects
	return b.addCliMW(redirectPolicyMW, 0)
}

// NotFollowRedirects disables following redirects for the client.
func (b *Builder) NotFollowRedirects() *Builder {
	return b.RedirectPolicy(func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse })
}

// FollowOnlyHostRedirects configures whether the client should only follow redirects within the
// same host.
func (b *Builder) FollowOnlyHostRedirects() *Builder {
	b.followOnlyHostRedirects = true
	return b.addCliMW(redirectPolicyMW, 0)
}

// ForwardHeadersOnRedirect adds a middleware to the ClientBuilder object that ensures HTTP headers are
// forwarded during a redirect.
func (b *Builder) ForwardHeadersOnRedirect() *Builder {
	b.forwardHeadersOnRedirect = true
	return b.addCliMW(redirectPolicyMW, 0)
}

// RedirectPolicy sets a custom redirect policy for the client.
func (b *Builder) RedirectPolicy(fn func(*http.Request, []*http.Request) error) *Builder {
	b.checkRedirect = fn
	return b.addCliMW(redirectPolicyMW, 0)
}

// String generate a string representation of the ClientBuilder instance.
func (b Builder) String() string { return fmt.Sprintf("%#v", b) }
