package discovery

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"sync"
	"time"
)

// Kind describes a known server type.
type Kind struct {
	Name     string
	APIType  string
	Port     int
	NeedsKey bool
}

// Found is a server discovered on the network.
type Found struct {
	Kind    Kind
	BaseURL string
	Models  []string
}

// knownServers lists server types by their default port and display name.
// Ollama is listed separately because it uses a non-OpenAI probe endpoint.
var knownServers = []Kind{
	{Name: "Ollama", APIType: "openai", Port: 11434},
	{Name: "LM Studio", APIType: "openai", Port: 1234},
	{Name: "vLLM", APIType: "openai", Port: 8000, NeedsKey: true},
	{Name: "LocalAI", APIType: "openai", Port: 8080},
	{Name: "Jan", APIType: "openai", Port: 1337},
	{Name: "Kobold", APIType: "openai", Port: 5001},
	{Name: "Tabby", APIType: "openai", Port: 9090},
}

// Scan concurrently probes localhost and LAN gateway addresses for known LLM servers.
// Results are returned in the order they are found (non-deterministic).
func Scan(ctx context.Context) []Found {
	hosts := localHosts()

	var (
		mu      sync.Mutex
		results []Found
		wg      sync.WaitGroup
	)

	probeCtx, cancel := context.WithTimeout(ctx, 4*time.Second)
	defer cancel()

	httpClient := &http.Client{Timeout: 600 * time.Millisecond}

	for _, host := range hosts {
		for _, kind := range knownServers {
			host := host
			wg.Add(1)
			go func(kind Kind) {
				defer wg.Done()
				baseURL := fmt.Sprintf("http://%s:%d", host, kind.Port)
				models, err := probe(probeCtx, httpClient, baseURL, kind)
				if err != nil {
					return
				}
				mu.Lock()
				results = append(results, Found{Kind: kind, BaseURL: baseURL, Models: models})
				mu.Unlock()
			}(kind)
		}
	}

	wg.Wait()
	return results
}

// Models fetches the model list from a single base URL and kind (for the models command).
func Models(ctx context.Context, baseURL string, kind Kind) ([]string, error) {
	httpClient := &http.Client{Timeout: 5 * time.Second}
	return probe(ctx, httpClient, baseURL, kind)
}

// localHosts returns localhost plus the .1 gateway of every local IPv4 subnet.
func localHosts() []string {
	seen := map[string]bool{"127.0.0.1": true}
	hosts := []string{"127.0.0.1"}

	ifaces, err := net.Interfaces()
	if err != nil {
		return hosts
	}

	for _, iface := range ifaces {
		if iface.Flags&net.FlagUp == 0 {
			continue
		}
		addrs, err := iface.Addrs()
		if err != nil {
			continue
		}
		for _, addr := range addrs {
			ipnet, ok := addr.(*net.IPNet)
			if !ok {
				continue
			}
			ip := ipnet.IP.To4()
			if ip == nil || ip.IsLoopback() {
				continue
			}
			// derive the likely gateway: network address + 1
			gw := ip.Mask(ipnet.Mask)
			gw[3] = 1
			h := gw.String()
			if !seen[h] {
				seen[h] = true
				hosts = append(hosts, h)
			}
		}
	}
	return hosts
}

// probe attempts to identify the server at baseURL and returns its model list.
func probe(ctx context.Context, client *http.Client, baseURL string, kind Kind) ([]string, error) {
	// Ollama has its own endpoint; everything else is OpenAI-compatible
	if kind.Name == "Ollama" {
		models, err := probeOllama(ctx, client, baseURL)
		if err != nil {
			// Ollama also exposes /v1/models since v0.1.27 — fall back
			return probeOpenAI(ctx, client, baseURL)
		}
		return models, nil
	}
	return probeOpenAI(ctx, client, baseURL)
}

// --- Ollama ---

type ollamaTagsResponse struct {
	Models []struct {
		Name string `json:"name"`
	} `json:"models"`
}

func probeOllama(ctx context.Context, client *http.Client, baseURL string) ([]string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/api/tags", nil)
	if err != nil {
		return nil, err
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()

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

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

	models := make([]string, len(body.Models))
	for i, m := range body.Models {
		models[i] = m.Name
	}
	return models, nil
}

// --- OpenAI-compatible ---

type openAIModelsResponse struct {
	Data []struct {
		ID string `json:"id"`
	} `json:"data"`
}

func probeOpenAI(ctx context.Context, client *http.Client, baseURL string) ([]string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/v1/models", nil)
	if err != nil {
		return nil, err
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()

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

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

	models := make([]string, len(body.Data))
	for i, m := range body.Data {
		models[i] = m.ID
	}
	return models, nil
}
