package report

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

	log "github.com/sirupsen/logrus"
	"github.com/weaveworks/common/mtime"
)

// Table types
const (
	MulticolumnTableType = "multicolumn-table"
	PropertyListType     = "property-list"
)

const (
	tableEntryKeySeparator = "___"
	truncationCountPrefix  = "table_truncation_count_"
)

// AddPrefixMulticolumnTable appends arbitrary rows to the Node, returning a new node.
func (node Node) AddPrefixMulticolumnTable(prefix string, rows []Row) Node {
	now := mtime.Now()
	for _, row := range rows {
		// Add all the row values as separate entries
		for columnID, value := range row.Entries {
			key := strings.Join([]string{row.ID, columnID}, tableEntryKeySeparator)
			node = node.WithLatest(prefix+key, now, value)
		}
	}
	return node
}

// AddPrefixPropertyList appends arbitrary key-value pairs to the Node, returning a new node.
func (node Node) AddPrefixPropertyList(prefix string, propertyList map[string]string) Node {
	now := mtime.Now()
	for label, value := range propertyList {
		node = node.WithLatest(prefix+label, now, value)
	}
	return node
}

// WithoutPrefix returns the string with trimmed prefix and a
// boolean information of whether that prefix was really there.
// NOTE: Consider moving this function to utilities.
func WithoutPrefix(s string, prefix string) (string, bool) {
	return strings.TrimPrefix(s, prefix), len(prefix) > 0 && strings.HasPrefix(s, prefix)
}

// ExtractMulticolumnTable returns the rows to build a multicolumn table from this node
func (node Node) ExtractMulticolumnTable(template TableTemplate) (rows []Row) {
	rowsMapByID := map[string]Row{}

	// Itearate through the whole of our map to extract all the values with the key
	// with the given prefix. Since multicolumn tables don't support fixed rows (yet),
	// all the table values will be stored under the table prefix.
	// NOTE: It would be nice to optimize this part by only iterating through the keys
	// with the given prefix. If it is possible to traverse the keys in the Latest map
	// in a sorted order, then having LowerBoundEntry(key) and UpperBoundEntry(key)
	// methods should be enough to implement ForEachWithPrefix(prefix) straightforwardly.
	node.Latest.ForEach(func(key string, _ time.Time, value string) {
		if keyWithoutPrefix, ok := WithoutPrefix(key, template.Prefix); ok {
			ids := strings.Split(keyWithoutPrefix, tableEntryKeySeparator)
			rowID, columnID := ids[0], ids[1]
			// If the row with the given ID doesn't yet exist, we create an empty one.
			if _, ok := rowsMapByID[rowID]; !ok {
				rowsMapByID[rowID] = Row{
					ID:      rowID,
					Entries: map[string]string{},
				}
			}
			// At this point, the row with that ID always exists, so we just update the value.
			rowsMapByID[rowID].Entries[columnID] = value
		}
	})

	// Gather a list of rows.
	rows = make([]Row, 0, len(rowsMapByID))
	for _, row := range rowsMapByID {
		rows = append(rows, row)
	}

	// Return the rows sorted by ID.
	sort.Sort(rowsByID(rows))
	return rows
}

// ExtractPropertyList returns the rows to build a property list from this node
func (node Node) ExtractPropertyList(template TableTemplate) (rows []Row) {
	valuesMapByLabel := map[string]string{}

	// Itearate through the whole of our map to extract all the values with the key
	// with the given prefix as well as the keys corresponding to the fixed table rows.
	node.Latest.ForEach(func(key string, _ time.Time, value string) {
		if label, ok := template.FixedRows[key]; ok {
			valuesMapByLabel[label] = value
		} else if label, ok := WithoutPrefix(key, template.Prefix); ok {
			valuesMapByLabel[label] = value
		}
	})

	// Gather a label-value formatted list of rows.
	rows = make([]Row, 0, len(valuesMapByLabel))
	for label, value := range valuesMapByLabel {
		rows = append(rows, Row{
			ID: "label_" + label,
			Entries: map[string]string{
				"label": label,
				"value": value,
			},
		})
	}

	// Return the rows sorted by ID.
	sort.Sort(rowsByID(rows))
	return rows
}

// ExtractTable returns the rows to build either a property list or a
// generic table from this node. It also returns the number of rows,
// if any, that were truncated. The probes used to limit the number of
// labels, env vars and Weave Net connections they report, but this
// logic has since been removed. So the code here dealing with
// truncation is only retained in order to handle legacy reports.
func (node Node) ExtractTable(template TableTemplate) (rows []Row, truncationCount int) {
	switch template.Type {
	case MulticolumnTableType:
		rows = node.ExtractMulticolumnTable(template)
	default: // By default assume it's a property list (for backward compatibility).
		rows = node.ExtractPropertyList(template)
	}

	truncationCount = 0
	if str, ok := node.Latest.Lookup(truncationCountPrefix + template.Prefix); ok {
		if n, err := fmt.Sscanf(str, "%d", &truncationCount); n != 1 || err != nil {
			log.Warnf("Unexpected truncation count format %q", str)
		}
	}

	return rows, truncationCount
}

// Column is the type for multi-column tables in the UI.
type Column struct {
	ID       string `json:"id"`
	Label    string `json:"label"`
	DataType string `json:"dataType"`
}

// Row is the type that holds the table data for the UI. Entries map from column ID to cell value.
type Row struct {
	ID      string            `json:"id"`
	Entries map[string]string `json:"entries"`
}

type rowsByID []Row

func (t rowsByID) Len() int           { return len(t) }
func (t rowsByID) Swap(i, j int)      { t[i], t[j] = t[j], t[i] }
func (t rowsByID) Less(i, j int) bool { return t[i].ID < t[j].ID }

// Table is the type for a table in the UI.
type Table struct {
	ID              string   `json:"id"`
	Label           string   `json:"label"`
	Type            string   `json:"type"`
	Columns         []Column `json:"columns"`
	Rows            []Row    `json:"rows"`
	TruncationCount int      `json:"truncationCount,omitempty"`
}

type tablesByID []Table

func (t tablesByID) Len() int           { return len(t) }
func (t tablesByID) Swap(i, j int)      { t[i], t[j] = t[j], t[i] }
func (t tablesByID) Less(i, j int) bool { return t[i].ID < t[j].ID }

// TableTemplate describes how to render a table for the UI.
type TableTemplate struct {
	ID      string   `json:"id"`
	Label   string   `json:"label"`
	Prefix  string   `json:"prefix"`
	Type    string   `json:"type"`
	Columns []Column `json:"columns"`
	// FixedRows indicates what predetermined rows to render each entry is
	// indexed by the key to extract the row value is mapped to the row
	// label
	FixedRows map[string]string `json:"fixedRows"`
}

// Copy returns a value-copy of the TableTemplate
func (t TableTemplate) Copy() TableTemplate {
	fixedRowsCopy := make(map[string]string, len(t.FixedRows))
	for key, value := range t.FixedRows {
		fixedRowsCopy[key] = value
	}
	t.FixedRows = fixedRowsCopy
	return t
}

// Merge other into t, returning a fresh copy.  Does fieldwise max -
// whilst this isn't particularly meaningful, at least it idempotent,
// commutativite and associative.
func (t TableTemplate) Merge(other TableTemplate) TableTemplate {
	max := func(s1, s2 string) string {
		if s1 > s2 {
			return s1
		}
		return s2
	}

	// NOTE: Consider actually merging the columns and fixed rows.
	fixedRows := t.FixedRows
	if len(other.FixedRows) > len(fixedRows) {
		fixedRows = other.FixedRows
	}
	columns := t.Columns
	if len(other.Columns) > len(columns) {
		columns = other.Columns
	}

	// TODO: Refactor the merging logic, as mixing the types now might result in
	// invalid tables. Maybe we should return an error if the types are different?
	return TableTemplate{
		ID:        max(t.ID, other.ID),
		Label:     max(t.Label, other.Label),
		Prefix:    max(t.Prefix, other.Prefix),
		Type:      max(t.Type, other.Type),
		Columns:   columns,
		FixedRows: fixedRows,
	}
}

// TableTemplates is a mergeable set of TableTemplate
type TableTemplates map[string]TableTemplate

// Tables renders the TableTemplates for a given node.
func (t TableTemplates) Tables(node Node) []Table {
	if len(t) == 0 {
		return nil
	}
	result := make([]Table, 0, len(t))
	for _, template := range t {
		rows, truncationCount := node.ExtractTable(template)
		// Extract the type from the template; default to
		// property list for backwards-compatibility.
		tableType := template.Type
		if tableType == "" {
			tableType = PropertyListType
		}
		result = append(result, Table{
			ID:              template.ID,
			Label:           template.Label,
			Columns:         template.Columns,
			Type:            tableType,
			Rows:            rows,
			TruncationCount: truncationCount,
		})
	}
	sort.Sort(tablesByID(result))
	return result
}

// Copy returns a value copy of the TableTemplates
func (t TableTemplates) Copy() TableTemplates {
	if t == nil {
		return nil
	}
	result := TableTemplates{}
	for k, v := range t {
		result[k] = v.Copy()
	}
	return result
}

// Merge merges two sets of TableTemplates
func (t TableTemplates) Merge(other TableTemplates) TableTemplates {
	if t == nil && other == nil {
		return nil
	}
	if len(other) > len(t) {
		t, other = other, t
	}
	result := t.Copy()
	for k, v := range other {
		if existing, ok := result[k]; ok {
			result[k] = v.Merge(existing)
		} else {
			result[k] = v
		}
	}
	return result
}
