package platform

import (
	"context"
	"encoding/json"
	"fmt"
)

// ErrViewNotFound is the error for a missing View.
const ErrViewNotFound = ChronografError("View not found")

// ViewService represents a service for managing View data.
type ViewService interface {
	// FindViewByID returns a single View by ID.
	FindViewByID(ctx context.Context, id ID) (*View, error)

	// FindViews returns a list of Views that match filter and the total count of matching Views.
	// Additional options provide pagination & sorting.
	FindViews(ctx context.Context, filter ViewFilter) ([]*View, int, error)

	// CreateView creates a new View and sets b.ID with the new identifier.
	CreateView(ctx context.Context, b *View) error

	// UpdateView updates a single View with changeset.
	// Returns the new View state after update.
	UpdateView(ctx context.Context, id ID, upd ViewUpdate) (*View, error)

	// DeleteView removes a View by ID.
	DeleteView(ctx context.Context, id ID) error
}

// ViewUpdate is a struct for updating Views.
type ViewUpdate struct {
	ViewContentsUpdate
	Properties ViewProperties
}

// Valid validates the update struct. It expects minimal values to be set.
func (u ViewUpdate) Valid() error {
	_, ok := u.Properties.(EmptyViewProperties)
	if u.Name == nil && ok {
		return fmt.Errorf("expected at least one attribute to be updated")
	}

	return nil
}

// ViewContentsUpdate is a struct for updating the non properties content of a View.
type ViewContentsUpdate struct {
	Name *string `json:"name"`
}

// ViewFilter represents a set of filter that restrict the returned results.
type ViewFilter struct {
	ID *ID
}

// View holds positional and visual information for a View.
type View struct {
	ViewContents
	Properties ViewProperties
}

// ViewContents is the id and name of a specific view.
type ViewContents struct {
	ID   ID     `json:"id,omitempty"`
	Name string `json:"name"`
}

// ViewProperties is used to mark other structures as conforming to a View.
type ViewProperties interface {
	viewProperties()
}

// EmptyViewProperties is visualization that has no values
type EmptyViewProperties struct{}

func (v EmptyViewProperties) viewProperties() {}

// UnmarshalViewPropertiesJSON unmarshals JSON bytes into a ViewProperties.
func UnmarshalViewPropertiesJSON(b []byte) (ViewProperties, error) {
	var v struct {
		B json.RawMessage `json:"properties"`
	}

	if err := json.Unmarshal(b, &v); err != nil {
		return nil, err
	}

	if len(v.B) == 0 {
		// Then there wasn't any visualization field, so there's no need unmarshal it
		return EmptyViewProperties{}, nil
	}

	var t struct {
		Shape string `json:"shape"`
		Type  string `json:"type"`
	}

	if err := json.Unmarshal(v.B, &t); err != nil {
		return nil, err
	}

	var vis ViewProperties
	switch t.Shape {
	case "chronograf-v2":
		switch t.Type {
		case "line":
			var lv LineViewProperties
			if err := json.Unmarshal(v.B, &lv); err != nil {
				return nil, err
			}
			vis = lv
		case "single-stat":
			var ssv SingleStatViewProperties
			if err := json.Unmarshal(v.B, &ssv); err != nil {
				return nil, err
			}
			vis = ssv
		case "gauge":
			var gv GaugeViewProperties
			if err := json.Unmarshal(v.B, &gv); err != nil {
				return nil, err
			}
			vis = gv
		case "step-plot":
			var spv StepPlotViewProperties
			if err := json.Unmarshal(v.B, &spv); err != nil {
				return nil, err
			}
			vis = spv
		case "stacked":
			var sv StackedViewProperties
			if err := json.Unmarshal(v.B, &sv); err != nil {
				return nil, err
			}
			vis = sv
		case "table":
			var tv TableViewProperties
			if err := json.Unmarshal(v.B, &tv); err != nil {
				return nil, err
			}
			vis = tv
		case "log-viewer": // happens in log viewer stays in log viewer.
			var lv LogViewProperties
			if err := json.Unmarshal(v.B, &lv); err != nil {
				return nil, err
			}
			vis = lv
		case "line-plus-single-stat":
			var lv LinePlusSingleStatProperties
			if err := json.Unmarshal(v.B, &lv); err != nil {
				return nil, err
			}
			vis = lv
		}
	case "empty":
		var ev EmptyViewProperties
		if err := json.Unmarshal(v.B, &ev); err != nil {
			return nil, err
		}
		vis = ev
	default:
		return nil, fmt.Errorf("unknown type %v", t.Shape)
	}

	return vis, nil
}

// MarshalViewPropertiesJSON encodes a view into JSON bytes.
func MarshalViewPropertiesJSON(v ViewProperties) ([]byte, error) {
	var s interface{}
	switch vis := v.(type) {
	case SingleStatViewProperties:
		s = struct {
			Shape string `json:"shape"`
			SingleStatViewProperties
		}{
			Shape:                    "chronograf-v2",
			SingleStatViewProperties: vis,
		}
	case TableViewProperties:
		s = struct {
			Shape string `json:"shape"`
			TableViewProperties
		}{
			Shape:               "chronograf-v2",
			TableViewProperties: vis,
		}
	case GaugeViewProperties:
		s = struct {
			Shape string `json:"shape"`
			GaugeViewProperties
		}{
			Shape:               "chronograf-v2",
			GaugeViewProperties: vis,
		}
	case LineViewProperties:
		s = struct {
			Shape string `json:"shape"`
			LineViewProperties
		}{
			Shape:              "chronograf-v2",
			LineViewProperties: vis,
		}
	case LinePlusSingleStatProperties:
		s = struct {
			Shape string `json:"shape"`
			LinePlusSingleStatProperties
		}{
			Shape:                        "chronograf-v2",
			LinePlusSingleStatProperties: vis,
		}
	case StepPlotViewProperties:
		s = struct {
			Shape string `json:"shape"`
			StepPlotViewProperties
		}{
			Shape:                  "chronograf-v2",
			StepPlotViewProperties: vis,
		}
	case StackedViewProperties:
		s = struct {
			Shape string `json:"shape"`
			StackedViewProperties
		}{
			Shape:                 "chronograf-v2",
			StackedViewProperties: vis,
		}
	case LogViewProperties:
		s = struct {
			Shape string `json:"shape"`
			LogViewProperties
		}{
			Shape:             "chronograf-v2",
			LogViewProperties: vis,
		}
	default:
		s = struct {
			Shape string `json:"shape"`
			EmptyViewProperties
		}{
			Shape:               "empty",
			EmptyViewProperties: EmptyViewProperties{},
		}
	}
	return json.Marshal(s)
}

// MarshalJSON encodes a view to JSON bytes.
func (c View) MarshalJSON() ([]byte, error) {
	vis, err := MarshalViewPropertiesJSON(c.Properties)
	if err != nil {
		return nil, err
	}

	return json.Marshal(struct {
		ViewContents
		ViewProperties json.RawMessage `json:"properties"`
	}{
		ViewContents:   c.ViewContents,
		ViewProperties: vis,
	})
}

// UnmarshalJSON decodes JSON bytes into the corresponding view type (those that implement ViewProperties).
func (c *View) UnmarshalJSON(b []byte) error {
	if err := json.Unmarshal(b, &c.ViewContents); err != nil {
		return err
	}

	v, err := UnmarshalViewPropertiesJSON(b)
	if err != nil {
		return err
	}
	c.Properties = v
	return nil
}

// UnmarshalJSON decodes JSON bytes into the corresponding view update type (those that implement ViewProperties).
func (u *ViewUpdate) UnmarshalJSON(b []byte) error {
	if err := json.Unmarshal(b, &u.ViewContentsUpdate); err != nil {
		return err
	}

	v, err := UnmarshalViewPropertiesJSON(b)
	if err != nil {
		return err
	}
	u.Properties = v
	return nil
}

// MarshalJSON encodes a view to JSON bytes.
func (u ViewUpdate) MarshalJSON() ([]byte, error) {
	vis, err := MarshalViewPropertiesJSON(u.Properties)
	if err != nil {
		return nil, err
	}

	return json.Marshal(struct {
		ViewContentsUpdate
		ViewProperties json.RawMessage `json:"properties,omitempty"`
	}{
		ViewContentsUpdate: u.ViewContentsUpdate,
		ViewProperties:     vis,
	})
}

// LinePlusSingleStatProperties represents options for line plus single stat view in Chronograf
type LinePlusSingleStatProperties struct {
	Queries       []DashboardQuery `json:"queries"`
	Axes          map[string]Axis  `json:"axes"`
	Type          string           `json:"type"`
	Legend        Legend           `json:"legend"`
	ViewColors    []ViewColor      `json:"colors"`
	Prefix        string           `json:"prefix"`
	Suffix        string           `json:"suffix"`
	DecimalPlaces DecimalPlaces    `json:"decimalPlaces"`
}

// LineViewProperties represents options for line view in Chronograf
type LineViewProperties struct {
	Queries    []DashboardQuery `json:"queries"`
	Axes       map[string]Axis  `json:"axes"`
	Type       string           `json:"type"`
	Legend     Legend           `json:"legend"`
	ViewColors []ViewColor      `json:"colors"`
}

// StepPlotViewProperties represents options for step plot view in Chronograf
type StepPlotViewProperties struct {
	Queries    []DashboardQuery `json:"queries"`
	Axes       map[string]Axis  `json:"axes"`
	Type       string           `json:"type"`
	Legend     Legend           `json:"legend"`
	ViewColors []ViewColor      `json:"colors"`
}

// StackedViewProperties represents options for stacked view in Chronograf
type StackedViewProperties struct {
	Queries    []DashboardQuery `json:"queries"`
	Axes       map[string]Axis  `json:"axes"`
	Type       string           `json:"type"`
	Legend     Legend           `json:"legend"`
	ViewColors []ViewColor      `json:"colors"`
}

// SingleStatViewProperties represents options for single stat view in Chronograf
type SingleStatViewProperties struct {
	Type          string           `json:"type"`
	Queries       []DashboardQuery `json:"queries"`
	Prefix        string           `json:"prefix"`
	Suffix        string           `json:"suffix"`
	ViewColors    []ViewColor      `json:"colors"`
	DecimalPlaces DecimalPlaces    `json:"decimalPlaces"`
}

// GaugeViewProperties represents options for gauge view in Chronograf
type GaugeViewProperties struct {
	Type          string           `json:"type"`
	Queries       []DashboardQuery `json:"queries"`
	Prefix        string           `json:"prefix"`
	Suffix        string           `json:"suffix"`
	ViewColors    []ViewColor      `json:"colors"`
	DecimalPlaces DecimalPlaces    `json:"decimalPlaces"`
}

// TableViewProperties represents options for table view in Chronograf
type TableViewProperties struct {
	Type          string           `json:"type"`
	Queries       []DashboardQuery `json:"queries"`
	ViewColors    []ViewColor      `json:"colors"`
	TableOptions  TableOptions     `json:"tableOptions"`
	FieldOptions  []RenamableField `json:"fieldOptions"`
	TimeFormat    string           `json:"timeFormat"`
	DecimalPlaces DecimalPlaces    `json:"decimalPlaces"`
}

// LogViewProperties represents options for log viewer in Chronograf.
type LogViewProperties struct {
	Type    string            `json:"type"`
	Columns []LogViewerColumn `json:"columns"`
}

// LogViewerColumn represents a specific column in a Log Viewer.
type LogViewerColumn struct {
	Name     string             `json:"name"`
	Position int32              `json:"position"`
	Settings []LogColumnSetting `json:"settings"`
}

// LogColumnSetting represent the settings for a specific column of a Log Viewer.
type LogColumnSetting struct {
	Type  string `json:"type"`
	Value string `json:"value"`
	Name  string `json:"name,omitempty"`
}

func (LineViewProperties) viewProperties()           {}
func (LinePlusSingleStatProperties) viewProperties() {}
func (StepPlotViewProperties) viewProperties()       {}
func (SingleStatViewProperties) viewProperties()     {}
func (StackedViewProperties) viewProperties()        {}
func (GaugeViewProperties) viewProperties()          {}
func (TableViewProperties) viewProperties()          {}
func (LogViewProperties) viewProperties()            {}

/////////////////////////////
// Old Chronograf Types
/////////////////////////////

// DashboardQuery includes state for the query builder.  This is a transition
// struct while we move to the full InfluxQL AST
// TODO(desa): this should be platform.ID
type DashboardQuery struct {
	Label  string `json:"label,omitempty"` // Label is the Y-Axis label for the data
	Range  *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
	Text   string `json:"text"`
	Type   string `json:"type"`
	Source string `json:"source"` // Source is the optional URI to the data source for this queryConfig
}

// Range represents an upper and lower bound for data
type Range struct {
	Upper int64 `json:"upper"` // Upper is the upper bound
	Lower int64 `json:"lower"` // Lower is the lower bound
}

// TimeShift represents a shift to apply to an influxql query's time range
type TimeShift struct {
	Label    string `json:"label"`    // Label user facing description
	Unit     string `json:"unit"`     // Unit influxql time unit representation i.e. ms, s, m, h, d
	Quantity string `json:"quantity"` // Quantity number of units
}

// Field represent influxql fields and functions from the UI
type Field struct {
	Value interface{} `json:"value"`
	Type  string      `json:"type"`
	Alias string      `json:"alias"`
	Args  []Field     `json:"args,omitempty"`
}

// GroupBy represents influxql group by tags from the UI
type GroupBy struct {
	Time string   `json:"time"`
	Tags []string `json:"tags"`
}

// DurationRange represents the lower and upper durations of the query config
type DurationRange struct {
	Upper string `json:"upper"`
	Lower string `json:"lower"`
}

// Axis represents the visible extents of a visualization
type Axis struct {
	Bounds       []string `json:"bounds"` // bounds are an arbitrary list of client-defined strings that specify the viewport for a View
	LegacyBounds [2]int64 `json:"-"`      // legacy bounds are for testing a migration from an earlier version of axis
	Label        string   `json:"label"`  // label is a description of this Axis
	Prefix       string   `json:"prefix"` // Prefix represents a label prefix for formatting axis values
	Suffix       string   `json:"suffix"` // Suffix represents a label suffix for formatting axis values
	Base         string   `json:"base"`   // Base represents the radix for formatting axis values
	Scale        string   `json:"scale"`  // Scale is the axis formatting scale. Supported: "log", "linear"
}

// ViewColor represents the encoding of data into visualizations
type ViewColor struct {
	ID    string `json:"id"`    // ID is the unique id of the View color
	Type  string `json:"type"`  // Type is how the color is used. Accepted (min,max,threshold)
	Hex   string `json:"hex"`   // Hex is the hex number of the color
	Name  string `json:"name"`  // Name is the user-facing name of the hex color
	Value string `json:"value"` // Value is the data value mapped to this color
}

// Legend represents the encoding of data into a legend
type Legend struct {
	Type        string `json:"type,omitempty"`
	Orientation string `json:"orientation,omitempty"`
}

// TableOptions is a type of options for a DashboardView with type Table
type TableOptions struct {
	VerticalTimeAxis bool           `json:"verticalTimeAxis"`
	SortBy           RenamableField `json:"sortBy"`
	Wrapping         string         `json:"wrapping"`
	FixFirstColumn   bool           `json:"fixFirstColumn"`
}

// RenamableField is a column/row field in a DashboardView of type Table
type RenamableField struct {
	InternalName string `json:"internalName"`
	DisplayName  string `json:"displayName"`
	Visible      bool   `json:"visible"`
}

// DecimalPlaces indicates whether decimal places should be enforced, and how many digits it should show.
type DecimalPlaces struct {
	IsEnforced bool  `json:"isEnforced"`
	Digits     int32 `json:"digits"`
}