// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package registry

import (
	"context"
	"errors"
	"fmt"
	"time"

	v0 "github.com/modelcontextprotocol/registry/pkg/api/v0"

	"github.com/stacklok/toolhive-core/registry/converters"
	types "github.com/stacklok/toolhive-core/registry/types"
	"github.com/stacklok/toolhive/pkg/registry/api"
	"github.com/stacklok/toolhive/pkg/registry/auth"
)

// APIRegistryProvider provides registry data from an MCP Registry API endpoint
// It queries the API on-demand for each operation, ensuring fresh data.
type APIRegistryProvider struct {
	*BaseProvider
	apiURL         string
	allowPrivateIp bool
	client         api.Client
	tokenSource    auth.TokenSource
	skillsClient   api.SkillsClient
}

// NewAPIRegistryProvider creates a new API registry provider.
// If tokenSource is non-nil, all API requests will include authentication.
func NewAPIRegistryProvider(apiURL string, allowPrivateIp bool, tokenSource auth.TokenSource) (*APIRegistryProvider, error) {
	// Create API client
	client, err := api.NewClient(apiURL, allowPrivateIp, tokenSource)
	if err != nil {
		return nil, fmt.Errorf("failed to create API client: %w", err)
	}

	// Create skills client (best-effort — skills API may not be available)
	skillsClient, _ := api.NewSkillsClient(apiURL, allowPrivateIp, tokenSource)

	p := &APIRegistryProvider{
		apiURL:         apiURL,
		allowPrivateIp: allowPrivateIp,
		client:         client,
		tokenSource:    tokenSource,
		skillsClient:   skillsClient,
	}

	// Initialize the base provider with the GetRegistry function
	p.BaseProvider = NewBaseProvider(p.GetRegistry)

	// Skip validation probe when auth is configured. The OAuth browser flow
	// requires user interaction which cannot complete within the validation timeout.
	// The endpoint will be validated on first real use instead.
	if tokenSource == nil {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		// Try to list servers with a small limit to verify API functionality
		_, err = client.ListServers(ctx, &api.ListOptions{Limit: 1})
		if err != nil {
			if errors.Is(err, api.ErrRegistryUnauthorized) {
				return nil, fmt.Errorf(
					"registry at %s returned 401 Unauthorized\n\n"+
						"If this registry requires authentication, configure it with:\n"+
						"  thv config set-registry <registry-url> --issuer <issuer-url> --client-id <client-id>: %w",
					apiURL, auth.ErrRegistryAuthRequired,
				)
			}
			return nil, &UnavailableError{URL: apiURL, Err: err}
		}
	}

	return p, nil
}

// GetRegistry returns the registry data by fetching all servers from the API
// This method queries the API and converts all servers to ToolHive format.
// Note: This can be slow for large registries as it fetches everything.
func (p *APIRegistryProvider) GetRegistry() (*types.Registry, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	// Fetch all servers from the API
	servers, err := p.client.ListServers(ctx, nil)
	if err != nil {
		// Propagate auth errors so API handlers can return structured responses.
		// ErrRegistryAuthRequired: no token available locally (never tried the registry).
		// ErrRegistryUnauthorized: token was sent but rejected by the registry (401/403).
		// Both are wrapped with ErrRegistryAuthRequired so the API layer returns 503.
		if errors.Is(err, auth.ErrRegistryAuthRequired) {
			return nil, fmt.Errorf("no registry credentials available: %w", err)
		}
		if errors.Is(err, api.ErrRegistryUnauthorized) {
			return nil, fmt.Errorf("registry rejected credentials: %w", auth.ErrRegistryAuthRequired)
		}
		return nil, &UnavailableError{URL: p.apiURL, Err: err}
	}

	// Convert servers to ToolHive format
	serverMetadata, err := ConvertServersToMetadata(servers)
	if err != nil {
		return nil, fmt.Errorf("failed to convert servers to ToolHive format: %w", err)
	}

	// Build Registry structure
	registry := &types.Registry{
		Version:       "1.0.0",
		LastUpdated:   time.Now().Format(time.RFC3339),
		Servers:       make(map[string]*types.ImageMetadata),
		RemoteServers: make(map[string]*types.RemoteServerMetadata),
		Groups:        []*types.Group{},
	}

	// Separate servers into container and remote
	for _, server := range serverMetadata {
		if server.IsRemote() {
			if remoteServer, ok := server.(*types.RemoteServerMetadata); ok {
				registry.RemoteServers[remoteServer.Name] = remoteServer
			}
		} else {
			if imageServer, ok := server.(*types.ImageMetadata); ok {
				registry.Servers[imageServer.Name] = imageServer
			}
		}
	}

	return registry, nil
}

// GetServer returns a specific server by name (queries API directly)
func (p *APIRegistryProvider) GetServer(name string) (types.ServerMetadata, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Try direct API lookup first (supports both reverse-DNS and simple names)
	// Build potential reverse-DNS name
	reverseDNSName := converters.BuildReverseDNSName(name)

	// Try the reverse-DNS format first
	serverJSON, err := p.client.GetServer(ctx, reverseDNSName)
	if err == nil {
		return ConvertServerJSON(serverJSON)
	}

	// If that failed and the name is already in reverse-DNS format, try as-is
	if reverseDNSName != name {
		serverJSON, err = p.client.GetServer(ctx, name)
		if err == nil {
			return ConvertServerJSON(serverJSON)
		}
	}

	// Fall back to search for backward compatibility
	servers, err := p.client.SearchServers(ctx, name)
	if err != nil {
		return nil, fmt.Errorf("failed to find server %s: %w", name, err)
	}

	// Find exact match in search results
	for _, server := range servers {
		simpleName := converters.ExtractServerName(server.Name)
		if simpleName == name || server.Name == name {
			return ConvertServerJSON(server)
		}
	}

	return nil, fmt.Errorf("%w: %s", ErrServerNotFound, name)
}

// SearchServers searches for servers matching the query (queries API directly)
func (p *APIRegistryProvider) SearchServers(query string) ([]types.ServerMetadata, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Search via API
	servers, err := p.client.SearchServers(ctx, query)
	if err != nil {
		return nil, fmt.Errorf("failed to search servers: %w", err)
	}

	return ConvertServersToMetadata(servers)
}

// ListServers returns all servers from the API
func (p *APIRegistryProvider) ListServers() ([]types.ServerMetadata, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	servers, err := p.client.ListServers(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to list servers: %w", err)
	}

	return ConvertServersToMetadata(servers)
}

// GetSkill returns a specific skill by namespace and name from the API.
func (p *APIRegistryProvider) GetSkill(namespace, name string) (*types.Skill, error) {
	if p.skillsClient == nil {
		return nil, nil
	}
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	return p.skillsClient.GetSkill(ctx, namespace, name)
}

// SearchSkills searches for skills matching the query via the API.
func (p *APIRegistryProvider) SearchSkills(query string) ([]types.Skill, error) {
	if p.skillsClient == nil {
		return nil, nil
	}
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	result, err := p.skillsClient.SearchSkills(ctx, query)
	if err != nil {
		return nil, err
	}
	skills := make([]types.Skill, 0, len(result.Skills))
	for _, s := range result.Skills {
		if s != nil {
			skills = append(skills, *s)
		}
	}
	return skills, nil
}

// ConvertServerJSON converts an MCP Registry API ServerJSON to ToolHive ServerMetadata
// Uses converters from converters.go (same package)
// Note: Only handles OCI packages and remote servers, skips npm/pypi by design
func ConvertServerJSON(serverJSON *v0.ServerJSON) (types.ServerMetadata, error) {
	if serverJSON == nil {
		return nil, fmt.Errorf("serverJSON is nil")
	}

	// Determine if this is a remote server or container-based server
	// Remote servers have the 'remotes' field populated
	// Container servers have the 'packages' field populated
	var result types.ServerMetadata
	var err error

	if len(serverJSON.Remotes) > 0 {
		result, err = converters.ServerJSONToRemoteServerMetadata(serverJSON)
	} else if len(serverJSON.Packages) == 0 {
		// Skip servers without packages or remotes (incomplete entries)
		return nil, fmt.Errorf("server %s has no packages or remotes, skipping", serverJSON.Name)
	} else {
		// ServerJSONToImageMetadata only handles OCI packages, will error on npm/pypi
		result, err = converters.ServerJSONToImageMetadata(serverJSON)
	}

	if err != nil {
		return nil, err
	}

	return result, nil
}

// ConvertServersToMetadata converts a slice of ServerJSON to a slice of ServerMetadata
// Skips servers that cannot be converted (e.g., incomplete entries)
// Uses official converters from toolhive-catalog package
func ConvertServersToMetadata(servers []*v0.ServerJSON) ([]types.ServerMetadata, error) {
	result := make([]types.ServerMetadata, 0, len(servers))

	for _, server := range servers {
		metadata, err := ConvertServerJSON(server)
		if err != nil {
			// Skip servers that can't be converted (e.g., missing packages/remotes)
			// Log the error but continue processing other servers
			continue
		}
		result = append(result, metadata)
	}

	return result, nil
}
