package webagent

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
)

type ModelProfileHandler struct {
	store ModelProfileStore
}

var (
	modelProfileHTTPClient  = newModelProfileHTTPClient()
	modelProfileDialContext = (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 15 * time.Second}).DialContext
	modelProfileLookupIP    = net.DefaultResolver.LookupIP
)

func (handler *ModelProfileHandler) List(r api.AuthenticatedRequest) (api.Response, error) {
	if handler.store == nil {
		return api.Response{}, api.ErrNotFound
	}
	profiles, err := handler.store.ListModelProfilesForUser(r.Context(), r.UserID())
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(modelProfileResponses(profiles)), nil
}

func (handler *ModelProfileHandler) Get(r api.AuthenticatedRequest) (api.Response, error) {
	if handler.store == nil {
		return api.Response{}, api.ErrNotFound
	}
	profileID := strings.TrimSpace(r.Gin.Param("profileId"))
	if profileID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "profileId is required")
	}
	profile, err := handler.store.GetModelProfileForUser(r.Context(), r.UserID(), profileID)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(modelProfileResponse(profile)), nil
}

func (handler *ModelProfileHandler) Create(r api.AuthenticatedJSON[dto.ModelProfileCreateRequest]) (api.Response, error) {
	if handler.store == nil {
		return api.Response{}, api.ErrNotFound
	}
	request := r.Body
	if strings.TrimSpace(request.DisplayName) == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "displayName is required")
	}
	models := normalizeModelProfileRequestModels(request)
	if len(models) == 0 {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "model is required")
	}
	profiles, err := handler.store.CreateModelProfileForUser(r.Context(), r.UserID(), agent.CreateModelProfileParams{
		DisplayName: request.DisplayName,
		Models:      models,
		Model:       request.Model,
		BaseURL:     request.BaseURL,
		APIKey:      request.APIKey,
		WireAPI:     request.WireAPI,
	})
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(modelProfileResponses(profiles)), nil
}

func (handler *ModelProfileHandler) Update(r api.AuthenticatedJSON[dto.ModelProfileUpdateRequest]) (api.Response, error) {
	if handler.store == nil {
		return api.Response{}, api.ErrNotFound
	}
	profileID := strings.TrimSpace(r.Gin.Param("profileId"))
	if profileID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "profileId is required")
	}
	request := r.Body
	if strings.TrimSpace(request.DisplayName) == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "displayName is required")
	}
	models := normalizeModelProfileUpdateRequestModels(request)
	if len(models) == 0 {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "model is required")
	}
	profiles, err := handler.store.UpdateModelProfileForUser(r.Context(), r.UserID(), profileID, agent.UpdateModelProfileParams{
		DisplayName: request.DisplayName,
		Models:      models,
		Model:       request.Model,
		BaseURL:     request.BaseURL,
		APIKey:      request.APIKey,
		WireAPI:     request.WireAPI,
	})
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(modelProfileResponses(profiles)), nil
}

func (handler *ModelProfileHandler) Delete(r api.AuthenticatedRequest) (api.Response, error) {
	if handler.store == nil {
		return api.Response{}, api.ErrNotFound
	}
	profileID := strings.TrimSpace(r.Gin.Param("profileId"))
	if profileID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "profileId is required")
	}
	if err := handler.store.DeleteModelProfileForUser(r.Context(), r.UserID(), profileID); err != nil {
		return api.Response{}, err
	}
	return api.OK(dto.EmptyResponse{}), nil
}

func (handler *ModelProfileHandler) Models(r api.AuthenticatedJSON[dto.ModelProfileModelsRequest]) (api.Response, error) {
	models, err := fetchModelProfileModels(r.Context(), r.Body)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(dto.ModelProfileModelsResponse{Models: models}), nil
}

func (handler *ModelProfileHandler) TestConnection(r api.AuthenticatedJSON[dto.ModelProfileConnectionTestRequest]) (api.Response, error) {
	if err := testModelProfileConnection(r.Context(), r.Body); err != nil {
		return api.Response{}, err
	}
	return api.OK(dto.EmptyResponse{}), nil
}

func modelProfileResponses(profiles []agent.ModelProfile) []dto.ModelProfile {
	responses := make([]dto.ModelProfile, 0, len(profiles))
	for _, profile := range profiles {
		responses = append(responses, modelProfileResponse(profile))
	}
	return responses
}

func modelProfileResponse(profile agent.ModelProfile) dto.ModelProfile {
	return dto.ModelProfile{
		ID:               profile.ID,
		ProviderID:       profile.ProviderID,
		DisplayName:      profile.DisplayName,
		Model:            profile.Model,
		BaseURL:          profile.BaseURL,
		APIKey:           profile.APIKey,
		WireAPI:          profile.WireAPI,
		APIKeyConfigured: profile.APIKeyConfigured,
		CreatedAt:        profile.CreatedAt,
		UpdatedAt:        profile.UpdatedAt,
	}
}

func normalizeModelProfileRequestModels(request dto.ModelProfileCreateRequest) []string {
	models := make([]string, 0, len(request.Models)+1)
	seen := make(map[string]struct{}, len(request.Models)+1)
	for _, model := range append(request.Models, request.Model) {
		trimmed := strings.TrimSpace(model)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		models = append(models, trimmed)
	}
	return models
}

func normalizeModelProfileUpdateRequestModels(request dto.ModelProfileUpdateRequest) []string {
	models := make([]string, 0, len(request.Models)+1)
	seen := make(map[string]struct{}, len(request.Models)+1)
	for _, model := range append(request.Models, request.Model) {
		trimmed := strings.TrimSpace(model)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		models = append(models, trimmed)
	}
	return models
}

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

func fetchModelProfileModels(ctx context.Context, request dto.ModelProfileModelsRequest) ([]string, error) {
	baseURL := strings.TrimSpace(request.BaseURL)
	apiKey := strings.TrimSpace(request.APIKey)
	wireAPI := strings.TrimSpace(request.WireAPI)
	if baseURL == "" {
		return nil, api.NewHTTPError(http.StatusBadRequest, "baseUrl is required")
	}
	if apiKey == "" {
		return nil, api.NewHTTPError(http.StatusBadRequest, "apiKey is required")
	}
	if wireAPI == "" {
		wireAPI = "openai"
	}
	endpoint, headers, err := modelProfileModelsEndpoint(baseURL, apiKey, wireAPI)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, api.NewHTTPError(http.StatusBadRequest, "model endpoint is invalid")
	}
	for key, value := range headers {
		req.Header.Set(key, value)
	}

	resp, err := modelProfileHTTPClient.Do(req)
	if err != nil {
		if httpErr := modelProfileTransportHTTPError(err); httpErr != nil {
			return nil, httpErr
		}
		return nil, api.NewHTTPError(http.StatusBadGateway, "failed to reach model endpoint")
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusMethodNotAllowed {
		return []string{}, nil
	}
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return nil, api.NewHTTPError(http.StatusBadGateway, "API key was rejected")
	}
	if resp.StatusCode != http.StatusOK {
		return nil, api.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("model endpoint returned HTTP %d", resp.StatusCode))
	}

	var parsed upstreamModelsResponse
	if err := json.NewDecoder(io.LimitReader(resp.Body, 2<<20)).Decode(&parsed); err != nil {
		return []string{}, nil
	}
	seen := make(map[string]struct{}, len(parsed.Data))
	models := make([]string, 0, len(parsed.Data))
	for _, item := range parsed.Data {
		modelID := strings.TrimSpace(item.ID)
		if modelID == "" {
			continue
		}
		if _, ok := seen[modelID]; ok {
			continue
		}
		seen[modelID] = struct{}{}
		models = append(models, modelID)
	}
	sort.Strings(models)
	return models, nil
}

func testModelProfileConnection(ctx context.Context, request dto.ModelProfileConnectionTestRequest) error {
	baseURL := strings.TrimSpace(request.BaseURL)
	apiKey := strings.TrimSpace(request.APIKey)
	wireAPI := strings.TrimSpace(request.WireAPI)
	model := strings.TrimSpace(request.Model)
	if baseURL == "" {
		return api.NewHTTPError(http.StatusBadRequest, "baseUrl is required")
	}
	if apiKey == "" {
		return api.NewHTTPError(http.StatusBadRequest, "apiKey is required")
	}
	if model == "" {
		return api.NewHTTPError(http.StatusBadRequest, "model is required")
	}
	if wireAPI == "" {
		wireAPI = "openai"
	}
	endpoint, headers, body, err := modelProfileConnectionTestRequest(baseURL, apiKey, wireAPI, model)
	if err != nil {
		return err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
	if err != nil {
		return api.NewHTTPError(http.StatusBadRequest, "test endpoint is invalid")
	}
	for key, value := range headers {
		req.Header.Set(key, value)
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")

	resp, err := modelProfileHTTPClient.Do(req)
	if err != nil {
		if httpErr := modelProfileTransportHTTPError(err); httpErr != nil {
			return httpErr
		}
		return api.NewHTTPError(http.StatusBadGateway, "failed to reach test endpoint")
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<20))
		return api.NewHTTPError(http.StatusBadGateway, "API key was rejected")
	}
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<20))
		return api.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("test endpoint returned HTTP %d", resp.StatusCode))
	}
	var raw json.RawMessage
	if err := json.NewDecoder(io.LimitReader(resp.Body, 2<<20)).Decode(&raw); err != nil {
		return api.NewHTTPError(http.StatusBadGateway, "test endpoint returned invalid JSON")
	}
	return nil
}

func modelProfileModelsEndpoint(baseURL string, apiKey string, wireAPI string) (string, map[string]string, error) {
	base, err := modelProfileAPIBaseURL(baseURL)
	if err != nil {
		return "", nil, err
	}

	headers := map[string]string{}
	switch strings.TrimSpace(wireAPI) {
	case "anthropic":
		headers["x-api-key"] = apiKey
		headers["anthropic-version"] = "2023-06-01"
		return base + "/v1/models", headers, nil
	case "openai", "responses":
		headers["Authorization"] = "Bearer " + apiKey
		return base + "/models", headers, nil
	default:
		return "", nil, api.NewHTTPError(http.StatusBadRequest, "unsupported wireApi")
	}
}

func modelProfileConnectionTestRequest(baseURL string, apiKey string, wireAPI string, model string) (string, map[string]string, []byte, error) {
	base, err := modelProfileAPIBaseURL(baseURL)
	if err != nil {
		return "", nil, nil, err
	}

	headers := map[string]string{}
	switch strings.TrimSpace(wireAPI) {
	case "anthropic":
		headers["x-api-key"] = apiKey
		headers["anthropic-version"] = "2023-06-01"
		body, err := json.Marshal(map[string]any{
			"model":      model,
			"max_tokens": 1,
			"messages": []map[string]string{
				{"role": "user", "content": "Reply with OK."},
			},
		})
		return base + "/v1/messages", headers, body, err
	case "openai":
		headers["Authorization"] = "Bearer " + apiKey
		body, err := json.Marshal(map[string]any{
			"model":      model,
			"max_tokens": 1,
			"stream":     false,
			"messages": []map[string]string{
				{"role": "user", "content": "Reply with OK."},
			},
		})
		return base + "/chat/completions", headers, body, err
	case "responses":
		headers["Authorization"] = "Bearer " + apiKey
		body, err := json.Marshal(map[string]any{
			"model":             model,
			"input":             "Reply with OK.",
			"max_output_tokens": 1,
			"stream":            false,
		})
		return base + "/responses", headers, body, err
	default:
		return "", nil, nil, api.NewHTTPError(http.StatusBadRequest, "unsupported wireApi")
	}
}

func modelProfileAPIBaseURL(baseURL string) (string, error) {
	parsed, err := url.Parse(baseURL)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return "", api.NewHTTPError(http.StatusBadRequest, "Base URL is invalid")
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return "", api.NewHTTPError(http.StatusBadRequest, "Base URL must use http or https")
	}
	if parsed.Hostname() == "" {
		return "", api.NewHTTPError(http.StatusBadRequest, "Base URL host is required")
	}
	return strings.TrimRight(parsed.String(), "/"), nil
}

func newModelProfileHTTPClient() *http.Client {
	transport := http.DefaultTransport.(*http.Transport).Clone()
	transport.Proxy = nil
	transport.DialContext = dialModelProfileHost
	return &http.Client{
		Timeout:   15 * time.Second,
		Transport: transport,
		CheckRedirect: func(*http.Request, []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
}

func dialModelProfileHost(ctx context.Context, network string, address string) (net.Conn, error) {
	host, port, err := net.SplitHostPort(address)
	if err != nil {
		return nil, fmt.Errorf("parse model profile dial target: %w", err)
	}
	ips, err := modelProfileHostIPs(ctx, host)
	if err != nil {
		return nil, err
	}
	var lastErr error
	for _, ip := range ips {
		conn, err := modelProfileDialContext(ctx, network, net.JoinHostPort(ip.String(), port))
		if err == nil {
			return conn, nil
		}
		lastErr = err
	}
	if lastErr != nil {
		return nil, lastErr
	}
	return nil, fmt.Errorf("model profile base URL host has no IP addresses")
}

func modelProfileHostIPs(ctx context.Context, hostname string) ([]net.IP, error) {
	trimmed := strings.TrimSpace(hostname)
	if trimmed == "" {
		return nil, api.NewHTTPError(http.StatusBadRequest, "Base URL host is required")
	}
	trimmed = strings.TrimSuffix(trimmed, ".")
	lower := strings.ToLower(trimmed)
	if lower == "localhost" || strings.HasSuffix(lower, ".localhost") {
		return nil, api.NewHTTPError(http.StatusBadRequest, "Base URL host is not allowed")
	}
	if ip := net.ParseIP(trimmed); ip != nil {
		if !modelProfileIPIsAllowed(ip) {
			return nil, api.NewHTTPError(http.StatusBadRequest, "Base URL host is not allowed")
		}
		return []net.IP{normalizeModelProfileIP(ip)}, nil
	}
	ips, err := modelProfileLookupIP(ctx, "ip", trimmed)
	if err != nil {
		return nil, api.NewHTTPError(http.StatusBadRequest, "Base URL host cannot be resolved")
	}
	if len(ips) == 0 {
		return nil, api.NewHTTPError(http.StatusBadRequest, "Base URL host cannot be resolved")
	}
	allowedIPs := make([]net.IP, 0, len(ips))
	for _, ip := range ips {
		if !modelProfileIPIsAllowed(ip) {
			return nil, api.NewHTTPError(http.StatusBadRequest, "Base URL host is not allowed")
		}
		allowedIPs = append(allowedIPs, normalizeModelProfileIP(ip))
	}
	return allowedIPs, nil
}

func modelProfileIPIsAllowed(ip net.IP) bool {
	if ip == nil {
		return false
	}
	ip = normalizeModelProfileIP(ip)
	if !ip.IsGlobalUnicast() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast() {
		return false
	}
	for _, blocked := range modelProfileBlockedIPNets {
		if blocked.Contains(ip) {
			return false
		}
	}
	return true
}

func normalizeModelProfileIP(ip net.IP) net.IP {
	if ipv4 := ip.To4(); ipv4 != nil {
		return ipv4
	}
	return ip
}

func modelProfileTransportHTTPError(err error) error {
	var httpErr api.HTTPError
	if errors.As(err, &httpErr) {
		return httpErr
	}
	return nil
}

var modelProfileBlockedIPNets = mustParseModelProfileIPNets([]string{
	"0.0.0.0/8",
	"100.64.0.0/10",
	"127.0.0.0/8",
	"169.254.0.0/16",
	"192.0.0.0/24",
	"192.0.2.0/24",
	"198.18.0.0/15",
	"198.51.100.0/24",
	"203.0.113.0/24",
	"224.0.0.0/4",
	"240.0.0.0/4",
	"255.255.255.255/32",
	"::/128",
	"::1/128",
	"64:ff9b::/96",
	"100::/64",
	"2001:db8::/32",
	"fe80::/10",
	"ff00::/8",
})

func mustParseModelProfileIPNets(values []string) []*net.IPNet {
	nets := make([]*net.IPNet, 0, len(values))
	for _, value := range values {
		_, parsed, err := net.ParseCIDR(value)
		if err != nil {
			panic(err)
		}
		nets = append(nets, parsed)
	}
	return nets
}
