// 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 trigger

import (
	"fmt"
	"time"

	"github.com/apache/skywalking-rover/pkg/logger"
	"github.com/apache/skywalking-rover/pkg/process/api"
	"github.com/apache/skywalking-rover/pkg/profiling/continuous/base"
	"github.com/apache/skywalking-rover/pkg/profiling/task"
	taskBase "github.com/apache/skywalking-rover/pkg/profiling/task/base"

	v3 "skywalking.apache.org/repo/goapi/collect/ebpf/profiling/v3"
)

var log = logger.GetLogger("profiling", "continuous", "trigger")

type BaseTrigger struct {
	profilingCache map[string]*task.Context
	executeTime    time.Duration
	silenceTime    time.Duration

	profilingTaskDimension func(p api.ProcessInterface) string
	mainProcessSelector    func(ps []api.ProcessInterface) api.ProcessInterface
	taskSetter             func(task *taskBase.ProfilingTask, processes []api.ProcessInterface, thresholds []base.ThresholdCause)
	reportSetter           func(report *v3.ContinuousProfilingReport, processes []api.ProcessInterface, thresholds []base.ThresholdCause)
}

func NewSingleProcessBaseTrigger(_ *base.ContinuousConfig,
	taskSetter func(task *taskBase.ProfilingTask, processes []api.ProcessInterface, thresholds []base.ThresholdCause),
	reportSetter func(report *v3.ContinuousProfilingReport, processes []api.ProcessInterface, thresholds []base.ThresholdCause)) *BaseTrigger {
	trigger := &BaseTrigger{
		profilingTaskDimension: func(p api.ProcessInterface) string {
			return p.ID()
		},
		mainProcessSelector: func(ps []api.ProcessInterface) api.ProcessInterface {
			return ps[0]
		},
		taskSetter:     taskSetter,
		reportSetter:   reportSetter,
		profilingCache: make(map[string]*task.Context),
	}
	return trigger
}

func NewMultipleProcessBasedTrigger(_ *base.ContinuousConfig,
	profilingTaskDimension func(p api.ProcessInterface) string,
	mainProcessSelector func(ps []api.ProcessInterface) api.ProcessInterface,
	taskSetter func(task *taskBase.ProfilingTask, processes []api.ProcessInterface, thresholds []base.ThresholdCause),
	reportSetter func(report *v3.ContinuousProfilingReport, processes []api.ProcessInterface, thresholds []base.ThresholdCause)) *BaseTrigger {
	trigger := &BaseTrigger{
		profilingTaskDimension: profilingTaskDimension,
		mainProcessSelector:    mainProcessSelector,
		taskSetter:             taskSetter,
		reportSetter:           reportSetter,
		profilingCache:         make(map[string]*task.Context),
	}
	return trigger
}

func (c *BaseTrigger) Init(conf *base.ContinuousConfig) error {
	executeDuration, err := time.ParseDuration(conf.Trigger.ExecuteDuration)
	if err != nil {
		return fmt.Errorf("check trigger task execute duration error: %v", err)
	}

	silenceDuration, err := time.ParseDuration(conf.Trigger.SilenceDuration)
	if err != nil {
		return fmt.Errorf("check trigger task silence duration error: %v", err)
	}

	c.executeTime = executeDuration
	c.silenceTime = silenceDuration
	return nil
}

func (c *BaseTrigger) ShouldTrigger(p api.ProcessInterface) bool {
	return c.shouldTriggerFromDimension(c.profilingTaskDimension(p))
}

func (c *BaseTrigger) TriggerTasks(reporter base.TriggerReporter, causes []base.ThresholdCause) int {
	executeCount := 0

	// build needs profiling processes data cache
	// key: dimension, value: map[process][]thresholds
	dimensionedProcessThresholds := make(map[string]map[api.ProcessInterface][]base.ThresholdCause)
	for _, cause := range causes {
		causeProcess := cause.Process()
		dimension := c.profilingTaskDimension(causeProcess)
		if !c.shouldTriggerFromDimension(dimension) {
			continue
		}

		processThresholds := dimensionedProcessThresholds[dimension]
		if processThresholds == nil {
			processThresholds = make(map[api.ProcessInterface][]base.ThresholdCause)
			dimensionedProcessThresholds[dimension] = processThresholds
		}

		processThresholds[causeProcess] = append(processThresholds[causeProcess], cause)
	}

	// reports task through cache
	for dimension, processWithThresholds := range dimensionedProcessThresholds {
		processes := make([]api.ProcessInterface, 0)
		var mainProcess api.ProcessInterface
		for process := range processWithThresholds {
			processes = append(processes, process)
		}
		if len(processes) == 1 {
			mainProcess = processes[0]
		} else {
			mainProcess = c.mainProcessSelector(processes)
		}

		thresholdCauses := processWithThresholds[mainProcess]
		taskContext, err := reporter.ReportProcesses(mainProcess, processes, thresholdCauses,
			func(task *taskBase.ProfilingTask) {
				task.MaxRunningDuration = c.executeTime
				c.taskSetter(task, processes, thresholdCauses)
			}, func(report *v3.ContinuousProfilingReport) {
				report.Duration = int32(c.executeTime.Seconds())
				c.reportSetter(report, processes, thresholdCauses)
			})
		if err != nil {
			log.Warnf("failure to report the cause, process id: %s, error: %v", mainProcess.ID(), err)
			continue
		}

		c.profilingCache[dimension] = taskContext
		executeCount++
	}
	return executeCount
}

func (c *BaseTrigger) shouldTriggerFromDimension(dimension string) bool {
	t := c.profilingCache[dimension]
	if t == nil {
		return true
	} else if t.IsRunning() {
		return false
	}
	return !t.RunningTime().IsZero() && time.Since(t.RunningTime()) > c.silenceTime
}
