// Package laozhang provides a Go client for the LaoZhang API gateway.
//
// LaoZhang is an OpenAI-compatible API gateway that provides access to various
// AI models including chat completion, image generation (Nano Banana Pro), and
// video generation (Veo 3.1).
//
// Basic usage:
//
//	client, err := laozhang.NewClient(laozhang.Config{
//	    APIKey: os.Getenv("LAOZHANG_API_KEY"),
//	})
//	if err != nil {
//	    log.Fatal(err)
//	}
//
//	// Generate an image
//	resp, err := client.GenerateImage(ctx, laozhang.ImageRequest{
//	    Prompt: "A serene Japanese garden",
//	})
//
// The client automatically handles retries for server errors (5xx) and rate
// limits (429) with exponential backoff. Use the sentinel errors (ErrRateLimit,
// ErrServerError, etc.) with errors.Is() for programmatic error handling.
package laozhang

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

	"git.threesix.ai/jordan/persona-community-5/pkg/httpclient"
)

const (
	defaultBaseURL      = "https://api.laozhang.ai/v1"
	defaultTimeout      = 60 * time.Second
	defaultVideoTimeout = 5 * time.Minute
	defaultMaxRetries   = 3
)

// Config holds configuration options for the LaoZhang client
type Config struct {
	APIKey       string        // Required: API key for authentication
	BaseURL      string        // Optional: defaults to https://api.laozhang.ai/v1
	Timeout      time.Duration // Optional: defaults to 60s
	VideoTimeout time.Duration // Optional: defaults to 5m (video generation takes 2-5 minutes)
	MaxRetries   int           // Optional: defaults to 3
	Logger       *slog.Logger  // Optional: defaults to slog.Default()
}

// Client is the LaoZhang API client
type Client struct {
	httpClient      *httpclient.Client // Standard timeout (60s) for text/image
	videoHTTPClient *httpclient.Client // Long timeout (5m) for video generation
	config          *Config
	logger          *slog.Logger
}

// NewClient creates a new LaoZhang API client
func NewClient(config Config) (*Client, error) {
	if config.APIKey == "" {
		return nil, fmt.Errorf("%w: API key is required", ErrInvalidConfig)
	}

	if config.BaseURL == "" {
		config.BaseURL = defaultBaseURL
	}

	if config.Timeout == 0 {
		config.Timeout = defaultTimeout
	}

	if config.VideoTimeout == 0 {
		config.VideoTimeout = defaultVideoTimeout
	}

	if config.MaxRetries == 0 {
		config.MaxRetries = defaultMaxRetries
	}

	if config.Logger == nil {
		config.Logger = slog.Default()
	}

	// Validate base URL
	if _, err := url.Parse(config.BaseURL); err != nil {
		return nil, fmt.Errorf("%w: invalid base URL: %v", ErrInvalidConfig, err)
	}

	return &Client{
		httpClient: httpclient.New(httpclient.Config{
			Timeout:    config.Timeout,
			MaxRetries: config.MaxRetries,
			Logger:     config.Logger,
		}),
		videoHTTPClient: httpclient.New(httpclient.Config{
			Timeout:    config.VideoTimeout,
			MaxRetries: config.MaxRetries,
			Logger:     config.Logger,
		}),
		config: &config,
		logger: config.Logger,
	}, nil
}

// Health checks the health of the LaoZhang API
func (c *Client) Health(ctx context.Context) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.config.BaseURL+"/models", nil)
	if err != nil {
		return fmt.Errorf("create health check request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)

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

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		baseErr := classifyHTTPError(resp.StatusCode)
		return NewAPIError(resp.StatusCode, string(body), "", "", baseErr)
	}

	return nil
}

// ChatCompletion sends a chat completion request to the LaoZhang API
func (c *Client) ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
	if req.Model == "" {
		return nil, fmt.Errorf("%w: model is required", ErrInvalidConfig)
	}
	if len(req.Messages) == 0 {
		return nil, fmt.Errorf("%w: messages are required", ErrInvalidConfig)
	}

	respBody, err := c.doRequest(ctx, http.MethodPost, "/chat/completions", req)
	if err != nil {
		return nil, err
	}

	var chatResp ChatCompletionResponse
	if err := json.Unmarshal(respBody, &chatResp); err != nil {
		return nil, fmt.Errorf("unmarshal response: %w", err)
	}

	return &chatResp, nil
}

// ChatCompletionStream sends a streaming chat completion request.
// Returns chunks via the onChunk callback as tokens arrive from the API.
// The request is sent with stream=true and the response body is read as SSE.
func (c *Client) ChatCompletionStream(ctx context.Context, req ChatCompletionRequest, onChunk func(StreamChunk)) error {
	if req.Model == "" {
		return fmt.Errorf("%w: model is required", ErrInvalidConfig)
	}
	if len(req.Messages) == 0 {
		return fmt.Errorf("%w: messages are required", ErrInvalidConfig)
	}

	req.Stream = true

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

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.config.BaseURL+"/chat/completions", bytes.NewReader(jsonBody))
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}
	httpReq.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	httpReq.Header.Set("Content-Type", "application/json")

	// Use a raw http.Client with timeout for streaming (httpClient.Do may buffer)
	rawClient := &http.Client{Timeout: c.config.Timeout}
	resp, err := rawClient.Do(httpReq)
	if err != nil {
		return fmt.Errorf("stream request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		baseErr := classifyHTTPError(resp.StatusCode)
		return NewAPIError(resp.StatusCode, string(body), "", "", baseErr)
	}

	// Read SSE stream line by line
	scanner := bufio.NewScanner(resp.Body)
	for scanner.Scan() {
		line := scanner.Text()

		// Skip empty lines and comments
		if line == "" || line[0] == ':' {
			continue
		}

		// Parse SSE data lines
		if !strings.HasPrefix(line, "data: ") {
			continue
		}
		data := strings.TrimPrefix(line, "data: ")

		// [DONE] marks the end of the stream
		if data == "[DONE]" {
			onChunk(StreamChunk{Done: true})
			return nil
		}

		var chunk streamResponse
		if err := json.Unmarshal([]byte(data), &chunk); err != nil {
			c.logger.Debug("skipping unparseable stream chunk", "error", err)
			continue
		}

		if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
			onChunk(StreamChunk{Text: chunk.Choices[0].Delta.Content})
		}
	}

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

	// If we reach here without [DONE], send a final chunk
	onChunk(StreamChunk{Done: true})
	return nil
}

// StreamChunk represents a chunk from a streaming response.
type StreamChunk struct {
	Text string
	Done bool
}

// streamResponse is the JSON structure for streaming chat completion chunks.
type streamResponse struct {
	Choices []streamChoice `json:"choices"`
}

type streamChoice struct {
	Delta struct {
		Content string `json:"content"`
	} `json:"delta"`
	FinishReason *string `json:"finish_reason"`
}

// doRequest is a helper method for making HTTP requests
func (c *Client) doRequest(ctx context.Context, method, path string, bodyData interface{}) ([]byte, error) {
	var reqBody io.Reader
	if bodyData != nil {
		jsonBody, err := json.Marshal(bodyData)
		if err != nil {
			return nil, fmt.Errorf("marshal request: %w", err)
		}
		reqBody = bytes.NewReader(jsonBody)
	}

	req, err := http.NewRequestWithContext(ctx, method, c.config.BaseURL+path, reqBody)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	if bodyData != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

	// Success response
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return respBody, nil
	}

	// Parse error response
	var errResp ErrorResponse
	baseErr := classifyHTTPError(resp.StatusCode)

	if err := json.Unmarshal(respBody, &errResp); err != nil {
		// Failed to parse error response
		return nil, NewAPIError(
			resp.StatusCode,
			string(respBody),
			"",
			"",
			baseErr,
		)
	}

	// Successfully parsed error response
	return nil, NewAPIError(
		resp.StatusCode,
		errResp.Error.Message,
		errResp.Error.Type,
		errResp.Error.Code,
		baseErr,
	)
}

// doRequestVideo is like doRequest but uses the video HTTP client with a longer timeout.
// Video generation (Veo) takes 2-5 minutes, exceeding the standard 60s client timeout.
func (c *Client) doRequestVideo(ctx context.Context, method, path string, bodyData interface{}) ([]byte, error) {
	var reqBody io.Reader
	if bodyData != nil {
		jsonBody, err := json.Marshal(bodyData)
		if err != nil {
			return nil, fmt.Errorf("marshal request: %w", err)
		}
		reqBody = bytes.NewReader(jsonBody)
	}

	req, err := http.NewRequestWithContext(ctx, method, c.config.BaseURL+path, reqBody)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	if bodyData != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := c.videoHTTPClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

	// Success response
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return respBody, nil
	}

	// Parse error response
	var errResp ErrorResponse
	baseErr := classifyHTTPError(resp.StatusCode)

	if err := json.Unmarshal(respBody, &errResp); err != nil {
		return nil, NewAPIError(
			resp.StatusCode,
			string(respBody),
			"",
			"",
			baseErr,
		)
	}

	return nil, NewAPIError(
		resp.StatusCode,
		errResp.Error.Message,
		errResp.Error.Type,
		errResp.Error.Code,
		baseErr,
	)
}

// geminiBaseURL returns the base URL for Gemini API endpoints (without /v1 suffix)
func (c *Client) geminiBaseURL() string {
	// Strip /v1 suffix if present to get the root URL for Gemini endpoints
	baseURL := c.config.BaseURL
	if len(baseURL) > 3 && baseURL[len(baseURL)-3:] == "/v1" {
		return baseURL[:len(baseURL)-3]
	}
	return baseURL
}

// doRequestGemini is similar to doRequest but uses the Gemini base URL format
func (c *Client) doRequestGemini(ctx context.Context, method, path string, bodyData interface{}) ([]byte, error) {
	var reqBody io.Reader
	if bodyData != nil {
		jsonBody, err := json.Marshal(bodyData)
		if err != nil {
			return nil, fmt.Errorf("marshal request: %w", err)
		}
		reqBody = bytes.NewReader(jsonBody)
	}

	// Use Gemini base URL (without /v1)
	fullURL := c.geminiBaseURL() + path
	req, err := http.NewRequestWithContext(ctx, method, fullURL, reqBody)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	if bodyData != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

	// Success response
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return respBody, nil
	}

	// Parse error response (Gemini format may differ)
	baseErr := classifyHTTPError(resp.StatusCode)
	return nil, NewAPIError(
		resp.StatusCode,
		string(respBody),
		"",
		"",
		baseErr,
	)
}
