// Package httpclient provides a generic HTTP client wrapper with connection pooling,
// configurable timeouts, and proper resource management for AutoSSO.
// It ensures response bodies are properly closed and supports common HTTP methods
// with JSON request/response handling.
package httpclient

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"time"
)

// Config holds the configuration for creating a new HTTP client.
type Config struct {
	// Timeout is the total timeout for a request including connection, TLS handshake,
	// request sending, and response reading. Default is 30 seconds.
	Timeout time.Duration

	// MaxIdleConns controls the maximum number of idle (keep-alive) connections
	// across all hosts. Default is 100.
	MaxIdleConns int

	// MaxIdleConnsPerHost controls the maximum idle (keep-alive) connections
	// to keep per-host. Default is 10.
	MaxIdleConnsPerHost int

	// MaxConnsPerHost limits the total number of connections per host.
	// Zero means no limit. Default is 0.
	MaxConnsPerHost int

	// IdleConnTimeout is the maximum amount of time an idle connection will
	// remain idle before closing itself. Default is 90 seconds.
	IdleConnTimeout time.Duration

	// DialTimeout is the maximum amount of time a dial will wait for a connect
	// to complete. Default is 10 seconds.
	DialTimeout time.Duration

	// TLSHandshakeTimeout specifies the maximum amount of time waiting to wait
	// for a TLS handshake. Default is 10 seconds.
	TLSHandshakeTimeout time.Duration

	// ResponseHeaderTimeout specifies the amount of time to wait for a server's
	// response headers after fully writing the request. Default is 10 seconds.
	ResponseHeaderTimeout time.Duration

	// ExpectContinueTimeout specifies the amount of time to wait for a server's
	// first response headers after fully writing the request headers if the
	// request has an "Expect: 100-continue" header. Default is 1 second.
	ExpectContinueTimeout time.Duration
}

// DefaultConfig returns a Config with sensible defaults optimized for
// connection reuse and reasonable timeouts.
func DefaultConfig() Config {
	return Config{
		Timeout:               30 * time.Second,
		MaxIdleConns:          100,
		MaxIdleConnsPerHost:   10,
		MaxConnsPerHost:       0, // No limit
		IdleConnTimeout:       90 * time.Second,
		DialTimeout:           10 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ResponseHeaderTimeout: 10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
	}
}

// Client is a wrapper around http.Client that provides connection pooling,
// configurable timeouts, and proper resource management.
type Client struct {
	httpClient *http.Client
	config     Config
}

// New creates a new HTTP client with the given configuration.
// If cfg is nil, DefaultConfig() is used.
func New(cfg *Config) *Client {
	if cfg == nil {
		defaultCfg := DefaultConfig()
		cfg = &defaultCfg
	}

	// Create a custom transport with connection pooling
	transport := &http.Transport{
		// Connection pooling settings
		MaxIdleConns:        cfg.MaxIdleConns,
		MaxIdleConnsPerHost: cfg.MaxIdleConnsPerHost,
		MaxConnsPerHost:     cfg.MaxConnsPerHost,
		IdleConnTimeout:     cfg.IdleConnTimeout,

		// Timeout settings
		TLSHandshakeTimeout:   cfg.TLSHandshakeTimeout,
		ResponseHeaderTimeout: cfg.ResponseHeaderTimeout,
		ExpectContinueTimeout: cfg.ExpectContinueTimeout,

		// Custom dialer with timeout
		DialContext: (&net.Dialer{
			Timeout:   cfg.DialTimeout,
			KeepAlive: 30 * time.Second, // Enable TCP keep-alive
		}).DialContext,

		// Enable HTTP/2 support
		ForceAttemptHTTP2: true,
	}

	return &Client{
		httpClient: &http.Client{
			Transport: transport,
			Timeout:   cfg.Timeout,
		},
		config: *cfg,
	}
}

// Response wraps an HTTP response with helper methods for reading the body.
// The body is automatically closed when Read methods are called.
type Response struct {
	StatusCode int
	Status     string
	Headers    http.Header
	body       io.ReadCloser
	bodyRead   bool
}

// newResponse creates a new Response from an http.Response.
func newResponse(resp *http.Response) *Response {
	return &Response{
		StatusCode: resp.StatusCode,
		Status:     resp.Status,
		Headers:    resp.Header,
		body:       resp.Body,
		bodyRead:   false,
	}
}

// ReadBody reads and returns the response body as bytes.
// The body is automatically closed after reading.
// This method can only be called once.
func (r *Response) ReadBody() ([]byte, error) {
	if r.bodyRead {
		return nil, fmt.Errorf("response body already read")
	}
	r.bodyRead = true
	defer r.body.Close()

	return io.ReadAll(r.body)
}

// ReadJSON reads the response body and unmarshals it into the provided value.
// The body is automatically closed after reading.
// This method can only be called once.
func (r *Response) ReadJSON(v interface{}) error {
	if r.bodyRead {
		return fmt.Errorf("response body already read")
	}
	r.bodyRead = true
	defer r.body.Close()

	body, err := io.ReadAll(r.body)
	if err != nil {
		return fmt.Errorf("failed to read response body: %w", err)
	}

	if err := json.Unmarshal(body, v); err != nil {
		return fmt.Errorf("failed to unmarshal JSON response: %w", err)
	}

	return nil
}

// Close closes the response body if it hasn't been read yet.
// It's safe to call Close multiple times.
func (r *Response) Close() error {
	if r.bodyRead {
		return nil
	}
	r.bodyRead = true

	// Drain the body before closing to enable connection reuse
	_, _ = io.Copy(io.Discard, r.body)
	return r.body.Close()
}

// IsSuccess returns true if the status code is in the 2xx range.
func (r *Response) IsSuccess() bool {
	return r.StatusCode >= 200 && r.StatusCode < 300
}

// Do sends an HTTP request and returns the response.
// The caller is responsible for calling Response.Close() or one of the Read methods.
func (c *Client) Do(ctx context.Context, req *http.Request) (*Response, error) {
	// Ensure the request has the context
	req = req.WithContext(ctx)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, wrapError(err)
	}

	return newResponse(resp), nil
}

// Get performs an HTTP GET request.
func (c *Client) Get(ctx context.Context, url string, headers map[string]string) (*Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create GET request: %w", err)
	}

	for k, v := range headers {
		req.Header.Set(k, v)
	}

	return c.Do(ctx, req)
}

// Post performs an HTTP POST request with the given body.
func (c *Client) Post(ctx context.Context, url string, body io.Reader, headers map[string]string) (*Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
	if err != nil {
		return nil, fmt.Errorf("failed to create POST request: %w", err)
	}

	for k, v := range headers {
		req.Header.Set(k, v)
	}

	return c.Do(ctx, req)
}

// PostJSON performs an HTTP POST request with a JSON body.
func (c *Client) PostJSON(ctx context.Context, url string, body interface{}, headers map[string]string) (*Response, error) {
	jsonBody, err := json.Marshal(body)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal JSON body: %w", err)
	}

	if headers == nil {
		headers = make(map[string]string)
	}
	headers["Content-Type"] = "application/json"

	return c.Post(ctx, url, bytes.NewReader(jsonBody), headers)
}

// Put performs an HTTP PUT request with the given body.
func (c *Client) Put(ctx context.Context, url string, body io.Reader, headers map[string]string) (*Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body)
	if err != nil {
		return nil, fmt.Errorf("failed to create PUT request: %w", err)
	}

	for k, v := range headers {
		req.Header.Set(k, v)
	}

	return c.Do(ctx, req)
}

// PutJSON performs an HTTP PUT request with a JSON body.
func (c *Client) PutJSON(ctx context.Context, url string, body interface{}, headers map[string]string) (*Response, error) {
	jsonBody, err := json.Marshal(body)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal JSON body: %w", err)
	}

	if headers == nil {
		headers = make(map[string]string)
	}
	headers["Content-Type"] = "application/json"

	return c.Put(ctx, url, bytes.NewReader(jsonBody), headers)
}

// Delete performs an HTTP DELETE request.
func (c *Client) Delete(ctx context.Context, url string, headers map[string]string) (*Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create DELETE request: %w", err)
	}

	for k, v := range headers {
		req.Header.Set(k, v)
	}

	return c.Do(ctx, req)
}

// DeleteWithBody performs an HTTP DELETE request with a body.
func (c *Client) DeleteWithBody(ctx context.Context, url string, body io.Reader, headers map[string]string) (*Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, body)
	if err != nil {
		return nil, fmt.Errorf("failed to create DELETE request: %w", err)
	}

	for k, v := range headers {
		req.Header.Set(k, v)
	}

	return c.Do(ctx, req)
}

// Patch performs an HTTP PATCH request with the given body.
func (c *Client) Patch(ctx context.Context, url string, body io.Reader, headers map[string]string) (*Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPatch, url, body)
	if err != nil {
		return nil, fmt.Errorf("failed to create PATCH request: %w", err)
	}

	for k, v := range headers {
		req.Header.Set(k, v)
	}

	return c.Do(ctx, req)
}

// PatchJSON performs an HTTP PATCH request with a JSON body.
func (c *Client) PatchJSON(ctx context.Context, url string, body interface{}, headers map[string]string) (*Response, error) {
	jsonBody, err := json.Marshal(body)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal JSON body: %w", err)
	}

	if headers == nil {
		headers = make(map[string]string)
	}
	headers["Content-Type"] = "application/json"

	return c.Patch(ctx, url, bytes.NewReader(jsonBody), headers)
}

// Close closes idle connections in the client's connection pool.
// After calling Close, the client should not be used.
func (c *Client) Close() {
	c.httpClient.CloseIdleConnections()
}

// GetHTTPClient returns the underlying http.Client.
// This is useful when you need to pass the client to third-party libraries.
func (c *Client) GetHTTPClient() *http.Client {
	return c.httpClient
}

// GetConfig returns a copy of the client's configuration.
func (c *Client) GetConfig() Config {
	return c.config
}
