package cmd

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"

	"github.com/spf13/cobra"
	"github.com/tidbcloud/clinic-cli/internal/clinic"
)

func rawCommandHandler(ctx *RuntimeContext, args []string, stdout io.Writer) error {
	command := newRawCommand(staticRuntime(ctx), stdout)
	if err := executeCobraCommand(command, args); err != nil {
		return normalizeFamilyCommandError(err, "raw")
	}
	return nil
}

func newRawCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	command := newCommand(commandMetadata{
		Use:   "raw",
		Short: "Send direct requests to /clinic/api/* or /data-proxy/*.",
		Long:  "Send direct requests to Clinic or data-proxy APIs.",
	}, stdout)
	command.RunE = func(c *cobra.Command, _ []string) error {
		return c.Help()
	}
	command.Args = cobra.NoArgs
	command.AddCommand(
		newRawRequestCommand(runtime, stdout),
	)
	return command
}

type rawRequestOptions struct {
	method      string
	path        string
	queryParams []string
	headers     []string
	bodyArg     string
}

func newRawRequestCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	var opts rawRequestOptions
	command := newCommand(commandMetadata{
		Use:   "request",
		Short: "Send one HTTP request with optional query, headers, and JSON body.",
		Long: `Send a raw API request.

Behavior:
  --method and --path are required.
  --path must start with /clinic/api/ or /data-proxy/.
  --query and --header are repeatable flags that append one entry per occurrence.`,
		Example: `  clinic raw request --method GET --path /clinic/api/v1/dashboard/clusters --query query=tidb
  clinic raw request --method POST --path /data-proxy/query --body @payload.json`,
	}, stdout)
	command.Args = cobra.NoArgs
	command.Flags().StringVar(&opts.method, "method", "", "HTTP method.")
	command.Flags().StringVar(&opts.path, "path", "", "Request path.")
	command.Flags().StringArrayVar(&opts.queryParams, "query", nil, "Query parameter in key=value form (repeatable).")
	command.Flags().StringArrayVar(&opts.headers, "header", nil, "Header in Key: Value form (repeatable).")
	command.Flags().StringVar(&opts.bodyArg, "body", "", "JSON body or @file.")
	command.RunE = func(_ *cobra.Command, _ []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runRawRequest(ctx, opts, stdout)
	}
	return command
}

func runRawRequest(ctx *RuntimeContext, opts rawRequestOptions, stdout io.Writer) error {
	if opts.method == "" {
		return errors.New("missing required --method")
	}
	if opts.path == "" {
		return errors.New("missing required --path")
	}

	normalizedMethod, err := normalizeHTTPMethod(opts.method)
	if err != nil {
		return err
	}
	if err := validateRawPath(opts.path); err != nil {
		return err
	}

	query, err := buildRawQueryParams(opts.queryParams)
	if err != nil {
		return err
	}
	requestHeaders, err := buildRawHeaders(opts.headers)
	if err != nil {
		return err
	}
	body, err := parseRawBody(opts.bodyArg)
	if err != nil {
		return err
	}

	return executeClinicJSON(ctx, clinic.Request{
		Method:  normalizedMethod,
		Path:    opts.path,
		Query:   query,
		Headers: requestHeaders,
		Body:    body,
	}, stdout)
}

func normalizeHTTPMethod(method string) (string, error) {
	normalized := strings.ToUpper(strings.TrimSpace(method))
	switch normalized {
	case http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodOptions, http.MethodTrace, http.MethodConnect:
		return normalized, nil
	default:
		return "", fmt.Errorf("invalid method: %s", method)
	}
}

func validateRawPath(path string) error {
	if strings.HasPrefix(path, "/clinic/api/") || strings.HasPrefix(path, "/data-proxy/") {
		return nil
	}
	return errors.New("path must start with /clinic/api/ or /data-proxy/")
}

func buildRawQueryParams(entries []string) (url.Values, error) {
	params := url.Values{}
	for _, entry := range entries {
		key, value, ok := strings.Cut(entry, "=")
		if !ok || key == "" {
			return nil, fmt.Errorf("invalid query parameter: %s", entry)
		}
		params.Add(key, value)
	}
	return params, nil
}

func buildRawHeaders(entries []string) (http.Header, error) {
	headers := http.Header{}
	for _, entry := range entries {
		key, value, ok := strings.Cut(entry, ":")
		if !ok || strings.TrimSpace(key) == "" {
			return nil, fmt.Errorf("invalid header: %s", entry)
		}
		headers.Add(strings.TrimSpace(key), strings.TrimSpace(value))
	}
	return headers, nil
}

func parseRawBody(body string) (any, error) {
	if body == "" {
		return nil, nil
	}

	data := []byte(body)
	if strings.HasPrefix(body, "@") {
		filePath := strings.TrimPrefix(body, "@")
		if filePath == "" {
			return nil, errors.New("body file path must not be empty")
		}

		fileData, err := os.ReadFile(filePath)
		if err != nil {
			return nil, fmt.Errorf("read body file: %w", err)
		}
		data = fileData
	}

	var parsed any
	if err := json.Unmarshal(data, &parsed); err != nil {
		return nil, fmt.Errorf("parse body json: %w", err)
	}
	return parsed, nil
}
