package clusterrecovery

import (
	"errors"
	"time"

	"github.com/pingcap-inc/clinic/pkg/db"
)

type RollbackOperation string

const (
	RollbackDeleteCreated RollbackOperation = "delete_created"
	RollbackRestoreUpdate RollbackOperation = "restore_update"
)

type PersistentCluster struct {
	ID                 uint                 `json:"id"`
	CreatedAt          time.Time            `json:"created_at"`
	UpdatedAt          time.Time            `json:"updated_at"`
	DeletedAt          time.Time            `json:"deleted_at,omitempty"`
	DeletedAtValid     bool                 `json:"deleted_at_valid"`
	ClusterID          string               `json:"cluster_id"`
	Version            string               `json:"version"`
	Name               string               `json:"name"`
	OrgID              uint                 `json:"org_id"`
	Desc               string               `json:"desc"`
	StartTime          uint64               `json:"start_time"`
	EndTime            uint64               `json:"end_time"`
	Topology           *db.Topology         `json:"topology"`
	ClusterType        string               `json:"cluster_type"`
	Status             string               `json:"status"`
	DeployType         string               `json:"deploy_type"`
	DeployTypeV2       string               `json:"deploy_type_v2"`
	TenantID           uint64               `json:"tenant_id,string"`
	ShootName          string               `json:"shoot_name"`
	DataPlaneName      string               `json:"data_plane_name"`
	K8sNamespace       string               `json:"k8s_namespace"`
	NetworkName        string               `json:"network_name"`
	Region             string               `json:"region"`
	Provider           string               `json:"provider"`
	ProjectID          string               `json:"project_id"`
	CreationFinishedAt *time.Time           `json:"creation_finished_at"`
	Branches           db.StringSlice       `json:"branches"`
	ControlPlaneArch   string               `json:"control_plane_arch"`
	ClusterComponent   db.ClusterComponents `json:"cluster_component"`
	ParentID           string               `json:"parent_id"`
}

type RollbackEntry struct {
	ActionIndex             int                `json:"action_index"`
	Operation               RollbackOperation  `json:"operation"`
	Kind                    ObjectKind         `json:"kind"`
	ClusterID               string             `json:"cluster_id"`
	BeforeChecksum          string             `json:"before_checksum,omitempty"`
	ExpectedDesiredChecksum string             `json:"expected_desired_checksum"`
	Before                  *PersistentCluster `json:"before,omitempty"`
}

type RollbackBundle struct {
	SchemaVersion string          `json:"schema_version"`
	Environment   string          `json:"environment"`
	RunID         string          `json:"run_id"`
	ManifestHash  string          `json:"manifest_hash"`
	MutationClass MutationClass   `json:"mutation_class"`
	GeneratedAt   time.Time       `json:"generated_at"`
	Entries       []RollbackEntry `json:"entries"`
	BundleHash    string          `json:"bundle_hash"`
}

type RollbackSink interface {
	WriteRollbackBundle(*RollbackBundle) error
}

type FileRollbackSink struct {
	Path string
}

func (s FileRollbackSink) WriteRollbackBundle(bundle *RollbackBundle) error {
	return WriteJSONArtifact(s.Path, bundle)
}

func FinalizeRollbackBundle(bundle *RollbackBundle) error {
	if bundle == nil {
		return errors.New("rollback bundle is required")
	}
	bundle.GeneratedAt = bundle.GeneratedAt.UTC()
	bundle.BundleHash = ""
	if bundle.SchemaVersion != SchemaVersion || bundle.Environment != StagingEnvironment ||
		!runIDPattern.MatchString(bundle.RunID) || !isSHA256(bundle.ManifestHash) ||
		(bundle.MutationClass != MutationCreate && bundle.MutationClass != MutationUpdate) || bundle.GeneratedAt.IsZero() {
		return errors.New("rollback bundle metadata is invalid")
	}
	for _, entry := range bundle.Entries {
		if _, err := parsePositiveID(entry.ClusterID); err != nil || !isSHA256(entry.ExpectedDesiredChecksum) {
			return errors.New("rollback bundle entry is invalid")
		}
		if entry.Operation == RollbackRestoreUpdate {
			if entry.Before == nil || !isSHA256(entry.BeforeChecksum) {
				return errors.New("update rollback entry lacks a valid preimage")
			}
		} else if entry.Operation != RollbackDeleteCreated || entry.Before != nil {
			return errors.New("rollback operation is invalid")
		}
	}
	hash, err := checksumJSON(bundle)
	if err != nil {
		return err
	}
	bundle.BundleHash = hash
	return nil
}

func persistentClusterFrom(cluster *db.Cluster) *PersistentCluster {
	if cluster == nil {
		return nil
	}
	deletedAt := time.Time{}
	if cluster.DeletedAt.Valid {
		deletedAt = cluster.DeletedAt.Time.UTC()
	}
	return &PersistentCluster{
		ID:                 cluster.ID,
		CreatedAt:          cluster.CreatedAt.UTC(),
		UpdatedAt:          cluster.UpdatedAt.UTC(),
		DeletedAt:          deletedAt,
		DeletedAtValid:     cluster.DeletedAt.Valid,
		ClusterID:          cluster.ClusterID,
		Version:            cluster.Version,
		Name:               cluster.Name,
		OrgID:              cluster.OrgID,
		Desc:               cluster.Desc,
		StartTime:          cluster.StartTime,
		EndTime:            cluster.EndTime,
		Topology:           cluster.Topology,
		ClusterType:        cluster.ClusterType,
		Status:             cluster.Status,
		DeployType:         cluster.DeployType,
		DeployTypeV2:       cluster.DeployTypeV2,
		TenantID:           cluster.TenantID,
		ShootName:          cluster.ShootName,
		DataPlaneName:      cluster.DataPlaneName,
		K8sNamespace:       cluster.K8sNamespace,
		NetworkName:        cluster.NetworkName,
		Region:             cluster.Region,
		Provider:           cluster.Provider,
		ProjectID:          cluster.ProjectID,
		CreationFinishedAt: canonicalTimePointer(cluster.CreationFinishedAt),
		Branches:           append(db.StringSlice(nil), cluster.Branches...),
		ControlPlaneArch:   cluster.ControlPlaneArch,
		ClusterComponent:   cluster.ClusterComponent,
		ParentID:           cluster.ParentID,
	}
}
