package cmd

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

	"forgejo.zerova.net/public/fj/internal/config"
	"forgejo.zerova.net/public/fj/internal/git"
	"github.com/spf13/cobra"
)

var apiCmd = &cobra.Command{
	Use:   "api <endpoint> [flags]",
	Short: "Make an authenticated API request",
	Long: `Makes an authenticated HTTP request to the Forgejo API and prints the response.

The endpoint argument should be a path like "/repos/{owner}/{repo}/pulls".
Placeholders {owner} and {repo} are automatically replaced with values
detected from the current git repository.

If --field is used and no --method is specified, the method defaults to POST.`,
	Example: `  # List pull requests for the current repository
  fj api /repos/{owner}/{repo}/pulls

  # Create an issue
  fj api /repos/{owner}/{repo}/issues --method POST --field title=Bug --field body="It broke"

  # Get a specific user
  fj api /users/johndoe

  # Use raw body from stdin
  echo '{"title":"test"}' | fj api /repos/{owner}/{repo}/issues --input -`,
	Args: cobra.ExactArgs(1),
	RunE: runAPI,
}

func init() {
	rootCmd.AddCommand(apiCmd)

	apiCmd.Flags().StringP("method", "X", "", "HTTP method (default: GET, or POST if --field is used)")
	apiCmd.Flags().StringArrayP("field", "f", nil, "Add a typed field to the request body (key=value)")
	apiCmd.Flags().StringArrayP("raw-field", "F", nil, "Add a string field to the request body (key=value)")
	apiCmd.Flags().String("input", "", "Read request body from file (use \"-\" for stdin)")
	apiCmd.Flags().StringArrayP("header", "H", nil, "Add an HTTP request header (key:value)")
	apiCmd.Flags().Bool("silent", false, "Do not print the response body")
	apiCmd.Flags().BoolP("include", "i", false, "Include HTTP response headers in the output")
}

func runAPI(cmd *cobra.Command, args []string) error {
	endpoint := args[0]

	method, _ := cmd.Flags().GetString("method")
	fields, _ := cmd.Flags().GetStringArray("field")
	rawFields, _ := cmd.Flags().GetStringArray("raw-field")
	inputFile, _ := cmd.Flags().GetString("input")
	headers, _ := cmd.Flags().GetStringArray("header")
	hostname, _ := cmd.Flags().GetString("hostname")
	silent, _ := cmd.Flags().GetBool("silent")
	include, _ := cmd.Flags().GetBool("include")

	// Resolve hostname and token from config
	cfg, err := config.Load()
	if err != nil {
		return err
	}

	detectedHost := getDetectedHost()

	host, err := cfg.GetHost(hostname, detectedHost, getCwd())
	if err != nil {
		return err
	}

	// Interpolate {owner} and {repo} placeholders
	if strings.Contains(endpoint, "{owner}") || strings.Contains(endpoint, "{repo}") {
		owner, repo, detectErr := git.DetectRepo()
		if detectErr != nil {
			return fmt.Errorf("cannot determine repository for path interpolation: %w", detectErr)
		}
		endpoint = strings.ReplaceAll(endpoint, "{owner}", owner)
		endpoint = strings.ReplaceAll(endpoint, "{repo}", repo)
	}

	// Determine HTTP method
	hasBody := len(fields) > 0 || len(rawFields) > 0 || inputFile != ""
	if method == "" {
		if hasBody {
			method = http.MethodPost
		} else {
			method = http.MethodGet
		}
	}
	method = strings.ToUpper(method)

	// Build request body
	var body io.Reader
	if inputFile != "" {
		if len(fields) > 0 || len(rawFields) > 0 {
			return fmt.Errorf("--input cannot be combined with --field or --raw-field")
		}
		if inputFile == "-" {
			body = os.Stdin
		} else {
			f, openErr := os.Open(inputFile)
			if openErr != nil {
				return fmt.Errorf("failed to open input file: %w", openErr)
			}
			defer func() { _ = f.Close() }()
			body = f
		}
	} else if len(fields) > 0 || len(rawFields) > 0 {
		bodyMap := make(map[string]any)

		for _, f := range fields {
			key, value, parseErr := parseField(f, false)
			if parseErr != nil {
				return parseErr
			}
			bodyMap[key] = value
		}
		for _, f := range rawFields {
			key, value, parseErr := parseField(f, true)
			if parseErr != nil {
				return parseErr
			}
			bodyMap[key] = value
		}

		bodyBytes, marshalErr := json.Marshal(bodyMap)
		if marshalErr != nil {
			return fmt.Errorf("failed to marshal request body: %w", marshalErr)
		}
		body = bytes.NewReader(bodyBytes)
	}

	// Build URL
	baseURL := "https://" + host.Hostname + "/api/v1"
	if !strings.HasPrefix(endpoint, "/") {
		endpoint = "/" + endpoint
	}
	url := baseURL + endpoint

	// Create HTTP request
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	// Set auth header
	if host.Token != "" {
		req.Header.Set("Authorization", "token "+host.Token)
	}
	req.Header.Set("Accept", "application/json")
	if hasBody {
		req.Header.Set("Content-Type", "application/json")
	}

	// Apply custom headers
	for _, h := range headers {
		key, value, found := strings.Cut(h, ":")
		if !found {
			return fmt.Errorf("invalid header format %q (expected key:value)", h)
		}
		req.Header.Set(strings.TrimSpace(key), strings.TrimSpace(value))
	}

	// Execute request
	ios.StartSpinner("Requesting...")
	httpClient := &http.Client{}
	resp, err := httpClient.Do(req)
	ios.StopSpinner()
	if err != nil {
		return fmt.Errorf("failed to perform request: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	// Print response headers if requested
	if include {
		fmt.Fprintf(ios.Out, "%s %s\n", resp.Proto, resp.Status)
		for key, values := range resp.Header {
			for _, v := range values {
				fmt.Fprintf(ios.Out, "%s: %s\n", key, v)
			}
		}
		fmt.Fprintln(ios.Out)
	}

	// Read response body
	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to read response body: %w", err)
	}

	// Handle non-2xx status codes
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		if !silent {
			fmt.Fprint(ios.ErrOut, string(respBody))
			if len(respBody) > 0 && respBody[len(respBody)-1] != '\n' {
				fmt.Fprintln(ios.ErrOut)
			}
		}
		return fmt.Errorf("API request failed with status %d", resp.StatusCode)
	}

	if silent || len(respBody) == 0 {
		return nil
	}

	// Pretty-print JSON, or output raw if not JSON
	contentType := resp.Header.Get("Content-Type")
	if strings.Contains(contentType, "json") || json.Valid(respBody) {
		var parsed any
		if err := json.Unmarshal(respBody, &parsed); err == nil {
			enc := json.NewEncoder(ios.Out)
			enc.SetIndent("", "  ")
			return enc.Encode(parsed)
		}
	}

	// Raw output for non-JSON responses
	_, err = ios.Out.Write(respBody)
	return err
}

// parseField parses a "key=value" string. When rawString is true, the value is
// always treated as a string. Otherwise, the function attempts JSON type
// inference: booleans ("true"/"false"), null, numbers, and falls back to string.
func parseField(field string, rawString bool) (string, any, error) {
	key, value, found := strings.Cut(field, "=")
	if !found {
		return "", nil, fmt.Errorf("invalid field format %q (expected key=value)", field)
	}

	if rawString {
		return key, value, nil
	}

	// JSON type inference
	switch {
	case value == "true":
		return key, true, nil
	case value == "false":
		return key, false, nil
	case value == "null":
		return key, nil, nil
	default:
		// Try number
		if n, err := strconv.ParseInt(value, 10, 64); err == nil {
			return key, n, nil
		}
		if f, err := strconv.ParseFloat(value, 64); err == nil {
			return key, f, nil
		}
		return key, value, nil
	}
}
