package sfcrmappservice

import (
	"context"
	"net/http"
	"strings"

	querycodec "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/querycodec"
	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	sessionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/sessionv1"
)

// serveDiscovery owns the Salesforce HTTP documents, not Runtime version or
// quota policy. Version discovery is public; version resources and limits use
// the same Runtime-owned session validation as the other session reads.
func (s *Service) serveDiscovery(w http.ResponseWriter, r *http.Request) bool {
	path := r.URL.Path
	versions := path == "/services/data" || path == "/services/data/"
	version, limits := "", false
	if !versions {
		for _, candidate := range querycodec.SupportedVersions() {
			base := "/services/data/" + candidate
			switch path {
			case base, base + "/":
				version = candidate
			case base + "/limits", base + "/limits/":
				version, limits = candidate, true
			}
		}
		if version == "" {
			return false
		}
	}
	if r.Method != http.MethodGet {
		w.Header().Set("Allow", http.MethodGet)
		writeCRMError(w, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
		return true
	}
	s.requests.Add(1)
	if s.draining.Load() {
		s.drainRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(w, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return true
	}
	if versions {
		type apiVersion struct {
			Label   string `json:"label"`
			URL     string `json:"url"`
			Version string `json:"version"`
		}
		rows := make([]apiVersion, 0, len(querycodec.SupportedVersions()))
		for _, v := range querycodec.SupportedVersions() {
			rows = append(rows, apiVersion{Label: legacyVersionLabel(v), URL: "/services/data/" + v, Version: strings.TrimPrefix(v, "v")})
		}
		writeJSON(w, http.StatusOK, rows)
		s.successes.Add(1)
		return true
	}
	w.Header().Set("Cache-Control", "no-store")
	if _, ok := endUserBearer(r.Header.Values("Authorization")); !ok {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(w, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return true
	}
	if s.sessions == nil || s.sessions.read == nil {
		s.failures.Add(1)
		writeCRMError(w, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Session service temporarily unavailable")
		return true
	}
	s.inFlight.Add(1)
	defer s.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(r.Context(), s.requestTimeout)
	defer cancel()
	session, _, ok := s.sessions.readRequestSessionWithPolicy(w, r.WithContext(ctx), &sessionsemantic.DocumentPolicyRequest{
		RouteFamilyPrefix: "/services/data/",
		LinkRoutes:        []sessionsemantic.DocumentLinkRoute{{Link: "identity", Method: "GET", Path: "/id/:orgId/:userId"}, {Link: "identity", Method: "GET", Path: "/services/data/:version/id/:orgId/:userId"}},
	})
	if !ok {
		s.failures.Add(1)
		return true
	}
	if limits {
		writeJSON(w, http.StatusOK, limitRows(session.DocumentPolicy))
	} else {
		writeJSON(w, http.StatusOK, s.resourceLinks(version, session.Session.Principal.PrincipalID, session.DocumentPolicy))
	}
	s.successes.Add(1)
	return true
}

func (s *Service) resourceLinks(version, principalID string, policy *sessionsemantic.DocumentPolicy) map[string]string {
	base := "/services/data/" + version
	links := make(map[string]string)
	for _, key := range []string{"sobjects", "search", "query", "queryAll", "licensing", "connect", "tooling", "chatter", "recent", "analytics", "composite", "limits", "metadata", "theme", "quickActions", "appMenu", "actions", "parameterizedSearch", "process", "event", "jobs", "ui-api"} {
		links[key] = base + "/" + key
	}
	links["identity"] = s.sessions.publicOrigin + "/id/" + s.sessions.organizationID + "/" + principalID
	if policy.Restricted {
		allowed := make(map[string]bool, len(policy.AllowedLinks))
		for _, link := range policy.AllowedLinks {
			allowed[link] = true
		}
		for link := range links {
			if !allowed[link] {
				delete(links, link)
			}
		}
	}
	return links
}

func legacyVersionLabel(version string) string {
	switch version {
	case "v59.0":
		return "Winter '24"
	case "v60.0":
		return "Spring '24"
	case "v61.0":
		return "Summer '24"
	case "v62.0":
		return "Winter '25"
	case "v63.0":
		return "Spring '25"
	default:
		return version
	}
}
