// Copyright © 2019 The Knative Authors
//
// Licensed 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 commands

import (
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/duration"
	"knative.dev/client/pkg/printers"
	"knative.dev/pkg/apis"
)

// Max length When to truncate long strings (when not "all" mode switched on)
const TruncateAt = 100

func WriteMetadata(dw printers.PrefixWriter, m *metav1.ObjectMeta, printDetails bool) {
	dw.WriteAttribute("Name", m.Name)
	dw.WriteAttribute("Namespace", m.Namespace)
	WriteMapDesc(dw, m.Labels, "Labels", printDetails)
	WriteMapDesc(dw, m.Annotations, "Annotations", printDetails)
	dw.WriteAttribute("Age", Age(m.CreationTimestamp.Time))
}

var boringDomains = map[string]bool{
	"serving.knative.dev":   true,
	"client.knative.dev":    true,
	"kubectl.kubernetes.io": true,
}

func keyIsBoring(k string) bool {
	parts := strings.Split(k, "/")
	return len(parts) > 1 && boringDomains[parts[0]]
}

// Write a map either compact in a single line (possibly truncated) or, if printDetails is set,
// over multiple line, one line per key-value pair. The output is sorted by keys.
func WriteMapDesc(dw printers.PrefixWriter, m map[string]string, label string, details bool) {
	if len(m) == 0 {
		return
	}

	var keys []string
	for k := range m {
		if details || !keyIsBoring(k) {
			keys = append(keys, k)
		}
	}
	if len(keys) == 0 {
		return
	}
	sort.Strings(keys)

	if details {
		for i, key := range keys {
			l := ""
			if i == 0 {
				l = printers.Label(label)
			}
			dw.WriteColsLn(l, key+"="+m[key])
		}
		return
	}

	dw.WriteColsLn(printers.Label(label), joinAndTruncate(keys, m, TruncateAt-len(label)-2))
}

func Age(t time.Time) string {
	if t.IsZero() {
		return ""
	}
	return duration.ShortHumanDuration(time.Since(t))
}

func formatConditionType(condition apis.Condition) string {
	return string(condition.Type)
}

// Status in ASCII format
func formatStatus(c apis.Condition) string {
	switch c.Status {
	case corev1.ConditionTrue:
		return "++"
	case corev1.ConditionFalse:
		switch c.Severity {
		case apis.ConditionSeverityError:
			return "!!"
		case apis.ConditionSeverityWarning:
			return " W"
		case apis.ConditionSeverityInfo:
			return " I"
		default:
			return " !"
		}
	default:
		return "??"
	}
}

// Used for conditions table to do own formatting for the table,
// as the tabbed writer doesn't work nicely with colors
func getMaxTypeLen(conditions []apis.Condition) int {
	max := 0
	for _, condition := range conditions {
		if len(condition.Type) > max {
			max = len(condition.Type)
		}
	}
	return max
}

// Sort conditions: Ready first, followed by error, then Warning, then Info
func sortConditions(conditions []apis.Condition) []apis.Condition {
	// Don't change the orig slice
	ret := make([]apis.Condition, len(conditions))
	copy(ret, conditions)
	sort.SliceStable(ret, func(i, j int) bool {
		ic := &ret[i]
		jc := &ret[j]
		// Ready first
		if ic.Type == apis.ConditionReady {
			return jc.Type != apis.ConditionReady
		}
		if jc.Type == apis.ConditionReady {
			return false
		}
		// Among conditions of the same Severity, sort by Type
		if ic.Severity == jc.Severity {
			return ic.Type < jc.Type
		}
		// Error < Warning < Info
		switch ic.Severity {
		case apis.ConditionSeverityError:
			return true
		case apis.ConditionSeverityWarning:
			return jc.Severity == apis.ConditionSeverityInfo
		case apis.ConditionSeverityInfo:
			return false
		default:
			return false
		}
	})
	return ret
}

// Print out a table with conditions.
func WriteConditions(dw printers.PrefixWriter, conditions []apis.Condition, printMessage bool) {
	section := dw.WriteAttribute("Conditions", "")
	conditions = sortConditions(conditions)
	maxLen := getMaxTypeLen(conditions)
	formatHeader := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n"
	formatRow := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n"
	section.Writef(formatHeader, "OK", "TYPE", "AGE", "REASON")
	for _, condition := range conditions {
		ok := formatStatus(condition)
		reason := condition.Reason
		if printMessage && reason != "" {
			reason = fmt.Sprintf("%s (%s)", reason, condition.Message)
		}
		section.Writef(formatRow, ok, formatConditionType(condition), Age(condition.LastTransitionTime.Inner.Time), reason)
	}
}

// Writer a slice compact (printDetails == false) in one line, or over multiple line
// with key-value line-by-line (printDetails == true)
func WriteSliceDesc(dw printers.PrefixWriter, s []string, label string, printDetails bool) {

	if len(s) == 0 {
		return
	}

	if printDetails {
		for i, value := range s {
			if i == 0 {
				dw.WriteColsLn(printers.Label(label), value)
			} else {
				dw.WriteColsLn("", value)
			}
		}
		return
	}

	joined := strings.Join(s, ", ")
	if len(joined) > TruncateAt {
		joined = joined[:TruncateAt-4] + " ..."
	}
	dw.WriteAttribute(label, joined)
}

// Join to key=value pair, comma separated, and truncate if longer than a limit
func joinAndTruncate(sortedKeys []string, m map[string]string, width int) string {
	ret := ""
	for _, key := range sortedKeys {
		ret += fmt.Sprintf("%s=%s, ", key, m[key])
		if len(ret) > width {
			break
		}
	}
	// cut of two latest chars
	ret = strings.TrimRight(ret, ", ")
	if len(ret) <= width {
		return ret
	}
	return ret[:width-4] + " ..."
}
