// Package claudebox provides an HTTP client for the claudebox sidecar.
// This client is used by standalone workers to communicate with the local
// claudebox sidecar instead of using kubectl exec.
package claudebox

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

// Client is an HTTP client for the claudebox sidecar.
type Client struct {
	baseURL    string
	httpClient *http.Client
}

// ClientConfig holds configuration for the claudebox client.
type ClientConfig struct {
	// BaseURL is the base URL of the claudebox sidecar (e.g., "http://localhost:8080").
	BaseURL string

	// Timeout is the default request timeout.
	Timeout time.Duration
}

// NewClient creates a new claudebox client.
func NewClient(cfg ClientConfig) *Client {
	if cfg.Timeout == 0 {
		cfg.Timeout = 10 * time.Minute
	}
	return &Client{
		baseURL: strings.TrimSuffix(cfg.BaseURL, "/"),
		httpClient: &http.Client{
			Timeout: cfg.Timeout,
		},
	}
}

// HealthResponse is the health check response.
type HealthResponse struct {
	Status    string `json:"status"`
	Timestamp string `json:"timestamp"`
	WorkDir   string `json:"work_dir"`
}

// Health checks if the claudebox sidecar is healthy.
func (c *Client) Health(ctx context.Context) (*HealthResponse, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/health", nil)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

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

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("health check returned status %d", resp.StatusCode)
	}

	var result HealthResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	return &result, nil
}

// ExecuteRequest is the request to execute Claude Code.
type ExecuteRequest struct {
	Prompt       string            `json:"prompt"`
	AllowedTools []string          `json:"allowed_tools,omitempty"`
	WorkingDir   string            `json:"working_dir,omitempty"`
	Timeout      int               `json:"timeout_seconds,omitempty"` // seconds
	Metadata     map[string]string `json:"metadata,omitempty"`
}

// ExecuteResponse is the response from executing Claude Code.
type ExecuteResponse struct {
	Success     bool              `json:"success"`
	Output      string            `json:"output"`
	ExitCode    int               `json:"exit_code"`
	DurationMs  int64             `json:"duration_ms"`
	Error       string            `json:"error,omitempty"`
	SessionID   string            `json:"session_id,omitempty"`
	FinalOutput string            `json:"final_output,omitempty"`
	Artifacts   map[string]string `json:"artifacts,omitempty"`
}

// Execute runs Claude Code and returns the complete result.
func (c *Client) Execute(ctx context.Context, req *ExecuteRequest) (*ExecuteResponse, error) {
	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/execute", bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("execute: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return nil, fmt.Errorf("execute returned status %d (failed to read body: %w)", resp.StatusCode, readErr)
		}
		return nil, fmt.Errorf("execute returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var result ExecuteResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	return &result, nil
}

// StreamEvent is an SSE event from streaming execution.
type StreamEvent struct {
	Type      string         `json:"type"`
	Content   string         `json:"content,omitempty"`
	Stream    string         `json:"stream,omitempty"`
	ToolName  string         `json:"tool_name,omitempty"`
	Data      map[string]any `json:"data,omitempty"`
	Timestamp string         `json:"timestamp"`
}

// StreamEventHandler is called for each event during streaming execution.
type StreamEventHandler func(StreamEvent)

// ExecuteStream runs Claude Code and streams events to the handler.
func (c *Client) ExecuteStream(ctx context.Context, req *ExecuteRequest, handler StreamEventHandler) error {
	body, err := json.Marshal(req)
	if err != nil {
		return fmt.Errorf("marshal request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/execute/stream", bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "text/event-stream")

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return fmt.Errorf("execute stream: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return fmt.Errorf("execute stream returned status %d (failed to read body: %w)", resp.StatusCode, readErr)
		}
		return fmt.Errorf("execute stream returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	// Parse SSE events
	scanner := bufio.NewScanner(resp.Body)
	for scanner.Scan() {
		line := scanner.Text()
		if !strings.HasPrefix(line, "data: ") {
			continue
		}

		data := strings.TrimPrefix(line, "data: ")
		if data == "" {
			continue
		}

		var event StreamEvent
		if err := json.Unmarshal([]byte(data), &event); err != nil {
			continue // Skip malformed events
		}

		handler(event)
	}

	if err := scanner.Err(); err != nil {
		return fmt.Errorf("read stream: %w", err)
	}

	return nil
}

// GitCloneRequest is the request to clone a repository.
type GitCloneRequest struct {
	CloneURL string `json:"clone_url"`
	WorkDir  string `json:"work_dir,omitempty"`
}

// GitCloneResponse is the response from cloning.
type GitCloneResponse struct {
	Success bool   `json:"success"`
	Cloned  bool   `json:"cloned"`
	Error   string `json:"error,omitempty"`
}

// GitClone clones or updates a git repository.
func (c *Client) GitClone(ctx context.Context, cloneURL, workDir string) (*GitCloneResponse, error) {
	req := GitCloneRequest{
		CloneURL: cloneURL,
		WorkDir:  workDir,
	}

	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/git/clone", bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("git clone: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return nil, fmt.Errorf("git clone returned status %d (failed to read body: %w)", resp.StatusCode, readErr)
		}
		return nil, fmt.Errorf("git clone returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var result GitCloneResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	return &result, nil
}

// GitCommitAndPushRequest is the request to commit and push changes.
type GitCommitAndPushRequest struct {
	Message string `json:"message"`
	Push    bool   `json:"push"`
	WorkDir string `json:"work_dir,omitempty"`
}

// GitCommitAndPushResponse is the response from commit and push.
type GitCommitAndPushResponse struct {
	Success      bool     `json:"success"`
	HasChanges   bool     `json:"has_changes"`
	CommitSHA    string   `json:"commit_sha,omitempty"`
	FilesChanged []string `json:"files_changed,omitempty"`
	Pushed       bool     `json:"pushed"`
	Error        string   `json:"error,omitempty"`
}

// GitCommitAndPush commits and optionally pushes changes.
func (c *Client) GitCommitAndPush(ctx context.Context, message string, push bool, workDir string) (*GitCommitAndPushResponse, error) {
	req := GitCommitAndPushRequest{
		Message: message,
		Push:    push,
		WorkDir: workDir,
	}

	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/git/commit-and-push", bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("git commit: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return nil, fmt.Errorf("git commit returned status %d (failed to read body: %w)", resp.StatusCode, readErr)
		}
		return nil, fmt.Errorf("git commit returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var result GitCommitAndPushResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	return &result, nil
}

// GitStatusResponse is the response from git status.
type GitStatusResponse struct {
	IsRepo       bool     `json:"is_repo"`
	HasChanges   bool     `json:"has_changes"`
	ChangedFiles []string `json:"changed_files,omitempty"`
	Branch       string   `json:"branch,omitempty"`
	Error        string   `json:"error,omitempty"`
}

// GitStatus returns the git status of the workspace.
func (c *Client) GitStatus(ctx context.Context, workDir string) (*GitStatusResponse, error) {
	reqURL := c.baseURL + "/git/status"
	if workDir != "" {
		reqURL += "?work_dir=" + url.QueryEscape(workDir)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

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

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return nil, fmt.Errorf("git status returned status %d (failed to read body: %w)", resp.StatusCode, readErr)
		}
		return nil, fmt.Errorf("git status returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var result GitStatusResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	return &result, nil
}

// SDLCRequest is the request to run an SDLC command.
type SDLCRequest struct {
	Command string   `json:"command"`
	Args    []string `json:"args,omitempty"`
	WorkDir string   `json:"work_dir,omitempty"`
}

// SDLCResponse is the response from running an SDLC command.
type SDLCResponse struct {
	Success bool            `json:"success"`
	Output  string          `json:"output"`
	Data    json.RawMessage `json:"data,omitempty"`
	Error   string          `json:"error,omitempty"`
}

// RunSDLC executes an SDLC CLI command.
func (c *Client) RunSDLC(ctx context.Context, command string, args []string, workDir string) (*SDLCResponse, error) {
	req := SDLCRequest{
		Command: command,
		Args:    args,
		WorkDir: workDir,
	}

	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/sdlc", bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("sdlc: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return nil, fmt.Errorf("sdlc returned status %d (failed to read body: %w)", resp.StatusCode, readErr)
		}
		return nil, fmt.Errorf("sdlc returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var result SDLCResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	return &result, nil
}
