package clusterrecovery

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"sort"
	"time"

	"gorm.io/gorm"

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

type Snapshot struct {
	Organization *db.Organization
	Cluster      *db.Cluster
}

type ReadStore interface {
	ReadSnapshot(context.Context, ObjectKind, uint64, string) (*Snapshot, error)
}

type GormStore struct {
	db *gorm.DB
}

func NewGormStore(gormDB *gorm.DB) *GormStore {
	return &GormStore{db: gormDB}
}

func (s *GormStore) ReadSnapshot(ctx context.Context, kind ObjectKind, tenantID uint64, clusterID string) (*Snapshot, error) {
	if s == nil || s.db == nil {
		return nil, errors.New("gorm database is required")
	}
	if tenantID == 0 && kind != ObjectKindResourcePool {
		return nil, errors.New("tenant ID may be zero only for a resource pool")
	}
	if kind != ObjectKindMgmtCluster && kind != ObjectKindResourcePool {
		return nil, ErrSourceUnsupported
	}
	if _, err := parsePositiveID(clusterID); err != nil {
		return nil, fmt.Errorf("cluster ID: %w", err)
	}
	snapshot := &Snapshot{}
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var organizations []db.Organization
		if err := tx.Unscoped().
			Where("dbaas_org_id = ? AND org_type = ? AND deleted_at IS NULL", tenantID, string(view.CloudDataSource)).
			Limit(2).
			Find(&organizations).Error; err != nil {
			return err
		}
		if len(organizations) > 1 {
			return errors.New("multiple active Clinic organizations match the tenant")
		}
		if len(organizations) == 1 {
			organization := organizations[0]
			snapshot.Organization = &organization
		}

		var clusters []db.Cluster
		if err := tx.Unscoped().
			Where("cluster_id = ?", clusterID).
			Limit(2).
			Find(&clusters).Error; err != nil {
			return err
		}
		if len(clusters) > 1 {
			return errors.New("multiple Clinic cluster rows match cluster_id")
		}
		if len(clusters) == 1 {
			cluster := clusters[0]
			snapshot.Cluster = &cluster
		}
		return nil
	}, &sql.TxOptions{ReadOnly: true})
	if err != nil {
		return nil, fmt.Errorf("read Clinic recovery snapshot: %w", err)
	}
	return snapshot, nil
}

var mgmtOwnedFields = []string{
	"cluster_component",
	"control_plane_arch",
	"creation_finished_at",
	"data_plane_name",
	"deleted_at",
	"deploy_type",
	"deploy_type_v2",
	"end_time",
	"name",
	"parent_id",
	"project_id",
	"provider",
	"region",
	"shoot_name",
	"start_time",
	"status",
	"version",
}

var resourcePoolOwnedFields = []string{
	"data_plane_name",
	"deleted_at",
	"deploy_type",
	"deploy_type_v2",
	"end_time",
	"name",
	"provider",
	"region",
	"shoot_name",
	"start_time",
	"status",
	"version",
}

func ownedFields(kind ObjectKind) ([]string, error) {
	var fields []string
	switch kind {
	case ObjectKindMgmtCluster:
		fields = mgmtOwnedFields
	case ObjectKindResourcePool:
		fields = resourcePoolOwnedFields
	default:
		return nil, ErrSourceUnsupported
	}
	return append([]string(nil), fields...), nil
}

func diffOwnedFields(kind ObjectKind, before, after *db.Cluster) ([]FieldChange, error) {
	if before == nil || after == nil {
		return nil, errors.New("before and after clusters are required")
	}
	fields, err := ownedFields(kind)
	if err != nil {
		return nil, err
	}
	changes := make([]FieldChange, 0)
	for _, field := range fields {
		beforeValue, err := clusterFieldValue(before, field)
		if err != nil {
			return nil, err
		}
		afterValue, err := clusterFieldValue(after, field)
		if err != nil {
			return nil, err
		}
		if reflect.DeepEqual(beforeValue, afterValue) {
			continue
		}
		beforeHash, err := checksumJSON(beforeValue)
		if err != nil {
			return nil, err
		}
		afterHash, err := checksumJSON(afterValue)
		if err != nil {
			return nil, err
		}
		changes = append(changes, FieldChange{Field: field, BeforeHash: beforeHash, AfterHash: afterHash})
	}
	return changes, nil
}

func clusterFieldValue(cluster *db.Cluster, field string) (any, error) {
	switch field {
	case "cluster_component":
		return cluster.ClusterComponent, nil
	case "control_plane_arch":
		return cluster.ControlPlaneArch, nil
	case "creation_finished_at":
		return canonicalTimePointer(cluster.CreationFinishedAt), nil
	case "data_plane_name":
		return cluster.DataPlaneName, nil
	case "deleted_at":
		return canonicalDeletedAt(cluster.DeletedAt), nil
	case "deploy_type":
		return cluster.DeployType, nil
	case "deploy_type_v2":
		return cluster.DeployTypeV2, nil
	case "end_time":
		return cluster.EndTime, nil
	case "name":
		return cluster.Name, nil
	case "parent_id":
		return cluster.ParentID, nil
	case "project_id":
		return cluster.ProjectID, nil
	case "provider":
		return cluster.Provider, nil
	case "region":
		return cluster.Region, nil
	case "shoot_name":
		return cluster.ShootName, nil
	case "start_time":
		return cluster.StartTime, nil
	case "status":
		return cluster.Status, nil
	case "version":
		return cluster.Version, nil
	default:
		return nil, fmt.Errorf("unsupported owned cluster field %q", field)
	}
}

type deletionFingerprint struct {
	Valid bool      `json:"valid"`
	Time  time.Time `json:"time,omitempty"`
}

func canonicalDeletedAt(value gorm.DeletedAt) deletionFingerprint {
	deletedAt := time.Time{}
	if value.Valid {
		deletedAt = value.Time.UTC()
	}
	return deletionFingerprint{Valid: value.Valid, Time: deletedAt}
}

func hasTerminalMarker(cluster *db.Cluster) bool {
	return cluster != nil && (cluster.DeletedAt.Valid || cluster.EndTime != 0 || terminalStatus(cluster.Status))
}

func validateDesiredLifecycle(cluster *db.Cluster) error {
	if cluster == nil {
		return errors.New("desired cluster is required")
	}
	statusTerminal := terminalStatus(cluster.Status)
	if !statusTerminal {
		if cluster.DeletedAt.Valid || cluster.EndTime != 0 {
			return errors.New("active desired cluster contains terminal lifecycle fields")
		}
		return nil
	}
	if !cluster.DeletedAt.Valid || cluster.EndTime == 0 {
		return errors.New("terminal desired cluster lacks deletion metadata")
	}
	if cluster.DeletedAt.Time.UTC().Unix() != int64(cluster.EndTime) {
		return errors.New("terminal desired cluster deletion timestamps differ")
	}
	return nil
}

func clusterRecordChecksum(cluster *db.Cluster) (string, error) {
	if cluster == nil {
		return "", errors.New("cluster is required")
	}
	type recordFingerprint struct {
		ID                 uint                 `json:"id"`
		CreatedAt          time.Time            `json:"created_at"`
		UpdatedAt          time.Time            `json:"updated_at"`
		DeletedAt          deletionFingerprint  `json:"deleted_at"`
		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"`
		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"`
	}
	fingerprint := recordFingerprint{
		ID:                 cluster.ID,
		CreatedAt:          cluster.CreatedAt.UTC(),
		UpdatedAt:          cluster.UpdatedAt.UTC(),
		DeletedAt:          canonicalDeletedAt(cluster.DeletedAt),
		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:           cluster.Branches,
		ControlPlaneArch:   cluster.ControlPlaneArch,
		ClusterComponent:   cluster.ClusterComponent,
		ParentID:           cluster.ParentID,
	}
	return checksumJSON(fingerprint)
}

func desiredClusterChecksum(kind ObjectKind, cluster *db.Cluster) (string, error) {
	if cluster == nil {
		return "", errors.New("desired cluster is required")
	}
	fields, err := ownedFields(kind)
	if err != nil {
		return "", err
	}
	values := make(map[string]any, len(fields))
	for _, field := range fields {
		value, err := clusterFieldValue(cluster, field)
		if err != nil {
			return "", err
		}
		values[field] = value
	}
	payload := struct {
		Kind        ObjectKind     `json:"kind"`
		ClusterID   string         `json:"cluster_id"`
		TenantID    uint64         `json:"tenant_id"`
		OrgID       uint           `json:"org_id"`
		ClusterType string         `json:"cluster_type"`
		CreatedAt   time.Time      `json:"created_at"`
		Fields      map[string]any `json:"fields"`
	}{
		Kind:        kind,
		ClusterID:   cluster.ClusterID,
		TenantID:    cluster.TenantID,
		OrgID:       cluster.OrgID,
		ClusterType: cluster.ClusterType,
		CreatedAt:   cluster.CreatedAt.UTC(),
		Fields:      values,
	}
	return checksumJSON(payload)
}

func checksumJSON(value any) (string, error) {
	payload, err := json.Marshal(value)
	if err != nil {
		return "", fmt.Errorf("marshal checksum payload: %w", err)
	}
	sum := sha256.Sum256(payload)
	return hex.EncodeToString(sum[:]), nil
}

func canonicalTimePointer(value *time.Time) *time.Time {
	if value == nil {
		return nil
	}
	canonical := value.UTC()
	return &canonical
}

func changeFieldNames(changes []FieldChange) []string {
	fields := make([]string, 0, len(changes))
	for _, change := range changes {
		fields = append(fields, change.Field)
	}
	sort.Strings(fields)
	return fields
}
