package metrics

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/juju/errors"
	log "github.com/sirupsen/logrus"
	"github.com/tidbcloud/auto-deploy/internal/constants"
	"github.com/tidbcloud/auto-deploy/internal/utils"
	"gopkg.in/yaml.v3"
)

type Metric struct {
	Name   string            `yaml:"name" json:"name"`
	Value  float64           `yaml:"value" json:"value"`
	Labels map[string]string `yaml:"labels" json:"labels"`
}

type MetricFormat int

const (
	MF_PROMETHEOUS MetricFormat = iota + 1
	MF_CSV
	MF_YAML
	MF_JSON
)

const (
	LABEL_CLOUD_PROVIDER = "cloud-provider"
	LABEL_CLOUD_REGION   = "cloud-region"
	LABEL_REGION_NAME    = "region-name"
	LABEL_ENVIRONMENT    = "environment"
	LABEL_DEPLOY_TYPE    = "deploy-type"
	LABEL_DEPLOY_ID      = "deploy-id"
	LABEL_DEPOLY_METHOD  = "depoly-method"
)

const (
	METRIC_DEPLOY_TOTAL_DURATION = "deploy_total_seconds"
	METRIC_DEPLOY_STATUS         = "deploy_status"
	METRIC_QUEUED_DURATION       = "queued_seconds"
	METRIC_PREPARE_DURATION      = "prepare_seconds"
	METRIC_CREATE_K8S_DURATION   = "create_k8s_seconds"
	METRIC_JOBS_TOTAL_DURATION   = "jobs_total_seconds"
	METRIC_TOTAL_JOBS            = "jobs_total"
	METRIC_SUCCEED_JOBS          = "jobs_succeed"
	METRIC_FAILED_JOBS           = "jobs_failed"
	METRIC_DEPENDED_FAILED_JOBS  = "jobs_depended_failed"
	METRIC_SKIPPED_JOBS          = "jobs_skipped"
	METRIC_JOB_STATUS            = "deploy_job_status"
	METRIC_JOB_DURATION          = "deploy_job_seconds"
)

var commonLabels = make(map[string]string)
var metrics = make([]Metric, 0, 100)
var executionStartAt = time.Now()

func AddCommonLabel(key, value string) {
	commonLabels[key] = value
}

func AddQueuedDurationMetric() {
	createdAtTime, err := getCreatedAtTime()
	if err != nil { // no created at time
		return
	}
	startedAtTime, err := getStartedAtTime()
	if err != nil { // no started at time
		return
	}
	duration := startedAtTime.Sub(*createdAtTime)
	if duration.Seconds() > 0.0 {
		AddMetric(METRIC_QUEUED_DURATION, duration.Seconds())
	}
}

func AddPrepareDurationMetric() {
	startedAt, err := getStartedAtTime()
	if err != nil { // no started at time
		return
	}
	duration := time.Since(*startedAt)
	AddMetric(METRIC_PREPARE_DURATION, duration.Seconds())
}

func getStartedAtTime() (*time.Time, error) {
	str := os.Getenv(constants.DEPLOY_STARTED_AT_ENV)
	if str == "" {
		return nil, errors.NotAssigned
	}
	t, err := time.Parse("2006-01-02T15:04:05Z", str)
	if err != nil {
		return nil, errors.NotValidf("%s", str)
	}
	return &t, nil
}

func getCreatedAtTime() (*time.Time, error) {
	str := os.Getenv(constants.DEPLOY_CREATED_AT_ENV)
	if str == "" {
		return nil, errors.NotAssigned
	}
	t, err := time.Parse("2006-01-02T15:04:05Z", str)
	if err != nil {
		return nil, errors.NotValidf("%s", str)
	}
	return &t, nil
}

func AddDeployTotalDurationMetric() {
	startAt, err := getCreatedAtTime()
	if err != nil {
		startAt, err = getStartedAtTime()
	}
	if err != nil {
		startAt = &executionStartAt
		err = nil
	}

	duration := time.Since(*startAt)
	AddMetric(METRIC_DEPLOY_TOTAL_DURATION, duration.Seconds())
}

func AddMetric(name string, value float64) {
	AddMetricWithLabels(name, value, make(map[string]string))
}

func AddMetricWithLabels(name string, value float64, labels map[string]string) {
	log.WithFields(log.Fields{
		"Name":   name,
		"Value":  value,
		"Labels": labels,
	}).Debug("Add metric")
	metric := Metric{
		Name:   name,
		Value:  value,
		Labels: labels,
	}
	metrics = append(metrics, metric)
}

func WriteMetrics(file string, format MetricFormat) error {
	log.WithFields(log.Fields{
		"file":         file,
		"format":       format,
		"commonLabels": commonLabels,
	}).Info("Save metrics")
	for i := range metrics {
		metrics[i].Labels = utils.Merge(metrics[i].Labels, commonLabels)
	}

	var err error
	var bytes []byte
	switch format {
	case MF_YAML:
		bytes, err = metricsToYaml(metrics)
	case MF_JSON:
		bytes, err = metricsToJson(metrics)
	case MF_CSV:
		bytes, err = metricsToCSV(metrics)
	case MF_PROMETHEOUS:
		bytes, err = metricsToPrometheus(metrics)
	default:
		err = errors.Errorf("Not supported format")
	}
	if err != nil {
		return errors.Annotatef(err, "Failed to serial metrics")
	}

	err = os.WriteFile(file, bytes, 0o644&os.ModePerm)
	return err
}

func metricsToYaml(metrics []Metric) ([]byte, error) {
	return yaml.Marshal(metrics)
}

func metricsToJson(metrics []Metric) ([]byte, error) {
	return json.Marshal(metrics)
}

func metricsToCSV(metrics []Metric) ([]byte, error) {
	var buf bytes.Buffer
	w := csv.NewWriter(&buf)
	w.Write([]string{"Metric", "Value", "Labels"})
	record := make([]string, 3)
	for _, metric := range metrics {
		record[0] = metric.Name
		record[1] = fmt.Sprintf("%g", metric.Value)

		lables, err := json.Marshal(metric.Labels)
		if err != nil {
			return nil, err
		}
		record[2] = string(lables)
		w.Write(record)
	}
	w.Flush()

	return buf.Bytes(), nil
}

func metricsToPrometheus(metrics []Metric) ([]byte, error) {
	var buf bytes.Buffer
	for _, metric := range metrics {
		buf.WriteString(metric.Name)
		if _, err := writeLabelPairs(&buf, metric.Labels); err != nil {
			return nil, err
		}
		buf.WriteString(" ")
		buf.WriteString(fmt.Sprintf("%g", metric.Value))
	}

	return buf.Bytes(), nil
}

// enhancedWriter has all the enhanced write functions needed here. bufio.Writer
// implements it.
type enhancedWriter interface {
	io.Writer
	WriteRune(r rune) (n int, err error)
	WriteString(s string) (n int, err error)
	WriteByte(c byte) error
}

// writeLabelPairs converts a slice of LabelPair proto messages plus the
// explicitly given additional label pair into text formatted as required by the
// text format and writes it to 'w'. An empty slice in combination with an empty
// string 'additionalLabelName' results in nothing being written. Otherwise, the
// label pairs are written, escaped as required by the text format, and enclosed
// in '{...}'. The function returns the number of bytes written and any error
// encountered.
func writeLabelPairs(w enhancedWriter, labels map[string]string) (int, error) {
	var (
		written   int
		separator byte = '{'
	)
	for k, v := range labels {
		err := w.WriteByte(separator)
		written++
		if err != nil {
			return written, err
		}
		n, err := w.WriteString(k)
		written += n
		if err != nil {
			return written, err
		}
		n, err = w.WriteString(`="`)
		written += n
		if err != nil {
			return written, err
		}
		n, err = writeEscapedString(w, v, true)
		written += n
		if err != nil {
			return written, err
		}
		err = w.WriteByte('"')
		written++
		if err != nil {
			return written, err
		}
		separator = ','
	}

	err := w.WriteByte('}')
	written++
	if err != nil {
		return written, err
	}
	return written, nil
}

// writeEscapedString replaces '\' by '\\', new line character by '\n', and - if
// includeDoubleQuote is true - '"' by '\"'.
var (
	escaper       = strings.NewReplacer("\\", `\\`, "\n", `\n`)
	quotedEscaper = strings.NewReplacer("\\", `\\`, "\n", `\n`, "\"", `\"`)
)

func writeEscapedString(w enhancedWriter, v string, includeDoubleQuote bool) (int, error) {
	if includeDoubleQuote {
		return quotedEscaper.WriteString(w, v)
	}
	return escaper.WriteString(w, v)
}
