// Copyright (C) 2023 Marius Schellenberger

package client

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	neturl "net/url"
	"os"
	"strings"
	"time"

	"git.giftfish.de/ston1th/keyctl/pkg/api/types"
)

type Client struct {
	endpoint    string
	unix        string
	httpClient  *http.Client
	dialer      *net.Dialer
	dialerFunc  func(ctx context.Context, network, addr string) (net.Conn, error)
	debugWriter io.Writer
	//insecure    bool
}

var (
	ErrNotFound = errors.New("not found")
)

type ClientOption func(*Client)

func WithEndpoint(endpoint string) ClientOption {
	return func(client *Client) {
		u, err := url.Parse(endpoint)
		if err != nil {
			return
		}
		client.dialer = &net.Dialer{
			Timeout:   30 * time.Second,
			KeepAlive: 30 * time.Second,
		}
		if u.Scheme == "unix" {
			client.dialerFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
				return client.dialer.DialContext(ctx, "unix", u.Path)
			}
			client.endpoint = "http://unix"
			client.unix = u.Path
			return
		}
		client.dialerFunc = client.dialer.DialContext
		client.endpoint = strings.TrimRight(endpoint, "/")
	}
}

func WithDebugWriter(debugWriter io.Writer) ClientOption {
	return func(client *Client) {
		client.debugWriter = debugWriter
	}
}

func WithHTTPClient(httpClient *http.Client) ClientOption {
	return func(client *Client) {
		client.httpClient = httpClient
	}
}

//func WithInsecureClient(insecure bool) ClientOption {
//	return func(client *Client) {
//		client.insecure = insecure
//	}
//}

var (
	ErrMissingEndpointEnv   = errors.New("missing environment variable KEYCTL_ENDPOINT")
	ErrMissingEndpointInURL = errors.New("missing host in URL")
	ErrMissingIDInURL       = errors.New("missing ID in URL")
	ErrUnsupportedScheme    = errors.New("unsupported scheme")
	ErrMissingEndpoint      = errors.New("missing Endpoint in config")
)

type Config struct {
	Endpoint string `json:"endpoint"`
	//Insecure bool   `json:"insecure"`
}

func ClientOptionsFromConfig(cfg Config) (options []ClientOption, err error) {
	if cfg.Endpoint == "" {
		err = ErrMissingEndpoint
		return
	}
	options = []ClientOption{
		WithEndpoint(cfg.Endpoint),
		//WithInsecureClient(cfg.Insecure),
	}
	return
}

func ClientOptionsFromEnv() (options []ClientOption, err error) {
	endpoint := os.Getenv("KEYCTL_ENDPOINT")
	if endpoint == "" {
		err = ErrMissingEndpointEnv
		return
	}
	//insecure, _ := strconv.ParseBool(os.Getenv("HAPROXY_LB_INSECURE"))

	options = []ClientOption{
		WithEndpoint(endpoint),
		//WithInsecureClient(insecure),
	}
	return
}

func ClientOptionsFromURL(url string) (options []ClientOption, err error) {
	u, err := neturl.Parse(url)
	if err != nil {
		return nil, err
	}
	if u.Scheme != "keyctl" {
		return nil, fmt.Errorf("%w: %s", ErrUnsupportedScheme, u.Scheme)
	}
	if u.Host == "" {
		err = ErrMissingEndpointInURL
		return
	}
	//insecure, _ := strconv.ParseBool(os.Getenv("HAPROXY_LB_INSECURE"))
	var endpoint string
	if u.Query().Get("tls") == "true" {
		endpoint = "https://" + u.Host
	} else {
		endpoint = "http://" + u.Host
	}

	options = []ClientOption{
		WithEndpoint(endpoint),
		//WithInsecureClient(insecure),
	}
	return
}

func KeyFromURL(url string) (id, share string, err error) {
	u, err := neturl.Parse(url)
	if err != nil {
		return
	}
	if u.Scheme != "keyctl" {
		err = fmt.Errorf("%w: %s", ErrUnsupportedScheme, u.Scheme)
		return
	}
	q := u.Query()
	id = q.Get("id")
	if id == "" {
		err = ErrMissingIDInURL
		return
	}
	share = q.Get("share")
	return
}

func NewClient(options ...ClientOption) *Client {
	client := &Client{}

	for _, option := range options {
		option(client)
	}

	if client.httpClient == nil {
		client.httpClient = &http.Client{
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			},
			Transport: &http.Transport{
				DialContext: client.dialerFunc,
				//ForceAttemptHTTP2:     true,
				MaxIdleConns:    100,
				IdleConnTimeout: 90 * time.Second,
				//TLSHandshakeTimeout:   10 * time.Second,
				ExpectContinueTimeout: 1 * time.Second,
				//TLSClientConfig: &tls.Config{
				//	InsecureSkipVerify: client.insecure,
				//},
			},
		}
	}
	return client
}

func (c *Client) IsUnix() (socket string, ok bool) {
	return c.unix, c.unix != ""
}

func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
	url := c.endpoint + path
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}
	req = req.WithContext(ctx)
	return req, nil
}

func (c *Client) Do(r *http.Request, v any) (resp *http.Response, err error) {
	if c.debugWriter != nil {
		dumpReq, err := httputil.DumpRequestOut(r, true)
		if err != nil {
			return &http.Response{}, err
		}
		fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq)
	}
	resp, err = c.httpClient.Do(r)
	if err != nil {
		return
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		resp.Body.Close()
		return
	}
	resp.Body.Close()
	resp.Body = io.NopCloser(bytes.NewReader(body))

	if c.debugWriter != nil {
		dumpResp, err := httputil.DumpResponse(resp, true)
		if err != nil {
			return resp, err
		}
		fmt.Fprintf(c.debugWriter, "--- Response:\n%s\n\n", dumpResp)
	}

	err = decodeResp(resp, v)
	return
}

func (c *Client) FollowRedirect(r *http.Request, v any, delay time.Duration) (resp *http.Response, err error) {
	ctx := r.Context()
	rr := r.Clone(ctx)
	for {
		resp, err = c.Do(rr, v)
		if err == nil && resp.StatusCode == http.StatusOK {
			return
		}
		rdr, ok := err.(Redirect)
		if !ok {
			return
		}
		rr, err = c.NewRequest(ctx, "GET", rdr.Location, nil)
		if err != nil {
			return
		}
		time.Sleep(delay)
	}
}

func (c *Client) FollowRedirectLoop(r *http.Request, v any, delay time.Duration) (resp *http.Response, err error) {
	ctx := r.Context()
	rr := r.Clone(ctx)
	for {
		resp, err = c.Do(rr, v)
		rdr, ok := err.(Redirect)
		if err != nil && !ok {
			if errors.Is(err, types.ErrISE) ||
				errors.Is(err, types.ErrKeyNotFound) {
				return
			}
			if errors.Is(err, types.ErrReqNotFound) ||
				errors.Is(err, types.ErrKeyRequestRejected) {
				rr = r.Clone(ctx)
			}
			time.Sleep(delay)
			continue
		}
		if err == nil && resp.StatusCode == http.StatusOK {
			return
		}
		rr, err = c.NewRequest(ctx, "GET", rdr.Location, nil)
		if err != nil {
			return
		}
		time.Sleep(delay)
	}
}
