package lmstudio

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/danielmiessler/fabric/internal/chat"

	"github.com/danielmiessler/fabric/internal/domain"
	"github.com/danielmiessler/fabric/internal/plugins"
)

// NewClient creates a new LM Studio client with default configuration.
func NewClient() (ret *Client) {
	return NewClientCompatible("LM Studio", "http://localhost:1234/v1", nil)
}

// NewClientCompatible creates a new LM Studio client with custom configuration.
func NewClientCompatible(vendorName string, defaultBaseUrl string, configureCustom func() error) (ret *Client) {
	ret = &Client{}

	if configureCustom == nil {
		configureCustom = ret.configure
	}
	ret.PluginBase = plugins.NewVendorPluginBase(vendorName, configureCustom)
	ret.ApiUrl = ret.AddSetupQuestionCustom("API URL", true,
		fmt.Sprintf("Enter your %v URL (as a reminder, it is usually %v')", vendorName, defaultBaseUrl))
	return
}

// Client represents the LM Studio client.
type Client struct {
	*plugins.PluginBase
	ApiUrl     *plugins.SetupQuestion
	HttpClient *http.Client
}

// configure sets up the HTTP client.
func (c *Client) configure() error {
	c.HttpClient = &http.Client{}
	return nil
}

// ListModels returns a list of available models.
func (c *Client) ListModels() ([]string, error) {
	url := fmt.Sprintf("%s/models", c.ApiUrl.Value)

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

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

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	var result struct {
		Data []struct {
			ID string `json:"id"`
		} `json:"data"`
	}

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

	models := make([]string, len(result.Data))
	for i, model := range result.Data {
		models[i] = model.ID
	}

	return models, nil
}

func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions, channel chan domain.StreamUpdate) (err error) {
	url := fmt.Sprintf("%s/chat/completions", c.ApiUrl.Value)

	payload := map[string]any{
		"messages": msgs,
		"model":    opts.Model,
		"stream":   true, // Enable streaming
		"stream_options": map[string]any{
			"include_usage": true,
		},
	}

	var jsonPayload []byte
	if jsonPayload, err = json.Marshal(payload); err != nil {
		err = fmt.Errorf("failed to marshal payload: %w", err)
		return
	}

	var req *http.Request
	if req, err = http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload)); err != nil {
		err = fmt.Errorf("failed to create request: %w", err)
		return
	}

	req.Header.Set("Content-Type", "application/json")

	var resp *http.Response
	if resp, err = c.HttpClient.Do(req); err != nil {
		err = fmt.Errorf("failed to send request: %w", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
		return
	}

	defer close(channel)

	reader := bufio.NewReader(resp.Body)
	for {
		var line []byte
		if line, err = reader.ReadBytes('\n'); err != nil {
			if err == io.EOF {
				err = nil
				break
			}
			err = fmt.Errorf("error reading response: %w", err)
			return
		}

		if len(line) == 0 {
			continue
		}

		if after, ok := bytes.CutPrefix(line, []byte("data: ")); ok {
			line = after
		}

		if string(bytes.TrimSpace(line)) == "[DONE]" {
			break
		}

		var result map[string]any
		if err = json.Unmarshal(line, &result); err != nil {
			continue
		}

		// Handle Usage
		if usage, ok := result["usage"].(map[string]any); ok {
			var metadata domain.UsageMetadata
			if val, ok := usage["prompt_tokens"].(float64); ok {
				metadata.InputTokens = int(val)
			}
			if val, ok := usage["completion_tokens"].(float64); ok {
				metadata.OutputTokens = int(val)
			}
			if val, ok := usage["total_tokens"].(float64); ok {
				metadata.TotalTokens = int(val)
			}
			channel <- domain.StreamUpdate{
				Type:  domain.StreamTypeUsage,
				Usage: &metadata,
			}
		}

		var choices []any
		var ok bool
		if choices, ok = result["choices"].([]any); !ok || len(choices) == 0 {
			continue
		}

		var delta map[string]any
		if delta, ok = choices[0].(map[string]any)["delta"].(map[string]any); !ok {
			continue
		}

		var content string
		if content, _ = delta["content"].(string); content != "" {
			channel <- domain.StreamUpdate{
				Type:    domain.StreamTypeContent,
				Content: content,
			}
		}
	}

	return
}

func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions) (content string, err error) {
	url := fmt.Sprintf("%s/chat/completions", c.ApiUrl.Value)

	payload := map[string]any{
		"messages": msgs,
		"model":    opts.Model,
		// Add other options from opts if supported by LM Studio
	}

	var jsonPayload []byte
	if jsonPayload, err = json.Marshal(payload); err != nil {
		err = fmt.Errorf("failed to marshal payload: %w", err)
		return
	}

	var req *http.Request
	if req, err = http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonPayload)); err != nil {
		err = fmt.Errorf("failed to create request: %w", err)
		return
	}

	req.Header.Set("Content-Type", "application/json")

	var resp *http.Response
	if resp, err = c.HttpClient.Do(req); err != nil {
		err = fmt.Errorf("failed to send request: %w", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
		return
	}

	var result map[string]any
	if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
		err = fmt.Errorf("failed to decode response: %w", err)
		return
	}

	var choices []any
	var ok bool
	if choices, ok = result["choices"].([]any); !ok || len(choices) == 0 {
		err = fmt.Errorf("invalid response format: missing or empty choices")
		return
	}

	var message map[string]any
	if message, ok = choices[0].(map[string]any)["message"].(map[string]any); !ok {
		err = fmt.Errorf("invalid response format: missing message in first choice")
		return
	}

	if content, ok = message["content"].(string); !ok {
		err = fmt.Errorf("invalid response format: missing or non-string content in message")
		return
	}

	return
}

func (c *Client) Complete(ctx context.Context, prompt string, opts *domain.ChatOptions) (text string, err error) {
	url := fmt.Sprintf("%s/completions", c.ApiUrl.Value)

	payload := map[string]any{
		"prompt": prompt,
		"model":  opts.Model,
		// Add other options from opts if supported by LM Studio
	}

	var jsonPayload []byte
	if jsonPayload, err = json.Marshal(payload); err != nil {
		err = fmt.Errorf("failed to marshal payload: %w", err)
		return
	}

	var req *http.Request
	if req, err = http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonPayload)); err != nil {
		err = fmt.Errorf("failed to create request: %w", err)
		return
	}

	req.Header.Set("Content-Type", "application/json")

	var resp *http.Response
	if resp, err = c.HttpClient.Do(req); err != nil {
		err = fmt.Errorf("failed to send request: %w", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
		return
	}

	var result map[string]any
	if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
		err = fmt.Errorf("failed to decode response: %w", err)
		return
	}

	var choices []any
	var ok bool
	if choices, ok = result["choices"].([]any); !ok || len(choices) == 0 {
		err = fmt.Errorf("invalid response format: missing or empty choices")
		return
	}

	if text, ok = choices[0].(map[string]any)["text"].(string); !ok {
		err = fmt.Errorf("invalid response format: missing or non-string text in first choice")
		return
	}

	return
}

func (c *Client) GetEmbeddings(ctx context.Context, input string, opts *domain.ChatOptions) (embeddings []float64, err error) {
	url := fmt.Sprintf("%s/embeddings", c.ApiUrl.Value)

	payload := map[string]any{
		"input": input,
		"model": opts.Model,
		// Add other options from opts if supported by LM Studio
	}

	var jsonPayload []byte
	if jsonPayload, err = json.Marshal(payload); err != nil {
		err = fmt.Errorf("failed to marshal payload: %w", err)
		return
	}

	var req *http.Request
	if req, err = http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonPayload)); err != nil {
		err = fmt.Errorf("failed to create request: %w", err)
		return
	}

	req.Header.Set("Content-Type", "application/json")

	var resp *http.Response
	if resp, err = c.HttpClient.Do(req); err != nil {
		err = fmt.Errorf("failed to send request: %w", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
		return
	}

	var result struct {
		Data []struct {
			Embedding []float64 `json:"embedding"`
		} `json:"data"`
	}

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

	if len(result.Data) == 0 {
		err = fmt.Errorf("no embeddings returned")
		return
	}

	embeddings = result.Data[0].Embedding
	return
}

func (c *Client) NeedsRawMode(modelName string) bool {
	return false
}
