package apiclient

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/go-openapi/strfmt"
	"github.com/golang-jwt/jwt/v4"

	"github.com/crowdsecurity/crowdsec/pkg/apiclient/useragent"
	"github.com/crowdsecurity/crowdsec/pkg/models"
)

var (
	InsecureSkipVerify = false
	Cert               *tls.Certificate
	CaCertPool         *x509.CertPool
	lapiClient         *ApiClient
)

type TokenSave func(ctx context.Context, tokenKey string, token string) error

type ApiClient struct {
	// The http client used to make requests
	client *http.Client
	// Reuse a single struct instead of allocating one for each service on the heap.
	common service
	// config stuff
	BaseURL   *url.URL
	PapiURL   *url.URL
	URLPrefix string
	UserAgent string
	// exposed Services
	Decisions      *DecisionsService
	DecisionDelete *DecisionDeleteService
	Alerts         *AlertsService
	Allowlists     *AllowlistsService
	Auth           *AuthService
	Metrics        *MetricsService
	Signal         *SignalService
	HeartBeat      *HeartBeatService
	UsageMetrics   *UsageMetricsService
}

func (c *ApiClient) GetClient() *http.Client {
	return c.client
}

func (c *ApiClient) IsEnrolled() bool {
	jwtTransport := c.client.Transport.(*JWTTransport)
	tokenStr := jwtTransport.Token

	token, _ := jwt.Parse(tokenStr, nil)
	if token == nil {
		return false
	}

	claims := token.Claims.(jwt.MapClaims)
	_, ok := claims["organization_id"]

	return ok
}

func (c *ApiClient) GetSubscriptionType() string {
	jwtTransport := c.client.Transport.(*JWTTransport)
	tokenStr := jwtTransport.Token

	token, _ := jwt.Parse(tokenStr, nil)
	if token == nil {
		return ""
	}

	claims := token.Claims.(jwt.MapClaims)
	subscriptionType, ok := claims["subscription_type"].(string)
	if ok {
		return subscriptionType
	}

	return ""
}

func (c *ApiClient) GetTokenRefreshChan() chan struct{} {
	jwtTransport := c.client.Transport.(*JWTTransport)
	return jwtTransport.TokenRefreshChan
}

type service struct {
	client *ApiClient
}

func InitLAPIClient(ctx context.Context, apiUrl string, papiUrl string, login string, password string, scenarios []string) error {
	apiURL, err := url.Parse(apiUrl)
	if err != nil {
		return fmt.Errorf("parsing api url ('%s'): %w", apiURL, err)
	}

	papiURL, err := url.Parse(papiUrl)
	if err != nil {
		return fmt.Errorf("parsing polling api url ('%s'): %w", papiURL, err)
	}

	pwd := strfmt.Password(password)

	client := NewClient(&Config{
		MachineID:     login,
		Password:      pwd,
		URL:           apiURL,
		PapiURL:       papiURL,
		VersionPrefix: "v1",
		UpdateScenario: func(_ context.Context) ([]string, error) {
			return scenarios, nil
		},
	})

	authResp, _, err := client.Auth.AuthenticateWatcher(ctx, models.WatcherAuthRequest{
		MachineID: &login,
		Password:  &pwd,
		Scenarios: scenarios,
	})
	if err != nil {
		return fmt.Errorf("authenticate watcher (%s): %w", login, err)
	}

	var expiration time.Time
	if err := expiration.UnmarshalText([]byte(authResp.Expire)); err != nil {
		return fmt.Errorf("unable to parse jwt expiration: %w", err)
	}

	client.GetClient().Transport.(*JWTTransport).Token = authResp.Token
	client.GetClient().Transport.(*JWTTransport).Expiration = expiration

	lapiClient = client

	return nil
}

func GetLAPIClient() (*ApiClient, error) {
	if lapiClient == nil {
		return nil, errors.New("client not initialized")
	}

	return lapiClient, nil
}

func NewClient(config *Config) *ApiClient {
	userAgent := config.UserAgent
	if userAgent == "" {
		userAgent = useragent.Default()
	}

	t := &JWTTransport{
		MachineID:      &config.MachineID,
		Password:       &config.Password,
		UserAgent:      userAgent,
		VersionPrefix:  config.VersionPrefix,
		UpdateScenario: config.UpdateScenario,
		RetryConfig: NewRetryConfig(
			WithStatusCodeConfig(http.StatusUnauthorized, 2, false, true),
			WithStatusCodeConfig(http.StatusForbidden, 2, false, true),
			WithStatusCodeConfig(http.StatusTooManyRequests, 5, true, false),
			WithStatusCodeConfig(http.StatusServiceUnavailable, 5, true, false),
			WithStatusCodeConfig(http.StatusGatewayTimeout, 5, true, false),
		),
		TokenSave:        config.TokenSave,
		TokenRefreshChan: make(chan struct{}),
	}

	transport, baseURL := createTransport(config.URL)
	if transport != nil {
		t.Transport = transport
	} else {
		// can be httpmock.MockTransport
		if ht, ok := http.DefaultTransport.(*http.Transport); ok {
			t.Transport = ht.Clone()
		}
	}

	t.URL = baseURL

	tlsconfig := tls.Config{InsecureSkipVerify: InsecureSkipVerify}
	tlsconfig.RootCAs = CaCertPool

	if Cert != nil {
		tlsconfig.Certificates = []tls.Certificate{*Cert}
	}

	if t.Transport != nil {
		t.Transport.(*http.Transport).TLSClientConfig = &tlsconfig
	}

	c := &ApiClient{client: t.Client(), BaseURL: baseURL, UserAgent: userAgent, URLPrefix: config.VersionPrefix, PapiURL: config.PapiURL}
	c.common.client = c
	c.Decisions = (*DecisionsService)(&c.common)
	c.Alerts = (*AlertsService)(&c.common)
	c.Allowlists = (*AllowlistsService)(&c.common)
	c.Auth = (*AuthService)(&c.common)
	c.Metrics = (*MetricsService)(&c.common)
	c.Signal = (*SignalService)(&c.common)
	c.DecisionDelete = (*DecisionDeleteService)(&c.common)
	c.HeartBeat = (*HeartBeatService)(&c.common)
	c.UsageMetrics = (*UsageMetricsService)(&c.common)

	return c
}

func NewDefaultClient(url *url.URL, prefix string, userAgent string, client *http.Client) (*ApiClient, error) {
	transport, baseURL := createTransport(url)

	if client == nil {
		client = &http.Client{}

		if transport != nil {
			client.Transport = transport
		} else {
			if ht, ok := http.DefaultTransport.(*http.Transport); ok {
				ht = ht.Clone()
				tlsconfig := tls.Config{InsecureSkipVerify: InsecureSkipVerify}
				tlsconfig.RootCAs = CaCertPool

				if Cert != nil {
					tlsconfig.Certificates = []tls.Certificate{*Cert}
				}

				ht.TLSClientConfig = &tlsconfig
				client.Transport = ht
			}
		}
	}

	if userAgent == "" {
		userAgent = useragent.Default()
	}

	c := &ApiClient{client: client, BaseURL: baseURL, UserAgent: userAgent, URLPrefix: prefix}
	c.common.client = c
	c.Decisions = (*DecisionsService)(&c.common)
	c.Alerts = (*AlertsService)(&c.common)
	c.Allowlists = (*AllowlistsService)(&c.common)
	c.Auth = (*AuthService)(&c.common)
	c.Metrics = (*MetricsService)(&c.common)
	c.Signal = (*SignalService)(&c.common)
	c.DecisionDelete = (*DecisionDeleteService)(&c.common)
	c.HeartBeat = (*HeartBeatService)(&c.common)
	c.UsageMetrics = (*UsageMetricsService)(&c.common)

	return c, nil
}

func RegisterClient(ctx context.Context, config *Config, client *http.Client) (*ApiClient, error) {
	transport, baseURL := createTransport(config.URL)

	if client == nil {
		client = &http.Client{}
		if transport != nil {
			client.Transport = transport
		} else {
			tlsconfig := tls.Config{InsecureSkipVerify: InsecureSkipVerify}
			if Cert != nil {
				tlsconfig.RootCAs = CaCertPool
				tlsconfig.Certificates = []tls.Certificate{*Cert}
			}

			client.Transport = http.DefaultTransport.(*http.Transport).Clone()
			client.Transport.(*http.Transport).TLSClientConfig = &tlsconfig
		}
	} else if client.Transport == nil && transport != nil {
		client.Transport = transport
	}

	userAgent := config.UserAgent
	if userAgent == "" {
		userAgent = useragent.Default()
	}

	c := &ApiClient{client: client, BaseURL: baseURL, UserAgent: userAgent, URLPrefix: config.VersionPrefix}
	c.common.client = c
	c.Decisions = (*DecisionsService)(&c.common)
	c.Alerts = (*AlertsService)(&c.common)
	c.Auth = (*AuthService)(&c.common)

	resp, err := c.Auth.RegisterWatcher(ctx, models.WatcherRegistrationRequest{MachineID: &config.MachineID, Password: &config.Password, RegistrationToken: config.RegistrationToken})
	if err != nil {
		/*if we have http status, return it*/
		if resp != nil && resp.Response != nil {
			return nil, fmt.Errorf("api register (%s) http %s: %w", c.BaseURL, resp.Response.Status, err)
		}

		return nil, fmt.Errorf("api register (%s): %w", c.BaseURL, err)
	}

	return c, nil
}

func createTransport(url *url.URL) (*http.Transport, *url.URL) {
	urlString := url.String()

	// TCP transport
	if !strings.HasPrefix(urlString, "/") {
		return nil, url
	}

	// Unix transport
	url.Path = "/"
	url.Host = "unix"
	url.Scheme = "http"

	dialer := &net.Dialer{}
	socketPath := strings.TrimSuffix(urlString, "/")

	return &http.Transport{
		DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
			return dialer.DialContext(ctx, "unix", socketPath)
		},
	}, url
}

type Response struct {
	Response *http.Response
	// add our pagination stuff
	// NextPage int
	// ...
}

func newResponse(r *http.Response) *Response {
	return &Response{Response: r}
}

type ListOpts struct {
	// Page    int
	// PerPage int
}

type DeleteOpts struct {
	// ??
}

type AddOpts struct {
	// ??
}
