package tokenanalysis

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

const (
	EnvBaseURL = "AGENTTEAM_TOKEN_ANALYSIS_BASE_URL"
	EnvToken   = "AGENTTEAM_TOKEN_ANALYSIS_TOKEN"
	EnvAgentID = "AGENTTEAM_TOKEN_ANALYSIS_AGENT_ID"
)

type Client struct {
	BaseURL        string
	APIKey         string
	APIKeyProvider func() (string, error)
	AgentID        string
	AgentToken     string
	HTTP           *http.Client
}

type AnalysisMode uint8

const (
	AnalysisModeDefault AnalysisMode = iota
	AnalysisModePassive
	AnalysisModeStop
)

func (client Client) Analyze(ctx context.Context, request Request, mode AnalysisMode) (Report, error) {
	endpoint, err := client.endpoint(request, mode)
	if err != nil {
		return Report{}, err
	}
	httpClient := client.HTTP
	if httpClient == nil {
		httpClient = &http.Client{Timeout: 15 * time.Second}
	}
	httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return Report{}, err
	}
	if err := client.authorize(httpRequest); err != nil {
		return Report{}, err
	}
	response, err := httpClient.Do(httpRequest)
	if err != nil {
		return Report{}, fmt.Errorf("daemon_unavailable: %w", err)
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		message := readError(response.Body)
		if message == "" {
			message = http.StatusText(response.StatusCode)
		}
		return Report{}, fmt.Errorf("%s", message)
	}
	var report Report
	if err := decodeBoundedJSON(response.Body, MaxArtifactBytes, &report); err != nil {
		return Report{}, fmt.Errorf("decode token analysis: %w", err)
	}
	if err := ValidateReport(report); err != nil {
		return Report{}, fmt.Errorf("decode token analysis: %w", err)
	}
	return report, nil
}

func (client Client) SetCollection(ctx context.Context, enabled bool) (CollectionStatus, error) {
	payload, err := json.Marshal(struct {
		Enabled bool `json:"enabled"`
	}{Enabled: enabled})
	if err != nil {
		return CollectionStatus{}, err
	}
	return client.collection(ctx, http.MethodPut, payload)
}

func (client Client) collection(ctx context.Context, method string, payload []byte) (CollectionStatus, error) {
	baseURL := strings.TrimRight(strings.TrimSpace(client.BaseURL), "/")
	if baseURL == "" || (strings.TrimSpace(client.APIKey) == "" && client.APIKeyProvider == nil) {
		return CollectionStatus{}, errors.New("daemon_unavailable")
	}
	httpClient := client.HTTP
	if httpClient == nil {
		httpClient = &http.Client{Timeout: 10 * time.Second}
	}
	request, err := http.NewRequestWithContext(ctx, method, baseURL+"/api/internal/token-analysis/v2/collection", bytes.NewReader(payload))
	if err != nil {
		return CollectionStatus{}, err
	}
	if err := client.authorize(request); err != nil {
		return CollectionStatus{}, err
	}
	if len(payload) > 0 {
		request.Header.Set("Content-Type", "application/json")
	}
	response, err := httpClient.Do(request)
	if err != nil {
		return CollectionStatus{}, fmt.Errorf("daemon_unavailable: %w", err)
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return CollectionStatus{}, fmt.Errorf("%s", readError(response.Body))
	}
	var status CollectionStatus
	if err := decodeBoundedJSON(response.Body, 64<<10, &status); err != nil {
		return CollectionStatus{}, err
	}
	return status, nil
}

func (client Client) endpoint(request Request, mode AnalysisMode) (string, error) {
	baseURL := strings.TrimRight(strings.TrimSpace(client.BaseURL), "/")
	if baseURL == "" {
		return "", errors.New("daemon_unavailable")
	}
	path := "/api/internal/token-analysis/v2/analysis"
	if strings.TrimSpace(client.AgentToken) != "" {
		if strings.TrimSpace(client.AgentID) == "" {
			return "", errors.New("agent analysis scope is unavailable")
		}
		path = "/api/internal/token-analysis/v2/agents/" + url.PathEscape(client.AgentID) + "/analysis"
		if mode == AnalysisModeStop {
			return "", errors.New("Agent caller cannot change Token collection")
		}
	} else if strings.TrimSpace(client.APIKey) == "" && client.APIKeyProvider == nil {
		return "", errors.New("daemon_unavailable")
	}
	values := url.Values{}
	if strings.TrimSpace(request.Window) != "" {
		values.Set("window", request.Window)
	}
	if strings.TrimSpace(request.AgentRef) != "" {
		values.Set("agentRef", request.AgentRef)
	}
	if strings.TrimSpace(request.RunRef) != "" {
		values.Set("runRef", request.RunRef)
	}
	if strings.TrimSpace(request.Experiment.Case) != "" {
		values.Set("case", request.Experiment.Case)
	}
	if strings.TrimSpace(request.Experiment.ChangedVariable) != "" {
		values.Set("changedVariable", request.Experiment.ChangedVariable)
	}
	if strings.TrimSpace(request.Experiment.Workspace) != "" {
		values.Set("workspaceRevision", request.Experiment.Workspace)
	}
	if strings.TrimSpace(request.Experiment.Tools) != "" {
		values.Set("toolProfile", request.Experiment.Tools)
	}
	if strings.TrimSpace(request.Experiment.CacheState) != "" {
		values.Set("cacheState", request.Experiment.CacheState)
	}
	if strings.TrimSpace(request.Experiment.Outcome) != "" {
		values.Set("outcome", request.Experiment.Outcome)
	}
	if mode == AnalysisModeStop {
		values.Set("stop", "true")
	}
	if mode == AnalysisModePassive {
		values.Set("passive", "true")
	}
	return baseURL + path + "?" + values.Encode(), nil
}

func (client Client) authorize(request *http.Request) error {
	if strings.TrimSpace(client.AgentToken) != "" {
		request.Header.Set("Authorization", "Bearer "+client.AgentToken)
		return nil
	}
	apiKey := strings.TrimSpace(client.APIKey)
	if client.APIKeyProvider != nil {
		value, err := client.APIKeyProvider()
		if err != nil {
			return fmt.Errorf("create local Token analysis credential: %w", err)
		}
		apiKey = strings.TrimSpace(value)
	}
	if apiKey == "" {
		return errors.New("daemon_unavailable")
	}
	request.Header.Set("X-Api-Key", apiKey)
	return nil
}

func readError(reader io.Reader) string {
	var value struct {
		Error string `json:"error"`
	}
	_ = json.NewDecoder(io.LimitReader(reader, 64<<10)).Decode(&value)
	return strings.TrimSpace(value.Error)
}

func decodeBoundedJSON(reader io.Reader, limit int64, target any) error {
	data, err := io.ReadAll(io.LimitReader(reader, limit+1))
	if err != nil {
		return err
	}
	if int64(len(data)) > limit {
		return fmt.Errorf("response exceeds %d bytes", limit)
	}
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		return errors.New("response must contain one JSON value")
	}
	return nil
}
