package alert

import (
	"database/sql"
	"fmt"
	"github.com/jinzhu/gorm"
	"github.com/pingcap/log"
	"github.com/pingcap/tep/pkg/apis/apiserver"
	"github.com/prometheus/common/model"
	"github.com/wushilin/stream"
	"go.uber.org/zap"
	"time"
)

const (
	INSERTALERT      = `INSERT INTO alerts (status, starts_at, ends_at, generator_url, fingerprint) VALUES (?, ?, ?, ?, ?)`
	INSERTLABEL      = `INSERT INTO alert_labels (alert_id, label, value) VALUES (?, ?, ?)`
	INSERTANNOTATION = `INSERT INTO alert_annotations (alert_id, annotation, value) VALUES (?, ?, ?)`
	SELECTALERT      = `SELECT id, fingerprint, starts_at, ends_at FROM alerts WHERE fingerprint = ? AND (
			starts_at <= ? AND (ends_at >= ? OR (ends_at IS NULL) )
	)`

	DELETEALERT = `DELETE FROM alerts WHERE fingerprint = ? AND (
				starts_at <= ? AND
				( ends_at >= ? OR (ends_at IS NULL))
			)
	`
	ALERTNAMELABEL        = "alertname"
	INSTANCELABEL         = "env"
	EXPRLABEL             = "expr"
	LEVELLABEL            = "level"
	DESCRIPTIONANNOTATION = "description"
)

type AlertInDB struct {
	id           int
	status       string
	startsAt     time.Time
	endsAlert    *time.Time
	generatorURL string
	fingerprint  uint64
}

// Save implements Storer interface
func Save(data *apiserver.AlertGroup, db *gorm.DB) error {
	var r error
	stream.FromArray(data.Alerts).Each(func(a apiserver.PrometheusAlert) {
		start, end, err := convert(a)
		if err != nil {
			log.Error("convert time faild", zap.Error(err), zap.String("start", a.StartsAt), zap.String("end", a.EndsAt))
			r = err
			return
		}

		olaps, err := db.DB().Query(SELECTALERT, LabelSet(a.Labels).Fingerprint(), getEndTime(end), start)
		if err != nil {
			log.Error("query alert failed", zap.Error(err))
			r = err
			return
		}

		alerts, err := scanAlerts(olaps)
		if err != nil {
			log.Error("query alert failed", zap.Error(err))
			r = err
			return
		}

		start, end = mergeAlert(&a, start, end, alerts)

		err = unitOfWork(insertAlert(a, start, end, alerts), db.DB())
		if err != nil {
			log.Error("insert alert failed", zap.Error(err))
			r = err
		}
	})

	return r
}

func getEndTime(end time.Time) *time.Time {
	if end.IsZero() {
		t := time.Now().Add(time.Duration(3600 * 24 * 1000))
		return &t
	}

	return &end
}

func scanAlerts(rows *sql.Rows) ([]AlertInDB, error) {
	var merges []AlertInDB
	for rows.Next() {
		var ad AlertInDB
		err := rows.Scan(
			&ad.id,
			&ad.fingerprint,
			&ad.startsAt,
			&ad.endsAlert,
		)
		if err != nil {
			log.Error("merge alert failed", zap.Error(err))
			return merges, err
		}

		merges = append(merges, ad)
	}

	return merges, nil
}

func mergeAlert(alert *apiserver.PrometheusAlert, start, end time.Time, alerts []AlertInDB) (time.Time, time.Time) {
	stream.FromArray(alerts).Each(func(a AlertInDB) {
		if start.After(a.startsAt) {
			start = a.startsAt
		}

		if a.endsAlert != nil && end.Before(*a.endsAlert) {
			end = *a.endsAlert
		}

		if a.status == string(model.AlertResolved) {
			alert.Status = string(model.AlertResolved)
		}
	})

	return start, end
}

func insertAlert(alert apiserver.PrometheusAlert, start, end time.Time, alerts []AlertInDB) func(tx *sql.Tx) error {
	return func(tx *sql.Tx) error {
		if len(alerts) > 0 {
			_, err := tx.Exec(DELETEALERT, LabelSet(alert.Labels).Fingerprint(), getEndTime(end), start)
			if err != nil {
				return fmt.Errorf("failed to delete Alerts: %s", err)
			}
		}

		result, err := tx.Exec(INSERTALERT, alert.Status, start, replaceEmptyTime(end), alert.GeneratorURL, LabelSet(alert.Labels).Fingerprint())
		if err != nil {
			return fmt.Errorf("failed to insert into Alert: %s", err)
		}

		alertID, err := result.LastInsertId()
		if err != nil {
			return fmt.Errorf("failed to get Alert inserted id: %s", err)
		}

		for k, v := range alert.Labels {
			_, err := tx.Exec(INSERTLABEL, alertID, k, v)
			if err != nil {
				return fmt.Errorf("failed to insert into AlertLabel: %s", err)
			}
		}
		for k, v := range alert.Annotations {
			_, err := tx.Exec(INSERTANNOTATION, alertID, k, v)
			if err != nil {
				return fmt.Errorf("failed to insert into AlertAnnotation: %s", err)
			}
		}

		return nil
	}
}

func replaceEmptyTime(end time.Time) *time.Time {
	if end.IsZero() {
		return nil
	}

	return &end
}

func getTime(timeStr string) (time.Time, error) {
	return time.Parse(time.RFC3339, timeStr)
}

func convert(alert apiserver.PrometheusAlert) (time.Time, time.Time, error) {
	start, err := getTime(alert.StartsAt)
	if err != nil {
		return time.Time{}, time.Time{}, fmt.Errorf("convert end time faild:, %s", alert.EndsAt)
	}

	end, err := getTime(alert.EndsAt)
	if err != nil {
		return time.Time{}, time.Time{}, fmt.Errorf("convert end time faild:, %s", alert.EndsAt)
	}

	return start, end, nil
}

func unitOfWork(f func(tx *sql.Tx) error, db *sql.DB) error {
	tx, err := db.Begin()
	if err != nil {
		return fmt.Errorf("failed to begin transaction: %s", err)
	}

	err = f(tx)

	if err != nil {
		e := tx.Rollback()
		if e != nil {
			return fmt.Errorf("failed to rollback transaction (%s) after failing execution: %s", e, err)
		}
		return fmt.Errorf("failed execution: %s", err)
	}

	err = tx.Commit()
	if err != nil {
		return fmt.Errorf("failed to commit transaction: %s", err)
	}

	return nil
}

func DeleteExpireAlerts(timestampOfExpired int64, db *sql.DB) {
	clause := fmt.Sprintf(" where ends_at is not null and ends_at < from_unixtime(%d)", timestampOfExpired)
	deleteAlerts(clause, db)
}

func DeleteClusterAlerts(cluster string, db *sql.DB) {
	clause := fmt.Sprintf(" where id in (select distinct(alert_id) from alert_labels where label = '%s' and value='%s')", INSTANCELABEL, cluster)
	deleteAlerts(clause, db)
}

func deleteAlerts(caluse string, db *sql.DB) {
	sql := "delete from alerts "
	if caluse != "" {
		sql = fmt.Sprintf("%s %s", sql, caluse)
	}
	r, err := db.Exec(sql)
	if err != nil {
		log.Error("delet alerts failed", zap.Error(err))
		return
	}

	count, err := r.RowsAffected()
	if err != nil {
		log.Error("delete alerts failed", zap.Error(err))
		return
	}

	log.Info("delete alerts", zap.Int64("count", count))
}
