package tepserver

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/pingcap/log"
	"github.com/pingcap/tep/pkg/apis/apiserver/v1alpha1"
	"github.com/pingcap/tep/pkg/utils"
	"github.com/pingcap/tep/pkg/utils/cronutil"
	"go.uber.org/zap"
)

// CreateScheduledBackup create a new scheduled backup task
func (s *Server) CreateScheduledBackup(namespace string,
	backup *v1alpha1.TiDBClusterScheduledBackup) (*v1alpha1.TiDBClusterScheduledBackup, utils.APIStatus) {

	if namespace == "" {
		return nil, utils.NewAPIStatus(http.StatusBadRequest, "namespace can not be empty")
	}

	// convert schedule from readable format to crontab format before send to apiserver
	tmpArray := strings.Split(backup.Schedule, " ")
	if len(tmpArray) != 2 {
		return nil, utils.NewAPIStatus(http.StatusBadRequest, "schedule format incorrect")
	}
	cronSchedule, err := cronutil.ConvertScheduleFormat(tmpArray[1], strings.Split(tmpArray[0], ","), "0") // UTC for all
	if err != nil {
		log.Error("error converting schedule format", zap.String("schedule", backup.Schedule),
			zap.Error(err))
		return nil, utils.NewAPIStatus(http.StatusInternalServerError,
			fmt.Sprintf("unable to convert schedule format '%s'", backup.Schedule))
	}
	backup.Schedule = cronSchedule

	backup.StorageClassName = s.BackupStorageClass
	backup.StorageSize = s.BackupStorageSize
	// temporally defaut to use s3 type
	// TODO: add GCS storagetype
	backup.StorageType = "s3"
	backup.S3 = &v1alpha1.S3StorageProvider{
		Endpoint:     s.Endpoint,
		AccessKey:    s.AccessKey,
		SecretKey:    s.SecretKey,
		Region:       s.Region,
		Provider:     s.Provider,
		ACL:          s.ACL,
		StorageClass: s.RemoteStorageClass,
		Bucket:       namespace,
	}

	// build and send request to apiserver
	c := s.newAPIServerClient(fmt.Sprintf("/namespaces/%s/tidbcluster-schedule-backup", namespace))
	backupBytes, err := json.Marshal(backup)
	if err != nil {
		return nil, utils.NewAPIStatus(http.StatusInternalServerError,
			fmt.Sprintf("unable to encode request %v", backup))
	}
	resp, err := c.Post(bytes.NewBuffer(backupBytes))
	if err != nil {
		return nil, utils.NewAPIStatus(utils.ParseHTTPError(err),
			fmt.Sprintf("error creating tidbcluster scheduled backup, %v", err.Error()))
	}
	defer resp.Body.Close()

	backupResp, err := parseScheduledBackupResponse(resp)
	if err != nil {
		return nil, utils.NewAPIStatus(http.StatusInternalServerError, err.Error())
	}

	return backupResp, utils.NewAPIStatus(resp.StatusCode, resp.Status)
}

// CreateRestore create a new restore task
func (s *Server) CreateRestore(namespace string, restore *v1alpha1.TiDBClusterRestore) (*v1alpha1.TiDBClusterRestore, utils.APIStatus) {

	if namespace == "" {
		return nil, utils.NewAPIStatus(http.StatusBadRequest, "namespace can not be empty")
	}

	restore.StorageClassName = s.BackupStorageClass
	restore.StorageSize = s.BackupStorageSize

	// build and send request to apiserver
	c := s.newAPIServerClient(fmt.Sprintf("/namespaces/%s/tidbcluster-restore", namespace))
	restoreBytes, err := json.Marshal(restore)
	if err != nil {
		return nil, utils.NewAPIStatus(http.StatusInternalServerError,
			fmt.Sprintf("unable to encode request %v", restore))
	}
	resp, err := c.Post(bytes.NewBuffer(restoreBytes))
	if err != nil {
		return nil, utils.NewAPIStatus(utils.ParseHTTPError(err),
			fmt.Sprintf("error creating tidbcluster restore, %v", err.Error()))
	}
	defer resp.Body.Close()

	restoreResp, err := parseRestoreResponse(resp)
	if err != nil {
		return nil, utils.NewAPIStatus(http.StatusInternalServerError, err.Error())
	}

	return restoreResp, utils.NewAPIStatus(resp.StatusCode, resp.Status)
}

// CreateBackup create a new backup task
func (s *Server) CreateBackup(namespace string, backup *v1alpha1.TiDBClusterBackup) (*v1alpha1.TiDBClusterBackup, utils.APIStatus) {

	if namespace == "" {
		return nil, utils.NewAPIStatus(http.StatusBadRequest, "namespace can not be empty")
	}

	backup.StorageClassName = s.BackupStorageClass
	backup.StorageSize = s.BackupStorageSize
	// temporally defaut to use s3 type
	// TODO: add GCS storagetype
	backup.StorageType = "s3"
	backup.S3 = &v1alpha1.S3StorageProvider{
		Endpoint:     s.Endpoint,
		AccessKey:    s.AccessKey,
		SecretKey:    s.SecretKey,
		Region:       s.Region,
		Provider:     s.Provider,
		ACL:          s.ACL,
		StorageClass: s.RemoteStorageClass,
		Bucket:       namespace,
	}

	// build and send request to apiserver
	c := s.newAPIServerClient(fmt.Sprintf("/namespaces/%s/tidbcluster-backup", namespace))
	backupBytes, err := json.Marshal(backup)
	if err != nil {
		return nil, utils.NewAPIStatus(http.StatusInternalServerError,
			fmt.Sprintf("unable to encode request %v", backup))
	}
	resp, err := c.Post(bytes.NewBuffer(backupBytes))
	if err != nil {
		return nil, utils.NewAPIStatus(utils.ParseHTTPError(err),
			fmt.Sprintf("error creating tidbcluster backup, %v", err.Error()))
	}
	defer resp.Body.Close()

	backupResp, err := parseBackupResponse(resp)
	if err != nil {
		return nil, utils.NewAPIStatus(http.StatusInternalServerError, err.Error())
	}

	return backupResp, utils.NewAPIStatus(resp.StatusCode, resp.Status)
}

// UpdateScheduledBackup update the exist scheduled backup task for a namespace
func (s *Server) UpdateScheduledBackup(namespace string,
	backup *v1alpha1.TiDBClusterScheduledBackup) (*v1alpha1.TiDBClusterScheduledBackup, utils.APIStatus) {

	if namespace == "" {
		return nil, utils.NewAPIStatus(http.StatusBadRequest, "namespace can not be empty")
	}

	// convert schedule from readable format to crontab format before send to apiserver
	tmpArray := strings.Split(backup.Schedule, " ")
	if len(tmpArray) != 2 {
		return nil, utils.NewAPIStatus(http.StatusBadRequest, "schedule format incorrect")
	}
	cronSchedule, err := cronutil.ConvertScheduleFormat(tmpArray[1], strings.Split(tmpArray[0], ","), "0") // UTC for all
	if err != nil {
		log.Error("error converting schedule format", zap.String("schedule", backup.Schedule),
			zap.Error(err))
		return nil, utils.NewAPIStatus(http.StatusInternalServerError,
			fmt.Sprintf("unable to convert schedule format '%s'", backup.Schedule))
	}
	backup.Schedule = cronSchedule

	backup.StorageClassName = s.BackupStorageClass
	backup.StorageSize = s.BackupStorageSize
	// temporally defaut to use s3 type
	// TODO: add GCS storagetype
	backup.StorageType = "s3"
	backup.S3 = &v1alpha1.S3StorageProvider{
		Endpoint:     s.Endpoint,
		AccessKey:    s.AccessKey,
		SecretKey:    s.SecretKey,
		Region:       s.Region,
		Provider:     s.Provider,
		ACL:          s.ACL,
		StorageClass: s.RemoteStorageClass,
		Bucket:       namespace,
	}
	// add metadata
	backup.ObjectMeta.Name = "tidb-scheduled-backup-manager"

	// build and send request to apiserver
	c := s.newAPIServerClient(fmt.Sprintf("/namespaces/%s/tidbcluster-schedule-backup/tidb-scheduled-backup-manager", namespace))
	backupBytes, err := json.Marshal(backup)
	if err != nil {
		return nil, utils.NewAPIStatus(http.StatusInternalServerError,
			fmt.Sprintf("unable to encode request %v", backup))
	}
	resp, err := c.Put(bytes.NewBuffer(backupBytes))
	if err != nil {
		return nil, utils.NewAPIStatus(utils.ParseHTTPError(err),
			fmt.Sprintf("error creating tidbcluster scheduled backup, %v", err.Error()))
	}
	defer resp.Body.Close()

	backupResp, err := parseScheduledBackupResponse(resp)
	if err != nil {
		return nil, utils.NewAPIStatus(http.StatusInternalServerError, err.Error())
	}

	return backupResp, utils.NewAPIStatus(resp.StatusCode, resp.Status)
}

func parseScheduledBackupResponse(resp *http.Response) (*v1alpha1.TiDBClusterScheduledBackup, error) {
	var backupResp v1alpha1.TiDBClusterScheduledBackup
	backupBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("unable to read response %v", resp)
	}
	err = json.Unmarshal(backupBytes, &backupResp)
	if err != nil {
		log.Error("unable to decode response", zap.Any("data", resp))
		return nil, fmt.Errorf("unable to decode response %v", resp)
	}

	// convert schedule format in response as well
	stime, date, err := cronutil.ParseSchedule(backupResp.Schedule, "0") // UTC for all
	if err != nil {
		log.Error("error converting schedule format", zap.String("schedule", backupResp.Schedule),
			zap.Error(err))
		return nil, fmt.Errorf("unable to convert schedule format '%s'", backupResp.Schedule)
	}
	backupResp.Schedule = fmt.Sprintf("%s %s", strings.Join(date, ","), stime)
	return &backupResp, nil
}

func parseBackupResponse(resp *http.Response) (*v1alpha1.TiDBClusterBackup, error) {
	var backupResp v1alpha1.TiDBClusterBackup
	backupBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("unable to read response %v", resp)
	}
	err = json.Unmarshal(backupBytes, &backupResp)
	if err != nil {
		log.Error("unable to decode response", zap.Any("data", resp))
		return nil, fmt.Errorf("unable to decode response %v", resp)
	}

	return &backupResp, nil
}

func parseRestoreResponse(resp *http.Response) (*v1alpha1.TiDBClusterRestore, error) {
	var restoreResp v1alpha1.TiDBClusterRestore
	restoreBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("unable to read response %v", resp)
	}
	err = json.Unmarshal(restoreBytes, &restoreResp)
	if err != nil {
		log.Error("unable to decode response", zap.Any("data", resp))
		return nil, fmt.Errorf("unable to decode response %v", resp)
	}

	return &restoreResp, nil
}

// GetScheduledBackup query the scheduled backup task for a namespace
func (s *Server) GetScheduledBackup(namespace string) (*v1alpha1.TiDBClusterScheduledBackup, utils.APIStatus) {
	if namespace == "" {
		return nil, utils.NewAPIStatus(http.StatusBadRequest, "namespace can not be empty")
	}

	resp, status := s.APIServerGetURI(fmt.Sprintf("/namespaces/%s/tidbcluster-schedule-backup/tidb-scheduled-backup-manager", namespace))

	var backup v1alpha1.TiDBClusterScheduledBackup
	err := json.Unmarshal(resp, &backup)
	if err != nil {
		return nil, utils.NewAPIStatus(http.StatusInternalServerError,
			fmt.Sprintf("unable to decode response %v to map", resp))
	}

	stime, date, err := cronutil.ParseSchedule(backup.Schedule, "0") // UTC for all
	if err != nil {
		log.Error("error converting schedule format", zap.String("schedule", backup.Schedule),
			zap.Error(err))
		return nil, utils.NewAPIStatus(http.StatusInternalServerError,
			fmt.Sprintf("unable to convert schedule format '%s'", backup.Schedule))
	}
	backup.Schedule = fmt.Sprintf("%s %s", strings.Join(date, ","), stime)

	return &backup, status
}
