package clinic_internal_api

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/trace"
)

const maxResourceResponseBytes = 32 << 20

// UpstreamError reports a non-success response from Clinic's internal API.
// The response body is intentionally discarded so internal implementation
// details cannot escape through the public Data Proxy API.
type UpstreamError struct {
	StatusCode int
}

func (e *UpstreamError) Error() string {
	return fmt.Sprintf("clinic internal API returned HTTP status %d", e.StatusCode)
}

// DiagnosticContext contains the server-resolved identifiers used by
// diagnostic capabilities. Callers cannot override these values.
type DiagnosticContext struct {
	ProjectID  string `json:"projectId,omitempty"`
	ParentID   string `json:"parentId,omitempty"`
	ShootName  string `json:"shootName,omitempty"`
	KeyspaceID string `json:"keyspaceId,omitempty"`
}

// ClusterMetadata is Clinic's canonical cluster representation for Data
// Proxy resource APIs.
type ClusterMetadata struct {
	ClusterID         string            `json:"clusterId"`
	OrgID             string            `json:"orgId"`
	Name              string            `json:"name"`
	Status            string            `json:"status"`
	Deleted           bool              `json:"deleted"`
	Version           string            `json:"version"`
	Provider          string            `json:"provider"`
	Region            string            `json:"region"`
	DeployType        string            `json:"deployType"`
	DeployTypeV2      string            `json:"deployTypeV2"`
	CreatedAt         string            `json:"createdAt,omitempty"`
	DiagnosticContext DiagnosticContext `json:"diagnosticContext"`
}

// ClusterMetadataPage is an org-scoped page returned by Clinic.
type ClusterMetadataPage struct {
	Items    []ClusterMetadata `json:"items"`
	Page     int               `json:"page"`
	PageSize int               `json:"pageSize"`
	Total    int64             `json:"total"`
}

// Activity intentionally preserves the established snake_case Clinic
// contract and arbitrary JSON payload.
type Activity struct {
	Type            string            `json:"type"`
	Name            string            `json:"name"`
	DisplayName     string            `json:"display_name"`
	Catalog         string            `json:"catalog"`
	Severity        int               `json:"severity"`
	ActivityID      string            `json:"activity_id"`
	ApplicationID   string            `json:"application_id"`
	ConceptID       string            `json:"concept_id"`
	TargetID        string            `json:"target_id"`
	CalibrationTime int64             `json:"calibration_time"`
	Source          string            `json:"source"`
	Author          string            `json:"author"`
	ContextID       string            `json:"context_id"`
	Context         []string          `json:"context"`
	Tags            map[string]string `json:"tags,omitempty"`
	Payload         any               `json:"payload"`
}

type ActivitiesResponse struct {
	Activities []*Activity `json:"activities"`
	Partial    bool        `json:"partial"`
	Cursor     string      `json:"cursor"`
}

type ResourcePoolImage struct {
	Repo string `json:"repo,omitempty"`
	Tag  string `json:"tag,omitempty"`
}

type ResourcePoolStorage struct {
	Name          string `json:"name,omitempty"`
	Type          string `json:"type,omitempty"`
	SizeGi        uint32 `json:"size_gi,omitempty"`
	Iops          uint32 `json:"iops,omitempty"`
	ThroughputMbs uint32 `json:"throughput_mbs,omitempty"`
}

type ResourcePoolComponent struct {
	ID            string                 `json:"id"`
	ComponentType string                 `json:"component_type"`
	CPU           uint32                 `json:"cpu"`
	MemoryGi      uint32                 `json:"memory_gi"`
	NodeCount     uint32                 `json:"node_count"`
	MaxNodeCount  uint32                 `json:"max_node_count"`
	MinNodeCount  uint32                 `json:"min_node_count"`
	Storages      []*ResourcePoolStorage `json:"storages"`
	Version       string                 `json:"version"`
	Phase         string                 `json:"phase"`
	Image         *ResourcePoolImage     `json:"image,omitempty"`
}

func (c *Client) GetClusterMetadata(ctx context.Context, clusterID string) (*ClusterMetadata, error) {
	var result ClusterMetadata
	err := c.getResource(ctx, []string{"clusters", clusterID}, nil, &result)
	return &result, err
}

func (c *Client) GetOrgClusterMetadata(ctx context.Context, orgID uint64, clusterID string) (*ClusterMetadata, error) {
	var result ClusterMetadata
	err := c.getResource(ctx, []string{"orgs", strconv.FormatUint(orgID, 10), "clusters", clusterID}, nil, &result)
	return &result, err
}

func (c *Client) ListOrgClusterMetadata(ctx context.Context, orgID uint64, query url.Values) (*ClusterMetadataPage, error) {
	var result ClusterMetadataPage
	err := c.getResource(ctx, []string{"orgs", strconv.FormatUint(orgID, 10), "clusters"}, query, &result)
	if result.Items == nil {
		result.Items = make([]ClusterMetadata, 0)
	}
	return &result, err
}

func (c *Client) ListActivities(ctx context.Context, orgID uint64, clusterID string, query url.Values) (*ActivitiesResponse, error) {
	var result ActivitiesResponse
	err := c.getResource(ctx, []string{"orgs", strconv.FormatUint(orgID, 10), "clusters", clusterID, "activities"}, query, &result)
	if result.Activities == nil {
		result.Activities = make([]*Activity, 0)
	}
	return &result, err
}

func (c *Client) ListComponents(ctx context.Context, orgID uint64, clusterID string) ([]*ResourcePoolComponent, error) {
	var result []*ResourcePoolComponent
	err := c.getResource(ctx, []string{"orgs", strconv.FormatUint(orgID, 10), "clusters", clusterID, "components"}, nil, &result)
	if result == nil {
		result = make([]*ResourcePoolComponent, 0)
	}
	return result, err
}

func (c *Client) getResource(ctx context.Context, segments []string, query url.Values, target any) error {
	requestURL, err := c.resourceURL(segments, query)
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
	if err != nil {
		return fmt.Errorf("create Clinic internal API request: %w", err)
	}
	injectTraceHeaders(ctx, req.Header)

	httpClient := *c.httpClient
	httpClient.CheckRedirect = func(*http.Request, []*http.Request) error {
		return http.ErrUseLastResponse
	}
	resp, err := httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("call Clinic internal API: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, maxResourceResponseBytes))
		return &UpstreamError{StatusCode: resp.StatusCode}
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResourceResponseBytes+1))
	if err != nil {
		return fmt.Errorf("read Clinic internal API response: %w", err)
	}
	if len(body) > maxResourceResponseBytes {
		return fmt.Errorf("Clinic internal API response exceeds size limit")
	}
	decoder := json.NewDecoder(bytes.NewReader(body))
	decoder.UseNumber()
	if err := decoder.Decode(target); err != nil {
		return fmt.Errorf("decode Clinic internal API response: %w", err)
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return fmt.Errorf("decode Clinic internal API response: trailing JSON data")
	}
	return nil
}

func injectTraceHeaders(ctx context.Context, header http.Header) {
	otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(header))
	if spanContext := trace.SpanContextFromContext(ctx); spanContext.HasTraceID() {
		header.Set("X-Trace-ID", spanContext.TraceID().String())
	}
}

func (c *Client) resourceURL(segments []string, query url.Values) (*url.URL, error) {
	baseURL, err := url.Parse(c.baseURL)
	if err != nil || (baseURL.Scheme != "http" && baseURL.Scheme != "https") || baseURL.Host == "" {
		return nil, fmt.Errorf("clinic_api_base must be an absolute HTTP(S) URL")
	}
	if baseURL.User != nil || baseURL.RawQuery != "" || baseURL.Fragment != "" {
		return nil, fmt.Errorf("clinic_api_base must not contain userinfo, query, or fragment")
	}

	rawPath := strings.TrimRight(baseURL.EscapedPath(), "/") + "/clinic/api/v1/internal/data-proxy/v1"
	for _, segment := range segments {
		if segment == "" || segment == "." || segment == ".." {
			return nil, fmt.Errorf("Clinic internal API path segment is invalid")
		}
		rawPath += "/" + url.PathEscape(segment)
	}
	decodedPath, err := url.PathUnescape(rawPath)
	if err != nil {
		return nil, fmt.Errorf("build Clinic internal API path: %w", err)
	}
	baseURL.Path = decodedPath
	baseURL.RawPath = rawPath
	baseURL.RawQuery = query.Encode()
	return baseURL, nil
}
