package provider

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"sort"
	"strings"
	"time"
)

const (
	ModelFetchReasonInvalidBaseURL = "invalid_base_url"
	ModelFetchReasonUnreachable    = "base_url_unreachable"
	ModelFetchReasonAPIKeyRejected = "api_key_rejected"
	ModelFetchReasonRequestFailed  = "models_request_failed"
)

// ModelFetchError classifies failures from provider model-listing requests so
// dashboard validation can show stable reason-specific guidance.
type ModelFetchError struct {
	ReasonCode string
	Message    string
	Err        error
}

// Error returns the user-facing model-fetch failure message.
func (e *ModelFetchError) Error() string {
	return e.Message
}

// Unwrap returns the underlying transport or parsing error, when present.
func (e *ModelFetchError) Unwrap() error {
	return e.Err
}

// ModelFetchReasonCode extracts the stable dashboard reason code from err.
func ModelFetchReasonCode(err error) string {
	if err == nil {
		return ""
	}
	var modelErr *ModelFetchError
	if errors.As(err, &modelErr) {
		return modelErr.ReasonCode
	}
	return ""
}

// modelsResponse is the standard model-listing response shape.
type modelsResponse struct {
	Data []modelEntry `json:"data"`
}

type modelEntry struct {
	ID string `json:"id"`
}

var modelsHTTPClient = &http.Client{Timeout: 15 * time.Second}

// FetchModels calls the provider's model-listing endpoint and returns a
// sorted, deduplicated list of model IDs.
// Returns an error if the provider has no usable base URL or API key.
func FetchModels(p Provider) ([]string, error) {
	if p.APIKey == "" {
		return nil, &ModelFetchError{
			ReasonCode: ModelFetchReasonAPIKeyRejected,
			Message:    "API key is required before detecting models.",
		}
	}

	url, headers, err := modelsEndpoint(p)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return nil, &ModelFetchError{
			ReasonCode: ModelFetchReasonInvalidBaseURL,
			Message:    "Base URL is invalid. Include http:// or https:// and try again.",
			Err:        err,
		}
	}
	for k, v := range headers {
		req.Header.Set(k, v)
	}

	resp, err := modelsHTTPClient.Do(req)
	if err != nil {
		return nil, &ModelFetchError{
			ReasonCode: ModelFetchReasonUnreachable,
			Message:    "Cannot reach Base URL. Check the address and network, then retry.",
			Err:        err,
		}
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusMethodNotAllowed {
		return nil, nil
	}
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return nil, &ModelFetchError{
			ReasonCode: ModelFetchReasonAPIKeyRejected,
			Message:    "API key was rejected. Check the key and try again.",
		}
	}
	if resp.StatusCode != http.StatusOK {
		return nil, &ModelFetchError{
			ReasonCode: ModelFetchReasonRequestFailed,
			Message:    fmt.Sprintf("Model detection failed with HTTP %d. Check the Base URL and try again.", resp.StatusCode),
		}
	}

	var result modelsResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, nil
	}

	seen := make(map[string]bool, len(result.Data))
	models := make([]string, 0, len(result.Data))
	for _, m := range result.Data {
		id := strings.TrimSpace(m.ID)
		if id != "" && !seen[id] {
			seen[id] = true
			models = append(models, id)
		}
	}
	sort.Strings(models)
	return models, nil
}

// modelsEndpoint returns the URL and HTTP headers needed to list models
// for the given provider. OpenAI-compatible runtime config treats BaseURL as
// the API prefix entered by the user and appends /models to it.
func modelsEndpoint(p Provider) (string, map[string]string, error) {
	headers := make(map[string]string)
	base := p.ResolvedBaseURL()
	if base == "" {
		return "", nil, &ModelFetchError{
			ReasonCode: ModelFetchReasonInvalidBaseURL,
			Message:    "Base URL is required before detecting models.",
		}
	}

	for _, w := range p.WireAPIs {
		switch w {
		case WireAPIAnthropic:
			headers["x-api-key"] = p.APIKey
			headers["anthropic-version"] = "2023-06-01"
			return base + "/v1/models", headers, nil
		case WireAIOpenAI, WireAPIResponses:
			headers["Authorization"] = "Bearer " + p.APIKey
			return base + "/models", headers, nil
		}
	}

	// Fallback to OpenAI-compatible.
	headers["Authorization"] = "Bearer " + p.APIKey
	return base + "/models", headers, nil
}
