// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package interceptor

import (
	"context"
	"fmt"
	"strconv"
	"time"

	api "skywalking.apache.org/repo/goapi/query"

	"github.com/apache/skywalking-cli/pkg/contextkey"
	"github.com/apache/skywalking-cli/pkg/graphql/utils"

	"github.com/urfave/cli/v2"

	"github.com/apache/skywalking-cli/internal/model"
	"github.com/apache/skywalking-cli/pkg/logger"
)

func TryParseTime(unparsed string, userStep api.Step) (api.Step, time.Time, error) {
	var possibleError error
	for step, layout := range utils.StepFormats {
		t, err := time.Parse(layout, unparsed)
		if err == nil {
			return step, t, nil
		}
		possibleError = err
	}
	duration, err := time.ParseDuration(unparsed)
	if err == nil {
		return userStep, time.Now().Add(duration), nil
	}
	return userStep, time.Time{}, fmt.Errorf("the given time %v is neither absolute time nor relative time: %+v %+v", unparsed, possibleError, err)
}

// DurationInterceptor sets the duration if absent, and formats it accordingly,
// see ParseDuration
func DurationInterceptor(cliCtx *cli.Context) error {
	start := cliCtx.String("start")
	end := cliCtx.String("end")
	userStep := cliCtx.Generic("step")
	if timezone := cliCtx.String("timezone"); timezone != "" {
		if offset, err := strconv.Atoi(timezone); err == nil {
			// `offset` is in form of "+1300", while `time.FixedZone` takes offset in seconds
			time.Local = time.FixedZone("", offset/100*60*60)
		}
	}

	var s api.Step
	if userStep != nil {
		s = userStep.(*model.StepEnumValue).Selected
	}

	startTime, endTime, step, dt := ParseDuration(start, end, s)

	if err := cliCtx.Set("start", startTime.Format(utils.StepFormats[step])); err != nil {
		return err
	} else if err := cliCtx.Set("end", endTime.Format(utils.StepFormats[step])); err != nil {
		return err
	} else if err := cliCtx.Set("step", step.String()); err != nil {
		return err
	} else if err := cliCtx.Set("duration-type", dt.String()); err != nil {
		return err
	}

	ctx := cliCtx.Context
	ctx = context.WithValue(ctx, contextkey.DurationStart{}, startTime.Format(utils.StepFormats[step]))
	ctx = context.WithValue(ctx, contextkey.DurationEnd{}, endTime.Format(utils.StepFormats[step]))
	ctx = context.WithValue(ctx, contextkey.DurationStep{}, step)
	ctx = context.WithValue(ctx, contextkey.DurationType{}, utils.DurationType(dt.String()))
	cliCtx.Context = ctx

	return nil
}

// IsSetDurationFlags checks if the duration flags are set
func IsSetDurationFlags(ctx *cli.Context) bool {
	return ctx.IsSet("start") || ctx.IsSet("end") || ctx.IsSet("step")
}

// ParseDuration parses the `start` and `end` to a triplet, (startTime, endTime, step),
// based on the given `timezone`, however, if the given `timezone` is empty, UTC becomes the default timezone.
// if --start and --end are both absent,
// then: start := now - 30min; end := now
// if --start is given, --end is absent,
// then: end := now + 30 units, where unit is the precision of `start`, (hours, minutes, etc.)
// if --start is absent, --end is given,
// then: start := end - 30 units, where unit is the precision of `end`, (hours, minutes, etc.)
func ParseDuration(start, end string, userStep api.Step) (startTime, endTime time.Time, step api.Step, dt utils.DurationType) {
	logger.Log.Debugln("Start time:", start, "end time:", end, "timezone:", time.Local)

	now := time.Now()

	// both are absent
	if start == "" && end == "" {
		return now.Add(-30 * time.Minute), now, api.StepMinute, utils.BothAbsent
	}

	var err error

	// both are present
	if start != "" && end != "" {
		if userStep, startTime, err = TryParseTime(start, userStep); err != nil {
			logger.Log.Fatalln("Unsupported time format:", start, err)
		}
		if step, endTime, err = TryParseTime(end, userStep); err != nil {
			logger.Log.Fatalln("Unsupported time format:", end, err)
		}

		return startTime, endTime, step, utils.BothPresent
	} else if end == "" { // end is absent
		if step, startTime, err = TryParseTime(start, userStep); err != nil {
			logger.Log.Fatalln("Unsupported time format:", start, err)
		}
		return startTime, startTime.Add(30 * utils.StepDuration[step]), step, utils.EndAbsent
	}
	// start is absent
	if step, endTime, err = TryParseTime(end, userStep); err != nil {
		logger.Log.Fatalln("Unsupported time format:", end, err)
	}
	return endTime.Add(-30 * utils.StepDuration[step]), endTime, step, utils.StartAbsent
}

// AlignPrecision aligns the two time strings to same precision
// by truncating the more precise one
func AlignPrecision(start, end string) (_, _ string) {
	if len(start) < len(end) {
		return start, end[0:len(start)]
	}
	if len(start) > len(end) {
		return start[0:len(end)], end
	}
	return start, end
}
