package httpclient

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/url"
	"strings"
)

// Error types for HTTP client operations.
var (
	// ErrTimeout indicates that the request timed out.
	ErrTimeout = errors.New("request timed out")

	// ErrConnectionRefused indicates that the connection was refused.
	ErrConnectionRefused = errors.New("connection refused")

	// ErrDNSLookupFailed indicates that DNS lookup failed.
	ErrDNSLookupFailed = errors.New("DNS lookup failed")

	// ErrConnectionReset indicates that the connection was reset.
	ErrConnectionReset = errors.New("connection reset")

	// ErrCanceled indicates that the request was canceled.
	ErrCanceled = errors.New("request canceled")
)

// HTTPError represents an HTTP error with status code and response body.
type HTTPError struct {
	StatusCode int
	Status     string
	Body       []byte
	URL        string
}

// Error returns the error message.
func (e *HTTPError) Error() string {
	if len(e.Body) > 0 {
		// Truncate body if too long
		body := string(e.Body)
		if len(body) > 200 {
			body = body[:200] + "..."
		}
		return fmt.Sprintf("HTTP %d %s: %s (url: %s)", e.StatusCode, e.Status, body, e.URL)
	}
	return fmt.Sprintf("HTTP %d %s (url: %s)", e.StatusCode, e.Status, e.URL)
}

// NewHTTPError creates a new HTTPError from a Response.
func NewHTTPError(resp *Response, url string) *HTTPError {
	body, _ := resp.ReadBody()
	return &HTTPError{
		StatusCode: resp.StatusCode,
		Status:     resp.Status,
		Body:       body,
		URL:        url,
	}
}

// IsHTTPError checks if the error is an HTTPError.
func IsHTTPError(err error) bool {
	var httpErr *HTTPError
	return errors.As(err, &httpErr)
}

// GetHTTPError extracts the HTTPError from an error if present.
func GetHTTPError(err error) (*HTTPError, bool) {
	var httpErr *HTTPError
	if errors.As(err, &httpErr) {
		return httpErr, true
	}
	return nil, false
}

// wrapError wraps a low-level error into a more descriptive error type.
func wrapError(err error) error {
	if err == nil {
		return nil
	}

	// Check for context cancellation
	if errors.Is(err, context.Canceled) {
		return fmt.Errorf("%w: %v", ErrCanceled, err)
	}

	// Check for context deadline exceeded (timeout)
	if errors.Is(err, context.DeadlineExceeded) {
		return fmt.Errorf("%w: %v", ErrTimeout, err)
	}

	// Check for URL errors
	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		if urlErr.Timeout() {
			return fmt.Errorf("%w: %v", ErrTimeout, err)
		}
		// Unwrap and check the underlying error
		err = urlErr.Err
	}

	// Check for network errors
	var netErr net.Error
	if errors.As(err, &netErr) {
		if netErr.Timeout() {
			return fmt.Errorf("%w: %v", ErrTimeout, err)
		}
	}

	// Check for DNS errors
	var dnsErr *net.DNSError
	if errors.As(err, &dnsErr) {
		return fmt.Errorf("%w: %v", ErrDNSLookupFailed, err)
	}

	// Check for operation errors (connection refused, etc.)
	var opErr *net.OpError
	if errors.As(err, &opErr) {
		if opErr.Op == "dial" {
			// Check for connection refused
			if strings.Contains(opErr.Error(), "connection refused") {
				return fmt.Errorf("%w: %v", ErrConnectionRefused, err)
			}
		}
		// Check for connection reset
		if strings.Contains(opErr.Error(), "connection reset") {
			return fmt.Errorf("%w: %v", ErrConnectionReset, err)
		}
	}

	// Return the original error if no specific type matched
	return err
}

// IsTimeout checks if the error is a timeout error.
func IsTimeout(err error) bool {
	if err == nil {
		return false
	}

	// Check for our wrapped timeout error
	if errors.Is(err, ErrTimeout) {
		return true
	}

	// Check for context deadline exceeded
	if errors.Is(err, context.DeadlineExceeded) {
		return true
	}

	// Check for net.Error timeout
	var netErr net.Error
	if errors.As(err, &netErr) {
		return netErr.Timeout()
	}

	// Check for url.Error timeout
	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		return urlErr.Timeout()
	}

	return false
}

// IsCanceled checks if the error is a cancellation error.
func IsCanceled(err error) bool {
	if err == nil {
		return false
	}

	return errors.Is(err, ErrCanceled) || errors.Is(err, context.Canceled)
}

// IsConnectionError checks if the error is a connection-related error.
func IsConnectionError(err error) bool {
	if err == nil {
		return false
	}

	return errors.Is(err, ErrConnectionRefused) ||
		errors.Is(err, ErrConnectionReset) ||
		errors.Is(err, ErrDNSLookupFailed)
}

// IsRetryable checks if the error is retryable.
// Timeout, connection refused, connection reset, and DNS errors are considered retryable.
func IsRetryable(err error) bool {
	if err == nil {
		return false
	}

	// Timeout errors are retryable
	if IsTimeout(err) {
		return true
	}

	// Connection errors are retryable
	if IsConnectionError(err) {
		return true
	}

	// Check for HTTP errors - 5xx errors are retryable, 429 (rate limit) is retryable
	var httpErr *HTTPError
	if errors.As(err, &httpErr) {
		return httpErr.StatusCode >= 500 || httpErr.StatusCode == 429
	}

	return false
}
