package client

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

const defaultTimeout = 10 * time.Second

type Config struct {
	BaseURL   string
	APIToken  string
	Timeout   time.Duration
	UserAgent string
	Now       func() time.Time
}

type HTTPClient struct {
	baseURL    *url.URL
	apiToken   string
	httpClient *http.Client
	userAgent  string
	now        func() time.Time
}

type HTTPError struct {
	StatusCode int
	Body       string
}

func (e *HTTPError) Error() string {
	if e == nil {
		return ""
	}
	if e.Body == "" {
		return fmt.Sprintf("scanner returned status %d", e.StatusCode)
	}
	return fmt.Sprintf("scanner returned status %d: %s", e.StatusCode, e.Body)
}

func NewHTTPClient(cfg Config, httpClient *http.Client) (*HTTPClient, error) {
	base := strings.TrimSpace(cfg.BaseURL)
	if base == "" {
		return nil, errors.New("scanner base url is required")
	}
	parsed, err := url.Parse(base)
	if err != nil {
		return nil, fmt.Errorf("parse scanner base url: %w", err)
	}
	if parsed.Scheme == "" || parsed.Host == "" {
		return nil, errors.New("scanner base url must include scheme and host")
	}
	token := strings.TrimSpace(cfg.APIToken)
	if token == "" {
		return nil, errors.New("scanner api token is required")
	}
	timeout := cfg.Timeout
	if timeout <= 0 {
		timeout = defaultTimeout
	}
	if httpClient == nil {
		httpClient = &http.Client{Timeout: timeout}
	} else if httpClient.Timeout == 0 {
		clone := *httpClient
		clone.Timeout = timeout
		httpClient = &clone
	}
	now := cfg.Now
	if now == nil {
		now = time.Now
	}
	userAgent := strings.TrimSpace(cfg.UserAgent)
	if userAgent == "" {
		userAgent = "security-center-scan-client"
	}
	return &HTTPClient{
		baseURL:    parsed,
		apiToken:   token,
		httpClient: httpClient,
		userAgent:  userAgent,
		now:        now,
	}, nil
}

func (c *HTTPClient) CreateBatch(ctx context.Context, req CreateBatchRequest) (*CreateBatchResponse, error) {
	req.RequestID = strings.TrimSpace(req.RequestID)
	if req.RequestID == "" {
		return nil, errors.New("request_id is required")
	}
	if strings.TrimSpace(req.Source) == "" {
		req.Source = DefaultSource
	}
	if req.Source != DefaultSource {
		return nil, fmt.Errorf("scanner source must be %q", DefaultSource)
	}
	if len(req.Targets) == 0 {
		return nil, errors.New("at least one scanner target is required")
	}
	var payload json.RawMessage
	if err := c.doJSON(ctx, http.MethodPost, "/api/v1/reachability/batches", req.RequestID, req, &payload); err != nil {
		return nil, err
	}
	return decodeCreateBatchResponse(payload)
}

func (c *HTTPClient) GetBatch(ctx context.Context, scannerBatchID string) (*BatchResponse, error) {
	scannerBatchID = strings.TrimSpace(scannerBatchID)
	if scannerBatchID == "" {
		return nil, errors.New("scanner batch id is required")
	}
	var payload json.RawMessage
	if err := c.doJSON(ctx, http.MethodGet, "/api/v1/reachability/batches/"+url.PathEscape(scannerBatchID), "", nil, &payload); err != nil {
		return nil, err
	}
	return decodeBatchResponse(payload)
}

func (c *HTTPClient) GetResults(ctx context.Context, scannerBatchID string) (*ResultsResponse, error) {
	scannerBatchID = strings.TrimSpace(scannerBatchID)
	if scannerBatchID == "" {
		return nil, errors.New("scanner batch id is required")
	}
	var payload json.RawMessage
	if err := c.doJSON(ctx, http.MethodGet, "/api/v1/reachability/batches/"+url.PathEscape(scannerBatchID)+"/results", "", nil, &payload); err != nil {
		return nil, err
	}
	return decodeResultsResponse(payload)
}

func (c *HTTPClient) CancelBatch(ctx context.Context, scannerBatchID string, idempotencyKey string) (*BatchResponse, error) {
	scannerBatchID = strings.TrimSpace(scannerBatchID)
	if scannerBatchID == "" {
		return nil, errors.New("scanner batch id is required")
	}
	idempotencyKey = strings.TrimSpace(idempotencyKey)
	if idempotencyKey == "" {
		return nil, errors.New("idempotency key is required")
	}
	var payload json.RawMessage
	if err := c.doJSON(ctx, http.MethodPost, "/api/v1/reachability/batches/"+url.PathEscape(scannerBatchID)+"/cancel", idempotencyKey, nil, &payload); err != nil {
		return nil, err
	}
	if len(bytes.TrimSpace(payload)) == 0 {
		return &BatchResponse{BatchID: scannerBatchID, Status: "canceled"}, nil
	}
	return decodeBatchResponse(payload)
}

func (c *HTTPClient) doJSON(ctx context.Context, method, path, idempotencyKey string, body any, out any) error {
	var reader io.Reader
	if body != nil {
		encoded, err := json.Marshal(body)
		if err != nil {
			return fmt.Errorf("encode scanner request: %w", err)
		}
		reader = bytes.NewReader(encoded)
	}
	endpoint := c.baseURL.ResolveReference(&url.URL{Path: strings.TrimRight(c.baseURL.Path, "/") + path})
	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), reader)
	if err != nil {
		return fmt.Errorf("build scanner request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+c.apiToken)
	req.Header.Set("Accept", "application/json")
	req.Header.Set("X-Request-Timestamp", c.now().UTC().Format(time.RFC3339))
	req.Header.Set("User-Agent", c.userAgent)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if idempotencyKey != "" {
		req.Header.Set("Idempotency-Key", idempotencyKey)
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("scanner request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
	if err != nil {
		return fmt.Errorf("read scanner response: %w", err)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return &HTTPError{StatusCode: resp.StatusCode, Body: strings.TrimSpace(string(respBody))}
	}
	if out == nil || len(bytes.TrimSpace(respBody)) == 0 {
		return nil
	}
	if err := json.Unmarshal(respBody, out); err != nil {
		return fmt.Errorf("decode scanner response: %w", err)
	}
	return nil
}

func decodeCreateBatchResponse(payload json.RawMessage) (*CreateBatchResponse, error) {
	var wrapped struct {
		Batch           BatchResponse         `json:"batch"`
		Existed         bool                  `json:"existed"`
		RejectedTargets []BatchTargetResponse `json:"rejected_targets,omitempty"`
	}
	if err := json.Unmarshal(payload, &wrapped); err != nil {
		return nil, fmt.Errorf("decode scanner create response: %w", err)
	}
	if wrapped.Batch.BatchID != "" {
		return &CreateBatchResponse{
			BatchID:         wrapped.Batch.BatchID,
			RequestID:       wrapped.Batch.RequestID,
			Status:          wrapped.Batch.Status,
			AcceptedTargets: wrapped.Batch.AcceptedTargets,
			RejectedTargets: wrapped.Batch.RejectedTargets,
			QueuedJobs:      wrapped.Batch.QueuedJobs,
			RunningJobs:     wrapped.Batch.RunningJobs,
			SucceededJobs:   wrapped.Batch.SucceededJobs,
			FailedJobs:      wrapped.Batch.FailedJobs,
			SkippedJobs:     wrapped.Batch.SkippedJobs,
			Targets:         wrapped.RejectedTargets,
		}, nil
	}
	var flat CreateBatchResponse
	if err := json.Unmarshal(payload, &flat); err != nil {
		return nil, fmt.Errorf("decode scanner create response: %w", err)
	}
	return &flat, nil
}

func decodeBatchResponse(payload json.RawMessage) (*BatchResponse, error) {
	var wrapped struct {
		Batch BatchResponse `json:"batch"`
	}
	if err := json.Unmarshal(payload, &wrapped); err == nil && wrapped.Batch.BatchID != "" {
		return &wrapped.Batch, nil
	}
	var out BatchResponse
	if err := json.Unmarshal(payload, &out); err != nil {
		return nil, fmt.Errorf("decode scanner batch response: %w", err)
	}
	return &out, nil
}

func decodeResultsResponse(payload json.RawMessage) (*ResultsResponse, error) {
	var wrapped struct {
		Batch        BatchResponse       `json:"batch"`
		Observations []ObservationResult `json:"observations"`
	}
	if err := json.Unmarshal(payload, &wrapped); err != nil {
		return nil, fmt.Errorf("decode scanner results response: %w", err)
	}
	if wrapped.Batch.BatchID != "" {
		completedAt := wrapped.Batch.CompletedAt
		if completedAt == nil {
			completedAt = wrapped.Batch.UpdatedAt
		}
		return &ResultsResponse{
			BatchID:      wrapped.Batch.BatchID,
			RequestID:    wrapped.Batch.RequestID,
			Status:       wrapped.Batch.Status,
			CompletedAt:  completedAt,
			Observations: wrapped.Observations,
		}, nil
	}
	var flat ResultsResponse
	if err := json.Unmarshal(payload, &flat); err != nil {
		return nil, fmt.Errorf("decode scanner results response: %w", err)
	}
	return &flat, nil
}
