package check

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/influxdata/flux/ast"
	"github.com/influxdata/flux/ast/astutil"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/notification"
	"github.com/influxdata/influxdb/v2/notification/flux"
	"github.com/influxdata/influxdb/v2/query"
	"github.com/influxdata/influxdb/v2/query/fluxlang"
)

var _ influxdb.Check = (*Deadman)(nil)

// Deadman is the deadman check.
type Deadman struct {
	Base
	TimeSince *notification.Duration `json:"timeSince,omitempty"`
	StaleTime *notification.Duration `json:"staleTime,omitempty"`
	// If only zero values reported since time, trigger alert.
	// TODO(desa): Is this implemented in Flux?
	ReportZero bool                    `json:"reportZero"`
	Level      notification.CheckLevel `json:"level"`
}

// Type returns the type of the check.
func (c Deadman) Type() string {
	return "deadman"
}

// GenerateFlux returns a flux script for the Deadman provided.
func (c Deadman) GenerateFlux(lang fluxlang.FluxLanguageService) (string, error) {
	f, err := c.GenerateFluxAST(lang)
	if err != nil {
		return "", err
	}

	return astutil.Format(f)
}

// GenerateFluxAST returns a flux AST for the deadman provided. If there
// are any errors in the flux that the user provided the function will return
// an error for each error found when the script is parsed.
func (c Deadman) GenerateFluxAST(lang fluxlang.FluxLanguageService) (*ast.File, error) {
	p, err := query.Parse(lang, c.Query.Text)
	if p == nil {
		return nil, err
	}
	removeAggregateWindow(p)
	replaceDurationsWithEvery(p, c.StaleTime)
	removeStopFromRange(p)

	if errs := ast.GetErrors(p); len(errs) != 0 {
		return nil, multiError(errs)
	}

	// TODO(desa): this is a hack that we had to do as a result of https://github.com/influxdata/flux/issues/1701
	// when it is fixed we should use a separate file and not manipulate the existing one.
	if len(p.Files) != 1 {
		return nil, fmt.Errorf("expect a single file to be returned from query parsing got %d", len(p.Files))
	}

	f := p.Files[0]
	assignPipelineToData(f)

	f.Imports = append(f.Imports, flux.Imports("influxdata/influxdb/monitor", "experimental", "influxdata/influxdb/v1")...)
	f.Body = append(f.Body, c.generateFluxASTBody()...)

	return f, nil
}

func (c Deadman) generateFluxASTBody() []ast.Statement {
	var statements []ast.Statement
	statements = append(statements, c.generateTaskOption())
	statements = append(statements, c.generateFluxASTCheckDefinition("deadman"))
	statements = append(statements, c.generateLevelFn())
	statements = append(statements, c.generateFluxASTMessageFunction())
	return append(statements, c.generateFluxASTChecksFunction())
}

func (c Deadman) generateLevelFn() ast.Statement {
	fn := flux.Function(flux.FunctionParams("r"), flux.Member("r", "dead"))

	lvl := strings.ToLower(c.Level.String())

	return flux.DefineVariable(lvl, fn)
}

func (c Deadman) generateFluxASTChecksFunction() ast.Statement {
	dur := (*ast.DurationLiteral)(c.TimeSince)
	now := flux.Call(flux.Identifier("now"), flux.Object())
	sub := flux.Call(flux.Member("experimental", "subDuration"), flux.Object(flux.Property("from", now), flux.Property("d", dur)))
	return flux.ExpressionStatement(flux.Pipe(
		flux.Identifier("data"),
		flux.Call(flux.Member("v1", "fieldsAsCols"), flux.Object()),
		flux.Call(flux.Member("monitor", "deadman"), flux.Object(flux.Property("t", sub))),
		c.generateFluxASTChecksCall(),
	))
}

func (c Deadman) generateFluxASTChecksCall() *ast.CallExpression {
	objectProps := append(([]*ast.Property)(nil), flux.Property("data", flux.Identifier("check")))
	objectProps = append(objectProps, flux.Property("messageFn", flux.Identifier("messageFn")))

	// This assumes that the ThresholdConfigs we've been provided do not have duplicates.
	lvl := strings.ToLower(c.Level.String())
	objectProps = append(objectProps, flux.Property(lvl, flux.Identifier(lvl)))

	return flux.Call(flux.Member("monitor", "check"), flux.Object(objectProps...))
}

type deadmanAlias Deadman

// MarshalJSON implement json.Marshaler interface.
func (c Deadman) MarshalJSON() ([]byte, error) {
	return json.Marshal(
		struct {
			deadmanAlias
			Type string `json:"type"`
		}{
			deadmanAlias: deadmanAlias(c),
			Type:         c.Type(),
		})
}
