package mcp

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

// Client is a thin HTTP client that forwards requests to the certctl REST API.
// It handles auth, base URL resolution, and JSON marshaling.
type Client struct {
	baseURL    string
	apiKey     string
	httpClient *http.Client
}

// NewClient creates a new certctl API client. The control plane is HTTPS-only
// as of v2.2, so the transport is pinned to TLS 1.3 and optionally loads a
// PEM-encoded CA bundle from caBundlePath (empty means "trust the system
// roots"). The insecure flag disables certificate verification and is a
// dev-only opt-in documented in docs/tls.md — it must never be set in
// production. Returns an error if the CA bundle path is non-empty but the
// file is missing or contains no valid PEM-encoded certificates, so the
// caller can fail loud before any network call.
func NewClient(baseURL, apiKey, caBundlePath string, insecure bool) (*Client, error) {
	tlsConfig := &tls.Config{
		MinVersion:         tls.VersionTLS13,
		InsecureSkipVerify: insecure, //nolint:gosec // opt-in dev toggle, documented in docs/tls.md
	}
	if caBundlePath != "" {
		pemBytes, err := os.ReadFile(caBundlePath)
		if err != nil {
			return nil, fmt.Errorf("reading CA bundle at %q: %w", caBundlePath, err)
		}
		pool := x509.NewCertPool()
		if !pool.AppendCertsFromPEM(pemBytes) {
			return nil, fmt.Errorf("CA bundle at %q contains no valid PEM-encoded certificates", caBundlePath)
		}
		tlsConfig.RootCAs = pool
	}
	return &Client{
		baseURL: baseURL,
		apiKey:  apiKey,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
			Transport: &http.Transport{
				TLSClientConfig:       tlsConfig,
				ForceAttemptHTTP2:     true,
				MaxIdleConns:          10,
				IdleConnTimeout:       90 * time.Second,
				TLSHandshakeTimeout:   10 * time.Second,
				ExpectContinueTimeout: 1 * time.Second,
			},
		},
	}, nil
}

// Get performs an HTTP GET and returns the raw JSON response body.
func (c *Client) Get(path string, query url.Values) (json.RawMessage, error) {
	return c.do("GET", path, query, nil)
}

// Post performs an HTTP POST with a JSON body and returns the raw JSON response.
func (c *Client) Post(path string, body interface{}) (json.RawMessage, error) {
	return c.do("POST", path, nil, body)
}

// Put performs an HTTP PUT with a JSON body and returns the raw JSON response.
func (c *Client) Put(path string, body interface{}) (json.RawMessage, error) {
	return c.do("PUT", path, nil, body)
}

// Delete performs an HTTP DELETE and returns the raw JSON response (may be empty for 204).
func (c *Client) Delete(path string) (json.RawMessage, error) {
	return c.do("DELETE", path, nil, nil)
}

// DeleteWithQuery performs an HTTP DELETE with query parameters. I-004 adds
// this transport so MCP tools can target endpoints that carry flags in the
// query string (e.g. DELETE /api/v1/agents/{id}?force=true&reason=…). Client.Delete
// is path-only; without this method the retire tool silently drops force/reason,
// turning every cascade retire into a default soft-retire. Shares do()'s 204
// normalization and 4xx/5xx error propagation so tool authors get one contract.
func (c *Client) DeleteWithQuery(path string, query url.Values) (json.RawMessage, error) {
	return c.do("DELETE", path, query, nil)
}

// GetRaw performs an HTTP GET and returns the raw response body bytes and content type.
// Used for binary responses (DER CRL, OCSP).
func (c *Client) GetRaw(path string) ([]byte, string, error) {
	u, err := url.JoinPath(c.baseURL, path)
	if err != nil {
		return nil, "", fmt.Errorf("invalid URL: %w", err)
	}

	req, err := http.NewRequest("GET", u, nil)
	if err != nil {
		return nil, "", fmt.Errorf("creating request: %w", err)
	}

	if c.apiKey != "" {
		req.Header.Set("Authorization", "Bearer "+c.apiKey)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, "", fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, "", fmt.Errorf("reading response: %w", err)
	}

	if resp.StatusCode >= 400 {
		return nil, "", fmt.Errorf("API error (HTTP %d): %s", resp.StatusCode, string(data))
	}

	return data, resp.Header.Get("Content-Type"), nil
}

func (c *Client) do(method, path string, query url.Values, body interface{}) (json.RawMessage, error) {
	u, err := url.JoinPath(c.baseURL, path)
	if err != nil {
		return nil, fmt.Errorf("invalid URL: %w", err)
	}

	if query != nil && len(query) > 0 {
		u = u + "?" + query.Encode()
	}

	var bodyReader io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("marshaling request body: %w", err)
		}
		bodyReader = bytes.NewReader(data)
	}

	req, err := http.NewRequest(method, u, bodyReader)
	if err != nil {
		return nil, fmt.Errorf("creating request: %w", err)
	}

	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("Accept", "application/json")

	if c.apiKey != "" {
		req.Header.Set("Authorization", "Bearer "+c.apiKey)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("reading response: %w", err)
	}

	// 204 No Content — return empty JSON object
	if resp.StatusCode == 204 {
		return json.RawMessage(`{"status":"deleted"}`), nil
	}

	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("API error (HTTP %d): %s", resp.StatusCode, string(respBody))
	}

	return json.RawMessage(respBody), nil
}
