package router

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	openrtb2model "github.com/prebid/openrtb/v20/openrtb2"
	analyticsBuild "github.com/prebid/prebid-server/v4/analytics/build"
	"github.com/prebid/prebid-server/v4/config"
	"github.com/prebid/prebid-server/v4/currency"
	"github.com/prebid/prebid-server/v4/endpoints"
	"github.com/prebid/prebid-server/v4/endpoints/events"
	infoEndpoints "github.com/prebid/prebid-server/v4/endpoints/info"
	"github.com/prebid/prebid-server/v4/endpoints/openrtb2"
	"github.com/prebid/prebid-server/v4/errortypes"
	"github.com/prebid/prebid-server/v4/exchange"
	"github.com/prebid/prebid-server/v4/experiment/adscert"
	"github.com/prebid/prebid-server/v4/floors"
	"github.com/prebid/prebid-server/v4/gdpr"
	"github.com/prebid/prebid-server/v4/hooks"
	"github.com/prebid/prebid-server/v4/logger"
	"github.com/prebid/prebid-server/v4/macros"
	"github.com/prebid/prebid-server/v4/metrics"
	metricsConf "github.com/prebid/prebid-server/v4/metrics/config"
	"github.com/prebid/prebid-server/v4/modules"
	"github.com/prebid/prebid-server/v4/modules/moduledeps"
	"github.com/prebid/prebid-server/v4/openrtb_ext"
	"github.com/prebid/prebid-server/v4/ortb"
	"github.com/prebid/prebid-server/v4/pbs"
	pbc "github.com/prebid/prebid-server/v4/prebid_cache_client"
	"github.com/prebid/prebid-server/v4/router/aspects"
	"github.com/prebid/prebid-server/v4/server/ssl"
	storedRequestsConf "github.com/prebid/prebid-server/v4/stored_requests/config"
	"github.com/prebid/prebid-server/v4/usersync"
	"github.com/prebid/prebid-server/v4/util/jsonutil"
	"github.com/prebid/prebid-server/v4/util/uuidutil"
	"github.com/prebid/prebid-server/v4/version"

	_ "github.com/go-sql-driver/mysql"
	"github.com/julienschmidt/httprouter"
	_ "github.com/lib/pq"
	"github.com/rs/cors"
)

// NewJsonDirectoryServer is used to serve .json files from a directory as a single blob. For example,
// given a directory containing the files "a.json" and "b.json", this returns a Handle which serves JSON like:
//
//	{
//	  "a": { ... content from the file a.json ... },
//	  "b": { ... content from the file b.json ... }
//	}
//
// This function stores the file contents in memory, and should not be used on large directories.
// If the root directory, or any of the files in it, cannot be read, then the program will exit.
func NewJsonDirectoryServer(schemaDirectory string, validator openrtb_ext.BidderParamValidator) httprouter.Handle {
	return newJsonDirectoryServer(schemaDirectory, validator, openrtb_ext.GetAliasBidderToParent())
}

func newJsonDirectoryServer(schemaDirectory string, validator openrtb_ext.BidderParamValidator, aliases map[openrtb_ext.BidderName]openrtb_ext.BidderName) httprouter.Handle {
	// Slurp the files into memory first, since they're small and it minimizes request latency.
	files, err := os.ReadDir(schemaDirectory)
	if err != nil {
		logger.Fatalf("Failed to read directory %s: %v", schemaDirectory, err)
	}

	bidderMap := openrtb_ext.BuildBidderMap()

	data := make(map[string]json.RawMessage, len(files))
	for _, file := range files {
		bidder := strings.TrimSuffix(file.Name(), ".json")
		bidderName, isValid := bidderMap[bidder]
		if !isValid {
			logger.Fatalf("Schema exists for an unknown bidder: %s", bidder)
		}
		data[bidder] = json.RawMessage(validator.Schema(bidderName))
	}

	// Add in any aliases
	for aliasName, parentBidder := range aliases {
		data[string(aliasName)] = json.RawMessage(validator.Schema(parentBidder))
	}

	response, err := jsonutil.Marshal(data)
	if err != nil {
		logger.Fatalf("Failed to marshal bidder param JSON-schema: %v", err)
	}

	return func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
		w.Header().Add("Content-Type", "application/json")
		w.Write(response)
	}
}

func serveIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	http.ServeFile(w, r, "static/index.html")
}

type NoCache struct {
	Handler http.Handler
}

func (m NoCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate")
	w.Header().Add("Pragma", "no-cache")
	w.Header().Add("Expires", "0")
	m.Handler.ServeHTTP(w, r)
}

type Router struct {
	*httprouter.Router
	MetricsEngine   *metricsConf.DetailedMetricsEngine
	ParamsValidator openrtb_ext.BidderParamValidator

	shutdowns []func()
}

func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *Router, err error) {
	const schemaDirectory = "./static/bidder-params"

	r = &Router{
		Router: httprouter.New(),
	}

	certPool, certPoolCreateErr := ssl.CreateCertPool()
	if certPoolCreateErr != nil {
		logger.Infof("Could not load root certificates: %s \n", certPoolCreateErr.Error())
	}

	// load optional PEM certificate files
	var readCertErr error
	certPool, readCertErr = ssl.AppendPEMFileToCertPool(certPool, cfg.PemCertsFile)
	if readCertErr != nil {
		logger.Infof("Could not read certificates file: %s \n", readCertErr.Error())
	}

	generalHttpClient := &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyFromEnvironment,
			DialContext: defaultTransportDialContext(&net.Dialer{
				Timeout:   time.Duration(cfg.Client.Dialer.TimeoutSeconds) * time.Second,
				KeepAlive: time.Duration(cfg.Client.Dialer.KeepAliveSeconds) * time.Second,
			}),
			MaxConnsPerHost:       cfg.Client.MaxConnsPerHost,
			MaxIdleConns:          cfg.Client.MaxIdleConns,
			MaxIdleConnsPerHost:   cfg.Client.MaxIdleConnsPerHost,
			IdleConnTimeout:       time.Duration(cfg.Client.IdleConnTimeout) * time.Second,
			TLSClientConfig:       &tls.Config{RootCAs: certPool},
			TLSHandshakeTimeout:   time.Duration(cfg.Client.TLSHandshakeTimeout) * time.Second,
			ExpectContinueTimeout: time.Duration(cfg.Client.ExpectContinueTimeout) * time.Second,
		},
	}

	cacheHttpClient := &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyFromEnvironment,
			DialContext: defaultTransportDialContext(&net.Dialer{
				Timeout:   time.Duration(cfg.CacheClient.Dialer.TimeoutSeconds) * time.Second,
				KeepAlive: time.Duration(cfg.CacheClient.Dialer.KeepAliveSeconds) * time.Second,
			}),
			MaxConnsPerHost:       cfg.CacheClient.MaxConnsPerHost,
			MaxIdleConns:          cfg.CacheClient.MaxIdleConns,
			MaxIdleConnsPerHost:   cfg.CacheClient.MaxIdleConnsPerHost,
			IdleConnTimeout:       time.Duration(cfg.CacheClient.IdleConnTimeout) * time.Second,
			TLSHandshakeTimeout:   time.Duration(cfg.CacheClient.TLSHandshakeTimeout) * time.Second,
			ExpectContinueTimeout: time.Duration(cfg.CacheClient.ExpectContinueTimeout) * time.Second,
		},
	}

	floorFechterHttpClient := &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyFromEnvironment,
			DialContext: defaultTransportDialContext(&net.Dialer{
				Timeout:   time.Duration(cfg.PriceFloors.Fetcher.HttpClient.Dialer.TimeoutSeconds) * time.Second,
				KeepAlive: time.Duration(cfg.PriceFloors.Fetcher.HttpClient.Dialer.KeepAliveSeconds) * time.Second,
			}),
			MaxConnsPerHost:       cfg.PriceFloors.Fetcher.HttpClient.MaxConnsPerHost,
			MaxIdleConns:          cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConns,
			MaxIdleConnsPerHost:   cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConnsPerHost,
			IdleConnTimeout:       time.Duration(cfg.PriceFloors.Fetcher.HttpClient.IdleConnTimeout) * time.Second,
			TLSHandshakeTimeout:   time.Duration(cfg.PriceFloors.Fetcher.HttpClient.TLSHandshakeTimeout) * time.Second,
			ExpectContinueTimeout: time.Duration(cfg.PriceFloors.Fetcher.HttpClient.ExpectContinueTimeout) * time.Second,
		},
	}

	if err := checkSupportedUserSyncEndpoints(cfg.BidderInfos); err != nil {
		return nil, err
	}

	syncersByBidder, errs := usersync.BuildSyncers(cfg, cfg.BidderInfos)
	if len(errs) > 0 {
		return nil, errortypes.NewAggregateError("user sync", errs)
	}

	syncerKeys := make([]string, 0, len(syncersByBidder))
	syncerKeysHashSet := map[string]struct{}{}
	for _, syncer := range syncersByBidder {
		syncerKeysHashSet[syncer.Key()] = struct{}{}
	}
	for k := range syncerKeysHashSet {
		syncerKeys = append(syncerKeys, k)
	}

	normalizedGeoscopes := getNormalizedGeoscopes(cfg.BidderInfos)
	moduleDeps := moduledeps.ModuleDeps{HTTPClient: generalHttpClient, RateConvertor: rateConvertor, Geoscope: normalizedGeoscopes}
	repo, moduleStageNames, shutdownModules, err := modules.NewBuilder().Build(cfg.Hooks.Modules, moduleDeps)
	if err != nil {
		logger.Fatalf("Failed to init hook modules: %v", err)
	}

	// Metrics engine
	r.MetricsEngine = metricsConf.NewMetricsEngine(cfg, openrtb_ext.CoreBidderNames(), syncerKeys, moduleStageNames)
	shutdown, fetcher, ampFetcher, accounts, categoriesFetcher, videoFetcher, storedRespFetcher := storedRequestsConf.NewStoredRequests(cfg, r.MetricsEngine, generalHttpClient, r.Router)

	analyticsRunner := analyticsBuild.New(&cfg.Analytics)

	paramsValidator, err := openrtb_ext.NewBidderParamsValidator(schemaDirectory)
	if err != nil {
		logger.Fatalf("Failed to create the bidder params validator. %v", err)
	}

	activeBidders := exchange.GetActiveBidders(cfg.BidderInfos)
	disabledBidders := exchange.GetDisabledBidderWarningMessages(cfg.BidderInfos)

	defReqJSON := readDefaultRequest(cfg.DefReqConfig)

	gvlVendorIDs := cfg.BidderInfos.ToGVLVendorIDMap()
	vendorListFetcher := gdpr.NewVendorListFetcher(context.Background(), cfg.GDPR, generalHttpClient, r.MetricsEngine, gdpr.VendorListURLMaker)
	liveGVLVendorIDs := gdpr.NewLiveGVLVendorIDs()
	refreshInterval := time.Duration(cfg.GDPR.LiveGVLRefreshInterval) * time.Second
	gvlVendorIDTask := gdpr.NewGVLVendorIDTickerTask(refreshInterval, generalHttpClient, gdpr.VendorListURLMaker, liveGVLVendorIDs, r.MetricsEngine)
	gvlVendorIDTask.Start()
	gdprPermsBuilder := gdpr.NewPermissionsBuilder(cfg.GDPR, gvlVendorIDs, liveGVLVendorIDs, vendorListFetcher, r.MetricsEngine)
	tcf2CfgBuilder := gdpr.NewTCF2Config

	// register the analytics runner, modules and live GVL Vendor ID ticker task for shutdown
	r.shutdowns = append(r.shutdowns, shutdown, analyticsRunner.Shutdown, shutdownModules.Shutdown, gvlVendorIDTask.Stop)

	cacheClient := pbc.NewClient(cacheHttpClient, &cfg.CacheURL, &cfg.ExtCacheURL, r.MetricsEngine)

	adapters, singleFormatAdapters, adaptersErrs := exchange.BuildAdapters(generalHttpClient, cfg, cfg.BidderInfos, r.MetricsEngine)
	if len(adaptersErrs) > 0 {
		errs := errortypes.NewAggregateError("Failed to initialize adapters", adaptersErrs)
		return nil, errs
	}
	adsCertSigner, err := adscert.NewAdCertsSigner(cfg.Experiment.AdCerts)
	if err != nil {
		logger.Fatalf("Failed to create ads cert signer: %v", err)
	}

	requestValidator := ortb.NewRequestValidator(activeBidders, disabledBidders, paramsValidator)
	priceFloorFetcher := floors.NewPriceFloorFetcher(cfg.PriceFloors, floorFechterHttpClient, r.MetricsEngine)

	tmaxAdjustments := exchange.ProcessTMaxAdjustments(cfg.TmaxAdjustments)
	planBuilder := hooks.NewExecutionPlanBuilder(cfg.Hooks, repo)
	macroReplacer := macros.NewStringIndexBasedReplacer()
	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, requestValidator, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, rateConvertor, categoriesFetcher, adsCertSigner, macroReplacer, priceFloorFetcher, singleFormatAdapters)
	var uuidGenerator uuidutil.UUIDRandomGenerator
	openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, requestValidator, fetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments)
	if err != nil {
		logger.Fatalf("Failed to create the openrtb2 endpoint handler. %v", err)
	}

	ampEndpoint, err := openrtb2.NewAmpEndpoint(uuidGenerator, theExchange, requestValidator, ampFetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments)
	if err != nil {
		logger.Fatalf("Failed to create the amp endpoint handler. %v", err)
	}

	videoEndpoint, err := openrtb2.NewVideoEndpoint(uuidGenerator, theExchange, requestValidator, fetcher, videoFetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, cacheClient, tmaxAdjustments)
	if err != nil {
		logger.Fatalf("Failed to create the video endpoint handler. %v", err)
	}

	requestTimeoutHeaders := config.RequestTimeoutHeaders{}
	if cfg.RequestTimeoutHeaders != requestTimeoutHeaders {
		videoEndpoint = aspects.QueuedRequestTimeout(videoEndpoint, cfg.RequestTimeoutHeaders, r.MetricsEngine, metrics.ReqTypeVideo)
	}

	r.POST("/openrtb2/auction", openrtbEndpoint)
	r.POST("/openrtb2/video", videoEndpoint)
	r.GET("/openrtb2/amp", ampEndpoint)
	r.GET("/info/bidders", infoEndpoints.NewBiddersEndpoint(cfg.BidderInfos))
	r.GET("/info/bidders/:bidderName", infoEndpoints.NewBiddersDetailEndpoint(cfg.BidderInfos))
	r.GET("/bidders/params", NewJsonDirectoryServer(schemaDirectory, paramsValidator))
	r.POST("/cookie_sync", endpoints.NewCookieSyncEndpoint(syncersByBidder, cfg, gdprPermsBuilder, tcf2CfgBuilder, r.MetricsEngine, analyticsRunner, accounts, activeBidders).Handle)
	r.GET("/status", endpoints.NewStatusEndpoint(cfg.StatusResponse))
	r.GET("/", serveIndex)
	r.Handler("GET", "/version", endpoints.NewVersionEndpoint(version.Ver, version.Rev))
	r.ServeFiles("/static/*filepath", http.Dir("static"))

	// vtrack endpoint
	if cfg.VTrack.Enabled {
		vtrackEndpoint := events.NewVTrackEndpoint(cfg, accounts, cacheClient, cfg.BidderInfos, r.MetricsEngine)
		r.POST("/vtrack", vtrackEndpoint)
	}

	// event endpoint
	eventEndpoint := events.NewEventEndpoint(cfg, accounts, analyticsRunner, r.MetricsEngine)
	r.GET("/event", eventEndpoint)

	userSyncDeps := &pbs.UserSyncDeps{
		HostCookieConfig: &(cfg.HostCookie),
		ExternalUrl:      cfg.ExternalURL,
		RecaptchaSecret:  cfg.RecaptchaSecret,
		PriorityGroups:   cfg.UserSync.PriorityGroups,
		CertPool:         certPool,
	}

	r.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg, syncersByBidder, gdprPermsBuilder, tcf2CfgBuilder, analyticsRunner, accounts, r.MetricsEngine))
	r.GET("/getuids", endpoints.NewGetUIDsEndpoint(cfg.HostCookie))
	r.POST("/optout", userSyncDeps.OptOut)
	r.GET("/optout", userSyncDeps.OptOut)

	return r, nil
}

// defaultTransportDialContext returns the same dialer context as the default transport uses, copied from the library code.
func defaultTransportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) {
	return dialer.DialContext
}

// Shutdown closes any dependencies of the router that may need closing
func (r *Router) Shutdown() {
	logger.Infof("[PBS Router] shutting down")
	for _, shutdown := range r.shutdowns {
		shutdown()
	}
	logger.Infof("[PBS Router] shut down")
}

func checkSupportedUserSyncEndpoints(bidderInfos config.BidderInfos) error {
	for name, info := range bidderInfos {
		if info.Syncer == nil {
			continue
		}

		for _, endpoint := range info.Syncer.Supports {
			endpointLower := strings.ToLower(endpoint)
			switch endpointLower {
			case "iframe":
				if info.Syncer.IFrame == nil {
					logger.Warnf("bidder %s supports iframe user sync, but doesn't have a default and must be configured by the host", name)
				}
			case "redirect":
				if info.Syncer.Redirect == nil {
					logger.Warnf("bidder %s supports redirect user sync, but doesn't have a default and must be configured by the host", name)
				}
			default:
				return fmt.Errorf("failed to load bidder info for %s, user sync supported endpoint '%s' is unrecognized", name, endpoint)
			}
		}
	}
	return nil
}

// Fixes #648
//
// These CORS options pose a security risk... but it's a calculated one.
// People _must_ call us with "withCredentials" set to "true" because that's how we use the cookie sync info.
// We also must allow all origins because every site on the internet _could_ call us.
//
// This is an inherent security risk. However, PBS doesn't use cookies for authorization--just identification.
// We only store the User's ID for each Bidder, and each Bidder has already exposed a public cookie sync endpoint
// which returns that data anyway.
//
// For more info, see:
//
// - https://github.com/rs/cors/issues/55
// - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSNotSupportingCredentials
// - https://portswigger.net/blog/exploiting-cors-misconfigurations-for-bitcoins-and-bounties
func SupportCORS(handler http.Handler) http.Handler {
	c := cors.New(cors.Options{
		AllowCredentials: true,
		AllowOriginFunc: func(string) bool {
			return true
		},
		AllowedHeaders: []string{"Origin", "X-Requested-With", "Content-Type", "Accept"}})
	return c.Handler(handler)
}

func readDefaultRequest(defReqConfig config.DefReqConfig) []byte {
	switch defReqConfig.Type {
	case "file":
		return readDefaultRequestFromFile(defReqConfig)
	default:
		return []byte{}
	}
}

func readDefaultRequestFromFile(defReqConfig config.DefReqConfig) []byte {
	if len(defReqConfig.FileSystem.FileName) == 0 {
		return []byte{}
	}

	defaultRequestJSON, err := os.ReadFile(defReqConfig.FileSystem.FileName)
	if err != nil {
		logger.Fatalf("error reading default request from file %s: %v", defReqConfig.FileSystem.FileName, err)
		return []byte{}
	}

	// validate json is valid
	if err := jsonutil.UnmarshalValid(defaultRequestJSON, &openrtb2model.BidRequest{}); err != nil {
		logger.Fatalf("error parsing default request from file %s: %v", defReqConfig.FileSystem.FileName, err)
		return []byte{}
	}

	return defaultRequestJSON
}

func getNormalizedGeoscopes(bidderInfos config.BidderInfos) map[string][]string {
	geoscopes := make(map[string][]string, len(bidderInfos))

	for name, info := range bidderInfos {
		if len(info.Geoscope) > 0 {
			uppercasedGeoscopes := make([]string, len(info.Geoscope))
			for i, scope := range info.Geoscope {
				uppercasedGeoscopes[i] = strings.ToUpper(scope)
			}
			geoscopes[name] = uppercasedGeoscopes
		}
	}
	return geoscopes
}
