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

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/apache/skywalking-rover/pkg/core"

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

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

var log = logger.GetLogger("profiling", "task")

type Manager struct {
	moduleMgr       *module.Manager
	processOperator process.Operator
	profilingClient profiling_v3.EBPFProfilingServiceClient
	ctx             context.Context
	cancel          context.CancelFunc
	taskConfig      *base.TaskConfig

	tasks          map[string]*Context
	instanceID     string
	lastUpdateTime int64
}

func NewManager(ctx context.Context, moduleMgr *module.Manager, taskConfig *base.TaskConfig) (*Manager, error) {
	coreOperator := moduleMgr.FindModule(core.ModuleName).(core.Operator)
	connection := coreOperator.BackendOperator().GetConnection()
	profilingClient := profiling_v3.NewEBPFProfilingServiceClient(connection)
	processOperator := moduleMgr.FindModule(process.ModuleName).(process.Operator)
	if err := CheckProfilingTaskConfig(taskConfig, moduleMgr); err != nil {
		return nil, err
	}

	ctx, cancel := context.WithCancel(ctx)
	manager := &Manager{
		moduleMgr:       moduleMgr,
		processOperator: processOperator,
		profilingClient: profilingClient,
		taskConfig:      taskConfig,
		tasks:           make(map[string]*Context),
		instanceID:      coreOperator.InstanceID(),
		ctx:             ctx,
		cancel:          cancel,
	}
	return manager, nil
}

func (m *Manager) Start() {
}

func (m *Manager) BuildContextFromCommand(command *common_v3.Command) (*Context, error) {
	// analyze command
	t, err := base.ProfilingTaskFromCommand(command)
	if err != nil || t == nil {
		return nil, fmt.Errorf("parsing profiling task failure, command: %v, reason: %v", command.GetArgs(), err)
	}

	// find processes
	processes := make([]api.ProcessInterface, 0)
	for _, processID := range t.ProcessIDList {
		taskProcess := m.processOperator.FindProcessByID(processID)
		if taskProcess == nil {
			return nil, fmt.Errorf("could not found %s processes %s", t.TaskID, t.ProcessIDList)
		}
		processes = append(processes, taskProcess)
	}

	taskContext := &Context{task: t, processes: processes, status: NotRunning, recalcDuration: make(chan bool, 1)}
	// check existing task, extended the running time
	existTask := m.tasks[taskContext.BuildTaskIdentity()]
	// if task are same, then just rewrite the task information and return
	if existTask != nil && existTask.IsSameTask(taskContext) {
		existTask.task = t
		return existTask, nil
	}

	// init runner
	if err := m.initTaskContext(taskContext); err != nil {
		return nil, err
	}
	return taskContext, nil
}

func (m *Manager) BuildContextFromContinuous(processes []api.ProcessInterface,
	taskSetter func(task *base.ProfilingTask), taskIDGenerator func() (string, error)) (*Context, error) {
	task := base.ProfilingTaskFromContinuous(processes, taskSetter)

	taskContext := &Context{task: task, processes: processes, status: NotRunning, recalcDuration: make(chan bool, 1)}
	// if the task already exist, then return error
	existTask := m.tasks[taskContext.BuildTaskIdentity()]
	// if task are same, then just rewrite the task information and return
	if existTask != nil && existTask.IsSameTask(taskContext) {
		return nil, fmt.Errorf("already exist profiling task, so ignore")
	}

	taskID, err := taskIDGenerator()
	if err != nil {
		return nil, err
	}
	task.TaskID = taskID

	// init runner
	if err := m.initTaskContext(taskContext); err != nil {
		return nil, err
	}
	return taskContext, nil
}

func (m *Manager) initTaskContext(taskContext *Context) error {
	// init runner
	var r base.ProfileTaskRunner
	task := taskContext.task
	runner, err := NewProfilingRunner(task.TargetType, m.taskConfig, m.moduleMgr)
	if err != nil {
		return err
	} else if err := runner.Init(task, taskContext.processes); err != nil {
		return fmt.Errorf("could not init %s runner for task: %s: %v", task.TriggerType, task.TaskID, err)
	}
	r = runner

	taskContext.runner = r
	taskContext.ctx, taskContext.cancel = context.WithCancel(m.ctx)
	return nil
}

func (m *Manager) StartTask(c *Context) {
	// shutdown task if exists
	taskIdentity := c.BuildTaskIdentity()
	existTask := m.tasks[taskIdentity]
	if existTask != nil {
		// just extend the task time if the task are same
		if c.IsSameTask(existTask) {
			// notify to re-calculate the task duration(task stop timer)
			c.recalcDuration <- true
			return
		}

		// close task if not same
		id := m.tasks[taskIdentity].TaskID()
		log.Infof("existing profiling task: %s, so need to stop it", id)
		if err := m.ShutdownAndRemoveTask(m.tasks[taskIdentity]); err != nil {
			log.Warnf("shutdown existing profiling task failure, so cannot to start new profiling task: %v. reason: %v", c.task.TaskID, err)
			return
		}
	}

	currentMilli := time.Now().UnixNano() / int64(time.Millisecond)
	m.tasks[taskIdentity] = c

	// already reach time
	if currentMilli >= c.task.StartTime {
		m.runTask(c)
		return
	}

	// schedule to execute
	afterRun := time.Since(time.UnixMilli(c.task.StartTime))
	go func() {
		select {
		case <-time.After(afterRun):
			log.Infof("the profiling task need to wait %fmin to run: %s", afterRun.Minutes(), c.TaskID())
			m.runTask(c)
		case <-c.ctx.Done():
			return
		}
	}()
}

func (m *Manager) runTask(c *Context) {
	log.Infof("ready to starting profiling task: %s", c.TaskID())
	var wg sync.WaitGroup
	wg.Add(1)
	c.runningWg = &wg
	go func() {
		defer func() {
			wg.Done()
			c.status = Stopped
		}()

		notify := func() {
			c.status = Running
			c.startRunningTime = time.Now()
			m.afterProfilingStartSuccess(c)
		}
		// start running
		if err := c.runner.Run(m.ctx, notify); err != nil {
			log.Warnf("executing profiling task failure, taskId: %s, reason: %v", c.task.TaskID, err)
		}
	}()
}

func (m *Manager) afterProfilingStartSuccess(c *Context) {
	pidList := make([]int32, 0)
	for _, p := range c.processes {
		pidList = append(pidList, p.Pid())
	}
	log.Infof("profiling task has been started. taskId: %s, pid: %d", c.task.TaskID, pidList)
	go func() {
		for {
			endTime := c.startRunningTime.Add(c.task.MaxRunningDuration)
			select {
			// shutdown task when arrived task running task
			case <-time.After(time.Until(endTime)):
				log.Infof("arrived task running time, shutting down task: %s", c.task.TaskID)
				if err := m.shutdownTask(c); err != nil {
					log.Warnf("shutting down task failure: %s, reason: %v", c.task.TaskID, err)
				}
				return
			case <-c.recalcDuration:
				// re-calculate the task end-time
				log.Infof("received the extend duration task, task id: %s", c.task.TaskID)
				continue
			// shutdown when context finished
			case <-c.ctx.Done():
				if err := m.shutdownTask(c); err != nil {
					log.Warnf("shutting down task failure: %s, reason: %v", c.task.TaskID, err)
				}
				return
			}
		}
	}()
}

func (m *Manager) shutdownTask(c *Context) error {
	// return if not running
	if c.runningWg == nil {
		return nil
	}
	defer func() {
		if r := recover(); r != nil {
			log.Warnf("recover from shutdown task, id: %s, error: %v", c.TaskID(), r)
		}
	}()
	err := c.runner.Stop()
	c.runningWg.Wait()
	c.cancel()
	return err
}

func (m *Manager) ShutdownAndRemoveTask(c *Context) error {
	err := m.shutdownTask(c)
	delete(m.tasks, c.BuildTaskIdentity())
	return err
}

func (m *Manager) Shutdown() error {
	m.cancel()
	return nil
}

func (m *Manager) checkStoppedTaskAndRemoved() {
	for identity, t := range m.tasks {
		if t.status == Stopped {
			delete(m.tasks, identity)
		}
	}
}

func (m *Manager) StartingWatchTask() error {
	// query task
	tasks, err := m.profilingClient.QueryTasks(m.ctx, &profiling_v3.EBPFProfilingTaskQuery{
		RoverInstanceId:  m.instanceID,
		LatestUpdateTime: m.lastUpdateTime,
	})
	if err != nil {
		return err
	}
	if len(tasks.Commands) == 0 {
		log.Debugf("no profiling task found need to execute")
		return nil
	}

	// analyze profiling tasks
	taskContexts := make([]*Context, 0)
	lastUpdateTime := m.lastUpdateTime
	for _, cmd := range tasks.Commands {
		taskContext, err := m.BuildContextFromCommand(cmd)
		if err != nil {
			log.Warnf("could not execute task, ignored. %v", err)
			continue
		}

		if taskContext.UpdateTime() > lastUpdateTime {
			lastUpdateTime = taskContext.UpdateTime()
		}

		if !taskContext.CheckTaskRunnable() {
			continue
		}

		taskContexts = append(taskContexts, taskContext)
	}

	// update last task time
	m.lastUpdateTime = lastUpdateTime

	if len(taskContexts) == 0 {
		return nil
	}

	taskIDList := make([]string, len(taskContexts))
	for inx, c := range taskContexts {
		taskIDList[inx] = c.TaskID()
	}
	log.Infof("received %d profiling task: %v", len(taskContexts), taskIDList)

	// start tasks
	for _, t := range taskContexts {
		m.StartTask(t)
	}
	return nil
}

func (m *Manager) FlushProfilingData() error {
	// cleanup the stopped after flush profiling data to make sure all the profiling data been sent
	defer m.checkStoppedTaskAndRemoved()
	if len(m.tasks) == 0 {
		log.Debugf("no profiling task need to flush")
		return nil
	}

	stream, err := m.profilingClient.CollectProfilingData(m.ctx)
	if err != nil {
		return err
	}
	currentMilli := time.Now().UnixMilli()
	totalSendCount := make(map[string]int)
	for _, t := range m.tasks {
		data, err1 := t.runner.FlushData()
		if err1 != nil {
			log.Warnf("reading profiling task data failure. taskId: %s, error: %v", t.task.TaskID, err1)
			continue
		}

		if len(data) == 0 {
			continue
		}

		totalSendCount[t.TaskID()] += len(data)
		// only the first data have task metadata
		data[0].Task = &profiling_v3.EBPFProfilingTaskMetadata{
			TaskId:             t.task.TaskID,
			ProcessId:          t.task.ProcessIDList[0], // the profiling(draw flame-graph) task usually have the one process only
			ProfilingStartTime: t.startRunningTime.UnixMilli(),
			CurrentTime:        currentMilli,
		}

		for _, d := range data {
			// send each data, stop flush data if the stream have found error
			if err1 := stream.Send(d); err1 != nil {
				return err1
			}
		}
	}

	if len(totalSendCount) > 0 {
		log.Infof("send profiling data summary: %v", totalSendCount)
	}
	_, err = stream.CloseAndRecv()
	return err
}
