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"
)

// CreateMetadataScheduledBackup create a new scheduled backup task
func (s *Server) CreateMetadataScheduledBackup(backup *v1alpha1.MetadataScheduledBackup) (*v1alpha1.MetadataScheduledBackup, utils.APIStatus) {

	// 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,
	}

	// build and send request to apiserver
	c := s.newAPIServerClient("/metadata-schedule-backup")
	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 metadata scheduled backup, %v", err.Error()))
	}
	defer resp.Body.Close()

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

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

// CreateMetadataBackup create a new backup task
func (s *Server) CreateMetadataBackup(backup *v1alpha1.MetadataBackup) (*v1alpha1.MetadataBackup, utils.APIStatus) {

	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,
	}

	// build and send request to apiserver
	c := s.newAPIServerClient("/metadata-backup")
	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 metadata backup, %v", err.Error()))
	}
	defer resp.Body.Close()

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

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

// UpdateMetadataScheduledBackup update the exist metadata scheduled backup task
func (s *Server) UpdateMetadataScheduledBackup(backup *v1alpha1.MetadataScheduledBackup) (*v1alpha1.MetadataScheduledBackup, utils.APIStatus) {
	// 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,
	}
	// add metadata
	backup.ObjectMeta.Name = "metadata-scheduled-backup-manager"

	// build and send request to apiserver
	c := s.newAPIServerClient("/metadata-schedule-backup/metadata-scheduled-backup-manager")
	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 metadata scheduled backup, %v", err.Error()))
	}
	defer resp.Body.Close()

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

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

func parseMetadataScheduledBackupResponse(resp *http.Response) (*v1alpha1.MetadataScheduledBackup, error) {
	var backupResp v1alpha1.MetadataScheduledBackup
	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 parseMetadataBackupResponse(resp *http.Response) (*v1alpha1.MetadataBackup, error) {
	var backupResp v1alpha1.MetadataBackup
	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
}

// GetMetadataScheduledBackup query the metadata scheduled backup task
func (s *Server) GetMetadataScheduledBackup() (*v1alpha1.MetadataScheduledBackup, utils.APIStatus) {
	resp, status := s.APIServerGetURI("/metadata-schedule-backup/metadata-scheduled-backup-manager")

	var backup v1alpha1.MetadataScheduledBackup
	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
}
