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

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/apache/skywalking-rover/pkg/tools/operator"

	"github.com/cilium/ebpf/rlimit"

	"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"
	"github.com/apache/skywalking-rover/pkg/profiling/task/base"

	"github.com/spf13/cobra"

	"github.com/apache/skywalking-rover/pkg/boot"
)

func newCheckCmd() *cobra.Command {
	configPath := ""
	outputPath := ""
	outputFormat := ""
	cmd := &cobra.Command{
		Use: "check",
		RunE: func(*cobra.Command, []string) error {
			return check(configPath, outputPath, outputFormat)
		},
	}

	cmd.Flags().StringVarP(&configPath, "config", "c", "configs/rover_configs.yaml", "the rover config file path")
	cmd.Flags().StringVarP(&outputPath, "output", "o", "output.txt", "the rover check output file")
	cmd.Flags().StringVarP(&outputFormat, "format", "f", "plain", "the check output format, support \"json\", \"plain\"")
	return cmd
}

func check(configPath, outputPath, format string) error {
	if configPath == "" || outputPath == "" {
		return fmt.Errorf("the config and output path is required")
	}

	err := os.MkdirAll(filepath.Dir(outputPath), os.ModePerm)
	if err != nil {
		log.Fatalf("failed to create the output file directory: %v", err)
	}

	outFile, err := os.Create(outputPath)
	if err != nil {
		log.Fatalf("failed to create the output file: %v", err)
	}
	defer outFile.Close()

	notify := make(chan bool, 1)
	go func(notify chan bool) {
		err = boot.RunModules(context.Background(), configPath, func(manager *module.Manager) {
			// startup success
			processModuleStartSuccess(notify, nil, manager, outFile, format)
		})
		if err != nil {
			// startup failure
			processModuleStartSuccess(notify, err, nil, outFile, format)
		}
	}(notify)

	<-notify
	return nil
}

func processModuleStartSuccess(notify chan bool, err error, mgr *module.Manager, file io.Writer, format string) {
	data := &outputData{}
	defer func() {
		writeOutput(data, file, format)
		notify <- true
	}()
	if err != nil {
		data.Startup = err
		return
	}
	uname, err := operator.GetOSUname()
	if err != nil {
		data.Startup = err
		return
	}
	data.Kernel = uname.Release

	if err := rlimit.RemoveMemlock(); err != nil {
		data.Startup = err
		return
	}

	// wait processes
	processModule := mgr.FindModule(process.ModuleName).(*process.Module)
	var processes []api.ProcessInterface
	for i := 0; i < 3; i++ {
		processes = processModule.GetAllProcesses()
		if len(processes) != 0 {
			break
		}
		time.Sleep(time.Second)
	}
	if len(processes) == 0 {
		data.Startup = fmt.Errorf("no process")
		return
	}

	ctx := context.Background()
	data.OnCPU = testOnCPUProfiling(ctx, mgr, processes[0])
	data.OffCPU = testOffCPUProfiling(ctx, mgr, processes[0])
	data.Network = testNetworkProfiling(ctx, mgr, processes[0])
}

func testOnCPUProfiling(ctx context.Context, mgr *module.Manager, p api.ProcessInterface) error {
	return testWithRunner(ctx, base.TargetTypeOnCPU, &base.TaskConfig{
		OnCPU: &base.OnCPUConfig{
			Period: "9ms",
		},
	}, mgr, p)
}

func testOffCPUProfiling(ctx context.Context, mgr *module.Manager, p api.ProcessInterface) error {
	return testWithRunner(ctx, base.TargetTypeOffCPU, &base.TaskConfig{}, mgr, p)
}

func testNetworkProfiling(ctx context.Context, mgr *module.Manager, p api.ProcessInterface) error {
	return testWithRunner(ctx, base.TargetTypeNetworkTopology, &base.TaskConfig{
		Network: &base.NetworkConfig{
			MeterPrefix:    "test",
			ReportInterval: "10ms",
			ProtocolAnalyze: base.ProtocolAnalyzeConfig{
				PerCPUBufferSize: "10K",
				Parallels:        1,
				QueueSize:        5000,
			},
		},
	}, mgr, p)
}

func testWithRunner(ctx context.Context, taskType base.TargetType, taskConfig *base.TaskConfig,
	moduleManager *module.Manager, p api.ProcessInterface) error {
	runner, err := task.NewProfilingRunner(taskType, taskConfig, moduleManager)
	if err != nil {
		return err
	}

	if err := runner.Init(&base.ProfilingTask{}, []api.ProcessInterface{p}); err != nil {
		return err
	}

	if err := runner.Run(ctx, func() {
		_ = runner.Stop()
	}); err != nil {
		return err
	}

	return nil
}

func writeOutput(data *outputData, file io.Writer, format string) {
	if format != "json" {
		sprintData := fmt.Sprintf("Kernel: %s\nStartup: %s\nOnCPU: %s\nOffCPU: %s\nNetwork: %s",
			data.Kernel, errorOrSuccess(data.Startup), errorOrSuccess(data.OnCPU),
			errorOrSuccess(data.OffCPU), errorOrSuccess(data.Network))
		_, _ = file.Write([]byte(sprintData))
		return
	}
	// some error could not be marshaled, such as multierror
	jsonData := &outputDataJSON{
		Kernel:  data.Kernel,
		Startup: errorOrSuccess(data.Startup),
		OnCPU:   errorOrSuccess(data.OnCPU),
		OffCPU:  errorOrSuccess(data.OffCPU),
		Network: errorOrSuccess(data.Network),
	}
	marshal, err := json.Marshal(jsonData)
	if err != nil {
		log.Printf("format the output failure: %v", err)
		return
	}
	_, _ = file.Write(marshal)
}

func errorOrSuccess(data error) string {
	if data != nil {
		return data.Error()
	}
	return "true"
}

type outputData struct {
	Kernel  string
	Startup error
	OnCPU   error
	OffCPU  error
	Network error
}

type outputDataJSON struct {
	Kernel  string
	Startup string
	OnCPU   string
	OffCPU  string
	Network string
}
