package client

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/tests/contract/internal/liveapi"
)

const ResourceToken = "/services/oauth2/token"

type Client struct {
	cfg        liveapi.Config
	token      string
	httpClient *http.Client
}

func NewAnonymousClient(t testing.TB) *Client {
	t.Helper()

	return &Client{
		cfg:        liveapi.LoadConfig(t),
		httpClient: &http.Client{Timeout: 30 * time.Second},
	}
}

func NewAuthenticatedClient(t testing.TB) *Client {
	t.Helper()

	c := NewAnonymousClient(t)
	token, err := c.FetchToken(c.cfg.TokenMode)
	if err != nil {
		t.Fatalf("fetch access token: %v", err)
	}
	c.token = token
	return c
}

func (c *Client) Config() liveapi.Config {
	return c.cfg
}

func (c *Client) FetchToken(mode string) (string, error) {
	resp, err := c.IssueClientCredentialsToken(mode, TokenRequest{
		GrantType:    "client_credentials",
		ClientID:     c.cfg.ClientID,
		ClientSecret: c.cfg.ClientSecret,
	})
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("token status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}

	var tokenResp OAuthTokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return "", err
	}
	if strings.TrimSpace(tokenResp.AccessToken) == "" {
		return "", fmt.Errorf("empty access_token")
	}
	return tokenResp.AccessToken, nil
}

func (c *Client) IssueClientCredentialsToken(mode string, req TokenRequest) (*http.Response, error) {
	switch strings.ToLower(strings.TrimSpace(mode)) {
	case "json":
		return c.DoAbsolute(http.MethodPost, c.cfg.TokenURL, "application/json", mustJSON(req), nil, false)
	default:
		form := url.Values{}
		form.Set("grant_type", req.GrantType)
		form.Set("client_id", req.ClientID)
		form.Set("client_secret", req.ClientSecret)
		return c.DoAbsolute(http.MethodPost, c.cfg.TokenURL, "application/x-www-form-urlencoded", []byte(form.Encode()), nil, false)
	}
}

func (c *Client) Do(method, path, contentType string, body []byte) (*http.Response, error) {
	return c.DoAbsolute(method, c.resolveURL(path), contentType, body, nil, true)
}

func (c *Client) DoJSON(method, path string, body any) (*http.Response, error) {
	if body == nil {
		return c.Do(method, path, "application/json", nil)
	}
	return c.Do(method, path, "application/json", mustJSON(body))
}

func (c *Client) DoCSV(method, path, body string) (*http.Response, error) {
	return c.Do(method, path, "text/csv", []byte(body))
}

func (c *Client) DoWithoutAuth(method, path, contentType string, body []byte) (*http.Response, error) {
	return c.DoAbsolute(method, c.resolveURL(path), contentType, body, nil, false)
}

func (c *Client) DoAbsolute(method, rawURL, contentType string, body []byte, headers map[string]string, includeAuth bool) (*http.Response, error) {
	var bodyReader io.Reader
	if len(body) > 0 {
		bodyReader = bytes.NewReader(body)
	}

	req, err := http.NewRequest(method, rawURL, bodyReader)
	if err != nil {
		return nil, err
	}
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}
	req.Header.Set("Accept", "application/json")
	// Set Origin so the backend's CSRF middleware accepts state-changing requests.
	req.Header.Set("Origin", c.cfg.RootBaseURL)
	if includeAuth && strings.TrimSpace(c.token) != "" {
		req.Header.Set("Authorization", "Bearer "+c.token)
	}
	for key, value := range headers {
		req.Header.Set(key, value)
	}

	return c.httpClient.Do(req)
}

func (c *Client) resolveURL(path string) string {
	switch {
	case strings.HasPrefix(path, "http://"), strings.HasPrefix(path, "https://"):
		return path
	case strings.HasPrefix(path, "/services/"):
		return strings.TrimRight(c.cfg.RootBaseURL, "/") + path
	default:
		return strings.TrimRight(c.cfg.APIBaseURL, "/") + path
	}
}

func mustJSON(v any) []byte {
	body, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}
	return body
}
