package rag

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"time"

	"gitcode-mcp/internal/config"
)

type Runtime interface {
	LookPath(string) (string, error)
	IsLive(context.Context, string, time.Duration) (bool, string)
	ListModels(context.Context, string, time.Duration) ([]string, error)
	PullModel(context.Context, string, string, time.Duration) error
	EmbeddingSmoke(context.Context, string, string, time.Duration) error
	Start(context.Context, config.RAGProviderConfig) (string, error)
}

type OSRuntime struct{}

const minimumModelPullTimeout = 30 * time.Minute

type SetupProgress struct {
	Phase   string `json:"phase"`
	Message string `json:"message"`
	Model   string `json:"model,omitempty"`
}

type SetupRequest struct {
	Config   config.Config
	Profile  string
	Yes      bool
	DryRun   bool
	Runtime  Runtime
	Progress func(SetupProgress)
}

type SetupResult struct {
	Status              string   `json:"status"`
	Profile             string   `json:"profile"`
	Provider            string   `json:"provider"`
	ProviderType        string   `json:"provider_type"`
	Endpoint            string   `json:"endpoint"`
	Executable          string   `json:"executable,omitempty"`
	ExecutablePath      string   `json:"executable_path,omitempty"`
	Autostart           bool     `json:"autostart"`
	Model               string   `json:"model"`
	ModelAvailable      bool     `json:"model_available"`
	ModelStorePath      string   `json:"model_store_path,omitempty"`
	ProviderModelEnv    string   `json:"provider_model_env,omitempty"`
	ProviderModelPath   string   `json:"provider_model_path,omitempty"`
	ProviderInstalled   bool     `json:"provider_installed"`
	ProviderLive        bool     `json:"provider_live"`
	PullAttempted       bool     `json:"pull_attempted"`
	EmbeddingSmoke      string   `json:"embedding_smoke"`
	Actions             []string `json:"actions,omitempty"`
	NextActions         []string `json:"next_actions,omitempty"`
	Diagnostics         []string `json:"diagnostics,omitempty"`
	InstallInstructions []string `json:"install_instructions,omitempty"`
}

func Setup(ctx context.Context, req SetupRequest) (SetupResult, error) {
	runtime := req.Runtime
	if runtime == nil {
		runtime = OSRuntime{}
	}
	profileName := strings.TrimSpace(req.Profile)
	if profileName == "" {
		profileName = strings.TrimSpace(req.Config.RAG.DefaultProfile)
	}
	if profileName == "" {
		profileName = config.DefaultRAGProfile
	}
	profile, ok := req.Config.RAG.Profiles[profileName]
	if !ok {
		return SetupResult{}, fmt.Errorf("rag setup: profile %q is not configured", profileName)
	}
	providerName := strings.TrimSpace(profile.Provider)
	if providerName == "" {
		return SetupResult{}, fmt.Errorf("rag setup: profile %q has no provider", profileName)
	}
	provider, ok := req.Config.RAG.Providers[providerName]
	if !ok {
		return SetupResult{}, fmt.Errorf("rag setup: provider %q is not configured", providerName)
	}
	timeout := provider.Timeout
	if timeout <= 0 {
		timeout = 30 * time.Second
	}
	result := SetupResult{
		Status:              "checking",
		Profile:             profileName,
		Provider:            providerName,
		ProviderType:        firstNonEmpty(provider.Type, providerName),
		Endpoint:            provider.Endpoint,
		Executable:          provider.Executable,
		Autostart:           provider.Autostart,
		Model:               profile.Model,
		ModelStorePath:      req.Config.RAG.ModelStorePath,
		ProviderModelEnv:    provider.ModelStorage.Env,
		ProviderModelPath:   providerModelPath(provider),
		EmbeddingSmoke:      "skipped",
		InstallInstructions: append([]string(nil), provider.InstallHints...),
	}
	executable := strings.TrimSpace(provider.Executable)
	if executable != "" {
		path, err := runtime.LookPath(executable)
		if err == nil && strings.TrimSpace(path) != "" {
			result.ProviderInstalled = true
			result.ExecutablePath = path
		} else {
			result.Diagnostics = append(result.Diagnostics, "provider executable not found: "+executable)
			result.Actions = append(result.Actions, "install provider runtime")
			result.Status = "missing_provider"
			return result, nil
		}
	}
	live, liveMessage := runtime.IsLive(ctx, provider.Endpoint, timeout)
	result.ProviderLive = live
	if !live {
		if liveMessage != "" {
			result.Diagnostics = append(result.Diagnostics, liveMessage)
		}
		if provider.Autostart && providerStartupManaged(provider) {
			if req.DryRun {
				result.Actions = append(result.Actions, "start provider runtime")
				result.Status = "provider_not_running"
				return result, nil
			}
			startMessage, err := runtime.Start(ctx, provider)
			if err != nil {
				result.Diagnostics = append(result.Diagnostics, "provider autostart failed: "+err.Error())
				result.Actions = append(result.Actions, "start provider runtime")
				result.Status = "provider_not_running"
				return result, nil
			}
			if startMessage != "" {
				result.Diagnostics = append(result.Diagnostics, startMessage)
			}
			live, liveMessage = runtime.IsLive(ctx, provider.Endpoint, timeout)
			result.ProviderLive = live
		}
	}
	if !result.ProviderLive {
		if liveMessage != "" {
			result.Diagnostics = append(result.Diagnostics, liveMessage)
		}
		result.Actions = append(result.Actions, "start provider runtime")
		result.Status = "provider_not_running"
		return result, nil
	}
	models, err := runtime.ListModels(ctx, provider.Endpoint, timeout)
	if err != nil {
		result.Diagnostics = append(result.Diagnostics, "model list failed: "+err.Error())
		result.Status = "provider_error"
		return result, nil
	}
	result.ModelAvailable = containsModel(models, profile.Model)
	if !result.ModelAvailable {
		result.Actions = append(result.Actions,
			"run gitcode-mcp rag setup --yes to pull model "+profile.Model,
			"or run ollama pull "+profile.Model,
		)
		if req.DryRun || !req.Yes {
			result.Status = "missing_model"
			return result, nil
		}
		result.PullAttempted = true
		emitSetupProgress(req.Progress, SetupProgress{
			Phase:   "model_pull_started",
			Message: "pulling model; this can take several minutes",
			Model:   profile.Model,
		})
		pullTimeout := timeout
		if pullTimeout < minimumModelPullTimeout {
			pullTimeout = minimumModelPullTimeout
		}
		if pullErr := runtime.PullModel(ctx, provider.Endpoint, profile.Model, pullTimeout); pullErr != nil {
			// Ollama may continue a pull after the initiating HTTP request is
			// interrupted. Re-check provider state before reporting a failure so
			// a completed download is not misclassified as failed.
			models, listErr := runtime.ListModels(ctx, provider.Endpoint, timeout)
			if listErr == nil && containsModel(models, profile.Model) {
				result.ModelAvailable = true
				result.Diagnostics = append(result.Diagnostics, "model became available after pull transport ended: "+pullErr.Error())
			} else {
				result.Diagnostics = append(result.Diagnostics, "model pull failed: "+pullErr.Error())
				if listErr != nil {
					result.Diagnostics = append(result.Diagnostics, "model verification after pull failed: "+listErr.Error())
				}
				result.Status = "model_pull_failed"
				return result, nil
			}
		}
		if !result.ModelAvailable {
			models, err = runtime.ListModels(ctx, provider.Endpoint, timeout)
			if err != nil {
				result.Diagnostics = append(result.Diagnostics, "model list after pull failed: "+err.Error())
				result.Status = "provider_error"
				return result, nil
			}
			result.ModelAvailable = containsModel(models, profile.Model)
		}
		emitSetupProgress(req.Progress, SetupProgress{Phase: "model_pull_finished", Message: "model pull finished", Model: profile.Model})
	}
	if !result.ModelAvailable {
		result.Status = "missing_model"
		return result, nil
	}
	if req.DryRun {
		result.Status = "ready"
		return result, nil
	}
	if err := runtime.EmbeddingSmoke(ctx, provider.Endpoint, profile.Model, timeout); err != nil {
		result.EmbeddingSmoke = "failed"
		result.Diagnostics = append(result.Diagnostics, "embedding smoke failed: "+err.Error())
		result.Status = "smoke_failed"
		return result, nil
	}
	result.EmbeddingSmoke = "ok"
	result.Status = "ready"
	result.NextActions = []string{
		"gitcode-mcp rag status --repo OWNER/REPO",
		"gitcode-mcp rag index --repo OWNER/REPO",
		"gitcode-mcp rag search --repo OWNER/REPO QUERY",
	}
	return result, nil
}

func emitSetupProgress(progress func(SetupProgress), event SetupProgress) {
	if progress != nil {
		progress(event)
	}
}

func (OSRuntime) LookPath(executable string) (string, error) {
	return exec.LookPath(executable)
}

func (OSRuntime) IsLive(ctx context.Context, endpoint string, timeout time.Duration) (bool, string) {
	if strings.TrimSpace(endpoint) == "" {
		return false, "provider endpoint is not configured"
	}
	var payload map[string]any
	err := getJSON(ctx, endpoint+"/api/tags", timeout, &payload)
	if err != nil {
		return false, "provider endpoint is not reachable: " + err.Error()
	}
	return true, ""
}

func (OSRuntime) ListModels(ctx context.Context, endpoint string, timeout time.Duration) ([]string, error) {
	var payload struct {
		Models []struct {
			Name string `json:"name"`
		} `json:"models"`
	}
	if err := getJSON(ctx, endpoint+"/api/tags", timeout, &payload); err != nil {
		return nil, err
	}
	models := make([]string, 0, len(payload.Models))
	for _, model := range payload.Models {
		if strings.TrimSpace(model.Name) != "" {
			models = append(models, strings.TrimSpace(model.Name))
		}
	}
	return models, nil
}

func (OSRuntime) PullModel(ctx context.Context, endpoint, model string, timeout time.Duration) error {
	body := map[string]any{"name": model, "stream": false}
	return postJSON(ctx, endpoint+"/api/pull", timeout, body, nil)
}

func (OSRuntime) EmbeddingSmoke(ctx context.Context, endpoint, model string, timeout time.Duration) error {
	body := map[string]any{"model": model, "prompt": "gitcode-mcp readiness"}
	var payload struct {
		Embedding []float64 `json:"embedding"`
	}
	if err := postJSON(ctx, endpoint+"/api/embeddings", timeout, body, &payload); err != nil {
		return err
	}
	if len(payload.Embedding) == 0 {
		return errors.New("empty embedding")
	}
	return nil
}

func (OSRuntime) Start(ctx context.Context, provider config.RAGProviderConfig) (string, error) {
	executable := strings.TrimSpace(provider.Executable)
	if executable == "" {
		return "", errors.New("provider executable is not configured")
	}
	cmd := exec.CommandContext(ctx, executable, "serve")
	cmd.Stdout = io.Discard
	cmd.Stderr = io.Discard
	cmd.Env = os.Environ()
	for key, value := range provider.Env {
		if strings.TrimSpace(key) != "" {
			cmd.Env = append(cmd.Env, strings.TrimSpace(key)+"="+value)
		}
	}
	if strings.TrimSpace(provider.ModelStorage.Env) != "" && strings.TrimSpace(provider.ModelStorage.Path) != "" {
		cmd.Env = append(cmd.Env, strings.TrimSpace(provider.ModelStorage.Env)+"="+strings.TrimSpace(provider.ModelStorage.Path))
	}
	if err := cmd.Start(); err != nil {
		return "", err
	}
	return fmt.Sprintf("started provider process pid=%d", cmd.Process.Pid), nil
}

func getJSON(ctx context.Context, url string, timeout time.Duration, target any) error {
	client := &http.Client{Timeout: timeout}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("status %d", resp.StatusCode)
	}
	return json.NewDecoder(resp.Body).Decode(target)
}

func postJSON(ctx context.Context, url string, timeout time.Duration, body any, target any) error {
	data, err := json.Marshal(body)
	if err != nil {
		return err
	}
	client := &http.Client{Timeout: timeout}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(data))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("status %d", resp.StatusCode)
	}
	if target == nil {
		io.Copy(io.Discard, resp.Body)
		return nil
	}
	return json.NewDecoder(resp.Body).Decode(target)
}

func containsModel(models []string, want string) bool {
	want = strings.TrimSpace(want)
	for _, model := range models {
		if strings.TrimSpace(model) == want {
			return true
		}
	}
	return false
}

func providerModelPath(provider config.RAGProviderConfig) string {
	if strings.TrimSpace(provider.ModelStorage.Path) != "" {
		return strings.TrimSpace(provider.ModelStorage.Path)
	}
	if provider.ModelStorage.Env == "" {
		return ""
	}
	return provider.Env[provider.ModelStorage.Env]
}

func providerStartupManaged(provider config.RAGProviderConfig) bool {
	startup := strings.TrimSpace(provider.Startup)
	return startup == "" || startup == "managed"
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}