package http

import (
	"context"
	"net/http"

	"github.com/go-chi/chi"
	"github.com/influxdata/httprouter"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/authorizer"
	"github.com/influxdata/influxdb/v2/dbrp"
	"github.com/influxdata/influxdb/v2/http/metric"
	"github.com/influxdata/influxdb/v2/influxql"
	"github.com/influxdata/influxdb/v2/kit/feature"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/kit/prom"
	kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
	"github.com/influxdata/influxdb/v2/query"
	"github.com/influxdata/influxdb/v2/query/fluxlang"
	"github.com/influxdata/influxdb/v2/static"
	"github.com/influxdata/influxdb/v2/storage"
	"github.com/influxdata/influxdb/v2/task/taskmodel"
	"github.com/prometheus/client_golang/prometheus"
	"go.uber.org/zap"
)

// APIHandler is a collection of all the service handlers.
type APIHandler struct {
	chi.Router
}

// APIBackend is all services and associated parameters required to construct
// an APIHandler.
type APIBackend struct {
	AssetsPath     string // if empty then assets are served from bindata.
	UIDisabled     bool   // if true requests for the UI will return 404
	Logger         *zap.Logger
	FluxLogEnabled bool
	errors.HTTPErrorHandler
	SessionRenewDisabled bool
	// MaxBatchSizeBytes is the maximum number of bytes which can be written
	// in a single points batch
	MaxBatchSizeBytes int64

	// WriteParserMaxBytes specifies the maximum number of bytes that may be allocated when processing a single
	// write request. A value of zero specifies there is no limit.
	WriteParserMaxBytes int

	// WriteParserMaxLines specifies the maximum number of lines that may be parsed when processing a single
	// write request. A value of zero specifies there is no limit.
	WriteParserMaxLines int

	// WriteParserMaxValues specifies the maximum number of values that may be parsed when processing a single
	// write request. A value of zero specifies there is no limit.
	WriteParserMaxValues int

	NewQueryService func(*influxdb.Source) (query.ProxyQueryService, error)

	WriteEventRecorder metric.EventRecorder
	QueryEventRecorder metric.EventRecorder

	AlgoWProxy FeatureProxyHandler

	PointsWriter                    storage.PointsWriter
	DeleteService                   influxdb.DeleteService
	BackupService                   influxdb.BackupService
	SqlBackupRestoreService         influxdb.SqlBackupRestoreService
	BucketManifestWriter            influxdb.BucketManifestWriter
	RestoreService                  influxdb.RestoreService
	AuthorizationService            influxdb.AuthorizationService
	AuthorizationV1Service          influxdb.AuthorizationService
	PasswordV1Service               influxdb.PasswordsService
	AuthorizerV1                    influxdb.AuthorizerV1
	OnboardingService               influxdb.OnboardingService
	DBRPService                     influxdb.DBRPMappingService
	BucketService                   influxdb.BucketService
	SessionService                  influxdb.SessionService
	UserService                     influxdb.UserService
	OrganizationService             influxdb.OrganizationService
	UserResourceMappingService      influxdb.UserResourceMappingService
	LabelService                    influxdb.LabelService
	DashboardService                influxdb.DashboardService
	DashboardOperationLogService    influxdb.DashboardOperationLogService
	BucketOperationLogService       influxdb.BucketOperationLogService
	UserOperationLogService         influxdb.UserOperationLogService
	OrganizationOperationLogService influxdb.OrganizationOperationLogService
	SourceService                   influxdb.SourceService
	VariableService                 influxdb.VariableService
	PasswordsService                influxdb.PasswordsService
	InfluxqldService                influxql.ProxyQueryService
	FluxService                     query.ProxyQueryService
	FluxLanguageService             fluxlang.FluxLanguageService
	TaskService                     taskmodel.TaskService
	CheckService                    influxdb.CheckService
	TelegrafService                 influxdb.TelegrafConfigStore
	ScraperTargetStoreService       influxdb.ScraperTargetStoreService
	SecretService                   influxdb.SecretService
	LookupService                   influxdb.LookupService
	OrgLookupService                authorizer.OrgIDResolver
	DocumentService                 influxdb.DocumentService
	NotificationRuleStore           influxdb.NotificationRuleStore
	NotificationEndpointService     influxdb.NotificationEndpointService
	Flagger                         feature.Flagger
	FlagsHandler                    http.Handler
}

// PrometheusCollectors exposes the prometheus collectors associated with an APIBackend.
func (b *APIBackend) PrometheusCollectors() []prometheus.Collector {
	var cs []prometheus.Collector

	if pc, ok := b.WriteEventRecorder.(prom.PrometheusCollector); ok {
		cs = append(cs, pc.PrometheusCollectors()...)
	}

	if pc, ok := b.QueryEventRecorder.(prom.PrometheusCollector); ok {
		cs = append(cs, pc.PrometheusCollectors()...)
	}

	return cs
}

// APIHandlerOptFn is a functional input param to set parameters on
// the APIHandler.
type APIHandlerOptFn func(chi.Router)

// WithResourceHandler registers a resource handler on the APIHandler.
func WithResourceHandler(resHandler kithttp.ResourceHandler) APIHandlerOptFn {
	return func(h chi.Router) {
		h.Mount(resHandler.Prefix(), resHandler)
	}
}

// NewAPIHandler constructs all api handlers beneath it and returns an APIHandler
func NewAPIHandler(b *APIBackend, opts ...APIHandlerOptFn) *APIHandler {
	h := &APIHandler{
		Router: NewBaseChiRouter(kithttp.NewAPI(kithttp.WithLog(b.Logger))),
	}

	b.UserResourceMappingService = authorizer.NewURMService(b.OrgLookupService, b.UserResourceMappingService)

	h.Handle("/api/v2", serveLinksHandler(b.HTTPErrorHandler))

	checkBackend := NewCheckBackend(b.Logger.With(zap.String("handler", "check")), b)
	checkBackend.CheckService = authorizer.NewCheckService(b.CheckService,
		b.UserResourceMappingService, b.OrganizationService)
	h.Mount(prefixChecks, NewCheckHandler(b.Logger, checkBackend))

	deleteBackend := NewDeleteBackend(b.Logger.With(zap.String("handler", "delete")), b)
	h.Mount(prefixDelete, NewDeleteHandler(b.Logger, deleteBackend))

	documentBackend := NewDocumentBackend(b.Logger.With(zap.String("handler", "document")), b)
	documentBackend.DocumentService = authorizer.NewDocumentService(b.DocumentService)
	h.Mount(prefixDocuments, NewDocumentHandler(documentBackend))

	fluxBackend := NewFluxBackend(b.Logger.With(zap.String("handler", "query")), b)
	h.Mount(prefixQuery, NewFluxHandler(b.Logger, fluxBackend))

	notificationEndpointBackend := NewNotificationEndpointBackend(b.Logger.With(zap.String("handler", "notificationEndpoint")), b)
	notificationEndpointBackend.NotificationEndpointService = authorizer.NewNotificationEndpointService(b.NotificationEndpointService,
		b.UserResourceMappingService, b.OrganizationService)
	h.Mount(prefixNotificationEndpoints, NewNotificationEndpointHandler(notificationEndpointBackend.Logger(), notificationEndpointBackend))

	notificationRuleBackend := NewNotificationRuleBackend(b.Logger.With(zap.String("handler", "notification_rule")), b)
	notificationRuleBackend.NotificationRuleStore = authorizer.NewNotificationRuleStore(b.NotificationRuleStore,
		b.UserResourceMappingService, b.OrganizationService)
	h.Mount(prefixNotificationRules, NewNotificationRuleHandler(b.Logger, notificationRuleBackend))

	scraperBackend := NewScraperBackend(b.Logger.With(zap.String("handler", "scraper")), b)
	scraperBackend.ScraperStorageService = authorizer.NewScraperTargetStoreService(b.ScraperTargetStoreService,
		b.UserResourceMappingService,
		b.OrganizationService)
	h.Mount(prefixTargets, NewScraperHandler(b.Logger, scraperBackend))

	sourceBackend := NewSourceBackend(b.Logger.With(zap.String("handler", "source")), b)
	sourceBackend.SourceService = authorizer.NewSourceService(b.SourceService)
	sourceBackend.BucketService = authorizer.NewBucketService(b.BucketService)
	h.Mount(prefixSources, NewSourceHandler(b.Logger, sourceBackend))

	h.Mount("/api/v2/swagger.json", static.NewSwaggerHandler())

	taskLogger := b.Logger.With(zap.String("handler", "bucket"))
	taskBackend := NewTaskBackend(taskLogger, b)
	taskBackend.TaskService = authorizer.NewTaskService(taskLogger, b.TaskService)
	taskHandler := NewTaskHandler(b.Logger, taskBackend)
	h.Mount(prefixTasks, taskHandler)

	telegrafBackend := NewTelegrafBackend(b.Logger.With(zap.String("handler", "telegraf")), b)
	telegrafBackend.TelegrafService = authorizer.NewTelegrafConfigService(b.TelegrafService, b.UserResourceMappingService)
	h.Mount(prefixTelegrafPlugins, NewTelegrafHandler(b.Logger, telegrafBackend))
	h.Mount(prefixTelegraf, NewTelegrafHandler(b.Logger, telegrafBackend))

	h.Mount("/api/v2/flags", b.FlagsHandler)

	h.Mount(prefixResources, NewResourceListHandler())

	variableBackend := NewVariableBackend(b.Logger.With(zap.String("handler", "variable")), b)
	variableBackend.VariableService = authorizer.NewVariableService(b.VariableService)
	h.Mount(prefixVariables, NewVariableHandler(b.Logger, variableBackend))

	backupBackend := NewBackupBackend(b)
	backupBackend.BackupService = authorizer.NewBackupService(backupBackend.BackupService)
	backupBackend.SqlBackupRestoreService = authorizer.NewSqlBackupRestoreService(backupBackend.SqlBackupRestoreService)
	h.Mount(prefixBackup, NewBackupHandler(backupBackend))

	restoreBackend := NewRestoreBackend(b)
	restoreBackend.RestoreService = authorizer.NewRestoreService(restoreBackend.RestoreService)
	restoreBackend.SqlBackupRestoreService = authorizer.NewSqlBackupRestoreService(restoreBackend.SqlBackupRestoreService)
	h.Mount(prefixRestore, NewRestoreHandler(restoreBackend))

	h.Mount(dbrp.PrefixDBRP, dbrp.NewHTTPHandler(b.Logger, b.DBRPService, b.OrganizationService))

	writeBackend := NewWriteBackend(b.Logger.With(zap.String("handler", "write")), b)
	h.Mount(prefixWrite, NewWriteHandler(b.Logger, writeBackend,
		WithMaxBatchSizeBytes(b.MaxBatchSizeBytes),
		// WithParserOptions(
		//	models.WithParserMaxBytes(b.WriteParserMaxBytes),
		//	models.WithParserMaxLines(b.WriteParserMaxLines),
		//	models.WithParserMaxValues(b.WriteParserMaxValues),
		// ),
	))

	for _, o := range opts {
		o(h)
	}
	return h
}

var apiLinks = map[string]interface{}{
	// when adding new links, please take care to keep this list alphabetical
	// as this makes it easier to verify values against the swagger document.
	"authorizations": "/api/v2/authorizations",
	"backup":         "/api/v2/backup",
	"buckets":        "/api/v2/buckets",
	"dashboards":     "/api/v2/dashboards",
	"external": map[string]string{
		"statusFeed": "https://www.influxdata.com/feed/json",
	},
	"flags":                 "/api/v2/flags",
	"labels":                "/api/v2/labels",
	"variables":             "/api/v2/variables",
	"me":                    "/api/v2/me",
	"notificationRules":     "/api/v2/notificationRules",
	"notificationEndpoints": "/api/v2/notificationEndpoints",
	"orgs":                  "/api/v2/orgs",
	"query": map[string]string{
		"self":        "/api/v2/query",
		"ast":         "/api/v2/query/ast",
		"analyze":     "/api/v2/query/analyze",
		"suggestions": "/api/v2/query/suggestions",
	},
	"restore":  "/api/v2/restore",
	"setup":    "/api/v2/setup",
	"signin":   "/api/v2/signin",
	"signout":  "/api/v2/signout",
	"sources":  "/api/v2/sources",
	"scrapers": "/api/v2/scrapers",
	"swagger":  "/api/v2/swagger.json",
	"system": map[string]string{
		"metrics": "/metrics",
		"debug":   "/debug/pprof",
		"health":  "/health",
	},
	"tasks":     "/api/v2/tasks",
	"checks":    "/api/v2/checks",
	"telegrafs": "/api/v2/telegrafs",
	"plugins":   "/api/v2/telegraf/plugins",
	"users":     "/api/v2/users",
	"write":     "/api/v2/write",
	"delete":    "/api/v2/delete",
}

func serveLinksHandler(errorHandler errors.HTTPErrorHandler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if err := encodeResponse(ctx, w, http.StatusOK, apiLinks); err != nil {
			errorHandler.HandleHTTPError(ctx, err, w)
		}
	}
	return http.HandlerFunc(fn)
}

func decodeIDFromCtx(ctx context.Context, name string) (platform.ID, error) {
	params := httprouter.ParamsFromContext(ctx)
	idStr := params.ByName(name)

	if idStr == "" {
		return 0, &errors.Error{
			Code: errors.EInvalid,
			Msg:  "url missing " + name,
		}
	}

	var i platform.ID
	if err := i.DecodeFromString(idStr); err != nil {
		return 0, &errors.Error{
			Code: errors.EInvalid,
			Err:  err,
		}
	}

	return i, nil
}
