package liveapi

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"gopkg.in/yaml.v3"
)

const (
	defaultLocalBaseURL = "http://127.0.0.1:3001"
	defaultAPIVersion   = "v63.0"
)

type Config struct {
	TargetName   string
	RootBaseURL  string
	APIBaseURL   string
	APIVersion   string
	TokenURL     string
	ClientID     string
	ClientSecret string
	TokenMode    string
}

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

type sandboxYAML struct {
	Salesforce struct {
		TokenURL     string `yaml:"token_url"`
		APIBaseURL   string `yaml:"api_base_url"`
		APIVersion   string `yaml:"api_version"`
		ClientID     string `yaml:"client_id"`
		ClientSecret string `yaml:"client_secret"`
	} `yaml:"salesforce"`
}

func LoadConfig(t testing.TB) Config {
	t.Helper()

	targetName := strings.ToLower(strings.TrimSpace(os.Getenv("CONTRACT_TARGET")))
	if targetName == "" {
		targetName = "local"
	}

	var cfg Config
	switch targetName {
	case "sandbox":
		loaded, err := loadSandboxConfig()
		if err != nil {
			t.Fatalf("load sandbox config: %v", err)
		}
		cfg = loaded
	default:
		cfg = loadLocalConfig()
	}

	baseOverride := strings.TrimRight(strings.TrimSpace(os.Getenv("CONTRACT_BASE_URL")), "/")
	apiOverride := strings.TrimRight(strings.TrimSpace(os.Getenv("CONTRACT_API_BASE_URL")), "/")
	versionOverride := strings.TrimSpace(os.Getenv("CONTRACT_API_VERSION"))
	tokenOverride := strings.TrimRight(strings.TrimSpace(os.Getenv("CONTRACT_TOKEN_URL")), "/")
	hasBaseOverride := baseOverride != ""
	hasAPIOverride := apiOverride != ""
	hasVersionOverride := versionOverride != ""
	hasTokenOverride := tokenOverride != ""

	if hasBaseOverride {
		cfg.RootBaseURL = baseOverride
	}
	if hasAPIOverride {
		cfg.APIBaseURL = apiOverride
	}
	if hasVersionOverride {
		cfg.APIVersion = versionOverride
	}
	if hasTokenOverride {
		cfg.TokenURL = tokenOverride
	}
	if override := strings.TrimSpace(os.Getenv("CONTRACT_CLIENT_ID")); override != "" {
		cfg.ClientID = override
	}
	if override := strings.TrimSpace(os.Getenv("CONTRACT_CLIENT_SECRET")); override != "" {
		cfg.ClientSecret = override
	}
	if override := strings.TrimSpace(os.Getenv("CONTRACT_TOKEN_MODE")); override != "" {
		cfg.TokenMode = strings.ToLower(override)
	}

	if cfg.APIVersion == "" {
		cfg.APIVersion = defaultAPIVersion
	}
	if hasAPIOverride && !hasBaseOverride {
		cfg.RootBaseURL = rootBaseURLFromAPIBase(cfg.APIBaseURL)
	}
	if cfg.RootBaseURL == "" {
		cfg.RootBaseURL = rootBaseURLFromAPIBase(cfg.APIBaseURL)
	}
	if !hasAPIOverride && (cfg.APIBaseURL == "" || hasBaseOverride || hasVersionOverride) {
		cfg.APIBaseURL = strings.TrimRight(cfg.RootBaseURL, "/") + "/services/data/" + cfg.APIVersion
	}
	if !hasTokenOverride && (cfg.TokenURL == "" || hasBaseOverride || hasAPIOverride) {
		cfg.TokenURL = strings.TrimRight(cfg.RootBaseURL, "/") + "/services/oauth2/token"
	}
	if cfg.TokenMode == "" {
		cfg.TokenMode = defaultTokenMode(cfg.TokenURL)
	}

	return cfg
}

// NewClientWithToken creates a Client using a pre-obtained access token.
// This is useful for testing with non-default users (e.g. password grant).
func NewClientWithToken(cfg Config, token string) *Client {
	return &Client{
		cfg:        cfg,
		token:      token,
		httpClient: &http.Client{Timeout: 30 * time.Second},
	}
}

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

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

	token, err := c.fetchToken()
	if err != nil {
		t.Fatalf("fetch access token for %s target: %v", cfg.TargetName, err)
	}
	c.token = token
	return c
}

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

func (c *Client) DoWithHeaders(method, path, contentType string, body []byte, headers map[string]string) (*http.Response, error) {
	endpoint := c.resolveURL(path)
	var bodyReader io.Reader
	if len(body) > 0 {
		bodyReader = bytes.NewReader(body)
	}

	req, err := http.NewRequest(method, endpoint, bodyReader)
	if err != nil {
		return nil, err
	}
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Authorization", "Bearer "+c.token)
	// Set Origin header so the backend's CSRF middleware accepts state-changing
	// requests from this non-browser client (it validates Origin against
	// allowed origins; RootBaseURL matches the API_BASE_URL allow-list entry).
	req.Header.Set("Origin", c.cfg.RootBaseURL)
	for key, value := range headers {
		req.Header.Set(key, value)
	}
	return c.httpClient.Do(req)
}

func (c *Client) DoJSON(method, path string, body any) (*http.Response, error) {
	return c.DoJSONWithHeaders(method, path, body, nil)
}

func (c *Client) DoJSONWithHeaders(method, path string, body any, headers map[string]string) (*http.Response, error) {
	var raw []byte
	if body != nil {
		marshaled, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		raw = marshaled
	}
	return c.DoWithHeaders(method, path, "application/json", raw, headers)
}

func DecodeJSON(t testing.TB, resp *http.Response, into any) {
	t.Helper()
	defer resp.Body.Close()
	if err := json.NewDecoder(resp.Body).Decode(into); err != nil {
		t.Fatalf("decode json body: %v", err)
	}
}

func ReadBody(t testing.TB, resp *http.Response) []byte {
	t.Helper()
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("read response body: %v", err)
	}
	return body
}

func (c *Client) fetchToken() (string, error) {
	var req *http.Request
	var err error

	switch c.cfg.TokenMode {
	case "json":
		payload := map[string]string{
			"grant_type":    "client_credentials",
			"client_id":     c.cfg.ClientID,
			"client_secret": c.cfg.ClientSecret,
		}
		body, marshalErr := json.Marshal(payload)
		if marshalErr != nil {
			return "", marshalErr
		}
		req, err = http.NewRequest(http.MethodPost, c.cfg.TokenURL, bytes.NewReader(body))
		if err != nil {
			return "", err
		}
		req.Header.Set("Content-Type", "application/json")
	default:
		form := url.Values{}
		form.Set("grant_type", "client_credentials")
		form.Set("client_id", c.cfg.ClientID)
		form.Set("client_secret", c.cfg.ClientSecret)
		req, err = http.NewRequest(http.MethodPost, c.cfg.TokenURL, strings.NewReader(form.Encode()))
		if err != nil {
			return "", err
		}
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}

	resp, err := c.httpClient.Do(req)
	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 struct {
		AccessToken string `json:"access_token"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return "", err
	}
	if tokenResp.AccessToken == "" {
		return "", fmt.Errorf("empty access_token")
	}
	return tokenResp.AccessToken, nil
}

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 loadLocalConfig() Config {
	rootBaseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("NEXUSCRM_URL")), "/")
	if rootBaseURL == "" {
		rootBaseURL = defaultLocalBaseURL
	}
	apiVersion := strings.TrimSpace(os.Getenv("CONTRACT_API_VERSION"))
	if apiVersion == "" {
		apiVersion = defaultAPIVersion
	}
	return Config{
		TargetName:   "local",
		RootBaseURL:  rootBaseURL,
		APIBaseURL:   rootBaseURL + "/services/data/" + apiVersion,
		APIVersion:   apiVersion,
		TokenURL:     rootBaseURL + "/services/oauth2/token",
		ClientID:     defaultString(os.Getenv("CONTRACT_CLIENT_ID"), "test-gosf-client"),
		ClientSecret: defaultString(os.Getenv("CONTRACT_CLIENT_SECRET"), "test-gosf-secret-123"),
		TokenMode:    defaultTokenMode(rootBaseURL),
	}
}

func loadSandboxConfig() (Config, error) {
	path, err := sandboxConfigPath()
	if err != nil {
		return Config{}, err
	}
	raw, err := os.ReadFile(path)
	if err != nil {
		return Config{}, err
	}

	var parsed sandboxYAML
	if err := yaml.Unmarshal(raw, &parsed); err != nil {
		return Config{}, err
	}

	apiBaseURL := strings.TrimRight(parsed.Salesforce.APIBaseURL, "/")
	return Config{
		TargetName:   "sandbox",
		RootBaseURL:  rootBaseURLFromAPIBase(apiBaseURL),
		APIBaseURL:   apiBaseURL,
		APIVersion:   parsed.Salesforce.APIVersion,
		TokenURL:     strings.TrimRight(parsed.Salesforce.TokenURL, "/"),
		ClientID:     parsed.Salesforce.ClientID,
		ClientSecret: parsed.Salesforce.ClientSecret,
		TokenMode:    "form",
	}, nil
}

func sandboxConfigPath() (string, error) {
	_, filename, _, ok := runtime.Caller(0)
	if !ok {
		return "", fmt.Errorf("resolve sandbox config path: runtime caller unavailable")
	}
	return filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..", "sandbox.yaml")), nil
}

func rootBaseURLFromAPIBase(apiBaseURL string) string {
	marker := "/services/data/"
	if idx := strings.Index(apiBaseURL, marker); idx >= 0 {
		return strings.TrimRight(apiBaseURL[:idx], "/")
	}
	return strings.TrimRight(apiBaseURL, "/")
}

func defaultTokenMode(raw string) string {
	lower := strings.ToLower(strings.TrimSpace(raw))
	if strings.HasPrefix(lower, "https://") || strings.Contains(lower, "salesforce.com") {
		return "form"
	}
	return "json"
}

func defaultString(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}
