package main

import (
	"context"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"net/url"
	"os"
	"sort"
	"strconv"
	"strings"
	"text/template"
	"time"

	"github.com/fatih/color"
	"github.com/go-openapi/strfmt"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"gopkg.in/yaml.v2"

	"github.com/crowdsecurity/crowdsec/pkg/apiclient"
	"github.com/crowdsecurity/crowdsec/pkg/cwversion"
	"github.com/crowdsecurity/crowdsec/pkg/database"
	"github.com/crowdsecurity/crowdsec/pkg/models"
	"github.com/crowdsecurity/crowdsec/pkg/types"
)

func DecisionsFromAlert(alert *models.Alert) string {
	ret := ""
	var decMap = make(map[string]int)
	for _, decision := range alert.Decisions {
		k := *decision.Type
		if *decision.Simulated {
			k = fmt.Sprintf("(simul)%s", k)
		}
		v := decMap[k]
		decMap[k] = v + 1
	}
	for k, v := range decMap {
		if len(ret) > 0 {
			ret += " "
		}
		ret += fmt.Sprintf("%s:%d", k, v)
	}
	return ret
}

func DateFromAlert(alert *models.Alert) string {
	ts, err := time.Parse(time.RFC3339, alert.CreatedAt)
	if err != nil {
		log.Infof("while parsing %s with %s : %s", alert.CreatedAt, time.RFC3339, err)
		return alert.CreatedAt
	}
	return ts.Format(time.RFC822)
}

func SourceFromAlert(alert *models.Alert) string {

	//more than one item, just number and scope
	if len(alert.Decisions) > 1 {
		return fmt.Sprintf("%d %ss (%s)", len(alert.Decisions), *alert.Decisions[0].Scope, *alert.Decisions[0].Origin)
	}

	//fallback on single decision information
	if len(alert.Decisions) == 1 {
		return fmt.Sprintf("%s:%s", *alert.Decisions[0].Scope, *alert.Decisions[0].Value)
	}

	//try to compose a human friendly version
	if *alert.Source.Value != "" && *alert.Source.Scope != "" {
		scope := ""
		scope = fmt.Sprintf("%s:%s", *alert.Source.Scope, *alert.Source.Value)
		extra := ""
		if alert.Source.Cn != "" {
			extra = alert.Source.Cn
		}
		if alert.Source.AsNumber != "" {
			extra += fmt.Sprintf("/%s", alert.Source.AsNumber)
		}
		if alert.Source.AsName != "" {
			extra += fmt.Sprintf("/%s", alert.Source.AsName)
		}

		if extra != "" {
			scope += " (" + extra + ")"
		}
		return scope
	}
	return ""
}

func AlertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {

	if csConfig.Cscli.Output == "raw" {
		csvwriter := csv.NewWriter(os.Stdout)
		header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"}
		if printMachine {
			header = append(header, "machine")
		}
		err := csvwriter.Write(header)
		if err != nil {
			return err
		}
		for _, alertItem := range *alerts {
			row := []string{
				fmt.Sprintf("%d", alertItem.ID),
				*alertItem.Source.Scope,
				*alertItem.Source.Value,
				*alertItem.Scenario,
				alertItem.Source.Cn,
				alertItem.Source.GetAsNumberName(),
				DecisionsFromAlert(alertItem),
				*alertItem.StartAt,
			}
			if printMachine {
				row = append(row, alertItem.MachineID)
			}
			err := csvwriter.Write(row)
			if err != nil {
				return err
			}
		}
		csvwriter.Flush()
	} else if csConfig.Cscli.Output == "json" {
		x, _ := json.MarshalIndent(alerts, "", " ")
		fmt.Printf("%s", string(x))
	} else if csConfig.Cscli.Output == "human" {
		if len(*alerts) == 0 {
			fmt.Println("No active alerts")
			return nil
		}
		alertsTable(color.Output, alerts, printMachine)
	}
	return nil
}

var alertTemplate = `
################################################################################################

 - ID           : {{.ID}}
 - Date         : {{.CreatedAt}}
 - Machine      : {{.MachineID}}
 - Simulation   : {{.Simulated}}
 - Reason       : {{.Scenario}}
 - Events Count : {{.EventsCount}}
 - Scope:Value  : {{.Source.Scope}}{{if .Source.Value}}:{{.Source.Value}}{{end}}
 - Country      : {{.Source.Cn}}
 - AS           : {{.Source.AsName}}
 - Begin        : {{.StartAt}}
 - End          : {{.StopAt}}
 - UUID         : {{.UUID}}

`


func DisplayOneAlert(alert *models.Alert, withDetail bool) error {
	if csConfig.Cscli.Output == "human" {
		tmpl, err := template.New("alert").Parse(alertTemplate)
		if err != nil {
			return err
		}
		err = tmpl.Execute(os.Stdout, alert)
		if err != nil {
			return err
		}

		alertDecisionsTable(color.Output, alert)

		if len(alert.Meta) > 0 {
			fmt.Printf("\n - Context  :\n")
			sort.Slice(alert.Meta, func(i, j int) bool {
				return alert.Meta[i].Key < alert.Meta[j].Key
			})
			table := newTable(color.Output)
			table.SetRowLines(false)
			table.SetHeaders("Key", "Value")
			for _, meta := range alert.Meta {
				var valSlice []string
				if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil {
					return fmt.Errorf("unknown context value type '%s' : %s", meta.Value, err)
				}
				for _, value := range valSlice {
					table.AddRow(
						meta.Key,
						value,
					)
				}
			}
			table.Render()
		}

		if withDetail {
			fmt.Printf("\n - Events  :\n")
			for _, event := range alert.Events {
				alertEventTable(color.Output, event)
			}
		}
	}
	return nil
}

func NewAlertsCmd() *cobra.Command {
	var cmdAlerts = &cobra.Command{
		Use:               "alerts [action]",
		Short:             "Manage alerts",
		Args:              cobra.MinimumNArgs(1),
		DisableAutoGenTag: true,
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			var err error
			if err := csConfig.LoadAPIClient(); err != nil {
				return errors.Wrap(err, "loading api client")
			}
			apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
			if err != nil {
				return errors.Wrapf(err, "parsing api url %s", apiURL)
			}
			Client, err = apiclient.NewClient(&apiclient.Config{
				MachineID:     csConfig.API.Client.Credentials.Login,
				Password:      strfmt.Password(csConfig.API.Client.Credentials.Password),
				UserAgent:     fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
				URL:           apiURL,
				VersionPrefix: "v1",
			})

			if err != nil {
				return errors.Wrap(err, "new api client")
			}
			return nil
		},
	}

	cmdAlerts.AddCommand(NewAlertsListCmd())
	cmdAlerts.AddCommand(NewAlertsInspectCmd())
	cmdAlerts.AddCommand(NewAlertsFlushCmd())
	cmdAlerts.AddCommand(NewAlertsDeleteCmd())

	return cmdAlerts
}

func NewAlertsListCmd() *cobra.Command {
	var alertListFilter = apiclient.AlertsListOpts{
		ScopeEquals:    new(string),
		ValueEquals:    new(string),
		ScenarioEquals: new(string),
		IPEquals:       new(string),
		RangeEquals:    new(string),
		Since:          new(string),
		Until:          new(string),
		TypeEquals:     new(string),
		IncludeCAPI:    new(bool),
		OriginEquals:   new(string),
	}
	var limit = new(int)
	contained := new(bool)
	var printMachine bool
	var cmdAlertsList = &cobra.Command{
		Use:   "list [filters]",
		Short: "List alerts",
		Example: `cscli alerts list
cscli alerts list --ip 1.2.3.4
cscli alerts list --range 1.2.3.0/24
cscli alerts list -s crowdsecurity/ssh-bf
cscli alerts list --type ban`,
		DisableAutoGenTag: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			var err error

			if err := manageCliDecisionAlerts(alertListFilter.IPEquals, alertListFilter.RangeEquals,
				alertListFilter.ScopeEquals, alertListFilter.ValueEquals); err != nil {
				printHelp(cmd)
				return err
			}
			if limit != nil {
				alertListFilter.Limit = limit
			}

			if *alertListFilter.Until == "" {
				alertListFilter.Until = nil
			} else if strings.HasSuffix(*alertListFilter.Until, "d") {
				/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
				realDuration := strings.TrimSuffix(*alertListFilter.Until, "d")
				days, err := strconv.Atoi(realDuration)
				if err != nil {
					printHelp(cmd)
					return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Until)
				}
				*alertListFilter.Until = fmt.Sprintf("%d%s", days*24, "h")
			}
			if *alertListFilter.Since == "" {
				alertListFilter.Since = nil
			} else if strings.HasSuffix(*alertListFilter.Since, "d") {
				/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
				realDuration := strings.TrimSuffix(*alertListFilter.Since, "d")
				days, err := strconv.Atoi(realDuration)
				if err != nil {
					printHelp(cmd)
					return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Since)
				}
				*alertListFilter.Since = fmt.Sprintf("%d%s", days*24, "h")
			}

			if *alertListFilter.IncludeCAPI {
				*alertListFilter.Limit = 0
			}

			if *alertListFilter.TypeEquals == "" {
				alertListFilter.TypeEquals = nil
			}
			if *alertListFilter.ScopeEquals == "" {
				alertListFilter.ScopeEquals = nil
			}
			if *alertListFilter.ValueEquals == "" {
				alertListFilter.ValueEquals = nil
			}
			if *alertListFilter.ScenarioEquals == "" {
				alertListFilter.ScenarioEquals = nil
			}
			if *alertListFilter.IPEquals == "" {
				alertListFilter.IPEquals = nil
			}
			if *alertListFilter.RangeEquals == "" {
				alertListFilter.RangeEquals = nil
			}

			if *alertListFilter.OriginEquals == "" {
				alertListFilter.OriginEquals = nil
			}

			if contained != nil && *contained {
				alertListFilter.Contains = new(bool)
			}

			alerts, _, err := Client.Alerts.List(context.Background(), alertListFilter)
			if err != nil {
				return fmt.Errorf("unable to list alerts: %v", err)
			}

			err = AlertsToTable(alerts, printMachine)
			if err != nil {
				return fmt.Errorf("unable to list alerts: %v", err)
			}

			return nil
		},
	}
	cmdAlertsList.Flags().SortFlags = false
	cmdAlertsList.Flags().BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
	cmdAlertsList.Flags().StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
	cmdAlertsList.Flags().StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
	cmdAlertsList.Flags().StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
	cmdAlertsList.Flags().StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
	cmdAlertsList.Flags().StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)")
	cmdAlertsList.Flags().StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)")
	cmdAlertsList.Flags().StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)")
	cmdAlertsList.Flags().StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
	cmdAlertsList.Flags().StringVar(alertListFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
	cmdAlertsList.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
	cmdAlertsList.Flags().BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts")
	cmdAlertsList.Flags().IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)")

	return cmdAlertsList
}

func NewAlertsDeleteCmd() *cobra.Command {
	var ActiveDecision *bool
	var AlertDeleteAll bool
	var delAlertByID string
	contained := new(bool)
	var alertDeleteFilter = apiclient.AlertsDeleteOpts{
		ScopeEquals:    new(string),
		ValueEquals:    new(string),
		ScenarioEquals: new(string),
		IPEquals:       new(string),
		RangeEquals:    new(string),
	}
	var cmdAlertsDelete = &cobra.Command{
		Use: "delete [filters] [--all]",
		Short: `Delete alerts
/!\ This command can be use only on the same machine than the local API.`,
		Example: `cscli alerts delete --ip 1.2.3.4
cscli alerts delete --range 1.2.3.0/24
cscli alerts delete -s crowdsecurity/ssh-bf"`,
		DisableAutoGenTag: true,
		Aliases:           []string{"remove"},
		Args:              cobra.ExactArgs(0),
		PreRunE: func(cmd *cobra.Command, args []string) error {
			if AlertDeleteAll {
				return nil
			}
			if *alertDeleteFilter.ScopeEquals == "" && *alertDeleteFilter.ValueEquals == "" &&
				*alertDeleteFilter.ScenarioEquals == "" && *alertDeleteFilter.IPEquals == "" &&
				*alertDeleteFilter.RangeEquals == "" && delAlertByID == "" {
				_ = cmd.Usage()
				return fmt.Errorf("at least one filter or --all must be specified")
			}

			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			var err error

			if !AlertDeleteAll {
				if err := manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals,
					alertDeleteFilter.ScopeEquals, alertDeleteFilter.ValueEquals); err != nil {
					printHelp(cmd)
					return err
				}
				if ActiveDecision != nil {
					alertDeleteFilter.ActiveDecisionEquals = ActiveDecision
				}

				if *alertDeleteFilter.ScopeEquals == "" {
					alertDeleteFilter.ScopeEquals = nil
				}
				if *alertDeleteFilter.ValueEquals == "" {
					alertDeleteFilter.ValueEquals = nil
				}
				if *alertDeleteFilter.ScenarioEquals == "" {
					alertDeleteFilter.ScenarioEquals = nil
				}
				if *alertDeleteFilter.IPEquals == "" {
					alertDeleteFilter.IPEquals = nil
				}
				if *alertDeleteFilter.RangeEquals == "" {
					alertDeleteFilter.RangeEquals = nil
				}
				if contained != nil && *contained {
					alertDeleteFilter.Contains = new(bool)
				}
				limit := 0
				alertDeleteFilter.Limit = &limit
			} else {
				limit := 0
				alertDeleteFilter = apiclient.AlertsDeleteOpts{Limit: &limit}
			}

			var alerts *models.DeleteAlertsResponse
			if delAlertByID == "" {
				alerts, _, err = Client.Alerts.Delete(context.Background(), alertDeleteFilter)
				if err != nil {
					return fmt.Errorf("unable to delete alerts : %v", err)
				}
			} else {
				alerts, _, err = Client.Alerts.DeleteOne(context.Background(), delAlertByID)
				if err != nil {
					return fmt.Errorf("unable to delete alert: %v", err)
				}
			}
			log.Infof("%s alert(s) deleted", alerts.NbDeleted)

			return nil
		},
	}
	cmdAlertsDelete.Flags().SortFlags = false
	cmdAlertsDelete.Flags().StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)")
	cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
	cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
	cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
	cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
	cmdAlertsDelete.Flags().StringVar(&delAlertByID, "id", "", "alert ID")
	cmdAlertsDelete.Flags().BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts")
	cmdAlertsDelete.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
	return cmdAlertsDelete
}

func NewAlertsInspectCmd() *cobra.Command {
	var details bool
	var cmdAlertsInspect = &cobra.Command{
		Use:               `inspect "alert_id"`,
		Short:             `Show info about an alert`,
		Example:           `cscli alerts inspect 123`,
		DisableAutoGenTag: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			if len(args) == 0 {
				printHelp(cmd)
				return fmt.Errorf("missing alert_id")
			}
			for _, alertID := range args {
				id, err := strconv.Atoi(alertID)
				if err != nil {
					return fmt.Errorf("bad alert id %s", alertID)
				}
				alert, _, err := Client.Alerts.GetByID(context.Background(), id)
				if err != nil {
					return fmt.Errorf("can't find alert with id %s: %s", alertID, err)
				}
				switch csConfig.Cscli.Output {
				case "human":
					if err := DisplayOneAlert(alert, details); err != nil {
						continue
					}
				case "json":
					data, err := json.MarshalIndent(alert, "", "  ")
					if err != nil {
						return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err)
					}
					fmt.Printf("%s\n", string(data))
				case "raw":
					data, err := yaml.Marshal(alert)
					if err != nil {
						return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err)
					}
					fmt.Printf("%s\n", string(data))
				}
			}

			return nil
		},
	}
	cmdAlertsInspect.Flags().SortFlags = false
	cmdAlertsInspect.Flags().BoolVarP(&details, "details", "d", false, "show alerts with events")

	return cmdAlertsInspect
}

func NewAlertsFlushCmd() *cobra.Command {
	var maxItems int
	var maxAge string
	var cmdAlertsFlush = &cobra.Command{
		Use: `flush`,
		Short: `Flush alerts
/!\ This command can be used only on the same machine than the local API`,
		Example:           `cscli alerts flush --max-items 1000 --max-age 7d`,
		DisableAutoGenTag: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			var err error
			if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
				return fmt.Errorf("local API is disabled, please run this command on the local API machine")
			}
			dbClient, err = database.NewClient(csConfig.DbConfig)
			if err != nil {
				return fmt.Errorf("unable to create new database client: %s", err)
			}
			log.Info("Flushing alerts. !! This may take a long time !!")
			err = dbClient.FlushAlerts(maxAge, maxItems)
			if err != nil {
				return fmt.Errorf("unable to flush alerts: %s", err)
			}
			log.Info("Alerts flushed")

			return nil
		},
	}

	cmdAlertsFlush.Flags().SortFlags = false
	cmdAlertsFlush.Flags().IntVar(&maxItems, "max-items", 5000, "Maximum number of alert items to keep in the database")
	cmdAlertsFlush.Flags().StringVar(&maxAge, "max-age", "7d", "Maximum age of alert items to keep in the database")

	return cmdAlertsFlush
}
